# 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