# lib-core.crafting-display Recipe panel-widget. Reads recipes via `lib-core.crafting` and renders the known-subset as a vertical row-list inside a panel managed by `lib-core.panel`. Each row shows label + summary; row colour reflects per-row availability (full-colour when craftable, dim when blocked). Right-clicking a row opens a context-menu populated from actions registered via `register_action`. **Version:** 0.2.0 **Lib-ID:** lib-core.crafting-display **Requires:** lib-core.crafting v0.5.0, lib-core.panel v0.4.0, lib-core.inventory-list v0.1.0, lib-core.composition v0.4.0 **Tags:** crafting, ui, panel, widget, recipes ## Topology ```mermaid graph LR this["lib-core.crafting-display"] crafting["lib-core.crafting"] panel["lib-core.panel"] inv["lib-core.inventory-list"] comp["lib-core.composition"] this --> crafting this --> panel this --> inv this --> comp ``` `inventory-list` and `composition` are declared as direct deps for the engine's per-module non-transitive resolver: this lib does not call them directly, but `crafting` does, and modules consuming this widget must satisfy them. ## Scope (v0.2.0) v0.2 ships a recipe-list widget: - `create(arg, opts)` — builds a panel-compatible widget_def; `arg` is a bare container-entity-handle (Form 1, bw-compat) or a `function() -> locale` factory (Form 2) - `register_action` / `unregister_action` — context-menu actions (right-click on row) - `set_icon_resolver` / `set_label_resolver` / `set_summary_resolver` — override default row content - Render: label (left-aligned) + summary (right-aligned) - Row colour: full when `can_craft.ok==true`, dim otherwise - Input: right-click row opens context-menu via `panel.show_context_menu` **Intentional non-goals (deferred):** - Row scrolling - Icon-column rendering (resolver returns nil by default) - Tooltip / hover-detail - Stack-count display **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. ## Row Visibility + Availability rules For every frame, the widget rebuilds its row-list by walking `crafting.list_recipes()` and applying two independent gates: | Gate | Source | Effect on row | |-----------------|------------------------------------------|----------------------------------------------| | **Visibility** | `recipe.is_known(ctx)` | `false` → row is **hidden** entirely | | **Availability**| `crafting.can_craft(id, locale, ctx)` | `ok=false` → row visible but **dim colour** | The `locale` here is whatever the widget's locale-factory returns for the current frame (v0.2: per-frame fresh; v0.1 bw-compat: constant single-container). `ctx` is built from `opts.ctx_factory()` (default `function() return {} end`). Override `ctx_factory` to wire in actor-state, skill-level, faction-membership, etc. — whatever the consuming module needs `is_known` and `can_craft` to see. If `recipe.is_known(ctx)` raises, the row is hidden and a `[WARN]` message is emitted via `engine.print` (defensive — visibility predicate must not crash UI). No equivalent guard around `can_craft`: errors there propagate (consistent with rest of the lib stack). ## API ### `display.create(arg, opts) -> widget_def` Creates a crafting widget. **`arg`** is one of: - **Form 1 (bw-compat from v0.1):** bare container-entity-handle. Wrapped internally as a constant locale_factory (`function() return arg end`). - **Form 2 (NEW in v0.2):** `function() -> locale` — invoked per frame. Locale is either an entity-handle (Form-1 sub-semantics) or a table `{sources={...}, sink=...}`. Forward-compatible with dynamic topologies (player moves → adjacent-container set changes, no widget recreate required). **Syntax:** `display.create(arg: entity | function() -> locale, opts: table|nil) -> widget_def` **Example (Form 1, bw-compat):** ```lua local display = require("lib-core.crafting-display") local panel = require("lib-core.panel") local widget = display.create(workbench_entity, { title = "Workbench", widget_id = "crafting", pause_on_open = true, ctx_factory = function() return { actor = current_actor() } end, }) panel.register(widget.widget_id, widget) panel.bind_default_trigger("c", widget.widget_id) ``` **Example (Form 2, locale-factory):** ```lua local widget = display.create(function() return { sources = adjacent_containers(player), sink = player_backpack, } end, { title = "Workbench" }) ``` `opts` keys: - `title` (string, default `"Crafting"`) — panel title bar text - `widget_id` (string, default `"crafting"`) — key for `panel.register` - `pause_on_open` (bool, default `false`) — passed to panel for `is_pausing()` - `ctx_factory` (function, default `function() return {} end`) — per-frame ctx builder - `icon_resolver` (function) — overrides default icon resolver at creation time - `label_resolver` (function) — overrides default label resolver at creation time - `summary_resolver` (function) — overrides default summary resolver at creation time Loud-error if `arg` is `nil`. --- ### Action-Callback Signature (v0.2.0 Update) Callbacks receive `(recipe_id, ctx_inner)` where `ctx_inner` is: ```lua { locale = , -- NEW in v0.2 container = , -- bw-compat alias close_menu = function() ... end, refresh = function() ... end, } ``` Module action typically calls `crafting.craft(recipe_id, ctx_inner.locale, ctx)`. Existing v0.1 callers that use `ctx_inner.container` continue to work via the Form-1 bw-compat shim (sink-handle is treated internally as a single-container). --- ### `display.register_action(widget_def, label, callback)` **Syntax:** `display.register_action(widget_def: table, label: string, callback: function) -> void` **Example:** ```lua display.register_action(widget, "Craft", function(recipe_id, ctx) -- v0.2: prefer ctx.locale; ctx.container is a bw-compat alias. crafting.craft(recipe_id, ctx.locale, {}) ctx.close_menu() end) ``` Registers a context-menu action shown on right-click of any row. `callback` receives `(recipe_id, ctx_inner)` where `ctx_inner = { locale, container, close_menu, refresh }`. `locale` carries the currently-resolved locale (table for Form-2, bare handle for Form-1). `container` is a bw-compat alias pointing at `locale.sink` (Form-2) or the handle itself (Form-1). Loud-error on duplicate `label` or if `callback` is not a function. The default action-set is empty — all actions must be registered explicitly. Registration order is preserved (entries are kept in an array). Identical order is reflected in the context-menu rendering. --- ### `display.unregister_action(widget_def, label)` **Syntax:** `display.unregister_action(widget_def: table, label: string) -> void` Removes a context-menu action. Idempotent: no error if `label` was never registered. --- ### `display.set_icon_resolver(widget_def, fn)` **Syntax:** `display.set_icon_resolver(widget_def: table, fn: function) -> void` Replaces the icon resolver. `fn(recipe) -> any|nil` — the return value is opaque to the widget (the icon column is not rendered yet; resolver is wired in for atlas-icon support and for tests). --- ### `display.set_label_resolver(widget_def, fn)` **Syntax:** `display.set_label_resolver(widget_def: table, fn: function) -> void` **Example:** ```lua display.set_label_resolver(widget, function(recipe) return localize("recipe." .. recipe.id) or recipe.name or recipe.id end) ``` Replaces the label resolver. `fn(recipe) -> string`. Called each render frame per row. Default returns `recipe.name or recipe.id`. --- ### `display.set_summary_resolver(widget_def, fn)` **Syntax:** `display.set_summary_resolver(widget_def: table, fn: function) -> void` **Example:** ```lua display.set_summary_resolver(widget, function(recipe) return string.format("%d ingredients", #recipe.inputs) end) ``` Replaces the summary resolver. `fn(recipe) -> string`. Called each render frame per row. Default joins `recipe.inputs` template-ids with `" + "`, suffixing `"×N"` when `count>1`. --- ## Default Resolvers | Resolver | Default behavior | |--------------------|-------------------------------------------------------------| | `label_resolver` | `recipe.name or recipe.id` | | `summary_resolver` | `inputs[*].template` joined with `" + "`, `"×N"` when `count>1` | | `icon_resolver` | returns `nil` (icon column not rendered yet) | | `ctx_factory` | returns `{}` (empty table; replace to feed actor/skill ctx) | ## Test backdoors - `display._test_get_rows(widget_def)` — returns the per-frame row list (`{recipe, available}` pairs after is_known filtering) - `display._test_get_locale(widget_def)` — invokes the configured locale_factory and returns whatever it produces (v0.2) - `display._test_resolve_icon(widget_def, recipe)` — invokes current icon resolver - `display._test_resolve_label(widget_def, recipe)` — invokes current label resolver - `display._test_resolve_summary(widget_def, recipe)` — invokes current summary resolver All five are for test modules only and should not be called in production code. ## Glue-Pattern Minimal module setup: ```lua local display = require("lib-core.crafting-display") local crafting = require("lib-core.crafting") local panel = require("lib-core.panel") -- 1. Define recipes crafting.define_recipe{ id = "rock_pick", inputs = { { template = "items.rock", count = 1 }, { template = "items.stick", count = 1 }, }, output = { template = "items.rock_pick", count = 1 }, name = "Rock Pick", } -- 2. Create widget (workbench_entity is a list-container) local widget = display.create(workbench_entity, { title = "Workbench", widget_id = "crafting", pause_on_open = true, }) -- 3. Register actions display.register_action(widget, "Craft", function(recipe_id, ctx) -- v0.2: prefer ctx.locale (ctx.container is a bw-compat alias). crafting.craft(recipe_id, ctx.locale, {}) ctx.close_menu() end) -- 4. Register with panel + bind toggle key panel.register(widget.widget_id, widget) panel.bind_default_trigger("c", widget.widget_id) -- 5. Wire into update + render function M.update(dt) panel.update(dt) end function M.render() panel.render() end ```