initial: notify-display v0.1.0 — screen-space displays on lib-core.panel

This commit is contained in:
Calic
2026-06-14 01:36:20 +02:00
committed by Axel Meyer
commit 94d2a54a1c
4 changed files with 592 additions and 0 deletions

201
README.md Normal file
View File

@@ -0,0 +1,201 @@
# lib-core.notify-display
Screen-space display layer for `lib-core.notify`. Routes channel messages into three
UI modes: log (panel-widget list), toast (always-on HUD overlay), detail (modal
panel that auto-opens on new message).
**Version:** 0.1.0
**Lib-ID:** lib-core.notify-display
**Requires:** lib-core.notify v0.1.0, lib-core.panel v0.1.0
**Tags:** notify, display, ui, toast, log, detail, hud, overlay
## Topology
<!-- topology:start (auto-generated; do not edit) -->
```mermaid
graph LR
this["lib-core.notify-display"]
lib_notify["lib-core.notify"]
lib_panel["lib-core.panel"]
engine_render["engine.render.*"]
this --> lib_notify
this --> lib_panel
this --> engine_render
```
<!-- topology:end -->
## Scope (v0.1.0)
v0.1 ships three display modes driven by `notify` channel `display_mode` declarations:
- **log** — panel widget that renders channel history as a sorted list (newest first).
Plugs into `lib-core.panel` via `create_log_widget` + `panel.register`.
- **toast** — always-on HUD overlay with fade-in/hold/fade-out animation and
configurable position, max-visible cap, and per-message TTL.
- **detail** — subscribes to `display_mode="detail"` channels; on each new message
sets `current_detail` and auto-opens a registered panel widget.
**Intentional non-goals (deferred to v0.2+):**
- Scroll/pagination in log widget
- Multiple simultaneous toast columns
- Toast queue persistence (survives mode detach/re-attach)
- Per-severity toast theming overrides
- Keyboard dismiss for detail panel
## API
### `display.attach_mode(mode, opts)`
Subscribes to existing `notify` channels that match `mode`. Must be called
after `notify.create_channel` for those channels (channels created later are
not automatically picked up in v0.1).
| mode | required opts | optional opts |
|------|---------------|---------------|
| `"log"` | `panel_widget_id: string` | — |
| `"toast"` | — | `position`, `max_visible`, `default_ttl` |
| `"detail"` | `panel_widget_id: string` | — |
**Toast opts schema:**
```lua
{
position = "top-right", -- default; also: top-center, top-left,
-- bottom-right, bottom-center, bottom-left
max_visible = 5, -- oldest evicted when limit exceeded
default_ttl = 3.0, -- seconds; overridden per-msg by msg.ttl
}
```
Loud-error on unknown `mode` or invalid `position`. Missing `panel_widget_id` for
log/detail is a loud-error. Nil/missing `position` in toast silently defaults to
`"top-right"`.
---
### `display.detach_mode(mode)`
Unsubscribes all subscriptions held for channels whose `display_mode == mode`,
sets `mode_opts[mode] = nil`, and clears mode-specific state:
- toast → empties `active_toasts`
- detail → clears `current_detail`
- log → clears `widget_channels`
---
### `display.create_log_widget(opts) → widget_def`
Returns a widget definition table compatible with `panel.register`.
```lua
opts = {
widget_id = "game-log", -- default
title = "Game Log", -- default
pause_on_open = false, -- default
}
```
The returned `widget_def.render(ctx)` reads from `notify.get_history` for all
channels mapped to this `widget_id` (via `attach_mode("log", {panel_widget_id=...})`),
sorts by timestamp descending, and renders up to `floor(h / row_height)` rows.
Severity tinting: warn = `0xFFD000FF`, error = `0xFF4040FF`.
---
### `display.create_detail_widget(opts) → widget_def`
Returns a widget definition table compatible with `panel.register`.
```lua
opts = {
widget_id = "detail", -- default
title = "Detail", -- default
pause_on_open = false, -- default
}
```
Renders `current_detail` (the last message posted to any `display_mode="detail"`
channel). Shows `msg.text` as title, `msg.data.description` as body, remaining
`msg.data.*` keys as key-value rows. No-op if no detail message has arrived yet.
**Note:** Register the widget with `panel.register` **before** calling
`attach_mode("detail", ...)` so that the auto-open `panel.open` call succeeds
on the first message. If registered after attach, the first message silently
fails to open the panel (pcall-guarded), but `current_detail` is still set.
---
### `display.update(dt)`
Must be called each game-update frame. Removes expired toasts from `active_toasts`
(compares `engine.time.now()` against `spawn_time + ttl`). No-op if toast mode
is not attached.
---
### `display.render()`
Must be called each render frame (typically after `panel.render()`). Draws the
toast HUD overlay. No-op if toast mode is not attached or no toasts are active.
Toast geometry: 280 × 32 px, 8 px padding from screen edge. Positions stack in
the growth direction of the chosen anchor (top-* grows down, bottom-* grows up).
Alpha animation per toast:
- 010% of TTL: fade in
- 1085% of TTL: fully opaque
- 85100% of TTL: fade out
Screen dimensions: 1280 × 720 fallback (engine.render does not expose
`get_screen_size` to Lua in v0.1 — same limitation as lib-core.panel).
---
## Theme keys consumed
`notify-display` renders toasts using hardcoded colors (no own theme keys in v0.1).
Log and detail widgets render via `ctx.theme` supplied by `lib-core.panel`, consuming
the standard panel theme keys: `text_color`, `text_color_dim`, `font_size_title`,
`font_size_body`, `padding`, `row_height`.
---
## Glue pattern example
```lua
local notify = require("lib-core.notify")
local panel = require("lib-core.panel")
local display = require("lib-core.notify-display")
-- 1. Declare channels
notify.create_channel{ id = "game-events", filter = {"event"}, display_mode = "log" }
notify.create_channel{ id = "alerts", filter = {"alert"}, display_mode = "toast" }
notify.create_channel{ id = "item-detail", filter = {"item"}, display_mode = "detail" }
-- 2. Create and register widgets
local log_def = display.create_log_widget { widget_id = "game-log" }
local detail_def = display.create_detail_widget{ widget_id = "item-detail" }
panel.register("game-log", log_def)
panel.register("item-detail", detail_def)
-- 3. Attach modes (after register so detail auto-open works immediately)
display.attach_mode("log", { panel_widget_id = "game-log" })
display.attach_mode("toast", { position = "bottom-right", default_ttl = 4.0 })
display.attach_mode("detail", { panel_widget_id = "item-detail" })
-- 4. Per-frame calls
function game_update(dt)
panel.update(dt)
display.update(dt)
end
function game_render()
panel.render()
display.render() -- toast overlay on top
end
-- 5. Posting messages
notify.post{ tags = {"event"}, text = "Player entered the forest" }
notify.post{ tags = {"alert"}, text = "Low health!", ttl = 2.0, severity = "warn" }
notify.post{ tags = {"item"}, text = "Iron Sword",
data = { description = "A sturdy blade.", damage = 12 } }
```