Update UI Widget Patterns

John McCardle 2026-02-07 23:50:56 +00:00
commit 288977354a

@ -1,395 +1,395 @@
# UI Widget Patterns # UI Widget Patterns
Reusable patterns for building menus, dialogs, and HUD elements using Frame, Caption, and Sprite components. These patterns work independently of Grids. Reusable patterns for building menus, dialogs, and HUD elements using Frame, Caption, and Sprite components. These patterns work independently of Grids.
**Related Pages:** **Related Pages:**
- [[UI-Component-Hierarchy]] - Base UI components - [[UI-Component-Hierarchy]] - Base UI components
- [[Input-and-Events]] - Event handler reference - [[Input-and-Events]] - Event handler reference
- [[Animation-System]] - Animating widget transitions - [[Animation-System]] - Animating widget transitions
--- ---
## Setup Template ## Setup Template
Most widget patterns fit into this basic structure: Most widget patterns fit into this basic structure:
```python ```python
import mcrfpy import mcrfpy
# Create scene and get UI collection # Create scene and get UI collection
scene = mcrfpy.Scene("menu") scene = mcrfpy.Scene("menu")
ui = scene.children ui = scene.children
# Create root container for the menu/HUD # Create root container for the menu/HUD
root = mcrfpy.Frame(pos=(50, 50), size=(300, 400), root = mcrfpy.Frame(pos=(50, 50), size=(300, 400),
fill_color=mcrfpy.Color(30, 30, 40)) fill_color=mcrfpy.Color(30, 30, 40))
root.outline_color = mcrfpy.Color(80, 80, 100) root.outline_color = mcrfpy.Color(80, 80, 100)
root.outline = 2 root.outline = 2
ui.append(root) ui.append(root)
# Add widgets to root.children... # Add widgets to root.children...
mcrfpy.current_scene = scene mcrfpy.current_scene = scene
``` ```
--- ---
## Button ## Button
A clickable frame with label and hover feedback. A clickable frame with label and hover feedback.
```python ```python
def make_button(parent, pos, text, on_click): def make_button(parent, pos, text, on_click):
"""Create a button with hover effects.""" """Create a button with hover effects."""
btn = mcrfpy.Frame(pos=pos, size=(120, 32), btn = mcrfpy.Frame(pos=pos, size=(120, 32),
fill_color=mcrfpy.Color(60, 60, 80)) fill_color=mcrfpy.Color(60, 60, 80))
btn.outline_color = mcrfpy.Color(100, 100, 140) btn.outline_color = mcrfpy.Color(100, 100, 140)
btn.outline = 1 btn.outline = 1
label = mcrfpy.Caption(text=text, pos=(10, 6)) label = mcrfpy.Caption(text=text, pos=(10, 6))
label.fill_color = mcrfpy.Color(220, 220, 220) label.fill_color = mcrfpy.Color(220, 220, 220)
btn.children.append(label) btn.children.append(label)
# Hover effects - on_enter/on_exit receive (pos: Vector) # Hover effects - on_enter/on_exit receive (pos: Vector)
btn.on_enter = lambda pos: setattr(btn, 'fill_color', btn.on_enter = lambda pos: setattr(btn, 'fill_color',
mcrfpy.Color(80, 80, 110)) mcrfpy.Color(80, 80, 110))
btn.on_exit = lambda pos: setattr(btn, 'fill_color', btn.on_exit = lambda pos: setattr(btn, 'fill_color',
mcrfpy.Color(60, 60, 80)) mcrfpy.Color(60, 60, 80))
# Click handler receives (pos: Vector, button: MouseButton, action: InputState) # Click handler receives (pos: Vector, button: MouseButton, action: InputState)
def handle_click(pos, button, action): def handle_click(pos, button, action):
if button == mcrfpy.MouseButton.LEFT and action == mcrfpy.InputState.PRESSED: if button == mcrfpy.MouseButton.LEFT and action == mcrfpy.InputState.PRESSED:
on_click() on_click()
btn.on_click = handle_click btn.on_click = handle_click
parent.children.append(btn) parent.children.append(btn)
return btn return btn
# Usage # Usage
make_button(root, (20, 20), "New Game", lambda: start_new_game()) make_button(root, (20, 20), "New Game", lambda: start_new_game())
make_button(root, (20, 60), "Options", lambda: show_options()) make_button(root, (20, 60), "Options", lambda: show_options())
make_button(root, (20, 100), "Quit", lambda: sys.exit(0)) make_button(root, (20, 100), "Quit", lambda: sys.exit(0))
``` ```
--- ---
## Toggle / Checkbox ## Toggle / Checkbox
A button that toggles state and updates its appearance. A button that toggles state and updates its appearance.
```python ```python
def make_toggle(parent, pos, label_text, initial=False, on_change=None): def make_toggle(parent, pos, label_text, initial=False, on_change=None):
"""Create a toggle with visual state indicator.""" """Create a toggle with visual state indicator."""
state = {"checked": initial} state = {"checked": initial}
frame = mcrfpy.Frame(pos=pos, size=(160, 28), frame = mcrfpy.Frame(pos=pos, size=(160, 28),
fill_color=mcrfpy.Color(40, 40, 50)) fill_color=mcrfpy.Color(40, 40, 50))
# Checkbox indicator # Checkbox indicator
indicator = mcrfpy.Frame(pos=(6, 6), size=(16, 16)) indicator = mcrfpy.Frame(pos=(6, 6), size=(16, 16))
indicator.outline = 1 indicator.outline = 1
indicator.outline_color = mcrfpy.Color(120, 120, 140) indicator.outline_color = mcrfpy.Color(120, 120, 140)
frame.children.append(indicator) frame.children.append(indicator)
# Label # Label
label = mcrfpy.Caption(text=label_text, pos=(30, 5)) label = mcrfpy.Caption(text=label_text, pos=(30, 5))
label.fill_color = mcrfpy.Color(200, 200, 200) label.fill_color = mcrfpy.Color(200, 200, 200)
frame.children.append(label) frame.children.append(label)
def update_visual(): def update_visual():
if state["checked"]: if state["checked"]:
indicator.fill_color = mcrfpy.Color(100, 180, 100) indicator.fill_color = mcrfpy.Color(100, 180, 100)
else: else:
indicator.fill_color = mcrfpy.Color(50, 50, 60) indicator.fill_color = mcrfpy.Color(50, 50, 60)
def toggle(pos, button, action): def toggle(pos, button, action):
if button == mcrfpy.MouseButton.LEFT and action == mcrfpy.InputState.PRESSED: if button == mcrfpy.MouseButton.LEFT and action == mcrfpy.InputState.PRESSED:
state["checked"] = not state["checked"] state["checked"] = not state["checked"]
update_visual() update_visual()
if on_change: if on_change:
on_change(state["checked"]) on_change(state["checked"])
frame.on_click = toggle frame.on_click = toggle
update_visual() update_visual()
parent.children.append(frame) parent.children.append(frame)
return state # Return state dict for external access return state # Return state dict for external access
# Usage # Usage
music_toggle = make_toggle(root, (20, 20), "Music", initial=True, music_toggle = make_toggle(root, (20, 20), "Music", initial=True,
on_change=lambda on: set_music_volume(1.0 if on else 0.0)) on_change=lambda on: set_music_volume(1.0 if on else 0.0))
``` ```
--- ---
## Vertical Menu ## Vertical Menu
A list of selectable options with keyboard navigation support. A list of selectable options with keyboard navigation support.
```python ```python
class VerticalMenu: class VerticalMenu:
def __init__(self, parent, pos, options, on_select): def __init__(self, parent, pos, options, on_select):
""" """
options: list of (label, value) tuples options: list of (label, value) tuples
on_select: callback(value) when option chosen on_select: callback(value) when option chosen
""" """
self.options = options self.options = options
self.on_select = on_select self.on_select = on_select
self.selected = 0 self.selected = 0
self.frame = mcrfpy.Frame(pos=pos, self.frame = mcrfpy.Frame(pos=pos,
size=(180, len(options) * 28 + 8), size=(180, len(options) * 28 + 8),
fill_color=mcrfpy.Color(35, 35, 45)) fill_color=mcrfpy.Color(35, 35, 45))
parent.children.append(self.frame) parent.children.append(self.frame)
self.items = [] self.items = []
for i, (label, value) in enumerate(options): for i, (label, value) in enumerate(options):
item = mcrfpy.Caption(text=label, pos=(12, 4 + i * 28)) item = mcrfpy.Caption(text=label, pos=(12, 4 + i * 28))
item.fill_color = mcrfpy.Color(180, 180, 180) item.fill_color = mcrfpy.Color(180, 180, 180)
self.frame.children.append(item) self.frame.children.append(item)
self.items.append(item) self.items.append(item)
self._update_highlight() self._update_highlight()
def _update_highlight(self): def _update_highlight(self):
for i, item in enumerate(self.items): for i, item in enumerate(self.items):
if i == self.selected: if i == self.selected:
item.fill_color = mcrfpy.Color(255, 220, 100) item.fill_color = mcrfpy.Color(255, 220, 100)
else: else:
item.fill_color = mcrfpy.Color(180, 180, 180) item.fill_color = mcrfpy.Color(180, 180, 180)
def move_up(self): def move_up(self):
self.selected = (self.selected - 1) % len(self.options) self.selected = (self.selected - 1) % len(self.options)
self._update_highlight() self._update_highlight()
def move_down(self): def move_down(self):
self.selected = (self.selected + 1) % len(self.options) self.selected = (self.selected + 1) % len(self.options)
self._update_highlight() self._update_highlight()
def confirm(self): def confirm(self):
_, value = self.options[self.selected] _, value = self.options[self.selected]
self.on_select(value) self.on_select(value)
# Usage # Usage
menu = VerticalMenu(root, (20, 20), [ menu = VerticalMenu(root, (20, 20), [
("Continue", "continue"), ("Continue", "continue"),
("New Game", "new"), ("New Game", "new"),
("Options", "options"), ("Options", "options"),
("Quit", "quit") ("Quit", "quit")
], on_select=handle_menu_choice) ], on_select=handle_menu_choice)
# Keyboard navigation via scene.on_key # Keyboard navigation via scene.on_key
def handle_key(key, action): def handle_key(key, action):
if action != mcrfpy.InputState.PRESSED: if action != mcrfpy.InputState.PRESSED:
return return
if key == mcrfpy.Key.UP: if key == mcrfpy.Key.UP:
menu.move_up() menu.move_up()
elif key == mcrfpy.Key.DOWN: elif key == mcrfpy.Key.DOWN:
menu.move_down() menu.move_down()
elif key == mcrfpy.Key.ENTER: elif key == mcrfpy.Key.ENTER:
menu.confirm() menu.confirm()
scene.on_key = handle_key scene.on_key = handle_key
``` ```
--- ---
## Modal Dialog ## Modal Dialog
A dialog that captures all input until dismissed. A dialog that captures all input until dismissed.
```python ```python
class ModalDialog: class ModalDialog:
def __init__(self, scene, message, on_dismiss=None): def __init__(self, scene, message, on_dismiss=None):
self.scene = scene self.scene = scene
self.on_dismiss = on_dismiss self.on_dismiss = on_dismiss
ui = scene.children ui = scene.children
# Semi-transparent backdrop # Semi-transparent backdrop
self.backdrop = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), self.backdrop = mcrfpy.Frame(pos=(0, 0), size=(1024, 768),
fill_color=mcrfpy.Color(0, 0, 0, 160)) fill_color=mcrfpy.Color(0, 0, 0, 160))
self.backdrop.z_index = 900 self.backdrop.z_index = 900
self.backdrop.on_click = lambda pos, btn, action: None # Block clicks self.backdrop.on_click = lambda pos, btn, action: None # Block clicks
ui.append(self.backdrop) ui.append(self.backdrop)
# Dialog box # Dialog box
self.dialog = mcrfpy.Frame(pos=(312, 284), size=(400, 200), self.dialog = mcrfpy.Frame(pos=(312, 284), size=(400, 200),
fill_color=mcrfpy.Color(50, 50, 65)) fill_color=mcrfpy.Color(50, 50, 65))
self.dialog.outline_color = mcrfpy.Color(120, 120, 150) self.dialog.outline_color = mcrfpy.Color(120, 120, 150)
self.dialog.outline = 2 self.dialog.outline = 2
self.dialog.z_index = 901 self.dialog.z_index = 901
ui.append(self.dialog) ui.append(self.dialog)
# Message # Message
msg = mcrfpy.Caption(text=message, pos=(20, 20)) msg = mcrfpy.Caption(text=message, pos=(20, 20))
msg.fill_color = mcrfpy.Color(220, 220, 220) msg.fill_color = mcrfpy.Color(220, 220, 220)
self.dialog.children.append(msg) self.dialog.children.append(msg)
# OK button # OK button
ok_btn = mcrfpy.Frame(pos=(150, 140), size=(100, 36), ok_btn = mcrfpy.Frame(pos=(150, 140), size=(100, 36),
fill_color=mcrfpy.Color(70, 100, 70)) fill_color=mcrfpy.Color(70, 100, 70))
ok_btn.outline = 1 ok_btn.outline = 1
ok_btn.outline_color = mcrfpy.Color(100, 150, 100) ok_btn.outline_color = mcrfpy.Color(100, 150, 100)
ok_btn.on_click = lambda pos, btn, action: self.close() ok_btn.on_click = lambda pos, btn, action: self.close()
self.dialog.children.append(ok_btn) self.dialog.children.append(ok_btn)
ok_label = mcrfpy.Caption(text="OK", pos=(35, 8)) ok_label = mcrfpy.Caption(text="OK", pos=(35, 8))
ok_label.fill_color = mcrfpy.Color(220, 255, 220) ok_label.fill_color = mcrfpy.Color(220, 255, 220)
ok_btn.children.append(ok_label) ok_btn.children.append(ok_label)
# Capture keyboard # Capture keyboard
self._prev_handler = scene.on_key self._prev_handler = scene.on_key
def modal_keys(key, action): def modal_keys(key, action):
if action == mcrfpy.InputState.PRESSED: if action == mcrfpy.InputState.PRESSED:
if key == mcrfpy.Key.ENTER or key == mcrfpy.Key.ESCAPE: if key == mcrfpy.Key.ENTER or key == mcrfpy.Key.ESCAPE:
self.close() self.close()
scene.on_key = modal_keys scene.on_key = modal_keys
def close(self): def close(self):
ui = self.scene.children ui = self.scene.children
ui.remove(self.backdrop) ui.remove(self.backdrop)
ui.remove(self.dialog) ui.remove(self.dialog)
if self._prev_handler: if self._prev_handler:
self.scene.on_key = self._prev_handler self.scene.on_key = self._prev_handler
if self.on_dismiss: if self.on_dismiss:
self.on_dismiss() self.on_dismiss()
# Usage # Usage
dialog = ModalDialog(scene, "Game saved successfully!") dialog = ModalDialog(scene, "Game saved successfully!")
``` ```
--- ---
## Hotbar / Quick Slots ## Hotbar / Quick Slots
Number keys (1-9) mapped to inventory slots. Number keys (1-9) mapped to inventory slots.
```python ```python
class Hotbar: class Hotbar:
def __init__(self, parent, pos, slot_count=9): def __init__(self, parent, pos, slot_count=9):
self.slots = [] self.slots = []
self.items = [None] * slot_count self.items = [None] * slot_count
self.selected = 0 self.selected = 0
self.frame = mcrfpy.Frame(pos=pos, self.frame = mcrfpy.Frame(pos=pos,
size=(slot_count * 36 + 8, 44), size=(slot_count * 36 + 8, 44),
fill_color=mcrfpy.Color(30, 30, 40, 200)) fill_color=mcrfpy.Color(30, 30, 40, 200))
parent.children.append(self.frame) parent.children.append(self.frame)
for i in range(slot_count): for i in range(slot_count):
slot = mcrfpy.Frame(pos=(4 + i * 36, 4), size=(32, 32), slot = mcrfpy.Frame(pos=(4 + i * 36, 4), size=(32, 32),
fill_color=mcrfpy.Color(50, 50, 60)) fill_color=mcrfpy.Color(50, 50, 60))
slot.outline = 1 slot.outline = 1
slot.outline_color = mcrfpy.Color(80, 80, 100) slot.outline_color = mcrfpy.Color(80, 80, 100)
self.frame.children.append(slot) self.frame.children.append(slot)
self.slots.append(slot) self.slots.append(slot)
num = mcrfpy.Caption(text=str((i + 1) % 10), pos=(2, 2)) num = mcrfpy.Caption(text=str((i + 1) % 10), pos=(2, 2))
num.fill_color = mcrfpy.Color(100, 100, 120) num.fill_color = mcrfpy.Color(100, 100, 120)
slot.children.append(num) slot.children.append(num)
self._update_selection() self._update_selection()
def _update_selection(self): def _update_selection(self):
for i, slot in enumerate(self.slots): for i, slot in enumerate(self.slots):
if i == self.selected: if i == self.selected:
slot.outline_color = mcrfpy.Color(200, 180, 80) slot.outline_color = mcrfpy.Color(200, 180, 80)
slot.outline = 2 slot.outline = 2
else: else:
slot.outline_color = mcrfpy.Color(80, 80, 100) slot.outline_color = mcrfpy.Color(80, 80, 100)
slot.outline = 1 slot.outline = 1
def select(self, index): def select(self, index):
if 0 <= index < len(self.slots): if 0 <= index < len(self.slots):
self.selected = index self.selected = index
self._update_selection() self._update_selection()
# Usage # Usage
hotbar = Hotbar(root, (200, 700)) hotbar = Hotbar(root, (200, 700))
# Key mapping for number keys # Key mapping for number keys
num_keys = { num_keys = {
mcrfpy.Key.NUM_1: 0, mcrfpy.Key.NUM_2: 1, mcrfpy.Key.NUM_3: 2, mcrfpy.Key.NUM_1: 0, mcrfpy.Key.NUM_2: 1, mcrfpy.Key.NUM_3: 2,
mcrfpy.Key.NUM_4: 3, mcrfpy.Key.NUM_5: 4, mcrfpy.Key.NUM_6: 5, mcrfpy.Key.NUM_4: 3, mcrfpy.Key.NUM_5: 4, mcrfpy.Key.NUM_6: 5,
mcrfpy.Key.NUM_7: 6, mcrfpy.Key.NUM_8: 7, mcrfpy.Key.NUM_9: 8, mcrfpy.Key.NUM_7: 6, mcrfpy.Key.NUM_8: 7, mcrfpy.Key.NUM_9: 8,
} }
def handle_key(key, action): def handle_key(key, action):
if action != mcrfpy.InputState.PRESSED: if action != mcrfpy.InputState.PRESSED:
return return
if key in num_keys: if key in num_keys:
hotbar.select(num_keys[key]) hotbar.select(num_keys[key])
scene.on_key = handle_key scene.on_key = handle_key
``` ```
--- ---
## Draggable Window ## Draggable Window
A frame that can be dragged by its title bar. A frame that can be dragged by its title bar.
```python ```python
class DraggableWindow: class DraggableWindow:
def __init__(self, parent, pos, size, title): def __init__(self, parent, pos, size, title):
self.dragging = False self.dragging = False
self.drag_offset = (0, 0) self.drag_offset = (0, 0)
self.frame = mcrfpy.Frame(pos=pos, size=size, self.frame = mcrfpy.Frame(pos=pos, size=size,
fill_color=mcrfpy.Color(45, 45, 55)) fill_color=mcrfpy.Color(45, 45, 55))
self.frame.outline = 1 self.frame.outline = 1
self.frame.outline_color = mcrfpy.Color(100, 100, 120) self.frame.outline_color = mcrfpy.Color(100, 100, 120)
parent.children.append(self.frame) parent.children.append(self.frame)
# Title bar # Title bar
self.title_bar = mcrfpy.Frame(pos=(0, 0), size=(size[0], 24), self.title_bar = mcrfpy.Frame(pos=(0, 0), size=(size[0], 24),
fill_color=mcrfpy.Color(60, 60, 80)) fill_color=mcrfpy.Color(60, 60, 80))
self.frame.children.append(self.title_bar) self.frame.children.append(self.title_bar)
title_label = mcrfpy.Caption(text=title, pos=(8, 4)) title_label = mcrfpy.Caption(text=title, pos=(8, 4))
title_label.fill_color = mcrfpy.Color(200, 200, 220) title_label.fill_color = mcrfpy.Color(200, 200, 220)
self.title_bar.children.append(title_label) self.title_bar.children.append(title_label)
self.content_y = 28 self.content_y = 28
# Drag handling # Drag handling
# on_click: (pos: Vector, button: MouseButton, action: InputState) # on_click: (pos: Vector, button: MouseButton, action: InputState)
def start_drag(pos, button, action): def start_drag(pos, button, action):
if button == mcrfpy.MouseButton.LEFT and action == mcrfpy.InputState.PRESSED: if button == mcrfpy.MouseButton.LEFT and action == mcrfpy.InputState.PRESSED:
self.dragging = True self.dragging = True
self.drag_offset = (pos.x - self.frame.x, pos.y - self.frame.y) self.drag_offset = (pos.x - self.frame.x, pos.y - self.frame.y)
# on_move: (pos: Vector) # on_move: (pos: Vector)
def on_move(pos): def on_move(pos):
if self.dragging: if self.dragging:
self.frame.x = pos.x - self.drag_offset[0] self.frame.x = pos.x - self.drag_offset[0]
self.frame.y = pos.y - self.drag_offset[1] self.frame.y = pos.y - self.drag_offset[1]
# on_exit: (pos: Vector) # on_exit: (pos: Vector)
def stop_drag(pos): def stop_drag(pos):
self.dragging = False self.dragging = False
self.title_bar.on_click = start_drag self.title_bar.on_click = start_drag
self.title_bar.on_move = on_move self.title_bar.on_move = on_move
self.title_bar.on_exit = stop_drag self.title_bar.on_exit = stop_drag
# Usage # Usage
window = DraggableWindow(root, (100, 100), (250, 300), "Inventory") window = DraggableWindow(root, (100, 100), (250, 300), "Inventory")
# Add content to window.frame.children at y >= window.content_y # Add content to window.frame.children at y >= window.content_y
item_list = mcrfpy.Caption(text="Items here...", pos=(10, window.content_y + 10)) item_list = mcrfpy.Caption(text="Items here...", pos=(10, window.content_y + 10))
window.frame.children.append(item_list) window.frame.children.append(item_list)
``` ```
--- ---
## Related Pages ## Related Pages
- [[Input-and-Events]] - Event handler API reference - [[Input-and-Events]] - Event handler API reference
- [[Grid-Interaction-Patterns]] - Patterns for grid-based gameplay - [[Grid-Interaction-Patterns]] - Patterns for grid-based gameplay
- [[Animation-System]] - Animating widget transitions - [[Animation-System]] - Animating widget transitions
--- ---
*Last updated: 2026-02-07* *Last updated: 2026-02-07*