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>
This commit is contained in:
John McCardle 2026-01-28 18:58:25 -05:00
commit 55f6ea9502
41 changed files with 8493 additions and 0 deletions

View file

@ -0,0 +1,6 @@
# McRogueFace Cookbook - Primitive Widget Demos
"""
Demo scripts for individual widget components.
Each demo can be run interactively or in headless mode for screenshots.
"""

View file

@ -0,0 +1,296 @@
#!/usr/bin/env python3
"""Button Widget Demo - Clickable buttons with hover/press states
Interactive controls:
Click: Interact with buttons
1-4: Trigger button actions via keyboard
D: Toggle button 4 enabled/disabled
ESC: Exit demo
"""
import mcrfpy
import sys
# Add parent to path for imports
sys.path.insert(0, str(__file__).rsplit('/', 2)[0])
from lib.button import Button, create_button_row, create_button_column
class ButtonDemo:
def __init__(self):
self.scene = mcrfpy.Scene("button_demo")
self.ui = self.scene.children
self.click_count = 0
self.buttons = []
self.setup()
def setup(self):
"""Build the demo scene."""
# Background
bg = mcrfpy.Frame(
pos=(0, 0),
size=(1024, 768),
fill_color=mcrfpy.Color(20, 20, 25)
)
self.ui.append(bg)
# Title
title = mcrfpy.Caption(
text="Button Widget Demo",
pos=(512, 30),
font_size=28,
fill_color=mcrfpy.Color(255, 255, 255)
)
title.outline = 2
title.outline_color = mcrfpy.Color(0, 0, 0)
self.ui.append(title)
# Click counter display
self.counter_caption = mcrfpy.Caption(
text="Clicks: 0",
pos=(512, 70),
font_size=18,
fill_color=mcrfpy.Color(200, 200, 100)
)
self.ui.append(self.counter_caption)
# Section 1: Basic buttons with different styles
section1_label = mcrfpy.Caption(
text="Basic Buttons (click or press 1-4)",
pos=(100, 130),
font_size=16,
fill_color=mcrfpy.Color(150, 150, 150)
)
self.ui.append(section1_label)
# Default button
btn1 = Button(
"Default",
pos=(100, 160),
callback=lambda: self.on_button_click("Default")
)
self.buttons.append(btn1)
self.ui.append(btn1.frame)
# Custom color button
btn2 = Button(
"Custom",
pos=(240, 160),
fill_color=mcrfpy.Color(80, 50, 100),
hover_color=mcrfpy.Color(100, 70, 130),
press_color=mcrfpy.Color(120, 90, 150),
callback=lambda: self.on_button_click("Custom")
)
self.buttons.append(btn2)
self.ui.append(btn2.frame)
# Success-style button
btn3 = Button(
"Success",
pos=(380, 160),
fill_color=mcrfpy.Color(40, 120, 60),
hover_color=mcrfpy.Color(50, 150, 75),
press_color=mcrfpy.Color(60, 180, 90),
outline_color=mcrfpy.Color(100, 200, 120),
callback=lambda: self.on_button_click("Success")
)
self.buttons.append(btn3)
self.ui.append(btn3.frame)
# Danger-style button (toggleable)
self.btn4 = Button(
"Danger",
pos=(520, 160),
fill_color=mcrfpy.Color(150, 50, 50),
hover_color=mcrfpy.Color(180, 70, 70),
press_color=mcrfpy.Color(200, 90, 90),
outline_color=mcrfpy.Color(200, 100, 100),
callback=lambda: self.on_button_click("Danger")
)
self.buttons.append(self.btn4)
self.ui.append(self.btn4.frame)
# Section 2: Different sizes
section2_label = mcrfpy.Caption(
text="Button Sizes",
pos=(100, 240),
font_size=16,
fill_color=mcrfpy.Color(150, 150, 150)
)
self.ui.append(section2_label)
# Small button
small = Button(
"Small",
pos=(100, 270),
size=(80, 30),
font_size=12,
callback=lambda: self.on_button_click("Small")
)
self.ui.append(small.frame)
# Medium button (default size)
medium = Button(
"Medium",
pos=(200, 270),
callback=lambda: self.on_button_click("Medium")
)
self.ui.append(medium.frame)
# Large button
large = Button(
"Large Button",
pos=(340, 270),
size=(180, 50),
font_size=20,
callback=lambda: self.on_button_click("Large")
)
self.ui.append(large.frame)
# Section 3: Button row
section3_label = mcrfpy.Caption(
text="Button Row (auto-layout)",
pos=(100, 360),
font_size=16,
fill_color=mcrfpy.Color(150, 150, 150)
)
self.ui.append(section3_label)
row_buttons = create_button_row(
labels=["File", "Edit", "View", "Help"],
start_pos=(100, 390),
spacing=5,
size=(80, 35),
callbacks=[
lambda: self.on_button_click("File"),
lambda: self.on_button_click("Edit"),
lambda: self.on_button_click("View"),
lambda: self.on_button_click("Help"),
]
)
for btn in row_buttons:
self.ui.append(btn.frame)
# Section 4: Button column
section4_label = mcrfpy.Caption(
text="Button Column",
pos=(600, 240),
font_size=16,
fill_color=mcrfpy.Color(150, 150, 150)
)
self.ui.append(section4_label)
col_buttons = create_button_column(
labels=["New Game", "Load Game", "Options", "Quit"],
start_pos=(600, 270),
spacing=5,
size=(150, 35),
callbacks=[
lambda: self.on_button_click("New Game"),
lambda: self.on_button_click("Load Game"),
lambda: self.on_button_click("Options"),
lambda: self.on_button_click("Quit"),
]
)
for btn in col_buttons:
self.ui.append(btn.frame)
# Section 5: Disabled state
section5_label = mcrfpy.Caption(
text="Disabled State (press D to toggle)",
pos=(100, 470),
font_size=16,
fill_color=mcrfpy.Color(150, 150, 150)
)
self.ui.append(section5_label)
self.disabled_btn = Button(
"Disabled",
pos=(100, 500),
enabled=False,
callback=lambda: self.on_button_click("This shouldn't fire!")
)
self.ui.append(self.disabled_btn.frame)
self.toggle_info = mcrfpy.Caption(
text="Currently: Disabled",
pos=(240, 510),
font_size=14,
fill_color=mcrfpy.Color(180, 100, 100)
)
self.ui.append(self.toggle_info)
# Instructions
instr = mcrfpy.Caption(
text="Click buttons or press 1-4 | D: Toggle disabled | ESC: Exit",
pos=(50, 730),
font_size=14,
fill_color=mcrfpy.Color(120, 120, 120)
)
self.ui.append(instr)
# Last action display
self.action_caption = mcrfpy.Caption(
text="Last action: None",
pos=(50, 600),
font_size=16,
fill_color=mcrfpy.Color(100, 200, 100)
)
self.ui.append(self.action_caption)
def on_button_click(self, button_name):
"""Handle button click."""
self.click_count += 1
self.counter_caption.text = f"Clicks: {self.click_count}"
self.action_caption.text = f"Last action: Clicked '{button_name}'"
def on_key(self, key, state):
"""Handle keyboard input."""
if state != "start":
return
if key == "Escape":
sys.exit(0)
elif key == "Num1" and len(self.buttons) > 0:
self.buttons[0].callback()
elif key == "Num2" and len(self.buttons) > 1:
self.buttons[1].callback()
elif key == "Num3" and len(self.buttons) > 2:
self.buttons[2].callback()
elif key == "Num4" and len(self.buttons) > 3:
self.buttons[3].callback()
elif key == "D":
# Toggle disabled button
self.disabled_btn.enabled = not self.disabled_btn.enabled
if self.disabled_btn.enabled:
self.toggle_info.text = "Currently: Enabled"
self.toggle_info.fill_color = mcrfpy.Color(100, 180, 100)
else:
self.toggle_info.text = "Currently: Disabled"
self.toggle_info.fill_color = mcrfpy.Color(180, 100, 100)
def activate(self):
"""Activate the demo scene."""
self.scene.on_key = self.on_key
mcrfpy.current_scene = self.scene
def main():
"""Run the button demo."""
demo = ButtonDemo()
demo.activate()
# Headless mode: capture screenshot and exit
try:
if mcrfpy.headless_mode():
from mcrfpy import automation
mcrfpy.Timer("screenshot", lambda rt: (
automation.screenshot("screenshots/primitives/button_demo.png"),
sys.exit(0)
), 100)
except AttributeError:
# headless_mode() may not exist in all versions
pass
if __name__ == "__main__":
main()

