Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.4.0 Tags: notify, display, ui, toast, log, detail, hud, overlay
Topology
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).
create_log_widgetreturns a widget-def for the caller to register withlib-core.panelviapanel.register(the lib itself never callspanel.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 setscurrent_detailand 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:
{
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.
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.
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
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 } }