local fe = minetest.formspec_escape local hte = minetest.hypertext_escape local ctx local contexts = {} local inventories = {} local theme = {} -- MARK: Helpers -- Detects percentage units andconverts them into an appropriate number (based on `ref`). local function resolve_layout_units(value, ref) if type(value) == "string" then local num = tonumber(value) if num then return num end local percent = value:match "(%d+)%%" if ref and percent then return tonumber(percent) /100 *ref end error("Malformed layout units: "..value) else return value end end -- The same as resolve_layout_units, but allows (and notifies of) flex units. local function resolve_flex_layout_units(value, ref) if type(value) == "string" then local num = tonumber(value) if num then return num end local percent = value:match "(%d+)%%" if ref and percent then return tonumber(percent) /100 *ref end local flex = value:match "(%d+)x" if flex then return tonumber(flex), "flex" end error("Malformed layout units: "..value) else return value end end -- This should be a method on any element classes that support inline styling. local function add_elem_style(e, state, props) -- Allow omitting the state while keeping the properties the last argument. if not props then props = state state = "default" end e._styles[state] = imfs.style(e.__id..":"..state, props, true) return e end -- This should be a method on any element classes that support tooltips. local function add_elem_tooltip(e, text, bgcolor, txtcolor) -- 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) end return e end local function new_id() return "_"..minetest.get_us_time().."_"..math.random(1, 100000) end -- MARK: Data structures local observers = {} local state = { observers = observers, 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) end return e._val end, set = function(e, val) if val == nil then error "!" end e._old_val = e._val e._val = val for _, x in pairs(e._getters) do x:update() end end, __call = function(e, val) if val == nil then -- Getter return e:get() else -- Setter return e:set(val) end end } state.__index = state setmetatable(state, { __call = function(_, val) local e = {_getters = {}, _val = val} setmetatable(e, state) return e end }) local DerivedState = { get = function(e) if e._stale then e:update() end return e._val end, update = function(e) for dep in pairs(e._deps) do dep._getters[e.__id] = nil e._deps[dep] = nil end local tracker = e._deps table.insert(observers, tracker) local val = e._fn() table.remove(observers) e._val = val for dep in pairs(e._deps) do dep._getters[e.__id] = e end -- Don't propagate updates on initial computation to avoid stack overflow. if not e._stale then for _, x in pairs(e._getters) do x:update() end end e._stale = false end, __call = function(e) return e:get() end } DerivedState.__index = DerivedState function derive(fn) return setmetatable({_fn = fn, _stale = true, _getters = {}, _deps = {}, __id = minetest.get_us_time()}, DerivedState) end local function get(e, x) local item = e[x] local mt = getmetatable(item) if mt == state or mt == DerivedState then return fe(item()) end return fe(item) end -- MARK: Elements local fs_style = { render = function(e) local props = {} for k, v in pairs(e.props) do props[#props +1] = ";" props[#props +1] = k props[#props +1] = "=" props[#props +1] = tostring(v) end return string.format("style[%s%s]", e.name, table.concat(props)) end } fs_style.__index = fs_style setmetatable(fs_style, { __call = function(_, name, props, internal) local e = {name = name, props = props} setmetatable(e, fs_style) if not internal then table.insert(ctx, e) end return e end }) local fs_tooltip = { 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")) else return string.format( "tooltip[%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, "text"), get(e, "bgcolor"), get(e, "txtcolor") ) end end } fs_tooltip.__index = fs_tooltip setmetatable(fs_tooltip, { __call = function(_, x, y, w, h, text, bgcolor, txtcolor) local e = {x = x, y = y, w = w, h = h, text = text, bgcolor = bgcolor or "", txtcolor = txtcolor or ""} setmetatable(e, fs_tooltip) table.insert(ctx, e) return e end }) local function fs_named_tooltip(name, text, bgcolor, txtcolor) local e = {_name = name, text = text, bgcolor = bgcolor or "", txtcolor = txtcolor or ""} setmetatable(e, fs_tooltip) table.insert(ctx, e) return e end local fs_label = { render = function(e) return string.format("label[%f,%f;%s]", get(e, "x"), get(e, "y"), get(e, "txt")) end } fs_label.__index = fs_label setmetatable(fs_label, { __call = function(_, x, y, txt) local e = {x = x, y = y, txt = txt} setmetatable(e, fs_label) table.insert(ctx, e) return e end }) local fs_arealabel = { render = function(e) if e._scrollable then return string.format("textarea[%f,%f;%f,%f;;;%s]", get(e, "x"), get(e, "y"), get(e, "w"), get(e, "h"), get(e, "txt")) else return string.format("label[%f,%f;%f,%f;%s]", get(e, "x"), get(e, "y"), get(e, "w"), get(e, "h"), get(e, "txt")) end end, scrollable = function(e) e._scrollable = true return e end, tooltip = add_elem_tooltip, } fs_arealabel.__index = fs_arealabel setmetatable(fs_arealabel, { __call = function(_, x, y, w, h, txt) local e = {x = x, y = y, w = w, h = h, txt = txt} setmetatable(e, fs_arealabel) table.insert(ctx, e) return e end }) local fs_hypertext = { render = function(e) return string.format("hypertext[%f,%f;%f,%f;%s]", get(e, "x"), get(e, "y"), get(e, "w"), get(e, "h"), hte(get(e, "txt"))) end, onaction = function(e, fn) ctx._events.on_click[e.__id] = fn return e end, tooltip = add_elem_tooltip, } fs_hypertext.__index = fs_hypertext setmetatable(fs_hypertext, { __call = function(_, x, y, w, h, txt) local e = {x = x, y = y, w = w, h = h, txt = txt} e.__id = "_"..minetest.get_us_time().."_"..math.random(1, 100000) setmetatable(e, fs_hypertext) table.insert(ctx, e) return e end }) local fs_box = { render = function(e, x, y, w, h) return string.format("box[%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, "bg")) end, tooltip = add_elem_tooltip, } fs_box.__index = fs_box setmetatable(fs_box, { __call = function(_, x, y, w, h, bg) local e = {x = x, y = y, w = w, h = h, bg = bg} setmetatable(e, fs_box) table.insert(ctx, e) return e end }) local fs_image = { render = function(e, x, y, w, h) if e._anim_frames then return string.format( "animated_image[%f,%f;%f,%f;%s;%s;%f;%f;%f;%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, "texture"), get(e, "_anim_frames"), get(e, "_anim_duration"), get(e, "_anim_start"), 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")) end end, animated = function(e, frames, duration, start) -- We only need an ID if the image is animated and must persist its state. e.__id = e.__id or new_id() e._anim_frames = frames e._anim_duration = duration or 50 e._anim_start = start or 1 return e end, tooltip = add_elem_tooltip, } fs_image.__index = fs_image setmetatable(fs_image, { __call = function(_, x, y, w, h, texture, middle) local e = {x = x, y = y, w = w, h = h, texture = texture, middle = middle or ""} setmetatable(e, fs_image) table.insert(ctx, e) return e end }) 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")) end, tooltip = add_elem_tooltip, } fs_item_image.__index = fs_item_image setmetatable(fs_item_image, { __call = function(_, x, y, w, h, item) local e = {x = x, y = y, w = w, h = h, item = item} setmetatable(e, fs_item_image) table.insert(ctx, e) return e end }) local fs_model = { render = function(e, x, y, w, h) local out = {} for _, x in pairs(e._styles) do out[#out +1] = x:render() end out[#out +1] = string.format( "model[%f,%f;%f,%f;%s;%s;%s;%f;%s;%s;%s;%f]", x or get(e, "x"), y or get(e, "y"), w or get(e, "w"), h or get(e, "h"), e.__id, get(e, "mesh"), get(e, "textures"), get(e, "_rotation") or "", get(e, "_continuous") or "", get(e, "_mouse_control"), get(e, "_animation") or "", get(e, "_animation_speed") ) return table.concat(out) end, rotation = function(e, rot, continuous) e._rotation = rot e._continuous = continuous end, mouse_control = function(e, mouse_control) e._mouse_control = mouse_control ~= false and true or false end, animated = function(frames, speed) e._animation = frames e._animation_speed = speed or 1 end, style = add_elem_style, tooltip = add_elem_tooltip, } fs_model.__index = fs_model setmetatable(fs_model, { __call = function(_, x, y, w, h, mesh, textures) local e = {x = x, y = y, w = w, h = h, mesh = mesh, textures = textures, mouse_control = true} e.__id = new_id() setmetatable(e, fs_model) table.insert(ctx, e) return e end }) local fs_button = { render = function(e, x, y, w, h) local out = {} for _, x in pairs(e._styles) do out[#out +1] = x:render() end if e._item then out[#out +1] = string.format("item_image_button[%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, "_item"), e.__id, get(e, "label")) 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")) 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")) 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")) end return table.concat(out) end, image = function(e, img, pressed_img) e._image = img e._image_pressed = pressed_img end, item_image = function(e, item) e._item = item end, exit = function(e) e._exit = true end, onclick = function(e, fn) ctx._events.on_click[e.__id] = fn return e end, style = add_elem_style, tooltip = add_elem_tooltip, } fs_button.__index = fs_button setmetatable(fs_button, { __call = function(_, x, y, w, h, label) local e = {x = x, y = y, w = w, h = h, label = label, _styles = {}} e.__id = new_id() setmetatable(e, fs_button) table.insert(ctx, e) return e end }) local fs_checkbox = { render = function(e, x, y) return string.format("checkbox[%f,%f;%s;%s;%s]", x or get(e, "x"), y or get(e, "y"), e.__id, get(e, "label"), get(e, "checked") and "true" or "false") end, onchange = function(fn) ctx._events.on_change[e.__id] = fn return e end, tooltip = add_elem_tooltip, } fs_checkbox.__index = fs_checkbox setmetatable(fs_checkbox, { __call = function(_, x, y, label, checked) local e = {x = x, y = y, label = label or "", checked = checked or label or false} e.__id = new_id() setmetatable(e, fs_checkbox) table.insert(ctx, e) return e end }) local fs_list = { render = function(e, x, y, w, h) w = w or get(e, "w") h = h or get(e, "h") -- These lines will cause `w` and `h` to be interpreted as formspec coordinates rather than numbers of slots. -- w = w -((w -1) /4) -- h = h -((h -1) /4) return string.format("list[%s;%s;%f,%f;%d,%d;%s]", get(e, "location"), get(e, "list"), x or get(e, "x"), y or get(e, "y"), w, h, get(e, "start")) end } fs_list.__index = fs_list setmetatable(fs_list, { __call = function(_, x, y, w, h, location, list, start) local e = {x = x, y = y, w = w, h = h, location = location or "current_player", list = list or "main", start = start or ""} e.__id = new_id() setmetatable(e, fs_list) table.insert(ctx, e) return e end }) local fs_inventory = fs_list local fs_listring = { render = function(e) if e.location then return string.format("listring[%s;%s]", e.location, e.list) else return "listring[]" end end, } fs_listring.__index = fs_listring setmetatable(fs_listring, { __call = function(_, location, list) local e = location and list and {location = location, list = list} or {} setmetatable(e, fs_listring) table.insert(ctx, e) return e end }) local fs_field = { render = function(e, x, y, w, h) local out = {} for _, x in pairs(e._styles) do out[#out +1] = x:render() end x = x or get(e, "x") y = y or get(e, "y") w = w or get(e, "w") h = h or get(e, "h") if e._password then out[#out +1] = string.format("pwdfield[%f,%f;%f,%f;%s;%s]", x, y, w, h, e.__id, get(e, "label")) if not e._close_on_enter then out[#out +1] = string.format("field_close_on_enter[%s;false]", e.__id) end else out[#out +1] = string.format("%s[%f,%f;%f,%f;%s;%s;%s]", e._textarea and "textarea" or "field", x, y, w, h, e.__id, get(e, "label"), get(e, "value")) if not e._textarea and not e._close_on_enter then out[#out +1] = string.format("field_close_on_enter[%s;false]", e.__id) end end return table.concat(out) end, onenter = function(e, fn) ctx._events.on_enter[e.__id] = fn return e end, onchange = function(e, fn) ctx._events.on_change[e.__id] = fn return e end, close_on_enter = function(e) e._close_on_enter = true return e end, multiline = function(e) e._textarea = true return e end, password = function(e) e._password = true return e end, style = add_elem_style, tooltip = add_elem_tooltip, } 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 = {}} e.__id = new_id() setmetatable(e, fs_field) table.insert(ctx, e) return e end }) local function fs_textarea(...) return fs_field(...) :multiline() end local fs_scrollbar = { render = function(e, x, y, w, h) local out = {} if e._options then out[#out +1] = "scrollbaroptions[" local first = true for k, v in pairs(options) do if not first then out[#out +1] = ";" end out[#out +1] = k out[#out +1] = "=" out[#out +1] = v first = nil end out[#out +1] = "]" end out[#out +1] = string.format("scrollbar[%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, "orientation"), e.__id, get(e, "value")) return table.concat(out) end, options = function(e, props) e._options = props return e end, onchange = function(e, fn) ctx._events.on_scrollbar_event[e.__id] = fn return e end, style = add_elem_style, tooltip = add_elem_tooltip, } fs_scrollbar.__index = fs_scrollbar setmetatable(fs_scrollbar, { __call = function(_, x, y, w, h, orientation, value) 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) return e end }) local fs_scroll_container = { render = function(e, x, y, w, h, orient, fac, pad) 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 = { string.format("scroll_container[%f,%f;%f,%f;%s;%s;%s;%s]", x, y, w, h, e.__id, orient or get(e, "orientation"), fac or get(e, "factor"), pad or get(e, "padding") ) } 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 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) end 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] out[#out +1] = string.format("scrollbar[-800,-800;0,0;%s;%s;%s]", get(e, "orientation"), e.__id, v and v:sub(5) or "") end return table.concat(out) end, scrollbar = function(e, fn, ...) if type(fn) == "function" then e._scrollbar = fn() else e._scrollbar = fs_scrollbar(fn, ...) end e._scrollbar.__id = e.__id 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 return e end } 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} e.__id = new_id() setmetatable(e, fs_scroll_container) table.insert(ctx, e) ctx = e return e end }) local function fs_scroll_container_end() if getmetatable(ctx) ~= fs_scroll_container then minetest.log("warn", "`scroll_container_end` has no scroll container to end; it will be ignored.") return end ctx = ctx.__parent end -- MARK: Builtin layouting helpers local fs_group = { render = function(e, x, y, w, 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 cw, ch if c.w then cw = resolve_layout_units(get(c, "w"), e.width) ch = resolve_layout_units(get(c, "h"), e.height) end out[#out +1] = c:render(cx, cy, cw, ch) end return table.concat(out) end, } fs_group.__index = fs_group setmetatable(fs_group, { __call = function(_, x, y, w, h) local e = {x = x, y = y, w = w, h = h, _gap = 0, __parent = ctx, _events = setmetatable({}, {__index = ctx._events, __newindex = ctx._events}), __fs = ctx.__parent or ctx} setmetatable(e, fs_group) table.insert(ctx, e) ctx = e return e end }) local function fs_group_end() if getmetatable(ctx) ~= fs_group then minetest.log("warn", "`group_end` has no group to end; it will be ignored.") return end ctx = ctx.__parent end local fs_row = { 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 axis = e._direction == "column" and y or x local axis_size = e._direction == "column" and h or w local out = {} local total_grow = 0 local used_space = 0 local flex_found -- 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) end end end used_space = used_space +(e._gap *math.max(0, #e -1)) local grow_basis = total_grow > 0 and (math.max(0, axis_size -used_space) /total_grow) or 0 -- If any flex element exists, it will take up all unused space, so the total width is guaranteed to be 100%. -- Otherwise, we can simply use the size we already calculated for fixed elements. local total_width = flex_found and axis_size or used_space -- Pass 2: Assign element positions based on flex ratios. 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 end end -- Pass 3: Justify and render. local base = axis if e._align == "center" then base = axis +((axis_size -total_width) /2) elseif e._align == "right" then base = axis +axis_size -total_width end 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"))) end else if e._direction == "column" then out[#out +1] = c:render(resolve_layout_units(get(c, "x")), base +c.__flex_offset) else out[#out +1] = c:render(base +c.__flex_offset, resolve_layout_units(get(c, "y"))) end end end return table.concat(out) end, direction = function(e, dir) e._direction = dir return e end, gap = function(e, gap) e._gap = gap return e end, align = function(e, align) e._align = align return e end, } fs_row.__index = fs_row setmetatable(fs_row, { __call = function(_, x, y, w, h) local e = {x = x, y = y, w = w, h = h, _gap = 0, __parent = ctx, _events = setmetatable({}, {__index = ctx._events, __newindex = ctx._events}), __fs = ctx.__parent or ctx} setmetatable(e, fs_row) table.insert(ctx, e) ctx = e return e end }) local function fs_row_end() if getmetatable(ctx) ~= fs_row then minetest.log("warn", "`row_end` has no row to end; it will be ignored.") return end ctx = ctx.__parent end local function fs_column(...) return fs_row(...) :direction "column" end local fs_column_end = fs_row_end -- MARK: Building local Window = { render = function(e) local out = { "formspec_version[10]", string.format("size[%f,%f]", e.width, e.height) } if e._no_prepend then out[#out +1] = "no_prepend[]" end if e._position then out[#out +1] = string.format("position[%f,%f]", e._position.x, e._position.y) end if e._anchor then out[#out +1] = string.format("anchor[%f,%f]", e._anchor.x, e._anchor.y) end if e._padding then out[#out +1] = string.format("padding[%f,%f]", e._padding.x, e._padding.y) end if e._modal then out[#out +1] = "allow_close[false]" end for i = 1, #e do local c = e[i] local cx = resolve_layout_units(get(c, "x"), e.width) local cy = resolve_layout_units(get(c, "y"), e.height) 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) end out[#out +1] = c:render(cx, cy, cw, ch) end return table.concat(out) end, no_prepend = function(e) e._no_prepend = true return e end, position = function(e, x, y) e._position = {x = x, y = y} return e end, anchor = function(e, x, y) e._anchor = {x = x, y = y} return e end, padding = function(e, x, y) e._padding = {x = x, y = y} return e end, modal = function(e, modal) e._modal = modal ~= false and true or false return e end, onclose = function(e, fn) e._onclose = fn return e end } 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) return ctx end local function fs_end() local _ctx = ctx ctx = nil return _ctx end local Context = { update = function(e) -- `_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 e._dirty = true else e:rebuild() end end, rebuild = function(e) e._dirty = nil 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 table.remove(observers) fs._ctx = e for _, el in ipairs(fs) do for _, x in pairs(el) do local mt = getmetatable(x) if mt == state or mt == DerivedState then x._getters[e.id] = e e._linked_states[x] = true end end end e._events = fs._events local str = fs:render() if e._is_inventory then e._player:set_inventory_formspec(str) else minetest.show_formspec(e.target, e.id, str) end end, clear_state_bindings = function(e) for x in pairs(e._linked_states) do x._getters[e.id] = nil end end, deinit = function(e) e:clear_state_bindings() end } Context.__index = Context local function fs_show(target, fs, args) local id = "form"..new_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 {} }, Context) contexts[id] = ctx ctx:rebuild() return ctx end local function fs_set_inventory(p, fs, args) local id = "form"..new_id() local name = type(p) == "string" and p or p:get_player_name() local ctx = setmetatable({ _is_inventory = true, _player = type(p) == "string" and minetest.get_player_by_name(p) or p, formspec = fs, fields = {}, target = name, id = id, _linked_states = {}, args = args or {} }, Context) inventories[name] = ctx ctx:rebuild() return ctx end local function fs_remove_inventory(p) local name = p:get_player_name() local inv = inventories[name] if inv then inv:deinit() inventories[name] = nil end 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 -- 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() contexts[form] = nil else if ctx._dirty then ctx:rebuild() ctx._dirty = nil end end end end) -- Remove all contexts tied to a leaving player. minetest.register_on_leaveplayer(function(p) fs_remove_inventory(p) end) -- MARK: API exposure imfs = { _contexts = contexts, _inventories = inventories, state = state, derive = derive, get_field = get, begin = fs_begin, end_ = fs_end, show = fs_show, set_inventory = fs_set_inventory, remove_inventory = fs_remove_inventory, resolve_layout_units = resolve_layout_units, resolve_flex_layout_units = resolve_flex_layout_units, container_start = function() local container = {__parent = ctx, _events = setmetatable({}, {__index = ctx._events, __newindex = ctx._events}), __fs = ctx.__parent or ctx} table.insert(ctx, container) ctx = container end, container_end = function() ctx = ctx.__parent end, add_to_context = function(container) table.insert(ctx, container) end, theme = theme, style = fs_style, tooltip = fs_tooltip, _named_tooltip = fs_named_tooltip, label = fs_label, arealabel = fs_arealabel, box = fs_box, image = fs_image, item_image = fs_item_image, model = fs_model, button = fs_button, list = fs_list, inventory = fs_inventory, listring = fs_listring, field = fs_field, textarea = fs_textarea, checkbox = fs_checkbox, scrollbar = fs_scrollbar, scroll_container = fs_scroll_container, scroll_container_end = fs_scroll_container_end, group = fs_group, group_end = fs_group_end, row = fs_row, row_end = fs_row_end, column = fs_column, column_end = fs_column_end, -- Can be called in a game's base mod to globalize imfs for brevity. export = function() for k, v in pairs(imfs) do local key if k == "state" or k == "derive" then key = k elseif k == "end_" then key = "fs_end" else key = "fs_"..k end _G[key] = v end end }