Update "UI-Widget-Patterns.-"

John McCardle 2026-02-07 22:21:49 +00:00
commit 3cbfcf060b

@ -1,404 +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
mcrfpy.createScene("menu") scene = mcrfpy.Scene("menu")
ui = mcrfpy.sceneUI("menu") ui = scene.children
# Load assets # Create root container for the menu/HUD
font = mcrfpy.Font("assets/fonts/mono.ttf") root = mcrfpy.Frame(pos=(50, 50), size=(300, 400),
# texture = mcrfpy.Texture("assets/ui_sprites.png", grid_size=(16, 16)) fill_color=mcrfpy.Color(30, 30, 40))
root.outline_color = mcrfpy.Color(80, 80, 100)
# Create root container for the menu/HUD root.outline = 2
root = mcrfpy.Frame(pos=(50, 50), size=(300, 400)) ui.append(root)
root.fill_color = mcrfpy.Color(30, 30, 40)
root.outline_color = mcrfpy.Color(80, 80, 100) # Add widgets to root.children...
root.outline = 2
ui.append(root) mcrfpy.current_scene = scene
```
# Add widgets to root.children...
---
mcrfpy.setScene("menu")
``` ## Button
--- A clickable frame with label and hover feedback.
## Button ```python
def make_button(parent, pos, text, on_click):
A clickable frame with label and hover feedback. """Create a button with hover effects."""
btn = mcrfpy.Frame(pos=pos, size=(120, 32),
```python fill_color=mcrfpy.Color(60, 60, 80))
def make_button(parent, pos, text, on_click): btn.outline_color = mcrfpy.Color(100, 100, 140)
"""Create a button with hover effects.""" btn.outline = 1
btn = mcrfpy.Frame(pos=pos, size=(120, 32))
btn.fill_color = mcrfpy.Color(60, 60, 80) label = mcrfpy.Caption(text=text, pos=(10, 6))
btn.outline_color = mcrfpy.Color(100, 100, 140) label.fill_color = mcrfpy.Color(220, 220, 220)
btn.outline = 1 btn.children.append(label)
label = mcrfpy.Caption(pos=(10, 6), text=text) # Hover effects - on_enter/on_exit receive (pos: Vector)
label.fill_color = mcrfpy.Color(220, 220, 220) btn.on_enter = lambda pos: setattr(btn, 'fill_color',
btn.children.append(label) mcrfpy.Color(80, 80, 110))
btn.on_exit = lambda pos: setattr(btn, 'fill_color',
# Hover effects mcrfpy.Color(60, 60, 80))
def on_enter():
btn.fill_color = mcrfpy.Color(80, 80, 110) # Click handler receives (pos: Vector, button: MouseButton, action: InputState)
btn.outline_color = mcrfpy.Color(140, 140, 180) def handle_click(pos, button, action):
if button == mcrfpy.MouseButton.LEFT and action == mcrfpy.InputState.PRESSED:
def on_exit(): on_click()
btn.fill_color = mcrfpy.Color(60, 60, 80) btn.on_click = handle_click
btn.outline_color = mcrfpy.Color(100, 100, 140)
parent.children.append(btn)
btn.on_enter = on_enter return btn
btn.on_exit = on_exit
btn.on_click = lambda x, y, btn: on_click() # Usage
make_button(root, (20, 20), "New Game", lambda: start_new_game())
parent.children.append(btn) make_button(root, (20, 60), "Options", lambda: show_options())
return btn make_button(root, (20, 100), "Quit", lambda: sys.exit(0))
```
# Usage
make_button(root, (20, 20), "New Game", lambda: mcrfpy.setScene("game")) ---
make_button(root, (20, 60), "Options", lambda: show_options())
make_button(root, (20, 100), "Quit", lambda: mcrfpy.exit()) ## Toggle / Checkbox
```
A button that toggles state and updates its appearance.
---
```python
## Toggle / Checkbox def make_toggle(parent, pos, label_text, initial=False, on_change=None):
"""Create a toggle with visual state indicator."""
A button that toggles state and updates its appearance. state = {"checked": initial}
```python frame = mcrfpy.Frame(pos=pos, size=(160, 28),
def make_toggle(parent, pos, label_text, initial=False, on_change=None): fill_color=mcrfpy.Color(40, 40, 50))
"""Create a toggle with visual state indicator."""
state = {"checked": initial} # Checkbox indicator
indicator = mcrfpy.Frame(pos=(6, 6), size=(16, 16))
frame = mcrfpy.Frame(pos=pos, size=(160, 28)) indicator.outline = 1
frame.fill_color = mcrfpy.Color(40, 40, 50) indicator.outline_color = mcrfpy.Color(120, 120, 140)
frame.children.append(indicator)
# Checkbox indicator
indicator = mcrfpy.Frame(pos=(6, 6), size=(16, 16)) # Label
indicator.outline = 1 label = mcrfpy.Caption(text=label_text, pos=(30, 5))
indicator.outline_color = mcrfpy.Color(120, 120, 140) label.fill_color = mcrfpy.Color(200, 200, 200)
frame.children.append(indicator) frame.children.append(label)
# Label def update_visual():
label = mcrfpy.Caption(pos=(30, 5), text=label_text) if state["checked"]:
label.fill_color = mcrfpy.Color(200, 200, 200) indicator.fill_color = mcrfpy.Color(100, 180, 100)
frame.children.append(label) else:
indicator.fill_color = mcrfpy.Color(50, 50, 60)
def update_visual():
if state["checked"]: def toggle(pos, button, action):
indicator.fill_color = mcrfpy.Color(100, 180, 100) if button == mcrfpy.MouseButton.LEFT and action == mcrfpy.InputState.PRESSED:
else: state["checked"] = not state["checked"]
indicator.fill_color = mcrfpy.Color(50, 50, 60) update_visual()
if on_change:
def toggle(x, y, btn): on_change(state["checked"])
state["checked"] = not state["checked"]
update_visual() frame.on_click = toggle
if on_change: update_visual()
on_change(state["checked"])
parent.children.append(frame)
frame.on_click = toggle return state # Return state dict for external access
update_visual()
# Usage
parent.children.append(frame) music_toggle = make_toggle(root, (20, 20), "Music", initial=True,
return state # Return state dict for external access on_change=lambda on: set_music_volume(1.0 if on else 0.0))
```
# Usage
music_toggle = make_toggle(root, (20, 20), "Music", initial=True, ---
on_change=lambda on: set_music_volume(1.0 if on else 0.0))
``` ## Vertical Menu
--- A list of selectable options with keyboard navigation support.
## Vertical Menu ```python
class VerticalMenu:
A list of selectable options with keyboard navigation support. def __init__(self, parent, pos, options, on_select):
"""
```python options: list of (label, value) tuples
class VerticalMenu: on_select: callback(value) when option chosen
def __init__(self, parent, pos, options, on_select): """
""" self.options = options
options: list of (label, value) tuples self.on_select = on_select
on_select: callback(value) when option chosen self.selected = 0
"""
self.options = options self.frame = mcrfpy.Frame(pos=pos,
self.on_select = on_select size=(180, len(options) * 28 + 8),
self.selected = 0 fill_color=mcrfpy.Color(35, 35, 45))
parent.children.append(self.frame)
self.frame = mcrfpy.Frame(pos=pos, size=(180, len(options) * 28 + 8))
self.frame.fill_color = mcrfpy.Color(35, 35, 45) self.items = []
parent.children.append(self.frame) for i, (label, value) in enumerate(options):
item = mcrfpy.Caption(text=label, pos=(12, 4 + i * 28))
self.items = [] item.fill_color = mcrfpy.Color(180, 180, 180)
for i, (label, value) in enumerate(options): self.frame.children.append(item)
item = mcrfpy.Caption(pos=(12, 4 + i * 28), text=label) self.items.append(item)
item.fill_color = mcrfpy.Color(180, 180, 180)
self.frame.children.append(item) self._update_highlight()
self.items.append(item)
def _update_highlight(self):
self._update_highlight() for i, item in enumerate(self.items):
if i == self.selected:
def _update_highlight(self): item.fill_color = mcrfpy.Color(255, 220, 100)
for i, item in enumerate(self.items): else:
if i == self.selected: item.fill_color = mcrfpy.Color(180, 180, 180)
item.fill_color = mcrfpy.Color(255, 220, 100)
else: def move_up(self):
item.fill_color = mcrfpy.Color(180, 180, 180) self.selected = (self.selected - 1) % len(self.options)
self._update_highlight()
def move_up(self):
self.selected = (self.selected - 1) % len(self.options) def move_down(self):
self._update_highlight() self.selected = (self.selected + 1) % len(self.options)
self._update_highlight()
def move_down(self):
self.selected = (self.selected + 1) % len(self.options) def confirm(self):
self._update_highlight() _, value = self.options[self.selected]
self.on_select(value)
def confirm(self):
_, value = self.options[self.selected] # Usage
self.on_select(value) menu = VerticalMenu(root, (20, 20), [
("Continue", "continue"),
# Usage ("New Game", "new"),
menu = VerticalMenu(root, (20, 20), [ ("Options", "options"),
("Continue", "continue"), ("Quit", "quit")
("New Game", "new"), ], on_select=handle_menu_choice)
("Options", "options"),
("Quit", "quit") # Keyboard navigation via scene.on_key
], on_select=handle_menu_choice) def handle_key(key, action):
if action != mcrfpy.InputState.PRESSED:
def handle_key(key, pressed): return
if not pressed: if key == mcrfpy.Key.UP:
return menu.move_up()
if key == "Up": elif key == mcrfpy.Key.DOWN:
menu.move_up() menu.move_down()
elif key == "Down": elif key == mcrfpy.Key.ENTER:
menu.move_down() menu.confirm()
elif key == "Enter":
menu.confirm() scene.on_key = handle_key
```
mcrfpy.keypressScene(handle_key)
``` ---
--- ## Modal Dialog
## Modal Dialog A dialog that captures all input until dismissed.
A dialog that captures all input until dismissed. ```python
class ModalDialog:
```python def __init__(self, scene, message, on_dismiss=None):
class ModalDialog: self.scene = scene
def __init__(self, message, on_dismiss=None): self.on_dismiss = on_dismiss
self.on_dismiss = on_dismiss ui = scene.children
self.ui = mcrfpy.sceneUI(mcrfpy.currentScene())
# 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))
self.backdrop.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 x, y, b: None # Block clicks ui.append(self.backdrop)
self.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))
self.dialog.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)
self.ui.append(self.dialog)
# Message
# Message msg = mcrfpy.Caption(text=message, pos=(20, 20))
msg = mcrfpy.Caption(pos=(20, 20), text=message) 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))
ok_btn.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 x, y, b: 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(pos=(35, 8), text="OK") 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 = None
self._install_key_handler() def modal_keys(key, action):
if action == mcrfpy.InputState.PRESSED:
def _install_key_handler(self): if key == mcrfpy.Key.ENTER or key == mcrfpy.Key.ESCAPE:
def modal_keys(key, pressed): self.close()
if pressed and key in ("Enter", "Escape"): scene.on_key = modal_keys
self.close()
mcrfpy.keypressScene(modal_keys) def close(self):
ui = self.scene.children
def close(self): ui.remove(self.backdrop)
self.ui.remove(self.backdrop) ui.remove(self.dialog)
self.ui.remove(self.dialog) if self._prev_handler:
if self.on_dismiss: self.scene.on_key = self._prev_handler
self.on_dismiss() if self.on_dismiss:
self.on_dismiss()
# Usage
def show_message(text): # Usage
ModalDialog(text) dialog = ModalDialog(scene, "Game saved successfully!")
```
show_message("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
class Hotbar:
```python def __init__(self, parent, pos, slot_count=9):
class Hotbar: self.slots = []
def __init__(self, parent, pos, slot_count=9): self.items = [None] * slot_count
self.slots = [] self.selected = 0
self.items = [None] * slot_count # Item data per slot
self.selected = 0 self.frame = mcrfpy.Frame(pos=pos,
size=(slot_count * 36 + 8, 44),
self.frame = mcrfpy.Frame(pos=pos, size=(slot_count * 36 + 8, 44)) fill_color=mcrfpy.Color(30, 30, 40, 200))
self.frame.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))
slot.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))
# Slot number label num.fill_color = mcrfpy.Color(100, 100, 120)
num = mcrfpy.Caption(pos=(2, 2), text=str((i + 1) % 10)) slot.children.append(num)
num.fill_color = mcrfpy.Color(100, 100, 120)
slot.children.append(num) self._update_selection()
self._update_selection() def _update_selection(self):
for i, slot in enumerate(self.slots):
def _update_selection(self): if i == self.selected:
for i, slot in enumerate(self.slots): slot.outline_color = mcrfpy.Color(200, 180, 80)
if i == self.selected: slot.outline = 2
slot.outline_color = mcrfpy.Color(200, 180, 80) else:
slot.outline = 2 slot.outline_color = mcrfpy.Color(80, 80, 100)
else: slot.outline = 1
slot.outline_color = mcrfpy.Color(80, 80, 100)
slot.outline = 1 def select(self, index):
if 0 <= index < len(self.slots):
def select(self, index): self.selected = index
if 0 <= index < len(self.slots): self._update_selection()
self.selected = index
self._update_selection() # Usage
hotbar = Hotbar(root, (200, 700))
def use_selected(self):
item = self.items[self.selected] # Key mapping for number keys
if item: num_keys = {
item.use() 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,
def set_item(self, index, item): mcrfpy.Key.NUM_7: 6, mcrfpy.Key.NUM_8: 7, mcrfpy.Key.NUM_9: 8,
self.items[index] = item }
# Update slot visual (add sprite, etc.)
def handle_key(key, action):
# Usage if action != mcrfpy.InputState.PRESSED:
hotbar = Hotbar(root, (200, 700)) return
if key in num_keys:
def handle_key(key, pressed): hotbar.select(num_keys[key])
if not pressed:
return scene.on_key = handle_key
# Number keys select slots ```
if key.startswith("Num") and len(key) == 4:
num = int(key[3]) ---
index = (num - 1) if num > 0 else 9
hotbar.select(index) ## Draggable Window
elif key == "E":
hotbar.use_selected() A frame that can be dragged by its title bar.
mcrfpy.keypressScene(handle_key) ```python
``` class DraggableWindow:
def __init__(self, parent, pos, size, title):
--- self.dragging = False
self.drag_offset = (0, 0)
## Draggable Window
self.frame = mcrfpy.Frame(pos=pos, size=size,
A frame that can be dragged by its title bar. fill_color=mcrfpy.Color(45, 45, 55))
self.frame.outline = 1
```python self.frame.outline_color = mcrfpy.Color(100, 100, 120)
class DraggableWindow: parent.children.append(self.frame)
def __init__(self, parent, pos, size, title):
self.dragging = False # Title bar
self.drag_offset = (0, 0) self.title_bar = mcrfpy.Frame(pos=(0, 0), size=(size[0], 24),
fill_color=mcrfpy.Color(60, 60, 80))
self.frame = mcrfpy.Frame(pos=pos, size=size) self.frame.children.append(self.title_bar)
self.frame.fill_color = mcrfpy.Color(45, 45, 55)
self.frame.outline = 1 title_label = mcrfpy.Caption(text=title, pos=(8, 4))
self.frame.outline_color = mcrfpy.Color(100, 100, 120) title_label.fill_color = mcrfpy.Color(200, 200, 220)
parent.children.append(self.frame) self.title_bar.children.append(title_label)
# Title bar self.content_y = 28
self.title_bar = mcrfpy.Frame(pos=(0, 0), size=(size[0], 24))
self.title_bar.fill_color = mcrfpy.Color(60, 60, 80) # Drag handling
self.frame.children.append(self.title_bar) # on_click: (pos: Vector, button: MouseButton, action: InputState)
def start_drag(pos, button, action):
title_label = mcrfpy.Caption(pos=(8, 4), text=title) if button == mcrfpy.MouseButton.LEFT and action == mcrfpy.InputState.PRESSED:
title_label.fill_color = mcrfpy.Color(200, 200, 220) self.dragging = True
self.title_bar.children.append(title_label) self.drag_offset = (pos.x - self.frame.x, pos.y - self.frame.y)
# Content area reference # on_move: (pos: Vector)
self.content_y = 28 def on_move(pos):
if self.dragging:
# Drag handling self.frame.x = pos.x - self.drag_offset[0]
def start_drag(x, y, btn): self.frame.y = pos.y - self.drag_offset[1]
if btn == 0: # Left click
self.dragging = True # on_exit: (pos: Vector)
frame_pos = self.frame.pos def stop_drag(pos):
self.drag_offset = (x - frame_pos[0], y - frame_pos[1]) self.dragging = False
def on_move(x, y): self.title_bar.on_click = start_drag
if self.dragging: self.title_bar.on_move = on_move
new_x = x - self.drag_offset[0] self.title_bar.on_exit = stop_drag
new_y = y - self.drag_offset[1]
self.frame.pos = (new_x, new_y) # Usage
window = DraggableWindow(root, (100, 100), (250, 300), "Inventory")
def stop_drag():
self.dragging = False # Add content to window.frame.children at y >= window.content_y
item_list = mcrfpy.Caption(text="Items here...", pos=(10, window.content_y + 10))
self.title_bar.on_click = start_drag window.frame.children.append(item_list)
self.title_bar.on_move = on_move ```
self.title_bar.on_exit = stop_drag
---
# Usage
window = DraggableWindow(root, (100, 100), (250, 300), "Inventory") ## Related Pages
# Add content to window.frame.children at y >= window.content_y - [[Input-and-Events]] - Event handler API reference
item_list = mcrfpy.Caption(pos=(10, window.content_y + 10), text="Items here...") - [[Grid-Interaction-Patterns]] - Patterns for grid-based gameplay
window.frame.children.append(item_list) - [[Animation-System]] - Animating widget transitions
```
---
---
*Last updated: 2026-02-07*
## Related Pages
- [[Input-and-Events]] - Event handler API reference
- [[Grid-Interaction-Patterns]] - Patterns for grid-based gameplay
- [[Animation-System]] - Animating widget transitions
---
*Last updated: 2025-11-29*