McRogueFace/tests/cookbook/lib/grid_container.py
John McCardle 55f6ea9502 Add cookbook examples with updated callback signatures for #229, #230
Cookbook structure:
- lib/: Reusable component library (Button, StatBar, AnimationChain, etc.)
- primitives/: Demo apps for individual components
- features/: Demo apps for complex features (animation chaining, shaders)
- apps/: Complete mini-applications (calculator, dialogue system)
- automation/: Screenshot capture utilities

API signature updates applied:
- on_enter/on_exit/on_move callbacks now only receive (pos) per #230
- on_cell_enter/on_cell_exit callbacks only receive (cell_pos) per #230
- Animation chain library uses Timer-based sequencing (unaffected by #229)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 18:58:25 -05:00

344 lines
10 KiB
Python

# McRogueFace Cookbook - Grid Container Widget
"""
NxM clickable cells for inventory/slot systems.
Example:
from lib.grid_container import GridContainer
def on_cell_click(x, y, item):
print(f"Clicked cell ({x}, {y}): {item}")
inventory = GridContainer(
pos=(100, 100),
cell_size=(48, 48),
grid_dims=(5, 4), # 5 columns, 4 rows
on_cell_click=on_cell_click
)
scene.children.append(inventory.frame)
# Set cell contents
inventory.set_cell(0, 0, sprite_index=10, count=5)
"""
import mcrfpy
class GridContainer:
"""NxM clickable cells for inventory/slot systems.
Args:
pos: (x, y) position tuple
cell_size: (width, height) of each cell
grid_dims: (columns, rows) grid dimensions
on_cell_click: Callback(x, y, item_data) when cell is clicked
on_cell_hover: Callback(x, y, item_data) when cell is hovered
texture: Optional texture for sprites
empty_color: Background color for empty cells
filled_color: Background color for cells with items
selected_color: Background color for selected cell
hover_color: Background color when hovered
cell_outline: Cell border thickness
cell_spacing: Space between cells
Attributes:
frame: The outer frame (add this to scene)
selected: (x, y) of selected cell or None
"""
DEFAULT_EMPTY = mcrfpy.Color(40, 40, 45)
DEFAULT_FILLED = mcrfpy.Color(50, 50, 60)
DEFAULT_SELECTED = mcrfpy.Color(80, 100, 140)
DEFAULT_HOVER = mcrfpy.Color(60, 60, 75)
DEFAULT_OUTLINE = mcrfpy.Color(70, 70, 80)
def __init__(self, pos, cell_size, grid_dims, on_cell_click=None,
on_cell_hover=None, texture=None,
empty_color=None, filled_color=None,
selected_color=None, hover_color=None,
cell_outline=1, cell_spacing=2):
self.pos = pos
self.cell_size = cell_size
self.grid_dims = grid_dims # (cols, rows)
self.on_cell_click = on_cell_click
self.on_cell_hover = on_cell_hover
self.texture = texture
self.cell_outline = cell_outline
self.cell_spacing = cell_spacing
# Colors
self.empty_color = empty_color or self.DEFAULT_EMPTY
self.filled_color = filled_color or self.DEFAULT_FILLED
self.selected_color = selected_color or self.DEFAULT_SELECTED
self.hover_color = hover_color or self.DEFAULT_HOVER
self.outline_color = self.DEFAULT_OUTLINE
# State
self._selected = None
self._hovered = None
self._cells = {} # (x, y) -> cell data
self._cell_frames = {} # (x, y) -> frame
# Calculate total size
cols, rows = grid_dims
total_width = cols * cell_size[0] + (cols - 1) * cell_spacing
total_height = rows * cell_size[1] + (rows - 1) * cell_spacing
# Create outer frame
self.frame = mcrfpy.Frame(
pos=pos,
size=(total_width, total_height),
fill_color=mcrfpy.Color(0, 0, 0, 0), # Transparent
outline=0
)
# Create cell frames
self._create_cells()
def _create_cells(self):
"""Create all cell frames."""
cols, rows = self.grid_dims
cw, ch = self.cell_size
for row in range(rows):
for col in range(cols):
x = col * (cw + self.cell_spacing)
y = row * (ch + self.cell_spacing)
cell_frame = mcrfpy.Frame(
pos=(x, y),
size=self.cell_size,
fill_color=self.empty_color,
outline_color=self.outline_color,
outline=self.cell_outline
)
# Set up event handlers
def make_click(cx, cy):
def handler(pos, button, action):
if button == "left" and action == "end":
self._on_cell_clicked(cx, cy)
return handler
def make_enter(cx, cy):
def handler(pos, button, action):
self._on_cell_enter(cx, cy)
return handler
def make_exit(cx, cy):
def handler(pos, button, action):
self._on_cell_exit(cx, cy)
return handler
cell_frame.on_click = make_click(col, row)
cell_frame.on_enter = make_enter(col, row)
cell_frame.on_exit = make_exit(col, row)
self._cell_frames[(col, row)] = cell_frame
self.frame.children.append(cell_frame)
def _on_cell_clicked(self, x, y):
"""Handle cell click."""
old_selected = self._selected
self._selected = (x, y)
# Update display
if old_selected:
self._update_cell_display(*old_selected)
self._update_cell_display(x, y)
# Fire callback
if self.on_cell_click:
item = self._cells.get((x, y))
self.on_cell_click(x, y, item)
def _on_cell_enter(self, x, y):
"""Handle cell hover enter."""
self._hovered = (x, y)
self._update_cell_display(x, y)
if self.on_cell_hover:
item = self._cells.get((x, y))
self.on_cell_hover(x, y, item)
def _on_cell_exit(self, x, y):
"""Handle cell hover exit."""
if self._hovered == (x, y):
self._hovered = None
self._update_cell_display(x, y)
def _update_cell_display(self, x, y):
"""Update visual state of a cell."""
if (x, y) not in self._cell_frames:
return
frame = self._cell_frames[(x, y)]
has_item = (x, y) in self._cells
if (x, y) == self._selected:
frame.fill_color = self.selected_color
elif (x, y) == self._hovered:
frame.fill_color = self.hover_color
elif has_item:
frame.fill_color = self.filled_color
else:
frame.fill_color = self.empty_color
@property
def selected(self):
"""Currently selected cell (x, y) or None."""
return self._selected
@selected.setter
def selected(self, value):
"""Set selected cell."""
old = self._selected
self._selected = value
if old:
self._update_cell_display(*old)
if value:
self._update_cell_display(*value)
def get_selected_item(self):
"""Get the item in the selected cell."""
if self._selected:
return self._cells.get(self._selected)
return None
def set_cell(self, x, y, sprite_index=None, count=None, data=None):
"""Set cell contents.
Args:
x, y: Cell coordinates
sprite_index: Index in texture for sprite display
count: Stack count to display
data: Arbitrary data to associate with cell
"""
if (x, y) not in self._cell_frames:
return
cell_frame = self._cell_frames[(x, y)]
# Store cell data
self._cells[(x, y)] = {
'sprite_index': sprite_index,
'count': count,
'data': data
}
# Clear existing children except the frame itself
while len(cell_frame.children) > 0:
cell_frame.children.pop()
# Add sprite if we have texture and sprite_index
if self.texture and sprite_index is not None:
sprite = mcrfpy.Sprite(
pos=(2, 2),
texture=self.texture,
sprite_index=sprite_index
)
# Scale sprite to fit cell (with padding)
# Note: sprite scaling depends on texture cell size
cell_frame.children.append(sprite)
# Add count label if count > 1
if count is not None and count > 1:
count_label = mcrfpy.Caption(
text=str(count),
pos=(self.cell_size[0] - 8, self.cell_size[1] - 12),
fill_color=mcrfpy.Color(255, 255, 255),
font_size=10
)
count_label.outline = 1
count_label.outline_color = mcrfpy.Color(0, 0, 0)
cell_frame.children.append(count_label)
self._update_cell_display(x, y)
def get_cell(self, x, y):
"""Get cell data.
Args:
x, y: Cell coordinates
Returns:
Cell data dict or None if empty
"""
return self._cells.get((x, y))
def clear_cell(self, x, y):
"""Clear a cell's contents.
Args:
x, y: Cell coordinates
"""
if (x, y) in self._cells:
del self._cells[(x, y)]
if (x, y) in self._cell_frames:
cell_frame = self._cell_frames[(x, y)]
while len(cell_frame.children) > 0:
cell_frame.children.pop()
self._update_cell_display(x, y)
def clear_all(self):
"""Clear all cells."""
for key in list(self._cells.keys()):
self.clear_cell(*key)
self._selected = None
def find_empty_cell(self):
"""Find the first empty cell.
Returns:
(x, y) of empty cell or None if all full
"""
cols, rows = self.grid_dims
for row in range(rows):
for col in range(cols):
if (col, row) not in self._cells:
return (col, row)
return None
def is_full(self):
"""Check if all cells are filled."""
return len(self._cells) >= self.grid_dims[0] * self.grid_dims[1]
def swap_cells(self, x1, y1, x2, y2):
"""Swap contents of two cells.
Args:
x1, y1: First cell
x2, y2: Second cell
"""
cell1 = self._cells.get((x1, y1))
cell2 = self._cells.get((x2, y2))
if cell1:
self.set_cell(x2, y2, **cell1)
else:
self.clear_cell(x2, y2)
if cell2:
self.set_cell(x1, y1, **cell2)
else:
self.clear_cell(x1, y1)
def move_cell(self, from_x, from_y, to_x, to_y):
"""Move cell contents to another cell.
Args:
from_x, from_y: Source cell
to_x, to_y: Destination cell
Returns:
True if successful, False if destination not empty
"""
if (to_x, to_y) in self._cells:
return False
cell = self._cells.get((from_x, from_y))
if cell:
self.set_cell(to_x, to_y, **cell)
self.clear_cell(from_x, from_y)
return True
return False