McRogueFace/tests/cookbook/lib/toast.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

221 lines
6.9 KiB
Python

# McRogueFace Cookbook - Toast Notification Widget
"""
Auto-dismissing notification popups.
Example:
from lib.toast import ToastManager
# Create manager (once per scene)
toasts = ToastManager(scene)
# Show notifications
toasts.show("Game saved!")
toasts.show("Level up!", duration=5000, color=mcrfpy.Color(100, 200, 100))
"""
import mcrfpy
class Toast:
"""Single toast notification.
Internal class - use ToastManager to create toasts.
"""
DEFAULT_BG = mcrfpy.Color(50, 50, 60, 240)
DEFAULT_TEXT = mcrfpy.Color(255, 255, 255)
def __init__(self, message, y_position, width=300,
bg_color=None, text_color=None, duration=3000):
self.message = message
self.duration = duration
self.y_position = y_position
self.width = width
self._dismissed = False
# Colors
self.bg_color = bg_color or self.DEFAULT_BG
self.text_color = text_color or self.DEFAULT_TEXT
# Create toast frame (starts off-screen to the right)
self.frame = mcrfpy.Frame(
pos=(1024 + 10, y_position), # Start off-screen
size=(width, 40),
fill_color=self.bg_color,
outline_color=mcrfpy.Color(100, 100, 120),
outline=1
)
# Create message caption
self.caption = mcrfpy.Caption(
text=message,
pos=(15, 10),
fill_color=self.text_color,
font_size=14
)
self.frame.children.append(self.caption)
def slide_in(self, target_x):
"""Animate sliding in from the right."""
self.frame.animate("x", target_x, 0.3, mcrfpy.Easing.EASE_OUT)
def slide_out(self, callback=None):
"""Animate sliding out to the right."""
self._dismissed = True
self.frame.animate("x", 1024 + 10, 0.3, mcrfpy.Easing.EASE_IN)
if callback:
mcrfpy.Timer(f"toast_dismiss_{id(self)}", lambda rt: callback(), 350)
def move_up(self, new_y):
"""Animate moving to a new Y position."""
self.y_position = new_y
self.frame.animate("y", new_y, 0.2, mcrfpy.Easing.EASE_OUT)
@property
def is_dismissed(self):
"""Whether this toast has been dismissed."""
return self._dismissed
class ToastManager:
"""Manages auto-dismissing notification popups.
Args:
scene: Scene to add toasts to
position: Anchor position ("top-right", "bottom-right", "top-left", "bottom-left")
max_toasts: Maximum visible toasts (default: 5)
toast_width: Width of toast notifications
toast_spacing: Vertical spacing between toasts
margin: Margin from screen edge
Attributes:
scene: The scene toasts are added to
toasts: List of active toast objects
"""
def __init__(self, scene, position="top-right", max_toasts=5,
toast_width=300, toast_spacing=10, margin=20):
self.scene = scene
self.position = position
self.max_toasts = max_toasts
self.toast_width = toast_width
self.toast_spacing = toast_spacing
self.margin = margin
self.toasts = []
# Calculate anchor position
self._calculate_anchor()
def _calculate_anchor(self):
"""Calculate the anchor point based on position setting."""
# Assuming 1024x768 screen
if "right" in self.position:
self._anchor_x = 1024 - self.toast_width - self.margin
else:
self._anchor_x = self.margin
if "top" in self.position:
self._anchor_y = self.margin
self._direction = 1 # Stack downward
else:
self._anchor_y = 768 - 40 - self.margin # 40 = toast height
self._direction = -1 # Stack upward
def _get_toast_y(self, index):
"""Get Y position for toast at given index."""
return self._anchor_y + index * (40 + self.toast_spacing) * self._direction
def show(self, message, duration=3000, color=None):
"""Show a toast notification.
Args:
message: Text to display
duration: Time in ms before auto-dismiss (0 = never)
color: Optional background color
Returns:
Toast object
"""
# Create new toast
y_pos = self._get_toast_y(len(self.toasts))
toast = Toast(
message=message,
y_position=y_pos,
width=self.toast_width,
bg_color=color,
duration=duration
)
# Add to scene
self.scene.children.append(toast.frame)
self.toasts.append(toast)
# Animate in
toast.slide_in(self._anchor_x)
# Schedule auto-dismiss
if duration > 0:
timer_name = f"toast_auto_{id(toast)}"
mcrfpy.Timer(timer_name, lambda rt: self.dismiss(toast), duration)
# Remove oldest if over limit
while len(self.toasts) > self.max_toasts:
self.dismiss(self.toasts[0])
return toast
def dismiss(self, toast):
"""Dismiss a specific toast.
Args:
toast: Toast object to dismiss
"""
if toast not in self.toasts or toast.is_dismissed:
return
index = self.toasts.index(toast)
def on_dismissed():
# Remove from scene and list
if toast in self.toasts:
self.toasts.remove(toast)
# Try to remove from scene
try:
for i in range(len(self.scene.children)):
if self.scene.children[i] is toast.frame:
self.scene.children.pop()
break
except Exception:
pass
# Move remaining toasts up
self._reposition_toasts()
toast.slide_out(callback=on_dismissed)
def _reposition_toasts(self):
"""Reposition all remaining toasts after one is removed."""
for i, toast in enumerate(self.toasts):
if not toast.is_dismissed:
new_y = self._get_toast_y(i)
toast.move_up(new_y)
def dismiss_all(self):
"""Dismiss all active toasts."""
for toast in list(self.toasts):
self.dismiss(toast)
def show_success(self, message, duration=3000):
"""Show a success toast (green)."""
return self.show(message, duration, mcrfpy.Color(40, 120, 60, 240))
def show_error(self, message, duration=5000):
"""Show an error toast (red)."""
return self.show(message, duration, mcrfpy.Color(150, 50, 50, 240))
def show_warning(self, message, duration=4000):
"""Show a warning toast (yellow)."""
return self.show(message, duration, mcrfpy.Color(180, 150, 40, 240))
def show_info(self, message, duration=3000):
"""Show an info toast (blue)."""
return self.show(message, duration, mcrfpy.Color(50, 100, 150, 240))