Files
sporel-lib-core.crafting-di…/README.md
Calic eb8f72bcbd feat: lib-core.crafting-display v0.1.0 — Recipe Panel-Widget
UI layer for recipe-based crafting. Reads recipes from
lib-core.crafting, renders the known-subset as a vertical row-list
inside a panel managed by lib-core.panel, and dispatches right-click
context-menu actions registered by the consuming module.

Surface:
  create(container_entity, opts) -> widget_def
  register_action(widget, label, callback)
  unregister_action(widget, label)
  set_icon_resolver(widget, fn)
  set_label_resolver(widget, fn)
  set_summary_resolver(widget, fn)

Row-build is rebuilt per render frame from crafting.list_recipes():
  - Visibility gate: recipe.is_known(ctx); errors hide row + emit [WARN]
  - Availability gate: crafting.can_craft(id, container, ctx).ok
    drives row colour (full vs dim text_color).

ctx comes from opts.ctx_factory() — default returns {}; override to
feed actor/skill/faction state into both predicates. label_resolver
defaults to recipe.name or recipe.id; summary_resolver joins input
template-ids with " + " and suffixes "xN" for count>1.

Right-click hit-tests row-rects captured during render, then opens
the menu via panel.show_context_menu(x, y, actions). Action
callbacks receive (recipe_id, {container, close_menu, refresh}).
Registration order is preserved (array-based, not pairs-based) so
context-menu rendering matches registration sequence.

Depends on lib-core.crafting 0.1.0, lib-core.panel 0.1.1,
lib-core.inventory-list 0.1.0, lib-core.composition 0.3.0. The last
two are not called directly but are required as direct deps so
modules consuming this widget satisfy crafting's transitive needs
(engine resolver is per-module non-transitive).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-14 12:34:32 +02:00

8.5 KiB
Raw Blame History

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.1.0 Lib-ID: lib-core.crafting-display Requires: lib-core.crafting v0.1.0, lib-core.panel v0.1.1, lib-core.inventory-list v0.1.0, lib-core.composition v0.3.0 Tags: crafting, ui, panel, widget, recipes

Topology

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.1.0)

v0.1 ships a minimal recipe-list widget:

  • create(container_entity, opts) — builds a panel-compatible widget_def
  • 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

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, container, ctx) ok=false → row visible but dim colour

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(container_entity, opts)

Syntax: display.create(container_entity: entity, opts: table|nil) -> widget_def

Example:

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)

Creates a widget_def bound to container_entity (used as the input source for can_craft availability checks). 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 container_entity is nil.


display.register_action(widget_def, label, callback)

Syntax: display.register_action(widget_def: table, label: string, callback: function) -> void

Example:

display.register_action(widget, "Craft", function(recipe_id, ctx)
    crafting.craft(recipe_id, ctx.container, {})
    ctx.close_menu()
end)

Registers a context-menu action shown on right-click of any row. callback receives (recipe_id, context) where context = { container, close_menu, refresh }.

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 — return value is opaque to v0.1 (default icon column not yet rendered; resolver is wired in for v0.2 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:

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:

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 (no icon in v0.1)
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_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 four are for test modules only and should not be called in production code.

Glue-Pattern

Minimal module setup:

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)
    crafting.craft(recipe_id, ctx.container, {})
    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