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>
This commit is contained in:
parent
2daebc84b5
commit
55f6ea9502
41 changed files with 8493 additions and 0 deletions
344
tests/cookbook/lib/grid_container.py
Normal file
344
tests/cookbook/lib/grid_container.py
Normal file
|
|
@ -0,0 +1,344 @@
|
|||
# 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue