1501 lines
44 KiB
Lua
1501 lines
44 KiB
Lua
|
|
local fe = minetest.formspec_escape
|
|
local hte = minetest.hypertext_escape
|
|
|
|
local ctx
|
|
|
|
local player_contexts = {}
|
|
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*.?%d+)%%"
|
|
if ref and percent then
|
|
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
|
|
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*.?%d+)%%"
|
|
if ref and percent then
|
|
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*.?%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, 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
|
|
-- 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
|
|
|
|
local function string_or(a, b)
|
|
return (a ~= "" and a) or b
|
|
end
|
|
|
|
-- MARK: Data structures
|
|
|
|
local observers = {}
|
|
local state = {
|
|
observers = observers,
|
|
get = function(e)
|
|
local observer = observers[#observers]
|
|
if observer then
|
|
observer[e] = true
|
|
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(tostring(item() or ""))
|
|
end
|
|
return fe(tostring(item or ""))
|
|
end
|
|
|
|
local function get_raw(e, x)
|
|
local item = e[x]
|
|
local mt = getmetatable(item)
|
|
if mt == state or mt == DerivedState then
|
|
return item()
|
|
end
|
|
return 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 = {
|
|
_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"))
|
|
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 imfs.theme.tooltip_bg or "#444", txtcolor = txtcolor or imfs.theme.tooltip_text or "#aaa"}
|
|
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 imfs.theme.tooltip_bg or "#444", txtcolor = txtcolor or imfs.theme.tooltip_text or "#aaa"}
|
|
setmetatable(e, fs_tooltip)
|
|
table.insert(ctx, e)
|
|
return e
|
|
end
|
|
|
|
local fs_label = {
|
|
render = function(e, x, y)
|
|
return string.format("label[%f,%f;%s]", x or get(e, "x"), y or 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, 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")
|
|
|
|
if e._scrollable then
|
|
return string.format("textarea[%f,%f;%f,%f;;;%s]", x, y, w, h, get(e, "txt"))
|
|
else
|
|
return string.format("label[%f,%f;%f,%f;%s]", x, y, w, 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, x, y, w, h)
|
|
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
|
|
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]", 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)
|
|
-- 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]", 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,
|
|
}
|
|
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
|
|
|
|
local textures = get_raw(e, "textures")
|
|
if type(textures) ~= "string" then
|
|
textures = table.concat(textures, ",")
|
|
end
|
|
|
|
local rx = get_raw(e, "_rotation_x")
|
|
local ry = get_raw(e, "_rotation_y")
|
|
local rotation = ""
|
|
if rx and ry then
|
|
rotation = string.format("%s,%s", fe(rx), fe(ry))
|
|
end
|
|
|
|
local as = get_raw(e, "_animation_start")
|
|
local ae = get_raw(e, "_animation_end")
|
|
local animation = ""
|
|
if as and ae then
|
|
animation = string.format("%s,%s", fe(as), fe(ae))
|
|
end
|
|
|
|
out[#out +1] = string.format(
|
|
"model[%f,%f;%f,%f;%s;%s;%s;%s;%s;%s;%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, "mesh"), textures,
|
|
rotation, get(e, "_continuous"),
|
|
get(e, "_mouse_control"),
|
|
animation, get(e, "_animation_speed")
|
|
)
|
|
return table.concat(out)
|
|
end,
|
|
rotation = function(e, x, y, continuous)
|
|
e._rotation_x = x
|
|
e._rotation_y = y or 0
|
|
e._continuous = continuous
|
|
return e
|
|
end,
|
|
mouse_control = function(e, mouse_control)
|
|
e._mouse_control = mouse_control ~= false and true or false
|
|
return e
|
|
end,
|
|
animated = function(e, start, end_, speed)
|
|
e._animation_start = start
|
|
e._animation_end = end_
|
|
e._animation_speed = speed or 1
|
|
return e
|
|
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, _styles = {}}
|
|
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;;%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;%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
|
|
return e
|
|
end,
|
|
item_image = function(e, item)
|
|
e._item = item
|
|
return e
|
|
end,
|
|
exit = function(e)
|
|
e._exit = true
|
|
return e
|
|
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")
|
|
|
|
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 = type(w == "string") and w or w +((w -1) /4), h = type(h) == "string" and h or h +((h -1) /4), 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 = value and 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(e._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)
|
|
ctx[#ctx +1] = 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, 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
|
|
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
|
|
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, ...)
|
|
-- 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
|
|
fn()
|
|
else
|
|
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
|
|
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)
|
|
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, 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"), w)
|
|
ch = resolve_layout_units(get(c, "h"), h)
|
|
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]
|
|
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
|
|
|
|
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]
|
|
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
|
|
|
|
-- 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._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
|
|
|
|
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"), 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"), 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
|
|
|
|
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
|
|
|
|
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
|
|
if c.x then
|
|
cx = resolve_layout_units(get(c, "x"), e.width)
|
|
cy = resolve_layout_units(get(c, "y"), e.height)
|
|
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)
|
|
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,
|
|
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
|
|
|
|
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
|
|
e._dirty = true
|
|
else
|
|
e:rebuild()
|
|
end
|
|
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.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
|
|
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._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)
|
|
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)
|
|
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, 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 = {},
|
|
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, state)
|
|
local id = "form"..unique_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 = {},
|
|
state = state 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
|
|
|
|
local function handler(ctx, fields)
|
|
ctx._inert = true
|
|
|
|
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
|
|
|
|
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
|
|
|
|
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
|
|
|
|
imfs = {
|
|
_contexts = contexts,
|
|
_inventories = inventories,
|
|
|
|
state = state,
|
|
derive = derive,
|
|
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,
|
|
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,
|
|
hypertext = fs_hypertext,
|
|
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
|
|
}
|
|
|
|
|
|
|
|
local counter = imfs.state(0)
|
|
local str = imfs.state("")
|
|
local pressed = imfs.state(false)
|
|
|
|
local function fs()
|
|
imfs.begin(12, 10)
|
|
|
|
imfs.label(1, 2, "Counter: "..counter()) -- We don't need to use a derived state here because the window will detect this dependency.
|
|
imfs.label(1, 3, derive(function() return "Length: "..#str() end)) -- However, using a derived state will still work just fine.
|
|
|
|
if pressed() then -- Reactive conditional rendering can be achieved using states.
|
|
imfs.label(1, 4, "Pressed!!")
|
|
end
|
|
|
|
imfs.button(4, 0.5, 4, 0.75, "Increment")
|
|
:style {
|
|
bgcolor = "#aaf"
|
|
}
|
|
:style("hovered", {
|
|
bgcolor = "#faa"
|
|
})
|
|
:style("pressed", {
|
|
bgcolor = "#afa"
|
|
})
|
|
:onclick(function()
|
|
counter(counter() +1)
|
|
end)
|
|
|
|
imfs.field(4, 2, 4, 0.75, "Test", str)
|
|
:onchange(function(value)
|
|
str(value)
|
|
end)
|
|
:onenter(function(value)
|
|
str(value)
|
|
pressed(true)
|
|
end)
|
|
|
|
local m = imfs.model(1, 1, 3, 5, "firefly_character.gltf", {"firefly_character.png"})
|
|
:rotation(0, 90)
|
|
:tooltip "Test"
|
|
|
|
imfs.scrollbar(0, 0, 0.5, "100%")
|
|
|
|
imfs.scroll_container(4, 3, 8, 7)
|
|
:named("test")
|
|
|
|
for i = 0, 10 do -- One benefit of functional style over table style is that you can use loops seamlessly.
|
|
imfs.label(0, i, i)
|
|
end
|
|
|
|
imfs.row(0, 1, 8, 4)
|
|
|
|
imfs.box(0, 0, "1x", 2, "#f88") -- The "1x" unit for the width marks this element as dynamically resizing.
|
|
imfs.box(0, 0, counter() *0.25, 2, "#8f8")
|
|
imfs.box(0, 0, 2, 2, "#88f")
|
|
|
|
imfs.row_end()
|
|
|
|
imfs.column(0, 3, 4, 8)
|
|
:align "center" -- Notice that the elements are centered vertically in the column.
|
|
|
|
imfs.box(0, 0, 2, counter() *0.25, "#8f8")
|
|
imfs.box(0, 0, 2, 2, "#88f")
|
|
|
|
imfs.column_end()
|
|
|
|
imfs.scroll_container_end()
|
|
|
|
return imfs.end_()
|
|
end
|
|
|
|
minetest.register_chatcommand("demo", {
|
|
func = function(name)
|
|
imfs.show(name, fs)
|
|
end
|
|
})
|
|
|