# 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