Fix a lot of broken things.
This commit is contained in:
parent
6e0f653928
commit
be73993547
2 changed files with 309 additions and 95 deletions
19
README.md
19
README.md
|
|
@ -95,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.
|
||||
|
|
@ -180,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)`
|
||||
|
||||
|
|
@ -191,11 +197,20 @@ 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.
|
||||
|
|
@ -231,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)`
|
||||
|
||||
|
|
|
|||
385
init.lua
385
init.lua
|
|
@ -18,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
|
||||
|
|
@ -35,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
|
||||
|
|
@ -63,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
|
||||
|
||||
|
|
@ -90,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,
|
||||
|
|
@ -207,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"))
|
||||
|
|
@ -285,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
|
||||
|
|
@ -335,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)
|
||||
|
|
@ -360,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,
|
||||
}
|
||||
|
|
@ -453,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"))
|
||||
|
|
@ -608,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)
|
||||
|
|
@ -627,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
|
||||
|
|
@ -660,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
|
||||
})
|
||||
|
|
@ -684,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
|
||||
|
|
@ -698,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]
|
||||
|
|
@ -708,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
|
||||
|
|
@ -725,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)
|
||||
|
|
@ -747,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)
|
||||
|
|
@ -804,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
|
||||
|
|
@ -827,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
|
||||
|
||||
|
|
@ -846,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
|
||||
|
|
@ -932,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
|
||||
|
|
@ -973,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
|
||||
|
|
@ -980,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
|
||||
|
|
@ -1001,6 +1137,7 @@ local Context = {
|
|||
end,
|
||||
rebuild = function(e)
|
||||
e._dirty = nil
|
||||
e._ignore = true
|
||||
e:clear_state_bindings()
|
||||
|
||||
local tracker = e._linked_states
|
||||
|
|
@ -1012,6 +1149,12 @@ local Context = {
|
|||
|
||||
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
|
||||
|
|
@ -1027,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)
|
||||
|
|
@ -1046,6 +1191,8 @@ local Context = {
|
|||
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.
|
||||
|
|
@ -1057,7 +1204,7 @@ local Context = {
|
|||
Context.__index = Context
|
||||
|
||||
local function fs_show(target, fs, state)
|
||||
local id = "form"..new_id()
|
||||
local id = "form"..unique_id()
|
||||
local ctx = setmetatable({
|
||||
formspec = fs,
|
||||
fields = {},
|
||||
|
|
@ -1080,7 +1227,7 @@ local function fs_show(target, fs, state)
|
|||
end
|
||||
|
||||
local function fs_set_inventory(p, fs, state)
|
||||
local id = "form"..new_id()
|
||||
local id = "form"..unique_id()
|
||||
local name = type(p) == "string" and p or p:get_player_name()
|
||||
local ctx = setmetatable({
|
||||
_is_inventory = true,
|
||||
|
|
@ -1111,53 +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]
|
||||
local function handler(ctx, fields)
|
||||
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.
|
||||
|
||||
-- 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
|
||||
|
||||
-- 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
|
||||
|
||||
-- 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
|
||||
|
||||
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()
|
||||
else
|
||||
if ctx._dirty then
|
||||
ctx:rebuild()
|
||||
ctx._dirty = nil
|
||||
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
|
||||
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
|
||||
|
||||
-- Remove all contexts tied to a leaving player.
|
||||
minetest.register_on_leaveplayer(function(p)
|
||||
fs_remove_inventory(p)
|
||||
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
|
||||
|
||||
|
|
@ -1170,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,
|
||||
|
|
@ -1194,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