initial: panel v0.1.0 — generic panel framework
This commit is contained in:
305
README.md
Normal file
305
README.md
Normal file
@@ -0,0 +1,305 @@
|
||||
# lib-core.panel
|
||||
|
||||
Generic overlay-panel framework. Manages a single active widget at a time,
|
||||
handles input dispatch (mouse click + wheel, edge-detected), renders a
|
||||
titled panel overlay, and supports a context-menu layer. Designed as the
|
||||
glue layer between game modules and the engine render/input surfaces.
|
||||
|
||||
**Version:** 0.1.0
|
||||
**Lib-ID:** lib-core.panel
|
||||
**Requires:** engine.render.*, engine.input.*, lib-core.input (lazy, for default-trigger)
|
||||
**Tags:** panel, overlay, ui, input, context-menu
|
||||
|
||||
## Topology
|
||||
|
||||
<!-- topology:start (auto-generated; do not edit) -->
|
||||
```mermaid
|
||||
graph LR
|
||||
this["lib-core.panel"]
|
||||
engine_render["engine.render.*"]
|
||||
engine_input["engine.input.*"]
|
||||
lib_core_input["lib-core.input"]
|
||||
this --> engine_render
|
||||
this --> engine_input
|
||||
this -.->|lazy| lib_core_input
|
||||
```
|
||||
<!-- topology:end -->
|
||||
|
||||
## Scope (v0.1.0)
|
||||
|
||||
v0.1 ships a minimal single-active-widget panel system:
|
||||
|
||||
- Widget registry (register / unregister / open / close / toggle)
|
||||
- Theme system (13 configurable keys, capability-by-declaration guard)
|
||||
- Per-frame update with edge-detected mouse click + wheel dispatch
|
||||
- Context-menu (show, auto-reposition to screen bounds, hit-test, auto-close)
|
||||
- Default-trigger key binding via lib-core.input (lazy-required)
|
||||
- pause_on_open flag for game-pause gating
|
||||
|
||||
**Intentional non-goals (deferred):**
|
||||
- Multi-widget z-order / stacking panels
|
||||
- Always-on HUD widgets (non-modal overlays)
|
||||
- Keyboard navigation within widgets
|
||||
- Panel animation (fade in / out)
|
||||
- Screen-size from engine (no Lua-accessible get_screen_size; v0.1 falls back to 1280x720 constants matching default Sporel window config)
|
||||
|
||||
## API
|
||||
|
||||
### `panel.register(widget_id, widget_def)`
|
||||
|
||||
**Syntax:** `panel.register(widget_id: string, widget_def: table) -> void`
|
||||
|
||||
**Example:**
|
||||
```lua
|
||||
panel.register("inventory", {
|
||||
title = "Inventory",
|
||||
pause_on_open = true,
|
||||
render = function(ctx)
|
||||
-- ctx.bounds = {x, y, w, h}; ctx.theme; ctx.is_focused
|
||||
engine.render.draw_text("(empty)", ctx.bounds.x, ctx.bounds.y,
|
||||
ctx.theme.font_size_body, ctx.theme.text_color)
|
||||
end,
|
||||
handle_input = function(ctx, event)
|
||||
-- event.kind = "click"|"wheel"
|
||||
-- event.x, event.y, event.button (click only)
|
||||
-- event.dy (wheel only)
|
||||
end,
|
||||
})
|
||||
```
|
||||
|
||||
Registers a widget in the panel registry. `widget_def.render` and
|
||||
`widget_def.handle_input` must be functions; `widget_def.title` must be a
|
||||
string. Loud-error on duplicate `widget_id` or missing required fields.
|
||||
|
||||
---
|
||||
|
||||
### `panel.unregister(widget_id)`
|
||||
|
||||
**Syntax:** `panel.unregister(widget_id: string) -> void`
|
||||
|
||||
Removes the widget from the registry. If the widget is currently active,
|
||||
closes the panel (sets active to nil, clears any open context-menu).
|
||||
|
||||
---
|
||||
|
||||
### `panel.open(widget_id)`
|
||||
|
||||
**Syntax:** `panel.open(widget_id: string) -> void`
|
||||
|
||||
Sets `widget_id` as the active widget. Loud-error if `widget_id` has not
|
||||
been registered.
|
||||
|
||||
---
|
||||
|
||||
### `panel.close()`
|
||||
|
||||
**Syntax:** `panel.close() -> void`
|
||||
|
||||
Closes the active widget and clears any open context-menu.
|
||||
|
||||
---
|
||||
|
||||
### `panel.toggle(widget_id)`
|
||||
|
||||
**Syntax:** `panel.toggle(widget_id: string) -> void`
|
||||
|
||||
If `widget_id` is currently active, closes it. Otherwise opens it. Handy
|
||||
for key-binding toggle semantics without manual state tracking.
|
||||
|
||||
---
|
||||
|
||||
### `panel.is_open()`
|
||||
|
||||
**Syntax:** `panel.is_open() -> bool`
|
||||
|
||||
Returns `true` if any widget is currently active.
|
||||
|
||||
---
|
||||
|
||||
### `panel.is_pausing()`
|
||||
|
||||
**Syntax:** `panel.is_pausing() -> bool`
|
||||
|
||||
Returns `true` if the active widget has `pause_on_open = true`. Modules
|
||||
can use this to gate their update loop (skip physics/AI while panel is
|
||||
open).
|
||||
|
||||
---
|
||||
|
||||
### `panel.set_theme(overrides)`
|
||||
|
||||
**Syntax:** `panel.set_theme(overrides: table) -> void`
|
||||
|
||||
**Example:**
|
||||
```lua
|
||||
panel.set_theme({
|
||||
bg_color = 0x101010F8,
|
||||
padding = 12,
|
||||
font_size_body = 16,
|
||||
})
|
||||
```
|
||||
|
||||
Merges `overrides` into the active theme. Only keys declared in
|
||||
`DEFAULT_THEME` are accepted (capability-by-declaration). Loud-error on
|
||||
any unknown key with message `"panel.set_theme: unknown theme key '<k>'"`.
|
||||
|
||||
---
|
||||
|
||||
### `panel.get_theme()`
|
||||
|
||||
**Syntax:** `panel.get_theme() -> table`
|
||||
|
||||
Returns a shallow copy of the current merged theme. Safe to store; does
|
||||
not alias internal state.
|
||||
|
||||
---
|
||||
|
||||
### `panel.bind_default_trigger(key, widget_id)`
|
||||
|
||||
**Syntax:** `panel.bind_default_trigger(key: string|nil, widget_id: string) -> void`
|
||||
|
||||
**Example:**
|
||||
```lua
|
||||
panel.register("inventory", { ... })
|
||||
panel.bind_default_trigger("tab", "inventory")
|
||||
-- or: panel.bind_default_trigger(nil, "inventory") -- defaults to "tab"
|
||||
```
|
||||
|
||||
Binds a keyboard key as the default toggle trigger for `widget_id`. `key`
|
||||
defaults to `"tab"` when `nil`. Lazy-requires `lib-core.input` to avoid
|
||||
module-load-time cycles. Loud-error if `widget_id` is not registered.
|
||||
|
||||
---
|
||||
|
||||
### `panel.update(dt)`
|
||||
|
||||
**Syntax:** `panel.update(dt: number) -> void`
|
||||
|
||||
Must be called each game-update frame. Checks the default trigger key,
|
||||
reads mouse state, emits edge-detected click events (left and right
|
||||
independently), and dispatches wheel events. No-ops if no widget is active
|
||||
(but still tracks mouse state to avoid spurious edges on next open).
|
||||
|
||||
---
|
||||
|
||||
### `panel.render()`
|
||||
|
||||
**Syntax:** `panel.render() -> void`
|
||||
|
||||
Must be called each render frame (inside the engine render phase). Draws
|
||||
the panel background, border, and title, then invokes `widget_def.render(ctx)`.
|
||||
If a context-menu is open, renders it on top. No-op if no widget is active.
|
||||
|
||||
---
|
||||
|
||||
### `panel.show_context_menu(x, y, actions)`
|
||||
|
||||
**Syntax:** `panel.show_context_menu(x: number, y: number, actions: table) -> void`
|
||||
|
||||
**Example:**
|
||||
```lua
|
||||
panel.show_context_menu(event.x, event.y, {
|
||||
{ label = "Use", callback = function(info) use_item() end },
|
||||
{ label = "Drop", callback = function(info) drop_item() end },
|
||||
})
|
||||
```
|
||||
|
||||
Pops up a context-menu at `(x, y)`. `actions` must be a non-empty array of
|
||||
`{label: string, callback: function}` tables. Auto-repositions to remain
|
||||
within the screen boundary. The callback receives `{close_menu: function}`
|
||||
(which is a no-op in v0.1 since the menu closes itself before invoking the
|
||||
callback). Click outside any row auto-closes the menu.
|
||||
|
||||
---
|
||||
|
||||
### `panel._dispatch_event_for_test(event)` — for tests only
|
||||
|
||||
**Syntax:** `panel._dispatch_event_for_test(event: table) -> void`
|
||||
|
||||
Direct passthrough to the internal `_dispatch_event` function. Allows
|
||||
test-modules to simulate input events without a running game loop (since
|
||||
`M.update` is not exercised headless).
|
||||
|
||||
## Theme Schema
|
||||
|
||||
| Key | Default | Description |
|
||||
|-----|---------|-------------|
|
||||
| `bg_color` | `0x202020F0` | Panel background (RGBA packed) |
|
||||
| `border_color` | `0x808080FF` | Panel border |
|
||||
| `text_color` | `0xE0E0E0FF` | Primary text |
|
||||
| `text_color_dim` | `0xA0A0A0FF` | Dimmed / secondary text |
|
||||
| `selection_color` | `0x404080FF` | Row / item selection highlight |
|
||||
| `context_menu_bg` | `0x303030F8` | Context-menu background |
|
||||
| `context_menu_hover` | `0x404060FF` | Context-menu row hover |
|
||||
| `font_size_title` | `18` | Title bar font size (px) |
|
||||
| `font_size_body` | `14` | Content font size (px) |
|
||||
| `padding` | `8` | Inner padding (px) |
|
||||
| `row_height` | `24` | Row height for lists / menus (px) |
|
||||
| `panel_width_frac` | `0.5` | Panel width as fraction of screen |
|
||||
| `panel_height_frac` | `0.7` | Panel height as fraction of screen |
|
||||
|
||||
## Widget-Lifecycle-Contract
|
||||
|
||||
Widget `render(ctx)` is called each render frame while the widget is active.
|
||||
Widget `handle_input(ctx, event)` is called for each dispatched input event.
|
||||
|
||||
Both receive a `ctx` table:
|
||||
|
||||
```lua
|
||||
ctx = {
|
||||
bounds = {
|
||||
x = number, -- content area top-left x
|
||||
y = number, -- content area top-left y
|
||||
w = number, -- content area width
|
||||
h = number, -- content area height
|
||||
},
|
||||
theme = table, -- current merged theme (read-only by convention)
|
||||
is_focused = bool, -- true when this widget is the active one
|
||||
}
|
||||
```
|
||||
|
||||
The `event` table passed to `handle_input`:
|
||||
|
||||
```lua
|
||||
-- Mouse click:
|
||||
event = { kind = "click", x = number, y = number, button = "left"|"right" }
|
||||
|
||||
-- Mouse wheel:
|
||||
event = { kind = "wheel", dy = number } -- dy > 0 = scroll up, dy < 0 = scroll down
|
||||
```
|
||||
|
||||
Context-menu intercepts clicks before they reach `handle_input`. The widget
|
||||
calls `panel.show_context_menu(x, y, actions)` from within `handle_input`
|
||||
when a right-click (or any application-specific trigger) warrants a menu.
|
||||
|
||||
## Glue-Pattern
|
||||
|
||||
Minimal module setup (copy-paste-able):
|
||||
|
||||
```lua
|
||||
local panel = require("lib-core.panel")
|
||||
|
||||
-- 1. Register your widget once (e.g. in module init or M.load)
|
||||
panel.register("inventory", {
|
||||
title = "Inventory",
|
||||
pause_on_open = true,
|
||||
render = function(ctx) inventory_ui.draw(ctx) end,
|
||||
handle_input = function(ctx, event) inventory_ui.on_input(ctx, event) end,
|
||||
})
|
||||
|
||||
-- 2. Bind a toggle key (optional; defaults to Tab)
|
||||
panel.bind_default_trigger("i", "inventory")
|
||||
|
||||
-- 3. Wire into your module's update + render
|
||||
function M.update(dt)
|
||||
panel.update(dt)
|
||||
if not panel.is_pausing() then
|
||||
-- normal game logic here
|
||||
end
|
||||
end
|
||||
|
||||
function M.render()
|
||||
-- ... draw world ...
|
||||
panel.render() -- draws panel overlay on top
|
||||
end
|
||||
```
|
||||
Reference in New Issue
Block a user