feat: v0.2.0 locale_factory replaces single container

M.create now accepts either a function (locale_factory, called per
frame to resolve the current crafting locale) or a bare entity-handle
(wrapped internally as a constant factory for v0.1 bw-compat). The
factory's return value is forwarded to crafting.can_craft as the new
multi-source / single-sink locale, enabling dynamic topologies
(adjacent-container sets that change as the actor moves) without
recreating the widget.

Action callbacks now receive ctx_inner with both `locale` (the
currently-resolved locale, either the Form-2 table or the Form-1 bare
handle) and `container` as a bw-compat alias pointing at locale.sink
(Form-2) or the handle itself (Form-1). Existing v0.1 callers using
ctx.container keep working unchanged.

New backdoor _test_get_locale exposes the locale_factory's current
return value. README documents both Form-1 and Form-2 with examples
plus the updated callback contract.

Depends on lib-core.crafting v0.2.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Calic
2026-06-14 19:43:49 +02:00
parent b42b707a8e
commit 31e51ee8b7
3 changed files with 128 additions and 30 deletions

View File

@@ -1,5 +1,5 @@
-- =====================================================================
-- lib-core.crafting-display v0.1.0 — Recipe Panel-Widget
-- lib-core.crafting-display v0.2.0 — Recipe Panel-Widget
--
-- Sits on top of lib-core.panel and reads lib-core.crafting +
-- lib-core.inventory-list + lib-core.composition. Provides a ready-to-
@@ -10,7 +10,14 @@
-- registered via M.register_action.
--
-- Public API:
-- display.create(container_entity, opts) -> widget_def
-- display.create(arg, opts) -> widget_def
-- `arg` is either:
-- - a bare entity-handle (Form-1, v0.1 bw-compat) — wrapped as
-- a constant locale_factory internally; or
-- - a `function() -> locale` (Form-2, v0.2) — invoked per frame
-- to obtain the currently-resolved locale, which itself may be
-- a bare entity-handle or a `{sources={...}, sink=...}` table
-- as understood by `lib-core.crafting`.
-- display.register_action(widget_def, label, callback)
-- display.unregister_action(widget_def, label)
-- display.set_icon_resolver(widget_def, fn)
@@ -87,6 +94,10 @@ end
-- ---------------------------------------------------------------------
local function build_rows(widget)
local locale = widget._locale_factory()
if locale == nil then
return {}
end
local ctx = widget._ctx_factory()
if type(ctx) ~= "table" then ctx = {} end
local out = {}
@@ -99,7 +110,7 @@ local function build_rows(widget)
recipe.id, tostring(known)))
end
elseif known == true then
local match = crafting.can_craft(recipe.id, widget._container, ctx)
local match = crafting.can_craft(recipe.id, locale, ctx)
out[#out + 1] = {
recipe = recipe,
available = match.ok == true,
@@ -113,9 +124,15 @@ end
-- Public API: create
-- ---------------------------------------------------------------------
--- M.create(container_entity, opts) -> widget_def
--- Creates a crafting-recipe widget bound to `container_entity` (used as
--- the input source for can_craft availability checks).
--- M.create(arg, opts) -> widget_def
--- Creates a crafting-recipe widget. `arg` is either:
--- - a bare entity-handle (Form-1, bw-compat) used as the input source
--- for can_craft availability; internally wrapped as a constant
--- locale_factory `function() return arg end`; or
--- - a `function() -> locale` (Form-2) — called per frame to obtain
--- the current locale. The returned locale may itself be a bare
--- handle (Form-1 sub-semantics) or a `{sources={...}, sink=...}`
--- table as accepted by `lib-core.crafting`.
--- opts = {
--- title = string, default "Crafting"
--- widget_id = string, default "crafting"
@@ -125,14 +142,23 @@ end
--- label_resolver = function(recipe) -> string
--- summary_resolver = function(recipe) -> string
--- }
--- Loud-error if container_entity is nil.
function M.create(container_entity, opts)
if container_entity == nil then
error("crafting-display.create: container must not be nil", 2)
end
--- Loud-error if `arg` is nil.
function M.create(arg, opts)
opts = opts or {}
if arg == nil then
error("crafting-display.create: first arg (locale or "
.. "container) must not be nil", 2)
end
local locale_factory
if type(arg) == "function" then
locale_factory = arg
else
-- Bw-compat: bare entity-handle (userdata in Sporel) wrapped
-- as constant factory.
locale_factory = function() return arg end
end
local widget = {
_container = container_entity,
_locale_factory = locale_factory,
_opts = opts,
_actions = {},
_ctx_factory = opts.ctx_factory or default_ctx_factory,
@@ -143,6 +169,8 @@ function M.create(container_entity, opts)
widget_id = opts.widget_id or "crafting",
pause_on_open = opts.pause_on_open == true,
}
-- widget.render(ctx) + widget.handle_input(ctx, event) — panel
-- contract per ADR-0049 / panel/README.md
function widget.render(ctx)
M._render_widget(widget, ctx)
end
@@ -158,7 +186,12 @@ end
--- M.register_action(widget, label, callback)
--- Registers a context-menu action shown on right-click of any row.
--- callback(recipe_id, context) where context = { container, close_menu, refresh }.
--- callback(recipe_id, ctx_inner) where
--- ctx_inner = { locale, container, close_menu, refresh }
--- `locale` is the currently-resolved locale (Form-2 table or Form-1
--- bare handle, depending on what the locale_factory returns).
--- `container` is a bw-compat alias pointing at `locale.sink` (Form-2)
--- or the handle itself (Form-1).
--- Loud-error on duplicate label or non-function callback.
function M.register_action(widget, label, callback)
if type(label) ~= "string" or label == "" then
@@ -262,6 +295,14 @@ function M._handle_input_widget(widget, _ctx, event)
end
function M._invoke_context_menu(widget, recipe_id, mx, my)
local locale = widget._locale_factory()
local sink_alias
if type(locale) == "table" then
sink_alias = locale.sink
else
-- Form-1: bare handle is both source + sink.
sink_alias = locale
end
local entries = {}
for _, a in ipairs(widget._actions) do
local cb = a.callback -- capture for closure
@@ -269,8 +310,10 @@ function M._invoke_context_menu(widget, recipe_id, mx, my)
label = a.label,
callback = function(menu_ctx)
cb(recipe_id, {
container = widget._container,
close_menu = menu_ctx.close_menu,
locale = locale,
container = sink_alias, -- bw-compat alias
close_menu = (menu_ctx and menu_ctx.close_menu)
or function() end,
refresh = function() end, -- v0.1: free (next frame re-reads)
})
end,
@@ -290,6 +333,10 @@ function M._test_get_rows(widget)
return build_rows(widget)
end
function M._test_get_locale(widget)
return widget._locale_factory()
end
function M._test_resolve_icon(widget, recipe)
return widget._icon_resolver(recipe)
end