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
241
tests/cookbook/lib/button.py
Normal file
241
tests/cookbook/lib/button.py
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
# McRogueFace Cookbook - Button Widget
|
||||
"""
|
||||
Clickable button with hover/press states and visual feedback.
|
||||
|
||||
Example:
|
||||
from lib.button import Button
|
||||
|
||||
def on_click():
|
||||
print("Button clicked!")
|
||||
|
||||
btn = Button("Start Game", pos=(100, 100), callback=on_click)
|
||||
scene.children.append(btn.frame)
|
||||
"""
|
||||
import mcrfpy
|
||||
|
||||
|
||||
class Button:
|
||||
"""Clickable button with hover/press states.
|
||||
|
||||
Args:
|
||||
text: Button label text
|
||||
pos: (x, y) position tuple
|
||||
size: (width, height) tuple, default (120, 40)
|
||||
callback: Function to call on click (no arguments)
|
||||
fill_color: Background color (default: dark gray)
|
||||
hover_color: Color when mouse hovers (default: lighter gray)
|
||||
press_color: Color when pressed (default: even lighter)
|
||||
text_color: Label color (default: white)
|
||||
outline_color: Border color (default: white)
|
||||
outline: Border thickness (default: 2)
|
||||
font_size: Text size (default: 16)
|
||||
enabled: Whether button is clickable (default: True)
|
||||
|
||||
Attributes:
|
||||
frame: The underlying mcrfpy.Frame (add this to scene)
|
||||
label: The mcrfpy.Caption for the text
|
||||
is_hovered: True if mouse is over button
|
||||
is_pressed: True if button is being pressed
|
||||
enabled: Whether button responds to clicks
|
||||
"""
|
||||
|
||||
# Default colors
|
||||
DEFAULT_FILL = mcrfpy.Color(60, 60, 70)
|
||||
DEFAULT_HOVER = mcrfpy.Color(80, 80, 95)
|
||||
DEFAULT_PRESS = mcrfpy.Color(100, 100, 120)
|
||||
DEFAULT_DISABLED = mcrfpy.Color(40, 40, 45)
|
||||
DEFAULT_TEXT = mcrfpy.Color(255, 255, 255)
|
||||
DEFAULT_TEXT_DISABLED = mcrfpy.Color(120, 120, 120)
|
||||
DEFAULT_OUTLINE = mcrfpy.Color(200, 200, 210)
|
||||
|
||||
def __init__(self, text, pos, size=(120, 40), callback=None,
|
||||
fill_color=None, hover_color=None, press_color=None,
|
||||
text_color=None, outline_color=None, outline=2,
|
||||
font_size=16, enabled=True):
|
||||
self.text = text
|
||||
self.pos = pos
|
||||
self.size = size
|
||||
self.callback = callback
|
||||
self.font_size = font_size
|
||||
self._enabled = enabled
|
||||
|
||||
# Store colors
|
||||
self.fill_color = fill_color or self.DEFAULT_FILL
|
||||
self.hover_color = hover_color or self.DEFAULT_HOVER
|
||||
self.press_color = press_color or self.DEFAULT_PRESS
|
||||
self.text_color = text_color or self.DEFAULT_TEXT
|
||||
self.outline_color = outline_color or self.DEFAULT_OUTLINE
|
||||
|
||||
# State tracking
|
||||
self.is_hovered = False
|
||||
self.is_pressed = False
|
||||
|
||||
# Create the frame
|
||||
self.frame = mcrfpy.Frame(
|
||||
pos=pos,
|
||||
size=size,
|
||||
fill_color=self.fill_color,
|
||||
outline_color=self.outline_color,
|
||||
outline=outline
|
||||
)
|
||||
|
||||
# Create the label (centered in frame)
|
||||
self.label = mcrfpy.Caption(
|
||||
text=text,
|
||||
pos=(size[0] / 2, size[1] / 2 - font_size / 2),
|
||||
fill_color=self.text_color,
|
||||
font_size=font_size
|
||||
)
|
||||
self.frame.children.append(self.label)
|
||||
|
||||
# Set up event handlers
|
||||
self.frame.on_click = self._on_click
|
||||
self.frame.on_enter = self._on_enter
|
||||
self.frame.on_exit = self._on_exit
|
||||
|
||||
# Apply initial state
|
||||
if not enabled:
|
||||
self._apply_disabled_style()
|
||||
|
||||
def _on_click(self, pos, button, action):
|
||||
"""Handle click events."""
|
||||
if not self._enabled:
|
||||
return
|
||||
|
||||
if button == "left":
|
||||
if action == "start":
|
||||
self.is_pressed = True
|
||||
self.frame.fill_color = self.press_color
|
||||
# Animate a subtle press effect
|
||||
self._animate_press()
|
||||
elif action == "end":
|
||||
self.is_pressed = False
|
||||
# Restore hover or normal state
|
||||
if self.is_hovered:
|
||||
self.frame.fill_color = self.hover_color
|
||||
else:
|
||||
self.frame.fill_color = self.fill_color
|
||||
# Trigger callback on release if still over button
|
||||
if self.is_hovered and self.callback:
|
||||
self.callback()
|
||||
|
||||
def _on_enter(self, pos):
|
||||
"""Handle mouse enter.
|
||||
|
||||
Note: #230 - on_enter now only receives position, not button/action
|
||||
"""
|
||||
if not self._enabled:
|
||||
return
|
||||
self.is_hovered = True
|
||||
if not self.is_pressed:
|
||||
self.frame.fill_color = self.hover_color
|
||||
|
||||
def _on_exit(self, pos):
|
||||
"""Handle mouse exit.
|
||||
|
||||
Note: #230 - on_exit now only receives position, not button/action
|
||||
"""
|
||||
if not self._enabled:
|
||||
return
|
||||
self.is_hovered = False
|
||||
self.is_pressed = False
|
||||
self.frame.fill_color = self.fill_color
|
||||
|
||||
def _animate_press(self):
|
||||
"""Animate a subtle scale bounce on press."""
|
||||
# Small scale down then back up
|
||||
# Note: If scale animation is not available, this is a no-op
|
||||
try:
|
||||
# Animate origin for a press effect
|
||||
original_x = self.frame.x
|
||||
original_y = self.frame.y
|
||||
# Quick bounce using position
|
||||
self.frame.animate("x", original_x + 2, 0.05, mcrfpy.Easing.EASE_OUT)
|
||||
self.frame.animate("y", original_y + 2, 0.05, mcrfpy.Easing.EASE_OUT)
|
||||
except Exception:
|
||||
pass # Animation not critical
|
||||
|
||||
def _apply_disabled_style(self):
|
||||
"""Apply disabled visual style."""
|
||||
self.frame.fill_color = self.DEFAULT_DISABLED
|
||||
self.label.fill_color = self.DEFAULT_TEXT_DISABLED
|
||||
|
||||
def _apply_enabled_style(self):
|
||||
"""Restore enabled visual style."""
|
||||
self.frame.fill_color = self.fill_color
|
||||
self.label.fill_color = self.text_color
|
||||
|
||||
@property
|
||||
def enabled(self):
|
||||
"""Whether the button is clickable."""
|
||||
return self._enabled
|
||||
|
||||
@enabled.setter
|
||||
def enabled(self, value):
|
||||
"""Enable or disable the button."""
|
||||
self._enabled = value
|
||||
if value:
|
||||
self._apply_enabled_style()
|
||||
else:
|
||||
self._apply_disabled_style()
|
||||
self.is_hovered = False
|
||||
self.is_pressed = False
|
||||
|
||||
def set_text(self, text):
|
||||
"""Change the button label."""
|
||||
self.text = text
|
||||
self.label.text = text
|
||||
|
||||
def set_callback(self, callback):
|
||||
"""Change the click callback."""
|
||||
self.callback = callback
|
||||
|
||||
|
||||
def create_button_row(labels, start_pos, spacing=10, size=(120, 40), callbacks=None):
|
||||
"""Create a horizontal row of buttons.
|
||||
|
||||
Args:
|
||||
labels: List of button labels
|
||||
start_pos: (x, y) position of first button
|
||||
spacing: Pixels between buttons
|
||||
size: (width, height) for all buttons
|
||||
callbacks: List of callbacks (or None for no callbacks)
|
||||
|
||||
Returns:
|
||||
List of Button objects
|
||||
"""
|
||||
buttons = []
|
||||
x, y = start_pos
|
||||
callbacks = callbacks or [None] * len(labels)
|
||||
|
||||
for label, callback in zip(labels, callbacks):
|
||||
btn = Button(label, pos=(x, y), size=size, callback=callback)
|
||||
buttons.append(btn)
|
||||
x += size[0] + spacing
|
||||
|
||||
return buttons
|
||||
|
||||
|
||||
def create_button_column(labels, start_pos, spacing=10, size=(120, 40), callbacks=None):
|
||||
"""Create a vertical column of buttons.
|
||||
|
||||
Args:
|
||||
labels: List of button labels
|
||||
start_pos: (x, y) position of first button
|
||||
spacing: Pixels between buttons
|
||||
size: (width, height) for all buttons
|
||||
callbacks: List of callbacks (or None for no callbacks)
|
||||
|
||||
Returns:
|
||||
List of Button objects
|
||||
"""
|
||||
buttons = []
|
||||
x, y = start_pos
|
||||
callbacks = callbacks or [None] * len(labels)
|
||||
|
||||
for label, callback in zip(labels, callbacks):
|
||||
btn = Button(label, pos=(x, y), size=size, callback=callback)
|
||||
buttons.append(btn)
|
||||
y += size[1] + spacing
|
||||
|
||||
return buttons
|
||||
Loading…
Add table
Add a link
Reference in a new issue