feat: v0.4.0 Focus + Drag + Resize
Adds focus/raise/lower/get_focused API with click-to-focus auto-routing (hud-tier never focusable), drag-by-title-bar with 32px screen-clamp, resize via SE-corner handle with min/max_size clamps. Drag and resize move bounds outside the layout-fn via a per-window bounds_override that shadows resolve_bounds across render + dispatch + point_in_any_panel. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
180
README.md
180
README.md
@@ -1,11 +1,12 @@
|
||||
# lib-core.panel
|
||||
|
||||
Generic overlay-panel framework. v0.3.0 layers **z-tiers** (hud / normal
|
||||
/ top), **persistent windows**, and **input-block routing** (none / self
|
||||
/ all) on top of v0.2.0's multi-active model with 11 named layout-slot
|
||||
templates + custom-fn hook. Render iterates per-tier (hud → normal →
|
||||
top); input dispatch iterates reverse (top → hud, within-tier reverse
|
||||
open-order); modal `input_block="all"` swallows misses.
|
||||
Generic overlay-panel framework. v0.4.0 adds the **focus API** (`focus`
|
||||
/ `raise` / `lower` / `get_focused`), **click-to-focus** auto-routing,
|
||||
**drag-by-title-bar** (`draggable=true`), and **resize-by-SE-corner**
|
||||
(`resizable=true`) on top of v0.3.0's z-tier + persistent + input-block
|
||||
model. Drag and resize move bounds outside the layout-fn via a
|
||||
window-record `bounds_override`; min/max_size constraints and a
|
||||
32px-of-title-bar screen-clamp keep windows recoverable.
|
||||
|
||||
The single-active model from v0.1.1 is preserved as a backward-
|
||||
compatibility shim — `panel.open(id)`, `panel.close()`, `panel.is_open()`
|
||||
@@ -15,10 +16,10 @@ without an id-arg keep their old semantics. v0.3.0 additionally makes
|
||||
toolbars/HUDs); use `panel.close(id)` for explicit modder-controlled
|
||||
close.
|
||||
|
||||
**Version:** 0.3.0
|
||||
**Version:** 0.4.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, window-manager
|
||||
**Tags:** panel, overlay, ui, input, context-menu, window-manager, focus, drag, resize
|
||||
|
||||
## Topology
|
||||
|
||||
@@ -35,16 +36,36 @@ graph LR
|
||||
```
|
||||
<!-- topology:end -->
|
||||
|
||||
## Scope (v0.3.0)
|
||||
## Scope (v0.4.0)
|
||||
|
||||
v0.3.0 layers z-tiers + persistent + input-block on top of v0.2.0's
|
||||
multi-active model, fully backward-compatible with v0.2.0 and v0.1.1
|
||||
callers. The lib is the generic UI-Framework substrate — domain-free
|
||||
per ADR-0001/ADR-0049, consumed by Display-Libs (inventory-list-
|
||||
display, crafting-display, notify-display) and modules (vagrant-
|
||||
skeleton, map-editor).
|
||||
v0.4.0 adds focus API + click-to-focus + drag + resize on top of v0.3.0,
|
||||
fully backward-compatible with v0.3.0, v0.2.0, and v0.1.1 callers. The
|
||||
lib is the generic UI-Framework substrate — domain-free per ADR-0001/
|
||||
ADR-0049, consumed by Display-Libs (inventory-list-display, crafting-
|
||||
display, notify-display) and modules (vagrant-skeleton, map-editor).
|
||||
|
||||
### Supported (v0.3.0)
|
||||
### Supported (v0.4.0 — new)
|
||||
|
||||
- **Focus API**: `panel.focus(id)` / `panel.raise(id)` (alias) bring a
|
||||
widget to the top within open_order; `panel.lower(id)` moves it to
|
||||
the bottom; `panel.get_focused()` returns the currently-focused id
|
||||
(top-tier priority, else last-opened). hud-tier widgets are never
|
||||
promoted by `focus()` (they are cosmetic overlays by design).
|
||||
- **Click-to-focus**: a click that hits a non-hud window auto-routes
|
||||
through `focus()` before forwarding to the widget's handler.
|
||||
- **Drag**: `opts.draggable=true` makes left-click in the title-bar
|
||||
area start a drag. Drag updates a per-window `bounds_override` that
|
||||
shadows the layout-fn. Screen-clamp keeps at least 32px of the
|
||||
title-bar reachable. Chromeless widgets are excluded (no defined
|
||||
drag-handle).
|
||||
- **Resize**: `opts.resizable=true` renders a 12x12 SE-corner handle
|
||||
and starts a resize on left-click. Resize clamps to `opts.min_size`
|
||||
(default `{w=120, h=80}`) and `opts.max_size` (default
|
||||
`{w=99999, h=99999}`).
|
||||
- `bounds_override` survives the layout-fn until the modder closes the
|
||||
window or the test backdoor `_test_reset_all()` is called.
|
||||
|
||||
### Supported (v0.3.0 — unchanged)
|
||||
|
||||
- **Z-tiers**: render order hud → normal → top. Input dispatch reverse
|
||||
(top → hud, within-tier reverse open-order).
|
||||
@@ -59,9 +80,7 @@ skeleton, map-editor).
|
||||
hud→none, normal→self, top→all.
|
||||
- **Public `panel.point_in_any_panel(x, y)`**: helper for module-side
|
||||
canvas-click logic — returns true if (x,y) falls within any open
|
||||
panel's bounds. Useful for modules that read input directly via
|
||||
`engine.input.*` and want to skip canvas-paint when the click landed
|
||||
inside a panel widget.
|
||||
panel's bounds. v0.4.0: respects `bounds_override` from drag/resize.
|
||||
- Multi-active panels: multiple windows open simultaneously (unchanged
|
||||
from v0.2.0).
|
||||
- 11 layout-slot templates + custom-fn hook (unchanged).
|
||||
@@ -72,15 +91,9 @@ skeleton, map-editor).
|
||||
- Context-menu via `show_context_menu` (unchanged).
|
||||
- Default-trigger key-binding via `bind_default_trigger` (unchanged).
|
||||
|
||||
### Deferred (v0.4.0+)
|
||||
|
||||
- Click-to-focus and explicit focus()/raise()/lower() API.
|
||||
- Drag-by-title-bar (draggable opt).
|
||||
- Resize-by-corner (resizable opt).
|
||||
- min/max-size constraints + screen-clamp.
|
||||
|
||||
### Deferred (post-v0.4.0)
|
||||
|
||||
- Resize corners other than SE (NE/SW/NW + edge-resize).
|
||||
- Window-decoration themes (per-window title-bar styles).
|
||||
- Touch/mobile input adaptation.
|
||||
- Window animations (slide-in, fade-in).
|
||||
@@ -109,6 +122,16 @@ skeleton, map-editor).
|
||||
the FULL panel bounds as `ctx.bounds` (no title-bar inset) and must
|
||||
render its own background/border. Intended for persistent HUD widgets
|
||||
(toolbars, layer-pickers, palettes) that have their own visual style.
|
||||
- `draggable` — `bool` (default `false`, v0.4.0+). If true, a left-click
|
||||
in the title-bar area starts a drag-by-mouse. Ignored on chromeless
|
||||
widgets (no defined drag-handle).
|
||||
- `resizable` — `bool` (default `false`, v0.4.0+). If true, the window
|
||||
renders a 12x12 SE-corner handle that left-click starts a resize.
|
||||
Works on chromeless widgets too.
|
||||
- `min_size` — `{w=number, h=number}` (default `{w=120, h=80}`, v0.4.0+).
|
||||
Minimum window dimensions when resizing.
|
||||
- `max_size` — `{w=number, h=number}` (default `{w=99999, h=99999}`,
|
||||
v0.4.0+). Maximum window dimensions when resizing.
|
||||
|
||||
**Example:**
|
||||
```lua
|
||||
@@ -548,3 +571,108 @@ function M.render()
|
||||
panel.render() -- draws panel overlay on top
|
||||
end
|
||||
```
|
||||
|
||||
## v0.4.0 — Focus + Drag + Resize
|
||||
|
||||
### Focus API
|
||||
|
||||
```lua
|
||||
panel.focus(widget_id) -- bring to top within open_order (no-op for hud)
|
||||
panel.raise(widget_id) -- alias for focus(); reads as "raise to top"
|
||||
panel.lower(widget_id) -- move to bottom of open_order
|
||||
panel.get_focused() -- returns focused widget_id, or nil
|
||||
```
|
||||
|
||||
`focus(id)` removes `id` from `open_order` and re-appends at the top.
|
||||
**Hud-tier widgets are never focusable** — `focus()` and click-to-focus
|
||||
both no-op for them, since hud is by design a cosmetic overlay tier.
|
||||
`lower(id)` works for all tiers (lowering within tier is a layering
|
||||
operation, not focus).
|
||||
|
||||
`get_focused()` returns the topmost-tier last-opened widget: if any
|
||||
top-tier window is open it wins (regardless of normal-/hud-tier windows
|
||||
later in `open_order`), otherwise it returns the simple last-opened id.
|
||||
Returns `nil` if no windows are open.
|
||||
|
||||
### Click-to-Focus
|
||||
|
||||
A left-click that hits a non-hud window's bounds auto-focuses that
|
||||
widget before forwarding to its handler. This is the standard window-
|
||||
manager behavior modders expect from desktop UI. The auto-focus call
|
||||
goes through `M.focus`, so hud-tier widgets are naturally excluded.
|
||||
|
||||
### Drag
|
||||
|
||||
```lua
|
||||
panel.register("toolbox", widget, {
|
||||
layout = function(_sw, _sh) return {x=100, y=100, w=240, h=300} end,
|
||||
draggable = true,
|
||||
})
|
||||
```
|
||||
|
||||
Left-click in the **title-bar area** (the top `font_size_title + 2 *
|
||||
padding` pixels of the window) of a `draggable=true` window starts a
|
||||
drag. The window's `bounds_override` is updated each frame to follow
|
||||
the mouse, with a **32-pixel screen-clamp** so the title-bar always
|
||||
remains reachable on at least one edge — drag can push the window
|
||||
mostly off-screen but cannot lose it entirely.
|
||||
|
||||
Chromeless widgets are excluded from drag-detection (their title-bar
|
||||
is invisible — there is no defined drag-handle). If a modder sets both
|
||||
`chromeless = true` and `draggable = true`, the chromeless guard wins;
|
||||
clicks pass through to the widget's handler unchanged.
|
||||
|
||||
### Resize
|
||||
|
||||
```lua
|
||||
panel.register("toolbox", widget, {
|
||||
layout = function(_sw, _sh) return {x=100, y=100, w=240, h=300} end,
|
||||
resizable = true,
|
||||
min_size = { w = 180, h = 150 },
|
||||
max_size = { w = 600, h = 480 },
|
||||
})
|
||||
```
|
||||
|
||||
`resizable = true` renders a **12x12 SE-corner handle** (filled rect in
|
||||
`border_color`) and starts a resize on left-click within the handle.
|
||||
The handle is drawn even on chromeless widgets — the modder opted in,
|
||||
and the 12px square is the only visual cue. Resize updates the
|
||||
window's `bounds_override` `w/h`; resize **does not move** the
|
||||
top-left corner. Width and height clamp to `opts.min_size` and
|
||||
`opts.max_size`.
|
||||
|
||||
v0.4.0 supports the **SE corner only**. NE/SW/NW + edge-resize are
|
||||
deferred.
|
||||
|
||||
### Bounds-Override Pattern
|
||||
|
||||
When drag or resize first fires, the window's record gains a
|
||||
`bounds_override = {x, y, w, h}` field. From that point on, the helper
|
||||
`get_bounds_for(win, sw, sh)` returns `bounds_override` instead of
|
||||
calling `resolve_bounds(opts.layout, sw, sh)`. All render + dispatch +
|
||||
`point_in_any_panel` callsites use `get_bounds_for`, so the override
|
||||
is consistent across the API.
|
||||
|
||||
`bounds_override` survives until:
|
||||
- The widget is closed via `panel.close(id)` (cleared).
|
||||
- The widget is unregistered.
|
||||
- `panel._test_reset_all()` is called.
|
||||
|
||||
There is no public API to clear `bounds_override` — re-opening a closed
|
||||
draggable widget gives it the layout-fn's bounds again. Persistent
|
||||
windows that drag during a session keep their dragged position until
|
||||
the next process start (no save/load yet — deferred post-v0.4.0).
|
||||
|
||||
### Test backdoors (v0.4.0 additions)
|
||||
|
||||
- `panel._test_simulate_drag_start(id, mx, my)` — force drag-state.
|
||||
- `panel._test_simulate_drag_move(mx, my)` — drive one drag-step.
|
||||
- `panel._test_simulate_drag_end()` — release the drag.
|
||||
- `panel._test_simulate_resize_start(id, mx, my)` — force resize-state.
|
||||
- `panel._test_simulate_resize_move(mx, my)` — drive one resize-step.
|
||||
- `panel._test_simulate_resize_end()` — release the resize.
|
||||
- `panel._test_get_dragging_id()` — currently dragged widget_id, or nil.
|
||||
- `panel._test_get_resizing_id()` — currently resized widget_id, or nil.
|
||||
|
||||
The simulate-backdoors bypass `engine.input` polling so test-libs can
|
||||
drive drag + resize without a running game loop.
|
||||
|
||||
Reference in New Issue
Block a user