View file

@ -0,0 +1,287 @@
#!/usr/bin/env python3
"""Choice List Widget Demo - Vertical selectable list with keyboard/mouse navigation
Interactive controls:
Up/Down: Navigate choices
Enter: Confirm selection
Click: Select item
A: Add a new choice
R: Remove selected choice
ESC: Exit demo
"""
import mcrfpy
import sys
# Add parent to path for imports
sys.path.insert(0, str(__file__).rsplit('/', 2)[0])
from lib.choice_list import ChoiceList, create_menu
class ChoiceListDemo:
def __init__(self):
self.scene = mcrfpy.Scene("choice_list_demo")
self.ui = self.scene.children
self.lists = []
self.active_list_idx = 0
self.add_counter = 0
self.setup()
def setup(self):
"""Build the demo scene."""
# Background
bg = mcrfpy.Frame(
pos=(0, 0),
size=(1024, 768),
fill_color=mcrfpy.Color(20, 20, 25)
)
self.ui.append(bg)
# Title
title = mcrfpy.Caption(
text="Choice List Widget Demo",
pos=(512, 30),
font_size=28,
fill_color=mcrfpy.Color(255, 255, 255)
)
title.outline = 2
title.outline_color = mcrfpy.Color(0, 0, 0)
self.ui.append(title)
# Section 1: Basic choice list
section1_label = mcrfpy.Caption(
text="Main Menu (keyboard or click)",
pos=(50, 90),
font_size=16,
fill_color=mcrfpy.Color(150, 150, 150)
)
self.ui.append(section1_label)
self.main_list = ChoiceList(
pos=(50, 120),
size=(200, 150),
choices=["New Game", "Continue", "Options", "Credits", "Quit"],
on_select=self.on_main_select
)
self.lists.append(self.main_list)
self.ui.append(self.main_list.frame)
# Selection indicator
self.main_selection = mcrfpy.Caption(
text="Selected: New Game",
pos=(50, 280),
font_size=14,
fill_color=mcrfpy.Color(100, 200, 100)
)
self.ui.append(self.main_selection)
# Section 2: Custom styled list
section2_label = mcrfpy.Caption(
text="Difficulty Selection",
pos=(300, 90),
font_size=16,
fill_color=mcrfpy.Color(150, 150, 150)
)
self.ui.append(section2_label)
self.diff_list = ChoiceList(
pos=(300, 120),
size=(180, 120),
choices=["Easy", "Normal", "Hard", "Nightmare"],
on_select=self.on_diff_select,
selected_color=mcrfpy.Color(120, 60, 60),
hover_color=mcrfpy.Color(80, 40, 40),
normal_color=mcrfpy.Color(50, 30, 30)
)
self.lists.append(self.diff_list)
self.ui.append(self.diff_list.frame)
self.diff_selection = mcrfpy.Caption(
text="Difficulty: Easy",
pos=(300, 250),
font_size=14,
fill_color=mcrfpy.Color(200, 100, 100)
)
self.ui.append(self.diff_selection)
# Section 3: Dynamic list (add/remove items)
section3_label = mcrfpy.Caption(
text="Dynamic List (A: Add, R: Remove)",
pos=(530, 90),
font_size=16,
fill_color=mcrfpy.Color(150, 150, 150)
)
self.ui.append(section3_label)
self.dynamic_list = ChoiceList(
pos=(530, 120),
size=(200, 180),
choices=["Item 1", "Item 2", "Item 3"],
on_select=self.on_dynamic_select
)
self.lists.append(self.dynamic_list)
self.ui.append(self.dynamic_list.frame)
self.dynamic_info = mcrfpy.Caption(
text="Items: 3",
pos=(530, 310),
font_size=14,
fill_color=mcrfpy.Color(100, 150, 200)
)
self.ui.append(self.dynamic_info)
# Section 4: Menu with title (using helper)
section4_label = mcrfpy.Caption(
text="Menu with Title (helper function)",
pos=(780, 90),
font_size=16,
fill_color=mcrfpy.Color(150, 150, 150)
)
self.ui.append(section4_label)
menu_container, self.titled_list = create_menu(
pos=(780, 120),
choices=["Attack", "Defend", "Magic", "Item", "Flee"],
on_select=self.on_combat_select,
title="Combat",
width=180
)
self.lists.append(self.titled_list)
self.ui.append(menu_container)
self.combat_selection = mcrfpy.Caption(
text="Action: Attack",
pos=(780, 340),
font_size=14,
fill_color=mcrfpy.Color(200, 200, 100)
)
self.ui.append(self.combat_selection)
# Section 5: Long list (scrolling needed in future)
section5_label = mcrfpy.Caption(
text="Long List",
pos=(50, 350),
font_size=16,
fill_color=mcrfpy.Color(150, 150, 150)
)
self.ui.append(section5_label)
long_choices = [f"Option {i+1}" for i in range(10)]
self.long_list = ChoiceList(
pos=(50, 380),
size=(200, 300),
choices=long_choices,
on_select=self.on_long_select,
item_height=28
)
self.lists.append(self.long_list)
self.ui.append(self.long_list.frame)
self.long_selection = mcrfpy.Caption(
text="Long list: Option 1",
pos=(50, 690),
font_size=14,
fill_color=mcrfpy.Color(150, 150, 200)
)
self.ui.append(self.long_selection)
# Active list indicator
self.active_indicator = mcrfpy.Caption(
text="Active list: Main Menu (Tab to switch)",
pos=(300, 400),
font_size=14,
fill_color=mcrfpy.Color(200, 200, 200)
)
self.ui.append(self.active_indicator)
# Instructions
instr = mcrfpy.Caption(
text="Up/Down: Navigate | Enter: Confirm | Tab: Switch list | A: Add | R: Remove | ESC: Exit",
pos=(50, 730),
font_size=14,
fill_color=mcrfpy.Color(120, 120, 120)
)
self.ui.append(instr)
def on_main_select(self, index, value):
"""Handle main menu selection."""
self.main_selection.text = f"Selected: {value}"
def on_diff_select(self, index, value):
"""Handle difficulty selection."""
self.diff_selection.text = f"Difficulty: {value}"
def on_dynamic_select(self, index, value):
"""Handle dynamic list selection."""
self.dynamic_info.text = f"Selected: {value} (Items: {len(self.dynamic_list.choices)})"
def on_combat_select(self, index, value):
"""Handle combat menu selection."""
self.combat_selection.text = f"Action: {value}"
def on_long_select(self, index, value):
"""Handle long list selection."""
self.long_selection.text = f"Long list: {value}"
def _update_active_indicator(self):
"""Update the active list indicator."""
names = ["Main Menu", "Difficulty", "Dynamic", "Combat", "Long List"]
if self.active_list_idx < len(names):
self.active_indicator.text = f"Active list: {names[self.active_list_idx]} (Tab to switch)"
def on_key(self, key, state):
"""Handle keyboard input."""
if state != "start":
return
if key == "Escape":
sys.exit(0)
# Get active list
active = self.lists[self.active_list_idx] if self.lists else None
if key == "Up" and active:
active.navigate(-1)
elif key == "Down" and active:
active.navigate(1)
elif key == "Enter" and active:
active.confirm()
elif key == "Tab":
# Switch active list
self.active_list_idx = (self.active_list_idx + 1) % len(self.lists)
self._update_active_indicator()
elif key == "A":
# Add item to dynamic list
self.add_counter += 1
self.dynamic_list.add_choice(f"New Item {self.add_counter}")
self.dynamic_info.text = f"Items: {len(self.dynamic_list.choices)}"
elif key == "R":
# Remove selected from dynamic list
if len(self.dynamic_list.choices) > 1:
self.dynamic_list.remove_choice(self.dynamic_list.selected_index)
self.dynamic_info.text = f"Items: {len(self.dynamic_list.choices)}"
def activate(self):
"""Activate the demo scene."""
self.scene.on_key = self.on_key
mcrfpy.current_scene = self.scene
def main():
"""Run the choice list demo."""
demo = ChoiceListDemo()
demo.activate()
# Headless mode: capture screenshot and exit
try:
if mcrfpy.headless_mode():
from mcrfpy import automation
mcrfpy.Timer("screenshot", lambda rt: (
automation.screenshot("screenshots/primitives/choice_list_demo.png"),
sys.exit(0)
), 100)
except AttributeError:
pass
if __name__ == "__main__":
main()

