Update "UI-Widget-Patterns.-"

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

@ -17,23 +17,19 @@ Most widget patterns fit into this basic structure:
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
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 # 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),
root.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.setScene("menu") mcrfpy.current_scene = scene
``` ```
--- ---
@ -45,35 +41,34 @@ 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),
btn.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(pos=(10, 6), text=text) 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 # Hover effects - on_enter/on_exit receive (pos: Vector)
def on_enter(): btn.on_enter = lambda pos: setattr(btn, 'fill_color',
btn.fill_color = mcrfpy.Color(80, 80, 110) mcrfpy.Color(80, 80, 110))
btn.outline_color = mcrfpy.Color(140, 140, 180) btn.on_exit = lambda pos: setattr(btn, 'fill_color',
mcrfpy.Color(60, 60, 80))
def on_exit(): # Click handler receives (pos: Vector, button: MouseButton, action: InputState)
btn.fill_color = mcrfpy.Color(60, 60, 80) def handle_click(pos, button, action):
btn.outline_color = mcrfpy.Color(100, 100, 140) if button == mcrfpy.MouseButton.LEFT and action == mcrfpy.InputState.PRESSED:
on_click()
btn.on_enter = on_enter btn.on_click = handle_click
btn.on_exit = on_exit
btn.on_click = lambda x, y, btn: on_click()
parent.children.append(btn) parent.children.append(btn)
return btn return btn
# Usage # Usage
make_button(root, (20, 20), "New Game", lambda: mcrfpy.setScene("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: mcrfpy.exit()) make_button(root, (20, 100), "Quit", lambda: sys.exit(0))
``` ```
--- ---
@ -87,8 +82,8 @@ 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),
frame.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))
@ -97,7 +92,7 @@ def make_toggle(parent, pos, label_text, initial=False, on_change=None):
frame.children.append(indicator) frame.children.append(indicator)
# Label # Label
label = mcrfpy.Caption(pos=(30, 5), text=label_text) 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)
@ -107,11 +102,12 @@ def make_toggle(parent, pos, label_text, initial=False, on_change=None):
else: else:
indicator.fill_color = mcrfpy.Color(50, 50, 60) indicator.fill_color = mcrfpy.Color(50, 50, 60)
def toggle(x, y, btn): def toggle(pos, button, action):
state["checked"] = not state["checked"] if button == mcrfpy.MouseButton.LEFT and action == mcrfpy.InputState.PRESSED:
update_visual() state["checked"] = not state["checked"]
if on_change: update_visual()
on_change(state["checked"]) if on_change:
on_change(state["checked"])
frame.on_click = toggle frame.on_click = toggle
update_visual() update_visual()
@ -141,13 +137,14 @@ class VerticalMenu:
self.on_select = on_select self.on_select = on_select
self.selected = 0 self.selected = 0
self.frame = mcrfpy.Frame(pos=pos, size=(180, len(options) * 28 + 8)) self.frame = mcrfpy.Frame(pos=pos,
self.frame.fill_color = mcrfpy.Color(35, 35, 45) size=(180, len(options) * 28 + 8),
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(pos=(12, 4 + i * 28), text=label) 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)
@ -181,17 +178,18 @@ menu = VerticalMenu(root, (20, 20), [
("Quit", "quit") ("Quit", "quit")
], on_select=handle_menu_choice) ], on_select=handle_menu_choice)
def handle_key(key, pressed): # Keyboard navigation via scene.on_key
if not pressed: def handle_key(key, action):
if action != mcrfpy.InputState.PRESSED:
return return
if key == "Up": if key == mcrfpy.Key.UP:
menu.move_up() menu.move_up()
elif key == "Down": elif key == mcrfpy.Key.DOWN:
menu.move_down() menu.move_down()
elif key == "Enter": elif key == mcrfpy.Key.ENTER:
menu.confirm() menu.confirm()
mcrfpy.keypressScene(handle_key) scene.on_key = handle_key
``` ```
--- ---
@ -202,63 +200,63 @@ A dialog that captures all input until dismissed.
```python ```python
class ModalDialog: class ModalDialog:
def __init__(self, message, on_dismiss=None): def __init__(self, scene, message, on_dismiss=None):
self.scene = scene
self.on_dismiss = on_dismiss self.on_dismiss = on_dismiss
self.ui = mcrfpy.sceneUI(mcrfpy.currentScene()) 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),
self.backdrop.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 x, y, b: None # Block clicks self.backdrop.on_click = lambda pos, btn, action: None # Block clicks
self.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),
self.dialog.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
self.ui.append(self.dialog) ui.append(self.dialog)
# Message # Message
msg = mcrfpy.Caption(pos=(20, 20), text=message) 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),
ok_btn.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 x, y, b: 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(pos=(35, 8), text="OK") 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 = None self._prev_handler = scene.on_key
self._install_key_handler()
def _install_key_handler(self): def modal_keys(key, action):
def modal_keys(key, pressed): if action == mcrfpy.InputState.PRESSED:
if pressed and key in ("Enter", "Escape"): if key == mcrfpy.Key.ENTER or key == mcrfpy.Key.ESCAPE:
self.close() self.close()
mcrfpy.keypressScene(modal_keys) scene.on_key = modal_keys
def close(self): def close(self):
self.ui.remove(self.backdrop) ui = self.scene.children
self.ui.remove(self.dialog) ui.remove(self.backdrop)
ui.remove(self.dialog)
if 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
def show_message(text): dialog = ModalDialog(scene, "Game saved successfully!")
ModalDialog(text)
show_message("Game saved successfully!")
``` ```
--- ---
@ -271,23 +269,23 @@ Number keys (1-9) mapped to inventory slots.
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 # Item data per slot self.items = [None] * slot_count
self.selected = 0 self.selected = 0
self.frame = mcrfpy.Frame(pos=pos, size=(slot_count * 36 + 8, 44)) self.frame = mcrfpy.Frame(pos=pos,
self.frame.fill_color = mcrfpy.Color(30, 30, 40, 200) size=(slot_count * 36 + 8, 44),
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),
slot.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)
# Slot number label num = mcrfpy.Caption(text=str((i + 1) % 10), pos=(2, 2))
num = mcrfpy.Caption(pos=(2, 2), text=str((i + 1) % 10))
num.fill_color = mcrfpy.Color(100, 100, 120) num.fill_color = mcrfpy.Color(100, 100, 120)
slot.children.append(num) slot.children.append(num)
@ -307,30 +305,23 @@ class Hotbar:
self.selected = index self.selected = index
self._update_selection() 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 # Usage
hotbar = Hotbar(root, (200, 700)) hotbar = Hotbar(root, (200, 700))
def handle_key(key, pressed): # Key mapping for number keys
if not pressed: num_keys = {
return mcrfpy.Key.NUM_1: 0, mcrfpy.Key.NUM_2: 1, mcrfpy.Key.NUM_3: 2,
# Number keys select slots mcrfpy.Key.NUM_4: 3, mcrfpy.Key.NUM_5: 4, mcrfpy.Key.NUM_6: 5,
if key.startswith("Num") and len(key) == 4: mcrfpy.Key.NUM_7: 6, mcrfpy.Key.NUM_8: 7, mcrfpy.Key.NUM_9: 8,
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) def handle_key(key, action):
if action != mcrfpy.InputState.PRESSED:
return
if key in num_keys:
hotbar.select(num_keys[key])
scene.on_key = handle_key
``` ```
--- ---
@ -345,38 +336,38 @@ class DraggableWindow:
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,
self.frame.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),
self.title_bar.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(pos=(8, 4), text=title) 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)
# Content area reference
self.content_y = 28 self.content_y = 28
# Drag handling # Drag handling
def start_drag(x, y, btn): # on_click: (pos: Vector, button: MouseButton, action: InputState)
if btn == 0: # Left click def start_drag(pos, button, action):
if button == mcrfpy.MouseButton.LEFT and action == mcrfpy.InputState.PRESSED:
self.dragging = True self.dragging = True
frame_pos = self.frame.pos self.drag_offset = (pos.x - self.frame.x, pos.y - self.frame.y)
self.drag_offset = (x - frame_pos[0], y - frame_pos[1])
def on_move(x, y): # on_move: (pos: Vector)
def on_move(pos):
if self.dragging: if self.dragging:
new_x = x - self.drag_offset[0] self.frame.x = pos.x - self.drag_offset[0]
new_y = y - self.drag_offset[1] self.frame.y = pos.y - self.drag_offset[1]
self.frame.pos = (new_x, new_y)
def stop_drag(): # on_exit: (pos: Vector)
def stop_drag(pos):
self.dragging = False self.dragging = False
self.title_bar.on_click = start_drag self.title_bar.on_click = start_drag
@ -387,7 +378,7 @@ class DraggableWindow:
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(pos=(10, window.content_y + 10), text="Items here...") 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)
``` ```
@ -401,4 +392,4 @@ window.frame.children.append(item_list)
--- ---
*Last updated: 2025-11-29* *Last updated: 2026-02-07*