From 6e0f6539288948c4687981ffd5660f1969211f36 Mon Sep 17 00:00:00 2001 From: Signal Date: Wed, 28 Jan 2026 14:08:23 -0500 Subject: [PATCH] Allow closing a context from within its builder function. --- README.md | 8 ++++++-- init.lua | 31 +++++++++++++++++++++++++------ 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 98e940e..81c8874 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,8 @@ Builder functions are used by either `imfs.show` (to show an interface to a play You can pass a table as the last argument to an entry point like `imfs.show` to hold state external to the builder function; it will then be passed as an argument to the builder function during every rebuild. This way, you can declare the builder function itself in one place and use it in another without having to rely entirely on closure capture or global variables. +Builder functions may also take a second argument, which is a function that will close the current context. + Imfs does not strictly require using a builder function: if an interface is purely static and need never change, you can use a prebuilt element tree instead of a builder function, which avoids rebuilding the tree every time the interface is shown. ### Elements @@ -198,13 +200,15 @@ Ends the current window, and returns it. Note: The name ends with an underscore Show the interface defined by the provided builder function to the given player. The generated context will remain active until the player closes the formspec or leaves the game. -This returns a `Context` object, which is the the top-level dependent for all states used in the interface. Unlike in some other libraries, you don't really store state on the `Context` object (the builder function can't access it directly). Instead, you should store state as local variables in the function from which you call `show` (or somewhere else if they should be global) along with the builder function; that way, the builder function can capture the state specific to the action and the player that invoked the interface and only that. You can alternatively declare the builder function externally, then pass the new state objects in the `args` table, as it will be passed on to the builder function and will persist across rebuilds. This is arguably the better option since it avoids re-allocating the builder function every time it's first shown. +This returns a `Context` object, which is the the top-level dependent for all states used in the interface. Unlike in some other libraries, you don't really store state on the `Context` object (the builder function can't access it directly). Instead, you should store state as local variables in the function from which you call `show` (or somewhere else if they should be global) along with the builder function; that way, the builder function can capture the state specific to the action and the player that invoked the interface and only that. You can alternatively declare the builder function externally, then pass the new state objects in the `state` table, as it will be passed on to the builder function and will persist across rebuilds. This is arguably the better option since it avoids re-allocating the builder function every time it's first shown. + +If `builder` is a function, it will be called with two arguments: 1) the `state` table, and 2) a function that will manually close the context. (Externally, you can call `:close()` on the context to close it manually.) You can pass a static imfs tree instead of a builder if you so desire. ### `imfs.set_inventory(player, builder, state)` -Set `player`'s inventory to the interface defined by `builder`. This context will persist indefinitely until it is manually removed or the player leaves the game. Otherwise, this behaves in the same way as `imfs.show`. +Set `player`'s inventory to the interface defined by `builder`. This context will persist indefinitely until it is manually removed or the player leaves the game. Otherwise, this behaves in the same way as `imfs.show`, with the exception that if `builder` is a function, it will not receive a `close` function as an argument (since inventories cannot be closed, only replaced). ### `imfs.clear_inventory(player)` diff --git a/init.lua b/init.lua index 7a8fc44..80bfbbd 100644 --- a/init.lua +++ b/init.lua @@ -4,6 +4,7 @@ local hte = minetest.hypertext_escape local ctx +local player_contexts = {} local contexts = {} local inventories = {} @@ -1005,7 +1006,9 @@ local Context = { local tracker = e._linked_states table.insert(observers, tracker) - local fs = type(e.formspec) == "function" and e.formspec(e.args) or e.formspec + local fs = type(e.formspec) == "function" and e.formspec(e.state, function() e:close() end) or e.formspec + + e._window = fs table.remove(observers) @@ -1036,12 +1039,24 @@ local Context = { end end, deinit = function(e) + if contexts[e.id] then + contexts[e.id] = nil + end + if e._window._onclose then + e._window:_onclose() + end e:clear_state_bindings() + end, + close = function(e) + -- Inventories cannot be 'closed', only replaced. + if e._is_inventory then return end + minetest.close_formspec(e.target, e.id) + e:deinit() end } Context.__index = Context -local function fs_show(target, fs, args) +local function fs_show(target, fs, state) local id = "form"..new_id() local ctx = setmetatable({ formspec = fs, @@ -1049,17 +1064,22 @@ local function fs_show(target, fs, args) target = type(target) == "string" and target or target:get_player_name(), id = id, _linked_states = {}, - args = args or {} + state = state or {} }, Context) + if player_contexts[ctx.target] then + player_contexts[ctx.target]:deinit() + end + contexts[id] = ctx + player_contexts[ctx.target] = ctx ctx:rebuild() return ctx end -local function fs_set_inventory(p, fs, args) +local function fs_set_inventory(p, fs, state) local id = "form"..new_id() local name = type(p) == "string" and p or p:get_player_name() local ctx = setmetatable({ @@ -1070,7 +1090,7 @@ local function fs_set_inventory(p, fs, args) target = name, id = id, _linked_states = {}, - args = args or {} + state = state or {} }, Context) inventories[name] = ctx @@ -1125,7 +1145,6 @@ minetest.register_on_player_receive_fields(function(p, form, fields) if fields.quit then ctx:deinit() - contexts[form] = nil else if ctx._dirty then ctx:rebuild()