McRogueFace/tests/cookbook/lib/button.py
John McCardle 55f6ea9502 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>
2026-01-28 18:58:25 -05:00

241 lines
7.7 KiB
Python

# 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