feat: v0.2.0 multi-active panels with layout-slots
Migrate the internal model from single-active scalar to multi-active
open_order array + per-id window records. register accepts opts.layout
(11 named slots: center, left, right, top, bottom, the four corners,
left-half, right-half — plus a custom function(sw,sh)->{x,y,w,h} hook
for HP-bars and bespoke positioning). open/close/is_open accept an
optional id argument for per-id semantics; the no-arg forms preserve
the v0.1.1 contract (close() removes the focused last-opened, is_open()
returns true if any panel is open).
Input dispatch iterates open_order in reverse so the topmost panel gets
the first crack at a click; misses fall through to lower windows.
is_pausing now checks every open window for pause_on_open=true, not
just the focused one.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
131
README.md
131
README.md
@@ -1,11 +1,17 @@
|
||||
# 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.
|
||||
Generic overlay-panel framework. v0.2.0 supports multiple panels open at
|
||||
the same time with 11 named layout-slot templates + custom-fn hook for
|
||||
bespoke positioning. Handles input dispatch (mouse click + wheel,
|
||||
edge-detected with reverse-open-order hit-test), renders titled panel
|
||||
overlays, and supports a context-menu layer on top. Designed as the glue
|
||||
layer between game modules and the engine render/input surfaces.
|
||||
|
||||
**Version:** 0.1.1
|
||||
The single-active model from v0.1.1 is preserved as a backward-
|
||||
compatibility shim — `panel.open(id)`, `panel.close()`, `panel.is_open()`
|
||||
without an id-arg keep their old semantics.
|
||||
|
||||
**Version:** 0.2.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
|
||||
@@ -45,9 +51,11 @@ v0.1 ships a minimal single-active-widget panel system:
|
||||
|
||||
## API
|
||||
|
||||
### `panel.register(widget_id, widget_def)`
|
||||
### `panel.register(widget_id, widget_def, opts?)`
|
||||
|
||||
**Syntax:** `panel.register(widget_id: string, widget_def: table) -> void`
|
||||
**Syntax:** `panel.register(widget_id: string, widget_def: table, opts?: table) -> void`
|
||||
|
||||
`opts` is optional. Accepted keys: `layout` (slot-name string or `function(sw, sh) -> {x,y,w,h}`; defaults to `"center"`) and `z_tier` (`"normal"`; v0.2.0 only the normal tier is exposed). See the [Multi-Active + Layout-Slots](#v020--multi-active--layout-slots) section below for the full slot table.
|
||||
|
||||
**Example:**
|
||||
```lua
|
||||
@@ -86,16 +94,20 @@ closes the panel (sets active to nil, clears any open context-menu).
|
||||
|
||||
**Syntax:** `panel.open(widget_id: string) -> void`
|
||||
|
||||
Sets `widget_id` as the active widget. Loud-error if `widget_id` has not
|
||||
been registered.
|
||||
Opens `widget_id`. The window is pushed onto the top of `open_order`
|
||||
(becomes focused). Other open windows remain open. If `widget_id` is
|
||||
already open, it is brought to the front (re-focused). Loud-error if
|
||||
`widget_id` has not been registered.
|
||||
|
||||
---
|
||||
|
||||
### `panel.close()`
|
||||
### `panel.close(widget_id?)`
|
||||
|
||||
**Syntax:** `panel.close() -> void`
|
||||
**Syntax:** `panel.close(widget_id?: string) -> void`
|
||||
|
||||
Closes the active widget and clears any open context-menu.
|
||||
With `widget_id`: closes that specific window. Without arg (v0.1.1
|
||||
bw-compat): closes the focused (last-opened) window. Clears any open
|
||||
context-menu when the last open window is closed.
|
||||
|
||||
---
|
||||
|
||||
@@ -103,16 +115,18 @@ Closes the active widget and clears any open context-menu.
|
||||
|
||||
**Syntax:** `panel.toggle(widget_id: string) -> void`
|
||||
|
||||
If `widget_id` is currently active, closes it. Otherwise opens it. Handy
|
||||
If `widget_id` is currently open, closes it. Otherwise opens it. Handy
|
||||
for key-binding toggle semantics without manual state tracking.
|
||||
|
||||
---
|
||||
|
||||
### `panel.is_open()`
|
||||
### `panel.is_open(widget_id?)`
|
||||
|
||||
**Syntax:** `panel.is_open() -> bool`
|
||||
**Syntax:** `panel.is_open(widget_id?: string) -> bool`
|
||||
|
||||
Returns `true` if any widget is currently active.
|
||||
With `widget_id`: returns `true` iff that specific window is currently
|
||||
open. Without arg (v0.1.1 bw-compat): returns `true` if ANY window is
|
||||
open.
|
||||
|
||||
---
|
||||
|
||||
@@ -120,9 +134,9 @@ Returns `true` if any widget is currently active.
|
||||
|
||||
**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).
|
||||
Returns `true` if ANY currently-open window has `pause_on_open = true`.
|
||||
Modules can use this to gate their update loop (skip physics/AI while a
|
||||
pausing panel is open).
|
||||
|
||||
---
|
||||
|
||||
@@ -220,6 +234,85 @@ 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).
|
||||
|
||||
---
|
||||
|
||||
## v0.2.0 — Multi-Active + Layout-Slots
|
||||
|
||||
Multiple panels can now be open at the same time. The previous
|
||||
single-active model is preserved as the default for v0.1.1 callers via
|
||||
a backward-compatibility shim.
|
||||
|
||||
### Layout-Slots
|
||||
|
||||
`panel.register(id, widget_def, {layout = ...})` accepts:
|
||||
|
||||
- `nil` → `"center"` default.
|
||||
- One of the 11 named templates (see table below).
|
||||
- A custom function `function(sw, sh) -> {x, y, w, h}` for bespoke
|
||||
positioning (HP-bar, status-display, etc.).
|
||||
|
||||
| Slot | x | y | w | h |
|
||||
|---|---|---|---|---|
|
||||
| `"center"` (default) | 25% sw | 20% sh | 50% sw | 60% sh |
|
||||
| `"left"` | 0 | 0 | 40% sw | sh |
|
||||
| `"right"` | 60% sw | 0 | 40% sw | sh |
|
||||
| `"top"` | 0 | 0 | sw | 30% sh |
|
||||
| `"bottom"` | 0 | 70% sh | sw | 30% sh |
|
||||
| `"top-left"` | 0 | 0 | 40% sw | 50% sh |
|
||||
| `"top-right"` | 60% sw | 0 | 40% sw | 50% sh |
|
||||
| `"bottom-left"` | 0 | 50% sh | 40% sw | 50% sh |
|
||||
| `"bottom-right"` | 60% sw | 50% sh | 40% sw | 50% sh |
|
||||
| `"left-half"` | 0 | 0 | 50% sw | sh |
|
||||
| `"right-half"` | 50% sw | 0 | 50% sw | sh |
|
||||
|
||||
Unknown slot-name strings raise a loud-error at `panel.register` (so
|
||||
typos surface immediately, not in the next frame's render). Custom
|
||||
functions that error at runtime fall back to `"center"` with an
|
||||
`engine.print` warning.
|
||||
|
||||
Bounds are re-resolved every render frame, so layout-slots react to
|
||||
screen-resizes automatically.
|
||||
|
||||
### Multi-Active API
|
||||
|
||||
- `panel.open(id)` — opens; does NOT close other open panels. If already
|
||||
open, brings to front (re-focused).
|
||||
- `panel.close(id)` — closes that specific id.
|
||||
- `panel.close()` — bw-compat: closes the focused (last-opened) panel.
|
||||
- `panel.is_open(id)` — id-specific.
|
||||
- `panel.is_open()` — bw-compat: any panel open.
|
||||
- `panel.is_pausing()` — true if ANY open panel has `pause_on_open=true`.
|
||||
|
||||
### Hit-Test Order
|
||||
|
||||
Input events dispatch through panels in **reverse open-order** (last-
|
||||
opened first). A click that hits a panel's bounds is consumed by that
|
||||
panel and not propagated further. A click that misses all panels is
|
||||
dropped (v0.2.0 does not route to game; v0.3+ may add that).
|
||||
|
||||
Wheel events route to the focused (last-opened) panel only.
|
||||
|
||||
### Bw-Compat Guarantee
|
||||
|
||||
All v0.1.1 callers (`register(id, widget)` without opts, `open(id)`,
|
||||
`close()`, `is_open()`) keep their existing semantics. The one
|
||||
behavioral change: `panel.open(A)` followed by `panel.open(B)` now
|
||||
leaves BOTH open instead of replacing A.
|
||||
|
||||
If you relied on the v0.1.1 "open(B) closes A" behavior, call
|
||||
`panel.close()` before `panel.open(B)` to keep the single-active idiom.
|
||||
|
||||
### Test backdoors (v0.2.0 additions)
|
||||
|
||||
- `panel._test_reset_all()` — clears widgets + windows + open_order +
|
||||
ctx_menu (theme + triggers preserved).
|
||||
- `panel._test_get_open_ids()` — array of currently-open ids in
|
||||
open-order (last = focused).
|
||||
- `panel._test_get_focused_id()` — last-opened id, or nil.
|
||||
- `panel._test_get_window_bounds(id)` — resolved `{x,y,w,h}` for an open
|
||||
window, or nil if not open.
|
||||
- `panel._test_get_screen_size()` — current screen size used by layouts.
|
||||
|
||||
## Theme Schema
|
||||
|
||||
| Key | Default | Description |
|
||||
|
||||
Reference in New Issue
Block a user