diff --git a/README.md b/README.md index 9a73f2e..ea4bc12 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,33 @@ v0.1 ships a minimal recipe-list widget: - Tooltip / hover-detail - 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 For every frame, the widget rebuilds its row-list by walking diff --git a/init.lua b/init.lua index bd1fa29..76c973f 100644 --- a/init.lua +++ b/init.lua @@ -17,28 +17,40 @@ -- display.set_label_resolver(widget_def, fn) -- display.set_summary_resolver(widget_def, fn) -- --- widget_def conforms to the panel widget contract: --- widget_def.render(theme, x, y, w, h) --- widget_def.handle_input(input_state, theme, x, y, w, h) +-- widget_def conforms to the panel widget contract (see lib-core.panel +-- README §Widget-Lifecycle-Contract): +-- widget_def.render(ctx) +-- widget_def.handle_input(ctx, event) -- 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): -- - Row scrolling -- - Custom row layouts (icon column, summary column, etc.) -- - Tooltip / hover-detail -- - 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 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 --- resolver is per-module non-transitive). The lib does not call into them --- directly; crafting handles container access internally. -local _ = inventory -local _ = composition +-- inventory-list + composition are required for transitive completeness: +-- the engine's per-module resolver is non-transitive, so consumers of this +-- lib must satisfy crafting's indirect deps here even though this lib does +-- not call into them directly. +require("lib-core.inventory-list") +require("lib-core.composition") local M = {} @@ -131,11 +143,11 @@ function M.create(container_entity, opts) widget_id = opts.widget_id or "crafting", pause_on_open = opts.pause_on_open == true, } - function widget.render(theme, x, y, w, h) - M._render_widget(widget, theme, x, y, w, h) + function widget.render(ctx) + M._render_widget(widget, ctx) end - function widget.handle_input(input_state, theme, x, y, w, h) - return M._handle_input_widget(widget, input_state, theme, x, y, w, h) + function widget.handle_input(ctx, event) + return M._handle_input_widget(widget, ctx, event) end return widget end @@ -188,26 +200,38 @@ function M.set_summary_resolver(widget, fn) widget._summary_resolver = fn end -- Render -- --------------------------------------------------------------------- -function M._render_widget(widget, theme, x, y, w, h) - local rows = build_rows(widget) - local row_h = (theme and theme.row_height) or 28 - local pad = (theme and theme.padding) or 8 - local txt_col_full = (theme and theme.text_color) or {1.0, 1.0, 1.0, 1.0} - local txt_col_dim = (theme and theme.text_color_dim) or {0.6, 0.6, 0.6, 1.0} +-- M._render_widget(widget, ctx) +-- Called each render frame by panel via widget.render. Lays out one row +-- per known recipe, dim-colored when can_craft returns ok=false. +-- ctx = { bounds = {x,y,w,h}, theme = table, is_focused = bool } +-- theme keys consumed: row_height, padding, text_color, text_color_dim +-- (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 = {} for i, row in ipairs(rows) do local color = row.available and txt_col_full or txt_col_dim local label = widget._label_resolver(row.recipe) local summary = widget._summary_resolver(row.recipe) if engine and engine.render and engine.render.draw_text then - engine.render.draw_text(label, x + pad, cy, color) - engine.render.draw_text(summary, x + w - pad - 100, cy, color) + engine.render.draw_text(label, + 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 widget._render_rows[i] = { 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 end @@ -217,16 +241,21 @@ end -- 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 - local mx, my = input_state.mouse_x, input_state.mouse_y - if not (mx and my) then return false end + if event.kind ~= "click" or event.button ~= "right" then + return false + end + local mx, my = event.x, event.y 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 input_state.right_clicked then - M._invoke_context_menu(widget, r.recipe_id, mx, my) - return true - end + if mx >= r.x and mx <= r.x + r.w + and my >= r.y and my <= r.y + r.h then + M._invoke_context_menu(widget, r.recipe_id, mx, my) + return true end end return false @@ -249,7 +278,7 @@ function M._invoke_context_menu(widget, recipe_id, mx, my) end if #entries == 0 then return end 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