fix: align widget contract with lib-core.panel API
The widget contract was implemented as render(theme, x, y, w, h) and
handle_input(input_state, theme, x, y, w, h), but lib-core.panel
dispatches widgets as render(ctx) and handle_input(ctx, event) where
ctx = {bounds = {x,y,w,h}, theme, is_focused} and event carries
{kind, x, y, button} for clicks or {kind, dy} for wheel.
The mismatch would have surfaced as a crash on the first render frame
(theme.row_height read on a nil first arg) and as a permanently dead
right-click (no field matched input_state.right_clicked because the
real signature passes an event table). Both bugs were masked by the
existing tests, which exercise the public registration surface but
never drove render or handle_input headless.
Changes:
- widget.render and widget.handle_input now match panel's contract.
- _render_widget consumes ctx.bounds + ctx.theme; reads packed-RGBA
text colours directly instead of falling back to synthetic float
arrays (panel theme stores 0xRRGGBBAA integers).
- _handle_input_widget dispatches on event.kind == "click" and
event.button == "right", iterating _render_rows for hit-testing.
- draw_text now passes theme.font_size_body so the engine receives
the full (text, x, y, size, color) signature.
- Side-effect requires for lib-core.inventory-list and
lib-core.composition replace the unused-local sentinels, dropping
the underscore-shadowing.
- _invoke_context_menu trusts mx/my as preconditions and no longer
defends with `or 0` defaults — the entry-point guards nil.
README documents the widget contract explicitly and captures four
v0.2 hardening notes (is_known cache, WARN rate-limit, empty-action
diagnostic, defensive nil-guard) so the deferral is traceable.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
27
README.md
27
README.md
@@ -51,6 +51,33 @@ v0.1 ships a minimal recipe-list widget:
|
|||||||
- Tooltip / hover-detail
|
- Tooltip / hover-detail
|
||||||
- Stack-count display
|
- Stack-count display
|
||||||
|
|
||||||
|
**v0.2 hardening notes (deferred):**
|
||||||
|
- `is_known(ctx)` is called per recipe per frame; no per-frame memoization.
|
||||||
|
Large recipe-registries may want a cache layer.
|
||||||
|
- `is_known(ctx)` errors emit one `[WARN]` per failure per frame; no rate-
|
||||||
|
limit (spammy if a recipe is permanently broken).
|
||||||
|
- Right-click on a row with an empty action-set is silently dropped; a
|
||||||
|
diagnostic warning would help modders catch missing `register_action`
|
||||||
|
calls.
|
||||||
|
|
||||||
|
## Widget Contract
|
||||||
|
|
||||||
|
The widget conforms to `lib-core.panel`'s widget-lifecycle contract (see
|
||||||
|
`lib-core/panel/README.md` §Widget-Lifecycle-Contract):
|
||||||
|
|
||||||
|
```lua
|
||||||
|
widget.render(ctx)
|
||||||
|
widget.handle_input(ctx, event)
|
||||||
|
```
|
||||||
|
|
||||||
|
where `ctx = {bounds = {x,y,w,h}, theme = table, is_focused = bool}` and
|
||||||
|
the panel dispatches input events as `{kind = "click", x, y, button}` or
|
||||||
|
`{kind = "wheel", dy}`. Theme colours are packed-RGBA integers (e.g.
|
||||||
|
`0xE0E0E0FF`), matching the `panel.DEFAULT_THEME` schema.
|
||||||
|
|
||||||
|
Right-click on a row invokes `panel.show_context_menu(x, y, actions)`
|
||||||
|
with the registered actions; left-click and wheel are ignored in v0.1.
|
||||||
|
|
||||||
## Row Visibility + Availability rules
|
## Row Visibility + Availability rules
|
||||||
|
|
||||||
For every frame, the widget rebuilds its row-list by walking
|
For every frame, the widget rebuilds its row-list by walking
|
||||||
|
|||||||
95
init.lua
95
init.lua
@@ -17,28 +17,40 @@
|
|||||||
-- display.set_label_resolver(widget_def, fn)
|
-- display.set_label_resolver(widget_def, fn)
|
||||||
-- display.set_summary_resolver(widget_def, fn)
|
-- display.set_summary_resolver(widget_def, fn)
|
||||||
--
|
--
|
||||||
-- widget_def conforms to the panel widget contract:
|
-- widget_def conforms to the panel widget contract (see lib-core.panel
|
||||||
-- widget_def.render(theme, x, y, w, h)
|
-- README §Widget-Lifecycle-Contract):
|
||||||
-- widget_def.handle_input(input_state, theme, x, y, w, h)
|
-- widget_def.render(ctx)
|
||||||
|
-- widget_def.handle_input(ctx, event)
|
||||||
-- widget_def.title (string)
|
-- widget_def.title (string)
|
||||||
--
|
--
|
||||||
|
-- ctx = { bounds = {x,y,w,h}, theme = table, is_focused = bool }
|
||||||
|
-- event = { kind = "click", x, y, button = "left"|"right" }
|
||||||
|
-- | { kind = "wheel", dy = number }
|
||||||
|
--
|
||||||
-- DEFERRED (v0.1 non-goals):
|
-- DEFERRED (v0.1 non-goals):
|
||||||
-- - Row scrolling
|
-- - Row scrolling
|
||||||
-- - Custom row layouts (icon column, summary column, etc.)
|
-- - Custom row layouts (icon column, summary column, etc.)
|
||||||
-- - Tooltip / hover-detail
|
-- - Tooltip / hover-detail
|
||||||
-- - Stack-count display
|
-- - Stack-count display
|
||||||
|
--
|
||||||
|
-- v0.2 hardening notes (deferred):
|
||||||
|
-- - is_known(ctx) is called per recipe per frame; result is not cached.
|
||||||
|
-- Hot recipe-registries may want a per-frame memoization layer.
|
||||||
|
-- - is_known(ctx) errors emit one [WARN] per failure per frame; no rate-
|
||||||
|
-- limit (spammy on a permanently-broken recipe).
|
||||||
|
-- - Empty action-set + right-click is silently dropped; consider a
|
||||||
|
-- diagnostic warning to help modders detect missing register_action calls.
|
||||||
-- =====================================================================
|
-- =====================================================================
|
||||||
|
|
||||||
local crafting = require("lib-core.crafting")
|
local crafting = require("lib-core.crafting")
|
||||||
local panel = require("lib-core.panel")
|
local panel = require("lib-core.panel")
|
||||||
local inventory = require("lib-core.inventory-list")
|
|
||||||
local composition = require("lib-core.composition")
|
|
||||||
|
|
||||||
-- inventory + composition are required for transitive completeness (engine
|
-- inventory-list + composition are required for transitive completeness:
|
||||||
-- resolver is per-module non-transitive). The lib does not call into them
|
-- the engine's per-module resolver is non-transitive, so consumers of this
|
||||||
-- directly; crafting handles container access internally.
|
-- lib must satisfy crafting's indirect deps here even though this lib does
|
||||||
local _ = inventory
|
-- not call into them directly.
|
||||||
local _ = composition
|
require("lib-core.inventory-list")
|
||||||
|
require("lib-core.composition")
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
@@ -131,11 +143,11 @@ function M.create(container_entity, opts)
|
|||||||
widget_id = opts.widget_id or "crafting",
|
widget_id = opts.widget_id or "crafting",
|
||||||
pause_on_open = opts.pause_on_open == true,
|
pause_on_open = opts.pause_on_open == true,
|
||||||
}
|
}
|
||||||
function widget.render(theme, x, y, w, h)
|
function widget.render(ctx)
|
||||||
M._render_widget(widget, theme, x, y, w, h)
|
M._render_widget(widget, ctx)
|
||||||
end
|
end
|
||||||
function widget.handle_input(input_state, theme, x, y, w, h)
|
function widget.handle_input(ctx, event)
|
||||||
return M._handle_input_widget(widget, input_state, theme, x, y, w, h)
|
return M._handle_input_widget(widget, ctx, event)
|
||||||
end
|
end
|
||||||
return widget
|
return widget
|
||||||
end
|
end
|
||||||
@@ -188,26 +200,38 @@ function M.set_summary_resolver(widget, fn) widget._summary_resolver = fn end
|
|||||||
-- Render
|
-- Render
|
||||||
-- ---------------------------------------------------------------------
|
-- ---------------------------------------------------------------------
|
||||||
|
|
||||||
function M._render_widget(widget, theme, x, y, w, h)
|
-- M._render_widget(widget, ctx)
|
||||||
local rows = build_rows(widget)
|
-- Called each render frame by panel via widget.render. Lays out one row
|
||||||
local row_h = (theme and theme.row_height) or 28
|
-- per known recipe, dim-colored when can_craft returns ok=false.
|
||||||
local pad = (theme and theme.padding) or 8
|
-- ctx = { bounds = {x,y,w,h}, theme = table, is_focused = bool }
|
||||||
local txt_col_full = (theme and theme.text_color) or {1.0, 1.0, 1.0, 1.0}
|
-- theme keys consumed: row_height, padding, text_color, text_color_dim
|
||||||
local txt_col_dim = (theme and theme.text_color_dim) or {0.6, 0.6, 0.6, 1.0}
|
-- (all defined in lib-core.panel DEFAULT_THEME — see panel/README.md).
|
||||||
|
function M._render_widget(widget, ctx)
|
||||||
|
local rows = build_rows(widget)
|
||||||
|
local bounds = ctx.bounds
|
||||||
|
local theme = ctx.theme
|
||||||
|
local row_h = theme.row_height
|
||||||
|
local pad = theme.padding
|
||||||
|
local txt_col_full = theme.text_color
|
||||||
|
local txt_col_dim = theme.text_color_dim
|
||||||
|
|
||||||
local cy = y + pad
|
local cy = bounds.y + pad
|
||||||
widget._render_rows = {}
|
widget._render_rows = {}
|
||||||
for i, row in ipairs(rows) do
|
for i, row in ipairs(rows) do
|
||||||
local color = row.available and txt_col_full or txt_col_dim
|
local color = row.available and txt_col_full or txt_col_dim
|
||||||
local label = widget._label_resolver(row.recipe)
|
local label = widget._label_resolver(row.recipe)
|
||||||
local summary = widget._summary_resolver(row.recipe)
|
local summary = widget._summary_resolver(row.recipe)
|
||||||
if engine and engine.render and engine.render.draw_text then
|
if engine and engine.render and engine.render.draw_text then
|
||||||
engine.render.draw_text(label, x + pad, cy, color)
|
engine.render.draw_text(label,
|
||||||
engine.render.draw_text(summary, x + w - pad - 100, cy, color)
|
bounds.x + pad, cy,
|
||||||
|
theme.font_size_body, color)
|
||||||
|
engine.render.draw_text(summary,
|
||||||
|
bounds.x + bounds.w - pad - 100, cy,
|
||||||
|
theme.font_size_body, color)
|
||||||
end
|
end
|
||||||
widget._render_rows[i] = {
|
widget._render_rows[i] = {
|
||||||
recipe_id = row.recipe.id,
|
recipe_id = row.recipe.id,
|
||||||
x = x, y = cy, w = w, h = row_h,
|
x = bounds.x, y = cy, w = bounds.w, h = row_h,
|
||||||
}
|
}
|
||||||
cy = cy + row_h
|
cy = cy + row_h
|
||||||
end
|
end
|
||||||
@@ -217,16 +241,21 @@ end
|
|||||||
-- Input
|
-- Input
|
||||||
-- ---------------------------------------------------------------------
|
-- ---------------------------------------------------------------------
|
||||||
|
|
||||||
function M._handle_input_widget(widget, input_state, _theme, _x, _y, _w, _h)
|
-- M._handle_input_widget(widget, ctx, event)
|
||||||
|
-- Dispatched by panel for each input event while widget is active.
|
||||||
|
-- Right-click on a row opens the context menu.
|
||||||
|
-- Wheel events are silently ignored (scroll deferred to v0.2).
|
||||||
|
function M._handle_input_widget(widget, _ctx, event)
|
||||||
if not widget._render_rows then return false end
|
if not widget._render_rows then return false end
|
||||||
local mx, my = input_state.mouse_x, input_state.mouse_y
|
if event.kind ~= "click" or event.button ~= "right" then
|
||||||
if not (mx and my) then return false end
|
return false
|
||||||
|
end
|
||||||
|
local mx, my = event.x, event.y
|
||||||
for _, r in ipairs(widget._render_rows) do
|
for _, r in ipairs(widget._render_rows) do
|
||||||
if mx >= r.x and mx <= r.x + r.w and my >= r.y and my <= r.y + r.h then
|
if mx >= r.x and mx <= r.x + r.w
|
||||||
if input_state.right_clicked then
|
and my >= r.y and my <= r.y + r.h then
|
||||||
M._invoke_context_menu(widget, r.recipe_id, mx, my)
|
M._invoke_context_menu(widget, r.recipe_id, mx, my)
|
||||||
return true
|
return true
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
return false
|
return false
|
||||||
@@ -249,7 +278,7 @@ function M._invoke_context_menu(widget, recipe_id, mx, my)
|
|||||||
end
|
end
|
||||||
if #entries == 0 then return end
|
if #entries == 0 then return end
|
||||||
if panel.show_context_menu then
|
if panel.show_context_menu then
|
||||||
panel.show_context_menu(mx or 0, my or 0, entries)
|
panel.show_context_menu(mx, my, entries)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user