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
6
tests/cookbook/primitives/__init__.py
Normal file
6
tests/cookbook/primitives/__init__.py
Normal 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.
|
||||
"""
|
||||
296
tests/cookbook/primitives/demo_button.py
Normal file
296
tests/cookbook/primitives/demo_button.py
Normal 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()
|
||||
287
tests/cookbook/primitives/demo_choice_list.py
Normal file
287
tests/cookbook/primitives/demo_choice_list.py
Normal 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()
|
||||
380
tests/cookbook/primitives/demo_click_pickup.py
Normal file
380
tests/cookbook/primitives/demo_click_pickup.py
Normal 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()
|
||||
270
tests/cookbook/primitives/demo_drag_drop_grid.py
Normal file
270
tests/cookbook/primitives/demo_drag_drop_grid.py
Normal 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()
|
||||
332
tests/cookbook/primitives/demo_stat_bar.py
Normal file
332
tests/cookbook/primitives/demo_stat_bar.py
Normal 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()
|
||||
248
tests/cookbook/primitives/demo_text_box.py
Normal file
248
tests/cookbook/primitives/demo_text_box.py
Normal 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()
|
||||
237
tests/cookbook/primitives/demo_toast.py
Normal file
237
tests/cookbook/primitives/demo_toast.py
Normal 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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue