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>
221 lines
6.9 KiB
Python
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))
|