mirror of
https://github.com/luanti-org/luanti.git
synced 2025-06-27 16:36:03 +00:00
Create Lua frontend UI code
This commit is contained in:
parent
abfb319e9b
commit
9cc73f16f0
13 changed files with 1646 additions and 1 deletions
10
.luacheckrc
10
.luacheckrc
|
@ -22,7 +22,15 @@ read_globals = {
|
||||||
"ValueNoise", "ValueNoiseMap",
|
"ValueNoise", "ValueNoiseMap",
|
||||||
|
|
||||||
string = {fields = {"split", "trim"}},
|
string = {fields = {"split", "trim"}},
|
||||||
table = {fields = {"copy", "copy_with_metatables", "getn", "indexof", "keyof", "insert_all"}},
|
table = {fields = {
|
||||||
|
"copy",
|
||||||
|
"copy_with_metatables",
|
||||||
|
"getn",
|
||||||
|
"indexof",
|
||||||
|
"keyof",
|
||||||
|
"insert_all",
|
||||||
|
"merge",
|
||||||
|
}},
|
||||||
math = {fields = {"hypot", "round"}},
|
math = {fields = {"hypot", "round"}},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -579,6 +579,15 @@ function table.insert_all(t, other)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function table.merge(...)
|
||||||
|
local new = {}
|
||||||
|
for _, t in ipairs{...} do
|
||||||
|
table.insert_all(new, t)
|
||||||
|
end
|
||||||
|
return new
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
function table.key_value_swap(t)
|
function table.key_value_swap(t)
|
||||||
local ti = {}
|
local ti = {}
|
||||||
for k,v in pairs(t) do
|
for k,v in pairs(t) do
|
||||||
|
@ -872,3 +881,47 @@ function core.parse_coordinates(x, y, z, relative_to)
|
||||||
local rz = core.parse_relative_number(z, relative_to.z)
|
local rz = core.parse_relative_number(z, relative_to.z)
|
||||||
return rx and ry and rz and { x = rx, y = ry, z = rz }
|
return rx and ry and rz and { x = rx, y = ry, z = rz }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function class_try_return(obj, ...)
|
||||||
|
if select("#", ...) ~= 0 then
|
||||||
|
return ...
|
||||||
|
end
|
||||||
|
return obj
|
||||||
|
end
|
||||||
|
|
||||||
|
local function class_call(class, ...)
|
||||||
|
local obj = setmetatable({}, class)
|
||||||
|
|
||||||
|
if obj.new then
|
||||||
|
return class_try_return(obj, obj:new(...))
|
||||||
|
end
|
||||||
|
|
||||||
|
return obj
|
||||||
|
end
|
||||||
|
|
||||||
|
function core.class(super)
|
||||||
|
local class = setmetatable({}, {__call = class_call, __index = super})
|
||||||
|
class.__index = class
|
||||||
|
|
||||||
|
return class
|
||||||
|
end
|
||||||
|
|
||||||
|
function core.super(class)
|
||||||
|
local meta = getmetatable(class)
|
||||||
|
return meta and meta.__index
|
||||||
|
end
|
||||||
|
|
||||||
|
function core.is_subclass(class, super)
|
||||||
|
while class ~= nil do
|
||||||
|
if class == super then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
class = core.super(class)
|
||||||
|
end
|
||||||
|
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function core.is_instance(obj, class)
|
||||||
|
return type(obj) == "table" and core.is_subclass(getmetatable(obj), class)
|
||||||
|
end
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
local scriptpath = core.get_builtin_path()
|
local scriptpath = core.get_builtin_path()
|
||||||
local commonpath = scriptpath .. "common" .. DIR_DELIM
|
local commonpath = scriptpath .. "common" .. DIR_DELIM
|
||||||
local gamepath = scriptpath .. "game".. DIR_DELIM
|
local gamepath = scriptpath .. "game".. DIR_DELIM
|
||||||
|
local uipath = scriptpath .. "ui" .. DIR_DELIM
|
||||||
|
|
||||||
-- Shared between builtin files, but
|
-- Shared between builtin files, but
|
||||||
-- not exposed to outer context
|
-- not exposed to outer context
|
||||||
|
@ -39,6 +40,7 @@ dofile(gamepath .. "hud.lua")
|
||||||
dofile(gamepath .. "knockback.lua")
|
dofile(gamepath .. "knockback.lua")
|
||||||
dofile(gamepath .. "async.lua")
|
dofile(gamepath .. "async.lua")
|
||||||
dofile(gamepath .. "death_screen.lua")
|
dofile(gamepath .. "death_screen.lua")
|
||||||
|
dofile(uipath .. "init.lua")
|
||||||
|
|
||||||
core.after(0, builtin_shared.cache_content_ids)
|
core.after(0, builtin_shared.cache_content_ids)
|
||||||
|
|
||||||
|
|
143
builtin/ui/context.lua
Normal file
143
builtin/ui/context.lua
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
-- Luanti
|
||||||
|
-- SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
-- Copyright (C) 2025 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||||
|
|
||||||
|
ui.Context = core.class()
|
||||||
|
|
||||||
|
local open_contexts = {}
|
||||||
|
|
||||||
|
local OPEN_WINDOW = 0x00
|
||||||
|
local REOPEN_WINDOW = 0x01
|
||||||
|
local UPDATE_WINDOW = 0x02
|
||||||
|
local CLOSE_WINDOW = 0x03
|
||||||
|
|
||||||
|
function ui.Context:new(builder, player, state)
|
||||||
|
self._builder = ui._req(builder, "function")
|
||||||
|
self._player = ui._req(player, "string")
|
||||||
|
self._state = ui._opt(state, "table", {})
|
||||||
|
|
||||||
|
self._id = nil
|
||||||
|
self._window = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function ui.Context:open(param)
|
||||||
|
if self:is_open() then
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
self:_open_window()
|
||||||
|
self:_build_window(ui._opt(param, "table", {}))
|
||||||
|
|
||||||
|
local data = ui._encode("BL Z", OPEN_WINDOW, self._id,
|
||||||
|
self._window:_encode(self._player, true))
|
||||||
|
|
||||||
|
core.send_ui_message(self._player, data)
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function ui.Context:reopen(param)
|
||||||
|
if not self:is_open() then
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
local close_id = self:_close_window()
|
||||||
|
self:_open_window()
|
||||||
|
self:_build_window(ui._opt(param, "table", {}))
|
||||||
|
|
||||||
|
local data = ui._encode("BLL Z", REOPEN_WINDOW, self._id, close_id,
|
||||||
|
self._window:_encode(self._player, true))
|
||||||
|
|
||||||
|
core.send_ui_message(self._player, data)
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function ui.Context:update(param)
|
||||||
|
if not self:is_open() then
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
self:_build_window(ui._opt(param, "table", {}))
|
||||||
|
|
||||||
|
local data = ui._encode("BL Z", UPDATE_WINDOW, self._id,
|
||||||
|
self._window:_encode(self._player, false))
|
||||||
|
|
||||||
|
core.send_ui_message(self._player, data)
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function ui.Context:close()
|
||||||
|
if not self:is_open() then
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
local close_id = self:_close_window()
|
||||||
|
local data = ui._encode("BL", CLOSE_WINDOW, close_id)
|
||||||
|
|
||||||
|
core.send_ui_message(self._player, data)
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function ui.Context:is_open()
|
||||||
|
return self._id ~= nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function ui.Context:get_builder()
|
||||||
|
return self._builder
|
||||||
|
end
|
||||||
|
|
||||||
|
function ui.Context:get_player()
|
||||||
|
return self._player
|
||||||
|
end
|
||||||
|
|
||||||
|
function ui.Context:get_state()
|
||||||
|
return self._state
|
||||||
|
end
|
||||||
|
|
||||||
|
function ui.Context:set_state(state)
|
||||||
|
self._state = ui._req(state, "table")
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
local last_id = 0
|
||||||
|
|
||||||
|
function ui.Context:_open_window()
|
||||||
|
self._id = last_id
|
||||||
|
last_id = last_id + 1
|
||||||
|
|
||||||
|
open_contexts[self._id] = self
|
||||||
|
end
|
||||||
|
|
||||||
|
function ui.Context:_build_window(param)
|
||||||
|
self._window = self._builder(self, self._player, self._state, param)
|
||||||
|
|
||||||
|
ui._req(self._window, ui.Window)
|
||||||
|
assert(not self._window._context, "Window object has already been used")
|
||||||
|
|
||||||
|
self._window._context = self
|
||||||
|
end
|
||||||
|
|
||||||
|
function ui.Context:_close_window()
|
||||||
|
local close_id = self._id
|
||||||
|
|
||||||
|
self._id = nil
|
||||||
|
self._window = nil
|
||||||
|
|
||||||
|
open_contexts[close_id] = nil
|
||||||
|
return close_id
|
||||||
|
end
|
||||||
|
|
||||||
|
function ui.get_open_contexts()
|
||||||
|
local contexts = {}
|
||||||
|
for _, context in pairs(open_contexts) do
|
||||||
|
table.insert(contexts, context)
|
||||||
|
end
|
||||||
|
return contexts
|
||||||
|
end
|
||||||
|
|
||||||
|
core.register_on_leaveplayer(function(player)
|
||||||
|
for _, context in pairs(open_contexts) do
|
||||||
|
if context:get_player() == player:get_player_name() then
|
||||||
|
context:_close_window()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
140
builtin/ui/elem.lua
Normal file
140
builtin/ui/elem.lua
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
-- Luanti
|
||||||
|
-- SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
-- Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||||
|
|
||||||
|
ui._elem_types = {}
|
||||||
|
|
||||||
|
function ui._new_type(base, type, type_id, id_required)
|
||||||
|
local class = core.class(base)
|
||||||
|
|
||||||
|
class._type = type
|
||||||
|
class._type_id = type_id
|
||||||
|
class._id_required = id_required
|
||||||
|
|
||||||
|
ui._elem_types[type] = class
|
||||||
|
|
||||||
|
return class
|
||||||
|
end
|
||||||
|
|
||||||
|
function ui.derive_elem(base, type)
|
||||||
|
assert(core.is_subclass(base, ui.Elem))
|
||||||
|
ui._req(type, "id")
|
||||||
|
|
||||||
|
assert(not ui._elem_types[type],
|
||||||
|
"Derived element name already used: '" .. type .. "'")
|
||||||
|
|
||||||
|
return ui._new_type(base, type, base._type_id, base._id_required)
|
||||||
|
end
|
||||||
|
|
||||||
|
ui.Elem = ui._new_type(nil, "elem", 0x00, false)
|
||||||
|
|
||||||
|
function ui.Elem:new(param)
|
||||||
|
local function make_elem(props)
|
||||||
|
self:_init(ui._req(props, "table"))
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
if type(param) == "string" then
|
||||||
|
self._id = ui._req(param, "id")
|
||||||
|
return make_elem
|
||||||
|
end
|
||||||
|
|
||||||
|
self._id = ui.new_id()
|
||||||
|
return make_elem(param)
|
||||||
|
end
|
||||||
|
|
||||||
|
function ui.Elem:_init(props)
|
||||||
|
self._groups = {}
|
||||||
|
self._children = {}
|
||||||
|
|
||||||
|
self._props = ui._cascade_props(props.props or props, {})
|
||||||
|
self._styles = {}
|
||||||
|
|
||||||
|
-- Set by parent ui.Elem
|
||||||
|
self._parent = nil
|
||||||
|
|
||||||
|
-- Set by ui.Window
|
||||||
|
self._boxes = {main = true}
|
||||||
|
self._window = nil
|
||||||
|
|
||||||
|
if self._id_required then
|
||||||
|
assert(not ui._is_reserved_id(self._id),
|
||||||
|
"Element ID is required for '" .. self._type .. "'")
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, group in ipairs(ui._opt_array(props.groups, "id", {})) do
|
||||||
|
self._groups[group] = true
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, child in ipairs(ui._opt_array(props.children, ui.Elem, props)) do
|
||||||
|
if core.is_instance(child, ui.Elem) then
|
||||||
|
assert(child._parent == nil,
|
||||||
|
"Element '" .. child._id .. "' already has a parent")
|
||||||
|
assert(not core.is_instance(child, ui.Root),
|
||||||
|
"ui.Root may not be a child element")
|
||||||
|
|
||||||
|
child._parent = self
|
||||||
|
table.insert(self._children, child)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, style in ipairs(ui._opt_array(props.styles, ui.Style, props)) do
|
||||||
|
if core.is_instance(style, ui.Style) then
|
||||||
|
table.insert(self._styles, style)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, item in ipairs(props) do
|
||||||
|
assert(core.is_instance(item, ui.Elem) or core.is_instance(item, ui.Style))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function ui.Elem:_get_flat()
|
||||||
|
local elems = {self}
|
||||||
|
for _, child in ipairs(self._children) do
|
||||||
|
table.insert_all(elems, child:_get_flat())
|
||||||
|
end
|
||||||
|
return elems
|
||||||
|
end
|
||||||
|
|
||||||
|
function ui.Elem:_encode()
|
||||||
|
return ui._encode("Bz S", self._type_id, self._id, self:_encode_fields())
|
||||||
|
end
|
||||||
|
|
||||||
|
function ui.Elem:_encode_fields()
|
||||||
|
local fl = ui._make_flags()
|
||||||
|
|
||||||
|
if ui._shift_flag(fl, #self._children > 0) then
|
||||||
|
local child_ids = {}
|
||||||
|
for i, child in ipairs(self._children) do
|
||||||
|
child_ids[i] = child._id
|
||||||
|
end
|
||||||
|
|
||||||
|
ui._encode_flag(fl, "Z", ui._encode_array("z", child_ids))
|
||||||
|
end
|
||||||
|
|
||||||
|
self:_encode_box(fl, self._boxes.main)
|
||||||
|
|
||||||
|
return ui._encode_flags(fl)
|
||||||
|
end
|
||||||
|
|
||||||
|
function ui.Elem:_encode_box(fl, box)
|
||||||
|
-- Element encoding always happens after styles are computed and boxes are
|
||||||
|
-- populated with style indices. So, if this box has any styles applied to
|
||||||
|
-- it, encode the relevant states.
|
||||||
|
if not ui._shift_flag(fl, box.n > 0) then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local box_fl = ui._make_flags()
|
||||||
|
|
||||||
|
-- For each state, check if there is any styling. If there is, add it
|
||||||
|
-- to the box's flags.
|
||||||
|
for i = ui._STATE_NONE, ui._NUM_STATES - 1 do
|
||||||
|
if ui._shift_flag(box_fl, box[i] ~= ui._NO_STYLE) then
|
||||||
|
ui._encode_flag(box_fl, "I", box[i])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
ui._encode_flag(fl, "s", ui._encode_flags(box_fl))
|
||||||
|
end
|
18
builtin/ui/init.lua
Normal file
18
builtin/ui/init.lua
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
-- Luanti
|
||||||
|
-- SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
-- Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||||
|
|
||||||
|
ui = {}
|
||||||
|
|
||||||
|
local UI_PATH = core.get_builtin_path() .. "ui" .. DIR_DELIM
|
||||||
|
|
||||||
|
dofile(UI_PATH .. "util.lua")
|
||||||
|
dofile(UI_PATH .. "selector.lua")
|
||||||
|
dofile(UI_PATH .. "style.lua")
|
||||||
|
dofile(UI_PATH .. "elem.lua")
|
||||||
|
|
||||||
|
dofile(UI_PATH .. "static_elems.lua")
|
||||||
|
|
||||||
|
dofile(UI_PATH .. "window.lua")
|
||||||
|
dofile(UI_PATH .. "context.lua")
|
||||||
|
dofile(UI_PATH .. "theme.lua")
|
569
builtin/ui/selector.lua
Normal file
569
builtin/ui/selector.lua
Normal file
|
@ -0,0 +1,569 @@
|
||||||
|
-- Luanti
|
||||||
|
-- SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
-- Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||||
|
|
||||||
|
ui._STATE_NONE = 0
|
||||||
|
ui._NUM_STATES = bit.lshift(1, 5)
|
||||||
|
ui._NO_STYLE = -1
|
||||||
|
|
||||||
|
local states_by_name = {
|
||||||
|
focused = bit.lshift(1, 0),
|
||||||
|
selected = bit.lshift(1, 1),
|
||||||
|
hovered = bit.lshift(1, 2),
|
||||||
|
pressed = bit.lshift(1, 3),
|
||||||
|
disabled = bit.lshift(1, 4),
|
||||||
|
}
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Selector parsing functions return a function. When called with an element as
|
||||||
|
the solitary parameter, this function will return a boolean, indicating whether
|
||||||
|
the element is matched by the selector. If the boolean is true, a table of
|
||||||
|
tables {box=..., states=...} is also returned. If false, this is nil.
|
||||||
|
|
||||||
|
The keys of this table are unique hashes of the box, which serve to prevent
|
||||||
|
duplicate box/state combos from being generated. The values contain all the
|
||||||
|
combinations of boxes and states that the selector specifies. The box name may
|
||||||
|
be nil if the selector specified no box, in which case it will default to
|
||||||
|
"main" unless/until it is later intersected with a box selector. This list may
|
||||||
|
also be empty, which means that contradictory boxes were specified and no box
|
||||||
|
should be styled. The list will not contain duplicates.
|
||||||
|
--]]
|
||||||
|
|
||||||
|
-- By default, most selectors leave the box unspecified and don't select any
|
||||||
|
-- particular state, leaving the state at zero.
|
||||||
|
local function make_box(name, states)
|
||||||
|
return {name = name, states = states or ui._STATE_NONE}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Hash the box to a string that represents that combination of box and states
|
||||||
|
-- uniquely to prevent duplicates in box tables.
|
||||||
|
local function hash_box(box)
|
||||||
|
return (box.name or "") .. "$" .. tostring(box.states)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function make_hashed(name, states)
|
||||||
|
local box = make_box(name, states)
|
||||||
|
return {[hash_box(box)] = box}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function result(matches, name, states)
|
||||||
|
if matches then
|
||||||
|
return true, make_hashed(name, states)
|
||||||
|
end
|
||||||
|
return false, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
ui._universal_sel = function()
|
||||||
|
return result(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
local simple_preds = {}
|
||||||
|
local func_preds = {}
|
||||||
|
|
||||||
|
simple_preds["no_children"] = function(elem)
|
||||||
|
return result(#elem._children == 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
simple_preds["first_child"] = function(elem)
|
||||||
|
return result(elem._parent == nil or elem._parent._children[1] == elem)
|
||||||
|
end
|
||||||
|
|
||||||
|
simple_preds["last_child"] = function(elem)
|
||||||
|
return result(elem._parent == nil or
|
||||||
|
elem._parent._children[#elem._parent._children] == elem)
|
||||||
|
end
|
||||||
|
|
||||||
|
simple_preds["only_child"] = function(elem)
|
||||||
|
return result(elem._parent == nil or #elem._parent._children == 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
func_preds["<"] = function(str)
|
||||||
|
local sel = ui._parse_sel(str, true, false)
|
||||||
|
|
||||||
|
return function(elem)
|
||||||
|
return result(elem._parent and sel(elem._parent))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
func_preds[">"] = function(str)
|
||||||
|
local sel = ui._parse_sel(str, true, false)
|
||||||
|
|
||||||
|
return function(elem)
|
||||||
|
for _, child in ipairs(elem._children) do
|
||||||
|
if sel(child) then
|
||||||
|
return result(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return result(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
func_preds["<<"] = function(str)
|
||||||
|
local sel = ui._parse_sel(str, true, false)
|
||||||
|
|
||||||
|
return function(elem)
|
||||||
|
local ancestor = elem._parent
|
||||||
|
|
||||||
|
while ancestor ~= nil do
|
||||||
|
if sel(ancestor) then
|
||||||
|
return result(true)
|
||||||
|
end
|
||||||
|
ancestor = ancestor._parent
|
||||||
|
end
|
||||||
|
|
||||||
|
return result(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
func_preds[">>"] = function(str)
|
||||||
|
local sel = ui._parse_sel(str, true, false)
|
||||||
|
|
||||||
|
return function(elem)
|
||||||
|
for _, descendant in ipairs(elem:_get_flat()) do
|
||||||
|
if descendant ~= elem and sel(descendant) then
|
||||||
|
return result(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return result(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
func_preds["<>"] = function(str)
|
||||||
|
local sel = ui._parse_sel(str, true, false)
|
||||||
|
|
||||||
|
return function(elem)
|
||||||
|
if not elem._parent then
|
||||||
|
return result(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, sibling in ipairs(elem._parent._children) do
|
||||||
|
if sibling ~= elem and sel(sibling) then
|
||||||
|
return result(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return result(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
func_preds["nth_child"] = function(str)
|
||||||
|
local index = tonumber(str)
|
||||||
|
assert(index, "Expected number for ?nth_child()")
|
||||||
|
|
||||||
|
return function(elem)
|
||||||
|
if not elem._parent then
|
||||||
|
return result(index == 1)
|
||||||
|
end
|
||||||
|
return result(elem._parent._children[index] == elem)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
func_preds["nth_last_child"] = function(str)
|
||||||
|
local rindex = tonumber(str)
|
||||||
|
assert(rindex, "Expected number for ?nth_last_child()")
|
||||||
|
|
||||||
|
return function(elem)
|
||||||
|
if not elem._parent then
|
||||||
|
return result(rindex == 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
local index = #elem._parent._children - rindex + 1
|
||||||
|
return result(elem._parent._children[index] == elem)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function is_nth_match(elem, sel, index, dir)
|
||||||
|
if not elem._parent then
|
||||||
|
return index == 1 and sel(elem)
|
||||||
|
end
|
||||||
|
|
||||||
|
local first, last
|
||||||
|
if dir == 1 then
|
||||||
|
first = 1
|
||||||
|
last = #elem._parent._children
|
||||||
|
else
|
||||||
|
first = #elem._parent._children
|
||||||
|
last = 1
|
||||||
|
end
|
||||||
|
|
||||||
|
local count = 0
|
||||||
|
for i = first, last, dir do
|
||||||
|
local sibling = elem._parent._children[i]
|
||||||
|
|
||||||
|
if sel(sibling) then
|
||||||
|
count = count + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
if count == index then
|
||||||
|
return sibling == elem
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
func_preds["first_match"] = function(str)
|
||||||
|
local sel = ui._parse_sel(str, true, false)
|
||||||
|
|
||||||
|
return function(elem)
|
||||||
|
return is_nth_match(elem, sel, 1, 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
func_preds["last_match"] = function(str)
|
||||||
|
local sel = ui._parse_sel(str, true, false)
|
||||||
|
|
||||||
|
return function(elem)
|
||||||
|
return is_nth_match(elem, sel, 1, -1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
func_preds["only_match"] = function(str)
|
||||||
|
local sel = ui._parse_sel(str, true, false)
|
||||||
|
|
||||||
|
return function(elem)
|
||||||
|
return is_nth_match(elem, sel, 1, 1) and is_nth_match(elem, sel, 1, -1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
func_preds["nth_match"] = function(str)
|
||||||
|
local sel, rest = ui._parse_sel(str, true, true)
|
||||||
|
local index = tonumber(rest)
|
||||||
|
assert(index, "Expected number after ';' for ?nth_match()")
|
||||||
|
|
||||||
|
return function(elem)
|
||||||
|
return is_nth_match(elem, sel, index, 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
func_preds["nth_last_match"] = function(str)
|
||||||
|
local sel, rest = ui._parse_sel(str, true, true)
|
||||||
|
local rindex = tonumber(rest)
|
||||||
|
assert(rindex, "Expected number after ';' for ?nth_last_match()")
|
||||||
|
|
||||||
|
return function(elem)
|
||||||
|
return is_nth_match(elem, sel, rindex, -1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function parse_term(str, pred)
|
||||||
|
str = str:trim()
|
||||||
|
assert(str ~= "", "Expected selector term")
|
||||||
|
|
||||||
|
-- We need to test the first character to see what sort of term we're
|
||||||
|
-- dealing with, and then usually parse from the rest of the string.
|
||||||
|
local prefix = str:sub(1, 1)
|
||||||
|
local suffix = str:sub(2)
|
||||||
|
|
||||||
|
if prefix == "*" then
|
||||||
|
-- Universal terms match everything and have no extra stuff to parse.
|
||||||
|
return ui._universal_sel, suffix, nil
|
||||||
|
|
||||||
|
elseif prefix == "#" then
|
||||||
|
-- Most selectors are similar to the ID selector, in that characters
|
||||||
|
-- for the ID string are parsed, and all the characters directly
|
||||||
|
-- afterwards are returned as the rest of the string after the term.
|
||||||
|
local id, rest = suffix:match("^([" .. ui._ID_CHARS .. "]+)(.*)$")
|
||||||
|
assert(id, "Expected ID after '#'")
|
||||||
|
|
||||||
|
return function(elem)
|
||||||
|
return result(elem._id == id)
|
||||||
|
end, rest, nil
|
||||||
|
|
||||||
|
elseif prefix == "%" then
|
||||||
|
local group, rest = suffix:match("^([" .. ui._ID_CHARS .. "]+)(.*)$")
|
||||||
|
assert(group, "Expected group after '%'")
|
||||||
|
|
||||||
|
return function(elem)
|
||||||
|
return result(elem._groups[group] ~= nil)
|
||||||
|
end, rest, nil
|
||||||
|
|
||||||
|
elseif prefix == "@" then
|
||||||
|
-- It's possible to check if a box exists in a predicate, but that
|
||||||
|
-- leads to different behaviors inside and outside of predicates. For
|
||||||
|
-- instance, @main@thumb effectively matches nothing by returning an
|
||||||
|
-- empty table of boxes, but would return true for scrollbars if used
|
||||||
|
-- in a predicate. So, prevent box selectors in predicates entirely.
|
||||||
|
assert(not pred, "Box selectors are invalid in predicate selectors")
|
||||||
|
|
||||||
|
-- First, check if this can be parsed as a universal box selector.
|
||||||
|
local name = suffix:sub(1, 1)
|
||||||
|
local rest
|
||||||
|
|
||||||
|
if name == "*" then
|
||||||
|
rest = suffix:sub(2)
|
||||||
|
|
||||||
|
return function(elem)
|
||||||
|
-- If we want all boxes, iterate over the boxes in the element
|
||||||
|
-- and add each of them to the full list of boxes.
|
||||||
|
local boxes = {}
|
||||||
|
|
||||||
|
for name in pairs(elem._boxes) do
|
||||||
|
local box = make_box(name, ui._STATE_NONE)
|
||||||
|
boxes[hash_box(box)] = box
|
||||||
|
end
|
||||||
|
|
||||||
|
return true, boxes
|
||||||
|
end, rest, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Otherwise, parse it as a normal box selector instead.
|
||||||
|
name, rest = suffix:match("^([" .. ui._ID_CHARS .. "]+)(.*)$")
|
||||||
|
assert(name, "Expected box or '*' after '@'")
|
||||||
|
|
||||||
|
return function(elem)
|
||||||
|
-- If the box is in the element, return it. Otherwise, the
|
||||||
|
-- selector doesn't match.
|
||||||
|
if elem._boxes[name] then
|
||||||
|
return result(true, name, ui._STATE_NONE)
|
||||||
|
end
|
||||||
|
return result(false)
|
||||||
|
end, rest, nil
|
||||||
|
|
||||||
|
elseif prefix == "$" then
|
||||||
|
-- Unfortunately, we can't detect the state of boxes from the server,
|
||||||
|
-- so we can't use them in predicates.
|
||||||
|
assert(not pred, "State selectors are invalid in predicate selectors")
|
||||||
|
|
||||||
|
local name, rest = suffix:match("^([" .. ui._ID_CHARS .. "]+)(.*)$")
|
||||||
|
assert(name, "Expected state after '$'")
|
||||||
|
|
||||||
|
local state = states_by_name[name]
|
||||||
|
assert(state, "Invalid state: '" .. name .. "'")
|
||||||
|
|
||||||
|
return function(elem)
|
||||||
|
-- States unconditionally match every element. Specify the state
|
||||||
|
-- that this term indicates but leave the box undefined.
|
||||||
|
return result(true, nil, state)
|
||||||
|
end, rest, nil
|
||||||
|
|
||||||
|
elseif prefix == "/" then
|
||||||
|
local type, rest = suffix:match("^([" .. ui._ID_CHARS .. "]+)%/(.*)$")
|
||||||
|
assert(type, "Expected window type after '/'")
|
||||||
|
|
||||||
|
assert(ui._window_types[type], "Invalid window type: '" .. type .. "'")
|
||||||
|
|
||||||
|
return function(elem)
|
||||||
|
return result(elem._window._type == type)
|
||||||
|
end, rest, nil
|
||||||
|
|
||||||
|
elseif prefix == "," or prefix == ";" then
|
||||||
|
-- Return nil instead of a function and return the prefix character to
|
||||||
|
-- instruct ui._parse_sel() to union or end the selector accordingly.
|
||||||
|
return nil, suffix, prefix
|
||||||
|
|
||||||
|
elseif prefix == "(" then
|
||||||
|
-- Parse a matching set of parentheses, and recursively pass the
|
||||||
|
-- contents into ui._parse_sel().
|
||||||
|
local sub, rest = str:match("^(%b())(.*)$")
|
||||||
|
assert(sub, "Unmatched ')' for '('")
|
||||||
|
|
||||||
|
return ui._parse_sel(sub:sub(2, -2), pred, false), rest, nil
|
||||||
|
|
||||||
|
elseif prefix == "!" then
|
||||||
|
-- Parse a single predicate term (NOT an entire predicate selector) and
|
||||||
|
-- ensure that it's a valid selector term, not a comma or semicolon.
|
||||||
|
local term, rest, _ = parse_term(suffix, true)
|
||||||
|
assert(term, "Expected selector term after '!'")
|
||||||
|
|
||||||
|
return function(elem)
|
||||||
|
return result(not term(elem))
|
||||||
|
end, rest, nil
|
||||||
|
|
||||||
|
elseif prefix == "?" then
|
||||||
|
-- Predicates may have different syntax depending on the name of the
|
||||||
|
-- predicate, so just parse the name initially.
|
||||||
|
local name, after = suffix:match("^([" .. ui._ID_CHARS .. "%<%>]+)(.*)$")
|
||||||
|
assert(name, "Expected predicate after '?'")
|
||||||
|
|
||||||
|
-- If this is a simple predicate, return its predicate function without
|
||||||
|
-- doing any further parsing.
|
||||||
|
local func = simple_preds[name]
|
||||||
|
if func then
|
||||||
|
return func, after, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- If this is a function predicate, we need to do more parsing.
|
||||||
|
func = func_preds[name]
|
||||||
|
if func then
|
||||||
|
-- Parse a matching pair of parentheses and get the trimmed
|
||||||
|
-- contents between them.
|
||||||
|
assert(after:sub(1, 1) == "(", "Expected '(' after '?" .. name .. "'")
|
||||||
|
|
||||||
|
local sub, rest = after:match("^(%b())(.*)$")
|
||||||
|
assert(sub, "Unmatched ')' for '?" .. name .. "('")
|
||||||
|
|
||||||
|
local contents = sub:sub(2, -2):trim()
|
||||||
|
return func(contents), rest, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Otherwise, there is no predicate by this name.
|
||||||
|
error("Invalid predicate: '?" .. name .. "'")
|
||||||
|
|
||||||
|
else
|
||||||
|
-- If we found no special character, it's either a type or it indicates
|
||||||
|
-- invalid characters in the selector string.
|
||||||
|
local type, rest = str:match("^([" .. ui._ID_CHARS .. "]+)(.*)$")
|
||||||
|
assert(type, "Unexpected character '" .. prefix .. "' in selector")
|
||||||
|
|
||||||
|
assert(ui._elem_types[type], "Invalid element type: '" .. type .. "'")
|
||||||
|
|
||||||
|
return function(elem)
|
||||||
|
return result(elem._type == type)
|
||||||
|
end, rest, nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function intersect_boxes(a_boxes, b_boxes)
|
||||||
|
local new_boxes = {}
|
||||||
|
|
||||||
|
for _, box_a in pairs(a_boxes) do
|
||||||
|
for _, box_b in pairs(b_boxes) do
|
||||||
|
-- Two boxes can only be merged if they're the same box or if one
|
||||||
|
-- or both selectors hasn't specified a box yet.
|
||||||
|
if box_a.name == nil or box_b.name == nil or box_a.name == box_b.name then
|
||||||
|
-- Create the new box by taking the specified box (if there is
|
||||||
|
-- one) and ORing the states together (making them more refer
|
||||||
|
-- to a more specific state).
|
||||||
|
local new_box = make_box(
|
||||||
|
box_a.name or box_b.name,
|
||||||
|
bit.bor(box_a.states, box_b.states)
|
||||||
|
)
|
||||||
|
|
||||||
|
-- Hash this box and add it into the table. This will be
|
||||||
|
-- effectively a no-op if there's already an identical box
|
||||||
|
-- hashed in the table.
|
||||||
|
new_boxes[hash_box(new_box)] = new_box
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return new_boxes
|
||||||
|
end
|
||||||
|
|
||||||
|
function ui._intersect_sels(sels)
|
||||||
|
return function(elem)
|
||||||
|
-- We start with the default box, and intersect the box and states from
|
||||||
|
-- every selector with it.
|
||||||
|
local all_boxes = make_hashed()
|
||||||
|
|
||||||
|
-- Loop through all of the selectors. All of them need to match for the
|
||||||
|
-- intersected selector to match.
|
||||||
|
for _, sel in ipairs(sels) do
|
||||||
|
local matches, boxes = sel(elem)
|
||||||
|
if not matches then
|
||||||
|
-- This selector doesn't match, so fail immediately.
|
||||||
|
return false, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Since the selector matched, intersect the boxes and states with
|
||||||
|
-- those of the other selectors. If two selectors both match an
|
||||||
|
-- element but specify different boxes, then this selector will
|
||||||
|
-- return true, but the boxes will be cancelled out in the
|
||||||
|
-- intersection, leaving an empty list of boxes.
|
||||||
|
if boxes then
|
||||||
|
all_boxes = intersect_boxes(all_boxes, boxes)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return true, all_boxes
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function ui._union_sels(sels)
|
||||||
|
return function(elem)
|
||||||
|
-- We initially have no boxes, and have to add them in as matching
|
||||||
|
-- selectors are unioned in.
|
||||||
|
local all_boxes = {}
|
||||||
|
local found_match = false
|
||||||
|
|
||||||
|
-- Loop through all of the selectors. If any of them match, this entire
|
||||||
|
-- unioned selector matches.
|
||||||
|
for _, sel in ipairs(sels) do
|
||||||
|
local matches, boxes = sel(elem)
|
||||||
|
|
||||||
|
if matches then
|
||||||
|
-- We found a match. However, we can't return true just yet
|
||||||
|
-- because we need to union the boxes and states from every
|
||||||
|
-- selector, not just this one.
|
||||||
|
found_match = true
|
||||||
|
|
||||||
|
if boxes then
|
||||||
|
-- Add the boxes from this selector into the table of all
|
||||||
|
-- the boxes. The hashing of boxes will automatically weed
|
||||||
|
-- out any duplicates.
|
||||||
|
for hash, box in pairs(boxes) do
|
||||||
|
all_boxes[hash] = box
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if found_match then
|
||||||
|
return true, all_boxes
|
||||||
|
end
|
||||||
|
return false, nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function ui._parse_sel(str, pred, partial)
|
||||||
|
str = str:trim()
|
||||||
|
assert(str ~= "", "Empty style selector")
|
||||||
|
|
||||||
|
local sub_sels = {}
|
||||||
|
local terms = {}
|
||||||
|
local done = false
|
||||||
|
|
||||||
|
-- Loop until we've read every term from the input string.
|
||||||
|
while not done do
|
||||||
|
-- Parse the next term from the input string.
|
||||||
|
local term, prefix
|
||||||
|
term, str, prefix = parse_term(str, pred)
|
||||||
|
|
||||||
|
-- If we read a term, insert this term into the list of terms for the
|
||||||
|
-- current sub-selector.
|
||||||
|
if term then
|
||||||
|
table.insert(terms, term)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Make sure that we have at least one selector term before each comma
|
||||||
|
-- or semicolon that we read.
|
||||||
|
if prefix then
|
||||||
|
assert(#terms > 0, "Expected selector term before '" .. prefix .. "'")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- If we read a comma or semicolon or have run out of terms, we need to
|
||||||
|
-- commit the terms we've read so far.
|
||||||
|
if prefix or str == "" then
|
||||||
|
-- If there's only one term, commit it directly. Otherwise,
|
||||||
|
-- intersect all the terms together.
|
||||||
|
if #terms == 1 then
|
||||||
|
table.insert(sub_sels, terms[1])
|
||||||
|
else
|
||||||
|
table.insert(sub_sels, ui._intersect_sels(terms))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Clear out the list of terms for the next sub-selector.
|
||||||
|
terms = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- If we read a semicolon or have run out of terms, we're done parsing.
|
||||||
|
-- We check for the semicolon case first since it is possible for the
|
||||||
|
-- string to be empty after reading the semicolon.
|
||||||
|
if prefix == ";" then
|
||||||
|
assert(partial, "Unexpected character ';' in selector")
|
||||||
|
done = true
|
||||||
|
elseif str == "" then
|
||||||
|
assert(prefix ~= ",", "Expected selector term after ','")
|
||||||
|
assert(not partial, "Expected ';' after end of selector")
|
||||||
|
done = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Now that we've read all the sub-selectors between the commas, we need to
|
||||||
|
-- commit them. We only need to union the terms if there's more than one.
|
||||||
|
if #sub_sels == 1 then
|
||||||
|
return sub_sels[1], str:trim()
|
||||||
|
end
|
||||||
|
return ui._union_sels(sub_sels), str:trim()
|
||||||
|
end
|
23
builtin/ui/static_elems.lua
Normal file
23
builtin/ui/static_elems.lua
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
-- Luanti
|
||||||
|
-- SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
-- Copyright (C) 2024 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||||
|
|
||||||
|
ui.Group = ui.derive_elem(ui.Elem, "group")
|
||||||
|
ui.Label = ui.derive_elem(ui.Elem, "label")
|
||||||
|
ui.Image = ui.derive_elem(ui.Elem, "image")
|
||||||
|
|
||||||
|
ui.Root = ui._new_type(ui.Elem, "root", 0x01, false)
|
||||||
|
|
||||||
|
function ui.Root:_init(props)
|
||||||
|
ui.Elem._init(self, props)
|
||||||
|
|
||||||
|
self._boxes.backdrop = true
|
||||||
|
end
|
||||||
|
|
||||||
|
function ui.Root:_encode_fields()
|
||||||
|
local fl = ui._make_flags()
|
||||||
|
|
||||||
|
self:_encode_box(fl, self._boxes.backdrop)
|
||||||
|
|
||||||
|
return ui._encode("SZ", ui.Elem._encode_fields(self), ui._encode_flags(fl))
|
||||||
|
end
|
281
builtin/ui/style.lua
Normal file
281
builtin/ui/style.lua
Normal file
|
@ -0,0 +1,281 @@
|
||||||
|
-- Luanti
|
||||||
|
-- SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
-- Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||||
|
|
||||||
|
ui.Style = core.class()
|
||||||
|
|
||||||
|
function ui.Style:new(param)
|
||||||
|
local function make_style(props)
|
||||||
|
self:_init(ui._req(props, "table"))
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
if type(param) == "string" then
|
||||||
|
self._sel = ui._parse_sel(param, false, false)
|
||||||
|
return make_style
|
||||||
|
end
|
||||||
|
|
||||||
|
self._sel = ui._universal_sel
|
||||||
|
return make_style(param)
|
||||||
|
end
|
||||||
|
|
||||||
|
function ui.Style:_init(props)
|
||||||
|
self._props = ui._cascade_props(props.props or props, {})
|
||||||
|
self._nested = table.merge(ui._opt_array(props.nested, ui.Style, props))
|
||||||
|
self._reset = ui._opt(props.reset, "boolean")
|
||||||
|
|
||||||
|
for _, item in ipairs(props) do
|
||||||
|
ui._req(item, ui.Style)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function ui.Style:_get_flat()
|
||||||
|
local flat_styles = {}
|
||||||
|
self:_get_flat_impl(flat_styles, ui._universal_sel)
|
||||||
|
return flat_styles
|
||||||
|
end
|
||||||
|
|
||||||
|
function ui.Style:_get_flat_impl(flat_styles, parent_sel)
|
||||||
|
-- Intersect our selector with our parent selector, resulting in a fully
|
||||||
|
-- qualified selector.
|
||||||
|
local full_sel = ui._intersect_sels({parent_sel, self._sel})
|
||||||
|
|
||||||
|
-- Copy this style's properties into a new style with the full selector.
|
||||||
|
local flat = ui.Style {
|
||||||
|
props = self._props,
|
||||||
|
reset = self._reset,
|
||||||
|
}
|
||||||
|
flat._sel = full_sel
|
||||||
|
|
||||||
|
table.insert(flat_styles, flat)
|
||||||
|
|
||||||
|
-- For each sub-style of this style, cascade it with our full selector and
|
||||||
|
-- add it to the list of flat styles.
|
||||||
|
for _, nested in ipairs(self._nested) do
|
||||||
|
nested:_get_flat_impl(flat_styles, full_sel)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local layout_type_map = {
|
||||||
|
place = 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
local dir_flags_map = {
|
||||||
|
none = 0,
|
||||||
|
x = 1,
|
||||||
|
y = 2,
|
||||||
|
both = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
local display_mode_map = {
|
||||||
|
visible = 0,
|
||||||
|
overflow = 1,
|
||||||
|
hidden = 2,
|
||||||
|
clipped = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
local icon_place_map = {
|
||||||
|
center = 0,
|
||||||
|
left = 1,
|
||||||
|
top = 2,
|
||||||
|
right = 3,
|
||||||
|
bottom = 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
local function opt_color(val, def)
|
||||||
|
assert(val == nil or core.colorspec_to_int(val))
|
||||||
|
return val or def
|
||||||
|
end
|
||||||
|
|
||||||
|
local function opt_vec2d(val, def)
|
||||||
|
ui._opt_array(val, "number")
|
||||||
|
assert(val == nil or #val == 1 or #val == 2)
|
||||||
|
return val or def
|
||||||
|
end
|
||||||
|
|
||||||
|
local function opt_rect(val, def)
|
||||||
|
ui._opt_array(val, "number")
|
||||||
|
assert(val == nil or #val == 1 or #val == 2 or #val == 4)
|
||||||
|
return val or def
|
||||||
|
end
|
||||||
|
|
||||||
|
local function cascade_layout(new, add, props)
|
||||||
|
new.layout = ui._opt_enum(add.layout, layout_type_map, props.layout)
|
||||||
|
new.clip = ui._opt_enum(add.clip, dir_flags_map, props.clip)
|
||||||
|
|
||||||
|
new.scale = ui._opt(add.scale, "number", props.scale)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function cascade_sizing(new, add, props)
|
||||||
|
new.size = opt_vec2d(add.size, props.size)
|
||||||
|
new.span = opt_vec2d(add.span, props.span)
|
||||||
|
|
||||||
|
new.pos = opt_vec2d(add.pos, props.pos)
|
||||||
|
new.anchor = opt_vec2d(add.anchor, props.anchor)
|
||||||
|
|
||||||
|
new.margin = opt_rect(add.margin, props.margin)
|
||||||
|
new.padding = opt_rect(add.padding, props.padding)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function cascade_layer(new, add, props, p)
|
||||||
|
new[p.."_image"] = ui._opt(add[p.."_image"], "string", props[p.."_image"])
|
||||||
|
new[p.."_fill"] = opt_color(add[p.."_fill"], props[p.."_fill"])
|
||||||
|
new[p.."_tint"] = opt_color(add[p.."_tint"], props[p.."_tint"])
|
||||||
|
|
||||||
|
new[p.."_scale"] = ui._opt(add[p.."_scale"], "number", props[p.."_scale"])
|
||||||
|
new[p.."_source"] = opt_rect(add[p.."_source"], props[p.."_source"])
|
||||||
|
|
||||||
|
new[p.."_frames"] = ui._opt(add[p.."_frames"], "number", props[p.."_frames"])
|
||||||
|
new[p.."_frame_time"] =
|
||||||
|
ui._opt(add[p.."_frame_time"], "number", props[p.."_frame_time"])
|
||||||
|
end
|
||||||
|
|
||||||
|
function ui._cascade_props(add, props)
|
||||||
|
local new = {}
|
||||||
|
|
||||||
|
cascade_layout(new, add, props)
|
||||||
|
cascade_sizing(new, add, props)
|
||||||
|
|
||||||
|
new.display = ui._opt_enum(add.display, display_mode_map, props.display)
|
||||||
|
|
||||||
|
cascade_layer(new, add, props, "box")
|
||||||
|
cascade_layer(new, add, props, "icon")
|
||||||
|
|
||||||
|
new.box_middle = opt_rect(add.box_middle, props.box_middle)
|
||||||
|
new.box_tile = ui._opt_enum(add.box_tile, dir_flags_map, props.box_tile)
|
||||||
|
|
||||||
|
new.icon_place = ui._opt_enum(add.icon_place, icon_place_map, props.icon_place)
|
||||||
|
new.icon_gutter = ui._opt(add.icon_gutter, "number", props.icon_gutter)
|
||||||
|
new.icon_overlap = ui._opt(add.icon_overlap, "boolean", props.icon_overlap)
|
||||||
|
|
||||||
|
return new
|
||||||
|
end
|
||||||
|
|
||||||
|
local function unpack_vec2d(vec)
|
||||||
|
if #vec == 2 then
|
||||||
|
return vec[1], vec[2]
|
||||||
|
elseif #vec == 1 then
|
||||||
|
return vec[1], vec[1]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function unpack_rect(rect)
|
||||||
|
if #rect == 4 then
|
||||||
|
return rect[1], rect[2], rect[3], rect[4]
|
||||||
|
elseif #rect == 2 then
|
||||||
|
return rect[1], rect[2], rect[1], rect[2]
|
||||||
|
elseif #rect == 1 then
|
||||||
|
return rect[1], rect[1], rect[1], rect[1]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function encode_layout(props)
|
||||||
|
local fl = ui._make_flags()
|
||||||
|
|
||||||
|
if ui._shift_flag(fl, props.layout) then
|
||||||
|
ui._encode_flag(fl, "B", layout_type_map[props.layout])
|
||||||
|
end
|
||||||
|
if ui._shift_flag(fl, props.clip) then
|
||||||
|
ui._encode_flag(fl, "B", dir_flags_map[props.clip])
|
||||||
|
end
|
||||||
|
|
||||||
|
if ui._shift_flag(fl, props.scale) then
|
||||||
|
ui._encode_flag(fl, "f", props.scale)
|
||||||
|
end
|
||||||
|
|
||||||
|
return fl
|
||||||
|
end
|
||||||
|
|
||||||
|
local function encode_sizing(props)
|
||||||
|
local fl = ui._make_flags()
|
||||||
|
|
||||||
|
if ui._shift_flag(fl, props.size) then
|
||||||
|
ui._encode_flag(fl, "ff", unpack_vec2d(props.size))
|
||||||
|
end
|
||||||
|
if ui._shift_flag(fl, props.span) then
|
||||||
|
ui._encode_flag(fl, "ff", unpack_vec2d(props.span))
|
||||||
|
end
|
||||||
|
|
||||||
|
if ui._shift_flag(fl, props.pos) then
|
||||||
|
ui._encode_flag(fl, "ff", unpack_vec2d(props.pos))
|
||||||
|
end
|
||||||
|
if ui._shift_flag(fl, props.anchor) then
|
||||||
|
ui._encode_flag(fl, "ff", unpack_vec2d(props.anchor))
|
||||||
|
end
|
||||||
|
|
||||||
|
if ui._shift_flag(fl, props.margin) then
|
||||||
|
ui._encode_flag(fl, "ffff", unpack_rect(props.margin))
|
||||||
|
end
|
||||||
|
if ui._shift_flag(fl, props.padding) then
|
||||||
|
ui._encode_flag(fl, "ffff", unpack_rect(props.padding))
|
||||||
|
end
|
||||||
|
|
||||||
|
return fl
|
||||||
|
end
|
||||||
|
|
||||||
|
local function encode_layer(props, p)
|
||||||
|
local fl = ui._make_flags()
|
||||||
|
|
||||||
|
if ui._shift_flag(fl, props[p.."_image"]) then
|
||||||
|
ui._encode_flag(fl, "z", props[p.."_image"])
|
||||||
|
end
|
||||||
|
if ui._shift_flag(fl, props[p.."_fill"]) then
|
||||||
|
ui._encode_flag(fl, "I", core.colorspec_to_int(props[p.."_fill"]))
|
||||||
|
end
|
||||||
|
if ui._shift_flag(fl, props[p.."_tint"]) then
|
||||||
|
ui._encode_flag(fl, "I", core.colorspec_to_int(props[p.."_tint"]))
|
||||||
|
end
|
||||||
|
|
||||||
|
if ui._shift_flag(fl, props[p.."_scale"]) then
|
||||||
|
ui._encode_flag(fl, "f", props[p.."_scale"])
|
||||||
|
end
|
||||||
|
if ui._shift_flag(fl, props[p.."_source"]) then
|
||||||
|
ui._encode_flag(fl, "ffff", unpack_rect(props[p.."_source"]))
|
||||||
|
end
|
||||||
|
|
||||||
|
if ui._shift_flag(fl, props[p.."_frames"]) then
|
||||||
|
ui._encode_flag(fl, "I", props[p.."_frames"])
|
||||||
|
end
|
||||||
|
if ui._shift_flag(fl, props[p.."_frame_time"]) then
|
||||||
|
ui._encode_flag(fl, "I", props[p.."_frame_time"])
|
||||||
|
end
|
||||||
|
|
||||||
|
return fl
|
||||||
|
end
|
||||||
|
|
||||||
|
local function encode_subflags(fl, sub_fl)
|
||||||
|
if ui._shift_flag(fl, sub_fl.flags ~= 0) then
|
||||||
|
ui._encode_flag(fl, "s", ui._encode_flags(sub_fl))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function ui._encode_props(props)
|
||||||
|
local fl = ui._make_flags()
|
||||||
|
|
||||||
|
encode_subflags(fl, encode_layout(props))
|
||||||
|
encode_subflags(fl, encode_sizing(props))
|
||||||
|
|
||||||
|
if ui._shift_flag(fl, props.display) then
|
||||||
|
ui._encode_flag(fl, "B", display_mode_map[props.display])
|
||||||
|
end
|
||||||
|
|
||||||
|
encode_subflags(fl, encode_layer(props, "box"))
|
||||||
|
encode_subflags(fl, encode_layer(props, "icon"))
|
||||||
|
|
||||||
|
if ui._shift_flag(fl, props.box_middle) then
|
||||||
|
ui._encode_flag(fl, "ffff", unpack_rect(props.box_middle))
|
||||||
|
end
|
||||||
|
if ui._shift_flag(fl, props.box_tile) then
|
||||||
|
ui._encode_flag(fl, "B", dir_flags_map[props.box_tile])
|
||||||
|
end
|
||||||
|
|
||||||
|
if ui._shift_flag(fl, props.icon_place) then
|
||||||
|
ui._encode_flag(fl, "B", icon_place_map[props.icon_place])
|
||||||
|
end
|
||||||
|
if ui._shift_flag(fl, props.icon_gutter) then
|
||||||
|
ui._encode_flag(fl, "f", props.icon_gutter)
|
||||||
|
end
|
||||||
|
ui._shift_flag_bool(fl, props.icon_overlap)
|
||||||
|
|
||||||
|
return ui._encode("s", ui._encode_flags(fl))
|
||||||
|
end
|
36
builtin/ui/theme.lua
Normal file
36
builtin/ui/theme.lua
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
-- Luanti
|
||||||
|
-- SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
-- Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||||
|
|
||||||
|
local prelude_theme = ui.Style {
|
||||||
|
ui.Style "root" {
|
||||||
|
pos = {1/2},
|
||||||
|
anchor = {1/2},
|
||||||
|
span = {0},
|
||||||
|
|
||||||
|
ui.Style "@backdrop" {
|
||||||
|
display = "hidden",
|
||||||
|
clip = "both",
|
||||||
|
},
|
||||||
|
ui.Style "@backdrop$focused" {
|
||||||
|
display = "visible",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ui.Style "image" {
|
||||||
|
icon_scale = 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
function ui.get_prelude_theme()
|
||||||
|
return prelude_theme
|
||||||
|
end
|
||||||
|
|
||||||
|
local default_theme = prelude_theme
|
||||||
|
|
||||||
|
function ui.get_default_theme()
|
||||||
|
return default_theme
|
||||||
|
end
|
||||||
|
|
||||||
|
function ui.set_default_theme(theme)
|
||||||
|
default_theme = ui._req(theme, ui.Style)
|
||||||
|
end
|
120
builtin/ui/util.lua
Normal file
120
builtin/ui/util.lua
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
-- Luanti
|
||||||
|
-- SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
-- Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||||
|
|
||||||
|
local next_id = 0
|
||||||
|
|
||||||
|
function ui.new_id()
|
||||||
|
-- Just increment a monotonic counter and return it as hex. Even at
|
||||||
|
-- unreasonably fast ID generation rates, it would take years for this
|
||||||
|
-- counter to hit the 2^53 limit and start generating duplicates.
|
||||||
|
next_id = next_id + 1
|
||||||
|
return string.format("_%X", next_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
ui._ID_CHARS = "a-zA-Z0-9_%-%:"
|
||||||
|
|
||||||
|
function ui.is_id(str)
|
||||||
|
return type(str) == "string" and str == str:match("^[" .. ui._ID_CHARS .. "]+$")
|
||||||
|
end
|
||||||
|
|
||||||
|
function ui._is_reserved_id(str)
|
||||||
|
return ui.is_id(str) and str:match("^[_-]")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- This coordinate size calculation copies the one for fixed-size formspec
|
||||||
|
-- coordinates in guiFormSpecMenu.cpp.
|
||||||
|
function ui.get_coord_size()
|
||||||
|
return math.floor(0.5555 * 96)
|
||||||
|
end
|
||||||
|
|
||||||
|
function ui._req(val, typ)
|
||||||
|
assert(type(val) == typ or
|
||||||
|
(typ == "id" and ui.is_id(val)) or core.is_instance(val, typ))
|
||||||
|
return val
|
||||||
|
end
|
||||||
|
|
||||||
|
function ui._opt(val, typ, def)
|
||||||
|
if val == nil then
|
||||||
|
return def
|
||||||
|
end
|
||||||
|
return ui._req(val, typ)
|
||||||
|
end
|
||||||
|
|
||||||
|
function ui._req_array(arr, typ)
|
||||||
|
for _, val in ipairs(ui._req(arr, "table")) do
|
||||||
|
ui._req(val, typ)
|
||||||
|
end
|
||||||
|
return arr
|
||||||
|
end
|
||||||
|
|
||||||
|
function ui._opt_array(arr, typ, def)
|
||||||
|
for _, val in ipairs(ui._opt(arr, "table", {})) do
|
||||||
|
ui._req(val, typ)
|
||||||
|
end
|
||||||
|
return arr or def
|
||||||
|
end
|
||||||
|
|
||||||
|
function ui._req_enum(val, enum)
|
||||||
|
assert(type(val) == "string" and enum[val])
|
||||||
|
return val
|
||||||
|
end
|
||||||
|
|
||||||
|
function ui._opt_enum(val, enum, def)
|
||||||
|
if val == nil then
|
||||||
|
return def
|
||||||
|
end
|
||||||
|
return ui._req_enum(val, enum)
|
||||||
|
end
|
||||||
|
|
||||||
|
ui._encode = core.encode_network
|
||||||
|
ui._decode = core.decode_network
|
||||||
|
|
||||||
|
function ui._encode_array(format, arr)
|
||||||
|
local formatted = {}
|
||||||
|
for _, val in ipairs(arr) do
|
||||||
|
table.insert(formatted, ui._encode(format, val))
|
||||||
|
end
|
||||||
|
|
||||||
|
return ui._encode("IZ", #formatted, table.concat(formatted))
|
||||||
|
end
|
||||||
|
|
||||||
|
function ui._pack_flags(...)
|
||||||
|
local flags = 0
|
||||||
|
for _, flag in ipairs({...}) do
|
||||||
|
flags = bit.bor(bit.lshift(flags, 1), flag and 1 or 0)
|
||||||
|
end
|
||||||
|
return flags
|
||||||
|
end
|
||||||
|
|
||||||
|
function ui._make_flags()
|
||||||
|
return {flags = 0, num_flags = 0, data = {}}
|
||||||
|
end
|
||||||
|
|
||||||
|
function ui._shift_flag(fl, flag)
|
||||||
|
-- OR the LSB with the condition, and then right rotate it to the MSB.
|
||||||
|
fl.flags = bit.ror(bit.bor(fl.flags, flag and 1 or 0), 1)
|
||||||
|
fl.num_flags = fl.num_flags + 1
|
||||||
|
|
||||||
|
return flag
|
||||||
|
end
|
||||||
|
|
||||||
|
function ui._shift_flag_bool(fl, flag)
|
||||||
|
if ui._shift_flag(fl, flag ~= nil) then
|
||||||
|
ui._shift_flag(fl, flag)
|
||||||
|
else
|
||||||
|
ui._shift_flag(fl, false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function ui._encode_flag(fl, ...)
|
||||||
|
table.insert(fl.data, ui._encode(...))
|
||||||
|
end
|
||||||
|
|
||||||
|
function ui._encode_flags(fl)
|
||||||
|
-- We've been shifting into the right the entire time, so flags are in the
|
||||||
|
-- upper bits; however, the protocol expects them to be in the lower bits.
|
||||||
|
-- So, shift them the appropriate amount into the lower bits.
|
||||||
|
local adjusted = bit.rshift(fl.flags, 32 - fl.num_flags)
|
||||||
|
return ui._encode("I", adjusted) .. table.concat(fl.data)
|
||||||
|
end
|
190
builtin/ui/window.lua
Normal file
190
builtin/ui/window.lua
Normal file
|
@ -0,0 +1,190 @@
|
||||||
|
-- Luanti
|
||||||
|
-- SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
-- Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||||
|
|
||||||
|
ui.Window = core.class()
|
||||||
|
|
||||||
|
ui._window_types = {
|
||||||
|
filter = 0,
|
||||||
|
mask = 1,
|
||||||
|
hud = 2,
|
||||||
|
chat = 3,
|
||||||
|
gui = 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
function ui.Window:new(param)
|
||||||
|
local function make_window(props)
|
||||||
|
self:_init(ui._req(props, "table"))
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
self._type = ui._req_enum(param, ui._window_types)
|
||||||
|
return make_window
|
||||||
|
end
|
||||||
|
|
||||||
|
function ui.Window:_init(props)
|
||||||
|
self._theme = ui._opt(props.theme, ui.Style, ui.get_default_theme())
|
||||||
|
self._styles = ui._opt_array(props.styles, ui.Style, props)
|
||||||
|
|
||||||
|
self._root = ui._req(props.root, ui.Root)
|
||||||
|
|
||||||
|
self._context = nil -- Set by ui.Context
|
||||||
|
|
||||||
|
self._elems = self._root:_get_flat()
|
||||||
|
self._elems_by_id = {}
|
||||||
|
|
||||||
|
for _, elem in ipairs(self._elems) do
|
||||||
|
local id = elem._id
|
||||||
|
|
||||||
|
assert(not self._elems_by_id[id], "Element has duplicate ID: '" .. id .. "'")
|
||||||
|
self._elems_by_id[id] = elem
|
||||||
|
|
||||||
|
assert(elem._window == nil, "Element '" .. elem._id .. "' already has a window")
|
||||||
|
elem._window = self
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, item in ipairs(props) do
|
||||||
|
ui._req(item, ui.Style)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function ui.Window:_encode(player, opening)
|
||||||
|
local enc_styles = self:_encode_styles()
|
||||||
|
local enc_elems = self:_encode_elems()
|
||||||
|
|
||||||
|
local data = ui._encode("ZzZ", enc_elems, self._root._id, enc_styles)
|
||||||
|
if opening then
|
||||||
|
data = ui._encode("ZB", data, ui._window_types[self._type])
|
||||||
|
end
|
||||||
|
|
||||||
|
return data
|
||||||
|
end
|
||||||
|
|
||||||
|
function ui.Window:_encode_styles()
|
||||||
|
-- Clear out all the boxes in every element.
|
||||||
|
for _, elem in ipairs(self._elems) do
|
||||||
|
for box in pairs(elem._boxes) do
|
||||||
|
elem._boxes[box] = {n = 0}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Get a cascaded and flattened list of all the styles for this window.
|
||||||
|
local styles = self:_get_full_style():_get_flat()
|
||||||
|
|
||||||
|
-- Take each style and apply its properties to every box and state matched
|
||||||
|
-- by its selector.
|
||||||
|
self:_apply_styles(styles)
|
||||||
|
|
||||||
|
-- Take the styled boxes and encode their styles into a single table,
|
||||||
|
-- replacing the boxes' style property tables with indices into this table.
|
||||||
|
local enc_styles = self:_index_styles()
|
||||||
|
|
||||||
|
return ui._encode_array("Z", enc_styles)
|
||||||
|
end
|
||||||
|
|
||||||
|
function ui.Window:_get_full_style()
|
||||||
|
-- The full style contains the theme, global styles, and local element
|
||||||
|
-- styles as sub-styles, in that order, to ensure the correct precedence.
|
||||||
|
local styles = table.merge({self._theme}, self._styles)
|
||||||
|
|
||||||
|
for _, elem in ipairs(self._elems) do
|
||||||
|
-- Cascade the inline style with the element's ID, ensuring that the
|
||||||
|
-- inline style globally refers to this element only.
|
||||||
|
local local_style = ui.Style("#" .. elem._id) {
|
||||||
|
props = elem._props,
|
||||||
|
nested = elem._styles,
|
||||||
|
}
|
||||||
|
table.insert(styles, local_style)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Return all these styles wrapped up into a single style.
|
||||||
|
return ui.Style {nested = styles}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function apply_style(elem, boxes, style)
|
||||||
|
-- Loop through each box, applying the styles accordingly. The table of
|
||||||
|
-- boxes may be empty, in which case nothing happens.
|
||||||
|
for _, box in pairs(boxes) do
|
||||||
|
local name = box.name or "main"
|
||||||
|
|
||||||
|
-- If this style resets all properties, find all states that are a
|
||||||
|
-- subset of the state being styled and clear their property tables.
|
||||||
|
if style._reset then
|
||||||
|
for i = ui._STATE_NONE, ui._NUM_STATES - 1 do
|
||||||
|
if bit.band(box.states, i) == box.states then
|
||||||
|
elem._boxes[name][i] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Get the existing style property table for this box if it exists.
|
||||||
|
local props = elem._boxes[name][box.states] or {}
|
||||||
|
|
||||||
|
-- Cascade the properties from this style onto the box.
|
||||||
|
elem._boxes[name][box.states] = ui._cascade_props(style._props, props)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function ui.Window:_apply_styles(styles)
|
||||||
|
-- Loop through each style and element and see if the style properties can
|
||||||
|
-- be applied to any boxes.
|
||||||
|
for _, style in ipairs(styles) do
|
||||||
|
for _, elem in ipairs(self._elems) do
|
||||||
|
-- Check if the selector for this style. If it matches, apply the
|
||||||
|
-- style to each of the applicable boxes.
|
||||||
|
local matches, boxes = style._sel(elem)
|
||||||
|
if matches then
|
||||||
|
apply_style(elem, boxes, style)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function index_style(box, i, style_indices, enc_styles)
|
||||||
|
-- If we have a style for this state, serialize it to a string. Identical
|
||||||
|
-- styles have identical strings, so we use this to our advantage.
|
||||||
|
local enc = ui._encode_props(box[i])
|
||||||
|
|
||||||
|
-- If we haven't serialized a style identical to this one before, store
|
||||||
|
-- this as the latest index in the list of style strings.
|
||||||
|
if not style_indices[enc] then
|
||||||
|
style_indices[enc] = #enc_styles
|
||||||
|
table.insert(enc_styles, enc)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Set the index of our state to the index of its style string, and keep
|
||||||
|
-- count of how many states with valid indices we have for this box so far.
|
||||||
|
box[i] = style_indices[enc]
|
||||||
|
box.n = box.n + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
function ui.Window:_index_styles()
|
||||||
|
local style_indices = {}
|
||||||
|
local enc_styles = {}
|
||||||
|
|
||||||
|
for _, elem in ipairs(self._elems) do
|
||||||
|
for _, box in pairs(elem._boxes) do
|
||||||
|
for i = ui._STATE_NONE, ui._NUM_STATES - 1 do
|
||||||
|
if box[i] then
|
||||||
|
-- If this box has a style, encode and index it.
|
||||||
|
index_style(box, i, style_indices, enc_styles)
|
||||||
|
else
|
||||||
|
-- Otherwise, this state has no style, so set it as such.
|
||||||
|
box[i] = ui._NO_STYLE
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return enc_styles
|
||||||
|
end
|
||||||
|
|
||||||
|
function ui.Window:_encode_elems()
|
||||||
|
local enc_elems = {}
|
||||||
|
|
||||||
|
for _, elem in ipairs(self._elems) do
|
||||||
|
table.insert(enc_elems, elem:_encode())
|
||||||
|
end
|
||||||
|
|
||||||
|
return ui._encode_array("Z", enc_elems)
|
||||||
|
end
|
|
@ -4237,6 +4237,8 @@ Helper functions
|
||||||
* since 5.12
|
* since 5.12
|
||||||
* `table` can also be non-table value, which will be returned as-is
|
* `table` can also be non-table value, which will be returned as-is
|
||||||
* preserves metatables as they are
|
* preserves metatables as they are
|
||||||
|
* Returns a deep copy of `table`, i.e. a copy of the table and all its
|
||||||
|
nested tables.
|
||||||
* `table.indexof(list, val)`: returns the smallest numerical index containing
|
* `table.indexof(list, val)`: returns the smallest numerical index containing
|
||||||
the value `val` in the table `list`. Non-numerical indices are ignored.
|
the value `val` in the table `list`. Non-numerical indices are ignored.
|
||||||
If `val` could not be found, `-1` is returned. `list` must not have
|
If `val` could not be found, `-1` is returned. `list` must not have
|
||||||
|
@ -4248,6 +4250,9 @@ Helper functions
|
||||||
* `table.insert_all(table, other_table)`:
|
* `table.insert_all(table, other_table)`:
|
||||||
* Appends all values in `other_table` to `table` - uses `#table + 1` to
|
* Appends all values in `other_table` to `table` - uses `#table + 1` to
|
||||||
find new indices.
|
find new indices.
|
||||||
|
* `table.merge(...)`:
|
||||||
|
* Merges multiple tables together into a new single table using
|
||||||
|
`table.insert_all()`.
|
||||||
* `table.key_value_swap(t)`: returns a table with keys and values swapped
|
* `table.key_value_swap(t)`: returns a table with keys and values swapped
|
||||||
* If multiple keys in `t` map to the same value, it is unspecified which
|
* If multiple keys in `t` map to the same value, it is unspecified which
|
||||||
value maps to that key.
|
value maps to that key.
|
||||||
|
@ -5993,6 +5998,63 @@ Utilities
|
||||||
* `core.urlencode(str)`: Encodes reserved URI characters by a
|
* `core.urlencode(str)`: Encodes reserved URI characters by a
|
||||||
percent sign followed by two hex digits. See
|
percent sign followed by two hex digits. See
|
||||||
[RFC 3986, section 2.3](https://datatracker.ietf.org/doc/html/rfc3986#section-2.3).
|
[RFC 3986, section 2.3](https://datatracker.ietf.org/doc/html/rfc3986#section-2.3).
|
||||||
|
* `core.class([super])`: Creates a new metatable-based class.
|
||||||
|
* `super` (optional): The superclass of the newly created class, or nil if
|
||||||
|
the class should not have a superclass.
|
||||||
|
* The class table is given a metatable with an `__index` field that points
|
||||||
|
to `super` and a `__call` metamethod that constructs a new object.
|
||||||
|
* By default, the only field in the class table is an `__index` field that
|
||||||
|
points to itself. When an instance of the class is created, the class
|
||||||
|
table is set as the metatable for the object.
|
||||||
|
* When a new object is constructed via `__call`, the `new()` method will be
|
||||||
|
called if it exists. If `new()` returns a nonzero number of values, then
|
||||||
|
those values will be returned from `__call`. Otherwise, if `new()`
|
||||||
|
returned no values, the object iself will be returned.
|
||||||
|
* Extra Lua metamethods like `__add` may be added to the class table, but
|
||||||
|
note that these fields will not be inherited by subclasses since Lua
|
||||||
|
doesn't consult `__index` when searching for metamethods.
|
||||||
|
* Example: The following code, demonstrating a simple example of classes
|
||||||
|
and inheritance, will print `Rectangle[area=6, filled=true]`:
|
||||||
|
```lua
|
||||||
|
local Shape = core.class()
|
||||||
|
Shape.name = "Shape"
|
||||||
|
|
||||||
|
function Shape:new(filled)
|
||||||
|
self.filled = filled
|
||||||
|
end
|
||||||
|
|
||||||
|
function Shape:describe()
|
||||||
|
return string.format("%s[area=%d, filled=%s]",
|
||||||
|
self.name, self:get_area(), self.filled)
|
||||||
|
end
|
||||||
|
|
||||||
|
local Rectangle = core.class(Shape)
|
||||||
|
Rectangle.name = "Rectangle"
|
||||||
|
|
||||||
|
function Rectangle:new(filled, width, height)
|
||||||
|
Shape.new(self, filled)
|
||||||
|
|
||||||
|
self.width = width
|
||||||
|
self.height = height
|
||||||
|
end
|
||||||
|
|
||||||
|
function Rectangle:get_area()
|
||||||
|
return self.width * self.height
|
||||||
|
end
|
||||||
|
|
||||||
|
local shape = Rectangle(true, 2, 3)
|
||||||
|
print(shape:describe())
|
||||||
|
|
||||||
|
assert(core.is_instance(shape, Shape))
|
||||||
|
assert(core.is_subclass(Rectangle, Shape))
|
||||||
|
assert(core.super(Rectangle) == Shape)
|
||||||
|
```
|
||||||
|
* `core.super(class)`: Returns the superclass of `class`, or nil if the table
|
||||||
|
is not a class or has no superclass.
|
||||||
|
* `core.is_subclass(class, super)`: Returns true if `class` is a subclass of
|
||||||
|
`super`.
|
||||||
|
* `core.is_instance(obj, class)`: Returns true if `obj` is an instance of
|
||||||
|
`class` or any of its subclasses.
|
||||||
|
|
||||||
Logging
|
Logging
|
||||||
-------
|
-------
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue