# 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 ```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 ``` ## 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: - 0–10% of TTL: fade in - 10–85% of TTL: fully opaque - 85–100% 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 } } ```