Add "UI Widget Patterns"

John McCardle 2025-11-29 23:44:58 +00:00
parent 466148a580
commit be816ac30a
1 changed files with 404 additions and 0 deletions

404
UI-Widget-Patterns.md Normal file

@ -0,0 +1,404 @@
# UI Widget Patterns
Reusable patterns for building menus, dialogs, and HUD elements using Frame, Caption, and Sprite components. These patterns work independently of Grids.
**Related Pages:**
- [[UI-Component-Hierarchy]] - Base UI components
- [[Input-and-Events]] - Event handler reference
- [[Animation-System]] - Animating widget transitions
---
## Setup Template
Most widget patterns fit into this basic structure:
```python
import mcrfpy
# Create scene and get UI collection
mcrfpy.createScene("menu")
ui = mcrfpy.sceneUI("menu")
# Load assets
font = mcrfpy.Font("assets/fonts/mono.ttf")
# texture = mcrfpy.Texture("assets/ui_sprites.png", grid_size=(16, 16))
# Create root container for the menu/HUD
root = mcrfpy.Frame(pos=(50, 50), size=(300, 400))
root.fill_color = mcrfpy.Color(30, 30, 40)
root.outline_color = mcrfpy.Color(80, 80, 100)
root.outline = 2
ui.append(root)
# Add widgets to root.children...
mcrfpy.setScene("menu")
```
---
## Button
A clickable frame with label and hover feedback.
```python
def make_button(parent, pos, text, on_click):
"""Create a button with hover effects."""
btn = mcrfpy.Frame(pos=pos, size=(120, 32))
btn.fill_color = mcrfpy.Color(60, 60, 80)
btn.outline_color = mcrfpy.Color(100, 100, 140)
btn.outline = 1
label = mcrfpy.Caption(pos=(10, 6), text=text)
label.fill_color = mcrfpy.Color(220, 220, 220)
btn.children.append(label)
# Hover effects
def on_enter():
btn.fill_color = mcrfpy.Color(80, 80, 110)
btn.outline_color = mcrfpy.Color(140, 140, 180)
def on_exit():
btn.fill_color = mcrfpy.Color(60, 60, 80)
btn.outline_color = mcrfpy.Color(100, 100, 140)
btn.on_enter = on_enter
btn.on_exit = on_exit
btn.on_click = lambda x, y, btn: on_click()
parent.children.append(btn)
return btn
# 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
def make_toggle(parent, pos, label_text, initial=False, on_change=None):
"""Create a toggle with visual state indicator."""
state = {"checked": initial}
frame = mcrfpy.Frame(pos=pos, size=(160, 28))
frame.fill_color = mcrfpy.Color(40, 40, 50)
# Checkbox indicator
indicator = mcrfpy.Frame(pos=(6, 6), size=(16, 16))
indicator.outline = 1
indicator.outline_color = mcrfpy.Color(120, 120, 140)
frame.children.append(indicator)
# Label
label = mcrfpy.Caption(pos=(30, 5), text=label_text)
label.fill_color = mcrfpy.Color(200, 200, 200)
frame.children.append(label)
def update_visual():
if state["checked"]:
indicator.fill_color = mcrfpy.Color(100, 180, 100)
else:
indicator.fill_color = mcrfpy.Color(50, 50, 60)
def toggle(x, y, btn):
state["checked"] = not state["checked"]
update_visual()
if on_change:
on_change(state["checked"])
frame.on_click = toggle
update_visual()
parent.children.append(frame)
return state # Return state dict for external access
# 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.
```python
class VerticalMenu:
def __init__(self, parent, pos, options, on_select):
"""
options: list of (label, value) tuples
on_select: callback(value) when option chosen
"""
self.options = options
self.on_select = on_select
self.selected = 0
self.frame = mcrfpy.Frame(pos=pos, size=(180, len(options) * 28 + 8))
self.frame.fill_color = mcrfpy.Color(35, 35, 45)
parent.children.append(self.frame)
self.items = []
for i, (label, value) in enumerate(options):
item = mcrfpy.Caption(pos=(12, 4 + i * 28), text=label)
item.fill_color = mcrfpy.Color(180, 180, 180)
self.frame.children.append(item)
self.items.append(item)
self._update_highlight()
def _update_highlight(self):
for i, item in enumerate(self.items):
if i == self.selected:
item.fill_color = mcrfpy.Color(255, 220, 100)
else:
item.fill_color = mcrfpy.Color(180, 180, 180)
def move_up(self):
self.selected = (self.selected - 1) % len(self.options)
self._update_highlight()
def move_down(self):
self.selected = (self.selected + 1) % len(self.options)
self._update_highlight()
def confirm(self):
_, value = self.options[self.selected]
self.on_select(value)
# Usage
menu = VerticalMenu(root, (20, 20), [
("Continue", "continue"),
("New Game", "new"),
("Options", "options"),
("Quit", "quit")
], on_select=handle_menu_choice)
def handle_key(key, pressed):
if not pressed:
return
if key == "Up":
menu.move_up()
elif key == "Down":
menu.move_down()
elif key == "Enter":
menu.confirm()
mcrfpy.keypressScene(handle_key)
```
---
## Modal Dialog
A dialog that captures all input until dismissed.
```python
class ModalDialog:
def __init__(self, message, on_dismiss=None):
self.on_dismiss = on_dismiss
self.ui = mcrfpy.sceneUI(mcrfpy.currentScene())
# Semi-transparent backdrop
self.backdrop = mcrfpy.Frame(pos=(0, 0), size=(1024, 768))
self.backdrop.fill_color = mcrfpy.Color(0, 0, 0, 160)
self.backdrop.z_index = 900
self.backdrop.on_click = lambda x, y, b: None # Block clicks
self.ui.append(self.backdrop)
# Dialog box
self.dialog = mcrfpy.Frame(pos=(312, 284), size=(400, 200))
self.dialog.fill_color = mcrfpy.Color(50, 50, 65)
self.dialog.outline_color = mcrfpy.Color(120, 120, 150)
self.dialog.outline = 2
self.dialog.z_index = 901
self.ui.append(self.dialog)
# Message
msg = mcrfpy.Caption(pos=(20, 20), text=message)
msg.fill_color = mcrfpy.Color(220, 220, 220)
self.dialog.children.append(msg)
# OK button
ok_btn = mcrfpy.Frame(pos=(150, 140), size=(100, 36))
ok_btn.fill_color = mcrfpy.Color(70, 100, 70)
ok_btn.outline = 1
ok_btn.outline_color = mcrfpy.Color(100, 150, 100)
ok_btn.on_click = lambda x, y, b: self.close()
self.dialog.children.append(ok_btn)
ok_label = mcrfpy.Caption(pos=(35, 8), text="OK")
ok_label.fill_color = mcrfpy.Color(220, 255, 220)
ok_btn.children.append(ok_label)
# Capture keyboard
self._prev_handler = None
self._install_key_handler()
def _install_key_handler(self):
def modal_keys(key, pressed):
if pressed and key in ("Enter", "Escape"):
self.close()
mcrfpy.keypressScene(modal_keys)
def close(self):
self.ui.remove(self.backdrop)
self.ui.remove(self.dialog)
if self.on_dismiss:
self.on_dismiss()
# Usage
def show_message(text):
ModalDialog(text)
show_message("Game saved successfully!")
```
---
## Hotbar / Quick Slots
Number keys (1-9) mapped to inventory slots.
```python
class Hotbar:
def __init__(self, parent, pos, slot_count=9):
self.slots = []
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.fill_color = mcrfpy.Color(30, 30, 40, 200)
parent.children.append(self.frame)
for i in range(slot_count):
slot = mcrfpy.Frame(pos=(4 + i * 36, 4), size=(32, 32))
slot.fill_color = mcrfpy.Color(50, 50, 60)
slot.outline = 1
slot.outline_color = mcrfpy.Color(80, 80, 100)
self.frame.children.append(slot)
self.slots.append(slot)
# Slot number label
num = mcrfpy.Caption(pos=(2, 2), text=str((i + 1) % 10))
num.fill_color = mcrfpy.Color(100, 100, 120)
slot.children.append(num)
self._update_selection()
def _update_selection(self):
for i, slot in enumerate(self.slots):
if i == self.selected:
slot.outline_color = mcrfpy.Color(200, 180, 80)
slot.outline = 2
else:
slot.outline_color = mcrfpy.Color(80, 80, 100)
slot.outline = 1
def select(self, index):
if 0 <= index < len(self.slots):
self.selected = index
self._update_selection()
def use_selected(self):
item = self.items[self.selected]
if item:
item.use()
def set_item(self, index, item):
self.items[index] = item
# Update slot visual (add sprite, etc.)
# Usage
hotbar = Hotbar(root, (200, 700))
def handle_key(key, pressed):
if not pressed:
return
# 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)
elif key == "E":
hotbar.use_selected()
mcrfpy.keypressScene(handle_key)
```
---
## Draggable Window
A frame that can be dragged by its title bar.
```python
class DraggableWindow:
def __init__(self, parent, pos, size, title):
self.dragging = False
self.drag_offset = (0, 0)
self.frame = mcrfpy.Frame(pos=pos, size=size)
self.frame.fill_color = mcrfpy.Color(45, 45, 55)
self.frame.outline = 1
self.frame.outline_color = mcrfpy.Color(100, 100, 120)
parent.children.append(self.frame)
# Title bar
self.title_bar = mcrfpy.Frame(pos=(0, 0), size=(size[0], 24))
self.title_bar.fill_color = mcrfpy.Color(60, 60, 80)
self.frame.children.append(self.title_bar)
title_label = mcrfpy.Caption(pos=(8, 4), text=title)
title_label.fill_color = mcrfpy.Color(200, 200, 220)
self.title_bar.children.append(title_label)
# Content area reference
self.content_y = 28
# Drag handling
def start_drag(x, y, btn):
if btn == 0: # Left click
self.dragging = True
frame_pos = self.frame.pos
self.drag_offset = (x - frame_pos[0], y - frame_pos[1])
def on_move(x, y):
if self.dragging:
new_x = x - self.drag_offset[0]
new_y = y - self.drag_offset[1]
self.frame.pos = (new_x, new_y)
def stop_drag():
self.dragging = False
self.title_bar.on_click = start_drag
self.title_bar.on_move = on_move
self.title_bar.on_exit = stop_drag
# Usage
window = DraggableWindow(root, (100, 100), (250, 300), "Inventory")
# Add content to window.frame.children at y >= window.content_y
item_list = mcrfpy.Caption(pos=(10, window.content_y + 10), text="Items here...")
window.frame.children.append(item_list)
```
---
## 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*