mirror of https://github.com/minetest/minetest.git
Merge f5397ca46c
into 38cacfa577
This commit is contained in:
commit
fecb6d1f16
|
@ -20,7 +20,14 @@ read_globals = {
|
|||
"PerlinNoise", "PerlinNoiseMap",
|
||||
|
||||
string = {fields = {"split", "trim"}},
|
||||
table = {fields = {"copy", "getn", "indexof", "insert_all"}},
|
||||
table = {fields = {
|
||||
"copy",
|
||||
"getn",
|
||||
"indexof",
|
||||
"insert_all",
|
||||
"merge",
|
||||
"shallow_copy",
|
||||
}},
|
||||
math = {fields = {"hypot", "round"}},
|
||||
}
|
||||
|
||||
|
|
|
@ -489,6 +489,15 @@ function table.copy(t, seen)
|
|||
end
|
||||
|
||||
|
||||
function table.shallow_copy(t)
|
||||
local new = {}
|
||||
for k, v in pairs(t) do
|
||||
new[k] = v
|
||||
end
|
||||
return new
|
||||
end
|
||||
|
||||
|
||||
function table.insert_all(t, other)
|
||||
if table.move then -- LuaJIT
|
||||
return table.move(other, 1, #other, #t + 1, t)
|
||||
|
@ -500,6 +509,15 @@ function table.insert_all(t, other)
|
|||
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)
|
||||
local ti = {}
|
||||
for k,v in pairs(t) do
|
||||
|
@ -763,3 +781,28 @@ function core.parse_coordinates(x, y, z, relative_to)
|
|||
local rz = core.parse_relative_number(z, relative_to.z)
|
||||
return rx and ry and rz and { x = rx, y = ry, z = rz }
|
||||
end
|
||||
|
||||
local function call(class, ...)
|
||||
local obj = core.class(class)
|
||||
if obj.new then
|
||||
obj:new(...)
|
||||
end
|
||||
return obj
|
||||
end
|
||||
|
||||
function core.class(super)
|
||||
super = super or {}
|
||||
super.__index = super
|
||||
super.__call = call
|
||||
|
||||
return setmetatable({}, super)
|
||||
end
|
||||
|
||||
function core.is_instance(obj, class)
|
||||
if type(obj) ~= "table" then
|
||||
return false
|
||||
end
|
||||
|
||||
local meta = getmetatable(obj)
|
||||
return meta == class or core.is_instance(meta, class)
|
||||
end
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
local scriptpath = core.get_builtin_path()
|
||||
local commonpath = scriptpath .. "common" .. DIR_DELIM
|
||||
local gamepath = scriptpath .. "game".. DIR_DELIM
|
||||
local uipath = scriptpath .. "ui" .. DIR_DELIM
|
||||
|
||||
-- Shared between builtin files, but
|
||||
-- not exposed to outer context
|
||||
|
@ -38,6 +39,7 @@ dofile(gamepath .. "forceloading.lua")
|
|||
dofile(gamepath .. "hud.lua")
|
||||
dofile(gamepath .. "knockback.lua")
|
||||
dofile(gamepath .. "async.lua")
|
||||
dofile(uipath .. "init.lua")
|
||||
|
||||
core.after(0, builtin_shared.cache_content_ids)
|
||||
|
||||
|
|
|
@ -0,0 +1,130 @@
|
|||
--[[
|
||||
Minetest
|
||||
Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU Lesser General Public License as published by
|
||||
the Free Software Foundation; either version 2.1 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
--]]
|
||||
|
||||
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(not ui._elem_types[type], "Derived element name already used")
|
||||
return ui._new_type(base, type, base._type_id, base._id_required)
|
||||
end
|
||||
|
||||
ui.Elem = ui._new_type(nil, "elem", 0x00, false)
|
||||
|
||||
ui.Label = ui.derive_elem(ui.Elem, "label")
|
||||
ui.Image = ui.derive_elem(ui.Elem, "image")
|
||||
|
||||
function ui.Elem:new(props)
|
||||
if self._id_required then
|
||||
assert(ui.is_id(props.id), "ID is required for '" .. self._type .. "'")
|
||||
end
|
||||
|
||||
self._id = props.id or ui.new_id()
|
||||
self._groups = {}
|
||||
self._boxes = {main = true}
|
||||
self._style = props.style or ui.Style{props = props}
|
||||
|
||||
self._children = table.merge(props.children or props)
|
||||
|
||||
-- Set by parent ui.Elem
|
||||
self._parent = nil
|
||||
self._index = nil
|
||||
self._rindex = nil
|
||||
|
||||
-- Set by ui.Window
|
||||
self._window = nil
|
||||
|
||||
assert(ui.is_id(self._id), "Element ID must be an ID string")
|
||||
|
||||
for _, group in ipairs(props.groups or {}) do
|
||||
assert(ui.is_id(group), "Element group must be an ID string")
|
||||
self._groups[group] = true
|
||||
end
|
||||
|
||||
for i, child in ipairs(self._children) do
|
||||
assert(child._parent == nil, "Element already has a parent")
|
||||
assert(not core.is_instance(child, ui.Root),
|
||||
"ui.Root can only be a root element")
|
||||
|
||||
child._parent = self
|
||||
child._index = i
|
||||
child._rindex = #self._children - i + 1
|
||||
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
|
|
@ -0,0 +1,34 @@
|
|||
--[[
|
||||
Minetest
|
||||
Copyright (C) 2024 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU Lesser General Public License as published by
|
||||
the Free Software Foundation; either version 2.1 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
--]]
|
||||
|
||||
ui.Root = ui._new_type(ui.Elem, "root", 0x01, false)
|
||||
|
||||
function ui.Root:new(props)
|
||||
ui.Elem.new(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
|
|
@ -0,0 +1,29 @@
|
|||
--[[
|
||||
Minetest
|
||||
Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU Lesser General Public License as published by
|
||||
the Free Software Foundation; either version 2.1 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
--]]
|
||||
|
||||
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 .. "window.lua")
|
||||
dofile(UI_PATH .. "elem_defs.lua")
|
|
@ -0,0 +1,497 @@
|
|||
--[[
|
||||
Minetest
|
||||
Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU Lesser General Public License as published by
|
||||
the Free Software Foundation; either version 2.1 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
--]]
|
||||
|
||||
ui._STATE_NONE = 0
|
||||
ui._NUM_STATES = bit.lshift(1, 5)
|
||||
ui._NO_STYLE = -1
|
||||
|
||||
--[[
|
||||
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 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 may be nil if the
|
||||
selector specified no box, in which case it should default to "main". 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 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
|
||||
|
||||
local simple_preds = {
|
||||
["empty"] = function(elem)
|
||||
return result(#elem._children == 0)
|
||||
end,
|
||||
|
||||
["first_child"] = function(elem)
|
||||
return result(elem._parent == nil or elem._index == 1)
|
||||
end,
|
||||
|
||||
["last_child"] = function(elem)
|
||||
return result(elem._parent == nil or elem._rindex == 1)
|
||||
end,
|
||||
|
||||
["only_child"] = function(elem)
|
||||
return result(elem._parent == nil or #elem._parent._children == 1)
|
||||
end,
|
||||
}
|
||||
|
||||
local sel_preds = {
|
||||
["<"] = function(sel)
|
||||
return function(elem)
|
||||
return result(elem._parent and sel(elem._parent))
|
||||
end
|
||||
end,
|
||||
|
||||
[">"] = function(sel)
|
||||
return function(elem)
|
||||
for _, child in ipairs(elem._children) do
|
||||
if sel(child) then
|
||||
return result(true)
|
||||
end
|
||||
end
|
||||
return result(false)
|
||||
end
|
||||
end,
|
||||
|
||||
["<<"] = function(sel)
|
||||
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,
|
||||
|
||||
[">>"] = function(sel)
|
||||
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,
|
||||
|
||||
["<>"] = function(sel)
|
||||
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,
|
||||
}
|
||||
|
||||
local special_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._index == index)
|
||||
end
|
||||
end,
|
||||
|
||||
["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
|
||||
return result(elem._rindex == rindex)
|
||||
end
|
||||
end,
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
|
||||
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 suffix, function(elem)
|
||||
return result(true)
|
||||
end
|
||||
|
||||
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 rest, function(elem)
|
||||
return result(elem._id == id)
|
||||
end
|
||||
|
||||
elseif prefix == "." then
|
||||
local group, rest = suffix:match("^([" .. ui._ID_CHARS .. "]+)(.*)$")
|
||||
assert(group, "Expected group after '.'")
|
||||
|
||||
return rest, function(elem)
|
||||
return result(elem._groups[group] ~= nil)
|
||||
end
|
||||
|
||||
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. @main@thumb
|
||||
effectively matches nothing by returning an empty table of boxes, but
|
||||
will return true for scrollbars, which a predicate will interpret as
|
||||
matching something. So, prevent it altogether. This problem
|
||||
fundamentally exists because we select elements, not boxes, since boxes
|
||||
and states are very much tied to the client-side of things.
|
||||
--]]
|
||||
assert(not pred, "Box selectors are invalid for predicates")
|
||||
|
||||
local name, rest = suffix:match("^([" .. ui._ID_CHARS .. "]+)(.*)$")
|
||||
assert(name, "Expected box after '@'")
|
||||
|
||||
return rest, function(elem)
|
||||
if elem._boxes[name] then
|
||||
-- If the box is in the element, return it.
|
||||
return result(true, name, ui._STATE_NONE)
|
||||
elseif name == "all" then
|
||||
-- If we want all boxes, iterate over the boxes in the element
|
||||
-- and add each of them to a 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
|
||||
|
||||
-- Otherwise, the selector doesn't match.
|
||||
return result(false)
|
||||
end
|
||||
|
||||
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, "Style selectors are invalid for predicates")
|
||||
|
||||
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 rest, 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
|
||||
|
||||
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 rest, function(elem)
|
||||
return result(elem._window._type == type)
|
||||
end
|
||||
|
||||
elseif prefix == "," then
|
||||
-- Since we don't know which terms came directly behind us, we return
|
||||
-- nil so that ui._parse_sel() can union the two selectors on either
|
||||
-- side of the comma instead of returning a selector function.
|
||||
return suffix, nil
|
||||
|
||||
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 rest, ui._parse_sel(sub:sub(2, -2), pred)
|
||||
|
||||
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.
|
||||
local rest, term = parse_term(suffix, true)
|
||||
assert(term, "Expected selector term after '!'")
|
||||
|
||||
return rest, function(elem)
|
||||
return result(not term(elem))
|
||||
end
|
||||
|
||||
elseif prefix == "?" then
|
||||
-- Predicates may have different syntax depending on the name of the
|
||||
-- predicate, so just parse the name initially.
|
||||
local name, rest = 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 rest, func
|
||||
end
|
||||
|
||||
-- If this is a function predicate, we need to do more parsing.
|
||||
func = sel_preds[name] or special_preds[name]
|
||||
if func then
|
||||
-- Parse a matching pair of parentheses and get the contents
|
||||
-- between them.
|
||||
assert(rest:sub(1, 1) == "(", "Expected '(' after '?" .. name .. "'")
|
||||
|
||||
local sub, rest = rest:match("^(%b())(.*)$")
|
||||
assert(sub, "Unmatched ')' for '?" .. name .. "('")
|
||||
|
||||
local contents = sub:sub(2, -2)
|
||||
|
||||
-- If this is a function predicate that wants a selector, parse the
|
||||
-- contents as a predicate selector and pass it on to the selector
|
||||
-- creation function.
|
||||
if sel_preds[name] then
|
||||
return rest, func(ui._parse_sel(contents, true))
|
||||
end
|
||||
|
||||
-- Otherwise, hand the string directly to the function for special
|
||||
-- processing, which we automatically trim for convenience.
|
||||
return rest, func(contents:trim())
|
||||
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 in selector: '" .. prefix .. "'")
|
||||
|
||||
assert(ui._elem_types[type], "Invalid element type: '" .. type .. "'")
|
||||
|
||||
return rest, function(elem)
|
||||
return result(elem._type == type)
|
||||
end
|
||||
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
|
||||
|
||||
local function 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)
|
||||
str = str:trim()
|
||||
assert(str ~= "", "Empty style selector")
|
||||
|
||||
-- Since selectors consisting of a single universal selector are very
|
||||
-- common (as a blank ui.Style selector defaults to that), give it a
|
||||
-- dedicated fast-path and skip all the parsing.
|
||||
if str == "*" then
|
||||
return function()
|
||||
return result(true)
|
||||
end
|
||||
end
|
||||
|
||||
local sub_sels = {}
|
||||
local terms = {}
|
||||
|
||||
-- Loop until we've read every term from the input string.
|
||||
repeat
|
||||
-- Parse the next term from the input string.
|
||||
local term
|
||||
str, term = parse_term(str, pred)
|
||||
|
||||
if term ~= nil then
|
||||
-- If we didn't read a comma, insert this term into the list of
|
||||
-- terms for the current sub-selector.
|
||||
table.insert(terms, term)
|
||||
else
|
||||
-- If we read a comma, make sure that we have terms before and
|
||||
-- after it so it's not dangling.
|
||||
assert(#terms > 0, "Expected selector term before ','")
|
||||
assert(str ~= "", "Expected selector term after ','")
|
||||
end
|
||||
|
||||
-- If we read a comma or ran out of terms, we need to commit the terms
|
||||
-- we've read so far.
|
||||
if term == nil 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
|
||||
until str == ""
|
||||
|
||||
-- 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]
|
||||
end
|
||||
return union_sels(sub_sels)
|
||||
end
|
|
@ -0,0 +1,200 @@
|
|||
--[[
|
||||
Minetest
|
||||
Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU Lesser General Public License as published by
|
||||
the Free Software Foundation; either version 2.1 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
--]]
|
||||
|
||||
ui.Style = core.class()
|
||||
|
||||
function ui.Style:new(props)
|
||||
self._sel = ui._parse_sel(props.sel or "*")
|
||||
self._props = ui._cascade_props(props.props or props, {})
|
||||
self._nested = table.merge(props.nested or props)
|
||||
self._reset = props.reset
|
||||
end
|
||||
|
||||
function ui.Style:_get_flat()
|
||||
local flat_styles = {}
|
||||
self:_get_flat_impl(flat_styles, ui._parse_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{
|
||||
reset = self._reset,
|
||||
props = self._props,
|
||||
}
|
||||
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 function cascade_layer(new, add, props, p)
|
||||
new[p.."_image"] = add[p.."_image"] or props[p.."_image"]
|
||||
new[p.."_fill"] = add[p.."_fill"] or props[p.."_fill"]
|
||||
new[p.."_tint"] = add[p.."_tint"] or props[p.."_tint"]
|
||||
|
||||
new[p.."_source"] = add[p.."_source"] or props[p.."_source"]
|
||||
new[p.."_middle"] = add[p.."_middle"] or props[p.."_middle"]
|
||||
new[p.."_middle_scale"] = add[p.."_middle_scale"] or props[p.."_middle_scale"]
|
||||
|
||||
new[p.."_frames"] = add[p.."_frames"] or props[p.."_frames"]
|
||||
new[p.."_frame_time"] = add[p.."_frame_time"] or props[p.."_frame_time"]
|
||||
end
|
||||
|
||||
function ui._cascade_props(add, props)
|
||||
local new = {}
|
||||
|
||||
new.size = add.size or props.size
|
||||
|
||||
new.rel_pos = add.rel_pos or props.rel_pos
|
||||
new.rel_anchor = add.rel_anchor or props.rel_anchor
|
||||
new.rel_size = add.rel_size or props.rel_size
|
||||
|
||||
new.margin = add.margin or props.margin
|
||||
new.padding = add.padding or props.padding
|
||||
|
||||
cascade_layer(new, add, props, "bg")
|
||||
cascade_layer(new, add, props, "fg")
|
||||
|
||||
new.fg_scale = add.fg_scale or props.fg_scale
|
||||
new.fg_halign = add.fg_halign or props.fg_halign
|
||||
new.fg_valign = add.fg_valign or props.fg_valign
|
||||
|
||||
new.visible = ui._apply_bool(add.visible, props.visible)
|
||||
new.noclip = ui._apply_bool(add.noclip, props.noclip)
|
||||
|
||||
return new
|
||||
end
|
||||
|
||||
local halign_map = {left = 0, center = 1, right = 2}
|
||||
local valign_map = {top = 0, center = 1, bottom = 2}
|
||||
local spacing_map = {
|
||||
before = 0,
|
||||
after = 1,
|
||||
outside = 2,
|
||||
around = 3,
|
||||
between = 4,
|
||||
evenly = 5,
|
||||
remove = 6,
|
||||
}
|
||||
|
||||
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_colorint(props[p.."_fill"]))
|
||||
end
|
||||
if ui._shift_flag(fl, props[p.."_tint"]) then
|
||||
ui._encode_flag(fl, "I", core.colorspec_to_colorint(props[p.."_tint"]))
|
||||
end
|
||||
|
||||
if ui._shift_flag(fl, props[p.."_source"]) then
|
||||
ui._encode_flag(fl, "ffff", unpack(props[p.."_source"]))
|
||||
end
|
||||
if ui._shift_flag(fl, props[p.."_middle"]) then
|
||||
ui._encode_flag(fl, "ffff", unpack(props[p.."_middle"]))
|
||||
end
|
||||
if ui._shift_flag(fl, props[p.."_middle_scale"]) then
|
||||
ui._encode_flag(fl, "f", props[p.."_middle_scale"])
|
||||
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
|
||||
|
||||
function ui._encode_props(props)
|
||||
local fl = ui._make_flags()
|
||||
|
||||
if ui._shift_flag(fl, props.size) then
|
||||
ui._encode_flag(fl, "ff", unpack(props.size))
|
||||
end
|
||||
|
||||
if ui._shift_flag(fl, props.rel_pos) then
|
||||
ui._encode_flag(fl, "ff", unpack(props.rel_pos))
|
||||
end
|
||||
if ui._shift_flag(fl, props.rel_anchor) then
|
||||
ui._encode_flag(fl, "ff", unpack(props.rel_anchor))
|
||||
end
|
||||
if ui._shift_flag(fl, props.rel_size) then
|
||||
ui._encode_flag(fl, "ff", unpack(props.rel_size))
|
||||
end
|
||||
|
||||
if ui._shift_flag(fl, props.margin) then
|
||||
ui._encode_flag(fl, "ffff", unpack(props.margin))
|
||||
end
|
||||
if ui._shift_flag(fl, props.padding) then
|
||||
ui._encode_flag(fl, "ffff", unpack(props.padding))
|
||||
end
|
||||
|
||||
local bg_fl = encode_layer(props, "bg")
|
||||
if ui._shift_flag(fl, bg_fl.flags ~= 0) then
|
||||
ui._encode_flag(fl, "s", ui._encode_flags(bg_fl))
|
||||
end
|
||||
local fg_fl = encode_layer(props, "fg")
|
||||
if ui._shift_flag(fl, fg_fl.flags ~= 0) then
|
||||
ui._encode_flag(fl, "s", ui._encode_flags(fg_fl))
|
||||
end
|
||||
|
||||
if ui._shift_flag(fl, props.fg_scale) then
|
||||
ui._encode_flag(fl, "f", props.fg_scale)
|
||||
end
|
||||
if ui._shift_flag(fl, props.fg_halign) then
|
||||
ui._encode_flag(fl, "B", halign_map[props.fg_halign])
|
||||
end
|
||||
if ui._shift_flag(fl, props.fg_valign) then
|
||||
ui._encode_flag(fl, "B", valign_map[props.fg_valign])
|
||||
end
|
||||
|
||||
if ui._shift_flag(fl, props.visible ~= nil) then
|
||||
ui._shift_flag(fl, props.visible)
|
||||
end
|
||||
if ui._shift_flag(fl, props.noclip ~= nil) then
|
||||
ui._shift_flag(fl, props.noclip)
|
||||
end
|
||||
|
||||
return ui._encode("s", ui._encode_flags(fl))
|
||||
end
|
||||
|
||||
local default_theme = ui.Style{}
|
||||
|
||||
function ui.get_default_theme()
|
||||
return default_theme
|
||||
end
|
||||
|
||||
function ui.set_default_theme(theme)
|
||||
default_theme = theme
|
||||
end
|
|
@ -0,0 +1,91 @@
|
|||
--[[
|
||||
Minetest
|
||||
Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU Lesser General Public License as published by
|
||||
the Free Software Foundation; either version 2.1 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
--]]
|
||||
|
||||
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
|
||||
|
||||
-- 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._apply_bool(add, prop)
|
||||
if add ~= nil then
|
||||
return add
|
||||
end
|
||||
return prop
|
||||
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._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
|
|
@ -0,0 +1,308 @@
|
|||
--[[
|
||||
Minetest
|
||||
Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU Lesser General Public License as published by
|
||||
the Free Software Foundation; either version 2.1 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
--]]
|
||||
|
||||
local open_windows = {}
|
||||
|
||||
local function build_window(id, param)
|
||||
local info = open_windows[id]
|
||||
if not info then
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
local window = info.builder(id, info.player, info.context, param or {})
|
||||
assert(core.is_instance(window, ui.Window),
|
||||
"Expected ui.Window to be returned from builder function")
|
||||
assert(not window._id, "Window object has already been returned")
|
||||
|
||||
window._id = id
|
||||
info.window = window
|
||||
|
||||
return window, info.player
|
||||
end
|
||||
|
||||
ui.Window = core.class()
|
||||
|
||||
ui._window_types = {
|
||||
bg = 0,
|
||||
mask = 1,
|
||||
hud = 2,
|
||||
message = 3,
|
||||
gui = 4,
|
||||
fg = 5,
|
||||
}
|
||||
|
||||
function ui.Window:new(props)
|
||||
self._id = nil -- Set by build_window()
|
||||
self._type = props.type
|
||||
|
||||
self._theme = props.theme or ui.get_default_theme()
|
||||
self._style = props.style or ui.Style{}
|
||||
|
||||
self._root = props.root
|
||||
|
||||
assert(ui._window_types[self._type], "Invalid window type")
|
||||
assert(core.is_instance(self._root, ui.Root),
|
||||
"Expected root of window to be ui.Root")
|
||||
|
||||
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 already has window")
|
||||
elem._window = self
|
||||
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 style, and inline element
|
||||
-- styles as sub-styles, in that order, to ensure the correct precedence.
|
||||
local styles = {self._theme, self._style}
|
||||
|
||||
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.
|
||||
table.insert(styles, ui.Style{
|
||||
sel = "#" .. elem._id,
|
||||
nested = {elem._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
|
||||
|
||||
local OPEN_WINDOW = 0x00
|
||||
local REOPEN_WINDOW = 0x01
|
||||
local UPDATE_WINDOW = 0x02
|
||||
local CLOSE_WINDOW = 0x03
|
||||
|
||||
local last_id = 0
|
||||
|
||||
function ui.open(builder, player, context, param)
|
||||
local id = last_id
|
||||
last_id = last_id + 1
|
||||
|
||||
open_windows[id] = {
|
||||
builder = builder,
|
||||
player = player,
|
||||
context = context or {},
|
||||
window = nil, -- Set by build_window()
|
||||
}
|
||||
|
||||
local window = build_window(id, param)
|
||||
local data = ui._encode("BL Z", OPEN_WINDOW, id,
|
||||
window:_encode(player, true))
|
||||
|
||||
core.send_ui_message(player, data)
|
||||
return id
|
||||
end
|
||||
|
||||
function ui.reopen(close_id, param)
|
||||
local new_id = last_id
|
||||
last_id = last_id + 1
|
||||
|
||||
open_windows[new_id] = open_windows[close_id]
|
||||
open_windows[close_id] = nil
|
||||
|
||||
local window, player = build_window(new_id, param)
|
||||
if not window then
|
||||
return nil
|
||||
end
|
||||
|
||||
local data = ui._encode("BLL Z", REOPEN_WINDOW, new_id, close_id,
|
||||
window:_encode(player, true))
|
||||
|
||||
core.send_ui_message(player, data)
|
||||
return new_id
|
||||
end
|
||||
|
||||
function ui.update(id, param)
|
||||
local window, player = build_window(id, param)
|
||||
if not window then
|
||||
return
|
||||
end
|
||||
|
||||
local data = ui._encode("BL Z", UPDATE_WINDOW, id,
|
||||
window:_encode(player, false))
|
||||
|
||||
core.send_ui_message(player, data)
|
||||
end
|
||||
|
||||
function ui.close(id)
|
||||
local info = open_windows[id]
|
||||
if not info then
|
||||
return
|
||||
end
|
||||
|
||||
local data = ui._encode("BL", CLOSE_WINDOW, id)
|
||||
|
||||
core.send_ui_message(info.player, data)
|
||||
open_windows[id] = nil
|
||||
end
|
||||
|
||||
function ui.get_window_info(id)
|
||||
local info = open_windows[id]
|
||||
if not info then
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Only return a subset of the fields that are relevant for the caller.
|
||||
return {
|
||||
builder = info.builder,
|
||||
player = info.player,
|
||||
context = info.context,
|
||||
}
|
||||
end
|
||||
|
||||
function ui.get_open_windows()
|
||||
local ids = {}
|
||||
for id in pairs(open_windows) do
|
||||
table.insert(ids, id)
|
||||
end
|
||||
return ids
|
||||
end
|
|
@ -4015,7 +4015,11 @@ Helper functions
|
|||
* `minetest.get_us_time()`
|
||||
* returns time with microsecond precision. May not return wall time.
|
||||
* `table.copy(table)`: returns a table
|
||||
* returns a deep copy of `table`
|
||||
* Returns a deep copy of `table`, i.e. a copy of the table and all its
|
||||
nested tables.
|
||||
* `table.shallow_copy(table)`:
|
||||
* Returns a shallow copy of `table`, i.e. only a copy of the table itself,
|
||||
but not any of the nested tables.
|
||||
* `table.indexof(list, val)`: returns the smallest numerical index containing
|
||||
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
|
||||
|
@ -4023,6 +4027,9 @@ Helper functions
|
|||
* `table.insert_all(table, other_table)`:
|
||||
* Appends all values in `other_table` to `table` - uses `#table + 1` to
|
||||
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
|
||||
* If multiple keys in `t` map to the same value, it is unspecified which
|
||||
value maps to that key.
|
||||
|
@ -5565,6 +5572,9 @@ Utilities
|
|||
* `minetest.colorspec_to_colorstring(colorspec)`: Converts a ColorSpec to a
|
||||
ColorString. If the ColorSpec is invalid, returns `nil`.
|
||||
* `colorspec`: The ColorSpec to convert
|
||||
* `minetest.colorspec_to_colorint(colorspec)`: Converts a ColorSpec to integer
|
||||
form. If the ColorSpec is invalid, returns `nil`.
|
||||
* `colorspec`: The ColorSpec to convert
|
||||
* `minetest.colorspec_to_bytes(colorspec)`: Converts a ColorSpec to a raw
|
||||
string of four bytes in an RGBA layout, returned as a string.
|
||||
* `colorspec`: The ColorSpec to convert
|
||||
|
@ -5573,8 +5583,8 @@ Utilities
|
|||
* `width`: Width of the image
|
||||
* `height`: Height of the image
|
||||
* `data`: Image data, one of:
|
||||
* array table of ColorSpec, length must be width*height
|
||||
* string with raw RGBA pixels, length must be width*height*4
|
||||
* array table of ColorSpec, length must be `width * height`
|
||||
* string with raw RGBA pixels, length must be `width * height * 4`
|
||||
* `compression`: Optional zlib compression level, number in range 0 to 9.
|
||||
The data is one-dimensional, starting in the upper left corner of the image
|
||||
and laid out in scanlines going from left to right, then top to bottom.
|
||||
|
@ -5584,6 +5594,43 @@ Utilities
|
|||
* `minetest.urlencode(str)`: Encodes reserved URI characters by a
|
||||
percent sign followed by two hex digits. See
|
||||
[RFC 3986, section 2.3](https://datatracker.ietf.org/doc/html/rfc3986#section-2.3).
|
||||
* `minetest.class([super])`: Creates a new metatable-based class.
|
||||
* `super` (optional): The superclass (i.e. the metatable) of the newly
|
||||
created class. If nil, an empty table will be used.
|
||||
* Lua metamethods may be added to the class, but they are not automatically
|
||||
inherited. Note that `__index` and `__call` metafields are automatically
|
||||
added to the metatable.
|
||||
* When a new object is constructed, the `new()` method, if present, will be
|
||||
called.
|
||||
* Example: The following code, demonstrating a simple example of classes
|
||||
and inheritance, will print `area=6, filled=true`:
|
||||
```lua
|
||||
local Shape = minetest.class()
|
||||
function Shape:new(filled)
|
||||
self.filled = filled
|
||||
end
|
||||
|
||||
function Shape:describe()
|
||||
return "area=" .. self:get_area() .. ", filled=" .. tostring(self.filled)
|
||||
end
|
||||
|
||||
local Rectangle = minetest.class(Shape)
|
||||
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())
|
||||
```
|
||||
* `minetest.is_instance(obj, class)`: Returns true if and only if `obj` is an
|
||||
instance of `class` or any of its subclasses.
|
||||
|
||||
Logging
|
||||
-------
|
||||
|
@ -7121,6 +7168,50 @@ Misc.
|
|||
* Example: `deserialize('print("foo")')`, returns `nil`
|
||||
(function call fails), returns
|
||||
`error:[string "print("foo")"]:1: attempt to call global 'print' (a nil value)`
|
||||
* `minetest.encode_network(format, ...)`: Encodes numbers and strings in binary
|
||||
format suitable for network transfer according to a format string.
|
||||
* Each character in the format string corresponds to an argument to the
|
||||
function. Possible format characters:
|
||||
* `b`: Signed 8-bit integer
|
||||
* `h`: Signed 16-bit integer
|
||||
* `i`: Signed 32-bit integer
|
||||
* `l`: Signed 64-bit integer
|
||||
* `B`: Unsigned 8-bit integer
|
||||
* `H`: Unsigned 16-bit integer
|
||||
* `I`: Unsigned 32-bit integer
|
||||
* `L`: Unsigned 64-bit integer
|
||||
* `f`: Single-precision floating point number
|
||||
* `s`: 16-bit size-prefixed string. Max 64 KB in size
|
||||
* `S`: 32-bit size-prefixed string. Max 64 MB in size
|
||||
* `z`: Null-terminated string. Cannot have embedded null characters
|
||||
* `Z`: Verbatim string with no size or terminator
|
||||
* ` `: Spaces are ignored
|
||||
* Integers are encoded in big-endian format, and floating point numbers are
|
||||
encoded in IEEE-754 format. Note that the full range of 64-bit integers
|
||||
cannot be represented in Lua's doubles.
|
||||
* If integers outside of the range of the corresponding type are encoded,
|
||||
integer wraparound will occur.
|
||||
* If a string that is too long for a size-prefixed string is encoded, it
|
||||
will be truncated.
|
||||
* If a string with an embedded null character is encoded as a null
|
||||
terminated string, it is truncated to the first null character.
|
||||
* Verbatim strings are added directly to the output as-is and can therefore
|
||||
have any size or contents, but the code on the decoding end cannot
|
||||
automatically detect its length.
|
||||
* `minetest.decode_network(format, data, ...)`: Decodes numbers and strings
|
||||
from a binary format created by `minetest.encode_network()` according to a
|
||||
format string.
|
||||
* The format string follows the same rules as `minetest.encode_network()`.
|
||||
The decoded values are returned as individual values from the function.
|
||||
* `Z` has special behavior; an extra argument has to be passed to the
|
||||
function for every `Z` specifier denoting how many characters to read.
|
||||
To read all remaining characters, use a size of `-1`.
|
||||
* If the end of the data is encountered while still reading values from the
|
||||
string, values of the correct type will still be returned, but strings of
|
||||
variable length will be truncated, and numbers and verbatim strings will
|
||||
use zeros for the missing bytes.
|
||||
* If a size-prefixed string has a size that is greater than the maximum, it
|
||||
will be truncated and the rest of the characters skipped.
|
||||
* `minetest.compress(data, method, ...)`: returns `compressed_data`
|
||||
* Compress a string of data.
|
||||
* `method` is a string identifying the compression method to be used.
|
||||
|
|
|
@ -254,3 +254,202 @@ local function test_gennotify_api()
|
|||
assert(#custom == 0, "custom ids not empty")
|
||||
end
|
||||
unittests.register("test_gennotify_api", test_gennotify_api)
|
||||
|
||||
unittests.register("test_encode_network", function()
|
||||
-- 8-bit integers
|
||||
assert(minetest.encode_network("bbbbbbb", 0, 1, -1, -128, 127, 255, 256) ==
|
||||
"\x00\x01\xFF\x80\x7F\xFF\x00")
|
||||
assert(minetest.encode_network("BBBBBBB", 0, 1, -1, -128, 127, 255, 256) ==
|
||||
"\x00\x01\xFF\x80\x7F\xFF\x00")
|
||||
|
||||
-- 16-bit integers
|
||||
assert(minetest.encode_network("hhhhhhhh",
|
||||
0, 1, 257, -1,
|
||||
-32768, 32767, 65535, 65536) ==
|
||||
"\x00\x00".."\x00\x01".."\x01\x01".."\xFF\xFF"..
|
||||
"\x80\x00".."\x7F\xFF".."\xFF\xFF".."\x00\x00")
|
||||
assert(minetest.encode_network("HHHHHHHH",
|
||||
0, 1, 257, -1,
|
||||
-32768, 32767, 65535, 65536) ==
|
||||
"\x00\x00".."\x00\x01".."\x01\x01".."\xFF\xFF"..
|
||||
"\x80\x00".."\x7F\xFF".."\xFF\xFF".."\x00\x00")
|
||||
|
||||
-- 32-bit integers
|
||||
assert(minetest.encode_network("iiiiiiii",
|
||||
0, 257, 2^24-1, -1,
|
||||
-2^31, 2^31-1, 2^32-1, 2^32) ==
|
||||
"\x00\x00\x00\x00".."\x00\x00\x01\x01".."\x00\xFF\xFF\xFF".."\xFF\xFF\xFF\xFF"..
|
||||
"\x80\x00\x00\x00".."\x7F\xFF\xFF\xFF".."\xFF\xFF\xFF\xFF".."\x00\x00\x00\x00")
|
||||
assert(minetest.encode_network("IIIIIIII",
|
||||
0, 257, 2^24-1, -1,
|
||||
-2^31, 2^31-1, 2^32-1, 2^32) ==
|
||||
"\x00\x00\x00\x00".."\x00\x00\x01\x01".."\x00\xFF\xFF\xFF".."\xFF\xFF\xFF\xFF"..
|
||||
"\x80\x00\x00\x00".."\x7F\xFF\xFF\xFF".."\xFF\xFF\xFF\xFF".."\x00\x00\x00\x00")
|
||||
|
||||
-- 64-bit integers
|
||||
assert(minetest.encode_network("llllll",
|
||||
0, 1,
|
||||
511, -1,
|
||||
2^53-1, -2^53) ==
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00".."\x00\x00\x00\x00\x00\x00\x00\x01"..
|
||||
"\x00\x00\x00\x00\x00\x00\x01\xFF".."\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF"..
|
||||
"\x00\x1F\xFF\xFF\xFF\xFF\xFF\xFF".."\xFF\xE0\x00\x00\x00\x00\x00\x00")
|
||||
assert(minetest.encode_network("LLLLLL",
|
||||
0, 1,
|
||||
511, -1,
|
||||
2^53-1, -2^53) ==
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00".."\x00\x00\x00\x00\x00\x00\x00\x01"..
|
||||
"\x00\x00\x00\x00\x00\x00\x01\xFF".."\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF"..
|
||||
"\x00\x1F\xFF\xFF\xFF\xFF\xFF\xFF".."\xFF\xE0\x00\x00\x00\x00\x00\x00")
|
||||
|
||||
-- Strings
|
||||
local max_16 = string.rep("*", 2^16 - 1)
|
||||
local max_32 = string.rep("*", 2^26)
|
||||
|
||||
assert(minetest.encode_network("ssss",
|
||||
"", "hello",
|
||||
max_16, max_16.."too long") ==
|
||||
"\x00\x00".. "\x00\x05hello"..
|
||||
"\xFF\xFF"..max_16.."\xFF\xFF"..max_16)
|
||||
assert(minetest.encode_network("SSSS",
|
||||
"", "hello",
|
||||
max_32, max_32.."too long") ==
|
||||
"\x00\x00\x00\x00".. "\x00\x00\x00\x05hello"..
|
||||
"\x04\x00\x00\x00"..max_32.."\x04\x00\x00\x00"..max_32)
|
||||
assert(minetest.encode_network("zzzz",
|
||||
"", "hello", "hello\0embedded", max_16.."longer") ==
|
||||
"\0".."hello\0".."hello\0".. max_16.."longer\0")
|
||||
assert(minetest.encode_network("ZZZZ",
|
||||
"", "hello", "hello\0embedded", max_16.."longer") ==
|
||||
"".."hello".."hello\0embedded"..max_16.."longer")
|
||||
|
||||
-- Spaces
|
||||
assert(minetest.encode_network("B I", 255, 2^31) == "\xFF\x80\x00\x00\x00")
|
||||
assert(minetest.encode_network(" B Zz ", 15, "abc", "xyz") == "\x0Fabcxyz\0")
|
||||
|
||||
-- Empty format strings
|
||||
assert(minetest.encode_network("") == "")
|
||||
assert(minetest.encode_network(" ", 5, "extra args") == "")
|
||||
end)
|
||||
|
||||
unittests.register("test_decode_network", function()
|
||||
local d
|
||||
|
||||
-- 8-bit integers
|
||||
d = {minetest.decode_network("bbbbb", "\x00\x01\x7F\x80\xFF")}
|
||||
assert(#d == 5)
|
||||
assert(d[1] == 0 and d[2] == 1 and d[3] == 127 and d[4] == -128 and d[5] == -1)
|
||||
|
||||
d = {minetest.decode_network("BBBBB", "\x00\x01\x7F\x80\xFF")}
|
||||
assert(#d == 5)
|
||||
assert(d[1] == 0 and d[2] == 1 and d[3] == 127 and d[4] == 128 and d[5] == 255)
|
||||
|
||||
-- 16-bit integers
|
||||
d = {minetest.decode_network("hhhhhh",
|
||||
"\x00\x00".."\x00\x01".."\x01\x01"..
|
||||
"\x7F\xFF".."\x80\x00".."\xFF\xFF")}
|
||||
assert(#d == 6)
|
||||
assert(d[1] == 0 and d[2] == 1 and d[3] == 257 and
|
||||
d[4] == 32767 and d[5] == -32768 and d[6] == -1)
|
||||
|
||||
d = {minetest.decode_network("HHHHHH",
|
||||
"\x00\x00".."\x00\x01".."\x01\x01"..
|
||||
"\x7F\xFF".."\x80\x00".."\xFF\xFF")}
|
||||
assert(#d == 6)
|
||||
assert(d[1] == 0 and d[2] == 1 and d[3] == 257 and
|
||||
d[4] == 32767 and d[5] == 32768 and d[6] == 65535)
|
||||
|
||||
-- 32-bit integers
|
||||
d = {minetest.decode_network("iiiiii",
|
||||
"\x00\x00\x00\x00".."\x00\x00\x00\x01".."\x00\xFF\xFF\xFF"..
|
||||
"\x7F\xFF\xFF\xFF".."\x80\x00\x00\x00".."\xFF\xFF\xFF\xFF")}
|
||||
assert(#d == 6)
|
||||
assert(d[1] == 0 and d[2] == 1 and d[3] == 2^24-1 and
|
||||
d[4] == 2^31-1 and d[5] == -2^31 and d[6] == -1)
|
||||
|
||||
d = {minetest.decode_network("IIIIII",
|
||||
"\x00\x00\x00\x00".."\x00\x00\x00\x01".."\x00\xFF\xFF\xFF"..
|
||||
"\x7F\xFF\xFF\xFF".."\x80\x00\x00\x00".."\xFF\xFF\xFF\xFF")}
|
||||
assert(#d == 6)
|
||||
assert(d[1] == 0 and d[2] == 1 and d[3] == 2^24-1 and
|
||||
d[4] == 2^31-1 and d[5] == 2^31 and d[6] == 2^32-1)
|
||||
|
||||
-- 64-bit integers
|
||||
d = {minetest.decode_network("llllll",
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00".."\x00\x00\x00\x00\x00\x00\x00\x01"..
|
||||
"\x00\x00\x00\x00\x00\x00\x01\xFF".."\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF"..
|
||||
"\x00\x1F\xFF\xFF\xFF\xFF\xFF\xFF".."\xFF\xE0\x00\x00\x00\x00\x00\x00")}
|
||||
assert(#d == 6)
|
||||
assert(d[1] == 0 and d[2] == 1 and d[3] == 511 and
|
||||
d[4] == -1 and d[5] == 2^53-1 and d[6] == -2^53)
|
||||
|
||||
d = {minetest.decode_network("LLLLLL",
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00".."\x00\x00\x00\x00\x00\x00\x00\x01"..
|
||||
"\x00\x00\x00\x00\x00\x00\x01\xFF".."\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF"..
|
||||
"\x00\x1F\xFF\xFF\xFF\xFF\xFF\xFF".."\xFF\xE0\x00\x00\x00\x00\x00\x00")}
|
||||
assert(#d == 6)
|
||||
assert(d[1] == 0 and d[2] == 1 and d[3] == 511 and
|
||||
d[4] == 2^64-1 and d[5] == 2^53-1 and d[6] == 2^64 - 2^53)
|
||||
|
||||
-- Floating point numbers
|
||||
local enc = minetest.encode_network("fff",
|
||||
0.0, 123.456, -987.654)
|
||||
assert(#enc == 3 * 4)
|
||||
|
||||
d = {minetest.decode_network("fff", enc)}
|
||||
assert(#d == 3)
|
||||
assert(d[1] == 0.0 and d[2] > 123.45 and d[2] < 123.46 and
|
||||
d[3] > -987.66 and d[3] < -987.65)
|
||||
|
||||
-- Strings
|
||||
local max_16 = string.rep("*", 2^16 - 1)
|
||||
local max_32 = string.rep("*", 2^26)
|
||||
|
||||
d = {minetest.decode_network("ssss",
|
||||
"\x00\x00".."\x00\x05hello".."\xFF\xFF"..max_16.."\x00\xFFtoo short")}
|
||||
assert(#d == 4)
|
||||
assert(d[1] == "" and d[2] == "hello" and d[3] == max_16 and d[4] == "too short")
|
||||
|
||||
d = {minetest.decode_network("SSSSS",
|
||||
"\x00\x00\x00\x00".."\x00\x00\x00\x05hello"..
|
||||
"\x04\x00\x00\x00"..max_32.."\x04\x00\x00\x08"..max_32.."too long"..
|
||||
"\x00\x00\x00\xFFtoo short")}
|
||||
assert(#d == 5)
|
||||
assert(d[1] == "" and d[2] == "hello" and
|
||||
d[3] == max_32 and d[4] == max_32 and d[5] == "too short")
|
||||
|
||||
d = {minetest.decode_network("zzzz", "\0".."hello\0".."missing end")}
|
||||
assert(#d == 4)
|
||||
assert(d[1] == "" and d[2] == "hello" and d[3] == "missing end" and d[4] == "")
|
||||
|
||||
-- Verbatim strings
|
||||
d = {minetest.decode_network("ZZZZ", "xxxyyyyyzzz", 3, 0, 5, -1)}
|
||||
assert(#d == 4)
|
||||
assert(d[1] == "xxx" and d[2] == "" and d[3] == "yyyyy" and d[4] == "zzz")
|
||||
|
||||
-- Read past end
|
||||
d = {minetest.decode_network("bhilBHILf", "")}
|
||||
assert(#d == 9)
|
||||
assert(d[1] == 0 and d[2] == 0 and d[3] == 0 and d[4] == 0 and
|
||||
d[5] == 0 and d[6] == 0 and d[7] == 0 and d[8] == 0 and d[9] == 0.0)
|
||||
|
||||
d = {minetest.decode_network("ZsSzZ", "xx", 4, 4)}
|
||||
assert(#d == 5)
|
||||
assert(d[1] == "xx\0\0" and d[2] == "" and d[3] == "" and
|
||||
d[4] == "" and d[5] == "\0\0\0\0")
|
||||
|
||||
-- Spaces
|
||||
d = {minetest.decode_network("B I", "\xFF\x80\x00\x00\x00")}
|
||||
assert(#d == 2)
|
||||
assert(d[1] == 255 and d[2] == 2^31)
|
||||
|
||||
d = {minetest.decode_network(" B Zz ", "\x0Fabcxyz\0", 3)}
|
||||
assert(#d == 3)
|
||||
assert(d[1] == 15 and d[2] == "abc" and d[3] == "xyz")
|
||||
|
||||
-- Empty format strings
|
||||
d = {minetest.decode_network("", "some random data")}
|
||||
assert(#d == 0)
|
||||
d = {minetest.decode_network(" ", "some random data", 3, 5)}
|
||||
assert(#d == 0)
|
||||
end)
|
||||
|
|
|
@ -207,6 +207,7 @@ public:
|
|||
void handleCommand_InventoryFormSpec(NetworkPacket* pkt);
|
||||
void handleCommand_DetachedInventory(NetworkPacket* pkt);
|
||||
void handleCommand_ShowFormSpec(NetworkPacket* pkt);
|
||||
void handleCommand_UiMessage(NetworkPacket* pkt);
|
||||
void handleCommand_SpawnParticle(NetworkPacket* pkt);
|
||||
void handleCommand_AddParticleSpawner(NetworkPacket* pkt);
|
||||
void handleCommand_DeleteParticleSpawner(NetworkPacket* pkt);
|
||||
|
|
|
@ -37,6 +37,7 @@ enum ClientEventType : u8
|
|||
CE_DEATHSCREEN,
|
||||
CE_SHOW_FORMSPEC,
|
||||
CE_SHOW_LOCAL_FORMSPEC,
|
||||
CE_UI_MESSAGE,
|
||||
CE_SPAWN_PARTICLE,
|
||||
CE_ADD_PARTICLESPAWNER,
|
||||
CE_DELETE_PARTICLESPAWNER,
|
||||
|
@ -106,6 +107,10 @@ struct ClientEvent
|
|||
std::string *formspec;
|
||||
std::string *formname;
|
||||
} show_formspec;
|
||||
struct
|
||||
{
|
||||
std::string *data;
|
||||
} ui_message;
|
||||
// struct{
|
||||
//} textures_updated;
|
||||
ParticleParameters *spawn_particle;
|
||||
|
|
|
@ -53,6 +53,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
#include "gui/guiOpenURL.h"
|
||||
#include "gui/guiVolumeChange.h"
|
||||
#include "gui/mainmenumanager.h"
|
||||
#include "gui/manager.h"
|
||||
#include "gui/profilergraph.h"
|
||||
#include "mapblock.h"
|
||||
#include "minimap.h"
|
||||
|
@ -835,6 +836,7 @@ private:
|
|||
void handleClientEvent_Deathscreen(ClientEvent *event, CameraOrientation *cam);
|
||||
void handleClientEvent_ShowFormSpec(ClientEvent *event, CameraOrientation *cam);
|
||||
void handleClientEvent_ShowLocalFormSpec(ClientEvent *event, CameraOrientation *cam);
|
||||
void handleClientEvent_UiMessage(ClientEvent *event, CameraOrientation *cam);
|
||||
void handleClientEvent_HandleParticleEvent(ClientEvent *event,
|
||||
CameraOrientation *cam);
|
||||
void handleClientEvent_HudAdd(ClientEvent *event, CameraOrientation *cam);
|
||||
|
@ -883,6 +885,7 @@ private:
|
|||
|
||||
std::unique_ptr<GameUI> m_game_ui;
|
||||
GUIChatConsole *gui_chat_console = nullptr; // Free using ->Drop()
|
||||
ui::GUIManagerElem *gui_manager_elem = nullptr; // Free using ->Drop()
|
||||
MapDrawControl *draw_control = nullptr;
|
||||
Camera *camera = nullptr;
|
||||
Clouds *clouds = nullptr; // Free using ->Drop()
|
||||
|
@ -1224,6 +1227,8 @@ void Game::shutdown()
|
|||
if (formspec)
|
||||
formspec->quitMenu();
|
||||
|
||||
ui::g_manager.reset();
|
||||
|
||||
// Clear text when exiting.
|
||||
m_game_ui->clearText();
|
||||
|
||||
|
@ -1237,6 +1242,8 @@ void Game::shutdown()
|
|||
|
||||
if (gui_chat_console)
|
||||
gui_chat_console->drop();
|
||||
if (gui_manager_elem)
|
||||
gui_manager_elem->drop();
|
||||
|
||||
if (sky)
|
||||
sky->drop();
|
||||
|
@ -1538,6 +1545,8 @@ bool Game::createClient(const GameStartData &start_data)
|
|||
if (mapper && client->modsLoaded())
|
||||
client->getScript()->on_minimap_ready(mapper);
|
||||
|
||||
ui::g_manager.setClient(client);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -1555,6 +1564,9 @@ bool Game::initGui()
|
|||
gui_chat_console = new GUIChatConsole(guienv, guienv->getRootGUIElement(),
|
||||
-1, chat_backend, client, &g_menumgr);
|
||||
|
||||
// Thingy to draw UI manager after chat but before formspecs.
|
||||
gui_manager_elem = new ui::GUIManagerElem(guienv, guiroot, -1);
|
||||
|
||||
if (g_touchscreengui)
|
||||
g_touchscreengui->init(texture_src);
|
||||
|
||||
|
@ -2793,6 +2805,7 @@ const ClientEventHandler Game::clientEventHandler[CLIENTEVENT_MAX] = {
|
|||
{&Game::handleClientEvent_Deathscreen},
|
||||
{&Game::handleClientEvent_ShowFormSpec},
|
||||
{&Game::handleClientEvent_ShowLocalFormSpec},
|
||||
{&Game::handleClientEvent_UiMessage},
|
||||
{&Game::handleClientEvent_HandleParticleEvent},
|
||||
{&Game::handleClientEvent_HandleParticleEvent},
|
||||
{&Game::handleClientEvent_HandleParticleEvent},
|
||||
|
@ -2898,6 +2911,12 @@ void Game::handleClientEvent_ShowLocalFormSpec(ClientEvent *event, CameraOrienta
|
|||
delete event->show_formspec.formname;
|
||||
}
|
||||
|
||||
void Game::handleClientEvent_UiMessage(ClientEvent *event, CameraOrientation *cam)
|
||||
{
|
||||
ui::g_manager.receiveMessage(*event->ui_message.data);
|
||||
delete event->ui_message.data;
|
||||
}
|
||||
|
||||
void Game::handleClientEvent_HandleParticleEvent(ClientEvent *event,
|
||||
CameraOrientation *cam)
|
||||
{
|
||||
|
@ -4310,7 +4329,7 @@ void Game::drawScene(ProfilerGraph *graph, RunStats *stats)
|
|||
draw_crosshair = false;
|
||||
|
||||
this->m_rendering_engine->draw_scene(sky_color, this->m_game_ui->m_flags.show_hud,
|
||||
draw_wield_tool, draw_crosshair);
|
||||
this->m_game_ui->m_flags.show_chat, draw_wield_tool, draw_crosshair);
|
||||
|
||||
/*
|
||||
Profiler graph
|
||||
|
|
|
@ -36,7 +36,7 @@ RenderingCore::~RenderingCore()
|
|||
delete shadow_renderer;
|
||||
}
|
||||
|
||||
void RenderingCore::draw(video::SColor _skycolor, bool _show_hud,
|
||||
void RenderingCore::draw(video::SColor _skycolor, bool _show_hud, bool _show_chat,
|
||||
bool _draw_wield_tool, bool _draw_crosshair)
|
||||
{
|
||||
v2u32 screensize = device->getVideoDriver()->getScreenSize();
|
||||
|
@ -46,6 +46,7 @@ void RenderingCore::draw(video::SColor _skycolor, bool _show_hud,
|
|||
context.draw_crosshair = _draw_crosshair;
|
||||
context.draw_wield_tool = _draw_wield_tool;
|
||||
context.show_hud = _show_hud;
|
||||
context.show_chat = _show_chat;
|
||||
|
||||
pipeline->reset(context);
|
||||
pipeline->run(context);
|
||||
|
|
|
@ -53,7 +53,7 @@ public:
|
|||
RenderingCore &operator=(const RenderingCore &) = delete;
|
||||
RenderingCore &operator=(RenderingCore &&) = delete;
|
||||
|
||||
void draw(video::SColor _skycolor, bool _show_hud,
|
||||
void draw(video::SColor _skycolor, bool _show_hud, bool _show_chat,
|
||||
bool _draw_wield_tool, bool _draw_crosshair);
|
||||
|
||||
v2u32 getVirtualSize() const;
|
||||
|
|
|
@ -46,6 +46,7 @@ struct PipelineContext
|
|||
v2u32 target_size;
|
||||
|
||||
bool show_hud {true};
|
||||
bool show_chat {true};
|
||||
bool draw_wield_tool {true};
|
||||
bool draw_crosshair {true};
|
||||
};
|
||||
|
|
|
@ -26,6 +26,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
#include "client/hud.h"
|
||||
#include "client/minimap.h"
|
||||
#include "client/shadows/dynamicshadowsrender.h"
|
||||
#include "gui/manager.h"
|
||||
|
||||
/// Draw3D pipeline step
|
||||
void Draw3D::run(PipelineContext &context)
|
||||
|
@ -43,6 +44,9 @@ void Draw3D::run(PipelineContext &context)
|
|||
|
||||
void DrawWield::run(PipelineContext &context)
|
||||
{
|
||||
ui::g_manager.preDraw();
|
||||
ui::g_manager.drawType(ui::WindowType::BG);
|
||||
|
||||
if (m_target)
|
||||
m_target->activate(context);
|
||||
|
||||
|
@ -60,12 +64,24 @@ void DrawHUD::run(PipelineContext &context)
|
|||
|
||||
if (context.draw_crosshair)
|
||||
context.hud->drawCrosshair();
|
||||
}
|
||||
|
||||
ui::g_manager.drawType(ui::WindowType::MASK);
|
||||
|
||||
if (context.show_hud) {
|
||||
context.hud->drawHotbar(context.client->getEnv().getLocalPlayer()->getWieldIndex());
|
||||
|
||||
context.hud->drawLuaElements(context.client->getCamera()->getOffset());
|
||||
ui::g_manager.drawType(ui::WindowType::HUD);
|
||||
|
||||
context.client->getCamera()->drawNametags();
|
||||
}
|
||||
|
||||
if (context.show_chat)
|
||||
ui::g_manager.drawType(ui::WindowType::MESSAGE);
|
||||
|
||||
context.device->getGUIEnvironment()->drawAll();
|
||||
ui::g_manager.drawType(ui::WindowType::FG);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -411,10 +411,10 @@ void RenderingEngine::finalize()
|
|||
core.reset();
|
||||
}
|
||||
|
||||
void RenderingEngine::draw_scene(video::SColor skycolor, bool show_hud,
|
||||
void RenderingEngine::draw_scene(video::SColor skycolor, bool show_hud, bool show_chat,
|
||||
bool draw_wield_tool, bool draw_crosshair)
|
||||
{
|
||||
core->draw(skycolor, show_hud, draw_wield_tool, draw_crosshair);
|
||||
core->draw(skycolor, show_hud, show_chat, draw_wield_tool, draw_crosshair);
|
||||
}
|
||||
|
||||
const VideoDriverInfo &RenderingEngine::getVideoDriverInfo(irr::video::E_DRIVER_TYPE type)
|
||||
|
|
|
@ -141,7 +141,7 @@ public:
|
|||
gui::IGUIEnvironment *guienv, ITextureSource *tsrc,
|
||||
float dtime = 0, int percent = 0, bool sky = true);
|
||||
|
||||
void draw_scene(video::SColor skycolor, bool show_hud,
|
||||
void draw_scene(video::SColor skycolor, bool show_hud, bool show_chat,
|
||||
bool draw_wield_tool, bool draw_crosshair);
|
||||
|
||||
void initialize(Client *client, Hud *hud);
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
set(gui_SRCS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/box.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/elem.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/generic_elems.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/guiAnimatedImage.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/guiBackgroundImage.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/guiBox.cpp
|
||||
|
@ -23,8 +26,11 @@ set(gui_SRCS
|
|||
${CMAKE_CURRENT_SOURCE_DIR}/guiTable.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/guiHyperText.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/guiVolumeChange.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/manager.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modalMenu.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/profilergraph.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/texture.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/touchscreengui.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/window.cpp
|
||||
PARENT_SCOPE
|
||||
)
|
||||
|
|
|
@ -0,0 +1,452 @@
|
|||
/*
|
||||
Minetest
|
||||
Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU Lesser General Public License as published by
|
||||
the Free Software Foundation; either version 2.1 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#include "gui/box.h"
|
||||
|
||||
#include "debug.h"
|
||||
#include "log.h"
|
||||
#include "porting.h"
|
||||
#include "gui/elem.h"
|
||||
#include "gui/manager.h"
|
||||
#include "gui/window.h"
|
||||
#include "util/serialize.h"
|
||||
|
||||
namespace ui
|
||||
{
|
||||
Align toAlign(u8 align)
|
||||
{
|
||||
if (align >= (u8)Align::MAX_ALIGN) {
|
||||
return Align::CENTER;
|
||||
}
|
||||
return (Align)align;
|
||||
}
|
||||
|
||||
void Layer::reset()
|
||||
{
|
||||
image = Texture();
|
||||
fill = BLANK;
|
||||
tint = WHITE;
|
||||
|
||||
source = rf32(0.0f, 0.0f, 1.0f, 1.0f);
|
||||
middle = rf32(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
middle_scale = 1.0f;
|
||||
|
||||
num_frames = 1;
|
||||
frame_time = 1000;
|
||||
}
|
||||
|
||||
void Layer::read(std::istream &full_is)
|
||||
{
|
||||
auto is = newIs(readStr16(full_is));
|
||||
u32 set_mask = readU32(is);
|
||||
|
||||
if (testShift(set_mask))
|
||||
image = g_manager.getTexture(readNullStr(is));
|
||||
if (testShift(set_mask))
|
||||
fill = readARGB8(is);
|
||||
if (testShift(set_mask))
|
||||
tint = readARGB8(is);
|
||||
|
||||
if (testShift(set_mask)) {
|
||||
source.UpperLeftCorner = readV2F32(is);
|
||||
source.LowerRightCorner = readV2F32(is);
|
||||
}
|
||||
if (testShift(set_mask)) {
|
||||
middle.UpperLeftCorner = clamp_vec(readV2F32(is));
|
||||
middle.LowerRightCorner = clamp_vec(readV2F32(is));
|
||||
}
|
||||
if (testShift(set_mask))
|
||||
middle_scale = std::max(readF32(is), 0.0f);
|
||||
|
||||
if (testShift(set_mask))
|
||||
num_frames = std::max(readU32(is), 1U);
|
||||
if (testShift(set_mask))
|
||||
frame_time = std::max(readU32(is), 1U);
|
||||
}
|
||||
|
||||
void Style::reset()
|
||||
{
|
||||
size = d2f32(0.0f, 0.0f);
|
||||
|
||||
rel_pos = v2f32(0.0f, 0.0f);
|
||||
rel_anchor = v2f32(0.0f, 0.0f);
|
||||
rel_size = d2s32(1.0f, 1.0f);
|
||||
|
||||
margin = rf32(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
padding = rf32(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
|
||||
bg.reset();
|
||||
fg.reset();
|
||||
|
||||
fg_scale = 1.0f;
|
||||
fg_halign = Align::CENTER;
|
||||
fg_valign = Align::CENTER;
|
||||
|
||||
visible = true;
|
||||
noclip = false;
|
||||
}
|
||||
|
||||
void Style::read(std::istream &is)
|
||||
{
|
||||
// No need to read a size prefix; styles are already read in as size-
|
||||
// prefixed strings in Window.
|
||||
u32 set_mask = readU32(is);
|
||||
|
||||
if (testShift(set_mask))
|
||||
size = clamp_vec(readV2F32(is));
|
||||
|
||||
if (testShift(set_mask))
|
||||
rel_pos = readV2F32(is);
|
||||
if (testShift(set_mask))
|
||||
rel_anchor = readV2F32(is);
|
||||
if (testShift(set_mask))
|
||||
rel_size = clamp_vec(readV2F32(is));
|
||||
|
||||
if (testShift(set_mask)) {
|
||||
margin.UpperLeftCorner = readV2F32(is);
|
||||
margin.LowerRightCorner = readV2F32(is);
|
||||
}
|
||||
if (testShift(set_mask)) {
|
||||
padding.UpperLeftCorner = readV2F32(is);
|
||||
padding.LowerRightCorner = readV2F32(is);
|
||||
}
|
||||
|
||||
if (testShift(set_mask))
|
||||
bg.read(is);
|
||||
if (testShift(set_mask))
|
||||
fg.read(is);
|
||||
|
||||
if (testShift(set_mask))
|
||||
fg_scale = std::max(readF32(is), 0.0f);
|
||||
if (testShift(set_mask))
|
||||
fg_halign = toAlign(readU8(is));
|
||||
if (testShift(set_mask))
|
||||
fg_valign = toAlign(readU8(is));
|
||||
|
||||
if (testShift(set_mask))
|
||||
visible = testShift(set_mask);
|
||||
if (testShift(set_mask))
|
||||
noclip = testShift(set_mask);
|
||||
}
|
||||
|
||||
Window &Box::getWindow()
|
||||
{
|
||||
return m_elem.getWindow();
|
||||
}
|
||||
|
||||
const Window &Box::getWindow() const
|
||||
{
|
||||
return m_elem.getWindow();
|
||||
}
|
||||
|
||||
void Box::reset()
|
||||
{
|
||||
m_style.reset();
|
||||
|
||||
for (State i = 0; i < m_style_refs.size(); i++) {
|
||||
m_style_refs[i] = NO_STYLE;
|
||||
}
|
||||
|
||||
m_draw_rect = rf32(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
m_child_rect = m_draw_rect;
|
||||
m_clip_rect = m_draw_rect;
|
||||
}
|
||||
|
||||
void Box::read(std::istream &full_is)
|
||||
{
|
||||
auto is = newIs(readStr16(full_is));
|
||||
u32 style_mask = readU32(is);
|
||||
|
||||
for (State i = 0; i < m_style_refs.size(); i++) {
|
||||
// If we have a style for this state in the mask, add it to the
|
||||
// list of styles.
|
||||
if (!testShift(style_mask)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
u32 style = readU32(is);
|
||||
if (getWindow().getStyleStr(style) != nullptr) {
|
||||
m_style_refs[i] = style;
|
||||
} else {
|
||||
errorstream << "Style " << style << " does not exist" << std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Box::layout(const rf32 &parent_rect, const rf32 &parent_clip)
|
||||
{
|
||||
// Before we layout the box, we need to recompute the style so we have
|
||||
// fully updated style properties.
|
||||
computeStyle();
|
||||
|
||||
// First, calculate the size of the box in absolute coordinates based
|
||||
// on the normalized size.
|
||||
d2f32 origin_size(
|
||||
(m_style.rel_size.Width * parent_rect.getWidth()),
|
||||
(m_style.rel_size.Height * parent_rect.getHeight())
|
||||
);
|
||||
|
||||
// Ensure that the normalized size of the box isn't smaller than the
|
||||
// minimum size.
|
||||
origin_size.Width = std::max(origin_size.Width, m_style.size.Width);
|
||||
origin_size.Height = std::max(origin_size.Height, m_style.size.Height);
|
||||
|
||||
// Then, create the rect of the box relative to the origin by
|
||||
// converting the normalized position absolute coordinates, while
|
||||
// accounting for the anchor based on the previously calculated size.
|
||||
v2f32 origin_pos(
|
||||
(m_style.rel_pos.X * parent_rect.getWidth()) -
|
||||
(m_style.rel_anchor.X * origin_size.Width),
|
||||
(m_style.rel_pos.Y * parent_rect.getHeight()) -
|
||||
(m_style.rel_anchor.Y * origin_size.Height)
|
||||
);
|
||||
|
||||
rf32 origin_rect(origin_pos, origin_size);
|
||||
|
||||
// The absolute rect of the box is made by shifting the origin to the
|
||||
// top left of the parent rect.
|
||||
rf32 abs_rect = origin_rect + parent_rect.UpperLeftCorner;
|
||||
|
||||
// The rect we draw to is the absolute rect adjusted for the margins.
|
||||
// Since this is the final rect, we ensure that it doesn't have a
|
||||
// negative size.
|
||||
m_draw_rect = clamp_rect(rf32(
|
||||
abs_rect.UpperLeftCorner + m_style.margin.UpperLeftCorner,
|
||||
abs_rect.LowerRightCorner - m_style.margin.LowerRightCorner
|
||||
));
|
||||
|
||||
// The rect that children and the foreground layer are drawn relative
|
||||
// to is the draw rect adjusted for padding. Make sure this rect is
|
||||
// never negative as well.
|
||||
m_child_rect = clamp_rect(rf32(
|
||||
m_draw_rect.UpperLeftCorner + m_style.padding.UpperLeftCorner,
|
||||
m_draw_rect.LowerRightCorner - m_style.padding.LowerRightCorner
|
||||
));
|
||||
|
||||
// If we are set to noclip, we clip to the same rect we draw to.
|
||||
// Otherwise, the clip rect is the drawing rect clipped against the
|
||||
// parent clip rect.
|
||||
m_clip_rect = m_style.noclip ? m_draw_rect : clip_rect(m_draw_rect, parent_clip);
|
||||
}
|
||||
|
||||
void Box::draw(Canvas &parent)
|
||||
{
|
||||
// Since layout() is always called before draw(), we already have fully
|
||||
// updated style properties.
|
||||
|
||||
// Don't draw anything if we aren't visible.
|
||||
if (!m_style.visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a new canvas relative to our parent to draw to.
|
||||
Canvas canvas(parent, m_style.noclip ? nullptr : &m_clip_rect);
|
||||
|
||||
// Draw our background and foreground layers.
|
||||
drawLayer(canvas, m_style.bg, m_draw_rect);
|
||||
drawForeground(canvas);
|
||||
}
|
||||
|
||||
void Box::drawForeground(Canvas &canvas)
|
||||
{
|
||||
// It makes no sense to draw a foreground when there's no image, since
|
||||
// it would otherwise take no room.
|
||||
if (!m_style.fg.image.isTexture()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The foreground layer is aligned and scaled in a particular area of
|
||||
// the box. First, get the size of the foreground layer.
|
||||
d2f32 src_size = m_style.fg.source.getSize();
|
||||
src_size.Height /= m_style.fg.num_frames;
|
||||
|
||||
d2s32 tex_size = m_style.fg.image.getSize();
|
||||
src_size.Width *= tex_size.Width;
|
||||
src_size.Height *= tex_size.Height;
|
||||
|
||||
// Then, compute the scale that we should use. A scale of zero means
|
||||
// the image should take up as much room as possible while still
|
||||
// preserving the aspect ratio of the image.
|
||||
float scale = m_style.fg_scale;
|
||||
|
||||
if (scale == 0.0f) {
|
||||
scale = std::min(
|
||||
m_child_rect.getWidth() / src_size.Width,
|
||||
m_child_rect.getHeight() / src_size.Height
|
||||
);
|
||||
}
|
||||
|
||||
d2f32 fg_size(src_size.Width * scale, src_size.Height * scale);
|
||||
|
||||
// Now, using the alignment options, position the foreground image
|
||||
// inside the remaining space.
|
||||
v2f32 fg_pos = m_child_rect.UpperLeftCorner;
|
||||
|
||||
if (m_style.fg_halign == Align::CENTER) {
|
||||
fg_pos.X += (m_child_rect.getWidth() - fg_size.Width) / 2.0f;
|
||||
} else if (m_style.fg_halign == Align::END) {
|
||||
fg_pos.X += m_child_rect.getWidth() - fg_size.Width;
|
||||
}
|
||||
|
||||
if (m_style.fg_valign == Align::CENTER) {
|
||||
fg_pos.Y += (m_child_rect.getHeight() - fg_size.Height) / 2.0f;
|
||||
} else if (m_style.fg_valign == Align::END) {
|
||||
fg_pos.Y += m_child_rect.getHeight() - fg_size.Height;
|
||||
}
|
||||
|
||||
// We have our position and size, so now we can draw the layer.
|
||||
drawLayer(canvas, m_style.fg, rf32(fg_pos, fg_size));
|
||||
}
|
||||
|
||||
void Box::drawLayer(Canvas &canvas, const Layer &layer, const rf32 &dst)
|
||||
{
|
||||
// Draw the fill color if it's not totally transparent.
|
||||
if (layer.fill.getAlpha() != 0x0) {
|
||||
canvas.drawRect(dst, layer.fill);
|
||||
}
|
||||
|
||||
// If there's no image, there's nothing else for us to do.
|
||||
if (!layer.image.isTexture()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If we have animations, we need to adjust the source rect by the
|
||||
// frame offset in accordance with the current frame.
|
||||
rf32 src = layer.source;
|
||||
|
||||
if (layer.num_frames > 1) {
|
||||
float frame_height = src.getHeight() / layer.num_frames;
|
||||
src.LowerRightCorner.Y = src.UpperLeftCorner.Y + frame_height;
|
||||
|
||||
float frame_offset = frame_height *
|
||||
((porting::getTimeMs() / layer.frame_time) % layer.num_frames);
|
||||
src.UpperLeftCorner.Y += frame_offset;
|
||||
src.LowerRightCorner.Y += frame_offset;
|
||||
}
|
||||
|
||||
// If the source rect for this image is flipped, we need to flip the
|
||||
// sign of our middle rect as well to get the right adjustments.
|
||||
rf32 src_middle = layer.middle;
|
||||
|
||||
if (src.getWidth() < 0.0f) {
|
||||
src_middle.UpperLeftCorner.X = -src_middle.UpperLeftCorner.X;
|
||||
src_middle.LowerRightCorner.X = -src_middle.LowerRightCorner.X;
|
||||
}
|
||||
if (src.getHeight() < 0.0f) {
|
||||
src_middle.UpperLeftCorner.Y = -src_middle.UpperLeftCorner.Y;
|
||||
src_middle.LowerRightCorner.Y = -src_middle.LowerRightCorner.Y;
|
||||
}
|
||||
|
||||
// Now we need to draw the texture as a nine-slice image. But first,
|
||||
// since the middle rect uses normalized coordinates, we need to
|
||||
// de-normalize it into actual pixels for the destination rect and
|
||||
// scale it by the middle rect scaling parameter.
|
||||
rf32 scaled_middle(
|
||||
layer.middle.UpperLeftCorner.X * layer.middle_scale * layer.image.getWidth(),
|
||||
layer.middle.UpperLeftCorner.Y * layer.middle_scale * layer.image.getHeight(),
|
||||
layer.middle.LowerRightCorner.X * layer.middle_scale * layer.image.getWidth(),
|
||||
layer.middle.LowerRightCorner.Y * layer.middle_scale * layer.image.getHeight()
|
||||
);
|
||||
|
||||
// Now draw each slice of the nine-slice image. If the middle rect
|
||||
// equals the whole source rect, this will automatically act like a
|
||||
// normal image.
|
||||
for (int y = 0; y < 3; y++) {
|
||||
for (int x = 0; x < 3; x++) {
|
||||
rf32 slice_src = src;
|
||||
rf32 slice_dst = dst;
|
||||
|
||||
switch (x) {
|
||||
case 0:
|
||||
slice_dst.LowerRightCorner.X =
|
||||
dst.UpperLeftCorner.X + scaled_middle.UpperLeftCorner.X;
|
||||
slice_src.LowerRightCorner.X =
|
||||
src.UpperLeftCorner.X + src_middle.UpperLeftCorner.X;
|
||||
break;
|
||||
|
||||
case 1:
|
||||
slice_dst.UpperLeftCorner.X += scaled_middle.UpperLeftCorner.X;
|
||||
slice_dst.LowerRightCorner.X -= scaled_middle.LowerRightCorner.X;
|
||||
slice_src.UpperLeftCorner.X += src_middle.UpperLeftCorner.X;
|
||||
slice_src.LowerRightCorner.X -= src_middle.LowerRightCorner.X;
|
||||
break;
|
||||
|
||||
case 2:
|
||||
slice_dst.UpperLeftCorner.X =
|
||||
dst.LowerRightCorner.X - scaled_middle.LowerRightCorner.X;
|
||||
slice_src.UpperLeftCorner.X =
|
||||
src.LowerRightCorner.X - src_middle.LowerRightCorner.X;
|
||||
break;
|
||||
}
|
||||
|
||||
switch (y) {
|
||||
case 0:
|
||||
slice_dst.LowerRightCorner.Y =
|
||||
dst.UpperLeftCorner.Y + scaled_middle.UpperLeftCorner.Y;
|
||||
slice_src.LowerRightCorner.Y =
|
||||
src.UpperLeftCorner.Y + src_middle.UpperLeftCorner.Y;
|
||||
break;
|
||||
|
||||
case 1:
|
||||
slice_dst.UpperLeftCorner.Y += scaled_middle.UpperLeftCorner.Y;
|
||||
slice_dst.LowerRightCorner.Y -= scaled_middle.LowerRightCorner.Y;
|
||||
slice_src.UpperLeftCorner.Y += src_middle.UpperLeftCorner.Y;
|
||||
slice_src.LowerRightCorner.Y -= src_middle.LowerRightCorner.Y;
|
||||
break;
|
||||
|
||||
case 2:
|
||||
slice_dst.UpperLeftCorner.Y =
|
||||
dst.LowerRightCorner.Y - scaled_middle.LowerRightCorner.Y;
|
||||
slice_src.UpperLeftCorner.Y =
|
||||
src.LowerRightCorner.Y - src_middle.LowerRightCorner.Y;
|
||||
break;
|
||||
}
|
||||
|
||||
// Draw this slice of the texture with the proper tint.
|
||||
canvas.drawTexture(slice_dst, layer.image, slice_src, layer.tint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Box::computeStyle()
|
||||
{
|
||||
// First, clear our current style and compute what state we're in.
|
||||
m_style.reset();
|
||||
State state = STATE_NONE;
|
||||
|
||||
// Loop over each style state from lowest precedence to highest since
|
||||
// they should be applied in that order.
|
||||
for (State i = 0; i < m_style_refs.size(); i++) {
|
||||
// If this state we're looking at is a subset of the current state,
|
||||
// then it's a match for styling.
|
||||
if ((state & i) != i) {
|
||||
continue;
|
||||
}
|
||||
|
||||
u32 index = m_style_refs[i];
|
||||
|
||||
// If the index for this state has an associated style string,
|
||||
// apply it to our current style.
|
||||
if (index != NO_STYLE) {
|
||||
auto is = newIs(*getWindow().getStyleStr(index));
|
||||
m_style.read(is);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,163 @@
|
|||
/*
|
||||
Minetest
|
||||
Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU Lesser General Public License as published by
|
||||
the Free Software Foundation; either version 2.1 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "irrlichttypes_extrabloated.h"
|
||||
#include "gui/texture.h"
|
||||
#include "util/basic_macros.h"
|
||||
|
||||
#include <array>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
namespace ui
|
||||
{
|
||||
// Serialized enum; do not change order of entries.
|
||||
enum class Align
|
||||
{
|
||||
START,
|
||||
CENTER,
|
||||
END,
|
||||
|
||||
MAX_ALIGN,
|
||||
};
|
||||
|
||||
Align toAlign(u8 align);
|
||||
|
||||
struct Layer
|
||||
{
|
||||
Texture image;
|
||||
video::SColor fill;
|
||||
video::SColor tint;
|
||||
|
||||
rf32 source;
|
||||
rf32 middle;
|
||||
float middle_scale;
|
||||
|
||||
u32 num_frames;
|
||||
u32 frame_time;
|
||||
|
||||
Layer()
|
||||
{
|
||||
reset();
|
||||
}
|
||||
|
||||
void reset();
|
||||
void read(std::istream &is);
|
||||
};
|
||||
|
||||
struct Style
|
||||
{
|
||||
d2f32 size;
|
||||
|
||||
v2f32 rel_pos;
|
||||
v2f32 rel_anchor;
|
||||
d2f32 rel_size;
|
||||
|
||||
rf32 margin;
|
||||
rf32 padding;
|
||||
|
||||
Layer bg;
|
||||
Layer fg;
|
||||
|
||||
float fg_scale;
|
||||
Align fg_halign;
|
||||
Align fg_valign;
|
||||
|
||||
bool visible;
|
||||
bool noclip;
|
||||
|
||||
Style()
|
||||
{
|
||||
reset();
|
||||
}
|
||||
|
||||
void reset();
|
||||
void read(std::istream &is);
|
||||
};
|
||||
|
||||
class Elem;
|
||||
class Window;
|
||||
|
||||
class Box
|
||||
{
|
||||
public:
|
||||
using State = u32;
|
||||
|
||||
// These states are organized in order of precedence. States with a
|
||||
// larger value will override the styles of states with a lower value.
|
||||
static constexpr State STATE_NONE = 0;
|
||||
|
||||
static constexpr State STATE_FOCUSED = 1 << 0;
|
||||
static constexpr State STATE_SELECTED = 1 << 1;
|
||||
static constexpr State STATE_HOVERED = 1 << 2;
|
||||
static constexpr State STATE_PRESSED = 1 << 3;
|
||||
static constexpr State STATE_DISABLED = 1 << 4;
|
||||
|
||||
static constexpr State NUM_STATES = 1 << 5;
|
||||
|
||||
private:
|
||||
// Indicates that there is no style string for this state combination.
|
||||
static constexpr u32 NO_STYLE = -1;
|
||||
|
||||
Elem &m_elem;
|
||||
|
||||
Style m_style;
|
||||
std::array<u32, NUM_STATES> m_style_refs;
|
||||
|
||||
rf32 m_draw_rect;
|
||||
rf32 m_child_rect;
|
||||
rf32 m_clip_rect;
|
||||
|
||||
public:
|
||||
Box(Elem &elem) :
|
||||
m_elem(elem)
|
||||
{
|
||||
reset();
|
||||
}
|
||||
|
||||
DISABLE_CLASS_COPY(Box)
|
||||
ALLOW_CLASS_MOVE(Box)
|
||||
|
||||
Elem &getElem() { return m_elem; }
|
||||
const Elem &getElem() const { return m_elem; }
|
||||
|
||||
Window &getWindow();
|
||||
const Window &getWindow() const;
|
||||
|
||||
const Style &getStyle() const { return m_style; }
|
||||
|
||||
const rf32 &getDrawRect() const { return m_draw_rect; }
|
||||
const rf32 &getChildRect() const { return m_child_rect; }
|
||||
const rf32 &getChildClip() const { return m_clip_rect; }
|
||||
|
||||
void reset();
|
||||
void read(std::istream &is);
|
||||
|
||||
void layout(const rf32 &parent_rect, const rf32 &parent_clip);
|
||||
void draw(Canvas &parent);
|
||||
|
||||
private:
|
||||
void drawForeground(Canvas &canvas);
|
||||
void drawLayer(Canvas &canvas, const Layer &layer, const rf32 &dst);
|
||||
|
||||
void computeStyle();
|
||||
};
|
||||
}
|
|
@ -0,0 +1,147 @@
|
|||
/*
|
||||
Minetest
|
||||
Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU Lesser General Public License as published by
|
||||
the Free Software Foundation; either version 2.1 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#include "gui/elem.h"
|
||||
|
||||
#include "debug.h"
|
||||
#include "log.h"
|
||||
#include "gui/manager.h"
|
||||
#include "gui/window.h"
|
||||
#include "util/serialize.h"
|
||||
|
||||
// Include every element header for Elem::create()
|
||||
#include "gui/generic_elems.h"
|
||||
|
||||
namespace ui
|
||||
{
|
||||
std::unique_ptr<Elem> Elem::create(Type type, Window &window, std::string id)
|
||||
{
|
||||
std::unique_ptr<Elem> elem = nullptr;
|
||||
|
||||
#define CREATE(name, type) \
|
||||
case name: \
|
||||
elem = std::make_unique<type>(window, std::move(id)); \
|
||||
break
|
||||
|
||||
switch (type) {
|
||||
CREATE(ELEM, Elem);
|
||||
CREATE(ROOT, Root);
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
#undef CREATE
|
||||
|
||||
// It's a pain to call reset() in the constructor of every single
|
||||
// element due to how virtual functions work in C++, so we reset
|
||||
// elements after creating them here.
|
||||
elem->reset();
|
||||
return elem;
|
||||
}
|
||||
|
||||
Elem::Elem(Window &window, std::string id) :
|
||||
m_window(window),
|
||||
m_id(std::move(id)),
|
||||
m_main_box(*this)
|
||||
{}
|
||||
|
||||
void Elem::reset()
|
||||
{
|
||||
m_order = (size_t)-1;
|
||||
|
||||
m_parent = nullptr;
|
||||
m_children.clear();
|
||||
|
||||
m_main_box.reset();
|
||||
}
|
||||
|
||||
void Elem::read(std::istream &is)
|
||||
{
|
||||
u32 set_mask = readU32(is);
|
||||
|
||||
if (testShift(set_mask))
|
||||
readChildren(is);
|
||||
if (testShift(set_mask))
|
||||
m_main_box.read(is);
|
||||
}
|
||||
|
||||
void Elem::layout(const rf32 &parent_rect, const rf32 &parent_clip)
|
||||
{
|
||||
layoutBoxes(parent_rect, parent_clip);
|
||||
layoutChildren();
|
||||
}
|
||||
|
||||
void Elem::drawAll(Canvas &canvas)
|
||||
{
|
||||
draw(canvas);
|
||||
|
||||
for (Elem *child : m_children) {
|
||||
child->drawAll(canvas);
|
||||
}
|
||||
}
|
||||
|
||||
void Elem::layoutBoxes(const rf32 &parent_rect, const rf32 &parent_clip)
|
||||
{
|
||||
m_main_box.layout(parent_rect, parent_clip);
|
||||
}
|
||||
|
||||
void Elem::layoutChildren()
|
||||
{
|
||||
for (Elem *child : m_children) {
|
||||
child->layout(m_main_box.getChildRect(), m_main_box.getChildClip());
|
||||
}
|
||||
}
|
||||
|
||||
void Elem::draw(Canvas &canvas)
|
||||
{
|
||||
m_main_box.draw(canvas);
|
||||
}
|
||||
|
||||
void Elem::readChildren(std::istream &is)
|
||||
{
|
||||
u32 num_children = readU32(is);
|
||||
|
||||
for (size_t i = 0; i < num_children; i++) {
|
||||
std::string id = readNullStr(is);
|
||||
Elem *child = m_window.getElem(id, true);
|
||||
|
||||
if (child == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Check if this child already has a parent before adding it as a
|
||||
* child. Elements are deserialized in unspecified order rather
|
||||
* than a prefix order of parents before their children, so
|
||||
* isolated circular element refrences are still possible. However,
|
||||
* cycles including the root are impossible, so recursion starting
|
||||
* with the root element is safe and will always terminate.
|
||||
*/
|
||||
if (child->m_parent != nullptr) {
|
||||
errorstream << "Element \"" << id << "\" already has parent \"" <<
|
||||
child->m_parent->m_id << "\"" << std::endl;
|
||||
} else if (child == m_window.getRoot()) {
|
||||
errorstream << "Element \"" << id <<
|
||||
"\" is the root element and cannot have a parent" << std::endl;
|
||||
} else {
|
||||
m_children.push_back(child);
|
||||
child->m_parent = this;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
Minetest
|
||||
Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU Lesser General Public License as published by
|
||||
the Free Software Foundation; either version 2.1 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "irrlichttypes_extrabloated.h"
|
||||
#include "gui/box.h"
|
||||
#include "util/basic_macros.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace ui
|
||||
{
|
||||
class Window;
|
||||
|
||||
class Elem
|
||||
{
|
||||
public:
|
||||
// Serialized enum; do not change values of entries.
|
||||
enum Type
|
||||
{
|
||||
ELEM = 0x00,
|
||||
ROOT = 0x01,
|
||||
};
|
||||
|
||||
private:
|
||||
// The window and ID are intrinsic to the element's identity, so they
|
||||
// are set by the constructor and aren't cleared in reset() or changed
|
||||
// in read().
|
||||
Window &m_window;
|
||||
std::string m_id;
|
||||
|
||||
size_t m_order;
|
||||
|
||||
Elem *m_parent;
|
||||
std::vector<Elem *> m_children;
|
||||
|
||||
Box m_main_box;
|
||||
|
||||
public:
|
||||
static std::unique_ptr<Elem> create(Type type, Window &window, std::string id);
|
||||
|
||||
Elem(Window &window, std::string id);
|
||||
|
||||
DISABLE_CLASS_COPY(Elem)
|
||||
ALLOW_CLASS_MOVE(Elem)
|
||||
|
||||
virtual ~Elem() = default;
|
||||
|
||||
Window &getWindow() { return m_window; }
|
||||
const Window &getWindow() const { return m_window; }
|
||||
|
||||
const std::string &getId() const { return m_id; }
|
||||
virtual Type getType() const { return ELEM; }
|
||||
|
||||
size_t getOrder() { return m_order; }
|
||||
void setOrder(size_t order) { m_order = order; }
|
||||
|
||||
Elem *getParent() { return m_parent; }
|
||||
const std::vector<Elem *> &getChildren() { return m_children; }
|
||||
|
||||
Box &getMainBox() { return m_main_box; }
|
||||
|
||||
virtual void reset();
|
||||
virtual void read(std::istream &is);
|
||||
|
||||
void layout(const rf32 &parent_rect, const rf32 &parent_clip);
|
||||
void drawAll(Canvas &canvas);
|
||||
|
||||
protected:
|
||||
virtual void layoutBoxes(const rf32 &parent_rect, const rf32 &parent_clip);
|
||||
virtual void layoutChildren();
|
||||
|
||||
virtual void draw(Canvas &canvas);
|
||||
|
||||
private:
|
||||
void readChildren(std::istream &is);
|
||||
};
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
Minetest
|
||||
Copyright (C) 2024 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU Lesser General Public License as published by
|
||||
the Free Software Foundation; either version 2.1 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#include "gui/generic_elems.h"
|
||||
|
||||
#include "debug.h"
|
||||
#include "log.h"
|
||||
#include "gui/manager.h"
|
||||
#include "util/serialize.h"
|
||||
|
||||
namespace ui
|
||||
{
|
||||
void Root::reset()
|
||||
{
|
||||
Elem::reset();
|
||||
|
||||
m_backdrop_box.reset();
|
||||
}
|
||||
|
||||
void Root::read(std::istream &is)
|
||||
{
|
||||
auto super = newIs(readStr32(is));
|
||||
Elem::read(super);
|
||||
|
||||
u32 set_mask = readU32(is);
|
||||
|
||||
if (testShift(set_mask))
|
||||
m_backdrop_box.read(is);
|
||||
}
|
||||
|
||||
void Root::layoutBoxes(const rf32 &parent_rect, const rf32 &parent_clip)
|
||||
{
|
||||
m_backdrop_box.layout(parent_rect, parent_clip);
|
||||
Elem::layoutBoxes(m_backdrop_box.getChildRect(), m_backdrop_box.getChildClip());
|
||||
}
|
||||
|
||||
void Root::draw(Canvas &canvas)
|
||||
{
|
||||
m_backdrop_box.draw(canvas);
|
||||
Elem::draw(canvas);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
Minetest
|
||||
Copyright (C) 2024 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU Lesser General Public License as published by
|
||||
the Free Software Foundation; either version 2.1 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "irrlichttypes_extrabloated.h"
|
||||
#include "gui/box.h"
|
||||
#include "gui/elem.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
namespace ui
|
||||
{
|
||||
class Root : public Elem
|
||||
{
|
||||
private:
|
||||
Box m_backdrop_box;
|
||||
|
||||
public:
|
||||
Root(Window &window, std::string id) :
|
||||
Elem(window, std::move(id)),
|
||||
m_backdrop_box(*this)
|
||||
{}
|
||||
|
||||
virtual Type getType() const override { return ROOT; }
|
||||
|
||||
virtual void reset() override;
|
||||
virtual void read(std::istream &is) override;
|
||||
|
||||
protected:
|
||||
virtual void layoutBoxes(const rf32 &parent_rect, const rf32 &parent_clip);
|
||||
|
||||
virtual void draw(Canvas &canvas);
|
||||
};
|
||||
}
|
|
@ -0,0 +1,192 @@
|
|||
/*
|
||||
Minetest
|
||||
Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU Lesser General Public License as published by
|
||||
the Free Software Foundation; either version 2.1 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#include "gui/manager.h"
|
||||
|
||||
#include "debug.h"
|
||||
#include "log.h"
|
||||
#include "settings.h"
|
||||
#include "client/client.h"
|
||||
#include "client/renderingengine.h"
|
||||
#include "client/tile.h"
|
||||
#include "util/serialize.h"
|
||||
|
||||
namespace ui
|
||||
{
|
||||
bool testShift(u32 &bits)
|
||||
{
|
||||
bool test = bits & 1;
|
||||
bits >>= 1;
|
||||
return test;
|
||||
}
|
||||
|
||||
std::string readStr16(std::istream &is)
|
||||
{
|
||||
return deSerializeString16(is, true);
|
||||
}
|
||||
|
||||
std::string readStr32(std::istream &is)
|
||||
{
|
||||
return deSerializeString32(is, true);
|
||||
}
|
||||
|
||||
std::string readNullStr(std::istream &is)
|
||||
{
|
||||
std::string str;
|
||||
std::getline(is, str, '\0');
|
||||
return str;
|
||||
}
|
||||
|
||||
void writeStr16(std::ostream &os, const std::string &str)
|
||||
{
|
||||
os << serializeString16(str, true);
|
||||
}
|
||||
|
||||
void writeStr32(std::ostream &os, const std::string &str)
|
||||
{
|
||||
os << serializeString32(str, true);
|
||||
}
|
||||
|
||||
void writeNullStr(std::ostream &os, const std::string &str)
|
||||
{
|
||||
os << str.substr(0, strlen(str.c_str())) << '\0';
|
||||
}
|
||||
|
||||
std::istringstream newIs(std::string str)
|
||||
{
|
||||
return std::istringstream(std::move(str), std::ios_base::binary);
|
||||
}
|
||||
|
||||
std::ostringstream newOs()
|
||||
{
|
||||
return std::ostringstream(std::ios_base::binary);
|
||||
}
|
||||
|
||||
Texture Manager::getTexture(const std::string &name) const
|
||||
{
|
||||
return Texture(m_client->tsrc()->getTexture(name));
|
||||
}
|
||||
|
||||
float Manager::getPixelSize(WindowType type) const
|
||||
{
|
||||
if (type == WindowType::GUI || type == WindowType::MESSAGE) {
|
||||
return m_gui_pixel_size;
|
||||
}
|
||||
return m_hud_pixel_size;
|
||||
}
|
||||
|
||||
d2f32 Manager::getScreenSize(WindowType type) const
|
||||
{
|
||||
video::IVideoDriver *driver = RenderingEngine::get_video_driver();
|
||||
d2u32 screen_size = driver->getScreenSize();
|
||||
|
||||
float pixel_size = getPixelSize(type);
|
||||
|
||||
return d2f32(
|
||||
screen_size.Width / pixel_size,
|
||||
screen_size.Height / pixel_size
|
||||
);
|
||||
}
|
||||
|
||||
void Manager::reset()
|
||||
{
|
||||
m_client = nullptr;
|
||||
|
||||
m_windows.clear();
|
||||
}
|
||||
|
||||
void Manager::removeWindow(u64 id)
|
||||
{
|
||||
auto it = m_windows.find(id);
|
||||
if (it == m_windows.end()) {
|
||||
infostream << "Window " << id << " is already closed" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
m_windows.erase(it);
|
||||
}
|
||||
|
||||
void Manager::receiveMessage(const std::string &data)
|
||||
{
|
||||
auto is = newIs(data);
|
||||
|
||||
u32 action = readU8(is);
|
||||
u64 id = readU64(is);
|
||||
|
||||
switch (action) {
|
||||
case REOPEN_WINDOW: {
|
||||
u64 close_id = readU64(is);
|
||||
removeWindow(close_id);
|
||||
}
|
||||
// fallthrough
|
||||
|
||||
case OPEN_WINDOW: {
|
||||
auto it = m_windows.find(id);
|
||||
if (it != m_windows.end()) {
|
||||
infostream << "Window " << id << " is already open" << std::endl;
|
||||
break;
|
||||
}
|
||||
|
||||
it = m_windows.emplace(id, Window(id)).first;
|
||||
it->second.read(is, true);
|
||||
break;
|
||||
}
|
||||
|
||||
case UPDATE_WINDOW: {
|
||||
auto it = m_windows.find(id);
|
||||
if (it != m_windows.end()) {
|
||||
it->second.read(is, false);
|
||||
} else {
|
||||
infostream << "Window " << id << " does not exist" << std::endl;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case CLOSE_WINDOW:
|
||||
removeWindow(id);
|
||||
break;
|
||||
|
||||
default:
|
||||
errorstream << "Invalid manager action: " << action << std::endl;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Manager::preDraw()
|
||||
{
|
||||
float base_size = RenderingEngine::getDisplayDensity();
|
||||
m_gui_pixel_size = base_size * g_settings->getFloat("gui_scaling");
|
||||
m_hud_pixel_size = base_size * g_settings->getFloat("hud_scaling");
|
||||
}
|
||||
|
||||
void Manager::drawType(WindowType type)
|
||||
{
|
||||
Texture::begin();
|
||||
|
||||
for (auto &it : m_windows) {
|
||||
if (it.second.getType() == type) {
|
||||
it.second.drawAll();
|
||||
}
|
||||
}
|
||||
|
||||
Texture::end();
|
||||
}
|
||||
|
||||
Manager g_manager;
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
Minetest
|
||||
Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU Lesser General Public License as published by
|
||||
the Free Software Foundation; either version 2.1 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "irrlichttypes_extrabloated.h"
|
||||
#include "gui/texture.h"
|
||||
#include "gui/window.h"
|
||||
#include "util/basic_macros.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <map>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
class Client;
|
||||
|
||||
namespace ui
|
||||
{
|
||||
// Define a few functions that are particularly useful for UI serialization
|
||||
// and deserialization.
|
||||
bool testShift(u32 &bits);
|
||||
|
||||
// The UI purposefully avoids dealing with SerializationError, so it uses
|
||||
// always uses truncating or null-terminated string functions. Hence, we
|
||||
// make convenience wrappers around the string functions in "serialize.h".
|
||||
std::string readStr16(std::istream &is);
|
||||
std::string readStr32(std::istream &is);
|
||||
std::string readNullStr(std::istream &is);
|
||||
|
||||
void writeStr16(std::ostream &os, const std::string &str);
|
||||
void writeStr32(std::ostream &os, const std::string &str);
|
||||
void writeNullStr(std::ostream &os, const std::string &str);
|
||||
|
||||
// Convenience functions to create new binary string streams.
|
||||
std::istringstream newIs(std::string str);
|
||||
std::ostringstream newOs();
|
||||
|
||||
class Manager
|
||||
{
|
||||
public:
|
||||
// Serialized enum; do not change values of entries.
|
||||
enum ReceiveAction
|
||||
{
|
||||
OPEN_WINDOW = 0x00,
|
||||
REOPEN_WINDOW = 0x01,
|
||||
UPDATE_WINDOW = 0x02,
|
||||
CLOSE_WINDOW = 0x03,
|
||||
};
|
||||
|
||||
private:
|
||||
Client *m_client;
|
||||
|
||||
float m_gui_pixel_size = 0.0f;
|
||||
float m_hud_pixel_size = 0.0f;
|
||||
|
||||
// Use map rather than unordered_map so that windows are always sorted
|
||||
// by window ID to make sure that they are drawn in order of creation.
|
||||
std::map<u64, Window> m_windows;
|
||||
|
||||
public:
|
||||
Manager()
|
||||
{
|
||||
reset();
|
||||
}
|
||||
|
||||
DISABLE_CLASS_COPY(Manager)
|
||||
|
||||
Client *getClient() const { return m_client; }
|
||||
void setClient(Client *client) { m_client = client; }
|
||||
|
||||
Texture getTexture(const std::string &name) const;
|
||||
|
||||
float getPixelSize(WindowType type) const;
|
||||
d2f32 getScreenSize(WindowType type) const;
|
||||
|
||||
void reset();
|
||||
void removeWindow(u64 id);
|
||||
|
||||
void receiveMessage(const std::string &data);
|
||||
|
||||
void preDraw();
|
||||
void drawType(WindowType type);
|
||||
};
|
||||
|
||||
extern Manager g_manager;
|
||||
|
||||
// Inconveniently, we need a way to draw the "gui" window types after the
|
||||
// chat console but before other GUIs like the key change menu, formspecs,
|
||||
// etc. So, we inject our own mini Irrlicht element in between.
|
||||
class GUIManagerElem : public gui::IGUIElement
|
||||
{
|
||||
public:
|
||||
GUIManagerElem(gui::IGUIEnvironment* env, gui::IGUIElement* parent, s32 id) :
|
||||
gui::IGUIElement(gui::EGUIET_ELEMENT, env, parent, id, rs32())
|
||||
{}
|
||||
|
||||
virtual void draw() override
|
||||
{
|
||||
g_manager.drawType(ui::WindowType::GUI);
|
||||
gui::IGUIElement::draw();
|
||||
}
|
||||
};
|
||||
}
|
|
@ -0,0 +1,255 @@
|
|||
/*
|
||||
Minetest
|
||||
Copyright (C) 2022 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU Lesser General Public License as published by
|
||||
the Free Software Foundation; either version 2.1 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#include "gui/texture.h"
|
||||
|
||||
#include "client/renderingengine.h"
|
||||
#include "debug.h"
|
||||
#include "log.h"
|
||||
|
||||
namespace ui
|
||||
{
|
||||
static video::IVideoDriver *driver() {
|
||||
return RenderingEngine::get_video_driver();
|
||||
}
|
||||
|
||||
// The current render target. We keep track of this in set_as_target() so
|
||||
// as to only change render targets when necessary. It is only valid
|
||||
// between start_drawing() and end_drawing(); otherwise, it's nullptr.
|
||||
video::ITexture *s_current_target = nullptr;
|
||||
|
||||
static void force_screen_target()
|
||||
{
|
||||
// Force-set the render target to the screen, regardless of the current
|
||||
// value of s_current_target.
|
||||
driver()->setRenderTarget(nullptr, false, 0);
|
||||
s_current_target = nullptr;
|
||||
}
|
||||
|
||||
static void set_as_target(video::ITexture *target, bool clear = false,
|
||||
video::SColor color = BLANK)
|
||||
{
|
||||
// Don't change the render target if it's already set.
|
||||
if (s_current_target == target) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If we want to clear it to a certain color, then we go ahead and
|
||||
// clear the depth buffer as well.
|
||||
u16 to_clear = clear ? video::ECBF_ALL : video::ECBF_NONE;
|
||||
|
||||
if (driver()->setRenderTarget(target, to_clear, color)) {
|
||||
// The call succeeded, so update the current target variable.
|
||||
s_current_target = target;
|
||||
} else {
|
||||
// The call failed, so we probably don't support render targets.
|
||||
errorstream << "Unable to set render target" << std::endl;
|
||||
force_screen_target();
|
||||
}
|
||||
}
|
||||
|
||||
void Texture::begin()
|
||||
{
|
||||
// Force set the target since we don't know what the target was before,
|
||||
// so s_current_target could possibly be invalid.
|
||||
force_screen_target();
|
||||
}
|
||||
|
||||
void Texture::end()
|
||||
{
|
||||
set_as_target(nullptr);
|
||||
}
|
||||
|
||||
Texture Texture::screen = {};
|
||||
|
||||
Texture::Texture(const d2s32 &size)
|
||||
{
|
||||
if (driver()->queryFeature(video::EVDF_RENDER_TO_TARGET)) {
|
||||
m_texture.grab(driver()->addRenderTargetTexture(d2u32(size)));
|
||||
if (!isTexture()) {
|
||||
errorstream << "Unable to create render target" << std::endl;
|
||||
}
|
||||
|
||||
// The default contents of a texture are appear to be unspecified,
|
||||
// even though some it's usually transparent by default. So, we
|
||||
// explicitly make it transparent.
|
||||
drawFill(BLANK);
|
||||
}
|
||||
}
|
||||
|
||||
Texture::Texture(video::ITexture *texture)
|
||||
{
|
||||
m_texture.grab(texture);
|
||||
}
|
||||
|
||||
Texture::~Texture()
|
||||
{
|
||||
// If the reference count is one, only Irrlicht still holds a reference
|
||||
// to the texture, so we can remove it from the driver.
|
||||
if (isTexture() && m_texture.get()->getReferenceCount() == 1) {
|
||||
driver()->removeTexture(m_texture.get());
|
||||
}
|
||||
}
|
||||
|
||||
d2s32 Texture::getSize() const
|
||||
{
|
||||
if (isTexture()) {
|
||||
return d2s32(m_texture->getOriginalSize());
|
||||
}
|
||||
return d2s32(driver()->getScreenSize());
|
||||
}
|
||||
|
||||
void Texture::drawPixel(
|
||||
const v2s32 &pos,
|
||||
video::SColor color,
|
||||
const rs32 *clip)
|
||||
{
|
||||
drawRect(rs32(pos, d2s32(0, 0)), color, clip);
|
||||
}
|
||||
|
||||
void Texture::drawRect(
|
||||
const rs32 &rect,
|
||||
video::SColor color,
|
||||
const rs32 *clip)
|
||||
{
|
||||
set_as_target(m_texture.get());
|
||||
driver()->draw2DRectangle(color, rect, clip);
|
||||
}
|
||||
|
||||
void Texture::drawTexture(
|
||||
const v2s32 &pos,
|
||||
const Texture &texture,
|
||||
const rs32 *src,
|
||||
const rs32 *clip,
|
||||
video::SColor tint)
|
||||
{
|
||||
d2s32 size = (src != nullptr) ? src->getSize() : texture.getSize();
|
||||
drawTexture(rs32(pos, size), texture, src, clip, tint);
|
||||
}
|
||||
|
||||
void Texture::drawTexture(
|
||||
const rs32 &rect,
|
||||
const Texture &texture,
|
||||
const rs32 *src,
|
||||
const rs32 *clip,
|
||||
video::SColor tint)
|
||||
{
|
||||
if (!texture.isTexture()) {
|
||||
errorstream << "Can't draw the screen to a texture" << std::endl;
|
||||
return;
|
||||
}
|
||||
if (m_texture.get() == texture.m_texture.get()) {
|
||||
errorstream << "Can't draw a texture to itself" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
set_as_target(m_texture.get());
|
||||
|
||||
// If we don't have a source rectangle, make it encompass the entire
|
||||
// texture.
|
||||
rs32 texture_rect(texture.getSize());
|
||||
if (src == nullptr) {
|
||||
src = &texture_rect;
|
||||
}
|
||||
|
||||
// All the corners should have the same tint.
|
||||
video::SColor tints[] = {tint, tint, tint, tint};
|
||||
|
||||
driver()->draw2DImage(
|
||||
texture.m_texture.get(), rect, *src, clip, tints, true);
|
||||
}
|
||||
|
||||
void Texture::drawFill(video::SColor color)
|
||||
{
|
||||
if (isTexture()) {
|
||||
/* There's no normal way to fill a texture with a color; drawing a
|
||||
* rect will add alpha, not replace it. So, use setRenderTarget()
|
||||
* to clear it instead. Irrlicht will ignore the call if this
|
||||
* texture is already the current render target, so we set it to
|
||||
* the screen first before attempting to clear the texture.
|
||||
*/
|
||||
set_as_target(nullptr);
|
||||
set_as_target(m_texture.get(), true, color);
|
||||
} else {
|
||||
// The screen can't have transparency, so make the color opaque and
|
||||
// draw a rectangle across the entire screen.
|
||||
color.setAlpha(0xFF);
|
||||
driver()->draw2DRectangle(color, rs32(getSize()));
|
||||
}
|
||||
}
|
||||
|
||||
Canvas::Canvas(Texture &texture, float scale, const rf32 *clip) :
|
||||
m_texture(texture),
|
||||
m_scale(scale)
|
||||
{
|
||||
if (clip == nullptr) {
|
||||
m_clip_ptr = nullptr;
|
||||
} else {
|
||||
m_clip = rs32(
|
||||
clip->UpperLeftCorner.X * m_scale,
|
||||
clip->UpperLeftCorner.Y * m_scale,
|
||||
clip->LowerRightCorner.X * m_scale,
|
||||
clip->LowerRightCorner.Y * m_scale
|
||||
);
|
||||
m_clip_ptr = &m_clip;
|
||||
}
|
||||
}
|
||||
|
||||
void Canvas::drawRect(
|
||||
const rf32 &rect,
|
||||
video::SColor color)
|
||||
{
|
||||
m_texture.drawRect(getDrawRect(rect), color, m_clip_ptr);
|
||||
}
|
||||
|
||||
void Canvas::drawTexture(
|
||||
const v2f32 &pos,
|
||||
const Texture &texture,
|
||||
const rf32 &src,
|
||||
video::SColor tint)
|
||||
{
|
||||
drawTexture(rf32(pos, d2f32(texture.getSize())), texture, src, tint);
|
||||
}
|
||||
|
||||
void Canvas::drawTexture(
|
||||
const rf32 &rect,
|
||||
const Texture &texture,
|
||||
const rf32 &src,
|
||||
video::SColor tint)
|
||||
{
|
||||
rs32 draw_src(
|
||||
src.UpperLeftCorner.X * texture.getWidth(),
|
||||
src.UpperLeftCorner.Y * texture.getHeight(),
|
||||
src.LowerRightCorner.X * texture.getWidth(),
|
||||
src.LowerRightCorner.Y * texture.getHeight()
|
||||
);
|
||||
m_texture.drawTexture(
|
||||
getDrawRect(rect), texture, &draw_src, m_clip_ptr, tint);
|
||||
}
|
||||
|
||||
rs32 Canvas::getDrawRect(const rf32 &rect) const
|
||||
{
|
||||
return rs32(
|
||||
rect.UpperLeftCorner.X * m_scale,
|
||||
rect.UpperLeftCorner.Y * m_scale,
|
||||
rect.LowerRightCorner.X * m_scale,
|
||||
rect.LowerRightCorner.Y * m_scale
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,185 @@
|
|||
/*
|
||||
Minetest
|
||||
Copyright (C) 2022 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU Lesser General Public License as published by
|
||||
the Free Software Foundation; either version 2.1 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "irr_ptr.h"
|
||||
#include "irrlichttypes_extrabloated.h"
|
||||
|
||||
namespace ui
|
||||
{
|
||||
using d2s32 = core::dimension2di;
|
||||
using d2u32 = core::dimension2du;
|
||||
using d2f32 = core::dimension2df;
|
||||
|
||||
using rs32 = core::recti;
|
||||
using rf32 = core::rectf;
|
||||
|
||||
const video::SColor BLANK = 0x00000000;
|
||||
const video::SColor BLACK = 0xFF000000;
|
||||
const video::SColor WHITE = 0xFFFFFFFF;
|
||||
|
||||
template<typename T>
|
||||
core::vector2d<T> clamp_vec(core::vector2d<T> vec)
|
||||
{
|
||||
if (vec.X < 0)
|
||||
vec.X = 0;
|
||||
if (vec.Y < 0)
|
||||
vec.Y = 0;
|
||||
return vec;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
core::rect<T> clamp_rect(core::rect<T> rect)
|
||||
{
|
||||
if (rect.getWidth() < 0)
|
||||
rect.LowerRightCorner.X = rect.UpperLeftCorner.X;
|
||||
if (rect.getHeight() < 0)
|
||||
rect.LowerRightCorner.Y = rect.UpperLeftCorner.Y;
|
||||
return rect;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
core::rect<T> clip_rect(core::rect<T> first, const core::rect<T> &second)
|
||||
{
|
||||
first.clipAgainst(second);
|
||||
return first;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
T &dim_at(core::dimension2d<T> &dim, size_t index)
|
||||
{
|
||||
return index == 0 ? dim.Width : dim.Height;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
const T &dim_at(const core::dimension2d<T> &dim, size_t index)
|
||||
{
|
||||
return index == 0 ? dim.Width : dim.Height;
|
||||
}
|
||||
|
||||
class Texture
|
||||
{
|
||||
private:
|
||||
irr_ptr<video::ITexture> m_texture = nullptr;
|
||||
|
||||
public:
|
||||
// These functions must surround any drawing calls to Texture instances
|
||||
// to ensure proper tracking of render targets. Raw Irrlicht draw calls
|
||||
// should be used with caution to avoid messing up render targets.
|
||||
static void begin();
|
||||
static void end();
|
||||
|
||||
static Texture screen;
|
||||
|
||||
Texture() = default;
|
||||
Texture(video::ITexture *texture);
|
||||
Texture(const d2s32 &size);
|
||||
|
||||
~Texture();
|
||||
|
||||
d2s32 getSize() const;
|
||||
|
||||
s32 getWidth() const { return getSize().Width; }
|
||||
s32 getHeight() const { return getSize().Height; }
|
||||
|
||||
bool isTexture() const
|
||||
{
|
||||
return m_texture.get() != nullptr;
|
||||
}
|
||||
|
||||
void drawPixel(
|
||||
const v2s32 &pos,
|
||||
video::SColor color,
|
||||
const rs32 *clip = nullptr);
|
||||
|
||||
void drawRect(
|
||||
const rs32 &rect,
|
||||
video::SColor color,
|
||||
const rs32 *clip = nullptr);
|
||||
|
||||
void drawTexture(
|
||||
const v2s32 &pos,
|
||||
const Texture &texture,
|
||||
const rs32 *src = nullptr,
|
||||
const rs32 *clip = nullptr,
|
||||
video::SColor tint = WHITE);
|
||||
|
||||
void drawTexture(
|
||||
const rs32 &rect,
|
||||
const Texture &texture,
|
||||
const rs32 *src = nullptr,
|
||||
const rs32 *clip = nullptr,
|
||||
video::SColor tint = WHITE);
|
||||
|
||||
void drawFill(video::SColor color);
|
||||
|
||||
friend bool operator==(const Texture &left, const Texture &right)
|
||||
{
|
||||
return left.m_texture == right.m_texture;
|
||||
}
|
||||
|
||||
friend bool operator!=(const Texture &left, const Texture &right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
};
|
||||
|
||||
class Canvas
|
||||
{
|
||||
private:
|
||||
Texture &m_texture;
|
||||
|
||||
float m_scale;
|
||||
|
||||
rs32 m_clip;
|
||||
rs32 *m_clip_ptr;
|
||||
|
||||
public:
|
||||
Canvas(Texture &texture, float scale, const rf32 *clip = nullptr);
|
||||
|
||||
Canvas(Canvas &canvas, const rf32 *clip = nullptr) :
|
||||
Canvas(canvas.getTexture(), canvas.getScale(), clip)
|
||||
{}
|
||||
|
||||
Texture &getTexture() { return m_texture; }
|
||||
const Texture &getTexture() const { return m_texture; }
|
||||
|
||||
float getScale() const { return m_scale; }
|
||||
|
||||
void drawRect(
|
||||
const rf32 &rect,
|
||||
video::SColor color);
|
||||
|
||||
void drawTexture(
|
||||
const v2f32 &pos,
|
||||
const Texture &texture,
|
||||
const rf32 &src = rf32(0.0f, 0.0f, 1.0f, 1.0f),
|
||||
video::SColor tint = WHITE);
|
||||
|
||||
void drawTexture(
|
||||
const rf32 &rect,
|
||||
const Texture &texture,
|
||||
const rf32 &src = rf32(0.0f, 0.0f, 1.0f, 1.0f),
|
||||
video::SColor tint = WHITE);
|
||||
|
||||
private:
|
||||
rs32 getDrawRect(const rf32 &rect) const;
|
||||
};
|
||||
}
|
|
@ -0,0 +1,284 @@
|
|||
/*
|
||||
Minetest
|
||||
Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU Lesser General Public License as published by
|
||||
the Free Software Foundation; either version 2.1 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#include "gui/window.h"
|
||||
|
||||
#include "debug.h"
|
||||
#include "log.h"
|
||||
#include "settings.h"
|
||||
#include "client/client.h"
|
||||
#include "client/renderingengine.h"
|
||||
#include "client/tile.h"
|
||||
#include "gui/manager.h"
|
||||
#include "gui/texture.h"
|
||||
#include "util/serialize.h"
|
||||
|
||||
namespace ui
|
||||
{
|
||||
WindowType toWindowType(u8 type)
|
||||
{
|
||||
if (type >= (u8)WindowType::MAX_TYPE) {
|
||||
return WindowType::HUD;
|
||||
}
|
||||
return (WindowType)type;
|
||||
}
|
||||
|
||||
Elem *Window::getElem(const std::string &id, bool required)
|
||||
{
|
||||
// Empty IDs may be valid values if the element is optional.
|
||||
if (id.empty() && !required) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// If the ID is not empty, then we need to search for an actual
|
||||
// element. Not finding one means that an error occurred.
|
||||
auto it = m_elems.find(id);
|
||||
if (it != m_elems.end()) {
|
||||
return it->second.get();
|
||||
}
|
||||
|
||||
errorstream << "Element \"" << id << "\" does not exist" << std::endl;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const std::string *Window::getStyleStr(u32 index) const
|
||||
{
|
||||
if (index < m_style_strs.size()) {
|
||||
return &m_style_strs[index];
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void Window::reset()
|
||||
{
|
||||
m_elems.clear();
|
||||
m_ordered_elems.clear();
|
||||
|
||||
m_root_elem = nullptr;
|
||||
|
||||
m_style_strs.clear();
|
||||
}
|
||||
|
||||
void Window::read(std::istream &is, bool opening)
|
||||
{
|
||||
std::unordered_map<Elem *, std::string> elem_contents;
|
||||
readElems(is, elem_contents);
|
||||
|
||||
readRootElem(is);
|
||||
readStyles(is);
|
||||
|
||||
if (opening)
|
||||
m_type = toWindowType(readU8(is));
|
||||
|
||||
// Assuming no earlier step failed, we can proceed to read in all the
|
||||
// properties. Otherwise, reset the window entirely.
|
||||
if (m_root_elem != nullptr) {
|
||||
updateElems(elem_contents);
|
||||
} else {
|
||||
reset();
|
||||
}
|
||||
}
|
||||
|
||||
float Window::getPixelSize() const
|
||||
{
|
||||
return g_manager.getPixelSize(m_type);
|
||||
}
|
||||
|
||||
d2f32 Window::getScreenSize() const
|
||||
{
|
||||
return g_manager.getScreenSize(m_type);
|
||||
}
|
||||
|
||||
void Window::drawAll()
|
||||
{
|
||||
if (m_root_elem == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
rf32 parent_rect(getScreenSize());
|
||||
m_root_elem->layout(parent_rect, parent_rect);
|
||||
|
||||
Canvas canvas(Texture::screen, getPixelSize());
|
||||
m_root_elem->drawAll(canvas);
|
||||
}
|
||||
|
||||
void Window::readElems(std::istream &is,
|
||||
std::unordered_map<Elem *, std::string> &elem_contents)
|
||||
{
|
||||
// Read in all the new elements and updates to existing elements.
|
||||
u32 num_elems = readU32(is);
|
||||
|
||||
std::unordered_map<std::string, std::unique_ptr<Elem>> new_elems;
|
||||
|
||||
for (size_t i = 0; i < num_elems; i++) {
|
||||
u32 type = readU8(is);
|
||||
std::string id = readNullStr(is);
|
||||
|
||||
// Make sure that elements have valid IDs. If the string has non-ID
|
||||
// characters in it, though, we don't particularly care.
|
||||
if (id.empty()) {
|
||||
errorstream << "Element has empty ID" << std::endl;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Each element has a size prefix stating how big the element is.
|
||||
// This allows new fields to be added to elements without breaking
|
||||
// compatibility. So, read it in as a string and save it for later.
|
||||
std::string contents = readStr32(is);
|
||||
|
||||
// If this is a duplicate element, skip it right away.
|
||||
if (new_elems.find(id) != new_elems.end()) {
|
||||
errorstream << "Duplicate element \"" << id << "\"" << std::endl;
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Now we need to decide whether to create a new element or to
|
||||
* modify the state of an already existing one. This allows
|
||||
* changing attributes of an element (like the style or the
|
||||
* element's children) while leaving leaving persistent state
|
||||
* intact (such as the position of a scrollbar or the contents of a
|
||||
* text field).
|
||||
*/
|
||||
std::unique_ptr<Elem> elem = nullptr;
|
||||
|
||||
// Search for a pre-existing element.
|
||||
auto it = m_elems.find(id);
|
||||
|
||||
if (it == m_elems.end() || it->second->getType() != type) {
|
||||
// If the element was not found or the existing element has the
|
||||
// wrong type, create a new element.
|
||||
elem = Elem::create((Elem::Type)type, *this, id);
|
||||
|
||||
// If we couldn't create the element, the type was invalid.
|
||||
// Skip this element entirely.
|
||||
if (elem == nullptr) {
|
||||
errorstream << "Element \"" << id << "\" has an invalid type: " <<
|
||||
type << std::endl;
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// Otherwise, use the existing element.
|
||||
elem = std::move(it->second);
|
||||
}
|
||||
|
||||
// Now that we've gotten our element, reset its contents.
|
||||
elem->reset();
|
||||
|
||||
// We need to read in all elements before updating each element, so
|
||||
// save the element's contents for later.
|
||||
elem_contents[elem.get()] = contents;
|
||||
new_elems.emplace(id, std::move(elem));
|
||||
}
|
||||
|
||||
// Set these elements as our list of new elements.
|
||||
m_elems = std::move(new_elems);
|
||||
|
||||
// Clear the ordered elements for now. They will be regenerated later.
|
||||
m_ordered_elems.clear();
|
||||
}
|
||||
|
||||
void Window::readRootElem(std::istream &is)
|
||||
{
|
||||
// Get the root element of the window and make sure it's valid.
|
||||
m_root_elem = getElem(readNullStr(is), true);
|
||||
|
||||
if (m_root_elem == nullptr) {
|
||||
errorstream << "Window " << m_id << " has no root element" << std::endl;
|
||||
reset();
|
||||
} else if (m_root_elem->getType() != Elem::ROOT) {
|
||||
errorstream << "Window " << m_id <<
|
||||
" has wrong type for root element" << std::endl;
|
||||
reset();
|
||||
}
|
||||
}
|
||||
|
||||
void Window::readStyles(std::istream &is)
|
||||
{
|
||||
// Styles are stored in their raw binary form; every time a style needs
|
||||
// to be recalculated, these binary strings can be applied one over the
|
||||
// other, resulting in automatic cascading styles.
|
||||
u32 num_styles = readU32(is);
|
||||
m_style_strs.clear();
|
||||
|
||||
for (size_t i = 0; i < num_styles; i++) {
|
||||
m_style_strs.push_back(readStr16(is));
|
||||
}
|
||||
}
|
||||
|
||||
void Window::updateElems(std::unordered_map<Elem *, std::string> &elem_contents)
|
||||
{
|
||||
// Now that we have a fully updated window, we can update each element
|
||||
// with its contents. We couldn't do this before because elements need
|
||||
// to be able to call getElem() and getStyleStr().
|
||||
for (auto &contents : elem_contents) {
|
||||
auto is = newIs(std::move(contents.second));
|
||||
contents.first->read(is);
|
||||
}
|
||||
|
||||
// Check the depth of the element tree; if it's too deep, there's
|
||||
// potential for stack overflow.
|
||||
if (!checkTree(m_root_elem, 1)) {
|
||||
reset();
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the ordering of the elements so we can do iteration rather
|
||||
// than recursion when searching through the elements in order.
|
||||
updateElemOrdering(m_root_elem, 0);
|
||||
}
|
||||
|
||||
bool Window::checkTree(Elem *elem, size_t depth) const
|
||||
{
|
||||
if (depth > MAX_TREE_DEPTH) {
|
||||
errorstream << "Window " << m_id <<
|
||||
" exceeds max tree depth: " << MAX_TREE_DEPTH << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
for (Elem *child : elem->getChildren()) {
|
||||
if (child->getType() == Elem::ROOT) {
|
||||
errorstream << "Element of root type \"" << child->getId() <<
|
||||
"\" is not root of window" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!checkTree(child, depth + 1)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t Window::updateElemOrdering(Elem *elem, size_t order)
|
||||
{
|
||||
// The parent gets ordered before its children since the ordering of
|
||||
// elements follows draw order.
|
||||
elem->setOrder(order);
|
||||
m_ordered_elems.push_back(elem);
|
||||
|
||||
for (Elem *child : elem->getChildren()) {
|
||||
// Order this element's children using the next index after the
|
||||
// parent, returning the index of the last child element.
|
||||
order = updateElemOrdering(child, order + 1);
|
||||
}
|
||||
|
||||
return order;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
Minetest
|
||||
Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU Lesser General Public License as published by
|
||||
the Free Software Foundation; either version 2.1 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "irrlichttypes_extrabloated.h"
|
||||
#include "gui/elem.h"
|
||||
#include "util/basic_macros.h"
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace ui
|
||||
{
|
||||
// Serialized enum; do not change order of entries.
|
||||
enum class WindowType
|
||||
{
|
||||
BG,
|
||||
MASK,
|
||||
HUD,
|
||||
MESSAGE,
|
||||
GUI,
|
||||
FG,
|
||||
|
||||
MAX_TYPE,
|
||||
};
|
||||
|
||||
WindowType toWindowType(u8 type);
|
||||
|
||||
class Window
|
||||
{
|
||||
private:
|
||||
static constexpr size_t MAX_TREE_DEPTH = 64;
|
||||
|
||||
u64 m_id;
|
||||
WindowType m_type = WindowType::GUI;
|
||||
|
||||
std::unordered_map<std::string, std::unique_ptr<Elem>> m_elems;
|
||||
std::vector<Elem *> m_ordered_elems;
|
||||
|
||||
Elem *m_root_elem;
|
||||
|
||||
std::vector<std::string> m_style_strs;
|
||||
|
||||
public:
|
||||
Window(u64 id) :
|
||||
m_id(id)
|
||||
{
|
||||
reset();
|
||||
}
|
||||
|
||||
DISABLE_CLASS_COPY(Window)
|
||||
ALLOW_CLASS_MOVE(Window)
|
||||
|
||||
u64 getId() const { return m_id; }
|
||||
|
||||
WindowType getType() const { return m_type; }
|
||||
|
||||
const std::vector<Elem *> &getElems() { return m_ordered_elems; }
|
||||
Elem *getElem(const std::string &id, bool required);
|
||||
|
||||
Elem *getRoot() { return m_root_elem; }
|
||||
|
||||
const std::string *getStyleStr(u32 index) const;
|
||||
|
||||
void reset();
|
||||
void read(std::istream &is, bool opening);
|
||||
|
||||
float getPixelSize() const;
|
||||
d2f32 getScreenSize() const;
|
||||
|
||||
void drawAll();
|
||||
|
||||
private:
|
||||
void readElems(std::istream &is,
|
||||
std::unordered_map<Elem *, std::string> &elem_contents);
|
||||
void readRootElem(std::istream &is);
|
||||
void readStyles(std::istream &is);
|
||||
|
||||
void updateElems(std::unordered_map<Elem *, std::string> &elem_contents);
|
||||
bool checkTree(Elem *elem, size_t depth) const;
|
||||
size_t updateElemOrdering(Elem *elem, size_t order);
|
||||
};
|
||||
}
|
|
@ -120,7 +120,7 @@ const ToClientCommandHandler toClientCommandTable[TOCLIENT_NUM_MSG_TYPES] =
|
|||
{ "TOCLIENT_SET_MOON", TOCLIENT_STATE_CONNECTED, &Client::handleCommand_HudSetMoon }, // 0x5b
|
||||
{ "TOCLIENT_SET_STARS", TOCLIENT_STATE_CONNECTED, &Client::handleCommand_HudSetStars }, // 0x5c
|
||||
{ "TOCLIENT_MOVE_PLAYER_REL", TOCLIENT_STATE_CONNECTED, &Client::handleCommand_MovePlayerRel }, // 0x5d,
|
||||
null_command_handler,
|
||||
{ "TOCLIENT_UI_MESSAGE", TOCLIENT_STATE_CONNECTED, &Client::handleCommand_UiMessage }, // 0x5e,
|
||||
null_command_handler,
|
||||
{ "TOCLIENT_SRP_BYTES_S_B", TOCLIENT_STATE_NOT_CONNECTED, &Client::handleCommand_SrpBytesSandB }, // 0x60
|
||||
{ "TOCLIENT_FORMSPEC_PREPEND", TOCLIENT_STATE_CONNECTED, &Client::handleCommand_FormspecPrepend }, // 0x61,
|
||||
|
|
|
@ -999,6 +999,17 @@ void Client::handleCommand_ShowFormSpec(NetworkPacket* pkt)
|
|||
m_client_event_queue.push(event);
|
||||
}
|
||||
|
||||
void Client::handleCommand_UiMessage(NetworkPacket* pkt)
|
||||
{
|
||||
std::string *data = new std::string(pkt->getString(0), pkt->getSize());
|
||||
|
||||
ClientEvent *event = new ClientEvent();
|
||||
event->type = CE_UI_MESSAGE;
|
||||
event->ui_message.data = data;
|
||||
|
||||
m_client_event_queue.push(event);
|
||||
}
|
||||
|
||||
void Client::handleCommand_SpawnParticle(NetworkPacket* pkt)
|
||||
{
|
||||
std::string datastring(pkt->getString(0), pkt->getSize());
|
||||
|
|
|
@ -223,6 +223,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
AO_CMD_SET_BONE_POSITION extended
|
||||
Add TOCLIENT_MOVE_PLAYER_REL
|
||||
Move default minimap from client-side C++ to server-side builtin Lua
|
||||
Add TOCLIENT_UI_MESSAGE
|
||||
[scheduled bump for 5.9.0]
|
||||
*/
|
||||
|
||||
|
@ -811,6 +812,12 @@ enum ToClientCommand : u16
|
|||
v3f added_pos
|
||||
*/
|
||||
|
||||
TOCLIENT_UI_MESSAGE = 0x5e,
|
||||
/*
|
||||
Complicated variable-length structure with many optional fields and
|
||||
length-prefixed data for future compatibility.
|
||||
*/
|
||||
|
||||
TOCLIENT_SRP_BYTES_S_B = 0x60,
|
||||
/*
|
||||
Belonging to AUTH_MECHANISM_SRP.
|
||||
|
|
|
@ -220,7 +220,7 @@ const ClientCommandFactory clientCommandFactoryTable[TOCLIENT_NUM_MSG_TYPES] =
|
|||
{ "TOCLIENT_SET_MOON", 0, true }, // 0x5b
|
||||
{ "TOCLIENT_SET_STARS", 0, true }, // 0x5c
|
||||
{ "TOCLIENT_MOVE_PLAYER_REL", 0, true }, // 0x5d
|
||||
null_command_factory, // 0x5e
|
||||
{ "TOCLIENT_UI_MESSAGE", 0, true }, // 0x5e
|
||||
null_command_factory, // 0x5f
|
||||
{ "TOCLIENT_SRP_BYTES_S_B", 0, true }, // 0x60
|
||||
{ "TOCLIENT_FORMSPEC_PREPEND", 0, true }, // 0x61
|
||||
|
|
|
@ -437,6 +437,19 @@ int ModApiServer::l_show_formspec(lua_State *L)
|
|||
return 1;
|
||||
}
|
||||
|
||||
// send_ui_message(player, data)
|
||||
int ModApiServer::l_send_ui_message(lua_State *L)
|
||||
{
|
||||
NO_MAP_LOCK_REQUIRED;
|
||||
|
||||
size_t len;
|
||||
const char *player = luaL_checkstring(L, 1);
|
||||
const char *data = luaL_checklstring(L, 2, &len);
|
||||
|
||||
getServer(L)->sendUiMessage(player, data, len);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// get_current_modname()
|
||||
int ModApiServer::l_get_current_modname(lua_State *L)
|
||||
{
|
||||
|
@ -749,6 +762,7 @@ void ModApiServer::Initialize(lua_State *L, int top)
|
|||
API_FCT(chat_send_all);
|
||||
API_FCT(chat_send_player);
|
||||
API_FCT(show_formspec);
|
||||
API_FCT(send_ui_message);
|
||||
API_FCT(sound_play);
|
||||
API_FCT(sound_stop);
|
||||
API_FCT(sound_fade);
|
||||
|
|
|
@ -70,6 +70,9 @@ private:
|
|||
// show_formspec(playername,formname,formspec)
|
||||
static int l_show_formspec(lua_State *L);
|
||||
|
||||
// send_ui_message(player, data)
|
||||
static int l_send_ui_message(lua_State *L);
|
||||
|
||||
// sound_play(spec, parameters)
|
||||
static int l_sound_play(lua_State *L);
|
||||
|
||||
|
|
|
@ -605,6 +605,20 @@ int ModApiUtil::l_colorspec_to_colorstring(lua_State *L)
|
|||
return 0;
|
||||
}
|
||||
|
||||
// colorspec_to_colorint(colorspec)
|
||||
int ModApiUtil::l_colorspec_to_colorint(lua_State *L)
|
||||
{
|
||||
NO_MAP_LOCK_REQUIRED;
|
||||
|
||||
video::SColor color(0);
|
||||
if (read_color(L, 1, &color)) {
|
||||
lua_pushnumber(L, color.color);
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// colorspec_to_bytes(colorspec)
|
||||
int ModApiUtil::l_colorspec_to_bytes(lua_State *L)
|
||||
{
|
||||
|
@ -625,6 +639,183 @@ int ModApiUtil::l_colorspec_to_bytes(lua_State *L)
|
|||
return 0;
|
||||
}
|
||||
|
||||
// encode_network(format, ...)
|
||||
int ModApiUtil::l_encode_network(lua_State *L)
|
||||
{
|
||||
NO_MAP_LOCK_REQUIRED;
|
||||
|
||||
std::string format = readParam<std::string>(L, 1);
|
||||
std::ostringstream os(std::ios_base::binary);
|
||||
|
||||
int arg = 2;
|
||||
for (size_t i = 0; i < format.size(); i++) {
|
||||
switch (format[i]) {
|
||||
case 'b':
|
||||
// Casting the double to a signed integer larger than the target
|
||||
// integer results in proper integer wraparound behavior.
|
||||
writeS8(os, (s64)luaL_checknumber(L, arg));
|
||||
break;
|
||||
case 'h':
|
||||
writeS16(os, (s64)luaL_checknumber(L, arg));
|
||||
break;
|
||||
case 'i':
|
||||
writeS32(os, (s64)luaL_checknumber(L, arg));
|
||||
break;
|
||||
case 'l':
|
||||
writeS64(os, (s64)luaL_checknumber(L, arg));
|
||||
break;
|
||||
case 'B':
|
||||
// Casting to an unsigned integer doesn't result in the proper
|
||||
// integer conversions being applied, so we still use signed.
|
||||
writeU8(os, (s64)luaL_checknumber(L, arg));
|
||||
break;
|
||||
case 'H':
|
||||
writeU16(os, (s64)luaL_checknumber(L, arg));
|
||||
break;
|
||||
case 'I':
|
||||
writeU32(os, (s64)luaL_checknumber(L, arg));
|
||||
break;
|
||||
case 'L':
|
||||
// For the 64-bit integers, we can never experience integer
|
||||
// overflow due to the limited range of Lua's doubles, but we can
|
||||
// have underflow, hence why we cast to s64 first.
|
||||
writeU64(os, (s64)luaL_checknumber(L, arg));
|
||||
break;
|
||||
case 'f':
|
||||
writeF32(os, luaL_checknumber(L, arg));
|
||||
break;
|
||||
case 's': {
|
||||
std::string str = readParam<std::string>(L, arg);
|
||||
os << serializeString16(str, true);
|
||||
break;
|
||||
}
|
||||
case 'S': {
|
||||
std::string str = readParam<std::string>(L, arg);
|
||||
os << serializeString32(str, true);
|
||||
break;
|
||||
}
|
||||
case 'z': {
|
||||
std::string str = readParam<std::string>(L, arg);
|
||||
os << str.substr(0, strlen(str.c_str())) << '\0';
|
||||
break;
|
||||
}
|
||||
case 'Z':
|
||||
os << readParam<std::string>(L, arg);
|
||||
break;
|
||||
case ' ':
|
||||
// Continue because we don't want to increment arg.
|
||||
continue;
|
||||
default:
|
||||
throw LuaError("Invalid format string");
|
||||
}
|
||||
|
||||
arg++;
|
||||
}
|
||||
|
||||
std::string data = os.str();
|
||||
lua_pushlstring(L, data.c_str(), data.size());
|
||||
return 1;
|
||||
}
|
||||
|
||||
// decode_network(format, data)
|
||||
int ModApiUtil::l_decode_network(lua_State *L)
|
||||
{
|
||||
NO_MAP_LOCK_REQUIRED;
|
||||
|
||||
std::string format = readParam<std::string>(L, 1);
|
||||
std::string data = readParam<std::string>(L, 2);
|
||||
std::istringstream is(data, std::ios_base::binary);
|
||||
|
||||
// Make sure we have space for all our returned arguments.
|
||||
lua_checkstack(L, format.size());
|
||||
|
||||
// Set up tracking for verbatim strings and the number of return values.
|
||||
int num_args = lua_gettop(L);
|
||||
int arg = 3;
|
||||
int ret = 0;
|
||||
|
||||
for (size_t i = 0; i < format.size(); i++) {
|
||||
switch (format[i]) {
|
||||
case 'b':
|
||||
lua_pushnumber(L, readS8(is));
|
||||
break;
|
||||
case 'h':
|
||||
lua_pushnumber(L, readS16(is));
|
||||
break;
|
||||
case 'i':
|
||||
lua_pushnumber(L, readS32(is));
|
||||
break;
|
||||
case 'l':
|
||||
lua_pushnumber(L, readS64(is));
|
||||
break;
|
||||
case 'B':
|
||||
lua_pushnumber(L, readU8(is));
|
||||
break;
|
||||
case 'H':
|
||||
lua_pushnumber(L, readU16(is));
|
||||
break;
|
||||
case 'I':
|
||||
lua_pushnumber(L, readU32(is));
|
||||
break;
|
||||
case 'L':
|
||||
lua_pushnumber(L, readU64(is));
|
||||
break;
|
||||
case 'f':
|
||||
lua_pushnumber(L, readF32(is));
|
||||
break;
|
||||
case 's': {
|
||||
std::string str = deSerializeString16(is, true);
|
||||
lua_pushlstring(L, str.c_str(), str.size());
|
||||
break;
|
||||
}
|
||||
case 'S': {
|
||||
std::string str = deSerializeString32(is, true);
|
||||
lua_pushlstring(L, str.c_str(), str.size());
|
||||
break;
|
||||
}
|
||||
case 'z': {
|
||||
std::string str;
|
||||
std::getline(is, str, '\0');
|
||||
|
||||
lua_pushlstring(L, str.c_str(), str.size());
|
||||
break;
|
||||
}
|
||||
case 'Z': {
|
||||
if (arg > num_args) {
|
||||
throw LuaError("Missing verbatim string size");
|
||||
}
|
||||
|
||||
double size = luaL_checknumber(L, arg);
|
||||
std::string str;
|
||||
|
||||
if (size < 0) {
|
||||
// Read the entire rest of the input stream.
|
||||
std::ostringstream os(std::ios_base::binary);
|
||||
os << is.rdbuf();
|
||||
str = os.str();
|
||||
} else if (size != 0) {
|
||||
// Read the specified number of characters.
|
||||
str.resize(size);
|
||||
is.read(&str[0], size);
|
||||
}
|
||||
|
||||
lua_pushlstring(L, str.c_str(), str.size());
|
||||
arg++;
|
||||
break;
|
||||
}
|
||||
case ' ':
|
||||
// Continue because we don't want to increment ret.
|
||||
continue;
|
||||
default:
|
||||
throw LuaError("Invalid format string");
|
||||
}
|
||||
|
||||
ret++;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// encode_png(w, h, data, level)
|
||||
int ModApiUtil::l_encode_png(lua_State *L)
|
||||
{
|
||||
|
@ -714,8 +905,12 @@ void ModApiUtil::Initialize(lua_State *L, int top)
|
|||
API_FCT(sha1);
|
||||
API_FCT(sha256);
|
||||
API_FCT(colorspec_to_colorstring);
|
||||
API_FCT(colorspec_to_colorint);
|
||||
API_FCT(colorspec_to_bytes);
|
||||
|
||||
API_FCT(encode_network);
|
||||
API_FCT(decode_network);
|
||||
|
||||
API_FCT(encode_png);
|
||||
|
||||
API_FCT(get_last_run_mod);
|
||||
|
@ -748,11 +943,15 @@ void ModApiUtil::InitializeClient(lua_State *L, int top)
|
|||
API_FCT(sha1);
|
||||
API_FCT(sha256);
|
||||
API_FCT(colorspec_to_colorstring);
|
||||
API_FCT(colorspec_to_colorint);
|
||||
API_FCT(colorspec_to_bytes);
|
||||
|
||||
API_FCT(get_last_run_mod);
|
||||
API_FCT(set_last_run_mod);
|
||||
|
||||
API_FCT(encode_network);
|
||||
API_FCT(decode_network);
|
||||
|
||||
API_FCT(urlencode);
|
||||
|
||||
LuaSettings::create(L, g_settings, g_settings_path);
|
||||
|
@ -792,8 +991,12 @@ void ModApiUtil::InitializeAsync(lua_State *L, int top)
|
|||
API_FCT(sha1);
|
||||
API_FCT(sha256);
|
||||
API_FCT(colorspec_to_colorstring);
|
||||
API_FCT(colorspec_to_colorint);
|
||||
API_FCT(colorspec_to_bytes);
|
||||
|
||||
API_FCT(encode_network);
|
||||
API_FCT(decode_network);
|
||||
|
||||
API_FCT(encode_png);
|
||||
|
||||
API_FCT(get_last_run_mod);
|
||||
|
|
|
@ -20,6 +20,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
#pragma once
|
||||
|
||||
#include "lua_api/l_base.h"
|
||||
#include "util/serialize.h"
|
||||
|
||||
class AsyncEngine;
|
||||
|
||||
|
@ -119,9 +120,18 @@ private:
|
|||
// colorspec_to_colorstring(colorspec)
|
||||
static int l_colorspec_to_colorstring(lua_State *L);
|
||||
|
||||
// colorspec_to_colorint(colorspec)
|
||||
static int l_colorspec_to_colorint(lua_State *L);
|
||||
|
||||
// colorspec_to_bytes(colorspec)
|
||||
static int l_colorspec_to_bytes(lua_State *L);
|
||||
|
||||
// encode_network(format, ...)
|
||||
static int l_encode_network(lua_State *L);
|
||||
|
||||
// decode_network(format, data)
|
||||
static int l_decode_network(lua_State *L);
|
||||
|
||||
// encode_png(w, h, data, level)
|
||||
static int l_encode_png(lua_State *L);
|
||||
|
||||
|
|
|
@ -3364,6 +3364,19 @@ bool Server::showFormspec(const char *playername, const std::string &formspec,
|
|||
return true;
|
||||
}
|
||||
|
||||
void Server::sendUiMessage(const char *name, const char *data, size_t len)
|
||||
{
|
||||
RemotePlayer *player = m_env->getPlayer(name);
|
||||
if (!player) {
|
||||
return;
|
||||
}
|
||||
|
||||
NetworkPacket pkt(TOCLIENT_UI_MESSAGE, 0, player->getPeerId());
|
||||
pkt.putRawString(data, len);
|
||||
|
||||
Send(&pkt);
|
||||
}
|
||||
|
||||
u32 Server::hudAdd(RemotePlayer *player, HudElement *form)
|
||||
{
|
||||
if (!player)
|
||||
|
|
|
@ -330,6 +330,8 @@ public:
|
|||
void addShutdownError(const ModError &e);
|
||||
|
||||
bool showFormspec(const char *name, const std::string &formspec, const std::string &formname);
|
||||
void sendUiMessage(const char *name, const char *data, size_t len);
|
||||
|
||||
Map & getMap() { return m_env->getMap(); }
|
||||
ServerEnvironment & getEnv() { return *m_env; }
|
||||
v3f findSpawnPos();
|
||||
|
|
|
@ -34,39 +34,54 @@ FloatType g_serialize_f32_type = FLOATTYPE_UNKNOWN;
|
|||
//// String
|
||||
////
|
||||
|
||||
std::string serializeString16(std::string_view plain)
|
||||
std::string serializeString16(std::string_view plain, bool truncate)
|
||||
{
|
||||
std::string s;
|
||||
char buf[2];
|
||||
size_t size = plain.size();
|
||||
|
||||
if (plain.size() > STRING_MAX_LEN)
|
||||
throw SerializationError("String too long for serializeString16");
|
||||
s.reserve(2 + plain.size());
|
||||
if (size > STRING_MAX_LEN) {
|
||||
if (truncate) {
|
||||
size = STRING_MAX_LEN;
|
||||
} else {
|
||||
throw SerializationError("String too long for serializeString16");
|
||||
}
|
||||
}
|
||||
|
||||
writeU16((u8 *)&buf[0], plain.size());
|
||||
s.append(buf, 2);
|
||||
char size_buf[2];
|
||||
writeU16((u8 *)size_buf, size);
|
||||
|
||||
s.reserve(2 + size);
|
||||
s.append(size_buf, 2);
|
||||
s.append(plain.substr(0, size));
|
||||
|
||||
s.append(plain);
|
||||
return s;
|
||||
}
|
||||
|
||||
std::string deSerializeString16(std::istream &is)
|
||||
std::string deSerializeString16(std::istream &is, bool truncate)
|
||||
{
|
||||
std::string s;
|
||||
char buf[2];
|
||||
char size_buf[2];
|
||||
|
||||
is.read(buf, 2);
|
||||
if (is.gcount() != 2)
|
||||
is.read(size_buf, 2);
|
||||
if (is.gcount() != 2) {
|
||||
if (truncate) {
|
||||
return s;
|
||||
}
|
||||
throw SerializationError("deSerializeString16: size not read");
|
||||
}
|
||||
|
||||
u16 s_size = readU16((u8 *)buf);
|
||||
if (s_size == 0)
|
||||
u16 size = readU16((u8 *)size_buf);
|
||||
if (size == 0) {
|
||||
return s;
|
||||
}
|
||||
|
||||
s.resize(s_size);
|
||||
is.read(&s[0], s_size);
|
||||
if (is.gcount() != s_size)
|
||||
s.resize(size);
|
||||
is.read(&s[0], size);
|
||||
if (truncate) {
|
||||
s.resize(is.gcount());
|
||||
} else if (is.gcount() != size) {
|
||||
throw SerializationError("deSerializeString16: couldn't read all chars");
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
@ -76,44 +91,72 @@ std::string deSerializeString16(std::istream &is)
|
|||
//// Long String
|
||||
////
|
||||
|
||||
std::string serializeString32(std::string_view plain)
|
||||
std::string serializeString32(std::string_view plain, bool truncate)
|
||||
{
|
||||
std::string s;
|
||||
char buf[4];
|
||||
size_t size = plain.size();
|
||||
|
||||
if (plain.size() > LONG_STRING_MAX_LEN)
|
||||
throw SerializationError("String too long for serializeLongString");
|
||||
s.reserve(4 + plain.size());
|
||||
if (size > LONG_STRING_MAX_LEN) {
|
||||
if (truncate) {
|
||||
size = LONG_STRING_MAX_LEN;
|
||||
} else {
|
||||
throw SerializationError("String too long for serializeString32");
|
||||
}
|
||||
}
|
||||
|
||||
char size_buf[4];
|
||||
writeU32((u8 *)size_buf, size);
|
||||
|
||||
s.reserve(4 + size);
|
||||
s.append(size_buf, 4);
|
||||
s.append(plain.substr(0, size));
|
||||
|
||||
writeU32((u8*)&buf[0], plain.size());
|
||||
s.append(buf, 4);
|
||||
s.append(plain);
|
||||
return s;
|
||||
}
|
||||
|
||||
std::string deSerializeString32(std::istream &is)
|
||||
std::string deSerializeString32(std::istream &is, bool truncate)
|
||||
{
|
||||
std::string s;
|
||||
char buf[4];
|
||||
char size_buf[4];
|
||||
|
||||
is.read(buf, 4);
|
||||
if (is.gcount() != 4)
|
||||
throw SerializationError("deSerializeLongString: size not read");
|
||||
|
||||
u32 s_size = readU32((u8 *)buf);
|
||||
if (s_size == 0)
|
||||
return s;
|
||||
|
||||
// We don't really want a remote attacker to force us to allocate 4GB...
|
||||
if (s_size > LONG_STRING_MAX_LEN) {
|
||||
throw SerializationError("deSerializeLongString: "
|
||||
"string too long: " + itos(s_size) + " bytes");
|
||||
is.read(size_buf, 4);
|
||||
if (is.gcount() != 4) {
|
||||
if (truncate) {
|
||||
return s;
|
||||
}
|
||||
throw SerializationError("deSerializeString32: size not read");
|
||||
}
|
||||
|
||||
s.resize(s_size);
|
||||
is.read(&s[0], s_size);
|
||||
if ((u32)is.gcount() != s_size)
|
||||
throw SerializationError("deSerializeLongString: couldn't read all chars");
|
||||
u32 size = readU32((u8 *)size_buf);
|
||||
u32 ignore = 0;
|
||||
if (size == 0) {
|
||||
return s;
|
||||
}
|
||||
|
||||
if (size > LONG_STRING_MAX_LEN) {
|
||||
if (truncate) {
|
||||
ignore = size - LONG_STRING_MAX_LEN;
|
||||
size = LONG_STRING_MAX_LEN;
|
||||
} else {
|
||||
// We don't really want a remote attacker to force us to allocate 4GB...
|
||||
throw SerializationError("deSerializeString32: "
|
||||
"string too long: " + itos(size) + " bytes");
|
||||
}
|
||||
}
|
||||
|
||||
s.resize(size);
|
||||
is.read(&s[0], size);
|
||||
if (truncate) {
|
||||
s.resize(is.gcount());
|
||||
} else if (is.gcount() != size) {
|
||||
throw SerializationError("deSerializeString32: couldn't read all chars");
|
||||
}
|
||||
|
||||
// If the string was truncated due to exceeding the string max length, we
|
||||
// need to ignore the rest of the characters.
|
||||
if (truncate) {
|
||||
is.seekg(ignore, std::ios_base::cur);
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
|
|
@ -461,16 +461,16 @@ inline v3f clampToF1000(v3f v)
|
|||
}
|
||||
|
||||
// Creates a string with the length as the first two bytes
|
||||
std::string serializeString16(std::string_view plain);
|
||||
std::string serializeString16(std::string_view plain, bool truncate = false);
|
||||
|
||||
// Reads a string with the length as the first two bytes
|
||||
std::string deSerializeString16(std::istream &is);
|
||||
std::string deSerializeString16(std::istream &is, bool truncate = false);
|
||||
|
||||
// Creates a string with the length as the first four bytes
|
||||
std::string serializeString32(std::string_view plain);
|
||||
std::string serializeString32(std::string_view plain, bool truncate = false);
|
||||
|
||||
// Reads a string with the length as the first four bytes
|
||||
std::string deSerializeString32(std::istream &is);
|
||||
std::string deSerializeString32(std::istream &is, bool truncate = false);
|
||||
|
||||
// Creates a string encoded in JSON format (almost equivalent to a C string literal)
|
||||
std::string serializeJsonString(std::string_view plain);
|
||||
|
|
Loading…
Reference in New Issue