Compare commits
2 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| be73993547 | |||
| 6e0f653928 |
2 changed files with 340 additions and 103 deletions
27
README.md
27
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
|
||||
|
|
@ -93,6 +95,12 @@ If you want to use the state API for a system of your own, remember that:
|
|||
* When set, a state notifies each entry in `_getters` by calling that object's `:update()` method.
|
||||
* To collect state dependencies from a given function, 1) create a tracking table and `table.insert(imfs.state.observers, tracker)`, 2) call the function, and 3) `table.remove(imfs.state.observers)`. The tracking table will then be filled with all states that were accessed between the `table.insert` and `table.remove`.
|
||||
|
||||
### IDs
|
||||
|
||||
One of the biggest problems with the immediate-mode approach is that it is difficult to determine when a particular element has changed, since a given build run is not guaranteed to match the output of the previous run in any way, and if element IDs were unique, it would be nigh-impossible to determine what an element's previous value was without shifting to a more retained-mode approach. To work around this, the internal IDs of elements are assigned somewhat deterministically based on the order of their creation. Thus, if a given builder always produces the same sequence of element creations in the same order, each element will always be assigned the same ID on every run. This heuristic, of course, breaks down when a builder function does a lot of conditional or iterative rendering, because the size and ordering of the element tree can change significantly across rebuilds. To solve this, imfs provides `imfs.scope()`. This will create a named sub-scope, causing elements created within it to be identified relative to the beginning of the sub-scope, rather than the global scope. Thus, if an element tree within a scope doesn't change layout, the elements will have stable IDs that allow `onchange` and the like to work properly.
|
||||
|
||||
With that said, it is generally not important for you to use scopes in your UI, because most elements don't depend on knowledge of their previous state. The exceptions include fields and scroll containers. Fields need to know their old value in order to determine whether to fire an `onchange` event, otherwise they would be compelled to call `onchange` every time some element triggers an action (even if that action did not affect state). Unmanaged scroll containers need to know their previous state so that they can preserve their scroll position across rebuilds despite having no external state object with which to track it. (Note also that these elements still need not be enclosed in a scope if the element tree preceding them is constant.)
|
||||
|
||||
# Example code
|
||||
|
||||
Create a mod with this code and run `/demo` to see the example in action.
|
||||
|
|
@ -178,7 +186,7 @@ minetest.register_chatcommand("demo", {
|
|||
|
||||
### `imfs.export()`
|
||||
|
||||
Export the imfs API to \_G, with "fs_" prefixes. This is mainly intended for custom, from-scratch games to make life a bit easier.
|
||||
Export the imfs API to `_G`, with "fs_" prefixes. This is mainly intended for custom, from-scratch games to make life a bit easier.
|
||||
|
||||
### `imfs.begin(window_width, window_height)`
|
||||
|
||||
|
|
@ -189,22 +197,33 @@ Begin a root imfs container. This returns a `Window` object with the following m
|
|||
* `Window:padding(x, y)`: Set the padding of the formspec window. See `padding[]` in the [Minetest API documentation](https://api.luanti.org/formspec/).
|
||||
* `Window:modal([modal])`: Make this window modal (meaning that it cannot be directly closed by the user with Esc). If `modal` is false, there will be no effect (this can be useful for making modality state-dependent).
|
||||
* `Window:onclose(fn)`: Registers `fn` to be called when the window closes. (This may be triggered when the target player leaves the game.)
|
||||
* `Window:bgcolor(foreground, fullscreen)`: Set the window's background color. If `fullscreen` is absent or false, `foreground` will be applied as the window's foreground color. If `fullscreen` is `true`, `foreground` will be applied as the window's background color. If `fullscreen` is a string, `foreground` will be the window's foreground color and `fullscreen` will be the window's background color.
|
||||
|
||||
### `imfs.end_()`
|
||||
|
||||
Ends the current window, and returns it. Note: The name ends with an underscore so as not to conflict with the Lua keyword (I couldn't think of a better word for ending the window than 'end').
|
||||
|
||||
### `imfs.scope(name)`
|
||||
|
||||
Begins a scope with the given name. All subsequent elements will be identified with indices relative to the beginning of the scope.
|
||||
|
||||
### `imfs.scope_end()`
|
||||
|
||||
Exits the last entered scope. Subsequent elements will resume identification relative to the parent scope.
|
||||
|
||||
### `imfs.show(player, builder, state)`
|
||||
|
||||
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)`
|
||||
|
||||
|
|
@ -227,7 +246,7 @@ End the current container and revert to its parent. This can be used to create c
|
|||
|
||||
Note that all elements have a `:render()` method that outputs their formspec representation, and accepts overrides for x, y, width, and height as extra arguments. This is used for creating custom layouting containers which interpret position and size in a non-absolute way.
|
||||
|
||||
Many sized elements have a `:tooltip(text[, background_color[, text_color]])` method that creates a tooltip for the element. Note that for elements without a `name` attribute in the formspec format, this will be an area tooltip and will not be occluded by any overlaying elements. Since imfs doesn't track which elements occlude which, it is up to you to not add area tooltips to occluded elements.
|
||||
Many sized elements have a `:tooltip(text, enabled[, background_color[, text_color]])` method that creates a tooltip for the element. Note that for elements without a `name` attribute in the formspec format, this will be an area tooltip and will not be occluded by any overlaying elements. Since imfs doesn't track which elements occlude which, it is up to you to not add area tooltips to occluded elements. An easy way to do this is to pass a boolean as the second argument: if `false`, the tooltip will not be added. If the second argument is not a boolean, it will be interpreted as `background_color` (and `background_color` as `text_color`).
|
||||
|
||||
### `imfs.label(x, y, text)`
|
||||
|
||||
|
|
|
|||
416
init.lua
416
init.lua
|
|
@ -4,6 +4,7 @@ local hte = minetest.hypertext_escape
|
|||
|
||||
local ctx
|
||||
|
||||
local player_contexts = {}
|
||||
local contexts = {}
|
||||
local inventories = {}
|
||||
|
||||
|
|
@ -17,11 +18,14 @@ local function resolve_layout_units(value, ref)
|
|||
local num = tonumber(value)
|
||||
if num then return num end
|
||||
|
||||
local percent = value:match "(%d+)%%"
|
||||
local percent = value:match "(%d*.?%d+)%%"
|
||||
if ref and percent then
|
||||
return tonumber(percent) /100 *ref
|
||||
local sign, offset = value:match "%%%s*([+-])%s*(%d*.?%d+)"
|
||||
return tonumber(percent) /100 *ref +(tonumber(offset or 0) *(sign == "-" and -1 or 1))
|
||||
end
|
||||
|
||||
print(string.match("100%", "(%d*.?%d+)%%"), percent, ref)
|
||||
|
||||
error("Malformed layout units: "..value)
|
||||
else
|
||||
return value
|
||||
|
|
@ -34,12 +38,13 @@ local function resolve_flex_layout_units(value, ref)
|
|||
local num = tonumber(value)
|
||||
if num then return num end
|
||||
|
||||
local percent = value:match "(%d+)%%"
|
||||
local percent = value:match "(%d*.?%d+)%%"
|
||||
if ref and percent then
|
||||
return tonumber(percent) /100 *ref
|
||||
local sign, offset = value:match "%%%s*([+-])%s*(%d*.?%d+)"
|
||||
return tonumber(percent) /100 *ref +(tonumber(offset or 0) *(sign == "-" and -1 or 1))
|
||||
end
|
||||
|
||||
local flex = value:match "(%d+)x"
|
||||
local flex = value:match "(%d*.?%d+)x"
|
||||
if flex then
|
||||
return tonumber(flex), "flex"
|
||||
end
|
||||
|
|
@ -62,18 +67,49 @@ local function add_elem_style(e, state, props)
|
|||
end
|
||||
|
||||
-- This should be a method on any element classes that support tooltips.
|
||||
local function add_elem_tooltip(e, text, bgcolor, txtcolor)
|
||||
local function add_elem_tooltip(e, text, enabled, bgcolor, txtcolor)
|
||||
-- Permit the user to pass a boolean to disable the tooltip (useful for modal dialogs that should occlude tooltips without more bothersome composition boilerplate than necessary).
|
||||
if enabled == false then
|
||||
return
|
||||
elseif enabled ~= true then
|
||||
txtcolor = bgcolor
|
||||
bgcolor = enabled
|
||||
end
|
||||
-- For named elements, prefer name-associated tooltips.
|
||||
if e.__id then
|
||||
imfs._named_tooltip(e.__id, text, bgcolor, txtcolor)
|
||||
-- For other elements, if they have a size, use an area tooltip instead.
|
||||
elseif e.w then
|
||||
imfs.tooltip(e.x, e.y, e.w, e.h, text, bgcolor, txtcolor)
|
||||
-- We must overload `render()` in order to ensure the tooltip's position matches the parent's exactly in all cases.
|
||||
local render = e.render
|
||||
e.render = function(e, x, y, w, h)
|
||||
local out = render(e, x, y, w, h)
|
||||
return table.concat{out, imfs.tooltip.init(x or e.x, y or e.y, w or e.w, h or e.h, text, bgcolor, txtcolor, true):render()}
|
||||
end
|
||||
end
|
||||
return e
|
||||
end
|
||||
|
||||
local name_scope = {"_"}
|
||||
local nth_id = {0}
|
||||
|
||||
local function scope(name)
|
||||
table.insert(name_scope, name)
|
||||
table.insert(nth_id, 0)
|
||||
end
|
||||
|
||||
local function scope_end()
|
||||
table.remove(name_scope)
|
||||
table.remove(nth_id)
|
||||
end
|
||||
|
||||
local function new_id()
|
||||
local depth = #nth_id
|
||||
nth_id[depth] = nth_id[depth] +1
|
||||
return table.concat(name_scope, ".").."_"..nth_id[depth]
|
||||
end
|
||||
|
||||
local function unique_id()
|
||||
return "_"..minetest.get_us_time().."_"..math.random(1, 100000)
|
||||
end
|
||||
|
||||
|
|
@ -89,7 +125,7 @@ local state = {
|
|||
get = function(e)
|
||||
local observer = observers[#observers]
|
||||
if observer then
|
||||
observer[e] = true -- Add this base state as a dep (using table for set-like uniqueness)
|
||||
observer[e] = true
|
||||
end
|
||||
return e._val
|
||||
end,
|
||||
|
|
@ -206,6 +242,12 @@ setmetatable(fs_style, {
|
|||
})
|
||||
|
||||
local fs_tooltip = {
|
||||
_no_layout = true,
|
||||
init = function(x, y, w, h, text, bgcolor, txtcolor)
|
||||
local e = {x = x, y = y, w = w, h = h, text = text, bgcolor = bgcolor or imfs.theme.tooltip_bg or "#444", txtcolor = txtcolor or imfs.theme.tooltip_text or "#aaa"}
|
||||
setmetatable(e, imfs.tooltip)
|
||||
return e
|
||||
end,
|
||||
render = function(e, x, y, w, h)
|
||||
if e._name then
|
||||
return string.format("tooltip[%s;%s;%s;%s]", e._name, get(e, "text"), get(e, "bgcolor"), get(e, "txtcolor"))
|
||||
|
|
@ -284,7 +326,7 @@ setmetatable(fs_arealabel, {
|
|||
|
||||
local fs_hypertext = {
|
||||
render = function(e, x, y, w, h)
|
||||
return string.format("hypertext[%f,%f;%f,%f;%s]", x or get(e, "x"), y or get(e, "y"), w or get(e, "w"), h or get(e, "h"), hte(get(e, "txt")))
|
||||
return string.format("hypertext[%f,%f;%f,%f;%s;%s]", x or get(e, "x"), y or get(e, "y"), w or get(e, "w"), h or get(e, "h"), e.__id, get(e, "txt"))
|
||||
end,
|
||||
onaction = function(e, fn)
|
||||
ctx._events.on_click[e.__id] = fn
|
||||
|
|
@ -334,7 +376,7 @@ local fs_image = {
|
|||
get(e, "middle")
|
||||
)
|
||||
else
|
||||
return string.format("image[%f,%f;%f,%f;%s;%s]", get(e, "x"), get(e, "y"), get(e, "w"), get(e, "h"), get(e, "texture"), get(e, "middle"))
|
||||
return string.format("image[%f,%f;%f,%f;%s;%s]", x or get(e, "x"), y or get(e, "y"), w or get(e, "w"), h or get(e, "h"), get(e, "texture"), get(e, "middle"))
|
||||
end
|
||||
end,
|
||||
animated = function(e, frames, duration, start)
|
||||
|
|
@ -359,7 +401,7 @@ setmetatable(fs_image, {
|
|||
|
||||
local fs_item_image = {
|
||||
render = function(e, x, y, w, h)
|
||||
return string.format("item_image[%f,%f;%f,%f;%s]", get(e, "x"), get(e, "y"), get(e, "w"), get(e, "h"), get(e, "item"))
|
||||
return string.format("item_image[%f,%f;%f,%f;%s]", x or get(e, "x"), y or get(e, "y"), w or get(e, "w"), h or get(e, "h"), get(e, "item"))
|
||||
end,
|
||||
tooltip = add_elem_tooltip,
|
||||
}
|
||||
|
|
@ -452,9 +494,9 @@ local fs_button = {
|
|||
elseif e._image then
|
||||
if e._image_pressed then
|
||||
-- We never specify noclip or border here. That's a job for styles.
|
||||
out[#out +1] = string.format("image_button[%f,%f;%f,%f;%s;%s;;;%s]", x or get(e, "x"), y or get(e, "y"), w or get(e, "w"), h or get(e, "h"), get(e, "_image"), e.__id, get(e, "label"), get(e, "_image_pressed"))
|
||||
out[#out +1] = string.format("image_button[%f,%f;%f,%f;%s;%s;%s;;%s]", x or get(e, "x"), y or get(e, "y"), w or get(e, "w"), h or get(e, "h"), get(e, "_image"), e.__id, get(e, "label"), get(e, "_image_pressed"))
|
||||
else
|
||||
out[#out +1] = string.format("image_button%s[%f,%f;%f,%f;%s;%s]", e._exit and "_exit" or "", x or get(e, "x"), y or get(e, "y"), w or get(e, "w"), h or get(e, "h"), get(e, "_image"), e.__id, get(e, "label"))
|
||||
out[#out +1] = string.format("image_button%s[%f,%f;%f,%f;%s;%s;%s]", e._exit and "_exit" or "", x or get(e, "x"), y or get(e, "y"), w or get(e, "w"), h or get(e, "h"), get(e, "_image"), e.__id, get(e, "label"))
|
||||
end
|
||||
else
|
||||
out[#out +1] = string.format("button%s[%f,%f;%f,%f;%s;%s]", e._exit and "_exit" or "", x or get(e, "x"), y or get(e, "y"), w or get(e, "w"), h or get(e, "h"), e.__id, get(e, "label"))
|
||||
|
|
@ -607,7 +649,7 @@ local fs_field = {
|
|||
fs_field.__index = fs_field
|
||||
setmetatable(fs_field, {
|
||||
__call = function(_, x, y, w, h, label, value)
|
||||
local e = {x = x, y = y, w = w, h = h, label = label or "", value = value or label or "", _styles = {}}
|
||||
local e = {x = x, y = y, w = w, h = h, label = value and label or "", value = value or label or "", _styles = {}}
|
||||
e.__id = new_id()
|
||||
setmetatable(e, fs_field)
|
||||
table.insert(ctx, e)
|
||||
|
|
@ -626,7 +668,7 @@ local fs_scrollbar = {
|
|||
if e._options then
|
||||
out[#out +1] = "scrollbaroptions["
|
||||
local first = true
|
||||
for k, v in pairs(options) do
|
||||
for k, v in pairs(e._options) do
|
||||
if not first then
|
||||
out[#out +1] = ";"
|
||||
end
|
||||
|
|
@ -659,7 +701,7 @@ setmetatable(fs_scrollbar, {
|
|||
local e = {x = x, y = y, w = w, h = h, orientation = orientation or "vertical", value = value or "", _styles = {}}
|
||||
e.__id = "_"..minetest.get_us_time().."_"..math.random(1, 100000)
|
||||
setmetatable(e, fs_scrollbar)
|
||||
table.insert(ctx, e)
|
||||
ctx[#ctx +1] = e
|
||||
return e
|
||||
end
|
||||
})
|
||||
|
|
@ -683,8 +725,11 @@ local fs_scroll_container = {
|
|||
}
|
||||
for i = 1, #e do
|
||||
local c = e[i]
|
||||
local cx = resolve_layout_units(get(c, "x"), w)
|
||||
local cy = resolve_layout_units(get(c, "y"), h)
|
||||
local cx, cy
|
||||
if c.x then
|
||||
cx = resolve_layout_units(get_raw(c, "x"), w)
|
||||
cy = resolve_layout_units(get_raw(c, "y"), h)
|
||||
end
|
||||
|
||||
local cw, ch
|
||||
if c.w then
|
||||
|
|
@ -697,7 +742,6 @@ local fs_scroll_container = {
|
|||
out[#out +1] = "scroll_container_end[]"
|
||||
|
||||
if e._scrollbar then
|
||||
e._scrollbar.value = e._scroll_pos or ""
|
||||
out[#out +1] = e._scrollbar:render()
|
||||
else
|
||||
local v = e.__fs._ctx.fields[e.__id]
|
||||
|
|
@ -707,14 +751,38 @@ local fs_scroll_container = {
|
|||
return table.concat(out)
|
||||
end,
|
||||
scrollbar = function(e, fn, ...)
|
||||
-- Make a fake ctx to put the scrollbar in.
|
||||
ctx = setmetatable({
|
||||
__parent = ctx,
|
||||
_events = setmetatable({}, {
|
||||
__index = ctx._events,
|
||||
__newindex = ctx._events
|
||||
}),
|
||||
}, {
|
||||
-- Assignment trap to ensure that the scrollbar gets our ID immediately on construction.
|
||||
__newindex = function(tbl, key, value)
|
||||
value.__id = e.__id
|
||||
return rawset(tbl, key, value)
|
||||
end
|
||||
})
|
||||
if type(fn) == "function" then
|
||||
e._scrollbar = fn()
|
||||
fn()
|
||||
else
|
||||
e._scrollbar = fs_scrollbar(fn, ...)
|
||||
fs_scrollbar(fn, ...)
|
||||
end
|
||||
e._scrollbar = ctx[1]
|
||||
ctx = ctx.__parent
|
||||
e._scrollbar.__id = e.__id
|
||||
return e
|
||||
end,
|
||||
onscroll = function(e, fn)
|
||||
if e._scrollbar then
|
||||
e._scrollbar:onchange(fn)
|
||||
return e
|
||||
end
|
||||
e._scrollbar = fs_scrollbar(-800, -800, 0, 0, e._orientation, e.__id):onchange(fn)
|
||||
return e
|
||||
end,
|
||||
-- A non-transient name is required in order to preserve scroll position upon a rebuild.
|
||||
named = function(e, name)
|
||||
e.__id = name
|
||||
|
|
@ -724,7 +792,22 @@ local fs_scroll_container = {
|
|||
fs_scroll_container.__index = fs_scroll_container
|
||||
setmetatable(fs_scroll_container, {
|
||||
__call = function(_, x, y, w, h, orientation, factor, padding)
|
||||
local e = {x = x, y = y, w = w, h = h, orientation = orientation or "vertical", factor = factor or "", padding = padding or "0", _styles = {}, __parent = ctx, _events = setmetatable({}, {__index = ctx._events, __newindex = ctx._events}), __fs = ctx.__parent or ctx}
|
||||
local e = {
|
||||
x = x,
|
||||
y = y,
|
||||
w = w,
|
||||
h = h,
|
||||
orientation = orientation or "vertical",
|
||||
factor = factor or "",
|
||||
padding = padding or "0",
|
||||
_styles = {},
|
||||
__parent = ctx,
|
||||
_events = setmetatable({}, {
|
||||
__index = ctx._events,
|
||||
__newindex = ctx._events
|
||||
}),
|
||||
__fs = ctx.__parent or ctx
|
||||
}
|
||||
e.__id = new_id()
|
||||
setmetatable(e, fs_scroll_container)
|
||||
table.insert(ctx, e)
|
||||
|
|
@ -746,16 +829,24 @@ end
|
|||
|
||||
local fs_group = {
|
||||
render = function(e, x, y, w, h)
|
||||
x = x or get(e, "x")
|
||||
y = y or get(e, "y")
|
||||
w = w or get(e, "w")
|
||||
h = h or get(e, "h")
|
||||
|
||||
local out = {}
|
||||
for i = 1, #e do
|
||||
local c = e[i]
|
||||
local cx = resolve_layout_units(get(c, "x"), e.width) +(x or get(e, "x"))
|
||||
local cy = resolve_layout_units(get(c, "y"), e.height) +(y or get(e, "y"))
|
||||
local cx, cy
|
||||
if c.x then
|
||||
cx = resolve_layout_units(get_raw(c, "x"), w) +x
|
||||
cy = resolve_layout_units(get_raw(c, "y"), h) +y
|
||||
end
|
||||
|
||||
local cw, ch
|
||||
if c.w then
|
||||
cw = resolve_layout_units(get(c, "w"), e.width)
|
||||
ch = resolve_layout_units(get(c, "h"), e.height)
|
||||
cw = resolve_layout_units(get(c, "w"), w)
|
||||
ch = resolve_layout_units(get(c, "h"), h)
|
||||
end
|
||||
|
||||
out[#out +1] = c:render(cx, cy, cw, ch)
|
||||
|
|
@ -803,14 +894,16 @@ local fs_row = {
|
|||
-- Pass 1: Collect total sizing information to allow layout computation.
|
||||
for i = 1, #e do
|
||||
local c = e[i]
|
||||
local ca, ca_flex = resolve_flex_layout_units(get(c, e._direction == "column" and "h" or "w"))
|
||||
if c.w then
|
||||
if ca_flex then
|
||||
flex_found = true
|
||||
c.__flex = ca
|
||||
total_grow = total_grow +ca
|
||||
else
|
||||
used_space = used_space +(ca or 0)
|
||||
if not c._no_layout then
|
||||
local ca, ca_flex = resolve_flex_layout_units(get(c, e._direction == "column" and "h" or "w"), axis_size)
|
||||
if c.w then
|
||||
if ca_flex then
|
||||
flex_found = true
|
||||
c.__flex = ca
|
||||
total_grow = total_grow +ca
|
||||
else
|
||||
used_space = used_space +(ca or 0)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -826,12 +919,14 @@ local fs_row = {
|
|||
local current = 0
|
||||
for i = 1, #e do
|
||||
local c = e[i]
|
||||
c.__flex_offset = current
|
||||
if c.__flex then
|
||||
c.__flex = c.__flex *grow_basis
|
||||
current = current +c.__flex +e._gap
|
||||
else
|
||||
current = current +resolve_layout_units(get(c, e._direction == "column" and "h" or "w")) +e._gap
|
||||
if not c._no_layout then
|
||||
c.__flex_offset = current
|
||||
if c.__flex then
|
||||
c.__flex = c.__flex *grow_basis
|
||||
current = current +c.__flex +e._gap
|
||||
else
|
||||
current = current +resolve_layout_units(get(c, e._direction == "column" and "h" or "w"), axis_size) +e._gap
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -845,17 +940,31 @@ local fs_row = {
|
|||
|
||||
for i = 1, #e do
|
||||
local c = e[i]
|
||||
if c.w then
|
||||
if e._direction == "column" then
|
||||
out[#out +1] = c:render(resolve_layout_units(get(c, "x")), base +c.__flex_offset, resolve_layout_units(get(c, "w")), c.__flex)
|
||||
else
|
||||
out[#out +1] = c:render(base +c.__flex_offset, resolve_layout_units(get(c, "y")), c.__flex, resolve_layout_units(get(c, "h")))
|
||||
if c._no_layout then
|
||||
local cx, cy
|
||||
if c.x then
|
||||
cx = resolve_layout_units(get_raw(c, "x"), w) +x
|
||||
cy = resolve_layout_units(get_raw(c, "y"), h) +y
|
||||
end
|
||||
else
|
||||
|
||||
local cw, ch
|
||||
if c.w then
|
||||
cw = resolve_layout_units(get(c, "w"), w)
|
||||
ch = resolve_layout_units(get(c, "h"), h)
|
||||
end
|
||||
|
||||
out[#out +1] = c:render(cx, cy, cw, ch)
|
||||
elseif c.w then
|
||||
if e._direction == "column" then
|
||||
out[#out +1] = c:render(resolve_layout_units(get(c, "x")), base +c.__flex_offset)
|
||||
out[#out +1] = c:render(resolve_layout_units(get(c, "x"), w) +x, base +c.__flex_offset, resolve_layout_units(get(c, "w"), w), c.__flex)
|
||||
else
|
||||
out[#out +1] = c:render(base +c.__flex_offset, resolve_layout_units(get(c, "y")))
|
||||
out[#out +1] = c:render(base +c.__flex_offset, resolve_layout_units(get(c, "y"), h) +y, c.__flex, resolve_layout_units(get(c, "h"), h))
|
||||
end
|
||||
elseif c.x then
|
||||
if e._direction == "column" then
|
||||
out[#out +1] = c:render(resolve_layout_units(get(c, "x"), w) +x, base +c.__flex_offset)
|
||||
else
|
||||
out[#out +1] = c:render(base +c.__flex_offset, resolve_layout_units(get(c, "y"), h) +y)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -931,6 +1040,10 @@ local Window = {
|
|||
out[#out +1] = "allow_close[false]"
|
||||
end
|
||||
|
||||
if e._fullscreen then
|
||||
out[#out +1] = string.format("bgcolor[%s;%s;%s]", get(e, "_fgcolor"), get(e, "_fullscreen"), get(e, "_bgcolor"))
|
||||
end
|
||||
|
||||
for i = 1, #e do
|
||||
local c = e[i]
|
||||
local cx, cy
|
||||
|
|
@ -972,6 +1085,24 @@ local Window = {
|
|||
onclose = function(e, fn)
|
||||
e._onclose = fn
|
||||
return e
|
||||
end,
|
||||
bgcolor = function(e, foreground, fullscreen)
|
||||
if fullscreen then
|
||||
if fullscreen == true then
|
||||
e._fullscreen = "true"
|
||||
e._bgcolor = foreground
|
||||
else
|
||||
e._fullscreen = "both"
|
||||
e._fgcolor = foreground
|
||||
e._bgcolor = fullscreen
|
||||
end
|
||||
elseif not foreground then
|
||||
e._fullscreen = "neither"
|
||||
else
|
||||
e._fullscreen = "false"
|
||||
e._fgcolor = foreground
|
||||
end
|
||||
return e
|
||||
end
|
||||
}
|
||||
Window.__index = Window
|
||||
|
|
@ -979,17 +1110,23 @@ Window.__index = Window
|
|||
local function fs_begin(w, h)
|
||||
ctx = {_events = {on_click = {}, on_change = {}, on_scrollbar_event = {}, on_enter = {}}, width = w or 12, height = h or 10}
|
||||
setmetatable(ctx, Window)
|
||||
name_scope = {"_"}
|
||||
nth_id = {0}
|
||||
return ctx
|
||||
end
|
||||
|
||||
local function fs_end()
|
||||
local _ctx = ctx
|
||||
ctx = nil
|
||||
name_scope = nil
|
||||
nth_id = nil
|
||||
return _ctx
|
||||
end
|
||||
|
||||
local Context = {
|
||||
update = function(e)
|
||||
-- This is used to prevent changes in state from inside the builder to trigger another rebuild.
|
||||
if e._ignore then return end
|
||||
-- `_inert` should be set when many state updates may take place at once, to avoid a rapid succession of rebuilds.
|
||||
-- After these updates have taken place, the user should call `rebuild()` manually if `_dirty` is true.
|
||||
if e._inert then
|
||||
|
|
@ -1000,15 +1137,24 @@ local Context = {
|
|||
end,
|
||||
rebuild = function(e)
|
||||
e._dirty = nil
|
||||
e._ignore = true
|
||||
e:clear_state_bindings()
|
||||
|
||||
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)
|
||||
|
||||
e._ignore = nil
|
||||
|
||||
for x in pairs(e._linked_states) do
|
||||
x._getters[e.id] = e
|
||||
end
|
||||
|
||||
fs._ctx = e
|
||||
|
||||
for _, el in ipairs(fs) do
|
||||
|
|
@ -1024,7 +1170,9 @@ local Context = {
|
|||
e._events = fs._events
|
||||
|
||||
local str = fs:render()
|
||||
if e._is_inventory then
|
||||
if e._mainmenu then
|
||||
minetest.update_formspec(str)
|
||||
elseif e._is_inventory then
|
||||
e._player:set_inventory_formspec(str)
|
||||
else
|
||||
minetest.show_formspec(e.target, e.id, str)
|
||||
|
|
@ -1036,31 +1184,50 @@ 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()
|
||||
-- Kill our rebuild capability in case any callbacks fire on the way out.
|
||||
e.rebuild = function() end
|
||||
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 id = "form"..new_id()
|
||||
local function fs_show(target, fs, state)
|
||||
local id = "form"..unique_id()
|
||||
local ctx = setmetatable({
|
||||
formspec = fs,
|
||||
fields = {},
|
||||
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 id = "form"..new_id()
|
||||
local function fs_set_inventory(p, fs, state)
|
||||
local id = "form"..unique_id()
|
||||
local name = type(p) == "string" and p or p:get_player_name()
|
||||
local ctx = setmetatable({
|
||||
_is_inventory = true,
|
||||
|
|
@ -1070,7 +1237,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
|
||||
|
|
@ -1091,54 +1258,102 @@ end
|
|||
|
||||
-- MARK: Callback handling
|
||||
|
||||
minetest.register_on_player_receive_fields(function(p, form, fields)
|
||||
local name = p:get_player_name()
|
||||
local ctx = contexts[form]
|
||||
-- If this is an event from a player inventory, check if we have a context managing that inventory.
|
||||
-- No other special handling is needed, because the context already knows that it's an inventory and will react appropriately.
|
||||
if form == "" then
|
||||
ctx = inventories[name]
|
||||
end
|
||||
local function handler(ctx, fields)
|
||||
ctx._inert = true
|
||||
|
||||
-- Ensure that a) players can only trigger effects of formspecs that were opened legitimately, and b) only the player who opened it may interact with a given formspec instance.
|
||||
if ctx and ctx.target == name then
|
||||
ctx._inert = true
|
||||
-- We split event handling into two passes here in order to guarantee that action events take precedence over state change events.
|
||||
-- Otherwise, it would be possible for a field's state synchronization to accidentally overwrite state changed by an action, which is almost certainly not what the user intended to happen.
|
||||
|
||||
if fields.key_enter_field and ctx._events.on_enter[fields.key_enter_field] then
|
||||
ctx._events.on_enter[fields.key_enter_field](fields[fields.key_enter_field])
|
||||
end
|
||||
|
||||
for k, v in pairs(fields) do
|
||||
if ctx._events.on_click[k] then
|
||||
ctx._events.on_click[k](v)
|
||||
elseif ctx._events.on_scrollbar_event[k] then
|
||||
local ev = minetest.explode_scrollbar_event(v)
|
||||
ctx._events.on_scrollbar_event[k](ev.type, ev.value)
|
||||
elseif (not ctx.fields[k] or ctx.fields[k] ~= v) and ctx._events.on_change[k] then
|
||||
ctx._events.on_change[k](v)
|
||||
end
|
||||
end
|
||||
|
||||
ctx.fields = fields
|
||||
|
||||
ctx._inert = nil
|
||||
|
||||
if fields.quit then
|
||||
ctx:deinit()
|
||||
contexts[form] = nil
|
||||
else
|
||||
if ctx._dirty then
|
||||
ctx:rebuild()
|
||||
ctx._dirty = nil
|
||||
end
|
||||
-- First pass: Stateful element updates.
|
||||
for k, v in pairs(fields) do
|
||||
if ctx._events.on_scrollbar_event[k] then
|
||||
local ev = minetest.explode_scrollbar_event(v)
|
||||
ctx._events.on_scrollbar_event[k](ev.type, ev.value)
|
||||
elseif (not ctx.fields[k] or ctx.fields[k] ~= v) and ctx._events.on_change[k] then
|
||||
ctx._events.on_change[k](v)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
-- Remove all contexts tied to a leaving player.
|
||||
minetest.register_on_leaveplayer(function(p)
|
||||
fs_remove_inventory(p)
|
||||
end)
|
||||
-- Handle pressing Enter in a field. (This is somewhere between a state event and an action, so we process between passes.)
|
||||
if fields.key_enter_field and ctx._events.on_enter[fields.key_enter_field] then
|
||||
ctx._events.on_enter[fields.key_enter_field](fields[fields.key_enter_field])
|
||||
end
|
||||
|
||||
-- Second pass: Actions.
|
||||
for k, v in pairs(fields) do
|
||||
if ctx._events.on_click[k] then
|
||||
ctx._events.on_click[k](v)
|
||||
end
|
||||
end
|
||||
|
||||
ctx.fields = fields
|
||||
|
||||
ctx._inert = nil
|
||||
|
||||
if fields.quit then
|
||||
ctx:deinit()
|
||||
else
|
||||
if ctx._dirty then
|
||||
ctx:rebuild()
|
||||
ctx._dirty = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Compatibility with main menu usage.
|
||||
if minetest.update_formspec then
|
||||
local mainmenu_context
|
||||
function fs_show(fs, state)
|
||||
if mainmenu_context then
|
||||
mainmenu_context:deinit()
|
||||
end
|
||||
|
||||
local id = "form"..unique_id()
|
||||
local ctx = setmetatable({
|
||||
_mainmenu = true,
|
||||
formspec = fs,
|
||||
fields = {},
|
||||
target = "menu",
|
||||
id = id,
|
||||
_linked_states = {},
|
||||
state = state or {}
|
||||
}, Context)
|
||||
|
||||
mainmenu_context = ctx
|
||||
|
||||
ctx:rebuild()
|
||||
|
||||
return ctx
|
||||
end
|
||||
|
||||
fs_set_inventory = nil
|
||||
|
||||
minetest.button_handler = function(fields)
|
||||
handler(mainmenu_context, fields)
|
||||
end
|
||||
-- Normal in-game usage.
|
||||
else
|
||||
minetest.register_on_player_receive_fields(function(p, form, fields)
|
||||
local name = p:get_player_name()
|
||||
local ctx = contexts[form]
|
||||
-- If this is an event from a player inventory, check if we have a context managing that inventory.
|
||||
-- No other special handling is needed, because the context already knows that it's an inventory and will react appropriately.
|
||||
if form == "" then
|
||||
ctx = inventories[name]
|
||||
end
|
||||
|
||||
-- Ensure that a) players can only trigger effects of formspecs that were opened legitimately, and b) only the player who opened it may interact with a given formspec instance.
|
||||
if ctx and ctx.target == name then
|
||||
handler(ctx, fields)
|
||||
end
|
||||
end)
|
||||
|
||||
-- Remove all contexts tied to a leaving player.
|
||||
minetest.register_on_leaveplayer(function(p)
|
||||
fs_remove_inventory(p)
|
||||
end)
|
||||
end
|
||||
|
||||
|
||||
-- MARK: API exposure
|
||||
|
||||
|
|
@ -1151,6 +1366,8 @@ imfs = {
|
|||
get_field = get,
|
||||
begin = fs_begin,
|
||||
end_ = fs_end,
|
||||
scope = scope,
|
||||
scope_end = scope_end,
|
||||
show = fs_show,
|
||||
set_inventory = fs_set_inventory,
|
||||
remove_inventory = fs_remove_inventory,
|
||||
|
|
@ -1175,6 +1392,7 @@ imfs = {
|
|||
_named_tooltip = fs_named_tooltip,
|
||||
label = fs_label,
|
||||
arealabel = fs_arealabel,
|
||||
hypertext = fs_hypertext,
|
||||
box = fs_box,
|
||||
image = fs_image,
|
||||
item_image = fs_item_image,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue