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",
|
"PerlinNoise", "PerlinNoiseMap",
|
||||||
|
|
||||||
string = {fields = {"split", "trim"}},
|
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"}},
|
math = {fields = {"hypot", "round"}},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -489,6 +489,15 @@ function table.copy(t, seen)
|
||||||
end
|
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)
|
function table.insert_all(t, other)
|
||||||
if table.move then -- LuaJIT
|
if table.move then -- LuaJIT
|
||||||
return table.move(other, 1, #other, #t + 1, t)
|
return table.move(other, 1, #other, #t + 1, t)
|
||||||
|
@ -500,6 +509,15 @@ function table.insert_all(t, other)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function table.merge(...)
|
||||||
|
local new = {}
|
||||||
|
for _, t in ipairs{...} do
|
||||||
|
table.insert_all(new, t)
|
||||||
|
end
|
||||||
|
return new
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
function table.key_value_swap(t)
|
function table.key_value_swap(t)
|
||||||
local ti = {}
|
local ti = {}
|
||||||
for k,v in pairs(t) do
|
for k,v in pairs(t) do
|
||||||
|
@ -763,3 +781,28 @@ function core.parse_coordinates(x, y, z, relative_to)
|
||||||
local rz = core.parse_relative_number(z, relative_to.z)
|
local rz = core.parse_relative_number(z, relative_to.z)
|
||||||
return rx and ry and rz and { x = rx, y = ry, z = rz }
|
return rx and ry and rz and { x = rx, y = ry, z = rz }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function 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 scriptpath = core.get_builtin_path()
|
||||||
local commonpath = scriptpath .. "common" .. DIR_DELIM
|
local commonpath = scriptpath .. "common" .. DIR_DELIM
|
||||||
local gamepath = scriptpath .. "game".. DIR_DELIM
|
local gamepath = scriptpath .. "game".. DIR_DELIM
|
||||||
|
local uipath = scriptpath .. "ui" .. DIR_DELIM
|
||||||
|
|
||||||
-- Shared between builtin files, but
|
-- Shared between builtin files, but
|
||||||
-- not exposed to outer context
|
-- not exposed to outer context
|
||||||
|
@ -38,6 +39,7 @@ dofile(gamepath .. "forceloading.lua")
|
||||||
dofile(gamepath .. "hud.lua")
|
dofile(gamepath .. "hud.lua")
|
||||||
dofile(gamepath .. "knockback.lua")
|
dofile(gamepath .. "knockback.lua")
|
||||||
dofile(gamepath .. "async.lua")
|
dofile(gamepath .. "async.lua")
|
||||||
|
dofile(uipath .. "init.lua")
|
||||||
|
|
||||||
core.after(0, builtin_shared.cache_content_ids)
|
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()`
|
* `minetest.get_us_time()`
|
||||||
* returns time with microsecond precision. May not return wall time.
|
* returns time with microsecond precision. May not return wall time.
|
||||||
* `table.copy(table)`: returns a table
|
* `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
|
* `table.indexof(list, val)`: returns the smallest numerical index containing
|
||||||
the value `val` in the table `list`. Non-numerical indices are ignored.
|
the value `val` in the table `list`. Non-numerical indices are ignored.
|
||||||
If `val` could not be found, `-1` is returned. `list` must not have
|
If `val` could not be found, `-1` is returned. `list` must not have
|
||||||
|
@ -4023,6 +4027,9 @@ Helper functions
|
||||||
* `table.insert_all(table, other_table)`:
|
* `table.insert_all(table, other_table)`:
|
||||||
* Appends all values in `other_table` to `table` - uses `#table + 1` to
|
* Appends all values in `other_table` to `table` - uses `#table + 1` to
|
||||||
find new indices.
|
find new indices.
|
||||||
|
* `table.merge(...)`:
|
||||||
|
* Merges multiple tables together into a new single table using
|
||||||
|
`table.insert_all()`.
|
||||||
* `table.key_value_swap(t)`: returns a table with keys and values swapped
|
* `table.key_value_swap(t)`: returns a table with keys and values swapped
|
||||||
* If multiple keys in `t` map to the same value, it is unspecified which
|
* If multiple keys in `t` map to the same value, it is unspecified which
|
||||||
value maps to that key.
|
value maps to that key.
|
||||||
|
@ -5565,6 +5572,9 @@ Utilities
|
||||||
* `minetest.colorspec_to_colorstring(colorspec)`: Converts a ColorSpec to a
|
* `minetest.colorspec_to_colorstring(colorspec)`: Converts a ColorSpec to a
|
||||||
ColorString. If the ColorSpec is invalid, returns `nil`.
|
ColorString. If the ColorSpec is invalid, returns `nil`.
|
||||||
* `colorspec`: The ColorSpec to convert
|
* `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
|
* `minetest.colorspec_to_bytes(colorspec)`: Converts a ColorSpec to a raw
|
||||||
string of four bytes in an RGBA layout, returned as a string.
|
string of four bytes in an RGBA layout, returned as a string.
|
||||||
* `colorspec`: The ColorSpec to convert
|
* `colorspec`: The ColorSpec to convert
|
||||||
|
@ -5573,8 +5583,8 @@ Utilities
|
||||||
* `width`: Width of the image
|
* `width`: Width of the image
|
||||||
* `height`: Height of the image
|
* `height`: Height of the image
|
||||||
* `data`: Image data, one of:
|
* `data`: Image data, one of:
|
||||||
* array table of ColorSpec, length must be width*height
|
* array table of ColorSpec, length must be `width * height`
|
||||||
* string with raw RGBA pixels, length must be width*height*4
|
* string with raw RGBA pixels, length must be `width * height * 4`
|
||||||
* `compression`: Optional zlib compression level, number in range 0 to 9.
|
* `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
|
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.
|
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
|
* `minetest.urlencode(str)`: Encodes reserved URI characters by a
|
||||||
percent sign followed by two hex digits. See
|
percent sign followed by two hex digits. See
|
||||||
[RFC 3986, section 2.3](https://datatracker.ietf.org/doc/html/rfc3986#section-2.3).
|
[RFC 3986, section 2.3](https://datatracker.ietf.org/doc/html/rfc3986#section-2.3).
|
||||||
|
* `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
|
Logging
|
||||||
-------
|
-------
|
||||||
|
@ -7121,6 +7168,50 @@ Misc.
|
||||||
* Example: `deserialize('print("foo")')`, returns `nil`
|
* Example: `deserialize('print("foo")')`, returns `nil`
|
||||||
(function call fails), returns
|
(function call fails), returns
|
||||||
`error:[string "print("foo")"]:1: attempt to call global 'print' (a nil value)`
|
`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`
|
* `minetest.compress(data, method, ...)`: returns `compressed_data`
|
||||||
* Compress a string of data.
|
* Compress a string of data.
|
||||||
* `method` is a string identifying the compression method to be used.
|
* `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")
|
assert(#custom == 0, "custom ids not empty")
|
||||||
end
|
end
|
||||||
unittests.register("test_gennotify_api", test_gennotify_api)
|
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_InventoryFormSpec(NetworkPacket* pkt);
|
||||||
void handleCommand_DetachedInventory(NetworkPacket* pkt);
|
void handleCommand_DetachedInventory(NetworkPacket* pkt);
|
||||||
void handleCommand_ShowFormSpec(NetworkPacket* pkt);
|
void handleCommand_ShowFormSpec(NetworkPacket* pkt);
|
||||||
|
void handleCommand_UiMessage(NetworkPacket* pkt);
|
||||||
void handleCommand_SpawnParticle(NetworkPacket* pkt);
|
void handleCommand_SpawnParticle(NetworkPacket* pkt);
|
||||||
void handleCommand_AddParticleSpawner(NetworkPacket* pkt);
|
void handleCommand_AddParticleSpawner(NetworkPacket* pkt);
|
||||||
void handleCommand_DeleteParticleSpawner(NetworkPacket* pkt);
|
void handleCommand_DeleteParticleSpawner(NetworkPacket* pkt);
|
||||||
|
|
|
@ -37,6 +37,7 @@ enum ClientEventType : u8
|
||||||
CE_DEATHSCREEN,
|
CE_DEATHSCREEN,
|
||||||
CE_SHOW_FORMSPEC,
|
CE_SHOW_FORMSPEC,
|
||||||
CE_SHOW_LOCAL_FORMSPEC,
|
CE_SHOW_LOCAL_FORMSPEC,
|
||||||
|
CE_UI_MESSAGE,
|
||||||
CE_SPAWN_PARTICLE,
|
CE_SPAWN_PARTICLE,
|
||||||
CE_ADD_PARTICLESPAWNER,
|
CE_ADD_PARTICLESPAWNER,
|
||||||
CE_DELETE_PARTICLESPAWNER,
|
CE_DELETE_PARTICLESPAWNER,
|
||||||
|
@ -106,6 +107,10 @@ struct ClientEvent
|
||||||
std::string *formspec;
|
std::string *formspec;
|
||||||
std::string *formname;
|
std::string *formname;
|
||||||
} show_formspec;
|
} show_formspec;
|
||||||
|
struct
|
||||||
|
{
|
||||||
|
std::string *data;
|
||||||
|
} ui_message;
|
||||||
// struct{
|
// struct{
|
||||||
//} textures_updated;
|
//} textures_updated;
|
||||||
ParticleParameters *spawn_particle;
|
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/guiOpenURL.h"
|
||||||
#include "gui/guiVolumeChange.h"
|
#include "gui/guiVolumeChange.h"
|
||||||
#include "gui/mainmenumanager.h"
|
#include "gui/mainmenumanager.h"
|
||||||
|
#include "gui/manager.h"
|
||||||
#include "gui/profilergraph.h"
|
#include "gui/profilergraph.h"
|
||||||
#include "mapblock.h"
|
#include "mapblock.h"
|
||||||
#include "minimap.h"
|
#include "minimap.h"
|
||||||
|
@ -835,6 +836,7 @@ private:
|
||||||
void handleClientEvent_Deathscreen(ClientEvent *event, CameraOrientation *cam);
|
void handleClientEvent_Deathscreen(ClientEvent *event, CameraOrientation *cam);
|
||||||
void handleClientEvent_ShowFormSpec(ClientEvent *event, CameraOrientation *cam);
|
void handleClientEvent_ShowFormSpec(ClientEvent *event, CameraOrientation *cam);
|
||||||
void handleClientEvent_ShowLocalFormSpec(ClientEvent *event, CameraOrientation *cam);
|
void handleClientEvent_ShowLocalFormSpec(ClientEvent *event, CameraOrientation *cam);
|
||||||
|
void handleClientEvent_UiMessage(ClientEvent *event, CameraOrientation *cam);
|
||||||
void handleClientEvent_HandleParticleEvent(ClientEvent *event,
|
void handleClientEvent_HandleParticleEvent(ClientEvent *event,
|
||||||
CameraOrientation *cam);
|
CameraOrientation *cam);
|
||||||
void handleClientEvent_HudAdd(ClientEvent *event, CameraOrientation *cam);
|
void handleClientEvent_HudAdd(ClientEvent *event, CameraOrientation *cam);
|
||||||
|
@ -883,6 +885,7 @@ private:
|
||||||
|
|
||||||
std::unique_ptr<GameUI> m_game_ui;
|
std::unique_ptr<GameUI> m_game_ui;
|
||||||
GUIChatConsole *gui_chat_console = nullptr; // Free using ->Drop()
|
GUIChatConsole *gui_chat_console = nullptr; // Free using ->Drop()
|
||||||
|
ui::GUIManagerElem *gui_manager_elem = nullptr; // Free using ->Drop()
|
||||||
MapDrawControl *draw_control = nullptr;
|
MapDrawControl *draw_control = nullptr;
|
||||||
Camera *camera = nullptr;
|
Camera *camera = nullptr;
|
||||||
Clouds *clouds = nullptr; // Free using ->Drop()
|
Clouds *clouds = nullptr; // Free using ->Drop()
|
||||||
|
@ -1224,6 +1227,8 @@ void Game::shutdown()
|
||||||
if (formspec)
|
if (formspec)
|
||||||
formspec->quitMenu();
|
formspec->quitMenu();
|
||||||
|
|
||||||
|
ui::g_manager.reset();
|
||||||
|
|
||||||
// Clear text when exiting.
|
// Clear text when exiting.
|
||||||
m_game_ui->clearText();
|
m_game_ui->clearText();
|
||||||
|
|
||||||
|
@ -1237,6 +1242,8 @@ void Game::shutdown()
|
||||||
|
|
||||||
if (gui_chat_console)
|
if (gui_chat_console)
|
||||||
gui_chat_console->drop();
|
gui_chat_console->drop();
|
||||||
|
if (gui_manager_elem)
|
||||||
|
gui_manager_elem->drop();
|
||||||
|
|
||||||
if (sky)
|
if (sky)
|
||||||
sky->drop();
|
sky->drop();
|
||||||
|
@ -1538,6 +1545,8 @@ bool Game::createClient(const GameStartData &start_data)
|
||||||
if (mapper && client->modsLoaded())
|
if (mapper && client->modsLoaded())
|
||||||
client->getScript()->on_minimap_ready(mapper);
|
client->getScript()->on_minimap_ready(mapper);
|
||||||
|
|
||||||
|
ui::g_manager.setClient(client);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1555,6 +1564,9 @@ bool Game::initGui()
|
||||||
gui_chat_console = new GUIChatConsole(guienv, guienv->getRootGUIElement(),
|
gui_chat_console = new GUIChatConsole(guienv, guienv->getRootGUIElement(),
|
||||||
-1, chat_backend, client, &g_menumgr);
|
-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)
|
if (g_touchscreengui)
|
||||||
g_touchscreengui->init(texture_src);
|
g_touchscreengui->init(texture_src);
|
||||||
|
|
||||||
|
@ -2793,6 +2805,7 @@ const ClientEventHandler Game::clientEventHandler[CLIENTEVENT_MAX] = {
|
||||||
{&Game::handleClientEvent_Deathscreen},
|
{&Game::handleClientEvent_Deathscreen},
|
||||||
{&Game::handleClientEvent_ShowFormSpec},
|
{&Game::handleClientEvent_ShowFormSpec},
|
||||||
{&Game::handleClientEvent_ShowLocalFormSpec},
|
{&Game::handleClientEvent_ShowLocalFormSpec},
|
||||||
|
{&Game::handleClientEvent_UiMessage},
|
||||||
{&Game::handleClientEvent_HandleParticleEvent},
|
{&Game::handleClientEvent_HandleParticleEvent},
|
||||||
{&Game::handleClientEvent_HandleParticleEvent},
|
{&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;
|
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,
|
void Game::handleClientEvent_HandleParticleEvent(ClientEvent *event,
|
||||||
CameraOrientation *cam)
|
CameraOrientation *cam)
|
||||||
{
|
{
|
||||||
|
@ -4310,7 +4329,7 @@ void Game::drawScene(ProfilerGraph *graph, RunStats *stats)
|
||||||
draw_crosshair = false;
|
draw_crosshair = false;
|
||||||
|
|
||||||
this->m_rendering_engine->draw_scene(sky_color, this->m_game_ui->m_flags.show_hud,
|
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
|
Profiler graph
|
||||||
|
|
|
@ -36,7 +36,7 @@ RenderingCore::~RenderingCore()
|
||||||
delete shadow_renderer;
|
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)
|
bool _draw_wield_tool, bool _draw_crosshair)
|
||||||
{
|
{
|
||||||
v2u32 screensize = device->getVideoDriver()->getScreenSize();
|
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_crosshair = _draw_crosshair;
|
||||||
context.draw_wield_tool = _draw_wield_tool;
|
context.draw_wield_tool = _draw_wield_tool;
|
||||||
context.show_hud = _show_hud;
|
context.show_hud = _show_hud;
|
||||||
|
context.show_chat = _show_chat;
|
||||||
|
|
||||||
pipeline->reset(context);
|
pipeline->reset(context);
|
||||||
pipeline->run(context);
|
pipeline->run(context);
|
||||||
|
|
|
@ -53,7 +53,7 @@ public:
|
||||||
RenderingCore &operator=(const RenderingCore &) = delete;
|
RenderingCore &operator=(const RenderingCore &) = delete;
|
||||||
RenderingCore &operator=(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);
|
bool _draw_wield_tool, bool _draw_crosshair);
|
||||||
|
|
||||||
v2u32 getVirtualSize() const;
|
v2u32 getVirtualSize() const;
|
||||||
|
|
|
@ -46,6 +46,7 @@ struct PipelineContext
|
||||||
v2u32 target_size;
|
v2u32 target_size;
|
||||||
|
|
||||||
bool show_hud {true};
|
bool show_hud {true};
|
||||||
|
bool show_chat {true};
|
||||||
bool draw_wield_tool {true};
|
bool draw_wield_tool {true};
|
||||||
bool draw_crosshair {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/hud.h"
|
||||||
#include "client/minimap.h"
|
#include "client/minimap.h"
|
||||||
#include "client/shadows/dynamicshadowsrender.h"
|
#include "client/shadows/dynamicshadowsrender.h"
|
||||||
|
#include "gui/manager.h"
|
||||||
|
|
||||||
/// Draw3D pipeline step
|
/// Draw3D pipeline step
|
||||||
void Draw3D::run(PipelineContext &context)
|
void Draw3D::run(PipelineContext &context)
|
||||||
|
@ -43,6 +44,9 @@ void Draw3D::run(PipelineContext &context)
|
||||||
|
|
||||||
void DrawWield::run(PipelineContext &context)
|
void DrawWield::run(PipelineContext &context)
|
||||||
{
|
{
|
||||||
|
ui::g_manager.preDraw();
|
||||||
|
ui::g_manager.drawType(ui::WindowType::BG);
|
||||||
|
|
||||||
if (m_target)
|
if (m_target)
|
||||||
m_target->activate(context);
|
m_target->activate(context);
|
||||||
|
|
||||||
|
@ -60,12 +64,24 @@ void DrawHUD::run(PipelineContext &context)
|
||||||
|
|
||||||
if (context.draw_crosshair)
|
if (context.draw_crosshair)
|
||||||
context.hud->drawCrosshair();
|
context.hud->drawCrosshair();
|
||||||
|
}
|
||||||
|
|
||||||
|
ui::g_manager.drawType(ui::WindowType::MASK);
|
||||||
|
|
||||||
|
if (context.show_hud) {
|
||||||
context.hud->drawHotbar(context.client->getEnv().getLocalPlayer()->getWieldIndex());
|
context.hud->drawHotbar(context.client->getEnv().getLocalPlayer()->getWieldIndex());
|
||||||
|
|
||||||
context.hud->drawLuaElements(context.client->getCamera()->getOffset());
|
context.hud->drawLuaElements(context.client->getCamera()->getOffset());
|
||||||
|
ui::g_manager.drawType(ui::WindowType::HUD);
|
||||||
|
|
||||||
context.client->getCamera()->drawNametags();
|
context.client->getCamera()->drawNametags();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (context.show_chat)
|
||||||
|
ui::g_manager.drawType(ui::WindowType::MESSAGE);
|
||||||
|
|
||||||
context.device->getGUIEnvironment()->drawAll();
|
context.device->getGUIEnvironment()->drawAll();
|
||||||
|
ui::g_manager.drawType(ui::WindowType::FG);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -411,10 +411,10 @@ void RenderingEngine::finalize()
|
||||||
core.reset();
|
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)
|
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)
|
const VideoDriverInfo &RenderingEngine::getVideoDriverInfo(irr::video::E_DRIVER_TYPE type)
|
||||||
|
|
|
@ -141,7 +141,7 @@ public:
|
||||||
gui::IGUIEnvironment *guienv, ITextureSource *tsrc,
|
gui::IGUIEnvironment *guienv, ITextureSource *tsrc,
|
||||||
float dtime = 0, int percent = 0, bool sky = true);
|
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);
|
bool draw_wield_tool, bool draw_crosshair);
|
||||||
|
|
||||||
void initialize(Client *client, Hud *hud);
|
void initialize(Client *client, Hud *hud);
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
set(gui_SRCS
|
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}/guiAnimatedImage.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/guiBackgroundImage.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/guiBackgroundImage.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/guiBox.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/guiBox.cpp
|
||||||
|
@ -23,8 +26,11 @@ set(gui_SRCS
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/guiTable.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/guiTable.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/guiHyperText.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/guiHyperText.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/guiVolumeChange.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/guiVolumeChange.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/manager.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/modalMenu.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/modalMenu.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/profilergraph.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/profilergraph.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/texture.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/touchscreengui.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/touchscreengui.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/window.cpp
|
||||||
PARENT_SCOPE
|
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_MOON", TOCLIENT_STATE_CONNECTED, &Client::handleCommand_HudSetMoon }, // 0x5b
|
||||||
{ "TOCLIENT_SET_STARS", TOCLIENT_STATE_CONNECTED, &Client::handleCommand_HudSetStars }, // 0x5c
|
{ "TOCLIENT_SET_STARS", TOCLIENT_STATE_CONNECTED, &Client::handleCommand_HudSetStars }, // 0x5c
|
||||||
{ "TOCLIENT_MOVE_PLAYER_REL", TOCLIENT_STATE_CONNECTED, &Client::handleCommand_MovePlayerRel }, // 0x5d,
|
{ "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,
|
null_command_handler,
|
||||||
{ "TOCLIENT_SRP_BYTES_S_B", TOCLIENT_STATE_NOT_CONNECTED, &Client::handleCommand_SrpBytesSandB }, // 0x60
|
{ "TOCLIENT_SRP_BYTES_S_B", TOCLIENT_STATE_NOT_CONNECTED, &Client::handleCommand_SrpBytesSandB }, // 0x60
|
||||||
{ "TOCLIENT_FORMSPEC_PREPEND", TOCLIENT_STATE_CONNECTED, &Client::handleCommand_FormspecPrepend }, // 0x61,
|
{ "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);
|
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)
|
void Client::handleCommand_SpawnParticle(NetworkPacket* pkt)
|
||||||
{
|
{
|
||||||
std::string datastring(pkt->getString(0), pkt->getSize());
|
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
|
AO_CMD_SET_BONE_POSITION extended
|
||||||
Add TOCLIENT_MOVE_PLAYER_REL
|
Add TOCLIENT_MOVE_PLAYER_REL
|
||||||
Move default minimap from client-side C++ to server-side builtin Lua
|
Move default minimap from client-side C++ to server-side builtin Lua
|
||||||
|
Add TOCLIENT_UI_MESSAGE
|
||||||
[scheduled bump for 5.9.0]
|
[scheduled bump for 5.9.0]
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@ -811,6 +812,12 @@ enum ToClientCommand : u16
|
||||||
v3f added_pos
|
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,
|
TOCLIENT_SRP_BYTES_S_B = 0x60,
|
||||||
/*
|
/*
|
||||||
Belonging to AUTH_MECHANISM_SRP.
|
Belonging to AUTH_MECHANISM_SRP.
|
||||||
|
|
|
@ -220,7 +220,7 @@ const ClientCommandFactory clientCommandFactoryTable[TOCLIENT_NUM_MSG_TYPES] =
|
||||||
{ "TOCLIENT_SET_MOON", 0, true }, // 0x5b
|
{ "TOCLIENT_SET_MOON", 0, true }, // 0x5b
|
||||||
{ "TOCLIENT_SET_STARS", 0, true }, // 0x5c
|
{ "TOCLIENT_SET_STARS", 0, true }, // 0x5c
|
||||||
{ "TOCLIENT_MOVE_PLAYER_REL", 0, true }, // 0x5d
|
{ "TOCLIENT_MOVE_PLAYER_REL", 0, true }, // 0x5d
|
||||||
null_command_factory, // 0x5e
|
{ "TOCLIENT_UI_MESSAGE", 0, true }, // 0x5e
|
||||||
null_command_factory, // 0x5f
|
null_command_factory, // 0x5f
|
||||||
{ "TOCLIENT_SRP_BYTES_S_B", 0, true }, // 0x60
|
{ "TOCLIENT_SRP_BYTES_S_B", 0, true }, // 0x60
|
||||||
{ "TOCLIENT_FORMSPEC_PREPEND", 0, true }, // 0x61
|
{ "TOCLIENT_FORMSPEC_PREPEND", 0, true }, // 0x61
|
||||||
|
|
|
@ -437,6 +437,19 @@ int ModApiServer::l_show_formspec(lua_State *L)
|
||||||
return 1;
|
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()
|
// get_current_modname()
|
||||||
int ModApiServer::l_get_current_modname(lua_State *L)
|
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_all);
|
||||||
API_FCT(chat_send_player);
|
API_FCT(chat_send_player);
|
||||||
API_FCT(show_formspec);
|
API_FCT(show_formspec);
|
||||||
|
API_FCT(send_ui_message);
|
||||||
API_FCT(sound_play);
|
API_FCT(sound_play);
|
||||||
API_FCT(sound_stop);
|
API_FCT(sound_stop);
|
||||||
API_FCT(sound_fade);
|
API_FCT(sound_fade);
|
||||||
|
|
|
@ -70,6 +70,9 @@ private:
|
||||||
// show_formspec(playername,formname,formspec)
|
// show_formspec(playername,formname,formspec)
|
||||||
static int l_show_formspec(lua_State *L);
|
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)
|
// sound_play(spec, parameters)
|
||||||
static int l_sound_play(lua_State *L);
|
static int l_sound_play(lua_State *L);
|
||||||
|
|
||||||
|
|
|
@ -605,6 +605,20 @@ int ModApiUtil::l_colorspec_to_colorstring(lua_State *L)
|
||||||
return 0;
|
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)
|
// colorspec_to_bytes(colorspec)
|
||||||
int ModApiUtil::l_colorspec_to_bytes(lua_State *L)
|
int ModApiUtil::l_colorspec_to_bytes(lua_State *L)
|
||||||
{
|
{
|
||||||
|
@ -625,6 +639,183 @@ int ModApiUtil::l_colorspec_to_bytes(lua_State *L)
|
||||||
return 0;
|
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)
|
// encode_png(w, h, data, level)
|
||||||
int ModApiUtil::l_encode_png(lua_State *L)
|
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(sha1);
|
||||||
API_FCT(sha256);
|
API_FCT(sha256);
|
||||||
API_FCT(colorspec_to_colorstring);
|
API_FCT(colorspec_to_colorstring);
|
||||||
|
API_FCT(colorspec_to_colorint);
|
||||||
API_FCT(colorspec_to_bytes);
|
API_FCT(colorspec_to_bytes);
|
||||||
|
|
||||||
|
API_FCT(encode_network);
|
||||||
|
API_FCT(decode_network);
|
||||||
|
|
||||||
API_FCT(encode_png);
|
API_FCT(encode_png);
|
||||||
|
|
||||||
API_FCT(get_last_run_mod);
|
API_FCT(get_last_run_mod);
|
||||||
|
@ -748,11 +943,15 @@ void ModApiUtil::InitializeClient(lua_State *L, int top)
|
||||||
API_FCT(sha1);
|
API_FCT(sha1);
|
||||||
API_FCT(sha256);
|
API_FCT(sha256);
|
||||||
API_FCT(colorspec_to_colorstring);
|
API_FCT(colorspec_to_colorstring);
|
||||||
|
API_FCT(colorspec_to_colorint);
|
||||||
API_FCT(colorspec_to_bytes);
|
API_FCT(colorspec_to_bytes);
|
||||||
|
|
||||||
API_FCT(get_last_run_mod);
|
API_FCT(get_last_run_mod);
|
||||||
API_FCT(set_last_run_mod);
|
API_FCT(set_last_run_mod);
|
||||||
|
|
||||||
|
API_FCT(encode_network);
|
||||||
|
API_FCT(decode_network);
|
||||||
|
|
||||||
API_FCT(urlencode);
|
API_FCT(urlencode);
|
||||||
|
|
||||||
LuaSettings::create(L, g_settings, g_settings_path);
|
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(sha1);
|
||||||
API_FCT(sha256);
|
API_FCT(sha256);
|
||||||
API_FCT(colorspec_to_colorstring);
|
API_FCT(colorspec_to_colorstring);
|
||||||
|
API_FCT(colorspec_to_colorint);
|
||||||
API_FCT(colorspec_to_bytes);
|
API_FCT(colorspec_to_bytes);
|
||||||
|
|
||||||
|
API_FCT(encode_network);
|
||||||
|
API_FCT(decode_network);
|
||||||
|
|
||||||
API_FCT(encode_png);
|
API_FCT(encode_png);
|
||||||
|
|
||||||
API_FCT(get_last_run_mod);
|
API_FCT(get_last_run_mod);
|
||||||
|
|
|
@ -20,6 +20,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "lua_api/l_base.h"
|
#include "lua_api/l_base.h"
|
||||||
|
#include "util/serialize.h"
|
||||||
|
|
||||||
class AsyncEngine;
|
class AsyncEngine;
|
||||||
|
|
||||||
|
@ -119,9 +120,18 @@ private:
|
||||||
// colorspec_to_colorstring(colorspec)
|
// colorspec_to_colorstring(colorspec)
|
||||||
static int l_colorspec_to_colorstring(lua_State *L);
|
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)
|
// colorspec_to_bytes(colorspec)
|
||||||
static int l_colorspec_to_bytes(lua_State *L);
|
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)
|
// encode_png(w, h, data, level)
|
||||||
static int l_encode_png(lua_State *L);
|
static int l_encode_png(lua_State *L);
|
||||||
|
|
||||||
|
|
|
@ -3364,6 +3364,19 @@ bool Server::showFormspec(const char *playername, const std::string &formspec,
|
||||||
return true;
|
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)
|
u32 Server::hudAdd(RemotePlayer *player, HudElement *form)
|
||||||
{
|
{
|
||||||
if (!player)
|
if (!player)
|
||||||
|
|
|
@ -330,6 +330,8 @@ public:
|
||||||
void addShutdownError(const ModError &e);
|
void addShutdownError(const ModError &e);
|
||||||
|
|
||||||
bool showFormspec(const char *name, const std::string &formspec, const std::string &formname);
|
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(); }
|
Map & getMap() { return m_env->getMap(); }
|
||||||
ServerEnvironment & getEnv() { return *m_env; }
|
ServerEnvironment & getEnv() { return *m_env; }
|
||||||
v3f findSpawnPos();
|
v3f findSpawnPos();
|
||||||
|
|
|
@ -34,39 +34,54 @@ FloatType g_serialize_f32_type = FLOATTYPE_UNKNOWN;
|
||||||
//// String
|
//// String
|
||||||
////
|
////
|
||||||
|
|
||||||
std::string serializeString16(std::string_view plain)
|
std::string serializeString16(std::string_view plain, bool truncate)
|
||||||
{
|
{
|
||||||
std::string s;
|
std::string s;
|
||||||
char buf[2];
|
size_t size = plain.size();
|
||||||
|
|
||||||
if (plain.size() > STRING_MAX_LEN)
|
if (size > STRING_MAX_LEN) {
|
||||||
throw SerializationError("String too long for serializeString16");
|
if (truncate) {
|
||||||
s.reserve(2 + plain.size());
|
size = STRING_MAX_LEN;
|
||||||
|
} else {
|
||||||
|
throw SerializationError("String too long for serializeString16");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
writeU16((u8 *)&buf[0], plain.size());
|
char size_buf[2];
|
||||||
s.append(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;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string deSerializeString16(std::istream &is)
|
std::string deSerializeString16(std::istream &is, bool truncate)
|
||||||
{
|
{
|
||||||
std::string s;
|
std::string s;
|
||||||
char buf[2];
|
char size_buf[2];
|
||||||
|
|
||||||
is.read(buf, 2);
|
is.read(size_buf, 2);
|
||||||
if (is.gcount() != 2)
|
if (is.gcount() != 2) {
|
||||||
|
if (truncate) {
|
||||||
|
return s;
|
||||||
|
}
|
||||||
throw SerializationError("deSerializeString16: size not read");
|
throw SerializationError("deSerializeString16: size not read");
|
||||||
|
}
|
||||||
|
|
||||||
u16 s_size = readU16((u8 *)buf);
|
u16 size = readU16((u8 *)size_buf);
|
||||||
if (s_size == 0)
|
if (size == 0) {
|
||||||
return s;
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
s.resize(s_size);
|
s.resize(size);
|
||||||
is.read(&s[0], s_size);
|
is.read(&s[0], size);
|
||||||
if (is.gcount() != s_size)
|
if (truncate) {
|
||||||
|
s.resize(is.gcount());
|
||||||
|
} else if (is.gcount() != size) {
|
||||||
throw SerializationError("deSerializeString16: couldn't read all chars");
|
throw SerializationError("deSerializeString16: couldn't read all chars");
|
||||||
|
}
|
||||||
|
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
@ -76,44 +91,72 @@ std::string deSerializeString16(std::istream &is)
|
||||||
//// Long String
|
//// Long String
|
||||||
////
|
////
|
||||||
|
|
||||||
std::string serializeString32(std::string_view plain)
|
std::string serializeString32(std::string_view plain, bool truncate)
|
||||||
{
|
{
|
||||||
std::string s;
|
std::string s;
|
||||||
char buf[4];
|
size_t size = plain.size();
|
||||||
|
|
||||||
if (plain.size() > LONG_STRING_MAX_LEN)
|
if (size > LONG_STRING_MAX_LEN) {
|
||||||
throw SerializationError("String too long for serializeLongString");
|
if (truncate) {
|
||||||
s.reserve(4 + plain.size());
|
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;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string deSerializeString32(std::istream &is)
|
std::string deSerializeString32(std::istream &is, bool truncate)
|
||||||
{
|
{
|
||||||
std::string s;
|
std::string s;
|
||||||
char buf[4];
|
char size_buf[4];
|
||||||
|
|
||||||
is.read(buf, 4);
|
is.read(size_buf, 4);
|
||||||
if (is.gcount() != 4)
|
if (is.gcount() != 4) {
|
||||||
throw SerializationError("deSerializeLongString: size not read");
|
if (truncate) {
|
||||||
|
return s;
|
||||||
u32 s_size = readU32((u8 *)buf);
|
}
|
||||||
if (s_size == 0)
|
throw SerializationError("deSerializeString32: size not read");
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
s.resize(s_size);
|
u32 size = readU32((u8 *)size_buf);
|
||||||
is.read(&s[0], s_size);
|
u32 ignore = 0;
|
||||||
if ((u32)is.gcount() != s_size)
|
if (size == 0) {
|
||||||
throw SerializationError("deSerializeLongString: couldn't read all chars");
|
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;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
|
@ -461,16 +461,16 @@ inline v3f clampToF1000(v3f v)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates a string with the length as the first two bytes
|
// 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
|
// 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
|
// 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
|
// 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)
|
// Creates a string encoded in JSON format (almost equivalent to a C string literal)
|
||||||
std::string serializeJsonString(std::string_view plain);
|
std::string serializeJsonString(std::string_view plain);
|
||||||
|
|
Loading…
Reference in New Issue