View file

@ -0,0 +1,380 @@
#!/usr/bin/env python3
"""Click to Pick Up Demo - Toggle-based inventory interaction
Interactive controls:
Left click on item: Pick up item (cursor changes)
Left click on empty cell: Place item
Right click: Cancel pickup
ESC: Return to menu
This demonstrates:
- Click-to-toggle pickup mode (not hold-to-drag)
- Cursor sprite following mouse
- ColorLayer for cell highlighting
- Inventory organization pattern
"""
import mcrfpy
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# Item data for sprites
ITEMS = [
(103, "Shortsword"),
(104, "Longsword"),
(117, "Hammer"),
(119, "Axe"),
(101, "Buckler"),
(102, "Shield"),
(115, "Health Pot"),
(116, "Mana Pot"),
(129, "Wand"),
(130, "Staff"),
(114, "Str Potion"),
(127, "Lesser HP"),
]
class ClickPickupDemo:
"""Demo showing click-to-pickup inventory interaction."""
def __init__(self):
self.scene = mcrfpy.Scene("demo_click_pickup")
self.ui = self.scene.children
self.grid = None
self.tile_layer = None
self.color_layer = None
# Pickup state
self.held_entity = None
self.pickup_cell = None
self.cursor_sprite = None
self.last_hover_cell = None
# Track occupied cells
self.occupied_cells = {} # (x, y) -> entity
self.setup()
def setup(self):
"""Build the demo UI."""
# Background
bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=(25, 20, 30))
self.ui.append(bg)
# Title
title = mcrfpy.Caption(
text="Click to Pick Up",
pos=(512, 30),
font_size=28,
fill_color=(255, 255, 255)
)
title.outline = 2
title.outline_color = (0, 0, 0)
self.ui.append(title)
# Status caption
self.status = mcrfpy.Caption(
text="Click an item to pick it up",
pos=(512, 70),
font_size=16,
fill_color=(180, 180, 180)
)
self.ui.append(self.status)
# Create inventory grid - zoom in constructor for proper centering
grid_size = (8, 6)
cell_size = 64
grid_pixel_size = (grid_size[0] * cell_size, grid_size[1] * cell_size)
grid_pos = ((1024 - grid_pixel_size[0]) // 2, 140)
self.grid = mcrfpy.Grid(
pos=grid_pos,
size=grid_pixel_size,
grid_size=grid_size,
texture=mcrfpy.default_texture,
zoom=4.0 # 16px * 4 = 64px per cell
)
# Get tile layer and fill with slot tiles
self.tile_layer = self.grid.layers[0]
self.tile_layer.fill(46) # Floor/slot tile
# Add color layer for highlighting
self.color_layer = self.grid.add_layer('color', z_index=-1)
# Initialize with slight tint
for y in range(grid_size[1]):
for x in range(grid_size[0]):
self.color_layer.set((x, y), (200, 200, 200, 50))
# Add event handlers
self.grid.on_click = self._on_grid_click
self.grid.on_cell_enter = self._on_cell_enter
self.grid.on_move = self._on_grid_move
self.ui.append(self.grid)
# Populate with items
self._populate_grid()
# Create cursor sprite (initially invisible)
# This is a Frame with a Sprite child, positioned outside the grid
self.cursor_frame = mcrfpy.Frame(
pos=(0, 0),
size=(64, 64),
fill_color=(0, 0, 0, 0), # Transparent
outline=0
)
self.cursor_sprite = mcrfpy.Sprite(
pos=(0, 0),
texture=mcrfpy.default_texture,
sprite_index=0
)
self.cursor_sprite.scale = 4.0
self.cursor_sprite.visible = False
self.cursor_frame.children.append(self.cursor_sprite)
self.ui.append(self.cursor_frame)
# Item name display
self.item_name = mcrfpy.Caption(
text="",
pos=(512, 560),
font_size=18,
fill_color=(255, 220, 100)
)
self.ui.append(self.item_name)
# Instructions
instr = mcrfpy.Caption(
text="Left click: Pick up / Place | Right click: Cancel | ESC to exit",
pos=(512, 700),
font_size=14,
fill_color=(150, 150, 150)
)
self.ui.append(instr)
def _populate_grid(self):
"""Add items to the grid."""
# Place items in a pattern
positions = [
(0, 0), (2, 0), (4, 1), (6, 0),
(1, 2), (3, 2), (5, 2), (7, 3),
(0, 4), (2, 4), (4, 5), (6, 5),
]
for i, (x, y) in enumerate(positions):
if i >= len(ITEMS):
break
sprite_idx, name = ITEMS[i]
entity = mcrfpy.Entity()
self.grid.entities.append(entity)
entity.grid_pos = (x, y) # Use grid_pos for tile coordinates
entity.sprite_index = sprite_idx
entity.name = name # Store name for display
self.occupied_cells[(x, y)] = entity
def _get_grid_cell(self, screen_pos):
"""Convert screen position to grid cell coordinates."""
cell_size = 16 * self.grid.zoom
x = int((screen_pos[0] - self.grid.x) / cell_size)
y = int((screen_pos[1] - self.grid.y) / cell_size)
grid_w, grid_h = self.grid.grid_size
if 0 <= x < grid_w and 0 <= y < grid_h:
return (x, y)
return None
def _on_grid_click(self, pos, button, action):
"""Handle grid click."""
if action != "start":
return
cell = self._get_grid_cell(pos)
if cell is None:
return
x, y = cell
if button == "right":
# Cancel pickup
if self.held_entity:
self._cancel_pickup()
return
if button != "left":
return
if self.held_entity is None:
# Try to pick up
if cell in self.occupied_cells:
self._pickup_item(cell)
else:
# Try to place
if cell not in self.occupied_cells:
self._place_item(cell)
elif cell == self.pickup_cell:
# Clicked on original cell - cancel
self._cancel_pickup()
def _pickup_item(self, cell):
"""Pick up item from cell."""
entity = self.occupied_cells[cell]
self.held_entity = entity
self.pickup_cell = cell
# Hide the entity
entity.visible = False
# Mark source cell yellow
self.color_layer.set(cell, (255, 255, 100, 200))
# Setup cursor sprite
self.cursor_sprite.sprite_index = entity.sprite_index
self.cursor_sprite.visible = True
# Update status
name = getattr(entity, 'name', 'Item')
self.status.text = f"Holding: {name}"
self.status.fill_color = (100, 200, 255)
self.item_name.text = name
def _place_item(self, cell):
"""Place held item in cell."""
x, y = cell
# Move entity to new position
self.held_entity.grid_pos = (x, y)
self.held_entity.visible = True
# Update tracking
del self.occupied_cells[self.pickup_cell]
self.occupied_cells[cell] = self.held_entity
# Clear source cell highlight
self.color_layer.set(self.pickup_cell, (200, 200, 200, 50))
# Clear hover highlight
if self.last_hover_cell and self.last_hover_cell != self.pickup_cell:
self.color_layer.set(self.last_hover_cell, (200, 200, 200, 50))
# Hide cursor
self.cursor_sprite.visible = False
# Update status
self.status.text = f"Placed at ({x}, {y})"
self.status.fill_color = (100, 255, 100)
self.item_name.text = ""
self.held_entity = None
self.pickup_cell = None
self.last_hover_cell = None
def _cancel_pickup(self):
"""Cancel current pickup operation."""
if self.held_entity:
# Restore entity visibility
self.held_entity.visible = True
# Clear source cell highlight
self.color_layer.set(self.pickup_cell, (200, 200, 200, 50))
# Clear hover highlight
if self.last_hover_cell and self.last_hover_cell != self.pickup_cell:
self.color_layer.set(self.last_hover_cell, (200, 200, 200, 50))
# Hide cursor
self.cursor_sprite.visible = False
self.status.text = "Cancelled"
self.status.fill_color = (200, 150, 100)
self.item_name.text = ""
self.held_entity = None
self.pickup_cell = None
self.last_hover_cell = None
def _on_cell_enter(self, cell_pos):
"""Handle cell hover."""
x, y = int(cell_pos[0]), int(cell_pos[1])
cell = (x, y)
# Show item name on hover (when not holding)
if self.held_entity is None:
if cell in self.occupied_cells:
entity = self.occupied_cells[cell]
name = getattr(entity, 'name', 'Item')
self.item_name.text = name
else:
self.item_name.text = ""
return
# Clear previous hover highlight (if different from source)
if self.last_hover_cell and self.last_hover_cell != self.pickup_cell:
self.color_layer.set(self.last_hover_cell, (200, 200, 200, 50))
# Highlight current cell (if different from source)
if cell != self.pickup_cell:
if cell in self.occupied_cells:
self.color_layer.set(cell, (255, 100, 100, 200)) # Red - can't place
else:
self.color_layer.set(cell, (100, 255, 100, 200)) # Green - can place
self.last_hover_cell = cell
def _on_grid_move(self, pos):
"""Update cursor sprite position.
Note: #230 - on_move now only receives position, not button/action
"""
if self.cursor_sprite.visible:
# Position cursor centered on mouse
self.cursor_frame.x = pos[0] - 32
self.cursor_frame.y = pos[1] - 32
def on_key(self, key, state):
"""Handle keyboard input."""
if state != "start":
return
if key == "Escape":
if self.held_entity:
self._cancel_pickup()
return
try:
from cookbook_main import main
main()
except:
sys.exit(0)
def activate(self):
"""Activate the demo scene."""
self.scene.on_key = self.on_key
mcrfpy.current_scene = self.scene
def main():
"""Run the demo."""
demo = ClickPickupDemo()
demo.activate()
# Headless screenshot
try:
if mcrfpy.headless_mode():
from mcrfpy import automation
# Simulate picking up an item for screenshot
demo._pickup_item((0, 0))
demo.cursor_frame.x = 300
demo.cursor_frame.y = 350
mcrfpy.Timer("screenshot", lambda rt: (
automation.screenshot("screenshots/primitives/click_pickup.png"),
sys.exit(0)
), 100)
except AttributeError:
pass
if __name__ == "__main__":
main()

View file

@ -0,0 +1,270 @@
#!/usr/bin/env python3
"""Drag and Drop (Grid) Demo - Drag entities between grid cells
Interactive controls:
Left click + drag: Move entity to new cell
ESC: Return to menu
This demonstrates:
- Grid entity dragging with on_click and on_cell_enter
- ColorLayer for cell highlighting
- Collision detection (can't drop on occupied cells)
- Visual feedback during drag
"""
import mcrfpy
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# Item data for sprites
ITEMS = [
(103, "Shortsword"), # +1 atk
(104, "Longsword"), # +2 atk
(117, "Hammer"), # +2 atk
(119, "Axe"), # +3 atk
(101, "Buckler"), # +1 def
(102, "Shield"), # +2 def
(115, "Health Pot"),
(116, "Mana Pot"),
(129, "Wand"), # +1 atk, +4 int
(114, "Str Potion"),
]
class GridDragDropDemo:
"""Demo showing entity drag and drop on a grid."""
def __init__(self):
self.scene = mcrfpy.Scene("demo_drag_drop_grid")
self.ui = self.scene.children
self.grid = None
self.tile_layer = None
self.color_layer = None
self.dragging_entity = None
self.drag_start_cell = None
self.occupied_cells = set() # Track which cells have entities
self.setup()
def setup(self):
"""Build the demo UI."""
# Background
bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=(20, 25, 30))
self.ui.append(bg)
# Title
title = mcrfpy.Caption(
text="Grid Drag & Drop",
pos=(512, 30),
font_size=28,
fill_color=(255, 255, 255)
)
title.outline = 2
title.outline_color = (0, 0, 0)
self.ui.append(title)
# Status caption
self.status = mcrfpy.Caption(
text="Click and drag items to rearrange",
pos=(512, 70),
font_size=16,
fill_color=(180, 180, 180)
)
self.ui.append(self.status)
# Create grid - zoom in constructor for proper centering
grid_size = (10, 8)
cell_size = 48
grid_pixel_size = (grid_size[0] * cell_size, grid_size[1] * cell_size)
grid_pos = ((1024 - grid_pixel_size[0]) // 2, 150)
self.grid = mcrfpy.Grid(
pos=grid_pos,
size=grid_pixel_size,
grid_size=grid_size,
texture=mcrfpy.default_texture,
zoom=3.0 # Each cell is 16px * 3 = 48px
)
# Get tile layer and fill with floor tiles
self.tile_layer = self.grid.layers[0]
self.tile_layer.fill(46) # Floor tile
# Add color layer for highlighting (above tiles, below entities)
self.color_layer = self.grid.add_layer('color', z_index=-1)
# Add event handlers
self.grid.on_click = self._on_grid_click
self.grid.on_cell_enter = self._on_cell_enter
self.ui.append(self.grid)
# Add some entities to the grid
self._populate_grid()
# Instructions
instr = mcrfpy.Caption(
text="Click to pick up, drag to move, release to drop | Red = occupied | ESC to exit",
pos=(512, 700),
font_size=14,
fill_color=(150, 150, 150)
)
self.ui.append(instr)
def _populate_grid(self):
"""Add entities to the grid in a scattered pattern."""
# Place items at various positions
positions = [
(1, 1), (3, 1), (5, 2), (7, 1),
(2, 4), (4, 3), (6, 5), (8, 4),
(1, 6), (5, 6)
]
for i, (x, y) in enumerate(positions):
if i >= len(ITEMS):
break
sprite_idx, name = ITEMS[i]
entity = mcrfpy.Entity()
self.grid.entities.append(entity)
entity.grid_pos = (x, y) # Use grid_pos for tile coordinates
entity.sprite_index = sprite_idx
self.occupied_cells.add((x, y))
def _get_entity_at(self, x, y):
"""Get entity at grid position, or None."""
for entity in self.grid.entities:
gp = entity.grid_pos
ex, ey = int(gp[0]), int(gp[1])
if ex == x and ey == y:
return entity
return None
def _on_grid_click(self, pos, button, action):
"""Handle grid click for drag start/end."""
if button != "left":
return
# Convert screen pos to grid cell
grid_x = int((pos[0] - self.grid.x) / (16 * self.grid.zoom))
grid_y = int((pos[1] - self.grid.y) / (16 * self.grid.zoom))
# Bounds check
grid_w, grid_h = self.grid.grid_size
if not (0 <= grid_x < grid_w and 0 <= grid_y < grid_h):
return
if action == "start":
# Start drag if there's an entity here
entity = self._get_entity_at(grid_x, grid_y)
if entity:
self.dragging_entity = entity
self.drag_start_cell = (grid_x, grid_y)
self.status.text = f"Dragging from ({grid_x}, {grid_y})"
self.status.fill_color = (100, 200, 255)
# Highlight start cell yellow
self.color_layer.set((grid_x, grid_y), (255, 255, 100, 200))
elif action == "end":
if self.dragging_entity:
# Drop the entity
target_cell = (grid_x, grid_y)
if target_cell == self.drag_start_cell:
# Dropped in same cell - no change
self.status.text = "Cancelled - same cell"
elif target_cell in self.occupied_cells:
# Can't drop on occupied cell
self.status.text = f"Can't drop on occupied cell ({grid_x}, {grid_y})"
self.status.fill_color = (255, 100, 100)
else:
# Valid drop - move entity
self.occupied_cells.discard(self.drag_start_cell)
self.occupied_cells.add(target_cell)
self.dragging_entity.grid_pos = target_cell
self.status.text = f"Moved to ({grid_x}, {grid_y})"
self.status.fill_color = (100, 255, 100)
# Clear all highlights
self._clear_highlights()
self.dragging_entity = None
self.drag_start_cell = None
def _on_cell_enter(self, cell_pos):
"""Handle cell hover during drag."""
if not self.dragging_entity:
return
x, y = int(cell_pos[0]), int(cell_pos[1])
# Clear previous highlights (except start cell)
self._clear_highlights()
# Re-highlight start cell
if self.drag_start_cell:
self.color_layer.set(self.drag_start_cell, (255, 255, 100, 200))
# Highlight current cell
if (x, y) != self.drag_start_cell:
if (x, y) in self.occupied_cells:
self.color_layer.set((x, y), (255, 100, 100, 200)) # Red - can't drop
else:
self.color_layer.set((x, y), (100, 255, 100, 200)) # Green - can drop
# Move entity preview
self.dragging_entity.grid_pos = (x, y)
def _clear_highlights(self):
"""Clear all cell color highlights."""
grid_w, grid_h = self.grid.grid_size
for y in range(grid_h):
for x in range(grid_w):
self.color_layer.set((x, y), (0, 0, 0, 0))
def on_key(self, key, state):
"""Handle keyboard input."""
if state != "start":
return
if key == "Escape":
# Cancel any drag in progress
if self.dragging_entity and self.drag_start_cell:
self.dragging_entity.grid_pos = self.drag_start_cell
self._clear_highlights()
self.dragging_entity = None
self.drag_start_cell = None
self.status.text = "Drag cancelled"
return
# Return to cookbook menu or exit
try:
from cookbook_main import main
main()
except:
sys.exit(0)
def activate(self):
"""Activate the demo scene."""
self.scene.on_key = self.on_key
mcrfpy.current_scene = self.scene
def main():
"""Run the demo."""
demo = GridDragDropDemo()
demo.activate()
# Headless screenshot
try:
if mcrfpy.headless_mode():
from mcrfpy import automation
mcrfpy.Timer("screenshot", lambda rt: (
automation.screenshot("screenshots/primitives/drag_drop_grid.png"),
sys.exit(0)
), 100)
except AttributeError:
pass
if __name__ == "__main__":
main()

View file

@ -0,0 +1,332 @@
#!/usr/bin/env python3
"""Stat Bar Widget Demo - Progress bars for health, mana, XP, etc.
Interactive controls:
1-4: Decrease stat bars
Shift+1-4: Increase stat bars
F: Flash the health bar
R: Reset all bars
ESC: Exit demo
"""
import mcrfpy
import sys
# Add parent to path for imports
sys.path.insert(0, str(__file__).rsplit('/', 2)[0])
from lib.stat_bar import StatBar, create_stat_bar_group
class StatBarDemo:
def __init__(self):
self.scene = mcrfpy.Scene("stat_bar_demo")
self.ui = self.scene.children
self.bars = {}
self.setup()
def setup(self):
"""Build the demo scene."""
# Background
bg = mcrfpy.Frame(
pos=(0, 0),
size=(1024, 768),
fill_color=mcrfpy.Color(20, 20, 25)
)
self.ui.append(bg)
# Title
title = mcrfpy.Caption(
text="Stat Bar Widget Demo",
pos=(512, 30),
font_size=28,
fill_color=mcrfpy.Color(255, 255, 255)
)
title.outline = 2
title.outline_color = mcrfpy.Color(0, 0, 0)
self.ui.append(title)
# Section 1: Basic stat bars with labels
section1_label = mcrfpy.Caption(
text="Character Stats (press 1-4 to decrease, Shift+1-4 to increase)",
pos=(50, 90),
font_size=16,
fill_color=mcrfpy.Color(150, 150, 150)
)
self.ui.append(section1_label)
# Health bar
self.bars['hp'] = StatBar(
pos=(50, 120),
size=(250, 25),
current=75,
maximum=100,
fill_color=StatBar.HEALTH_COLOR,
label="HP"
)
self.ui.append(self.bars['hp'].frame)
# Mana bar
self.bars['mp'] = StatBar(
pos=(50, 155),
size=(250, 25),
current=50,
maximum=80,
fill_color=StatBar.MANA_COLOR,
label="MP"
)
self.ui.append(self.bars['mp'].frame)
# Stamina bar
self.bars['stamina'] = StatBar(
pos=(50, 190),
size=(250, 25),
current=90,
maximum=100,
fill_color=StatBar.STAMINA_COLOR,
label="Stamina"
)
self.ui.append(self.bars['stamina'].frame)
# XP bar
self.bars['xp'] = StatBar(
pos=(50, 225),
size=(250, 25),
current=250,
maximum=1000,
fill_color=StatBar.XP_COLOR,
label="XP"
)
self.ui.append(self.bars['xp'].frame)
# Section 2: Different sizes
section2_label = mcrfpy.Caption(
text="Different Sizes",
pos=(50, 290),
font_size=16,
fill_color=mcrfpy.Color(150, 150, 150)
)
self.ui.append(section2_label)
# Thin bar
thin_bar = StatBar(
pos=(50, 320),
size=(200, 10),
current=60,
maximum=100,
fill_color=mcrfpy.Color(100, 150, 200),
show_text=False
)
self.ui.append(thin_bar.frame)
thin_label = mcrfpy.Caption(
text="Thin (no text)",
pos=(260, 315),
font_size=12,
fill_color=mcrfpy.Color(120, 120, 120)
)
self.ui.append(thin_label)
# Wide bar
wide_bar = StatBar(
pos=(50, 345),
size=(400, 35),
current=450,
maximum=500,
fill_color=StatBar.SHIELD_COLOR,
label="Shield",
font_size=16
)
self.ui.append(wide_bar.frame)
# Section 3: Stat bar group
section3_label = mcrfpy.Caption(
text="Stat Bar Group (auto-layout)",
pos=(500, 90),
font_size=16,
fill_color=mcrfpy.Color(150, 150, 150)
)
self.ui.append(section3_label)
group = create_stat_bar_group([
{"name": "Strength", "current": 15, "max": 20, "color": mcrfpy.Color(200, 80, 80)},
{"name": "Dexterity", "current": 18, "max": 20, "color": mcrfpy.Color(80, 200, 80)},
{"name": "Intelligence", "current": 12, "max": 20, "color": mcrfpy.Color(80, 80, 200)},
{"name": "Wisdom", "current": 14, "max": 20, "color": mcrfpy.Color(200, 200, 80)},
{"name": "Charisma", "current": 10, "max": 20, "color": mcrfpy.Color(200, 80, 200)},
], start_pos=(500, 120), spacing=10, size=(220, 22))
for bar in group.values():
self.ui.append(bar.frame)
# Section 4: Edge cases
section4_label = mcrfpy.Caption(
text="Edge Cases",
pos=(50, 420),
font_size=16,
fill_color=mcrfpy.Color(150, 150, 150)
)
self.ui.append(section4_label)
# Empty bar
empty_bar = StatBar(
pos=(50, 450),
size=(200, 20),
current=0,
maximum=100,
fill_color=StatBar.HEALTH_COLOR,
label="Empty"
)
self.ui.append(empty_bar.frame)
# Full bar
full_bar = StatBar(
pos=(50, 480),
size=(200, 20),
current=100,
maximum=100,
fill_color=StatBar.STAMINA_COLOR,
label="Full"
)
self.ui.append(full_bar.frame)
# Overfill attempt (should clamp)
overfill_bar = StatBar(
pos=(50, 510),
size=(200, 20),
current=150, # Will be clamped to 100
maximum=100,
fill_color=StatBar.XP_COLOR,
label="Overfill"
)
self.ui.append(overfill_bar.frame)
# Section 5: Animation demo
section5_label = mcrfpy.Caption(
text="Animation Demo (watch the bars change)",
pos=(500, 290),
font_size=16,
fill_color=mcrfpy.Color(150, 150, 150)
)
self.ui.append(section5_label)
self.anim_bar = StatBar(
pos=(500, 320),
size=(250, 30),
current=50,
maximum=100,
fill_color=mcrfpy.Color(150, 100, 200),
label="Animated"
)
self.ui.append(self.anim_bar.frame)
# Start animation loop
self._anim_direction = 1
mcrfpy.Timer("anim_bar", self._animate_bar, 2000)
# Section 6: Flash effect
section6_label = mcrfpy.Caption(
text="Flash Effect (press F)",
pos=(500, 400),
font_size=16,
fill_color=mcrfpy.Color(150, 150, 150)
)
self.ui.append(section6_label)
self.flash_bar = StatBar(
pos=(500, 430),
size=(250, 30),
current=80,
maximum=100,
fill_color=StatBar.HEALTH_COLOR,
label="Flash Me"
)
self.ui.append(self.flash_bar.frame)
# Instructions
instr = mcrfpy.Caption(
text="1-4: Decrease bars | Shift+1-4: Increase bars | F: Flash | R: Reset | ESC: Exit",
pos=(50, 730),
font_size=14,
fill_color=mcrfpy.Color(120, 120, 120)
)
self.ui.append(instr)
# Status display
self.status = mcrfpy.Caption(
text="Status: Ready",
pos=(50, 600),
font_size=16,
fill_color=mcrfpy.Color(100, 200, 100)
)
self.ui.append(self.status)
def _animate_bar(self, runtime):
"""Animate the demo bar back and forth."""
current = self.anim_bar.current
if self._anim_direction > 0:
new_val = min(100, current + 30)
if new_val >= 100:
self._anim_direction = -1
else:
new_val = max(10, current - 30)
if new_val <= 10:
self._anim_direction = 1
self.anim_bar.set_value(new_val, animate=True)
def on_key(self, key, state):
"""Handle keyboard input."""
if state != "start":
return
if key == "Escape":
sys.exit(0)
# Number keys to modify bars
bar_keys = ['hp', 'mp', 'stamina', 'xp']
key_map = {"Num1": 0, "Num2": 1, "Num3": 2, "Num4": 3}
if key in key_map:
idx = key_map[key]
if idx < len(bar_keys):
bar = self.bars[bar_keys[idx]]
# Decrease by 10
bar.set_value(bar.current - 10, animate=True)
self.status.text = f"Status: Decreased {bar_keys[idx].upper()}"
elif key == "F":
self.flash_bar.flash()
self.status.text = "Status: Flash effect triggered!"
elif key == "R":
# Reset all bars
self.bars['hp'].set_value(75, 100, animate=True)
self.bars['mp'].set_value(50, 80, animate=True)
self.bars['stamina'].set_value(90, 100, animate=True)
self.bars['xp'].set_value(250, 1000, animate=True)
self.status.text = "Status: All bars reset"
def activate(self):
"""Activate the demo scene."""
self.scene.on_key = self.on_key
mcrfpy.current_scene = self.scene
def main():
"""Run the stat bar demo."""
demo = StatBarDemo()
demo.activate()
# Headless mode: capture screenshot and exit
try:
if mcrfpy.headless_mode():
from mcrfpy import automation
mcrfpy.Timer("screenshot", lambda rt: (
automation.screenshot("screenshots/primitives/stat_bar_demo.png"),
sys.exit(0)
), 100)
except AttributeError:
pass
if __name__ == "__main__":
main()

View file

@ -0,0 +1,248 @@
#!/usr/bin/env python3
"""Text Box Widget Demo - Word-wrapped text with typewriter effect
Interactive controls:
1: Show typewriter text
2: Show instant text
3: Skip animation
4: Clear text
D: Toggle dialogue mode
ESC: Exit demo
"""
import mcrfpy
import sys
# Add parent to path for imports
sys.path.insert(0, str(__file__).rsplit('/', 2)[0])
from lib.text_box import TextBox, DialogueBox
class TextBoxDemo:
def __init__(self):
self.scene = mcrfpy.Scene("text_box_demo")
self.ui = self.scene.children
self.setup()
def setup(self):
"""Build the demo scene."""
# Background
bg = mcrfpy.Frame(
pos=(0, 0),
size=(1024, 768),
fill_color=mcrfpy.Color(20, 20, 25)
)
self.ui.append(bg)
# Title
title = mcrfpy.Caption(
text="Text Box Widget Demo",
pos=(512, 30),
font_size=28,
fill_color=mcrfpy.Color(255, 255, 255)
)
title.outline = 2
title.outline_color = mcrfpy.Color(0, 0, 0)
self.ui.append(title)
# Section 1: Basic text box with typewriter
section1_label = mcrfpy.Caption(
text="Typewriter Effect (press 1 to play)",
pos=(50, 80),
font_size=16,
fill_color=mcrfpy.Color(150, 150, 150)
)
self.ui.append(section1_label)
self.typewriter_box = TextBox(
pos=(50, 110),
size=(400, 120),
text="",
chars_per_second=40
)
self.ui.append(self.typewriter_box.frame)
self.sample_text = (
"Welcome to McRogueFace! This is a demonstration of the "
"typewriter effect. Each character appears one at a time, "
"creating a classic RPG dialogue feel. You can adjust the "
"speed by changing the chars_per_second parameter."
)
# Completion indicator
self.completion_label = mcrfpy.Caption(
text="Status: Ready",
pos=(50, 240),
font_size=12,
fill_color=mcrfpy.Color(100, 100, 100)
)
self.ui.append(self.completion_label)
# Section 2: Instant text
section2_label = mcrfpy.Caption(
text="Instant Text (press 2 to change)",
pos=(500, 80),
font_size=16,
fill_color=mcrfpy.Color(150, 150, 150)
)
self.ui.append(section2_label)
self.instant_box = TextBox(
pos=(500, 110),
size=(450, 120),
text="This text appeared instantly. Press 2 to change it to different content.",
chars_per_second=0 # Instant display
)
self.ui.append(self.instant_box.frame)
# Section 3: Dialogue box with speaker
section3_label = mcrfpy.Caption(
text="Dialogue Box (press D to cycle speakers)",
pos=(50, 290),
font_size=16,
fill_color=mcrfpy.Color(150, 150, 150)
)
self.ui.append(section3_label)
self.dialogue_box = DialogueBox(
pos=(50, 320),
size=(600, 150),
speaker="Elder Sage",
text="Greetings, adventurer. I have been expecting you. The ancient prophecy speaks of one who would come to restore balance to our world.",
chars_per_second=35
)
self.ui.append(self.dialogue_box.frame)
self.dialogue_index = 0
self.dialogues = [
("Elder Sage", "Greetings, adventurer. I have been expecting you. The ancient prophecy speaks of one who would come to restore balance to our world."),
("Hero", "I'm not sure I'm the right person for this task. What exactly must I do?"),
("Elder Sage", "You must journey to the Forgotten Temple and retrieve the Crystal of Dawn. Only its light can dispel the darkness that threatens our land."),
("Mysterious Voice", "Beware... the path is fraught with danger. Many have tried and failed before you..."),
("Hero", "I accept this quest. Point me to the temple, and I shall not rest until the crystal is recovered!"),
]
# Section 4: Different styles
section4_label = mcrfpy.Caption(
text="Custom Styles",
pos=(50, 500),
font_size=16,
fill_color=mcrfpy.Color(150, 150, 150)
)
self.ui.append(section4_label)
# Dark theme
dark_box = TextBox(
pos=(50, 530),
size=(280, 100),
text="Dark theme with light text. Good for mysterious or ominous messages.",
chars_per_second=0,
bg_color=mcrfpy.Color(10, 10, 15),
text_color=mcrfpy.Color(180, 180, 200),
outline_color=mcrfpy.Color(60, 60, 80)
)
self.ui.append(dark_box.frame)
# Warning theme
warning_box = TextBox(
pos=(350, 530),
size=(280, 100),
text="Warning theme! Use for important alerts or danger notifications.",
chars_per_second=0,
bg_color=mcrfpy.Color(80, 40, 20),
text_color=mcrfpy.Color(255, 200, 100),
outline_color=mcrfpy.Color(200, 100, 50)
)
self.ui.append(warning_box.frame)
# System theme
system_box = TextBox(
pos=(650, 530),
size=(280, 100),
text="[SYSTEM] Connection established. Loading game data...",
chars_per_second=0,
bg_color=mcrfpy.Color(20, 40, 30),
text_color=mcrfpy.Color(100, 255, 150),
outline_color=mcrfpy.Color(50, 150, 80)
)
self.ui.append(system_box.frame)
# Instructions
instr = mcrfpy.Caption(
text="1: Play typewriter | 2: Change instant text | 3: Skip | 4: Clear | D: Next dialogue | ESC: Exit",
pos=(50, 730),
font_size=14,
fill_color=mcrfpy.Color(120, 120, 120)
)
self.ui.append(instr)
def on_typewriter_complete(self):
"""Called when typewriter animation finishes."""
self.completion_label.text = "Status: Animation complete!"
self.completion_label.fill_color = mcrfpy.Color(100, 200, 100)
def on_key(self, key, state):
"""Handle keyboard input."""
if state != "start":
return
if key == "Escape":
sys.exit(0)
elif key == "Num1":
# Start typewriter animation
self.typewriter_box.on_complete = self.on_typewriter_complete
self.typewriter_box.set_text(self.sample_text, animate=True)
self.completion_label.text = "Status: Playing..."
self.completion_label.fill_color = mcrfpy.Color(200, 200, 100)
elif key == "Num2":
# Change instant text
texts = [
"This text appeared instantly. Press 2 to change it to different content.",
"Here's some different content! Text boxes can hold any message you want.",
"The quick brown fox jumps over the lazy dog. Perfect for testing fonts!",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Classic placeholder text.",
]
import random
self.instant_box.set_text(random.choice(texts), animate=False)
elif key == "Num3":
# Skip animation
self.typewriter_box.skip_animation()
self.completion_label.text = "Status: Skipped"
self.completion_label.fill_color = mcrfpy.Color(150, 150, 150)
elif key == "Num4":
# Clear text
self.typewriter_box.clear()
self.completion_label.text = "Status: Cleared"
self.completion_label.fill_color = mcrfpy.Color(150, 150, 150)
elif key == "D":
# Cycle dialogue
self.dialogue_index = (self.dialogue_index + 1) % len(self.dialogues)
speaker, text = self.dialogues[self.dialogue_index]
self.dialogue_box.set_dialogue(speaker, text, animate=True)
def activate(self):
"""Activate the demo scene."""
self.scene.on_key = self.on_key
mcrfpy.current_scene = self.scene
def main():
"""Run the text box demo."""
demo = TextBoxDemo()
demo.activate()
# Headless mode: capture screenshot and exit
try:
if mcrfpy.headless_mode():
from mcrfpy import automation
# Trigger typewriter then screenshot
demo.typewriter_box.set_text(demo.sample_text[:50], animate=False)
mcrfpy.Timer("screenshot", lambda rt: (
automation.screenshot("screenshots/primitives/text_box_demo.png"),
sys.exit(0)
), 100)
except AttributeError:
pass
if __name__ == "__main__":
main()

View file

@ -0,0 +1,237 @@
#!/usr/bin/env python3
"""Toast Notification Demo - Auto-dismissing notification popups
Interactive controls:
1: Show default toast
2: Show success toast (green)
3: Show error toast (red)
4: Show warning toast (yellow)
5: Show info toast (blue)
S: Spam multiple toasts
C: Clear all toasts
ESC: Exit demo
"""
import mcrfpy
import sys
# Add parent to path for imports
sys.path.insert(0, str(__file__).rsplit('/', 2)[0])
from lib.toast import ToastManager
class ToastDemo:
def __init__(self):
self.scene = mcrfpy.Scene("toast_demo")
self.ui = self.scene.children
self.toast_count = 0
self.setup()
def setup(self):
"""Build the demo scene."""
# Background
bg = mcrfpy.Frame(
pos=(0, 0),
size=(1024, 768),
fill_color=mcrfpy.Color(20, 20, 25)
)
self.ui.append(bg)
# Title
title = mcrfpy.Caption(
text="Toast Notification Demo",
pos=(512, 30),
font_size=28,
fill_color=mcrfpy.Color(255, 255, 255)
)
title.outline = 2
title.outline_color = mcrfpy.Color(0, 0, 0)
self.ui.append(title)
# Create toast manager
self.toasts = ToastManager(self.scene, position="top-right", max_toasts=5)
# Instructions panel
panel = mcrfpy.Frame(
pos=(50, 100),
size=(400, 400),
fill_color=mcrfpy.Color(30, 30, 40),
outline_color=mcrfpy.Color(60, 60, 80),
outline=1
)
self.ui.append(panel)
panel_title = mcrfpy.Caption(
text="Toast Types",
pos=(200, 15),
font_size=18,
fill_color=mcrfpy.Color(200, 200, 200)
)
panel.children.append(panel_title)
# Type descriptions
types = [
("1 - Default", "Standard notification", mcrfpy.Color(200, 200, 200)),
("2 - Success", "Confirmation messages", mcrfpy.Color(100, 200, 100)),
("3 - Error", "Error notifications", mcrfpy.Color(200, 100, 100)),
("4 - Warning", "Warning alerts", mcrfpy.Color(200, 180, 80)),
("5 - Info", "Informational messages", mcrfpy.Color(100, 150, 200)),
]
for i, (key, desc, color) in enumerate(types):
y = 50 + i * 50
key_label = mcrfpy.Caption(
text=key,
pos=(20, y),
font_size=16,
fill_color=color
)
panel.children.append(key_label)
desc_label = mcrfpy.Caption(
text=desc,
pos=(20, y + 20),
font_size=12,
fill_color=mcrfpy.Color(150, 150, 150)
)
panel.children.append(desc_label)
# Additional controls
controls = [
("S - Spam", "Show multiple toasts quickly"),
("C - Clear", "Dismiss all active toasts"),
]
for i, (key, desc) in enumerate(controls):
y = 300 + i * 40
key_label = mcrfpy.Caption(
text=key,
pos=(20, y),
font_size=14,
fill_color=mcrfpy.Color(180, 180, 180)
)
panel.children.append(key_label)
desc_label = mcrfpy.Caption(
text=desc,
pos=(20, y + 18),
font_size=12,
fill_color=mcrfpy.Color(120, 120, 120)
)
panel.children.append(desc_label)
# Stats display
self.stats_label = mcrfpy.Caption(
text="Toasts shown: 0 | Active: 0",
pos=(50, 520),
font_size=14,
fill_color=mcrfpy.Color(150, 150, 150)
)
self.ui.append(self.stats_label)
# Preview area
preview_label = mcrfpy.Caption(
text="Toasts appear in the top-right corner ->",
pos=(500, 200),
font_size=16,
fill_color=mcrfpy.Color(100, 100, 100)
)
self.ui.append(preview_label)
arrow = mcrfpy.Caption(
text=">>>",
pos=(750, 200),
font_size=24,
fill_color=mcrfpy.Color(100, 100, 100)
)
self.ui.append(arrow)
# Instructions
instr = mcrfpy.Caption(
text="Press 1-5 to show different toast types | S: Spam | C: Clear all | ESC: Exit",
pos=(50, 730),
font_size=14,
fill_color=mcrfpy.Color(120, 120, 120)
)
self.ui.append(instr)
def update_stats(self):
"""Update the stats display."""
active = len([t for t in self.toasts.toasts if not t.is_dismissed])
self.stats_label.text = f"Toasts shown: {self.toast_count} | Active: {active}"
def on_key(self, key, state):
"""Handle keyboard input."""
if state != "start":
return
if key == "Escape":
sys.exit(0)
elif key == "Num1":
self.toast_count += 1
self.toasts.show(f"Default notification #{self.toast_count}")
self.update_stats()
elif key == "Num2":
self.toast_count += 1
self.toasts.show_success("Operation completed successfully!")
self.update_stats()
elif key == "Num3":
self.toast_count += 1
self.toasts.show_error("An error occurred!")
self.update_stats()
elif key == "Num4":
self.toast_count += 1
self.toasts.show_warning("Warning: Low health!")
self.update_stats()
elif key == "Num5":
self.toast_count += 1
self.toasts.show_info("New quest available")
self.update_stats()
elif key == "S":
# Spam multiple toasts
messages = [
"Game saved!",
"Achievement unlocked!",
"New item acquired!",
"Level up!",
"Quest complete!",
]
for msg in messages:
self.toast_count += 1
self.toasts.show(msg)
self.update_stats()
elif key == "C":
self.toasts.dismiss_all()
self.update_stats()
def activate(self):
"""Activate the demo scene."""
self.scene.on_key = self.on_key
mcrfpy.current_scene = self.scene
def main():
"""Run the toast demo."""
demo = ToastDemo()
demo.activate()
# Headless mode: show some toasts and screenshot
try:
if mcrfpy.headless_mode():
from mcrfpy import automation
# Show a few sample toasts
demo.toasts.show("Game saved!")
demo.toasts.show_success("Achievement unlocked!")
demo.toasts.show_error("Connection lost!")
mcrfpy.Timer("screenshot", lambda rt: (
automation.screenshot("screenshots/primitives/toast_demo.png"),
sys.exit(0)
), 500)
except AttributeError:
pass
if __name__ == "__main__":
main()