feat: Add focus system demo for #143
Implements a comprehensive Python-level focus management system showing: - FocusManager: central coordinator for keyboard routing, tab cycling, modal stack - ModifierTracker: workaround for tracking Shift/Ctrl/Alt state (#160) - FocusableGrid: WASD movement in a grid with player marker - TextInputWidget: text entry with cursor, backspace, home/end - MenuIcon: icons that open modal dialogs on Space/Enter Features demonstrated: - Click-to-focus on any widget - Tab/Shift+Tab cycling through focusable widgets - Visual focus indicators (blue outline) - Keyboard routing to focused widget - Modal dialog push/pop stack - Escape to close modals Addresses #143 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
89986323f8
commit
b6ec0fe7ab
1 changed files with 810 additions and 0 deletions
810
tests/demo/screens/focus_system_demo.py
Normal file
810
tests/demo/screens/focus_system_demo.py
Normal file
|
|
@ -0,0 +1,810 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Focus System Demo for McRogueFace
|
||||
|
||||
Demonstrates a Python-level focus management system using engine primitives.
|
||||
This shows how game developers can implement keyboard navigation without
|
||||
requiring C++ engine changes.
|
||||
|
||||
Features demonstrated:
|
||||
- Click-to-focus
|
||||
- Tab/Shift+Tab cycling
|
||||
- Visual focus indicators
|
||||
- Keyboard routing to focused widget
|
||||
- Modal focus stack
|
||||
- Three widget types: Grid (WASD), TextInput, MenuIcon
|
||||
|
||||
Issue: #143
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
# =============================================================================
|
||||
# Modifier Key Tracker (workaround until #160 is implemented)
|
||||
# =============================================================================
|
||||
|
||||
class ModifierTracker:
|
||||
"""Tracks modifier key state since engine doesn't expose this yet."""
|
||||
|
||||
def __init__(self):
|
||||
self.shift = False
|
||||
self.ctrl = False
|
||||
self.alt = False
|
||||
|
||||
def update(self, key: str, action: str):
|
||||
"""Call this from your key handler to update modifier state."""
|
||||
if key in ("LShift", "RShift"):
|
||||
self.shift = (action == "start")
|
||||
elif key in ("LControl", "RControl"):
|
||||
self.ctrl = (action == "start")
|
||||
elif key in ("LAlt", "RAlt"):
|
||||
self.alt = (action == "start")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Focus Manager
|
||||
# =============================================================================
|
||||
|
||||
class FocusManager:
|
||||
"""Central focus coordinator for a scene.
|
||||
|
||||
Manages which widget receives keyboard input, handles tab cycling,
|
||||
and maintains a modal stack for popup dialogs.
|
||||
"""
|
||||
|
||||
# Focus indicator colors
|
||||
FOCUS_COLOR = mcrfpy.Color(0, 150, 255) # Blue
|
||||
UNFOCUS_COLOR = mcrfpy.Color(80, 80, 80) # Dark gray
|
||||
FOCUS_OUTLINE = 3.0
|
||||
UNFOCUS_OUTLINE = 1.0
|
||||
|
||||
def __init__(self):
|
||||
self.widgets = [] # List of (widget, focusable: bool)
|
||||
self.focus_index = -1 # Currently focused widget index
|
||||
self.modal_stack = [] # Stack of (modal_frame, previous_focus_index)
|
||||
self.modifiers = ModifierTracker()
|
||||
|
||||
def register(self, widget, focusable: bool = True):
|
||||
"""Add a widget to the focus order.
|
||||
|
||||
Args:
|
||||
widget: Object implementing on_focus(), on_blur(), handle_key()
|
||||
focusable: Whether this widget can receive focus via Tab
|
||||
"""
|
||||
self.widgets.append((widget, focusable))
|
||||
# Give widget a reference back to us for click-to-focus
|
||||
widget._focus_manager = self
|
||||
widget._focus_index = len(self.widgets) - 1
|
||||
|
||||
def focus(self, widget_or_index):
|
||||
"""Set focus to a specific widget."""
|
||||
# Resolve to index
|
||||
if isinstance(widget_or_index, int):
|
||||
new_index = widget_or_index
|
||||
else:
|
||||
new_index = next(
|
||||
(i for i, (w, _) in enumerate(self.widgets) if w is widget_or_index),
|
||||
-1
|
||||
)
|
||||
|
||||
if new_index < 0 or new_index >= len(self.widgets):
|
||||
return
|
||||
|
||||
# Blur old widget
|
||||
if 0 <= self.focus_index < len(self.widgets):
|
||||
old_widget, _ = self.widgets[self.focus_index]
|
||||
if hasattr(old_widget, 'on_blur'):
|
||||
old_widget.on_blur()
|
||||
|
||||
# Focus new widget
|
||||
self.focus_index = new_index
|
||||
new_widget, _ = self.widgets[new_index]
|
||||
if hasattr(new_widget, 'on_focus'):
|
||||
new_widget.on_focus()
|
||||
|
||||
def cycle(self, direction: int = 1):
|
||||
"""Cycle focus to next/previous focusable widget.
|
||||
|
||||
Args:
|
||||
direction: 1 for next (Tab), -1 for previous (Shift+Tab)
|
||||
"""
|
||||
if not self.widgets:
|
||||
return
|
||||
|
||||
start = self.focus_index if self.focus_index >= 0 else 0
|
||||
current = start
|
||||
|
||||
for _ in range(len(self.widgets)):
|
||||
current = (current + direction) % len(self.widgets)
|
||||
widget, focusable = self.widgets[current]
|
||||
if focusable:
|
||||
self.focus(current)
|
||||
return
|
||||
|
||||
# No focusable widget found, stay where we are
|
||||
|
||||
def push_modal(self, modal_frame, first_focus_widget=None):
|
||||
"""Push a modal onto the focus stack.
|
||||
|
||||
Args:
|
||||
modal_frame: The Frame to show as modal
|
||||
first_focus_widget: Widget to focus inside modal (optional)
|
||||
"""
|
||||
# Save current focus
|
||||
self.modal_stack.append((modal_frame, self.focus_index))
|
||||
|
||||
# Show modal
|
||||
modal_frame.visible = True
|
||||
|
||||
# Focus first widget in modal if specified
|
||||
if first_focus_widget is not None:
|
||||
self.focus(first_focus_widget)
|
||||
|
||||
def pop_modal(self):
|
||||
"""Pop the top modal and restore previous focus."""
|
||||
if not self.modal_stack:
|
||||
return False
|
||||
|
||||
modal_frame, previous_focus = self.modal_stack.pop()
|
||||
modal_frame.visible = False
|
||||
|
||||
# Restore focus
|
||||
if previous_focus >= 0:
|
||||
self.focus(previous_focus)
|
||||
|
||||
return True
|
||||
|
||||
def handle_key(self, key: str, action: str) -> bool:
|
||||
"""Main key handler - route to focused widget or handle global keys.
|
||||
|
||||
Returns True if key was consumed.
|
||||
"""
|
||||
# Always update modifier state
|
||||
self.modifiers.update(key, action)
|
||||
|
||||
# Only process on key press, not release (key repeat sends multiple "start")
|
||||
if action != "start":
|
||||
return False
|
||||
|
||||
# Global: Escape closes modals
|
||||
if key == "Escape":
|
||||
if self.pop_modal():
|
||||
return True
|
||||
|
||||
# Global: Tab cycles focus
|
||||
if key == "Tab":
|
||||
direction = -1 if self.modifiers.shift else 1
|
||||
self.cycle(direction)
|
||||
return True
|
||||
|
||||
# Route to focused widget
|
||||
if 0 <= self.focus_index < len(self.widgets):
|
||||
widget, _ = self.widgets[self.focus_index]
|
||||
if hasattr(widget, 'handle_key'):
|
||||
if widget.handle_key(key, action):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Focusable Widgets
|
||||
# =============================================================================
|
||||
|
||||
class FocusableGrid:
|
||||
"""A grid where WASD keys move a player entity.
|
||||
|
||||
Demonstrates focus on a game-world element.
|
||||
"""
|
||||
|
||||
def __init__(self, x: float, y: float, grid_w: int, grid_h: int,
|
||||
tile_size: int = 16, zoom: float = 2.0):
|
||||
self.grid_w = grid_w
|
||||
self.grid_h = grid_h
|
||||
self.tile_size = tile_size
|
||||
self.zoom = zoom
|
||||
self.base_x = x
|
||||
self.base_y = y
|
||||
|
||||
# Calculate pixel dimensions
|
||||
self.cell_px = tile_size * zoom # Pixels per cell
|
||||
grid_pixel_w = grid_w * self.cell_px
|
||||
grid_pixel_h = grid_h * self.cell_px
|
||||
|
||||
# Create the grid background
|
||||
self.grid = mcrfpy.Grid(
|
||||
pos=(x, y),
|
||||
grid_size=(grid_w, grid_h),
|
||||
size=(grid_pixel_w, grid_pixel_h)
|
||||
)
|
||||
self.grid.zoom = zoom
|
||||
self.grid.fill_color = mcrfpy.Color(40, 40, 55)
|
||||
|
||||
# Add outline frame for focus indication
|
||||
self.outline_frame = mcrfpy.Frame(
|
||||
pos=(x - 2, y - 2),
|
||||
size=(grid_pixel_w + 4, grid_pixel_h + 4),
|
||||
fill_color=mcrfpy.Color(0, 0, 0, 0),
|
||||
outline_color=FocusManager.UNFOCUS_COLOR,
|
||||
outline=FocusManager.UNFOCUS_OUTLINE
|
||||
)
|
||||
|
||||
# Player marker (a bright square overlay)
|
||||
self.player_x = grid_w // 2
|
||||
self.player_y = grid_h // 2
|
||||
marker_size = self.cell_px - 4 # Slightly smaller than cell
|
||||
self.player_marker = mcrfpy.Frame(
|
||||
pos=(0, 0), # Will be positioned by _update_player_display
|
||||
size=(marker_size, marker_size),
|
||||
fill_color=mcrfpy.Color(255, 200, 50),
|
||||
outline_color=mcrfpy.Color(255, 150, 0),
|
||||
outline=2
|
||||
)
|
||||
self._update_player_display()
|
||||
|
||||
# Click handler
|
||||
self.grid.on_click = self._on_click
|
||||
|
||||
# Focus manager reference (set by FocusManager.register)
|
||||
self._focus_manager = None
|
||||
self._focus_index = -1
|
||||
|
||||
def _on_click(self, x, y, button, action):
|
||||
"""Handle click to focus this grid."""
|
||||
if self._focus_manager and action == "start":
|
||||
self._focus_manager.focus(self._focus_index)
|
||||
|
||||
def _update_player_display(self):
|
||||
"""Update the visual representation of player position."""
|
||||
# Position the player marker
|
||||
px = self.base_x + (self.player_x * self.cell_px) + 2
|
||||
py = self.base_y + (self.player_y * self.cell_px) + 2
|
||||
self.player_marker.x = px
|
||||
self.player_marker.y = py
|
||||
|
||||
def on_focus(self):
|
||||
"""Called when this widget gains focus."""
|
||||
self.outline_frame.outline_color = FocusManager.FOCUS_COLOR
|
||||
self.outline_frame.outline = FocusManager.FOCUS_OUTLINE
|
||||
|
||||
def on_blur(self):
|
||||
"""Called when this widget loses focus."""
|
||||
self.outline_frame.outline_color = FocusManager.UNFOCUS_COLOR
|
||||
self.outline_frame.outline = FocusManager.UNFOCUS_OUTLINE
|
||||
|
||||
def handle_key(self, key: str, action: str) -> bool:
|
||||
"""Handle WASD movement."""
|
||||
moves = {
|
||||
"W": (0, -1), "Up": (0, -1),
|
||||
"A": (-1, 0), "Left": (-1, 0),
|
||||
"S": (0, 1), "Down": (0, 1),
|
||||
"D": (1, 0), "Right": (1, 0),
|
||||
}
|
||||
|
||||
if key in moves:
|
||||
dx, dy = moves[key]
|
||||
new_x = self.player_x + dx
|
||||
new_y = self.player_y + dy
|
||||
|
||||
# Bounds check
|
||||
if 0 <= new_x < self.grid_w and 0 <= new_y < self.grid_h:
|
||||
self.player_x = new_x
|
||||
self.player_y = new_y
|
||||
self._update_player_display()
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def add_to_scene(self, ui):
|
||||
"""Add all components to a scene's UI collection."""
|
||||
ui.append(self.outline_frame)
|
||||
ui.append(self.grid)
|
||||
ui.append(self.player_marker)
|
||||
|
||||
|
||||
class TextInputWidget:
|
||||
"""A text input field with cursor and editing.
|
||||
|
||||
Demonstrates text entry with focus indication.
|
||||
"""
|
||||
|
||||
def __init__(self, x: float, y: float, width: float, label: str = "",
|
||||
placeholder: str = ""):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.width = width
|
||||
self.height = 28
|
||||
self.label_text = label
|
||||
self.placeholder_text = placeholder
|
||||
|
||||
# State
|
||||
self.text = ""
|
||||
self.cursor_pos = 0
|
||||
self.focused = False
|
||||
|
||||
# Create UI elements
|
||||
self._create_ui()
|
||||
|
||||
# Focus manager reference
|
||||
self._focus_manager = None
|
||||
self._focus_index = -1
|
||||
|
||||
def _create_ui(self):
|
||||
"""Create the visual components."""
|
||||
# Label above input
|
||||
if self.label_text:
|
||||
self.label = mcrfpy.Caption(
|
||||
text=self.label_text,
|
||||
pos=(self.x, self.y - 20)
|
||||
)
|
||||
self.label.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
|
||||
# Input background
|
||||
self.frame = mcrfpy.Frame(
|
||||
pos=(self.x, self.y),
|
||||
size=(self.width, self.height),
|
||||
fill_color=mcrfpy.Color(40, 40, 50),
|
||||
outline_color=FocusManager.UNFOCUS_COLOR,
|
||||
outline=FocusManager.UNFOCUS_OUTLINE
|
||||
)
|
||||
self.frame.on_click = self._on_click
|
||||
|
||||
# Placeholder text
|
||||
self.placeholder = mcrfpy.Caption(
|
||||
text=self.placeholder_text,
|
||||
pos=(self.x + 6, self.y + 5)
|
||||
)
|
||||
self.placeholder.fill_color = mcrfpy.Color(100, 100, 100)
|
||||
|
||||
# Actual text display
|
||||
self.display = mcrfpy.Caption(
|
||||
text="",
|
||||
pos=(self.x + 6, self.y + 5)
|
||||
)
|
||||
self.display.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
|
||||
# Cursor (thin frame)
|
||||
self.cursor = mcrfpy.Frame(
|
||||
pos=(self.x + 6, self.y + 4),
|
||||
size=(2, self.height - 8),
|
||||
fill_color=mcrfpy.Color(255, 255, 255)
|
||||
)
|
||||
self.cursor.visible = False
|
||||
|
||||
def _on_click(self, x, y, button, action):
|
||||
"""Handle click to focus."""
|
||||
if self._focus_manager and action == "start":
|
||||
self._focus_manager.focus(self._focus_index)
|
||||
|
||||
def _update_display(self):
|
||||
"""Update visual state."""
|
||||
self.display.text = self.text
|
||||
self.placeholder.visible = (not self.text and not self.focused)
|
||||
self._update_cursor()
|
||||
|
||||
def _update_cursor(self):
|
||||
"""Update cursor position."""
|
||||
# Approximate character width (monospace assumption)
|
||||
char_width = 10
|
||||
self.cursor.x = self.x + 6 + (self.cursor_pos * char_width)
|
||||
|
||||
def on_focus(self):
|
||||
"""Called when gaining focus."""
|
||||
self.focused = True
|
||||
self.frame.outline_color = FocusManager.FOCUS_COLOR
|
||||
self.frame.outline = FocusManager.FOCUS_OUTLINE
|
||||
self.cursor.visible = True
|
||||
self._update_display()
|
||||
|
||||
def on_blur(self):
|
||||
"""Called when losing focus."""
|
||||
self.focused = False
|
||||
self.frame.outline_color = FocusManager.UNFOCUS_COLOR
|
||||
self.frame.outline = FocusManager.UNFOCUS_OUTLINE
|
||||
self.cursor.visible = False
|
||||
self._update_display()
|
||||
|
||||
def handle_key(self, key: str, action: str) -> bool:
|
||||
"""Handle text input and editing keys."""
|
||||
if not self.focused:
|
||||
return False
|
||||
|
||||
old_text = self.text
|
||||
handled = True
|
||||
|
||||
if key == "BackSpace":
|
||||
if self.cursor_pos > 0:
|
||||
self.text = self.text[:self.cursor_pos-1] + self.text[self.cursor_pos:]
|
||||
self.cursor_pos -= 1
|
||||
elif key == "Delete":
|
||||
if self.cursor_pos < len(self.text):
|
||||
self.text = self.text[:self.cursor_pos] + self.text[self.cursor_pos+1:]
|
||||
elif key == "Left":
|
||||
self.cursor_pos = max(0, self.cursor_pos - 1)
|
||||
elif key == "Right":
|
||||
self.cursor_pos = min(len(self.text), self.cursor_pos + 1)
|
||||
elif key == "Home":
|
||||
self.cursor_pos = 0
|
||||
elif key == "End":
|
||||
self.cursor_pos = len(self.text)
|
||||
elif key in ("Return", "Tab"):
|
||||
# Don't consume - let focus manager handle
|
||||
handled = False
|
||||
elif len(key) == 1 and key.isprintable():
|
||||
# Insert character
|
||||
self.text = self.text[:self.cursor_pos] + key + self.text[self.cursor_pos:]
|
||||
self.cursor_pos += 1
|
||||
else:
|
||||
handled = False
|
||||
|
||||
self._update_display()
|
||||
return handled
|
||||
|
||||
def get_text(self) -> str:
|
||||
"""Get the current text value."""
|
||||
return self.text
|
||||
|
||||
def set_text(self, text: str):
|
||||
"""Set the text value."""
|
||||
self.text = text
|
||||
self.cursor_pos = len(text)
|
||||
self._update_display()
|
||||
|
||||
def add_to_scene(self, ui):
|
||||
"""Add all components to the scene."""
|
||||
if hasattr(self, 'label'):
|
||||
ui.append(self.label)
|
||||
ui.append(self.frame)
|
||||
ui.append(self.placeholder)
|
||||
ui.append(self.display)
|
||||
ui.append(self.cursor)
|
||||
|
||||
|
||||
class MenuIcon:
|
||||
"""An icon that opens a modal dialog when activated.
|
||||
|
||||
Demonstrates activation via Space/Enter and modal focus.
|
||||
"""
|
||||
|
||||
def __init__(self, x: float, y: float, size: float, icon_char: str,
|
||||
tooltip: str, modal_content_builder=None):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.size = size
|
||||
self.tooltip = tooltip
|
||||
self.modal_content_builder = modal_content_builder
|
||||
self.modal = None
|
||||
|
||||
# Create icon frame
|
||||
self.frame = mcrfpy.Frame(
|
||||
pos=(x, y),
|
||||
size=(size, size),
|
||||
fill_color=mcrfpy.Color(60, 60, 80),
|
||||
outline_color=FocusManager.UNFOCUS_COLOR,
|
||||
outline=FocusManager.UNFOCUS_OUTLINE
|
||||
)
|
||||
self.frame.on_click = self._on_click
|
||||
|
||||
# Icon character (centered)
|
||||
self.icon = mcrfpy.Caption(
|
||||
text=icon_char,
|
||||
pos=(x + size//3, y + size//6)
|
||||
)
|
||||
self.icon.fill_color = mcrfpy.Color(200, 200, 220)
|
||||
|
||||
# Tooltip (shown on hover/focus)
|
||||
self.tooltip_caption = mcrfpy.Caption(
|
||||
text=tooltip,
|
||||
pos=(x, y + size + 4)
|
||||
)
|
||||
self.tooltip_caption.fill_color = mcrfpy.Color(150, 150, 150)
|
||||
self.tooltip_caption.visible = False
|
||||
|
||||
# Focus manager reference
|
||||
self._focus_manager = None
|
||||
self._focus_index = -1
|
||||
|
||||
def _on_click(self, x, y, button, action):
|
||||
"""Handle click to focus or activate."""
|
||||
if not self._focus_manager:
|
||||
return
|
||||
|
||||
if action == "start":
|
||||
# If already focused, activate; otherwise just focus
|
||||
if self._focus_manager.focus_index == self._focus_index:
|
||||
self._activate()
|
||||
else:
|
||||
self._focus_manager.focus(self._focus_index)
|
||||
|
||||
def _activate(self):
|
||||
"""Open the modal dialog."""
|
||||
if self.modal and self._focus_manager:
|
||||
self._focus_manager.push_modal(self.modal)
|
||||
|
||||
def on_focus(self):
|
||||
"""Called when gaining focus."""
|
||||
self.frame.outline_color = FocusManager.FOCUS_COLOR
|
||||
self.frame.outline = FocusManager.FOCUS_OUTLINE
|
||||
self.frame.fill_color = mcrfpy.Color(80, 80, 110)
|
||||
self.tooltip_caption.visible = True
|
||||
|
||||
def on_blur(self):
|
||||
"""Called when losing focus."""
|
||||
self.frame.outline_color = FocusManager.UNFOCUS_COLOR
|
||||
self.frame.outline = FocusManager.UNFOCUS_OUTLINE
|
||||
self.frame.fill_color = mcrfpy.Color(60, 60, 80)
|
||||
self.tooltip_caption.visible = False
|
||||
|
||||
def handle_key(self, key: str, action: str) -> bool:
|
||||
"""Handle activation keys."""
|
||||
if key in ("Space", "Return"):
|
||||
self._activate()
|
||||
return True
|
||||
return False
|
||||
|
||||
def set_modal(self, modal_frame):
|
||||
"""Set the modal frame this icon opens."""
|
||||
self.modal = modal_frame
|
||||
|
||||
def add_to_scene(self, ui):
|
||||
"""Add all components to the scene."""
|
||||
ui.append(self.frame)
|
||||
ui.append(self.icon)
|
||||
ui.append(self.tooltip_caption)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Modal Dialog Builder
|
||||
# =============================================================================
|
||||
|
||||
def create_modal(x: float, y: float, width: float, height: float,
|
||||
title: str) -> mcrfpy.Frame:
|
||||
"""Create a modal dialog frame."""
|
||||
# Semi-transparent backdrop
|
||||
# Note: This is simplified - real implementation might want fullscreen backdrop
|
||||
|
||||
# Modal frame
|
||||
modal = mcrfpy.Frame(
|
||||
pos=(x, y),
|
||||
size=(width, height),
|
||||
fill_color=mcrfpy.Color(40, 40, 50),
|
||||
outline_color=mcrfpy.Color(100, 100, 120),
|
||||
outline=2
|
||||
)
|
||||
modal.visible = False
|
||||
|
||||
# Title
|
||||
title_caption = mcrfpy.Caption(
|
||||
text=title,
|
||||
pos=(x + 10, y + 8)
|
||||
)
|
||||
title_caption.fill_color = mcrfpy.Color(220, 220, 240)
|
||||
modal.children.append(title_caption)
|
||||
|
||||
# Close hint
|
||||
close_hint = mcrfpy.Caption(
|
||||
text="[Esc to close]",
|
||||
pos=(x + width - 100, y + 8)
|
||||
)
|
||||
close_hint.fill_color = mcrfpy.Color(120, 120, 140)
|
||||
modal.children.append(close_hint)
|
||||
|
||||
return modal
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Demo Scene Setup
|
||||
# =============================================================================
|
||||
|
||||
def create_demo_scene():
|
||||
"""Create and populate the focus system demo scene."""
|
||||
|
||||
# Create scene
|
||||
mcrfpy.createScene("focus_demo")
|
||||
ui = mcrfpy.sceneUI("focus_demo")
|
||||
|
||||
# Background
|
||||
bg = mcrfpy.Frame(
|
||||
pos=(0, 0),
|
||||
size=(1024, 768),
|
||||
fill_color=mcrfpy.Color(25, 25, 35)
|
||||
)
|
||||
ui.append(bg)
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption(
|
||||
text="Focus System Demo",
|
||||
pos=(20, 15)
|
||||
)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
ui.append(title)
|
||||
|
||||
# Instructions
|
||||
instructions = mcrfpy.Caption(
|
||||
text="Tab: cycle focus | Shift+Tab: reverse | WASD: move in grid | Space/Enter: activate | Esc: close modal",
|
||||
pos=(20, 45)
|
||||
)
|
||||
instructions.fill_color = mcrfpy.Color(150, 150, 170)
|
||||
ui.append(instructions)
|
||||
|
||||
# Create focus manager
|
||||
focus_mgr = FocusManager()
|
||||
|
||||
# --- Grid Section ---
|
||||
grid_label = mcrfpy.Caption(text="Game Grid (WASD to move)", pos=(50, 90))
|
||||
grid_label.fill_color = mcrfpy.Color(180, 180, 200)
|
||||
ui.append(grid_label)
|
||||
|
||||
grid_widget = FocusableGrid(50, 115, 10, 8, tile_size=16, zoom=2.0)
|
||||
grid_widget.add_to_scene(ui)
|
||||
focus_mgr.register(grid_widget)
|
||||
|
||||
# --- Text Inputs Section ---
|
||||
input_label = mcrfpy.Caption(text="Text Inputs", pos=(400, 90))
|
||||
input_label.fill_color = mcrfpy.Color(180, 180, 200)
|
||||
ui.append(input_label)
|
||||
|
||||
name_input = TextInputWidget(400, 130, 250, label="Name:", placeholder="Enter your name")
|
||||
name_input.add_to_scene(ui)
|
||||
focus_mgr.register(name_input)
|
||||
|
||||
class_input = TextInputWidget(400, 200, 250, label="Class:", placeholder="e.g. Warrior, Mage")
|
||||
class_input.add_to_scene(ui)
|
||||
focus_mgr.register(class_input)
|
||||
|
||||
notes_input = TextInputWidget(400, 270, 350, label="Notes:", placeholder="Additional notes...")
|
||||
notes_input.add_to_scene(ui)
|
||||
focus_mgr.register(notes_input)
|
||||
|
||||
# --- Menu Icons Section ---
|
||||
icons_label = mcrfpy.Caption(text="Menu Icons", pos=(50, 390))
|
||||
icons_label.fill_color = mcrfpy.Color(180, 180, 200)
|
||||
ui.append(icons_label)
|
||||
|
||||
# Help icon
|
||||
help_icon = MenuIcon(50, 420, 48, "?", "Help")
|
||||
help_icon.add_to_scene(ui)
|
||||
focus_mgr.register(help_icon)
|
||||
|
||||
help_modal = create_modal(200, 150, 400, 300, "Help")
|
||||
ui.append(help_modal)
|
||||
help_text = mcrfpy.Caption(
|
||||
text="This demo shows focus management.\n\nUse Tab to move between widgets.\nWASD moves the player in the grid.\nType in text fields.\nPress Space on icons to open dialogs.",
|
||||
pos=(210, 190)
|
||||
)
|
||||
help_text.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
help_modal.children.append(help_text)
|
||||
help_icon.set_modal(help_modal)
|
||||
|
||||
# Settings icon
|
||||
settings_icon = MenuIcon(110, 420, 48, "S", "Settings")
|
||||
settings_icon.add_to_scene(ui)
|
||||
focus_mgr.register(settings_icon)
|
||||
|
||||
settings_modal = create_modal(200, 150, 400, 250, "Settings")
|
||||
ui.append(settings_modal)
|
||||
settings_text = mcrfpy.Caption(
|
||||
text="Settings would go here.\n\n(This is a placeholder modal)",
|
||||
pos=(210, 190)
|
||||
)
|
||||
settings_text.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
settings_modal.children.append(settings_text)
|
||||
settings_icon.set_modal(settings_modal)
|
||||
|
||||
# Inventory icon
|
||||
inv_icon = MenuIcon(170, 420, 48, "I", "Inventory")
|
||||
inv_icon.add_to_scene(ui)
|
||||
focus_mgr.register(inv_icon)
|
||||
|
||||
inv_modal = create_modal(200, 150, 400, 300, "Inventory")
|
||||
ui.append(inv_modal)
|
||||
inv_text = mcrfpy.Caption(
|
||||
text="Your inventory:\n\n- Sword\n- Shield\n- 3x Potions",
|
||||
pos=(210, 190)
|
||||
)
|
||||
inv_text.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
inv_modal.children.append(inv_text)
|
||||
inv_icon.set_modal(inv_modal)
|
||||
|
||||
# --- Status Display ---
|
||||
status_frame = mcrfpy.Frame(
|
||||
pos=(50, 520),
|
||||
size=(700, 80),
|
||||
fill_color=mcrfpy.Color(35, 35, 45),
|
||||
outline_color=mcrfpy.Color(60, 60, 70),
|
||||
outline=1
|
||||
)
|
||||
ui.append(status_frame)
|
||||
|
||||
status_label = mcrfpy.Caption(text="Status", pos=(60, 530))
|
||||
status_label.fill_color = mcrfpy.Color(150, 150, 170)
|
||||
ui.append(status_label)
|
||||
|
||||
status_text = mcrfpy.Caption(text="Click or Tab to focus a widget", pos=(60, 555))
|
||||
status_text.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
ui.append(status_text)
|
||||
|
||||
# Store references for status updates
|
||||
demo_state = {
|
||||
'focus_mgr': focus_mgr,
|
||||
'status_text': status_text,
|
||||
'grid': grid_widget,
|
||||
'inputs': [name_input, class_input, notes_input],
|
||||
'icons': [help_icon, settings_icon, inv_icon],
|
||||
}
|
||||
|
||||
# Key handler that routes to focus manager
|
||||
def on_key(key: str, action: str):
|
||||
focus_mgr.handle_key(key, action)
|
||||
|
||||
# Update status display
|
||||
if focus_mgr.focus_index >= 0:
|
||||
widget, _ = focus_mgr.widgets[focus_mgr.focus_index]
|
||||
if widget is grid_widget:
|
||||
status_text.text = f"Grid focused - Player at ({grid_widget.player_x}, {grid_widget.player_y})"
|
||||
elif widget in demo_state['inputs']:
|
||||
idx = demo_state['inputs'].index(widget)
|
||||
labels = ["Name", "Class", "Notes"]
|
||||
status_text.text = f"{labels[idx]} input focused - Text: '{widget.get_text()}'"
|
||||
elif widget in demo_state['icons']:
|
||||
status_text.text = f"Icon focused: {widget.tooltip}"
|
||||
else:
|
||||
status_text.text = "No widget focused"
|
||||
|
||||
# Register key handler using Scene API
|
||||
scene = mcrfpy.sceneUI("focus_demo")
|
||||
# Note: We use keypressScene for function-based handler since we're not subclassing Scene
|
||||
mcrfpy.keypressScene(on_key)
|
||||
|
||||
# Set initial focus
|
||||
focus_mgr.focus(0)
|
||||
|
||||
# Activate scene
|
||||
mcrfpy.setScene("focus_demo")
|
||||
|
||||
return demo_state
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Entry Point
|
||||
# =============================================================================
|
||||
|
||||
def run_demo():
|
||||
"""Run the focus system demo."""
|
||||
print("=== Focus System Demo ===")
|
||||
print("Demonstrating Python-level focus management")
|
||||
print()
|
||||
print("Controls:")
|
||||
print(" Tab / Shift+Tab - Cycle between widgets")
|
||||
print(" WASD / Arrows - Move player in grid (when focused)")
|
||||
print(" Type - Enter text in inputs (when focused)")
|
||||
print(" Space / Enter - Activate icons (when focused)")
|
||||
print(" Escape - Close modal dialogs")
|
||||
print(" Click - Focus clicked widget")
|
||||
print()
|
||||
|
||||
demo_state = create_demo_scene()
|
||||
|
||||
# Set up exit timer for headless testing
|
||||
def check_exit(dt):
|
||||
# In headless mode, exit after a short delay
|
||||
# In interactive mode, this won't trigger
|
||||
pass
|
||||
|
||||
# mcrfpy.setTimer("demo_check", check_exit, 100)
|
||||
|
||||
|
||||
# Run if executed directly
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
from mcrfpy import automation
|
||||
|
||||
run_demo()
|
||||
|
||||
# If --screenshot flag, take a screenshot and exit
|
||||
if "--screenshot" in sys.argv or len(sys.argv) > 1:
|
||||
def take_screenshot(dt):
|
||||
automation.screenshot("focus_demo_screenshot.png")
|
||||
print("Screenshot saved: focus_demo_screenshot.png")
|
||||
sys.exit(0)
|
||||
mcrfpy.setTimer("screenshot", take_screenshot, 200)
|
||||
Loading…
Add table
Add a link
Reference in a new issue