Page:
UI Widget Patterns
Pages
AI and Pathfinding
Adding Python Bindings
Animation System
Design Proposals
Development Workflow
Entity Management
Grid Interaction Patterns
Grid Rendering Pipeline
Grid System
Grid TCOD Integration
Headless Mode
Home
Input and Events
Issue Roadmap
Performance Optimization Workflow
Performance and Profiling
Procedural Generation
Proposal: Next Gen Grid Entity System
Python Binding Layer
Rendering and Visuals
Strategic Direction
UI Component Hierarchy
UI Widget Patterns
Writing Tests
1
UI Widget Patterns
John McCardle edited this page 2025-11-29 23:44:58 +00:00
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:
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.
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.
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.
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.
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.
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.
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