From be816ac30a3edd8a8f76452b41932ba442187679 Mon Sep 17 00:00:00 2001 From: John McCardle Date: Sat, 29 Nov 2025 23:44:58 +0000 Subject: [PATCH] Add "UI Widget Patterns" --- UI-Widget-Patterns.md | 404 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 404 insertions(+) create mode 100644 UI-Widget-Patterns.md diff --git a/UI-Widget-Patterns.md b/UI-Widget-Patterns.md new file mode 100644 index 0000000..d2f1edb --- /dev/null +++ b/UI-Widget-Patterns.md @@ -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* \ No newline at end of file