mirror of
https://github.com/luanti-org/luanti.git
synced 2025-06-27 16:36:03 +00:00
Merge 69c492f59b
into 2d36d32da8
This commit is contained in:
commit
b36fe7aff6
66 changed files with 8538 additions and 108 deletions
10
.luacheckrc
10
.luacheckrc
|
@ -22,7 +22,15 @@ read_globals = {
|
|||
"ValueNoise", "ValueNoiseMap",
|
||||
|
||||
string = {fields = {"split", "trim"}},
|
||||
table = {fields = {"copy", "copy_with_metatables", "getn", "indexof", "keyof", "insert_all"}},
|
||||
table = {fields = {
|
||||
"copy",
|
||||
"copy_with_metatables",
|
||||
"getn",
|
||||
"indexof",
|
||||
"keyof",
|
||||
"insert_all",
|
||||
"merge",
|
||||
}},
|
||||
math = {fields = {"hypot", "round"}},
|
||||
}
|
||||
|
||||
|
|
|
@ -39,6 +39,7 @@ set(BUILD_SERVER FALSE CACHE BOOL "Build server")
|
|||
set(BUILD_UNITTESTS TRUE CACHE BOOL "Build unittests")
|
||||
set(BUILD_BENCHMARKS FALSE CACHE BOOL "Build benchmarks")
|
||||
set(BUILD_DOCUMENTATION TRUE CACHE BOOL "Build documentation")
|
||||
set(BUILD_UI FALSE CACHE BOOL "Build experimental UI API; requires BUILD_CLIENT and USE_SDL2")
|
||||
|
||||
set(DEFAULT_ENABLE_LTO TRUE)
|
||||
# by default don't enable on Debug builds to get faster builds
|
||||
|
@ -77,6 +78,7 @@ message(STATUS "BUILD_SERVER: " ${BUILD_SERVER})
|
|||
message(STATUS "BUILD_UNITTESTS: " ${BUILD_UNITTESTS})
|
||||
message(STATUS "BUILD_BENCHMARKS: " ${BUILD_BENCHMARKS})
|
||||
message(STATUS "BUILD_DOCUMENTATION: " ${BUILD_DOCUMENTATION})
|
||||
message(STATUS "BUILD_UI: " ${BUILD_UI})
|
||||
message(STATUS "RUN_IN_PLACE: " ${RUN_IN_PLACE})
|
||||
|
||||
set(WARN_ALL TRUE CACHE BOOL "Enable -Wall for Release build")
|
||||
|
@ -398,3 +400,12 @@ if(BUILD_WITH_TRACY)
|
|||
FetchContent_MakeAvailable(tracy)
|
||||
message(STATUS "Fetching Tracy - done")
|
||||
endif()
|
||||
|
||||
if(BUILD_UI)
|
||||
if(NOT BUILD_CLIENT)
|
||||
message(FATAL_ERROR "BUILD_UI requires BUILD_CLIENT")
|
||||
endif()
|
||||
if(NOT USE_SDL2)
|
||||
message(FATAL_ERROR "BUILD_UI requires USE_SDL2")
|
||||
endif()
|
||||
endif()
|
||||
|
|
|
@ -579,6 +579,15 @@ function table.insert_all(t, other)
|
|||
end
|
||||
|
||||
|
||||
function table.merge(...)
|
||||
local new = {}
|
||||
for _, t in ipairs{...} do
|
||||
table.insert_all(new, t)
|
||||
end
|
||||
return new
|
||||
end
|
||||
|
||||
|
||||
function table.key_value_swap(t)
|
||||
local ti = {}
|
||||
for k,v in pairs(t) do
|
||||
|
@ -872,3 +881,47 @@ function core.parse_coordinates(x, y, z, relative_to)
|
|||
local rz = core.parse_relative_number(z, relative_to.z)
|
||||
return rx and ry and rz and { x = rx, y = ry, z = rz }
|
||||
end
|
||||
|
||||
local function class_try_return(obj, ...)
|
||||
if select("#", ...) ~= 0 then
|
||||
return ...
|
||||
end
|
||||
return obj
|
||||
end
|
||||
|
||||
local function class_call(class, ...)
|
||||
local obj = setmetatable({}, class)
|
||||
|
||||
if obj.new then
|
||||
return class_try_return(obj, obj:new(...))
|
||||
end
|
||||
|
||||
return obj
|
||||
end
|
||||
|
||||
function core.class(super)
|
||||
local class = setmetatable({}, {__call = class_call, __index = super})
|
||||
class.__index = class
|
||||
|
||||
return class
|
||||
end
|
||||
|
||||
function core.super(class)
|
||||
local meta = getmetatable(class)
|
||||
return meta and meta.__index
|
||||
end
|
||||
|
||||
function core.is_subclass(class, super)
|
||||
while class ~= nil do
|
||||
if class == super then
|
||||
return true
|
||||
end
|
||||
class = core.super(class)
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function core.is_instance(obj, class)
|
||||
return type(obj) == "table" and core.is_subclass(getmetatable(obj), class)
|
||||
end
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
local scriptpath = core.get_builtin_path()
|
||||
local commonpath = scriptpath .. "common" .. DIR_DELIM
|
||||
local gamepath = scriptpath .. "game".. DIR_DELIM
|
||||
local uipath = scriptpath .. "ui" .. DIR_DELIM
|
||||
|
||||
-- Shared between builtin files, but
|
||||
-- not exposed to outer context
|
||||
|
@ -39,6 +40,7 @@ dofile(gamepath .. "hud.lua")
|
|||
dofile(gamepath .. "knockback.lua")
|
||||
dofile(gamepath .. "async.lua")
|
||||
dofile(gamepath .. "death_screen.lua")
|
||||
dofile(uipath .. "init.lua")
|
||||
|
||||
core.after(0, builtin_shared.cache_content_ids)
|
||||
|
||||
|
|
106
builtin/ui/clickable_elems.lua
Normal file
106
builtin/ui/clickable_elems.lua
Normal file
|
@ -0,0 +1,106 @@
|
|||
-- Luanti
|
||||
-- SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
-- Copyright (C) 2024 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
ui.Button = ui._new_type(ui.Elem, "button", 0x02, true)
|
||||
|
||||
function ui.Button:_init(props)
|
||||
ui.Elem._init(self, props)
|
||||
|
||||
self._disabled = ui._opt(props.disabled, "boolean")
|
||||
self._on_press = ui._opt(props.on_press, "function")
|
||||
end
|
||||
|
||||
function ui.Button:_encode_fields()
|
||||
local fl = ui._make_flags()
|
||||
|
||||
ui._shift_flag(fl, self._disabled)
|
||||
ui._shift_flag(fl, self._on_press)
|
||||
|
||||
return ui._encode("SZ", ui.Elem._encode_fields(self), ui._encode_flags(fl))
|
||||
end
|
||||
|
||||
ui.Button._handlers[0x00] = function(self, ev, data)
|
||||
return self._on_press
|
||||
end
|
||||
|
||||
ui.Toggle = ui._new_type(ui.Elem, "toggle", 0x03, true)
|
||||
|
||||
ui.Check = ui.derive_elem(ui.Toggle, "check")
|
||||
ui.Switch = ui.derive_elem(ui.Toggle, "switch")
|
||||
|
||||
function ui.Toggle:_init(props)
|
||||
ui.Elem._init(self, props)
|
||||
|
||||
self._disabled = ui._opt(props.disabled, "boolean")
|
||||
self._selected = ui._opt(props.selected, "boolean")
|
||||
|
||||
self._on_press = ui._opt(props.on_press, "function")
|
||||
self._on_change = ui._opt(props.on_change, "function")
|
||||
end
|
||||
|
||||
function ui.Toggle:_encode_fields()
|
||||
local fl = ui._make_flags()
|
||||
|
||||
ui._shift_flag(fl, self._disabled)
|
||||
ui._shift_flag_bool(fl, self._selected)
|
||||
|
||||
ui._shift_flag(fl, self._on_press)
|
||||
ui._shift_flag(fl, self._on_change)
|
||||
|
||||
return ui._encode("SZ", ui.Elem._encode_fields(self), ui._encode_flags(fl))
|
||||
end
|
||||
|
||||
ui.Toggle._handlers[0x00] = function(self, ev, data)
|
||||
return self._on_press
|
||||
end
|
||||
|
||||
ui.Toggle._handlers[0x01] = function(self, ev, data)
|
||||
local selected = ui._decode("B", data)
|
||||
ev.selected = selected ~= 0
|
||||
|
||||
return self._on_change
|
||||
end
|
||||
|
||||
ui.Option = ui._new_type(ui.Elem, "option", 0x04, true)
|
||||
|
||||
ui.Radio = ui.derive_elem(ui.Option, "radio")
|
||||
|
||||
function ui.Option:_init(props)
|
||||
ui.Elem._init(self, props)
|
||||
|
||||
self._disabled = ui._opt(props.disabled, "boolean")
|
||||
self._selected = ui._opt(props.selected, "boolean")
|
||||
|
||||
self._family = ui._opt(props.family, "id")
|
||||
|
||||
self._on_press = ui._opt(props.on_press, "function")
|
||||
self._on_change = ui._opt(props.on_change, "function")
|
||||
end
|
||||
|
||||
function ui.Option:_encode_fields()
|
||||
local fl = ui._make_flags()
|
||||
|
||||
ui._shift_flag(fl, self._disabled)
|
||||
ui._shift_flag_bool(fl, self._selected)
|
||||
|
||||
if ui._shift_flag(fl, self._family) then
|
||||
ui._encode_flag(fl, "z", self._family)
|
||||
end
|
||||
|
||||
ui._shift_flag(fl, self._on_press)
|
||||
ui._shift_flag(fl, self._on_change)
|
||||
|
||||
return ui._encode("SZ", ui.Elem._encode_fields(self), ui._encode_flags(fl))
|
||||
end
|
||||
|
||||
ui.Option._handlers[0x00] = function(self, ev, data)
|
||||
return self._on_press
|
||||
end
|
||||
|
||||
ui.Option._handlers[0x01] = function(self, ev, data)
|
||||
local selected = ui._decode("B", data)
|
||||
ev.selected = selected ~= 0
|
||||
|
||||
return self._on_change
|
||||
end
|
187
builtin/ui/context.lua
Normal file
187
builtin/ui/context.lua
Normal file
|
@ -0,0 +1,187 @@
|
|||
-- Luanti
|
||||
-- SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
-- Copyright (C) 2025 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
ui.Context = core.class()
|
||||
|
||||
local open_contexts = {}
|
||||
|
||||
local OPEN_WINDOW = 0x00
|
||||
local REOPEN_WINDOW = 0x01
|
||||
local UPDATE_WINDOW = 0x02
|
||||
local CLOSE_WINDOW = 0x03
|
||||
|
||||
function ui.Context:new(builder, player, state)
|
||||
self._builder = ui._req(builder, "function")
|
||||
self._player = ui._req(player, "string")
|
||||
self._state = ui._opt(state, "table", {})
|
||||
|
||||
self._id = nil
|
||||
self._window = nil
|
||||
end
|
||||
|
||||
function ui.Context:open(param)
|
||||
if self:is_open() then
|
||||
return self
|
||||
end
|
||||
|
||||
self:_open_window()
|
||||
self:_build_window(ui._opt(param, "table", {}))
|
||||
|
||||
local data = ui._encode("BL Z", OPEN_WINDOW, self._id,
|
||||
self._window:_encode(self._player, true))
|
||||
|
||||
core.send_ui_message(self._player, data)
|
||||
return self
|
||||
end
|
||||
|
||||
function ui.Context:reopen(param)
|
||||
if not self:is_open() then
|
||||
return self
|
||||
end
|
||||
|
||||
local close_id = self:_close_window()
|
||||
self:_open_window()
|
||||
self:_build_window(ui._opt(param, "table", {}))
|
||||
|
||||
local data = ui._encode("BLL Z", REOPEN_WINDOW, self._id, close_id,
|
||||
self._window:_encode(self._player, true))
|
||||
|
||||
core.send_ui_message(self._player, data)
|
||||
return self
|
||||
end
|
||||
|
||||
function ui.Context:update(param)
|
||||
if not self:is_open() then
|
||||
return self
|
||||
end
|
||||
|
||||
self:_build_window(ui._opt(param, "table", {}))
|
||||
|
||||
local data = ui._encode("BL Z", UPDATE_WINDOW, self._id,
|
||||
self._window:_encode(self._player, false))
|
||||
|
||||
core.send_ui_message(self._player, data)
|
||||
return self
|
||||
end
|
||||
|
||||
function ui.Context:close()
|
||||
if not self:is_open() then
|
||||
return self
|
||||
end
|
||||
|
||||
local close_id = self:_close_window()
|
||||
local data = ui._encode("BL", CLOSE_WINDOW, close_id)
|
||||
|
||||
core.send_ui_message(self._player, data)
|
||||
return self
|
||||
end
|
||||
|
||||
function ui.Context:is_open()
|
||||
return self._id ~= nil
|
||||
end
|
||||
|
||||
function ui.Context:get_builder()
|
||||
return self._builder
|
||||
end
|
||||
|
||||
function ui.Context:get_player()
|
||||
return self._player
|
||||
end
|
||||
|
||||
function ui.Context:get_state()
|
||||
return self._state
|
||||
end
|
||||
|
||||
function ui.Context:set_state(state)
|
||||
self._state = ui._req(state, "table")
|
||||
return self
|
||||
end
|
||||
|
||||
local last_id = 0
|
||||
|
||||
function ui.Context:_open_window()
|
||||
self._id = last_id
|
||||
last_id = last_id + 1
|
||||
|
||||
open_contexts[self._id] = self
|
||||
end
|
||||
|
||||
function ui.Context:_build_window(param)
|
||||
self._window = self._builder(self, self._player, self._state, param)
|
||||
|
||||
ui._req(self._window, ui.Window)
|
||||
assert(not self._window._context, "Window object has already been used")
|
||||
|
||||
self._window._context = self
|
||||
end
|
||||
|
||||
function ui.Context:_close_window()
|
||||
local close_id = self._id
|
||||
|
||||
self._id = nil
|
||||
self._window = nil
|
||||
|
||||
open_contexts[close_id] = nil
|
||||
return close_id
|
||||
end
|
||||
|
||||
function ui.get_open_contexts()
|
||||
local contexts = {}
|
||||
for _, context in pairs(open_contexts) do
|
||||
table.insert(contexts, context)
|
||||
end
|
||||
return contexts
|
||||
end
|
||||
|
||||
core.register_on_leaveplayer(function(player)
|
||||
for _, context in pairs(open_contexts) do
|
||||
if context:get_player() == player:get_player_name() then
|
||||
context:_close_window()
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
local WINDOW_EVENT = 0x00
|
||||
local ELEM_EVENT = 0x01
|
||||
|
||||
function core.receive_ui_message(player, data)
|
||||
local action, id, code, rest = ui._decode("BLB Z", data, -1)
|
||||
|
||||
-- Discard events for any window that isn't currently open, since it's
|
||||
-- probably due to network latency and events coming late.
|
||||
local context = open_contexts[id]
|
||||
if not context then
|
||||
core.log("info", "Window " .. id .. " is not open")
|
||||
return
|
||||
end
|
||||
|
||||
-- If the player doesn't match up with what we expected, ignore the
|
||||
-- (probably malicious) event.
|
||||
if context:get_player() ~= player then
|
||||
core.log("action", "Window " .. id .. " has player '" .. context:get_player() ..
|
||||
"', but received event from player '" .. player .. "'")
|
||||
return
|
||||
end
|
||||
|
||||
-- No events should ever fire for non-GUI windows.
|
||||
if context._window._type ~= "gui" then
|
||||
core.log("info", "Non-GUI window received event: " .. code)
|
||||
return
|
||||
end
|
||||
|
||||
-- Prepare the basic event table shared by all events.
|
||||
local ev = {
|
||||
context = context,
|
||||
player = context:get_player(),
|
||||
state = context:get_state(),
|
||||
}
|
||||
|
||||
if action == WINDOW_EVENT then
|
||||
context._window:_on_window_event(code, ev, rest)
|
||||
elseif action == ELEM_EVENT then
|
||||
context._window:_on_elem_event(code, ev, rest)
|
||||
else
|
||||
core.log("info", "Invalid window action: " .. action)
|
||||
end
|
||||
end
|
163
builtin/ui/elem.lua
Normal file
163
builtin/ui/elem.lua
Normal file
|
@ -0,0 +1,163 @@
|
|||
-- Luanti
|
||||
-- SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
-- Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
ui._elem_types = {}
|
||||
|
||||
function ui._new_type(base, type, type_id, id_required)
|
||||
local class = core.class(base)
|
||||
|
||||
class._type = type
|
||||
class._type_id = type_id
|
||||
class._id_required = id_required
|
||||
class._handlers = setmetatable({}, {__index = base and base._handlers})
|
||||
|
||||
ui._elem_types[type] = class
|
||||
|
||||
return class
|
||||
end
|
||||
|
||||
function ui.derive_elem(base, type)
|
||||
assert(core.is_subclass(base, ui.Elem))
|
||||
ui._req(type, "id")
|
||||
|
||||
assert(not ui._elem_types[type],
|
||||
"Derived element name already used: '" .. type .. "'")
|
||||
|
||||
return ui._new_type(base, type, base._type_id, base._id_required)
|
||||
end
|
||||
|
||||
ui.Elem = ui._new_type(nil, "elem", 0x00, false)
|
||||
|
||||
function ui.Elem:new(param)
|
||||
local function make_elem(props)
|
||||
self:_init(ui._req(props, "table"))
|
||||
return self
|
||||
end
|
||||
|
||||
if type(param) == "string" then
|
||||
self._id = ui._req(param, "id")
|
||||
return make_elem
|
||||
end
|
||||
|
||||
self._id = ui.new_id()
|
||||
return make_elem(param)
|
||||
end
|
||||
|
||||
function ui.Elem:_init(props)
|
||||
self._label = ui._opt(props.label, "string")
|
||||
|
||||
self._groups = {}
|
||||
self._children = {}
|
||||
|
||||
self._props = ui._cascade_props(props.props or props, {})
|
||||
self._styles = {}
|
||||
|
||||
-- Set by parent ui.Elem
|
||||
self._parent = nil
|
||||
|
||||
-- Set by ui.Window
|
||||
self._boxes = {main = true}
|
||||
self._window = nil
|
||||
|
||||
if self._id_required then
|
||||
assert(not ui._is_reserved_id(self._id),
|
||||
"Element ID is required for '" .. self._type .. "'")
|
||||
end
|
||||
|
||||
for _, group in ipairs(ui._opt_array(props.groups, "id", {})) do
|
||||
self._groups[group] = true
|
||||
end
|
||||
|
||||
for _, child in ipairs(ui._opt_array(props.children, ui.Elem, props)) do
|
||||
if core.is_instance(child, ui.Elem) then
|
||||
assert(child._parent == nil,
|
||||
"Element '" .. child._id .. "' already has a parent")
|
||||
assert(not core.is_instance(child, ui.Root),
|
||||
"ui.Root may not be a child element")
|
||||
|
||||
child._parent = self
|
||||
table.insert(self._children, child)
|
||||
end
|
||||
end
|
||||
|
||||
for _, style in ipairs(ui._opt_array(props.styles, ui.Style, props)) do
|
||||
if core.is_instance(style, ui.Style) then
|
||||
table.insert(self._styles, style)
|
||||
end
|
||||
end
|
||||
|
||||
for _, item in ipairs(props) do
|
||||
assert(core.is_instance(item, ui.Elem) or core.is_instance(item, ui.Style))
|
||||
end
|
||||
end
|
||||
|
||||
function ui.Elem:_get_flat()
|
||||
local elems = {self}
|
||||
for _, child in ipairs(self._children) do
|
||||
table.insert_all(elems, child:_get_flat())
|
||||
end
|
||||
return elems
|
||||
end
|
||||
|
||||
function ui.Elem:_encode()
|
||||
return ui._encode("Bz S", self._type_id, self._id, self:_encode_fields())
|
||||
end
|
||||
|
||||
function ui.Elem:_encode_fields()
|
||||
local fl = ui._make_flags()
|
||||
|
||||
if ui._shift_flag(fl, #self._children > 0) then
|
||||
local child_ids = {}
|
||||
for i, child in ipairs(self._children) do
|
||||
child_ids[i] = child._id
|
||||
end
|
||||
|
||||
ui._encode_flag(fl, "Z", ui._encode_array("z", child_ids))
|
||||
end
|
||||
|
||||
if ui._shift_flag(fl, self._label) then
|
||||
ui._encode_flag(fl, "s", self._label)
|
||||
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
|
||||
|
||||
function ui.Elem:_on_event(code, ev, data)
|
||||
-- Get the handler function for this event if we recognize it.
|
||||
local handler = self._handlers[code]
|
||||
if not handler then
|
||||
core.log("info", "Invalid event for " .. self._type_id .. ": " .. code)
|
||||
return
|
||||
end
|
||||
|
||||
-- If the event handler returned a callback function for the user, call it
|
||||
-- with the event table.
|
||||
local callback = handler(self, ev, data)
|
||||
if callback then
|
||||
callback(ev)
|
||||
end
|
||||
end
|
19
builtin/ui/init.lua
Normal file
19
builtin/ui/init.lua
Normal file
|
@ -0,0 +1,19 @@
|
|||
-- Luanti
|
||||
-- SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
-- Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
ui = {}
|
||||
|
||||
local UI_PATH = core.get_builtin_path() .. "ui" .. DIR_DELIM
|
||||
|
||||
dofile(UI_PATH .. "util.lua")
|
||||
dofile(UI_PATH .. "selector.lua")
|
||||
dofile(UI_PATH .. "style.lua")
|
||||
dofile(UI_PATH .. "elem.lua")
|
||||
|
||||
dofile(UI_PATH .. "clickable_elems.lua")
|
||||
dofile(UI_PATH .. "static_elems.lua")
|
||||
|
||||
dofile(UI_PATH .. "window.lua")
|
||||
dofile(UI_PATH .. "context.lua")
|
||||
dofile(UI_PATH .. "theme.lua")
|
582
builtin/ui/selector.lua
Normal file
582
builtin/ui/selector.lua
Normal file
|
@ -0,0 +1,582 @@
|
|||
-- Luanti
|
||||
-- SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
-- Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
ui._STATE_NONE = 0
|
||||
ui._NUM_STATES = bit.lshift(1, 5)
|
||||
ui._NO_STYLE = -1
|
||||
|
||||
local states_by_name = {
|
||||
focused = bit.lshift(1, 0),
|
||||
selected = bit.lshift(1, 1),
|
||||
hovered = bit.lshift(1, 2),
|
||||
pressed = bit.lshift(1, 3),
|
||||
disabled = bit.lshift(1, 4),
|
||||
}
|
||||
|
||||
--[[
|
||||
Selector parsing functions return a function. When called with an element as
|
||||
the solitary parameter, this function will return a boolean, indicating whether
|
||||
the element is matched by the selector. If the boolean is true, a table of
|
||||
tables {box=..., states=...} is also returned. If false, this is nil.
|
||||
|
||||
The keys of this table are unique hashes of the box, which serve to prevent
|
||||
duplicate box/state combos from being generated. The values contain all the
|
||||
combinations of boxes and states that the selector specifies. The box name may
|
||||
be nil if the selector specified no box, in which case it will default to
|
||||
"main" unless/until it is later intersected with a box selector. This list may
|
||||
also be empty, which means that contradictory boxes were specified and no box
|
||||
should be styled. The list will not contain duplicates.
|
||||
--]]
|
||||
|
||||
-- By default, most selectors leave the box unspecified and don't select any
|
||||
-- particular state, leaving the state at zero.
|
||||
local function make_box(name, states)
|
||||
return {name = name, states = states or ui._STATE_NONE}
|
||||
end
|
||||
|
||||
-- Hash the box to a string that represents that combination of box and states
|
||||
-- uniquely to prevent duplicates in box tables.
|
||||
local function hash_box(box)
|
||||
return (box.name or "") .. "$" .. tostring(box.states)
|
||||
end
|
||||
|
||||
local function make_hashed(name, states)
|
||||
local box = make_box(name, states)
|
||||
return {[hash_box(box)] = box}
|
||||
end
|
||||
|
||||
local function result(matches, name, states)
|
||||
if matches then
|
||||
return true, make_hashed(name, states)
|
||||
end
|
||||
return false, nil
|
||||
end
|
||||
|
||||
ui._universal_sel = function()
|
||||
return result(true)
|
||||
end
|
||||
|
||||
local simple_preds = {}
|
||||
local func_preds = {}
|
||||
|
||||
simple_preds["no_children"] = function(elem)
|
||||
return result(#elem._children == 0)
|
||||
end
|
||||
|
||||
simple_preds["first_child"] = function(elem)
|
||||
return result(elem._parent == nil or elem._parent._children[1] == elem)
|
||||
end
|
||||
|
||||
simple_preds["last_child"] = function(elem)
|
||||
return result(elem._parent == nil or
|
||||
elem._parent._children[#elem._parent._children] == elem)
|
||||
end
|
||||
|
||||
simple_preds["only_child"] = function(elem)
|
||||
return result(elem._parent == nil or #elem._parent._children == 1)
|
||||
end
|
||||
|
||||
func_preds["<"] = function(str)
|
||||
local sel = ui._parse_sel(str, true, false)
|
||||
|
||||
return function(elem)
|
||||
return result(elem._parent and sel(elem._parent))
|
||||
end
|
||||
end
|
||||
|
||||
func_preds[">"] = function(str)
|
||||
local sel = ui._parse_sel(str, true, false)
|
||||
|
||||
return function(elem)
|
||||
for _, child in ipairs(elem._children) do
|
||||
if sel(child) then
|
||||
return result(true)
|
||||
end
|
||||
end
|
||||
return result(false)
|
||||
end
|
||||
end
|
||||
|
||||
func_preds["<<"] = function(str)
|
||||
local sel = ui._parse_sel(str, true, false)
|
||||
|
||||
return function(elem)
|
||||
local ancestor = elem._parent
|
||||
|
||||
while ancestor ~= nil do
|
||||
if sel(ancestor) then
|
||||
return result(true)
|
||||
end
|
||||
ancestor = ancestor._parent
|
||||
end
|
||||
|
||||
return result(false)
|
||||
end
|
||||
end
|
||||
|
||||
func_preds[">>"] = function(str)
|
||||
local sel = ui._parse_sel(str, true, false)
|
||||
|
||||
return function(elem)
|
||||
for _, descendant in ipairs(elem:_get_flat()) do
|
||||
if descendant ~= elem and sel(descendant) then
|
||||
return result(true)
|
||||
end
|
||||
end
|
||||
return result(false)
|
||||
end
|
||||
end
|
||||
|
||||
func_preds["<>"] = function(str)
|
||||
local sel = ui._parse_sel(str, true, false)
|
||||
|
||||
return function(elem)
|
||||
if not elem._parent then
|
||||
return result(false)
|
||||
end
|
||||
|
||||
for _, sibling in ipairs(elem._parent._children) do
|
||||
if sibling ~= elem and sel(sibling) then
|
||||
return result(true)
|
||||
end
|
||||
end
|
||||
|
||||
return result(false)
|
||||
end
|
||||
end
|
||||
|
||||
func_preds["nth_child"] = function(str)
|
||||
local index = tonumber(str)
|
||||
assert(index, "Expected number for ?nth_child()")
|
||||
|
||||
return function(elem)
|
||||
if not elem._parent then
|
||||
return result(index == 1)
|
||||
end
|
||||
return result(elem._parent._children[index] == elem)
|
||||
end
|
||||
end
|
||||
|
||||
func_preds["nth_last_child"] = function(str)
|
||||
local rindex = tonumber(str)
|
||||
assert(rindex, "Expected number for ?nth_last_child()")
|
||||
|
||||
return function(elem)
|
||||
if not elem._parent then
|
||||
return result(rindex == 1)
|
||||
end
|
||||
|
||||
local index = #elem._parent._children - rindex + 1
|
||||
return result(elem._parent._children[index] == elem)
|
||||
end
|
||||
end
|
||||
|
||||
local function is_nth_match(elem, sel, index, dir)
|
||||
if not elem._parent then
|
||||
return index == 1 and sel(elem)
|
||||
end
|
||||
|
||||
local first, last
|
||||
if dir == 1 then
|
||||
first = 1
|
||||
last = #elem._parent._children
|
||||
else
|
||||
first = #elem._parent._children
|
||||
last = 1
|
||||
end
|
||||
|
||||
local count = 0
|
||||
for i = first, last, dir do
|
||||
local sibling = elem._parent._children[i]
|
||||
|
||||
if sel(sibling) then
|
||||
count = count + 1
|
||||
end
|
||||
|
||||
if count == index then
|
||||
return sibling == elem
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
func_preds["first_match"] = function(str)
|
||||
local sel = ui._parse_sel(str, true, false)
|
||||
|
||||
return function(elem)
|
||||
return is_nth_match(elem, sel, 1, 1)
|
||||
end
|
||||
end
|
||||
|
||||
func_preds["last_match"] = function(str)
|
||||
local sel = ui._parse_sel(str, true, false)
|
||||
|
||||
return function(elem)
|
||||
return is_nth_match(elem, sel, 1, -1)
|
||||
end
|
||||
end
|
||||
|
||||
func_preds["only_match"] = function(str)
|
||||
local sel = ui._parse_sel(str, true, false)
|
||||
|
||||
return function(elem)
|
||||
return is_nth_match(elem, sel, 1, 1) and is_nth_match(elem, sel, 1, -1)
|
||||
end
|
||||
end
|
||||
|
||||
func_preds["nth_match"] = function(str)
|
||||
local sel, rest = ui._parse_sel(str, true, true)
|
||||
local index = tonumber(rest)
|
||||
assert(index, "Expected number after ';' for ?nth_match()")
|
||||
|
||||
return function(elem)
|
||||
return is_nth_match(elem, sel, index, 1)
|
||||
end
|
||||
end
|
||||
|
||||
func_preds["nth_last_match"] = function(str)
|
||||
local sel, rest = ui._parse_sel(str, true, true)
|
||||
local rindex = tonumber(rest)
|
||||
assert(rindex, "Expected number after ';' for ?nth_last_match()")
|
||||
|
||||
return function(elem)
|
||||
return is_nth_match(elem, sel, rindex, -1)
|
||||
end
|
||||
end
|
||||
|
||||
func_preds["family"] = function(family)
|
||||
if family == "*" then
|
||||
return function(elem)
|
||||
return result(elem._family ~= nil)
|
||||
end
|
||||
end
|
||||
|
||||
assert(ui.is_id(family), "Expected '*' or ID string for ?family()")
|
||||
return function(elem)
|
||||
return result(elem._family == family)
|
||||
end
|
||||
end
|
||||
|
||||
local function parse_term(str, pred)
|
||||
str = str:trim()
|
||||
assert(str ~= "", "Expected selector term")
|
||||
|
||||
-- We need to test the first character to see what sort of term we're
|
||||
-- dealing with, and then usually parse from the rest of the string.
|
||||
local prefix = str:sub(1, 1)
|
||||
local suffix = str:sub(2)
|
||||
|
||||
if prefix == "*" then
|
||||
-- Universal terms match everything and have no extra stuff to parse.
|
||||
return ui._universal_sel, suffix, nil
|
||||
|
||||
elseif prefix == "#" then
|
||||
-- Most selectors are similar to the ID selector, in that characters
|
||||
-- for the ID string are parsed, and all the characters directly
|
||||
-- afterwards are returned as the rest of the string after the term.
|
||||
local id, rest = suffix:match("^([" .. ui._ID_CHARS .. "]+)(.*)$")
|
||||
assert(id, "Expected ID after '#'")
|
||||
|
||||
return function(elem)
|
||||
return result(elem._id == id)
|
||||
end, rest, nil
|
||||
|
||||
elseif prefix == "%" then
|
||||
local group, rest = suffix:match("^([" .. ui._ID_CHARS .. "]+)(.*)$")
|
||||
assert(group, "Expected group after '%'")
|
||||
|
||||
return function(elem)
|
||||
return result(elem._groups[group] ~= nil)
|
||||
end, rest, nil
|
||||
|
||||
elseif prefix == "@" then
|
||||
-- It's possible to check if a box exists in a predicate, but that
|
||||
-- leads to different behaviors inside and outside of predicates. For
|
||||
-- instance, @main@thumb effectively matches nothing by returning an
|
||||
-- empty table of boxes, but would return true for scrollbars if used
|
||||
-- in a predicate. So, prevent box selectors in predicates entirely.
|
||||
assert(not pred, "Box selectors are invalid in predicate selectors")
|
||||
|
||||
-- First, check if this can be parsed as a universal box selector.
|
||||
local name = suffix:sub(1, 1)
|
||||
local rest
|
||||
|
||||
if name == "*" then
|
||||
rest = suffix:sub(2)
|
||||
|
||||
return function(elem)
|
||||
-- If we want all boxes, iterate over the boxes in the element
|
||||
-- and add each of them to the full list of boxes.
|
||||
local boxes = {}
|
||||
|
||||
for name in pairs(elem._boxes) do
|
||||
local box = make_box(name, ui._STATE_NONE)
|
||||
boxes[hash_box(box)] = box
|
||||
end
|
||||
|
||||
return true, boxes
|
||||
end, rest, nil
|
||||
end
|
||||
|
||||
-- Otherwise, parse it as a normal box selector instead.
|
||||
name, rest = suffix:match("^([" .. ui._ID_CHARS .. "]+)(.*)$")
|
||||
assert(name, "Expected box or '*' after '@'")
|
||||
|
||||
return function(elem)
|
||||
-- If the box is in the element, return it. Otherwise, the
|
||||
-- selector doesn't match.
|
||||
if elem._boxes[name] then
|
||||
return result(true, name, ui._STATE_NONE)
|
||||
end
|
||||
return result(false)
|
||||
end, rest, nil
|
||||
|
||||
elseif prefix == "$" then
|
||||
-- Unfortunately, we can't detect the state of boxes from the server,
|
||||
-- so we can't use them in predicates.
|
||||
assert(not pred, "State selectors are invalid in predicate selectors")
|
||||
|
||||
local name, rest = suffix:match("^([" .. ui._ID_CHARS .. "]+)(.*)$")
|
||||
assert(name, "Expected state after '$'")
|
||||
|
||||
local state = states_by_name[name]
|
||||
assert(state, "Invalid state: '" .. name .. "'")
|
||||
|
||||
return function(elem)
|
||||
-- States unconditionally match every element. Specify the state
|
||||
-- that this term indicates but leave the box undefined.
|
||||
return result(true, nil, state)
|
||||
end, rest, nil
|
||||
|
||||
elseif prefix == "/" then
|
||||
local type, rest = suffix:match("^([" .. ui._ID_CHARS .. "]+)%/(.*)$")
|
||||
assert(type, "Expected window type after '/'")
|
||||
|
||||
assert(ui._window_types[type], "Invalid window type: '" .. type .. "'")
|
||||
|
||||
return function(elem)
|
||||
return result(elem._window._type == type)
|
||||
end, rest, nil
|
||||
|
||||
elseif prefix == "," or prefix == ";" then
|
||||
-- Return nil instead of a function and return the prefix character to
|
||||
-- instruct ui._parse_sel() to union or end the selector accordingly.
|
||||
return nil, suffix, prefix
|
||||
|
||||
elseif prefix == "(" then
|
||||
-- Parse a matching set of parentheses, and recursively pass the
|
||||
-- contents into ui._parse_sel().
|
||||
local sub, rest = str:match("^(%b())(.*)$")
|
||||
assert(sub, "Unmatched ')' for '('")
|
||||
|
||||
return ui._parse_sel(sub:sub(2, -2), pred, false), rest, nil
|
||||
|
||||
elseif prefix == "!" then
|
||||
-- Parse a single predicate term (NOT an entire predicate selector) and
|
||||
-- ensure that it's a valid selector term, not a comma or semicolon.
|
||||
local term, rest, _ = parse_term(suffix, true)
|
||||
assert(term, "Expected selector term after '!'")
|
||||
|
||||
return function(elem)
|
||||
return result(not term(elem))
|
||||
end, rest, nil
|
||||
|
||||
elseif prefix == "?" then
|
||||
-- Predicates may have different syntax depending on the name of the
|
||||
-- predicate, so just parse the name initially.
|
||||
local name, after = suffix:match("^([" .. ui._ID_CHARS .. "%<%>]+)(.*)$")
|
||||
assert(name, "Expected predicate after '?'")
|
||||
|
||||
-- If this is a simple predicate, return its predicate function without
|
||||
-- doing any further parsing.
|
||||
local func = simple_preds[name]
|
||||
if func then
|
||||
return func, after, nil
|
||||
end
|
||||
|
||||
-- If this is a function predicate, we need to do more parsing.
|
||||
func = func_preds[name]
|
||||
if func then
|
||||
-- Parse a matching pair of parentheses and get the trimmed
|
||||
-- contents between them.
|
||||
assert(after:sub(1, 1) == "(", "Expected '(' after '?" .. name .. "'")
|
||||
|
||||
local sub, rest = after:match("^(%b())(.*)$")
|
||||
assert(sub, "Unmatched ')' for '?" .. name .. "('")
|
||||
|
||||
local contents = sub:sub(2, -2):trim()
|
||||
return func(contents), rest, nil
|
||||
end
|
||||
|
||||
-- Otherwise, there is no predicate by this name.
|
||||
error("Invalid predicate: '?" .. name .. "'")
|
||||
|
||||
else
|
||||
-- If we found no special character, it's either a type or it indicates
|
||||
-- invalid characters in the selector string.
|
||||
local type, rest = str:match("^([" .. ui._ID_CHARS .. "]+)(.*)$")
|
||||
assert(type, "Unexpected character '" .. prefix .. "' in selector")
|
||||
|
||||
assert(ui._elem_types[type], "Invalid element type: '" .. type .. "'")
|
||||
|
||||
return function(elem)
|
||||
return result(elem._type == type)
|
||||
end, rest, nil
|
||||
end
|
||||
end
|
||||
|
||||
local function intersect_boxes(a_boxes, b_boxes)
|
||||
local new_boxes = {}
|
||||
|
||||
for _, box_a in pairs(a_boxes) do
|
||||
for _, box_b in pairs(b_boxes) do
|
||||
-- Two boxes can only be merged if they're the same box or if one
|
||||
-- or both selectors hasn't specified a box yet.
|
||||
if box_a.name == nil or box_b.name == nil or box_a.name == box_b.name then
|
||||
-- Create the new box by taking the specified box (if there is
|
||||
-- one) and ORing the states together (making them more refer
|
||||
-- to a more specific state).
|
||||
local new_box = make_box(
|
||||
box_a.name or box_b.name,
|
||||
bit.bor(box_a.states, box_b.states)
|
||||
)
|
||||
|
||||
-- Hash this box and add it into the table. This will be
|
||||
-- effectively a no-op if there's already an identical box
|
||||
-- hashed in the table.
|
||||
new_boxes[hash_box(new_box)] = new_box
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return new_boxes
|
||||
end
|
||||
|
||||
function ui._intersect_sels(sels)
|
||||
return function(elem)
|
||||
-- We start with the default box, and intersect the box and states from
|
||||
-- every selector with it.
|
||||
local all_boxes = make_hashed()
|
||||
|
||||
-- Loop through all of the selectors. All of them need to match for the
|
||||
-- intersected selector to match.
|
||||
for _, sel in ipairs(sels) do
|
||||
local matches, boxes = sel(elem)
|
||||
if not matches then
|
||||
-- This selector doesn't match, so fail immediately.
|
||||
return false, nil
|
||||
end
|
||||
|
||||
-- Since the selector matched, intersect the boxes and states with
|
||||
-- those of the other selectors. If two selectors both match an
|
||||
-- element but specify different boxes, then this selector will
|
||||
-- return true, but the boxes will be cancelled out in the
|
||||
-- intersection, leaving an empty list of boxes.
|
||||
if boxes then
|
||||
all_boxes = intersect_boxes(all_boxes, boxes)
|
||||
end
|
||||
end
|
||||
|
||||
return true, all_boxes
|
||||
end
|
||||
end
|
||||
|
||||
function ui._union_sels(sels)
|
||||
return function(elem)
|
||||
-- We initially have no boxes, and have to add them in as matching
|
||||
-- selectors are unioned in.
|
||||
local all_boxes = {}
|
||||
local found_match = false
|
||||
|
||||
-- Loop through all of the selectors. If any of them match, this entire
|
||||
-- unioned selector matches.
|
||||
for _, sel in ipairs(sels) do
|
||||
local matches, boxes = sel(elem)
|
||||
|
||||
if matches then
|
||||
-- We found a match. However, we can't return true just yet
|
||||
-- because we need to union the boxes and states from every
|
||||
-- selector, not just this one.
|
||||
found_match = true
|
||||
|
||||
if boxes then
|
||||
-- Add the boxes from this selector into the table of all
|
||||
-- the boxes. The hashing of boxes will automatically weed
|
||||
-- out any duplicates.
|
||||
for hash, box in pairs(boxes) do
|
||||
all_boxes[hash] = box
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if found_match then
|
||||
return true, all_boxes
|
||||
end
|
||||
return false, nil
|
||||
end
|
||||
end
|
||||
|
||||
function ui._parse_sel(str, pred, partial)
|
||||
str = str:trim()
|
||||
assert(str ~= "", "Empty style selector")
|
||||
|
||||
local sub_sels = {}
|
||||
local terms = {}
|
||||
local done = false
|
||||
|
||||
-- Loop until we've read every term from the input string.
|
||||
while not done do
|
||||
-- Parse the next term from the input string.
|
||||
local term, prefix
|
||||
term, str, prefix = parse_term(str, pred)
|
||||
|
||||
-- If we read a term, insert this term into the list of terms for the
|
||||
-- current sub-selector.
|
||||
if term then
|
||||
table.insert(terms, term)
|
||||
end
|
||||
|
||||
-- Make sure that we have at least one selector term before each comma
|
||||
-- or semicolon that we read.
|
||||
if prefix then
|
||||
assert(#terms > 0, "Expected selector term before '" .. prefix .. "'")
|
||||
end
|
||||
|
||||
-- If we read a comma or semicolon or have run out of terms, we need to
|
||||
-- commit the terms we've read so far.
|
||||
if prefix or str == "" then
|
||||
-- If there's only one term, commit it directly. Otherwise,
|
||||
-- intersect all the terms together.
|
||||
if #terms == 1 then
|
||||
table.insert(sub_sels, terms[1])
|
||||
else
|
||||
table.insert(sub_sels, ui._intersect_sels(terms))
|
||||
end
|
||||
|
||||
-- Clear out the list of terms for the next sub-selector.
|
||||
terms = {}
|
||||
end
|
||||
|
||||
-- If we read a semicolon or have run out of terms, we're done parsing.
|
||||
-- We check for the semicolon case first since it is possible for the
|
||||
-- string to be empty after reading the semicolon.
|
||||
if prefix == ";" then
|
||||
assert(partial, "Unexpected character ';' in selector")
|
||||
done = true
|
||||
elseif str == "" then
|
||||
assert(prefix ~= ",", "Expected selector term after ','")
|
||||
assert(not partial, "Expected ';' after end of selector")
|
||||
done = true
|
||||
end
|
||||
end
|
||||
|
||||
-- Now that we've read all the sub-selectors between the commas, we need to
|
||||
-- commit them. We only need to union the terms if there's more than one.
|
||||
if #sub_sels == 1 then
|
||||
return sub_sels[1], str:trim()
|
||||
end
|
||||
return ui._union_sels(sub_sels), str:trim()
|
||||
end
|
23
builtin/ui/static_elems.lua
Normal file
23
builtin/ui/static_elems.lua
Normal file
|
@ -0,0 +1,23 @@
|
|||
-- Luanti
|
||||
-- SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
-- Copyright (C) 2024 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
ui.Group = ui.derive_elem(ui.Elem, "group")
|
||||
ui.Label = ui.derive_elem(ui.Elem, "label")
|
||||
ui.Image = ui.derive_elem(ui.Elem, "image")
|
||||
|
||||
ui.Root = ui._new_type(ui.Elem, "root", 0x01, false)
|
||||
|
||||
function ui.Root:_init(props)
|
||||
ui.Elem._init(self, props)
|
||||
|
||||
self._boxes.backdrop = true
|
||||
end
|
||||
|
||||
function ui.Root:_encode_fields()
|
||||
local fl = ui._make_flags()
|
||||
|
||||
self:_encode_box(fl, self._boxes.backdrop)
|
||||
|
||||
return ui._encode("SZ", ui.Elem._encode_fields(self), ui._encode_flags(fl))
|
||||
end
|
347
builtin/ui/style.lua
Normal file
347
builtin/ui/style.lua
Normal file
|
@ -0,0 +1,347 @@
|
|||
-- Luanti
|
||||
-- SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
-- Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
ui.Style = core.class()
|
||||
|
||||
function ui.Style:new(param)
|
||||
local function make_style(props)
|
||||
self:_init(ui._req(props, "table"))
|
||||
return self
|
||||
end
|
||||
|
||||
if type(param) == "string" then
|
||||
self._sel = ui._parse_sel(param, false, false)
|
||||
return make_style
|
||||
end
|
||||
|
||||
self._sel = ui._universal_sel
|
||||
return make_style(param)
|
||||
end
|
||||
|
||||
function ui.Style:_init(props)
|
||||
self._props = ui._cascade_props(props.props or props, {})
|
||||
self._nested = table.merge(ui._opt_array(props.nested, ui.Style, props))
|
||||
self._reset = ui._opt(props.reset, "boolean")
|
||||
|
||||
for _, item in ipairs(props) do
|
||||
ui._req(item, ui.Style)
|
||||
end
|
||||
end
|
||||
|
||||
function ui.Style:_get_flat()
|
||||
local flat_styles = {}
|
||||
self:_get_flat_impl(flat_styles, ui._universal_sel)
|
||||
return flat_styles
|
||||
end
|
||||
|
||||
function ui.Style:_get_flat_impl(flat_styles, parent_sel)
|
||||
-- Intersect our selector with our parent selector, resulting in a fully
|
||||
-- qualified selector.
|
||||
local full_sel = ui._intersect_sels({parent_sel, self._sel})
|
||||
|
||||
-- Copy this style's properties into a new style with the full selector.
|
||||
local flat = ui.Style {
|
||||
props = self._props,
|
||||
reset = self._reset,
|
||||
}
|
||||
flat._sel = full_sel
|
||||
|
||||
table.insert(flat_styles, flat)
|
||||
|
||||
-- For each sub-style of this style, cascade it with our full selector and
|
||||
-- add it to the list of flat styles.
|
||||
for _, nested in ipairs(self._nested) do
|
||||
nested:_get_flat_impl(flat_styles, full_sel)
|
||||
end
|
||||
end
|
||||
|
||||
local layout_type_map = {
|
||||
place = 0,
|
||||
}
|
||||
|
||||
local dir_flags_map = {
|
||||
none = 0,
|
||||
x = 1,
|
||||
y = 2,
|
||||
both = 3,
|
||||
}
|
||||
|
||||
local display_mode_map = {
|
||||
visible = 0,
|
||||
overflow = 1,
|
||||
hidden = 2,
|
||||
clipped = 3,
|
||||
}
|
||||
|
||||
local icon_place_map = {
|
||||
center = 0,
|
||||
left = 1,
|
||||
top = 2,
|
||||
right = 3,
|
||||
bottom = 4,
|
||||
}
|
||||
|
||||
local align_map = {
|
||||
left = 0,
|
||||
center = 1,
|
||||
right = 2,
|
||||
}
|
||||
|
||||
local valign_map = {
|
||||
top = 0,
|
||||
center = 1,
|
||||
bottom = 2,
|
||||
}
|
||||
|
||||
local function opt_color(val, def)
|
||||
assert(val == nil or core.colorspec_to_int(val))
|
||||
return val or def
|
||||
end
|
||||
|
||||
local function opt_vec2d(val, def)
|
||||
ui._opt_array(val, "number")
|
||||
assert(val == nil or #val == 1 or #val == 2)
|
||||
return val or def
|
||||
end
|
||||
|
||||
local function opt_rect(val, def)
|
||||
ui._opt_array(val, "number")
|
||||
assert(val == nil or #val == 1 or #val == 2 or #val == 4)
|
||||
return val or def
|
||||
end
|
||||
|
||||
local function cascade_layout(new, add, props)
|
||||
new.layout = ui._opt_enum(add.layout, layout_type_map, props.layout)
|
||||
new.clip = ui._opt_enum(add.clip, dir_flags_map, props.clip)
|
||||
|
||||
new.scale = ui._opt(add.scale, "number", props.scale)
|
||||
end
|
||||
|
||||
local function cascade_sizing(new, add, props)
|
||||
new.size = opt_vec2d(add.size, props.size)
|
||||
new.span = opt_vec2d(add.span, props.span)
|
||||
|
||||
new.pos = opt_vec2d(add.pos, props.pos)
|
||||
new.anchor = opt_vec2d(add.anchor, props.anchor)
|
||||
|
||||
new.margin = opt_rect(add.margin, props.margin)
|
||||
new.padding = opt_rect(add.padding, props.padding)
|
||||
end
|
||||
|
||||
local function cascade_layer(new, add, props, p)
|
||||
new[p.."_image"] = ui._opt(add[p.."_image"], "string", props[p.."_image"])
|
||||
new[p.."_fill"] = opt_color(add[p.."_fill"], props[p.."_fill"])
|
||||
new[p.."_tint"] = opt_color(add[p.."_tint"], props[p.."_tint"])
|
||||
|
||||
new[p.."_scale"] = ui._opt(add[p.."_scale"], "number", props[p.."_scale"])
|
||||
new[p.."_source"] = opt_rect(add[p.."_source"], props[p.."_source"])
|
||||
|
||||
new[p.."_frames"] = ui._opt(add[p.."_frames"], "number", props[p.."_frames"])
|
||||
new[p.."_frame_time"] =
|
||||
ui._opt(add[p.."_frame_time"], "number", props[p.."_frame_time"])
|
||||
end
|
||||
|
||||
local function cascade_text(new, add, props)
|
||||
new.prepend = ui._opt(add.prepend, "string", props.prepend)
|
||||
new.append = ui._opt(add.append, "string", props.append)
|
||||
|
||||
new.text_color = opt_color(add.text_color, props.text_color)
|
||||
new.text_mark = opt_color(add.text_mark, props.text_mark)
|
||||
new.text_size = ui._opt(add.text_size, "number", props.text_size)
|
||||
|
||||
new.text_mono = ui._opt(add.text_mono, "boolean", props.text_mono)
|
||||
new.text_italic = ui._opt(add.text_italic, "boolean", props.text_italic)
|
||||
new.text_bold = ui._opt(add.text_bold, "boolean", props.text_bold)
|
||||
|
||||
new.text_align = ui._opt_enum(add.text_align, align_map, props.text_align)
|
||||
new.text_valign = ui._opt_enum(add.text_valign, valign_map, props.text_valign)
|
||||
end
|
||||
|
||||
function ui._cascade_props(add, props)
|
||||
local new = {}
|
||||
|
||||
cascade_layout(new, add, props)
|
||||
cascade_sizing(new, add, props)
|
||||
|
||||
new.display = ui._opt_enum(add.display, display_mode_map, props.display)
|
||||
|
||||
cascade_layer(new, add, props, "box")
|
||||
cascade_layer(new, add, props, "icon")
|
||||
|
||||
new.box_middle = opt_rect(add.box_middle, props.box_middle)
|
||||
new.box_tile = ui._opt_enum(add.box_tile, dir_flags_map, props.box_tile)
|
||||
|
||||
new.icon_place = ui._opt_enum(add.icon_place, icon_place_map, props.icon_place)
|
||||
new.icon_gutter = ui._opt(add.icon_gutter, "number", props.icon_gutter)
|
||||
new.icon_overlap = ui._opt(add.icon_overlap, "boolean", props.icon_overlap)
|
||||
|
||||
cascade_text(new, add, props)
|
||||
|
||||
return new
|
||||
end
|
||||
|
||||
local function unpack_vec2d(vec)
|
||||
if #vec == 2 then
|
||||
return vec[1], vec[2]
|
||||
elseif #vec == 1 then
|
||||
return vec[1], vec[1]
|
||||
end
|
||||
end
|
||||
|
||||
local function unpack_rect(rect)
|
||||
if #rect == 4 then
|
||||
return rect[1], rect[2], rect[3], rect[4]
|
||||
elseif #rect == 2 then
|
||||
return rect[1], rect[2], rect[1], rect[2]
|
||||
elseif #rect == 1 then
|
||||
return rect[1], rect[1], rect[1], rect[1]
|
||||
end
|
||||
end
|
||||
|
||||
local function encode_layout(props)
|
||||
local fl = ui._make_flags()
|
||||
|
||||
if ui._shift_flag(fl, props.layout) then
|
||||
ui._encode_flag(fl, "B", layout_type_map[props.layout])
|
||||
end
|
||||
if ui._shift_flag(fl, props.clip) then
|
||||
ui._encode_flag(fl, "B", dir_flags_map[props.clip])
|
||||
end
|
||||
|
||||
if ui._shift_flag(fl, props.scale) then
|
||||
ui._encode_flag(fl, "f", props.scale)
|
||||
end
|
||||
|
||||
return fl
|
||||
end
|
||||
|
||||
local function encode_sizing(props)
|
||||
local fl = ui._make_flags()
|
||||
|
||||
if ui._shift_flag(fl, props.size) then
|
||||
ui._encode_flag(fl, "ff", unpack_vec2d(props.size))
|
||||
end
|
||||
if ui._shift_flag(fl, props.span) then
|
||||
ui._encode_flag(fl, "ff", unpack_vec2d(props.span))
|
||||
end
|
||||
|
||||
if ui._shift_flag(fl, props.pos) then
|
||||
ui._encode_flag(fl, "ff", unpack_vec2d(props.pos))
|
||||
end
|
||||
if ui._shift_flag(fl, props.anchor) then
|
||||
ui._encode_flag(fl, "ff", unpack_vec2d(props.anchor))
|
||||
end
|
||||
|
||||
if ui._shift_flag(fl, props.margin) then
|
||||
ui._encode_flag(fl, "ffff", unpack_rect(props.margin))
|
||||
end
|
||||
if ui._shift_flag(fl, props.padding) then
|
||||
ui._encode_flag(fl, "ffff", unpack_rect(props.padding))
|
||||
end
|
||||
|
||||
return fl
|
||||
end
|
||||
|
||||
local function encode_layer(props, p)
|
||||
local fl = ui._make_flags()
|
||||
|
||||
if ui._shift_flag(fl, props[p.."_image"]) then
|
||||
ui._encode_flag(fl, "z", props[p.."_image"])
|
||||
end
|
||||
if ui._shift_flag(fl, props[p.."_fill"]) then
|
||||
ui._encode_flag(fl, "I", core.colorspec_to_int(props[p.."_fill"]))
|
||||
end
|
||||
if ui._shift_flag(fl, props[p.."_tint"]) then
|
||||
ui._encode_flag(fl, "I", core.colorspec_to_int(props[p.."_tint"]))
|
||||
end
|
||||
|
||||
if ui._shift_flag(fl, props[p.."_scale"]) then
|
||||
ui._encode_flag(fl, "f", props[p.."_scale"])
|
||||
end
|
||||
if ui._shift_flag(fl, props[p.."_source"]) then
|
||||
ui._encode_flag(fl, "ffff", unpack_rect(props[p.."_source"]))
|
||||
end
|
||||
|
||||
if ui._shift_flag(fl, props[p.."_frames"]) then
|
||||
ui._encode_flag(fl, "I", props[p.."_frames"])
|
||||
end
|
||||
if ui._shift_flag(fl, props[p.."_frame_time"]) then
|
||||
ui._encode_flag(fl, "I", props[p.."_frame_time"])
|
||||
end
|
||||
|
||||
return fl
|
||||
end
|
||||
|
||||
local function encode_text(props)
|
||||
local fl = ui._make_flags()
|
||||
|
||||
if ui._shift_flag(fl, props.prepend) then
|
||||
ui._encode_flag(fl, "s", props.prepend)
|
||||
end
|
||||
if ui._shift_flag(fl, props.append) then
|
||||
ui._encode_flag(fl, "s", props.append)
|
||||
end
|
||||
|
||||
if ui._shift_flag(fl, props.text_color) then
|
||||
ui._encode_flag(fl, "I", core.colorspec_to_int(props.text_color))
|
||||
end
|
||||
if ui._shift_flag(fl, props.text_mark) then
|
||||
ui._encode_flag(fl, "I", core.colorspec_to_int(props.text_mark))
|
||||
end
|
||||
if ui._shift_flag(fl, props.text_size) then
|
||||
ui._encode_flag(fl, "I", props.text_size)
|
||||
end
|
||||
|
||||
ui._shift_flag_bool(fl, props.text_mono)
|
||||
ui._shift_flag_bool(fl, props.text_italic)
|
||||
ui._shift_flag_bool(fl, props.text_bold)
|
||||
|
||||
if ui._shift_flag(fl, props.text_align) then
|
||||
ui._encode_flag(fl, "B", align_map[props.text_align])
|
||||
end
|
||||
if ui._shift_flag(fl, props.text_valign) then
|
||||
ui._encode_flag(fl, "B", valign_map[props.text_valign])
|
||||
end
|
||||
|
||||
return fl
|
||||
end
|
||||
|
||||
local function encode_subflags(fl, sub_fl)
|
||||
if ui._shift_flag(fl, sub_fl.flags ~= 0) then
|
||||
ui._encode_flag(fl, "s", ui._encode_flags(sub_fl))
|
||||
end
|
||||
end
|
||||
|
||||
function ui._encode_props(props)
|
||||
local fl = ui._make_flags()
|
||||
|
||||
encode_subflags(fl, encode_layout(props))
|
||||
encode_subflags(fl, encode_sizing(props))
|
||||
|
||||
if ui._shift_flag(fl, props.display) then
|
||||
ui._encode_flag(fl, "B", display_mode_map[props.display])
|
||||
end
|
||||
|
||||
encode_subflags(fl, encode_layer(props, "box"))
|
||||
encode_subflags(fl, encode_layer(props, "icon"))
|
||||
|
||||
if ui._shift_flag(fl, props.box_middle) then
|
||||
ui._encode_flag(fl, "ffff", unpack_rect(props.box_middle))
|
||||
end
|
||||
if ui._shift_flag(fl, props.box_tile) then
|
||||
ui._encode_flag(fl, "B", dir_flags_map[props.box_tile])
|
||||
end
|
||||
|
||||
if ui._shift_flag(fl, props.icon_place) then
|
||||
ui._encode_flag(fl, "B", icon_place_map[props.icon_place])
|
||||
end
|
||||
if ui._shift_flag(fl, props.icon_gutter) then
|
||||
ui._encode_flag(fl, "f", props.icon_gutter)
|
||||
end
|
||||
ui._shift_flag_bool(fl, props.icon_overlap)
|
||||
|
||||
encode_subflags(fl, encode_text(props))
|
||||
|
||||
return ui._encode("s", ui._encode_flags(fl))
|
||||
end
|
40
builtin/ui/theme.lua
Normal file
40
builtin/ui/theme.lua
Normal file
|
@ -0,0 +1,40 @@
|
|||
-- Luanti
|
||||
-- SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
-- Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
local prelude_theme = ui.Style {
|
||||
ui.Style "root" {
|
||||
pos = {1/2},
|
||||
anchor = {1/2},
|
||||
span = {0},
|
||||
|
||||
ui.Style "@backdrop" {
|
||||
display = "hidden",
|
||||
clip = "both",
|
||||
},
|
||||
ui.Style "@backdrop$focused" {
|
||||
display = "visible",
|
||||
},
|
||||
},
|
||||
ui.Style "image" {
|
||||
icon_scale = 0,
|
||||
},
|
||||
ui.Style "check, switch, radio" {
|
||||
icon_place = "left",
|
||||
text_align = "left",
|
||||
},
|
||||
}
|
||||
|
||||
function ui.get_prelude_theme()
|
||||
return prelude_theme
|
||||
end
|
||||
|
||||
local default_theme = prelude_theme
|
||||
|
||||
function ui.get_default_theme()
|
||||
return default_theme
|
||||
end
|
||||
|
||||
function ui.set_default_theme(theme)
|
||||
default_theme = ui._req(theme, ui.Style)
|
||||
end
|
120
builtin/ui/util.lua
Normal file
120
builtin/ui/util.lua
Normal file
|
@ -0,0 +1,120 @@
|
|||
-- Luanti
|
||||
-- SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
-- Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
local next_id = 0
|
||||
|
||||
function ui.new_id()
|
||||
-- Just increment a monotonic counter and return it as hex. Even at
|
||||
-- unreasonably fast ID generation rates, it would take years for this
|
||||
-- counter to hit the 2^53 limit and start generating duplicates.
|
||||
next_id = next_id + 1
|
||||
return string.format("_%X", next_id)
|
||||
end
|
||||
|
||||
ui._ID_CHARS = "a-zA-Z0-9_%-%:"
|
||||
|
||||
function ui.is_id(str)
|
||||
return type(str) == "string" and str == str:match("^[" .. ui._ID_CHARS .. "]+$")
|
||||
end
|
||||
|
||||
function ui._is_reserved_id(str)
|
||||
return ui.is_id(str) and str:match("^[_-]")
|
||||
end
|
||||
|
||||
-- This coordinate size calculation copies the one for fixed-size formspec
|
||||
-- coordinates in guiFormSpecMenu.cpp.
|
||||
function ui.get_coord_size()
|
||||
return math.floor(0.5555 * 96)
|
||||
end
|
||||
|
||||
function ui._req(val, typ)
|
||||
assert(type(val) == typ or
|
||||
(typ == "id" and ui.is_id(val)) or core.is_instance(val, typ))
|
||||
return val
|
||||
end
|
||||
|
||||
function ui._opt(val, typ, def)
|
||||
if val == nil then
|
||||
return def
|
||||
end
|
||||
return ui._req(val, typ)
|
||||
end
|
||||
|
||||
function ui._req_array(arr, typ)
|
||||
for _, val in ipairs(ui._req(arr, "table")) do
|
||||
ui._req(val, typ)
|
||||
end
|
||||
return arr
|
||||
end
|
||||
|
||||
function ui._opt_array(arr, typ, def)
|
||||
for _, val in ipairs(ui._opt(arr, "table", {})) do
|
||||
ui._req(val, typ)
|
||||
end
|
||||
return arr or def
|
||||
end
|
||||
|
||||
function ui._req_enum(val, enum)
|
||||
assert(type(val) == "string" and enum[val])
|
||||
return val
|
||||
end
|
||||
|
||||
function ui._opt_enum(val, enum, def)
|
||||
if val == nil then
|
||||
return def
|
||||
end
|
||||
return ui._req_enum(val, enum)
|
||||
end
|
||||
|
||||
ui._encode = core.encode_network
|
||||
ui._decode = core.decode_network
|
||||
|
||||
function ui._encode_array(format, arr)
|
||||
local formatted = {}
|
||||
for _, val in ipairs(arr) do
|
||||
table.insert(formatted, ui._encode(format, val))
|
||||
end
|
||||
|
||||
return ui._encode("IZ", #formatted, table.concat(formatted))
|
||||
end
|
||||
|
||||
function ui._pack_flags(...)
|
||||
local flags = 0
|
||||
for _, flag in ipairs({...}) do
|
||||
flags = bit.bor(bit.lshift(flags, 1), flag and 1 or 0)
|
||||
end
|
||||
return flags
|
||||
end
|
||||
|
||||
function ui._make_flags()
|
||||
return {flags = 0, num_flags = 0, data = {}}
|
||||
end
|
||||
|
||||
function ui._shift_flag(fl, flag)
|
||||
-- OR the LSB with the condition, and then right rotate it to the MSB.
|
||||
fl.flags = bit.ror(bit.bor(fl.flags, flag and 1 or 0), 1)
|
||||
fl.num_flags = fl.num_flags + 1
|
||||
|
||||
return flag
|
||||
end
|
||||
|
||||
function ui._shift_flag_bool(fl, flag)
|
||||
if ui._shift_flag(fl, flag ~= nil) then
|
||||
ui._shift_flag(fl, flag)
|
||||
else
|
||||
ui._shift_flag(fl, false)
|
||||
end
|
||||
end
|
||||
|
||||
function ui._encode_flag(fl, ...)
|
||||
table.insert(fl.data, ui._encode(...))
|
||||
end
|
||||
|
||||
function ui._encode_flags(fl)
|
||||
-- We've been shifting into the right the entire time, so flags are in the
|
||||
-- upper bits; however, the protocol expects them to be in the lower bits.
|
||||
-- So, shift them the appropriate amount into the lower bits.
|
||||
local adjusted = bit.rshift(fl.flags, 32 - fl.num_flags)
|
||||
return ui._encode("I", adjusted) .. table.concat(fl.data)
|
||||
end
|
283
builtin/ui/window.lua
Normal file
283
builtin/ui/window.lua
Normal file
|
@ -0,0 +1,283 @@
|
|||
-- Luanti
|
||||
-- SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
-- Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
ui.Window = core.class()
|
||||
|
||||
ui._window_types = {
|
||||
filter = 0,
|
||||
mask = 1,
|
||||
hud = 2,
|
||||
chat = 3,
|
||||
gui = 4,
|
||||
}
|
||||
|
||||
function ui.Window:new(param)
|
||||
local function make_window(props)
|
||||
self:_init(ui._req(props, "table"))
|
||||
return self
|
||||
end
|
||||
|
||||
self._type = ui._req_enum(param, ui._window_types)
|
||||
return make_window
|
||||
end
|
||||
|
||||
function ui.Window:_init(props)
|
||||
self._theme = ui._opt(props.theme, ui.Style, ui.get_default_theme())
|
||||
self._styles = ui._opt_array(props.styles, ui.Style, props)
|
||||
|
||||
self._root = ui._req(props.root, ui.Root)
|
||||
|
||||
self._focused = ui._opt(props.focused, "string")
|
||||
self._allow_close = ui._opt(props.allow_close, "boolean", true)
|
||||
|
||||
self._on_close = ui._opt(props.on_close, "function")
|
||||
self._on_submit = ui._opt(props.on_submit, "function")
|
||||
self._on_focus_change = ui._opt(props.on_focus_change, "function")
|
||||
|
||||
self._context = nil -- Set by ui.Context
|
||||
|
||||
self._elems = self._root:_get_flat()
|
||||
self._elems_by_id = {}
|
||||
|
||||
for _, elem in ipairs(self._elems) do
|
||||
local id = elem._id
|
||||
|
||||
assert(not self._elems_by_id[id], "Element has duplicate ID: '" .. id .. "'")
|
||||
self._elems_by_id[id] = elem
|
||||
|
||||
assert(elem._window == nil, "Element '" .. elem._id .. "' already has a window")
|
||||
elem._window = self
|
||||
end
|
||||
|
||||
if self._focused and self._focused ~= "" then
|
||||
assert(self._elems_by_id[self._focused],
|
||||
"Invalid focused element: '" .. self._focused .. "'")
|
||||
end
|
||||
|
||||
for _, item in ipairs(props) do
|
||||
ui._req(item, ui.Style)
|
||||
end
|
||||
end
|
||||
|
||||
function ui.Window:_encode(player, opening)
|
||||
local enc_styles = self:_encode_styles()
|
||||
local enc_elems = self:_encode_elems()
|
||||
|
||||
local fl = ui._make_flags()
|
||||
|
||||
if ui._shift_flag(fl, self._focused) then
|
||||
ui._encode_flag(fl, "z", self._focused)
|
||||
end
|
||||
ui._shift_flag(fl, opening and self._allow_close)
|
||||
|
||||
ui._shift_flag(fl, self._on_submit)
|
||||
ui._shift_flag(fl, self._on_focus_change)
|
||||
|
||||
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 ui._encode("ZZ", data, ui._encode_flags(fl))
|
||||
end
|
||||
|
||||
function ui.Window:_encode_styles()
|
||||
-- Clear out all the boxes in every element.
|
||||
for _, elem in ipairs(self._elems) do
|
||||
for box in pairs(elem._boxes) do
|
||||
elem._boxes[box] = {n = 0}
|
||||
end
|
||||
end
|
||||
|
||||
-- Get a cascaded and flattened list of all the styles for this window.
|
||||
local styles = self:_get_full_style():_get_flat()
|
||||
|
||||
-- Take each style and apply its properties to every box and state matched
|
||||
-- by its selector.
|
||||
self:_apply_styles(styles)
|
||||
|
||||
-- Take the styled boxes and encode their styles into a single table,
|
||||
-- replacing the boxes' style property tables with indices into this table.
|
||||
local enc_styles = self:_index_styles()
|
||||
|
||||
return ui._encode_array("Z", enc_styles)
|
||||
end
|
||||
|
||||
function ui.Window:_get_full_style()
|
||||
-- The full style contains the theme, global styles, and local element
|
||||
-- styles as sub-styles, in that order, to ensure the correct precedence.
|
||||
local styles = table.merge({self._theme}, self._styles)
|
||||
|
||||
for _, elem in ipairs(self._elems) do
|
||||
-- Cascade the inline style with the element's ID, ensuring that the
|
||||
-- inline style globally refers to this element only.
|
||||
local local_style = ui.Style("#" .. elem._id) {
|
||||
props = elem._props,
|
||||
nested = elem._styles,
|
||||
}
|
||||
table.insert(styles, local_style)
|
||||
end
|
||||
|
||||
-- Return all these styles wrapped up into a single style.
|
||||
return ui.Style {nested = styles}
|
||||
end
|
||||
|
||||
local function apply_style(elem, boxes, style)
|
||||
-- Loop through each box, applying the styles accordingly. The table of
|
||||
-- boxes may be empty, in which case nothing happens.
|
||||
for _, box in pairs(boxes) do
|
||||
local name = box.name or "main"
|
||||
|
||||
-- If this style resets all properties, find all states that are a
|
||||
-- subset of the state being styled and clear their property tables.
|
||||
if style._reset then
|
||||
for i = ui._STATE_NONE, ui._NUM_STATES - 1 do
|
||||
if bit.band(box.states, i) == box.states then
|
||||
elem._boxes[name][i] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Get the existing style property table for this box if it exists.
|
||||
local props = elem._boxes[name][box.states] or {}
|
||||
|
||||
-- Cascade the properties from this style onto the box.
|
||||
elem._boxes[name][box.states] = ui._cascade_props(style._props, props)
|
||||
end
|
||||
end
|
||||
|
||||
function ui.Window:_apply_styles(styles)
|
||||
-- Loop through each style and element and see if the style properties can
|
||||
-- be applied to any boxes.
|
||||
for _, style in ipairs(styles) do
|
||||
for _, elem in ipairs(self._elems) do
|
||||
-- Check if the selector for this style. If it matches, apply the
|
||||
-- style to each of the applicable boxes.
|
||||
local matches, boxes = style._sel(elem)
|
||||
if matches then
|
||||
apply_style(elem, boxes, style)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function index_style(box, i, style_indices, enc_styles)
|
||||
-- If we have a style for this state, serialize it to a string. Identical
|
||||
-- styles have identical strings, so we use this to our advantage.
|
||||
local enc = ui._encode_props(box[i])
|
||||
|
||||
-- If we haven't serialized a style identical to this one before, store
|
||||
-- this as the latest index in the list of style strings.
|
||||
if not style_indices[enc] then
|
||||
style_indices[enc] = #enc_styles
|
||||
table.insert(enc_styles, enc)
|
||||
end
|
||||
|
||||
-- Set the index of our state to the index of its style string, and keep
|
||||
-- count of how many states with valid indices we have for this box so far.
|
||||
box[i] = style_indices[enc]
|
||||
box.n = box.n + 1
|
||||
end
|
||||
|
||||
function ui.Window:_index_styles()
|
||||
local style_indices = {}
|
||||
local enc_styles = {}
|
||||
|
||||
for _, elem in ipairs(self._elems) do
|
||||
for _, box in pairs(elem._boxes) do
|
||||
for i = ui._STATE_NONE, ui._NUM_STATES - 1 do
|
||||
if box[i] then
|
||||
-- If this box has a style, encode and index it.
|
||||
index_style(box, i, style_indices, enc_styles)
|
||||
else
|
||||
-- Otherwise, this state has no style, so set it as such.
|
||||
box[i] = ui._NO_STYLE
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return enc_styles
|
||||
end
|
||||
|
||||
function ui.Window:_encode_elems()
|
||||
local enc_elems = {}
|
||||
|
||||
for _, elem in ipairs(self._elems) do
|
||||
table.insert(enc_elems, elem:_encode())
|
||||
end
|
||||
|
||||
return ui._encode_array("Z", enc_elems)
|
||||
end
|
||||
|
||||
function ui.Window:_on_window_event(code, ev, data)
|
||||
-- Get the handler function for this event if we recognize it.
|
||||
local handler = self._handlers[code]
|
||||
if not handler then
|
||||
core.log("info", "Invalid window event: " .. code)
|
||||
return
|
||||
end
|
||||
|
||||
-- If the event handler returned a callback function for the user, call it
|
||||
-- with the event table.
|
||||
local callback = handler(self, ev, data)
|
||||
if callback then
|
||||
callback(ev)
|
||||
end
|
||||
end
|
||||
|
||||
function ui.Window:_on_elem_event(code, ev, data)
|
||||
local type_id, target, rest = ui._decode("BzZ", data, -1)
|
||||
ev.target = target
|
||||
|
||||
-- Get the element for this ID. If it doesn't exist or has a different
|
||||
-- type, the window probably updated before receiving this event.
|
||||
local elem = self._elems_by_id[target]
|
||||
if not elem then
|
||||
core.log("info", "Dropped event for non-existent element '" .. target .. "'")
|
||||
return
|
||||
elseif elem._type_id ~= type_id then
|
||||
core.log("info", "Dropped event with type " .. type_id ..
|
||||
" sent to element with type " .. elem._type_id)
|
||||
return
|
||||
end
|
||||
|
||||
-- Pass the event and data to the element for further processing.
|
||||
elem:_on_event(code, ev, rest)
|
||||
end
|
||||
|
||||
ui.Window._handlers = {}
|
||||
|
||||
ui.Window._handlers[0x00] = function(self, ev, data)
|
||||
-- We should never receive an event for an uncloseable window. If we
|
||||
-- did, this player might be trying to cheat.
|
||||
if not self._allow_close then
|
||||
core.log("action", "Player '" .. self._context:get_player() ..
|
||||
"' closed uncloseable window")
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Since the window is now closed, remove the open window data.
|
||||
self._context:_close_window()
|
||||
return self._on_close
|
||||
end
|
||||
|
||||
ui.Window._handlers[0x01] = function(self, ev, data)
|
||||
return self._on_submit
|
||||
end
|
||||
|
||||
ui.Window._handlers[0x02] = function(self, ev, data)
|
||||
ev.unfocused, ev.focused = ui._decode("zz", data)
|
||||
|
||||
-- If the ID for either element doesn't exist, we probably updated the
|
||||
-- window to remove the element. Assume nothing is focused then.
|
||||
if not self._elems_by_id[ev.unfocused] then
|
||||
ev.unfocused = ""
|
||||
end
|
||||
if not self._elems_by_id[ev.focused] then
|
||||
ev.focused = ""
|
||||
end
|
||||
|
||||
return self._on_focus_change
|
||||
end
|
|
@ -14,6 +14,7 @@ General options and their default values:
|
|||
BUILD_UNITTESTS=TRUE - Build unittest sources
|
||||
BUILD_BENCHMARKS=FALSE - Build benchmark sources
|
||||
BUILD_DOCUMENTATION=TRUE - Build doxygen documentation
|
||||
BUILD_UI=TRUE - Build experimental UI API; requires BUILD_CLIENT and USE_SDL2
|
||||
CMAKE_BUILD_TYPE=Release - Type of build (Release vs. Debug)
|
||||
Release - Release build
|
||||
Debug - Debug build
|
||||
|
|
2087
doc/experimental_ui_api.md
Normal file
2087
doc/experimental_ui_api.md
Normal file
File diff suppressed because it is too large
Load diff
112
doc/lua_api.md
112
doc/lua_api.md
|
@ -4237,6 +4237,8 @@ Helper functions
|
|||
* since 5.12
|
||||
* `table` can also be non-table value, which will be returned as-is
|
||||
* preserves metatables as they are
|
||||
* Returns a deep copy of `table`, i.e. a copy of the table and all its
|
||||
nested tables.
|
||||
* `table.indexof(list, val)`: returns the smallest numerical index containing
|
||||
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
|
||||
|
@ -4248,6 +4250,9 @@ Helper functions
|
|||
* `table.insert_all(table, other_table)`:
|
||||
* Appends all values in `other_table` to `table` - uses `#table + 1` to
|
||||
find new indices.
|
||||
* `table.merge(...)`:
|
||||
* Merges multiple tables together into a new single table using
|
||||
`table.insert_all()`.
|
||||
* `table.key_value_swap(t)`: returns a table with keys and values swapped
|
||||
* If multiple keys in `t` map to the same value, it is unspecified which
|
||||
value maps to that key.
|
||||
|
@ -5971,6 +5976,9 @@ Utilities
|
|||
form. If the ColorSpec is invalid, returns `nil`. You can use this to parse
|
||||
ColorStrings.
|
||||
* `colorspec`: The ColorSpec to convert
|
||||
* `core.colorspec_to_int(colorspec)`: Converts a ColorSpec to integer form.
|
||||
If the ColorSpec is invalid, returns `nil`.
|
||||
* `colorspec`: The ColorSpec to convert
|
||||
* `core.time_to_day_night_ratio(time_of_day)`: Returns a "day-night ratio" value
|
||||
(as accepted by `ObjectRef:override_day_night_ratio`) that is equivalent to
|
||||
the given "time of day" value (as returned by `core.get_timeofday`).
|
||||
|
@ -5979,8 +5987,8 @@ Utilities
|
|||
* `width`: Width of the image
|
||||
* `height`: Height of the image
|
||||
* `data`: Image data, one of:
|
||||
* array table of ColorSpec, length must be width*height
|
||||
* string with raw RGBA pixels, length must be width*height*4
|
||||
* array table of ColorSpec, length must be `width * height`
|
||||
* string with raw RGBA pixels, length must be `width * height * 4`
|
||||
* `compression`: Optional zlib compression level, number in range 0 to 9.
|
||||
The data is one-dimensional, starting in the upper left corner of the image
|
||||
and laid out in scanlines going from left to right, then top to bottom.
|
||||
|
@ -5990,6 +5998,63 @@ Utilities
|
|||
* `core.urlencode(str)`: Encodes reserved URI characters by a
|
||||
percent sign followed by two hex digits. See
|
||||
[RFC 3986, section 2.3](https://datatracker.ietf.org/doc/html/rfc3986#section-2.3).
|
||||
* `core.class([super])`: Creates a new metatable-based class.
|
||||
* `super` (optional): The superclass of the newly created class, or nil if
|
||||
the class should not have a superclass.
|
||||
* The class table is given a metatable with an `__index` field that points
|
||||
to `super` and a `__call` metamethod that constructs a new object.
|
||||
* By default, the only field in the class table is an `__index` field that
|
||||
points to itself. When an instance of the class is created, the class
|
||||
table is set as the metatable for the object.
|
||||
* When a new object is constructed via `__call`, the `new()` method will be
|
||||
called if it exists. If `new()` returns a nonzero number of values, then
|
||||
those values will be returned from `__call`. Otherwise, if `new()`
|
||||
returned no values, the object iself will be returned.
|
||||
* Extra Lua metamethods like `__add` may be added to the class table, but
|
||||
note that these fields will not be inherited by subclasses since Lua
|
||||
doesn't consult `__index` when searching for metamethods.
|
||||
* Example: The following code, demonstrating a simple example of classes
|
||||
and inheritance, will print `Rectangle[area=6, filled=true]`:
|
||||
```lua
|
||||
local Shape = core.class()
|
||||
Shape.name = "Shape"
|
||||
|
||||
function Shape:new(filled)
|
||||
self.filled = filled
|
||||
end
|
||||
|
||||
function Shape:describe()
|
||||
return string.format("%s[area=%d, filled=%s]",
|
||||
self.name, self:get_area(), self.filled)
|
||||
end
|
||||
|
||||
local Rectangle = core.class(Shape)
|
||||
Rectangle.name = "Rectangle"
|
||||
|
||||
function Rectangle:new(filled, width, height)
|
||||
Shape.new(self, filled)
|
||||
|
||||
self.width = width
|
||||
self.height = height
|
||||
end
|
||||
|
||||
function Rectangle:get_area()
|
||||
return self.width * self.height
|
||||
end
|
||||
|
||||
local shape = Rectangle(true, 2, 3)
|
||||
print(shape:describe())
|
||||
|
||||
assert(core.is_instance(shape, Shape))
|
||||
assert(core.is_subclass(Rectangle, Shape))
|
||||
assert(core.super(Rectangle) == Shape)
|
||||
```
|
||||
* `core.super(class)`: Returns the superclass of `class`, or nil if the table
|
||||
is not a class or has no superclass.
|
||||
* `core.is_subclass(class, super)`: Returns true if `class` is a subclass of
|
||||
`super`.
|
||||
* `core.is_instance(obj, class)`: Returns true if `obj` is an instance of
|
||||
`class` or any of its subclasses.
|
||||
|
||||
Logging
|
||||
-------
|
||||
|
@ -7672,6 +7737,49 @@ Misc.
|
|||
* Example: `deserialize('print("foo")')`, returns `nil`
|
||||
(function call fails), returns
|
||||
`error:[string "print("foo")"]:1: attempt to call global 'print' (a nil value)`
|
||||
* `core.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.
|
||||
* `core.decode_network(format, data, ...)`: Decodes numbers and strings from
|
||||
binary format made by `core.encode_network()` according to a format string.
|
||||
* The format string follows the same rules as `core.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.
|
||||
* `core.compress(data, method, ...)`: returns `compressed_data`
|
||||
* Compress a string of data.
|
||||
* `method` is a string identifying the compression method to be used.
|
||||
|
|
|
@ -341,3 +341,202 @@ local function test_ipc_poll(cb)
|
|||
print("delta: " .. (core.get_us_time() - t0) .. "us")
|
||||
end
|
||||
unittests.register("test_ipc_poll", test_ipc_poll)
|
||||
|
||||
unittests.register("test_encode_network", function()
|
||||
-- 8-bit integers
|
||||
assert(core.encode_network("bbbbbbb", 0, 1, -1, -128, 127, 255, 256) ==
|
||||
"\x00\x01\xFF\x80\x7F\xFF\x00")
|
||||
assert(core.encode_network("BBBBBBB", 0, 1, -1, -128, 127, 255, 256) ==
|
||||
"\x00\x01\xFF\x80\x7F\xFF\x00")
|
||||
|
||||
-- 16-bit integers
|
||||
assert(core.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(core.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(core.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(core.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(core.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(core.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(core.encode_network("ssss",
|
||||
"", "hello",
|
||||
max_16, max_16.."too long") ==
|
||||
"\x00\x00".. "\x00\x05hello"..
|
||||
"\xFF\xFF"..max_16.."\xFF\xFF"..max_16)
|
||||
assert(core.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(core.encode_network("zzzz",
|
||||
"", "hello", "hello\0embedded", max_16.."longer") ==
|
||||
"\0".."hello\0".."hello\0".. max_16.."longer\0")
|
||||
assert(core.encode_network("ZZZZ",
|
||||
"", "hello", "hello\0embedded", max_16.."longer") ==
|
||||
"".."hello".."hello\0embedded"..max_16.."longer")
|
||||
|
||||
-- Spaces
|
||||
assert(core.encode_network("B I", 255, 2^31) == "\xFF\x80\x00\x00\x00")
|
||||
assert(core.encode_network(" B Zz ", 15, "abc", "xyz") == "\x0Fabcxyz\0")
|
||||
|
||||
-- Empty format strings
|
||||
assert(core.encode_network("") == "")
|
||||
assert(core.encode_network(" ", 5, "extra args") == "")
|
||||
end)
|
||||
|
||||
unittests.register("test_decode_network", function()
|
||||
local d
|
||||
|
||||
-- 8-bit integers
|
||||
d = {core.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 = {core.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 = {core.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 = {core.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 = {core.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 = {core.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 = {core.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 = {core.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 = core.encode_network("fff",
|
||||
0.0, 123.456, -987.654)
|
||||
assert(#enc == 3 * 4)
|
||||
|
||||
d = {core.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 = {core.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 = {core.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 = {core.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 = {core.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 = {core.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 = {core.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 = {core.decode_network("B I", "\xFF\x80\x00\x00\x00")}
|
||||
assert(#d == 2)
|
||||
assert(d[1] == 255 and d[2] == 2^31)
|
||||
|
||||
d = {core.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 = {core.decode_network("", "some random data")}
|
||||
assert(#d == 0)
|
||||
d = {core.decode_network(" ", "some random data", 3, 5)}
|
||||
assert(#d == 0)
|
||||
end)
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
#include "Keycodes.h"
|
||||
#include "irrString.h"
|
||||
|
||||
union SDL_Event;
|
||||
|
||||
namespace irr
|
||||
{
|
||||
//! Enumeration for all event types there are.
|
||||
|
@ -82,6 +84,11 @@ enum EEVENT_TYPE
|
|||
//! Application state events like a resume, pause etc.
|
||||
EET_APPLICATION_EVENT,
|
||||
|
||||
//! Any other raw SDL event that doesn't fall into the above categories. In
|
||||
// this case, only SEvent::SdlEvent contains valid information. Note that
|
||||
// SEvent::SdlEvent is non-nullptr for most other event types as well.
|
||||
EET_OTHER_SDL_EVENT,
|
||||
|
||||
//! This enum is never used, it only forces the compiler to
|
||||
//! compile these enumeration values to 32 bit.
|
||||
EGUIET_FORCE_32_BIT = 0x7fffffff
|
||||
|
@ -526,6 +533,7 @@ struct SEvent
|
|||
};
|
||||
|
||||
EEVENT_TYPE EventType;
|
||||
union SDL_Event *SdlEvent = nullptr;
|
||||
union
|
||||
{
|
||||
struct SGUIEvent GUIEvent;
|
||||
|
|
|
@ -714,6 +714,10 @@ bool CIrrDeviceSDL::run()
|
|||
// os::Printer::log("event: ", core::stringc((int)SDL_event.type).c_str(), ELL_INFORMATION); // just for debugging
|
||||
irrevent = {};
|
||||
|
||||
// Initially, we assume that there is no other applicable event type.
|
||||
irrevent.EventType = irr::EET_OTHER_SDL_EVENT;
|
||||
irrevent.SdlEvent = &SDL_event;
|
||||
|
||||
switch (SDL_event.type) {
|
||||
case SDL_MOUSEMOTION: {
|
||||
SDL_Keymod keymod = SDL_GetModState();
|
||||
|
@ -736,31 +740,28 @@ bool CIrrDeviceSDL::run()
|
|||
irrevent.MouseInput.ButtonStates = MouseButtonStates;
|
||||
irrevent.MouseInput.Shift = (keymod & KMOD_SHIFT) != 0;
|
||||
irrevent.MouseInput.Control = (keymod & KMOD_CTRL) != 0;
|
||||
|
||||
postEventFromUser(irrevent);
|
||||
break;
|
||||
}
|
||||
case SDL_MOUSEWHEEL: {
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 18)
|
||||
double wheel = SDL_event.wheel.preciseY;
|
||||
#else
|
||||
double wheel = SDL_event.wheel.y;
|
||||
#endif
|
||||
// wheel y can be 0 if scrolling sideways
|
||||
if (wheel == 0.0f)
|
||||
break;
|
||||
|
||||
SDL_Keymod keymod = SDL_GetModState();
|
||||
|
||||
irrevent.EventType = irr::EET_MOUSE_INPUT_EVENT;
|
||||
irrevent.MouseInput.Event = irr::EMIE_MOUSE_WHEEL;
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 18)
|
||||
irrevent.MouseInput.Wheel = SDL_event.wheel.preciseY;
|
||||
#else
|
||||
irrevent.MouseInput.Wheel = SDL_event.wheel.y;
|
||||
#endif
|
||||
irrevent.MouseInput.Wheel = wheel;
|
||||
irrevent.MouseInput.ButtonStates = MouseButtonStates;
|
||||
irrevent.MouseInput.Shift = (keymod & KMOD_SHIFT) != 0;
|
||||
irrevent.MouseInput.Control = (keymod & KMOD_CTRL) != 0;
|
||||
irrevent.MouseInput.X = MouseX;
|
||||
irrevent.MouseInput.Y = MouseY;
|
||||
|
||||
// wheel y can be 0 if scrolling sideways
|
||||
if (irrevent.MouseInput.Wheel == 0.0f)
|
||||
break;
|
||||
|
||||
postEventFromUser(irrevent);
|
||||
break;
|
||||
}
|
||||
case SDL_MOUSEBUTTONDOWN:
|
||||
|
@ -854,16 +855,22 @@ bool CIrrDeviceSDL::run()
|
|||
irrevent.MouseInput.Y = static_cast<s32>(SDL_event.button.y * ScaleY);
|
||||
irrevent.MouseInput.Shift = shift;
|
||||
irrevent.MouseInput.Control = control;
|
||||
postEventFromUser(irrevent);
|
||||
|
||||
if (irrevent.MouseInput.Event >= EMIE_LMOUSE_PRESSED_DOWN && irrevent.MouseInput.Event <= EMIE_MMOUSE_PRESSED_DOWN) {
|
||||
u32 clicks = checkSuccessiveClicks(irrevent.MouseInput.X, irrevent.MouseInput.Y, irrevent.MouseInput.Event);
|
||||
if (clicks == 2) {
|
||||
// Since we need to send two events, explicitly send the first
|
||||
// event and clear out the SdlEvent field for the second event so
|
||||
// we don't get duplicate SDL events.
|
||||
postEventFromUser(irrevent);
|
||||
irrevent.SdlEvent = nullptr;
|
||||
|
||||
irrevent.MouseInput.Event = (EMOUSE_INPUT_EVENT)(EMIE_LMOUSE_DOUBLE_CLICK + irrevent.MouseInput.Event - EMIE_LMOUSE_PRESSED_DOWN);
|
||||
postEventFromUser(irrevent);
|
||||
} else if (clicks == 3) {
|
||||
irrevent.MouseInput.Event = (EMOUSE_INPUT_EVENT)(EMIE_LMOUSE_TRIPLE_CLICK + irrevent.MouseInput.Event - EMIE_LMOUSE_PRESSED_DOWN);
|
||||
postEventFromUser(irrevent);
|
||||
irrevent.SdlEvent = nullptr;
|
||||
|
||||
irrevent.MouseInput.Event = (EMOUSE_INPUT_EVENT)(EMIE_LMOUSE_TRIPLE_CLICK + irrevent.MouseInput.Event - EMIE_LMOUSE_PRESSED_DOWN);
|
||||
}
|
||||
}
|
||||
} else if (irrevent.EventType == irr::EET_KEY_INPUT_EVENT) {
|
||||
|
@ -871,7 +878,6 @@ bool CIrrDeviceSDL::run()
|
|||
irrevent.KeyInput.PressedDown = SDL_event.type == SDL_MOUSEBUTTONDOWN;
|
||||
irrevent.KeyInput.Shift = shift;
|
||||
irrevent.KeyInput.Control = control;
|
||||
postEventFromUser(irrevent);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -880,9 +886,6 @@ bool CIrrDeviceSDL::run()
|
|||
irrevent.EventType = irr::EET_STRING_INPUT_EVENT;
|
||||
irrevent.StringInput.Str = new core::stringw();
|
||||
irr::core::utf8ToWString(*irrevent.StringInput.Str, SDL_event.text.text);
|
||||
postEventFromUser(irrevent);
|
||||
delete irrevent.StringInput.Str;
|
||||
irrevent.StringInput.Str = NULL;
|
||||
} break;
|
||||
|
||||
case SDL_KEYDOWN:
|
||||
|
@ -914,8 +917,6 @@ bool CIrrDeviceSDL::run()
|
|||
irrevent.KeyInput.Char = findCharToPassToIrrlicht(keysym, key,
|
||||
(SDL_event.key.keysym.mod & KMOD_NUM) != 0);
|
||||
irrevent.KeyInput.SystemKeyCode = scancode;
|
||||
|
||||
postEventFromUser(irrevent);
|
||||
} break;
|
||||
|
||||
case SDL_QUIT:
|
||||
|
@ -939,7 +940,6 @@ bool CIrrDeviceSDL::run()
|
|||
if (old_scale_x != ScaleX || old_scale_y != ScaleY) {
|
||||
irrevent.EventType = EET_APPLICATION_EVENT;
|
||||
irrevent.ApplicationEvent.EventType = EAET_DPI_CHANGED;
|
||||
postEventFromUser(irrevent);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -949,8 +949,6 @@ bool CIrrDeviceSDL::run()
|
|||
irrevent.EventType = irr::EET_USER_EVENT;
|
||||
irrevent.UserEvent.UserData1 = reinterpret_cast<uintptr_t>(SDL_event.user.data1);
|
||||
irrevent.UserEvent.UserData2 = reinterpret_cast<uintptr_t>(SDL_event.user.data2);
|
||||
|
||||
postEventFromUser(irrevent);
|
||||
break;
|
||||
|
||||
case SDL_FINGERDOWN:
|
||||
|
@ -961,8 +959,6 @@ bool CIrrDeviceSDL::run()
|
|||
irrevent.TouchInput.Y = static_cast<s32>(SDL_event.tfinger.y * Height);
|
||||
CurrentTouchCount++;
|
||||
irrevent.TouchInput.touchedCount = CurrentTouchCount;
|
||||
|
||||
postEventFromUser(irrevent);
|
||||
break;
|
||||
|
||||
case SDL_FINGERMOTION:
|
||||
|
@ -972,8 +968,6 @@ bool CIrrDeviceSDL::run()
|
|||
irrevent.TouchInput.X = static_cast<s32>(SDL_event.tfinger.x * Width);
|
||||
irrevent.TouchInput.Y = static_cast<s32>(SDL_event.tfinger.y * Height);
|
||||
irrevent.TouchInput.touchedCount = CurrentTouchCount;
|
||||
|
||||
postEventFromUser(irrevent);
|
||||
break;
|
||||
|
||||
case SDL_FINGERUP:
|
||||
|
@ -988,8 +982,6 @@ bool CIrrDeviceSDL::run()
|
|||
if (CurrentTouchCount > 0) {
|
||||
CurrentTouchCount--;
|
||||
}
|
||||
|
||||
postEventFromUser(irrevent);
|
||||
break;
|
||||
|
||||
// Contrary to what the SDL documentation says, SDL_APP_WILLENTERBACKGROUND
|
||||
|
@ -1017,6 +1009,14 @@ bool CIrrDeviceSDL::run()
|
|||
default:
|
||||
break;
|
||||
} // end switch
|
||||
|
||||
postEventFromUser(irrevent);
|
||||
|
||||
if (SDL_event.type == SDL_TEXTINPUT) {
|
||||
delete irrevent.StringInput.Str;
|
||||
irrevent.StringInput.Str = nullptr;
|
||||
}
|
||||
|
||||
resetReceiveTextInputEvents();
|
||||
} // end while
|
||||
|
||||
|
|
|
@ -529,6 +529,10 @@ if (BUILD_CLIENT)
|
|||
add_subdirectory(client)
|
||||
add_subdirectory(gui)
|
||||
add_subdirectory(irrlicht_changes)
|
||||
|
||||
if(BUILD_UI)
|
||||
add_subdirectory(ui)
|
||||
endif()
|
||||
endif(BUILD_CLIENT)
|
||||
|
||||
list(APPEND client_SRCS
|
||||
|
@ -542,10 +546,12 @@ list(APPEND client_SRCS
|
|||
if(BUILD_UNITTESTS)
|
||||
list(APPEND client_SRCS ${UNITTEST_CLIENT_SRCS})
|
||||
endif()
|
||||
|
||||
if(BUILD_BENCHMARKS)
|
||||
list(APPEND client_SRCS ${BENCHMARK_CLIENT_SRCS})
|
||||
endif()
|
||||
if(BUILD_UI)
|
||||
set(client_SRCS ${client_SRCS} ${ui_SRCS})
|
||||
endif()
|
||||
|
||||
# Server sources
|
||||
# (nothing here because a client always comes with a server)
|
||||
|
@ -587,6 +593,10 @@ if(BUILD_CLIENT)
|
|||
${FREETYPE_INCLUDE_DIRS}
|
||||
${SOUND_INCLUDE_DIRS}
|
||||
)
|
||||
|
||||
if(BUILD_UI)
|
||||
include_directories(SYSTEM ${SDL2_INCLUDE_DIRS})
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(USE_CURL)
|
||||
|
@ -756,6 +766,9 @@ if(BUILD_CLIENT)
|
|||
if(BUILD_WITH_TRACY)
|
||||
target_link_libraries(${PROJECT_NAME} Tracy::TracyClient)
|
||||
endif()
|
||||
if(BUILD_UI)
|
||||
target_link_libraries(${PROJECT_NAME} ${SDL2_LIBRARIES})
|
||||
endif()
|
||||
|
||||
if(PRECOMPILE_HEADERS)
|
||||
target_precompile_headers(${PROJECT_NAME} PRIVATE ${PRECOMPILED_HEADERS_LIST})
|
||||
|
|
|
@ -1283,6 +1283,14 @@ void Client::sendInventoryFields(const std::string &formname,
|
|||
Send(&pkt);
|
||||
}
|
||||
|
||||
void Client::sendUiMessage(const char *data, size_t len)
|
||||
{
|
||||
NetworkPacket pkt(TOSERVER_UI_MESSAGE, 0);
|
||||
pkt.putRawString(data, len);
|
||||
|
||||
Send(&pkt);
|
||||
}
|
||||
|
||||
void Client::sendInventoryAction(InventoryAction *a)
|
||||
{
|
||||
std::ostringstream os(std::ios_base::binary);
|
||||
|
|
|
@ -192,6 +192,7 @@ public:
|
|||
void handleCommand_InventoryFormSpec(NetworkPacket* pkt);
|
||||
void handleCommand_DetachedInventory(NetworkPacket* pkt);
|
||||
void handleCommand_ShowFormSpec(NetworkPacket* pkt);
|
||||
void handleCommand_UiMessage(NetworkPacket* pkt);
|
||||
void handleCommand_SpawnParticle(NetworkPacket* pkt);
|
||||
void handleCommand_AddParticleSpawner(NetworkPacket* pkt);
|
||||
void handleCommand_DeleteParticleSpawner(NetworkPacket* pkt);
|
||||
|
@ -230,6 +231,7 @@ public:
|
|||
const StringMap &fields);
|
||||
void sendInventoryFields(const std::string &formname,
|
||||
const StringMap &fields);
|
||||
void sendUiMessage(const char *data, size_t len);
|
||||
void sendInventoryAction(InventoryAction *a);
|
||||
void sendChatMessage(const std::wstring &message);
|
||||
void clearOutChatQueue();
|
||||
|
|
|
@ -24,6 +24,7 @@ enum ClientEventType : u8
|
|||
CE_SHOW_FORMSPEC,
|
||||
CE_SHOW_CSM_FORMSPEC,
|
||||
CE_SHOW_PAUSE_MENU_FORMSPEC,
|
||||
CE_UI_MESSAGE,
|
||||
CE_SPAWN_PARTICLE,
|
||||
CE_ADD_PARTICLESPAWNER,
|
||||
CE_DELETE_PARTICLESPAWNER,
|
||||
|
@ -90,6 +91,10 @@ struct ClientEvent
|
|||
std::string *formspec;
|
||||
std::string *formname;
|
||||
} show_formspec;
|
||||
struct
|
||||
{
|
||||
std::string *data;
|
||||
} ui_message;
|
||||
ParticleParameters *spawn_particle;
|
||||
struct
|
||||
{
|
||||
|
|
|
@ -64,6 +64,9 @@
|
|||
#if USE_SOUND
|
||||
#include "client/sound/sound_openal.h"
|
||||
#endif
|
||||
#if BUILD_UI
|
||||
#include "ui/manager.h"
|
||||
#endif
|
||||
|
||||
#include <csignal>
|
||||
|
||||
|
@ -727,6 +730,7 @@ private:
|
|||
void handleClientEvent_ShowFormSpec(ClientEvent *event, CameraOrientation *cam);
|
||||
void handleClientEvent_ShowCSMFormSpec(ClientEvent *event, CameraOrientation *cam);
|
||||
void handleClientEvent_ShowPauseMenuFormSpec(ClientEvent *event, CameraOrientation *cam);
|
||||
void handleClientEvent_UiMessage(ClientEvent *event, CameraOrientation *cam);
|
||||
void handleClientEvent_HandleParticleEvent(ClientEvent *event,
|
||||
CameraOrientation *cam);
|
||||
void handleClientEvent_HudAdd(ClientEvent *event, CameraOrientation *cam);
|
||||
|
@ -777,6 +781,9 @@ private:
|
|||
|
||||
std::unique_ptr<GameUI> m_game_ui;
|
||||
irr_ptr<GUIChatConsole> gui_chat_console;
|
||||
#if BUILD_UI
|
||||
irr_ptr<ui::GUIManagerElem> gui_manager_elem;
|
||||
#endif
|
||||
MapDrawControl *draw_control = nullptr;
|
||||
Camera *camera = nullptr;
|
||||
irr_ptr<Clouds> clouds;
|
||||
|
@ -1117,6 +1124,10 @@ void Game::run()
|
|||
void Game::shutdown()
|
||||
{
|
||||
// Delete text and menus first
|
||||
#if BUILD_UI
|
||||
ui::g_manager.reset();
|
||||
#endif
|
||||
|
||||
m_game_ui->clearText();
|
||||
m_game_formspec.reset();
|
||||
while (g_menumgr.menuCount() > 0) {
|
||||
|
@ -1129,6 +1140,9 @@ void Game::shutdown()
|
|||
clouds.reset();
|
||||
|
||||
gui_chat_console.reset();
|
||||
#if BUILD_UI
|
||||
gui_manager_elem.reset();
|
||||
#endif
|
||||
|
||||
sky.reset();
|
||||
|
||||
|
@ -1426,6 +1440,10 @@ bool Game::createClient(const GameStartData &start_data)
|
|||
if (mapper && client->modsLoaded())
|
||||
client->getScript()->on_minimap_ready(mapper);
|
||||
|
||||
#if BUILD_UI
|
||||
ui::g_manager.setClient(client);
|
||||
#endif
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -1454,6 +1472,12 @@ bool Game::initGui()
|
|||
gui_chat_console = make_irr<GUIChatConsole>(guienv, guienv->getRootGUIElement(),
|
||||
-1, chat_backend, client, &g_menumgr);
|
||||
|
||||
#if BUILD_UI
|
||||
// Thingy to draw UI manager after chat but before formspecs.
|
||||
gui_manager_elem = make_irr<ui::GUIManagerElem>(
|
||||
guienv, guienv->getRootGUIElement(), -1);
|
||||
#endif
|
||||
|
||||
if (shouldShowTouchControls())
|
||||
g_touchcontrols = new TouchControls(device, texture_src);
|
||||
|
||||
|
@ -2653,6 +2677,7 @@ const ClientEventHandler Game::clientEventHandler[CLIENTEVENT_MAX] = {
|
|||
{&Game::handleClientEvent_ShowFormSpec},
|
||||
{&Game::handleClientEvent_ShowCSMFormSpec},
|
||||
{&Game::handleClientEvent_ShowPauseMenuFormSpec},
|
||||
{&Game::handleClientEvent_UiMessage},
|
||||
{&Game::handleClientEvent_HandleParticleEvent},
|
||||
{&Game::handleClientEvent_HandleParticleEvent},
|
||||
{&Game::handleClientEvent_HandleParticleEvent},
|
||||
|
@ -2744,6 +2769,14 @@ void Game::handleClientEvent_ShowPauseMenuFormSpec(ClientEvent *event, CameraOri
|
|||
delete event->show_formspec.formname;
|
||||
}
|
||||
|
||||
void Game::handleClientEvent_UiMessage(ClientEvent *event, CameraOrientation *cam)
|
||||
{
|
||||
#if BUILD_UI
|
||||
ui::g_manager.receiveMessage(*event->ui_message.data);
|
||||
#endif
|
||||
delete event->ui_message.data;
|
||||
}
|
||||
|
||||
void Game::handleClientEvent_HandleParticleEvent(ClientEvent *event,
|
||||
CameraOrientation *cam)
|
||||
{
|
||||
|
@ -4156,7 +4189,7 @@ void Game::drawScene(ProfilerGraph *graph, RunStats *stats)
|
|||
draw_crosshair = false;
|
||||
|
||||
this->m_rendering_engine->draw_scene(sky_color, this->m_game_ui->m_flags.show_hud,
|
||||
draw_wield_tool, draw_crosshair);
|
||||
this->m_game_ui->m_flags.show_chat, draw_wield_tool, draw_crosshair);
|
||||
|
||||
/*
|
||||
Profiler graph
|
||||
|
|
|
@ -163,7 +163,7 @@ void GameUI::update(const RunStats &stats, Client *client, MapDrawControl *draw_
|
|||
m_guitext2->setVisible(m_flags.show_basic_debug);
|
||||
|
||||
setStaticText(m_guitext_info, m_infotext.c_str());
|
||||
m_guitext_info->setVisible(m_flags.show_hud && g_menumgr.menuCount() == 0);
|
||||
m_guitext_info->setVisible(m_flags.show_hud && !isMenuActive());
|
||||
|
||||
static const float statustext_time_max = 1.5f;
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ RenderingCore::~RenderingCore()
|
|||
delete shadow_renderer;
|
||||
}
|
||||
|
||||
void RenderingCore::draw(video::SColor _skycolor, bool _show_hud,
|
||||
void RenderingCore::draw(video::SColor _skycolor, bool _show_hud, bool _show_chat,
|
||||
bool _draw_wield_tool, bool _draw_crosshair)
|
||||
{
|
||||
v2u32 screensize = device->getVideoDriver()->getScreenSize();
|
||||
|
@ -31,6 +31,7 @@ void RenderingCore::draw(video::SColor _skycolor, bool _show_hud,
|
|||
context.draw_crosshair = _draw_crosshair;
|
||||
context.draw_wield_tool = _draw_wield_tool;
|
||||
context.show_hud = _show_hud;
|
||||
context.show_chat = _show_chat;
|
||||
|
||||
pipeline->reset(context);
|
||||
pipeline->run(context);
|
||||
|
|
|
@ -45,7 +45,7 @@ public:
|
|||
RenderingCore &operator=(const RenderingCore &) = delete;
|
||||
RenderingCore &operator=(RenderingCore &&) = delete;
|
||||
|
||||
void draw(video::SColor _skycolor, bool _show_hud,
|
||||
void draw(video::SColor _skycolor, bool _show_hud, bool _show_chat,
|
||||
bool _draw_wield_tool, bool _draw_crosshair);
|
||||
|
||||
v2u32 getVirtualSize() const;
|
||||
|
|
|
@ -38,6 +38,7 @@ struct PipelineContext
|
|||
v2u32 target_size;
|
||||
|
||||
bool show_hud {true};
|
||||
bool show_chat {true};
|
||||
bool draw_wield_tool {true};
|
||||
bool draw_crosshair {true};
|
||||
};
|
||||
|
|
|
@ -13,6 +13,10 @@
|
|||
#include "client/shadows/dynamicshadowsrender.h"
|
||||
#include <IGUIEnvironment.h>
|
||||
|
||||
#if BUILD_UI
|
||||
#include "ui/manager.h"
|
||||
#endif
|
||||
|
||||
/// Draw3D pipeline step
|
||||
void Draw3D::run(PipelineContext &context)
|
||||
{
|
||||
|
@ -29,6 +33,11 @@ void Draw3D::run(PipelineContext &context)
|
|||
|
||||
void DrawWield::run(PipelineContext &context)
|
||||
{
|
||||
#if BUILD_UI
|
||||
ui::g_manager.preDraw();
|
||||
ui::g_manager.drawType(ui::WindowType::FILTER);
|
||||
#endif
|
||||
|
||||
if (m_target)
|
||||
m_target->activate(context);
|
||||
|
||||
|
@ -46,10 +55,26 @@ void DrawHUD::run(PipelineContext &context)
|
|||
|
||||
if (context.draw_crosshair)
|
||||
context.hud->drawCrosshair();
|
||||
}
|
||||
|
||||
#if BUILD_UI
|
||||
ui::g_manager.drawType(ui::WindowType::MASK);
|
||||
#endif
|
||||
|
||||
if (context.show_hud) {
|
||||
context.hud->drawLuaElements(context.client->getCamera()->getOffset());
|
||||
#if BUILD_UI
|
||||
ui::g_manager.drawType(ui::WindowType::HUD);
|
||||
#endif
|
||||
|
||||
context.client->getCamera()->drawNametags();
|
||||
}
|
||||
|
||||
#if BUILD_UI
|
||||
if (context.show_chat)
|
||||
ui::g_manager.drawType(ui::WindowType::CHAT);
|
||||
#endif
|
||||
|
||||
context.device->getGUIEnvironment()->drawAll();
|
||||
}
|
||||
|
||||
|
|
|
@ -401,10 +401,10 @@ void RenderingEngine::finalize()
|
|||
core.reset();
|
||||
}
|
||||
|
||||
void RenderingEngine::draw_scene(video::SColor skycolor, bool show_hud,
|
||||
void RenderingEngine::draw_scene(video::SColor skycolor, bool show_hud, bool show_chat,
|
||||
bool draw_wield_tool, bool draw_crosshair)
|
||||
{
|
||||
core->draw(skycolor, show_hud, draw_wield_tool, draw_crosshair);
|
||||
core->draw(skycolor, show_hud, show_chat, draw_wield_tool, draw_crosshair);
|
||||
}
|
||||
|
||||
const VideoDriverInfo &RenderingEngine::getVideoDriverInfo(irr::video::E_DRIVER_TYPE type)
|
||||
|
|
|
@ -129,7 +129,7 @@ public:
|
|||
gui::IGUIEnvironment *guienv, ITextureSource *tsrc,
|
||||
float dtime = 0, int percent = 0, float *indef_pos = nullptr);
|
||||
|
||||
void draw_scene(video::SColor skycolor, bool show_hud,
|
||||
void draw_scene(video::SColor skycolor, bool show_hud, bool show_chat,
|
||||
bool draw_wield_tool, bool draw_crosshair);
|
||||
|
||||
void initialize(Client *client, Hud *hud);
|
||||
|
|
|
@ -41,5 +41,6 @@
|
|||
#cmakedefine01 CURSES_HAVE_NCURSESW_CURSES_H
|
||||
#cmakedefine01 BUILD_UNITTESTS
|
||||
#cmakedefine01 BUILD_BENCHMARKS
|
||||
#cmakedefine01 BUILD_UI
|
||||
#cmakedefine01 USE_SDL2
|
||||
#cmakedefine01 BUILD_WITH_TRACY
|
||||
|
|
|
@ -7,12 +7,17 @@
|
|||
/*
|
||||
All kinds of stuff that needs to be exposed from main.cpp
|
||||
*/
|
||||
#include "config.h"
|
||||
#include "modalMenu.h"
|
||||
#include <cassert>
|
||||
#include <list>
|
||||
|
||||
#include "IGUIEnvironment.h"
|
||||
|
||||
#if BUILD_UI
|
||||
#include "ui/manager.h"
|
||||
#endif
|
||||
|
||||
namespace irr::gui {
|
||||
class IGUIStaticText;
|
||||
}
|
||||
|
@ -67,10 +72,16 @@ public:
|
|||
// Returns true to prevent further processing
|
||||
virtual bool preprocessEvent(const SEvent& event)
|
||||
{
|
||||
if (m_stack.empty())
|
||||
return false;
|
||||
GUIModalMenu *mm = dynamic_cast<GUIModalMenu*>(m_stack.back());
|
||||
return mm && mm->preprocessEvent(event);
|
||||
if (!m_stack.empty()) {
|
||||
GUIModalMenu *mm = dynamic_cast<GUIModalMenu*>(m_stack.back());
|
||||
return mm && mm->preprocessEvent(event);
|
||||
#if BUILD_UI
|
||||
} else if (ui::g_manager.isFocused() && event.SdlEvent != nullptr) {
|
||||
return ui::g_manager.processInput(*event.SdlEvent);
|
||||
#endif
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t menuCount() const
|
||||
|
@ -109,7 +120,11 @@ extern MainMenuManager g_menumgr;
|
|||
|
||||
static inline bool isMenuActive()
|
||||
{
|
||||
#if BUILD_UI
|
||||
return g_menumgr.menuCount() != 0 || ui::g_manager.isFocused();
|
||||
#else
|
||||
return g_menumgr.menuCount() != 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
class MainGameCallback : public IGameCallback
|
||||
|
|
|
@ -105,7 +105,7 @@ const ToClientCommandHandler toClientCommandTable[TOCLIENT_NUM_MSG_TYPES] =
|
|||
{ "TOCLIENT_SET_MOON", TOCLIENT_STATE_CONNECTED, &Client::handleCommand_HudSetMoon }, // 0x5b
|
||||
{ "TOCLIENT_SET_STARS", TOCLIENT_STATE_CONNECTED, &Client::handleCommand_HudSetStars }, // 0x5c
|
||||
{ "TOCLIENT_MOVE_PLAYER_REL", TOCLIENT_STATE_CONNECTED, &Client::handleCommand_MovePlayerRel }, // 0x5d,
|
||||
null_command_handler,
|
||||
{ "TOCLIENT_UI_MESSAGE", TOCLIENT_STATE_CONNECTED, &Client::handleCommand_UiMessage }, // 0x5e,
|
||||
null_command_handler,
|
||||
{ "TOCLIENT_SRP_BYTES_S_B", TOCLIENT_STATE_NOT_CONNECTED, &Client::handleCommand_SrpBytesSandB }, // 0x60
|
||||
{ "TOCLIENT_FORMSPEC_PREPEND", TOCLIENT_STATE_CONNECTED, &Client::handleCommand_FormspecPrepend }, // 0x61,
|
||||
|
@ -188,7 +188,7 @@ const ServerCommandFactory serverCommandFactoryTable[TOSERVER_NUM_MSG_TYPES] =
|
|||
{ "TOSERVER_REMOVED_SOUNDS", 2, true }, // 0x3a
|
||||
{ "TOSERVER_NODEMETA_FIELDS", 0, true }, // 0x3b
|
||||
{ "TOSERVER_INVENTORY_FIELDS", 0, true }, // 0x3c
|
||||
null_command_factory, // 0x3d
|
||||
{ "TOSERVER_UI_MESSAGE", 0, true }, // 0x3d
|
||||
null_command_factory, // 0x3e
|
||||
null_command_factory, // 0x3f
|
||||
{ "TOSERVER_REQUEST_MEDIA", 1, true }, // 0x40
|
||||
|
|
|
@ -954,6 +954,17 @@ void Client::handleCommand_ShowFormSpec(NetworkPacket* pkt)
|
|||
m_client_event_queue.push(event);
|
||||
}
|
||||
|
||||
void Client::handleCommand_UiMessage(NetworkPacket* pkt)
|
||||
{
|
||||
std::string *data = new std::string(pkt->getString(0), pkt->getSize());
|
||||
|
||||
ClientEvent *event = new ClientEvent();
|
||||
event->type = CE_UI_MESSAGE;
|
||||
event->ui_message.data = data;
|
||||
|
||||
m_client_event_queue.push(event);
|
||||
}
|
||||
|
||||
void Client::handleCommand_SpawnParticle(NetworkPacket* pkt)
|
||||
{
|
||||
std::string datastring(pkt->getString(0), pkt->getSize());
|
||||
|
|
|
@ -59,8 +59,9 @@
|
|||
Rename TOSERVER_RESPAWN to TOSERVER_RESPAWN_LEGACY
|
||||
Support float animation frame numbers in TOCLIENT_LOCAL_PLAYER_ANIMATIONS
|
||||
[scheduled bump for 5.10.0]
|
||||
PROTOCOL VERSION 47
|
||||
PROTOCOL VERSION 47:
|
||||
Add particle blend mode "clip"
|
||||
Add TOCLIENT_UI_MESSAGE and TOSERVER_UI_MESSAGE
|
||||
[scheduled bump for 5.11.0]
|
||||
PROTOCOL VERSION 48
|
||||
Add compression to some existing packets
|
||||
|
|
|
@ -648,6 +648,12 @@ enum ToClientCommand : u16
|
|||
v3f added_pos
|
||||
*/
|
||||
|
||||
TOCLIENT_UI_MESSAGE = 0x5e,
|
||||
/*
|
||||
Complicated variable-length structure with many optional fields and
|
||||
length-prefixed data for future compatibility.
|
||||
*/
|
||||
|
||||
TOCLIENT_SRP_BYTES_S_B = 0x60,
|
||||
/*
|
||||
Belonging to AUTH_MECHANISM_SRP.
|
||||
|
@ -832,6 +838,11 @@ enum ToServerCommand : u16
|
|||
u8[len] field value
|
||||
*/
|
||||
|
||||
TOSERVER_UI_MESSAGE = 0x3d,
|
||||
/*
|
||||
Variable-length structure that changes depending on the message type.
|
||||
*/
|
||||
|
||||
TOSERVER_REQUEST_MEDIA = 0x40,
|
||||
/*
|
||||
u16 number of files requested
|
||||
|
|
|
@ -72,7 +72,7 @@ const ToServerCommandHandler toServerCommandTable[TOSERVER_NUM_MSG_TYPES] =
|
|||
{ "TOSERVER_REMOVED_SOUNDS", TOSERVER_STATE_INGAME, &Server::handleCommand_RemovedSounds }, // 0x3a
|
||||
{ "TOSERVER_NODEMETA_FIELDS", TOSERVER_STATE_INGAME, &Server::handleCommand_NodeMetaFields }, // 0x3b
|
||||
{ "TOSERVER_INVENTORY_FIELDS", TOSERVER_STATE_INGAME, &Server::handleCommand_InventoryFields }, // 0x3c
|
||||
null_command_handler, // 0x3d
|
||||
{ "TOSERVER_UI_MESSAGE", TOSERVER_STATE_INGAME, &Server::handleCommand_UiMessage }, // 0x3d
|
||||
null_command_handler, // 0x3e
|
||||
null_command_handler, // 0x3f
|
||||
{ "TOSERVER_REQUEST_MEDIA", TOSERVER_STATE_STARTUP, &Server::handleCommand_RequestMedia }, // 0x40
|
||||
|
@ -206,7 +206,7 @@ const ClientCommandFactory clientCommandFactoryTable[TOCLIENT_NUM_MSG_TYPES] =
|
|||
{ "TOCLIENT_SET_MOON", 0, true }, // 0x5b
|
||||
{ "TOCLIENT_SET_STARS", 0, true }, // 0x5c
|
||||
{ "TOCLIENT_MOVE_PLAYER_REL", 0, true }, // 0x5d
|
||||
null_command_factory, // 0x5e
|
||||
{ "TOCLIENT_UI_MESSAGE", 0, true }, // 0x5e
|
||||
null_command_factory, // 0x5f
|
||||
{ "TOCLIENT_SRP_BYTES_S_B", 0, true }, // 0x60
|
||||
{ "TOCLIENT_FORMSPEC_PREPEND", 0, true }, // 0x61
|
||||
|
|
|
@ -1422,6 +1422,15 @@ void Server::handleCommand_InventoryFields(NetworkPacket* pkt)
|
|||
actionstream << ", possible exploitation attempt" << std::endl;
|
||||
}
|
||||
|
||||
void Server::handleCommand_UiMessage(NetworkPacket* pkt)
|
||||
{
|
||||
session_t peer_id = pkt->getPeerId();
|
||||
RemoteClient *client = getClient(peer_id, CS_Invalid);
|
||||
|
||||
std::string data(pkt->getString(0), pkt->getSize());
|
||||
m_script->receive_ui_message(client->getName().c_str(), data);
|
||||
}
|
||||
|
||||
void Server::handleCommand_FirstSrp(NetworkPacket* pkt)
|
||||
{
|
||||
session_t peer_id = pkt->getPeerId();
|
||||
|
|
|
@ -245,3 +245,18 @@ void ScriptApiServer::on_dynamic_media_added(u32 token, const std::string &playe
|
|||
lua_pushstring(L, playername.c_str());
|
||||
PCALL_RES(lua_pcall(L, 1, 0, error_handler));
|
||||
}
|
||||
|
||||
void ScriptApiServer::receive_ui_message(const char *name, const std::string &data)
|
||||
{
|
||||
SCRIPTAPI_PRECHECKHEADER
|
||||
|
||||
int error_handler = PUSH_ERROR_HANDLER(L);
|
||||
|
||||
lua_getglobal(L, "core");
|
||||
lua_getfield(L, -1, "receive_ui_message");
|
||||
|
||||
lua_pushstring(L, name);
|
||||
lua_pushlstring(L, data.c_str(), data.size());
|
||||
|
||||
PCALL_RES(lua_pcall(L, 2, 0, error_handler));
|
||||
}
|
||||
|
|
|
@ -40,6 +40,8 @@ public:
|
|||
void freeDynamicMediaCallback(u32 token);
|
||||
void on_dynamic_media_added(u32 token, const std::string &playername);
|
||||
|
||||
void receive_ui_message(const char *name, const std::string &data);
|
||||
|
||||
private:
|
||||
void getAuthHandler();
|
||||
void readPrivileges(int index, std::set<std::string> &result);
|
||||
|
|
|
@ -418,6 +418,19 @@ int ModApiServer::l_show_formspec(lua_State *L)
|
|||
return 1;
|
||||
}
|
||||
|
||||
// send_ui_message(player, data)
|
||||
int ModApiServer::l_send_ui_message(lua_State *L)
|
||||
{
|
||||
NO_MAP_LOCK_REQUIRED;
|
||||
|
||||
size_t len;
|
||||
const char *player = luaL_checkstring(L, 1);
|
||||
const char *data = luaL_checklstring(L, 2, &len);
|
||||
|
||||
getServer(L)->sendUiMessage(player, data, len);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// get_current_modname()
|
||||
int ModApiServer::l_get_current_modname(lua_State *L)
|
||||
{
|
||||
|
@ -730,6 +743,7 @@ void ModApiServer::Initialize(lua_State *L, int top)
|
|||
API_FCT(chat_send_all);
|
||||
API_FCT(chat_send_player);
|
||||
API_FCT(show_formspec);
|
||||
API_FCT(send_ui_message);
|
||||
API_FCT(sound_play);
|
||||
API_FCT(sound_stop);
|
||||
API_FCT(sound_fade);
|
||||
|
|
|
@ -55,6 +55,9 @@ private:
|
|||
// show_formspec(playername,formname,formspec)
|
||||
static int l_show_formspec(lua_State *L);
|
||||
|
||||
// send_ui_message(player, data)
|
||||
static int l_send_ui_message(lua_State *L);
|
||||
|
||||
// sound_play(spec, parameters)
|
||||
static int l_sound_play(lua_State *L);
|
||||
|
||||
|
|
|
@ -585,10 +585,10 @@ int ModApiUtil::l_colorspec_to_colorstring(lua_State *L)
|
|||
snprintf(colorstring, 10, "#%02X%02X%02X%02X",
|
||||
color.getRed(), color.getGreen(), color.getBlue(), color.getAlpha());
|
||||
lua_pushstring(L, colorstring);
|
||||
return 1;
|
||||
} else {
|
||||
lua_pushnil(L);
|
||||
}
|
||||
|
||||
return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// colorspec_to_bytes(colorspec)
|
||||
|
@ -605,10 +605,10 @@ int ModApiUtil::l_colorspec_to_bytes(lua_State *L)
|
|||
(u8) color.getAlpha(),
|
||||
};
|
||||
lua_pushlstring(L, (const char*) colorbytes, 4);
|
||||
return 1;
|
||||
} else {
|
||||
lua_pushnil(L);
|
||||
}
|
||||
|
||||
return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// colorspec_to_table(colorspec)
|
||||
|
@ -619,10 +619,201 @@ int ModApiUtil::l_colorspec_to_table(lua_State *L)
|
|||
video::SColor color(0);
|
||||
if (read_color(L, 1, &color)) {
|
||||
push_ARGB8(L, color);
|
||||
return 1;
|
||||
} else {
|
||||
lua_pushnil(L);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// colorspec_to_int(colorspec)
|
||||
int ModApiUtil::l_colorspec_to_int(lua_State *L)
|
||||
{
|
||||
NO_MAP_LOCK_REQUIRED;
|
||||
|
||||
video::SColor color(0);
|
||||
if (read_color(L, 1, &color)) {
|
||||
lua_pushnumber(L, color.color);
|
||||
} else {
|
||||
lua_pushnil(L);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// 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 << std::string_view(str.c_str(), 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++;
|
||||
}
|
||||
|
||||
return 0;
|
||||
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;
|
||||
}
|
||||
|
||||
// time_to_day_night_ratio(time_of_day)
|
||||
|
@ -737,8 +928,12 @@ void ModApiUtil::Initialize(lua_State *L, int top)
|
|||
API_FCT(colorspec_to_colorstring);
|
||||
API_FCT(colorspec_to_bytes);
|
||||
API_FCT(colorspec_to_table);
|
||||
API_FCT(time_to_day_night_ratio);
|
||||
API_FCT(colorspec_to_int);
|
||||
|
||||
API_FCT(encode_network);
|
||||
API_FCT(decode_network);
|
||||
|
||||
API_FCT(time_to_day_night_ratio);
|
||||
API_FCT(encode_png);
|
||||
|
||||
API_FCT(get_last_run_mod);
|
||||
|
@ -774,6 +969,11 @@ void ModApiUtil::InitializeClient(lua_State *L, int top)
|
|||
API_FCT(colorspec_to_colorstring);
|
||||
API_FCT(colorspec_to_bytes);
|
||||
API_FCT(colorspec_to_table);
|
||||
API_FCT(colorspec_to_int);
|
||||
|
||||
API_FCT(encode_network);
|
||||
API_FCT(decode_network);
|
||||
|
||||
API_FCT(time_to_day_night_ratio);
|
||||
|
||||
API_FCT(get_last_run_mod);
|
||||
|
@ -820,8 +1020,12 @@ void ModApiUtil::InitializeAsync(lua_State *L, int top)
|
|||
API_FCT(colorspec_to_colorstring);
|
||||
API_FCT(colorspec_to_bytes);
|
||||
API_FCT(colorspec_to_table);
|
||||
API_FCT(time_to_day_night_ratio);
|
||||
API_FCT(colorspec_to_int);
|
||||
|
||||
API_FCT(encode_network);
|
||||
API_FCT(decode_network);
|
||||
|
||||
API_FCT(time_to_day_night_ratio);
|
||||
API_FCT(encode_png);
|
||||
|
||||
API_FCT(get_last_run_mod);
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
#pragma once
|
||||
|
||||
#include "lua_api/l_base.h"
|
||||
#include "util/serialize.h"
|
||||
|
||||
class AsyncEngine;
|
||||
|
||||
|
@ -110,6 +111,15 @@ private:
|
|||
// colorspec_to_table(colorspec)
|
||||
static int l_colorspec_to_table(lua_State *L);
|
||||
|
||||
// colorspec_to_int(colorspec)
|
||||
static int l_colorspec_to_int(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);
|
||||
|
||||
// time_to_day_night_ratio(time_of_day)
|
||||
static int l_time_to_day_night_ratio(lua_State *L);
|
||||
|
||||
|
|
|
@ -3403,6 +3403,19 @@ bool Server::showFormspec(const char *playername, const std::string &formspec,
|
|||
return true;
|
||||
}
|
||||
|
||||
void Server::sendUiMessage(const char *name, const char *data, size_t len)
|
||||
{
|
||||
RemotePlayer *player = m_env->getPlayer(name);
|
||||
if (!player) {
|
||||
return;
|
||||
}
|
||||
|
||||
NetworkPacket pkt(TOCLIENT_UI_MESSAGE, 0, player->getPeerId());
|
||||
pkt.putRawString(data, len);
|
||||
|
||||
Send(&pkt);
|
||||
}
|
||||
|
||||
u32 Server::hudAdd(RemotePlayer *player, HudElement *form)
|
||||
{
|
||||
if (!player)
|
||||
|
|
|
@ -227,6 +227,7 @@ public:
|
|||
void handleCommand_RemovedSounds(NetworkPacket* pkt);
|
||||
void handleCommand_NodeMetaFields(NetworkPacket* pkt);
|
||||
void handleCommand_InventoryFields(NetworkPacket* pkt);
|
||||
void handleCommand_UiMessage(NetworkPacket* pkt);
|
||||
void handleCommand_FirstSrp(NetworkPacket* pkt);
|
||||
void handleCommand_SrpBytesA(NetworkPacket* pkt);
|
||||
void handleCommand_SrpBytesM(NetworkPacket* pkt);
|
||||
|
@ -357,6 +358,8 @@ public:
|
|||
void addShutdownError(const ModError &e);
|
||||
|
||||
bool showFormspec(const char *name, const std::string &formspec, const std::string &formname);
|
||||
void sendUiMessage(const char *name, const char *data, size_t len);
|
||||
|
||||
Map & getMap() { return m_env->getMap(); }
|
||||
ServerEnvironment & getEnv() { return *m_env; }
|
||||
v3f findSpawnPos();
|
||||
|
|
10
src/ui/CMakeLists.txt
Normal file
10
src/ui/CMakeLists.txt
Normal file
|
@ -0,0 +1,10 @@
|
|||
set(ui_SRCS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/box.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/clickable_elems.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/elem.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/manager.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/static_elems.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/style.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/window.cpp
|
||||
PARENT_SCOPE
|
||||
)
|
692
src/ui/box.cpp
Normal file
692
src/ui/box.cpp
Normal file
|
@ -0,0 +1,692 @@
|
|||
// Luanti
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
#include "ui/box.h"
|
||||
|
||||
#include "debug.h"
|
||||
#include "log.h"
|
||||
#include "porting.h"
|
||||
#include "client/fontengine.h"
|
||||
#include "ui/elem.h"
|
||||
#include "ui/manager.h"
|
||||
#include "ui/window.h"
|
||||
#include "util/serialize.h"
|
||||
|
||||
#include <SDL2/SDL.h>
|
||||
|
||||
namespace ui
|
||||
{
|
||||
Window &Box::getWindow()
|
||||
{
|
||||
return m_elem.getWindow();
|
||||
}
|
||||
|
||||
const Window &Box::getWindow() const
|
||||
{
|
||||
return m_elem.getWindow();
|
||||
}
|
||||
|
||||
void Box::reset()
|
||||
{
|
||||
m_content.clear();
|
||||
m_label = "";
|
||||
|
||||
m_style.reset();
|
||||
|
||||
for (State i = 0; i < m_style_refs.size(); i++) {
|
||||
m_style_refs[i] = NO_STYLE;
|
||||
}
|
||||
|
||||
m_text = L"";
|
||||
m_font = nullptr;
|
||||
}
|
||||
|
||||
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 index = readU32(is);
|
||||
if (getWindow().getStyleStr(index) != nullptr) {
|
||||
m_style_refs[i] = index;
|
||||
} else {
|
||||
errorstream << "Style " << index << " does not exist" << std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Box::restyle()
|
||||
{
|
||||
// First, clear our current style and compute what state we're in.
|
||||
m_style.reset();
|
||||
State state = STATE_NONE;
|
||||
|
||||
if (m_elem.isBoxFocused(*this))
|
||||
state |= STATE_FOCUSED;
|
||||
if (m_elem.isBoxSelected(*this))
|
||||
state |= STATE_SELECTED;
|
||||
if (m_elem.isBoxHovered(*this))
|
||||
state |= STATE_HOVERED;
|
||||
if (m_elem.isBoxPressed(*this))
|
||||
state |= STATE_PRESSED;
|
||||
if (m_elem.isBoxDisabled(*this))
|
||||
state |= STATE_DISABLED;
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
// Now that we have updated text style properties, we can update our
|
||||
// cached text string and font object.
|
||||
m_text = utf8_to_wide(m_style.text.prepend) + utf8_to_wide(m_label) +
|
||||
utf8_to_wide(m_style.text.append);
|
||||
|
||||
FontSpec spec(m_style.text.size, m_style.text.mono ? FM_Mono : FM_Standard,
|
||||
m_style.text.bold, m_style.text.italic);
|
||||
m_font = g_fontengine->getFont(spec);
|
||||
|
||||
// Since our box has been restyled, the previously computed layout
|
||||
// information is no longer valid.
|
||||
m_min_layout = SizeF();
|
||||
m_min_content = SizeF();
|
||||
|
||||
m_display_rect = RectF();
|
||||
m_icon_rect = RectF();
|
||||
m_content_rect = RectF();
|
||||
|
||||
m_clip_rect = RectF();
|
||||
|
||||
// Finally, make sure to restyle our content.
|
||||
for (Box *box : m_content) {
|
||||
box->restyle();
|
||||
}
|
||||
}
|
||||
|
||||
void Box::resize()
|
||||
{
|
||||
for (Box *box : m_content) {
|
||||
box->resize();
|
||||
}
|
||||
|
||||
switch (m_style.layout.type) {
|
||||
case LayoutType::PLACE:
|
||||
resizePlace();
|
||||
break;
|
||||
}
|
||||
|
||||
resizeBox();
|
||||
}
|
||||
|
||||
void Box::relayout(RectF layout_rect, RectF layout_clip)
|
||||
{
|
||||
relayoutBox(layout_rect, layout_clip);
|
||||
|
||||
switch (m_style.layout.type) {
|
||||
case LayoutType::PLACE:
|
||||
relayoutPlace();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Box::draw()
|
||||
{
|
||||
if (m_style.display != DisplayMode::HIDDEN) {
|
||||
drawBox();
|
||||
drawItems();
|
||||
}
|
||||
|
||||
for (Box *box : m_content) {
|
||||
box->draw();
|
||||
}
|
||||
}
|
||||
|
||||
bool Box::isPointed() const
|
||||
{
|
||||
return m_clip_rect.contains(getWindow().getPointerPos());
|
||||
}
|
||||
|
||||
bool Box::isContentPointed() const {
|
||||
// If we're pointed, then we clearly have a pointed box.
|
||||
if (isPointed()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Search through our content. If any of them are contained within the
|
||||
// same element as this box, they are candidates for being pointed.
|
||||
for (Box *box : m_content) {
|
||||
if (&box->getElem() == &m_elem && box->isContentPointed()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Box::processInput(const SDL_Event &event)
|
||||
{
|
||||
switch (event.type) {
|
||||
case UI_USER(FOCUS_REQUEST):
|
||||
// The box is dynamic, so it can be focused.
|
||||
return true;
|
||||
|
||||
case UI_USER(FOCUS_CHANGED):
|
||||
// If the box is no longer focused, it can't be pressed.
|
||||
if (event.user.data1 == &m_elem) {
|
||||
setPressed(false);
|
||||
}
|
||||
return false;
|
||||
|
||||
case UI_USER(FOCUS_SUBVERTED):
|
||||
// If some non-focused element used an event instead of this one,
|
||||
// unpress the box because user interaction has been diverted.
|
||||
setPressed(false);
|
||||
return false;
|
||||
|
||||
case UI_USER(HOVER_REQUEST):
|
||||
// The box can be hovered if the pointer is inside it.
|
||||
return isPointed();
|
||||
|
||||
case UI_USER(HOVER_CHANGED):
|
||||
// Make this box hovered if the element became hovered and the
|
||||
// pointer is inside this box.
|
||||
setHovered(event.user.data2 == &m_elem && isPointed());
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool Box::processFullPress(const SDL_Event &event, void (*on_press)(Elem &))
|
||||
{
|
||||
switch (event.type) {
|
||||
case SDL_KEYDOWN:
|
||||
// If the space key is pressed not due to a key repeat, then the
|
||||
// box becomes pressed. If the escape key is pressed while the box
|
||||
// is pressed, that unpresses the box without triggering it.
|
||||
if (event.key.keysym.sym == SDLK_SPACE && !event.key.repeat) {
|
||||
setPressed(true);
|
||||
return true;
|
||||
} else if (event.key.keysym.sym == SDLK_ESCAPE && isPressed()) {
|
||||
setPressed(false);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
case SDL_KEYUP:
|
||||
// Releasing the space key while the box is pressed causes it to be
|
||||
// unpressed and triggered.
|
||||
if (event.key.keysym.sym == SDLK_SPACE && isPressed()) {
|
||||
setPressed(false);
|
||||
on_press(m_elem);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
case SDL_MOUSEBUTTONDOWN:
|
||||
// If the box is hovered, then pressing the left mouse button
|
||||
// causes it to be pressed. Otherwise, the mouse is directed at
|
||||
// some other box.
|
||||
if (isHovered() && event.button.button == SDL_BUTTON_LEFT) {
|
||||
setPressed(true);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
case SDL_MOUSEBUTTONUP:
|
||||
// If the mouse button was released, the box becomes unpressed. If
|
||||
// it was released while inside the bounds of the box, that counts
|
||||
// as the box being triggered.
|
||||
if (event.button.button == SDL_BUTTON_LEFT) {
|
||||
bool was_pressed = isPressed();
|
||||
setPressed(false);
|
||||
|
||||
if (isHovered() && was_pressed) {
|
||||
on_press(m_elem);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
||||
default:
|
||||
return processInput(event);
|
||||
}
|
||||
}
|
||||
|
||||
RectF Box::getLayerSource(const Layer &layer)
|
||||
{
|
||||
RectF src = layer.source;
|
||||
|
||||
// If we have animations, we need to adjust the source rect by the
|
||||
// frame offset in accordance with the current frame.
|
||||
if (layer.num_frames > 1) {
|
||||
float frame_height = src.H() / layer.num_frames;
|
||||
src.B = src.T + frame_height;
|
||||
|
||||
float frame_offset = frame_height *
|
||||
((porting::getTimeMs() / layer.frame_time) % layer.num_frames);
|
||||
src.T += frame_offset;
|
||||
src.B += frame_offset;
|
||||
}
|
||||
|
||||
return src;
|
||||
}
|
||||
|
||||
SizeF Box::getLayerSize(const Layer &layer)
|
||||
{
|
||||
return getLayerSource(layer).size() * getTextureSize(layer.image);
|
||||
}
|
||||
|
||||
DispF Box::getMiddleEdges()
|
||||
{
|
||||
// Scale the middle rect by the scaling factor and de-normalize it into
|
||||
// actual pixels based on the image source rect.
|
||||
return m_style.box_middle * DispF(getLayerSize(m_style.box)) * m_style.box.scale;
|
||||
}
|
||||
|
||||
void Box::resizeBox()
|
||||
{
|
||||
// First, we need to expand the minimum size of the box to accommodate
|
||||
// the size of any text it might contain.
|
||||
SizeF text_size = getWindow().getTextSize(m_font, m_text);
|
||||
m_min_content = m_min_content.max(text_size);
|
||||
|
||||
// If the box is set to clip its contents in either dimension, we can
|
||||
// set the minimum content size to zero for that coordinate.
|
||||
if (m_style.layout.clip == DirFlags::X || m_style.layout.clip == DirFlags::BOTH) {
|
||||
m_min_content.W = 0.0f;
|
||||
}
|
||||
if (m_style.layout.clip == DirFlags::Y || m_style.layout.clip == DirFlags::BOTH) {
|
||||
m_min_content.H = 0.0f;
|
||||
}
|
||||
|
||||
// We need to factor the icon into the minimum size of the box. The
|
||||
// minimum size of the padding rect is either the size of the contents
|
||||
// or the scaled icon, depending on which is larger. If the scale is
|
||||
// zero, then the icon doesn't add anything to the minimum size.
|
||||
SizeF icon_size = getLayerSize(m_style.icon) * m_style.icon.scale;
|
||||
SizeF padding_size = m_min_content.max(icon_size);
|
||||
|
||||
// If the icon should not overlap the content, then we must take into
|
||||
// account the extra space required for this, including the gutter.
|
||||
if (!m_style.icon_overlap && m_style.icon.image != nullptr) {
|
||||
switch (m_style.icon_place) {
|
||||
case IconPlace::CENTER:
|
||||
break;
|
||||
case IconPlace::LEFT:
|
||||
case IconPlace::RIGHT:
|
||||
padding_size.W = m_min_content.W + icon_size.W + m_style.icon_gutter;
|
||||
break;
|
||||
case IconPlace::TOP:
|
||||
case IconPlace::BOTTOM:
|
||||
padding_size.H = m_min_content.H + icon_size.H + m_style.icon_gutter;
|
||||
break;
|
||||
}
|
||||
|
||||
padding_size = padding_size.clip();
|
||||
}
|
||||
|
||||
// Now that we have a minimum size for the padding rect, we can
|
||||
// calculate the display rect size by adjusting for the padding and
|
||||
// middle rect edges. We also clamp the size of the display rect to be
|
||||
// at least as large as the user-specified minimum size.
|
||||
SizeF display_size = (padding_size + getMiddleEdges().extents() +
|
||||
m_style.sizing.padding.extents()).max(m_style.sizing.size);
|
||||
|
||||
// The final minimum size is the display size adjusted for the margin.
|
||||
m_min_layout = (display_size + m_style.sizing.margin.extents()).clip();
|
||||
}
|
||||
|
||||
void Box::relayoutBox(RectF layout_rect, RectF layout_clip)
|
||||
{
|
||||
// The display rect is created by insetting the layout rect by the
|
||||
// margin. The padding rect is inset from that by the middle rect edges
|
||||
// and the padding. We must make sure these do not have negative sizes.
|
||||
m_display_rect = layout_rect.insetBy(m_style.sizing.margin).clip();
|
||||
RectF padding_rect = m_display_rect.insetBy(
|
||||
getMiddleEdges() + m_style.sizing.padding).clip();
|
||||
|
||||
// The icon is aligned and scaled in a particular area of the box.
|
||||
// First, get the basic size of the icon rect.
|
||||
SizeF icon_size = getLayerSize(m_style.icon);
|
||||
|
||||
// Then, modify it based on 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.
|
||||
if (m_style.icon.scale == 0.0f) {
|
||||
SizeF max_icon = padding_rect.size();
|
||||
|
||||
// If the icon should not overlap the content, then we need to
|
||||
// adjust the area in which we compute the maximum scale by
|
||||
// subtracting the content and gutter from the padding rect size.
|
||||
if (!m_style.icon_overlap && m_style.icon.image != nullptr) {
|
||||
switch (m_style.icon_place) {
|
||||
case IconPlace::CENTER:
|
||||
break;
|
||||
case IconPlace::LEFT:
|
||||
case IconPlace::RIGHT:
|
||||
max_icon.W -= m_min_content.W + m_style.icon_gutter;
|
||||
break;
|
||||
case IconPlace::TOP:
|
||||
case IconPlace::BOTTOM:
|
||||
max_icon.H -= m_min_content.H + m_style.icon_gutter;
|
||||
break;
|
||||
}
|
||||
|
||||
max_icon = max_icon.clip();
|
||||
}
|
||||
|
||||
// Choose the scale factor based on the space we have for the icon,
|
||||
// choosing the smaller of the two possible image size ratios.
|
||||
icon_size *= std::min(max_icon.W / icon_size.W, max_icon.H / icon_size.H);
|
||||
} else {
|
||||
icon_size *= m_style.icon.scale;
|
||||
}
|
||||
|
||||
// Now that we have the size of the icon, we can compute the icon rect
|
||||
// based on the desired placement of the icon.
|
||||
PosF icon_start = padding_rect.TopLeft;
|
||||
PosF icon_center = icon_start + (padding_rect.size() - icon_size) / 2.0f;
|
||||
PosF icon_end = icon_start + (padding_rect.size() - icon_size);
|
||||
|
||||
switch (m_style.icon_place) {
|
||||
case IconPlace::CENTER:
|
||||
m_icon_rect = RectF(icon_center, icon_size);
|
||||
break;
|
||||
case IconPlace::LEFT:
|
||||
m_icon_rect = RectF(PosF(icon_start.X, icon_center.Y), icon_size);
|
||||
break;
|
||||
case IconPlace::TOP:
|
||||
m_icon_rect = RectF(PosF(icon_center.X, icon_start.Y), icon_size);
|
||||
break;
|
||||
case IconPlace::RIGHT:
|
||||
m_icon_rect = RectF(PosF(icon_end.X, icon_center.Y), icon_size);
|
||||
break;
|
||||
case IconPlace::BOTTOM:
|
||||
m_icon_rect = RectF(PosF(icon_center.X, icon_end.Y), icon_size);
|
||||
break;
|
||||
}
|
||||
|
||||
// If the overlap property is set or the icon is centered, the content
|
||||
// rect is identical to the padding rect. Otherwise, the content rect
|
||||
// needs to be adjusted to account for the icon and gutter.
|
||||
m_content_rect = padding_rect;
|
||||
|
||||
if (!m_style.icon_overlap && m_style.icon.image != nullptr) {
|
||||
switch (m_style.icon_place) {
|
||||
case IconPlace::CENTER:
|
||||
break;
|
||||
case IconPlace::LEFT:
|
||||
m_content_rect.L += icon_size.W + m_style.icon_gutter;
|
||||
break;
|
||||
case IconPlace::TOP:
|
||||
m_content_rect.T += icon_size.H + m_style.icon_gutter;
|
||||
break;
|
||||
case IconPlace::RIGHT:
|
||||
m_content_rect.R -= icon_size.W + m_style.icon_gutter;
|
||||
break;
|
||||
case IconPlace::BOTTOM:
|
||||
m_content_rect.B -= icon_size.H + m_style.icon_gutter;
|
||||
break;
|
||||
}
|
||||
|
||||
m_content_rect = m_content_rect.clip();
|
||||
}
|
||||
|
||||
// We set our clipping rect based on the display mode.
|
||||
switch (m_style.display) {
|
||||
case DisplayMode::VISIBLE:
|
||||
case DisplayMode::HIDDEN:
|
||||
// If the box is visible or hidden, then we clip the box and its
|
||||
// content as normal against the drawing and layout clip rects.
|
||||
m_clip_rect = m_display_rect.intersectWith(layout_clip);
|
||||
break;
|
||||
case DisplayMode::OVERFLOW:
|
||||
// If the box allows overflow, then clip to the drawing rect, since
|
||||
// we never want to expand outside our own visible boundaries, but
|
||||
// we don't clip to the layout clip rect.
|
||||
m_clip_rect = m_display_rect;
|
||||
break;
|
||||
case DisplayMode::CLIPPED:
|
||||
// If the box and its content should be entirely removed, then we
|
||||
// clip everything entirely.
|
||||
m_clip_rect = RectF();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Box::resizePlace()
|
||||
{
|
||||
for (Box *box : m_content) {
|
||||
// Calculate the size of the box according to the span and scale
|
||||
// factor. If the scale is zero, we don't know how big the span
|
||||
// will end up being, so the span size goes to zero.
|
||||
SizeF span_size = box->m_style.sizing.span * m_style.layout.scale;
|
||||
|
||||
// Ensure that the computed minimum size for our content is at
|
||||
// least as large as the minimum size of the box and its span size.
|
||||
m_min_content = m_min_content.max(box->m_min_layout).max(span_size);
|
||||
}
|
||||
}
|
||||
|
||||
void Box::relayoutPlace()
|
||||
{
|
||||
for (Box *box : m_content) {
|
||||
const Sizing &sizing = box->m_style.sizing;
|
||||
|
||||
// Compute the scale factor. If the scale is zero, then we use the
|
||||
// size of the parent box to achieve normalized coordinates.
|
||||
SizeF scale = m_style.layout.scale == 0.0f ?
|
||||
m_content_rect.size() : SizeF(m_style.layout.scale);
|
||||
|
||||
// Calculate the position and size of the box relative to the
|
||||
// origin, taking into account the scale factor and anchor. Also
|
||||
// make sure the size doesn't go below the minimum size.
|
||||
SizeF size = (sizing.span * scale).max(box->m_min_layout);
|
||||
SizeF pos = (sizing.pos * scale) - (sizing.anchor * size);
|
||||
|
||||
// The layout rect of the box is made by shifting the above rect by
|
||||
// the top left of the content rect.
|
||||
RectF layout_rect = RectF(m_content_rect.TopLeft + pos, size);
|
||||
box->relayout(layout_rect, m_clip_rect);
|
||||
}
|
||||
}
|
||||
|
||||
void Box::drawBox()
|
||||
{
|
||||
// First, fill the display rectangle with the fill color.
|
||||
getWindow().drawRect(m_display_rect, m_clip_rect, m_style.box.fill);
|
||||
|
||||
// If there's no image, then we don't need to do a bunch of
|
||||
// calculations in order to draw nothing.
|
||||
if (m_style.box.image == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
// For the image, first get the source rect adjusted for animations.
|
||||
RectF src = getLayerSource(m_style.box);
|
||||
|
||||
// We need to make sure the the middle rect is relative to the source
|
||||
// rect rather than the entire image, so scale the edges appropriately.
|
||||
DispF middle_src = m_style.box_middle * DispF(src.size());
|
||||
DispF middle_dst = getMiddleEdges();
|
||||
|
||||
// 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.
|
||||
if (src.W() < 0.0f) {
|
||||
middle_src.L = -middle_src.L;
|
||||
middle_src.R = -middle_src.R;
|
||||
}
|
||||
if (src.H() < 0.0f) {
|
||||
middle_src.T = -middle_src.T;
|
||||
middle_src.B = -middle_src.B;
|
||||
}
|
||||
|
||||
for (int slice_y = 0; slice_y < 3; slice_y++) {
|
||||
for (int slice_x = 0; slice_x < 3; slice_x++) {
|
||||
// Compute each slice of the nine-slice image. If the middle
|
||||
// rect equals the whole source rect, the middle slice will
|
||||
// occupy the entire display rectangle.
|
||||
RectF slice_src = src;
|
||||
RectF slice_dst = m_display_rect;
|
||||
|
||||
switch (slice_x) {
|
||||
case 0:
|
||||
slice_dst.R = slice_dst.L + middle_dst.L;
|
||||
slice_src.R = slice_src.L + middle_src.L;
|
||||
break;
|
||||
|
||||
case 1:
|
||||
slice_dst.L += middle_dst.L;
|
||||
slice_dst.R -= middle_dst.R;
|
||||
slice_src.L += middle_src.L;
|
||||
slice_src.R -= middle_src.R;
|
||||
break;
|
||||
|
||||
case 2:
|
||||
slice_dst.L = slice_dst.R - middle_dst.R;
|
||||
slice_src.L = slice_src.R - middle_src.R;
|
||||
break;
|
||||
}
|
||||
|
||||
switch (slice_y) {
|
||||
case 0:
|
||||
slice_dst.B = slice_dst.T + middle_dst.T;
|
||||
slice_src.B = slice_src.T + middle_src.T;
|
||||
break;
|
||||
|
||||
case 1:
|
||||
slice_dst.T += middle_dst.T;
|
||||
slice_dst.B -= middle_dst.B;
|
||||
slice_src.T += middle_src.T;
|
||||
slice_src.B -= middle_src.B;
|
||||
break;
|
||||
|
||||
case 2:
|
||||
slice_dst.T = slice_dst.B - middle_dst.B;
|
||||
slice_src.T = slice_src.B - middle_src.B;
|
||||
break;
|
||||
}
|
||||
|
||||
// If we have a tiled image, then some of the tiles may bleed
|
||||
// out of the slice rect, so we need to clip to both the
|
||||
// clipping rect and the destination rect.
|
||||
RectF slice_clip = m_clip_rect.intersectWith(slice_dst);
|
||||
|
||||
// If this slice is empty or has been entirely clipped, then
|
||||
// don't bother drawing anything.
|
||||
if (slice_clip.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// This may be a tiled image, so we need to calculate the size
|
||||
// of each tile. If the image is not tiled, this should equal
|
||||
// the size of the destination rect.
|
||||
SizeF tile_size = slice_dst.size();
|
||||
|
||||
if (m_style.box_tile != DirFlags::NONE) {
|
||||
// We need to calculate the tile size based on the texture
|
||||
// size and the scale of each tile. If the scale is too
|
||||
// small, then the number of tiles will explode, so we
|
||||
// clamp it to a reasonable minimum of 1/8 of a pixel.
|
||||
SizeF tex_size = getTextureSize(m_style.box.image);
|
||||
float tile_scale = std::max(m_style.box.scale, 0.125f);
|
||||
|
||||
if (m_style.box_tile != DirFlags::Y) {
|
||||
tile_size.W = slice_src.W() * tex_size.W * tile_scale;
|
||||
}
|
||||
if (m_style.box_tile != DirFlags::X) {
|
||||
tile_size.H = slice_src.H() * tex_size.H * tile_scale;
|
||||
}
|
||||
}
|
||||
|
||||
// Now we can draw each tile for this slice. If the image is
|
||||
// not tiled, then each of these loops will run only once.
|
||||
float tile_y = slice_dst.T;
|
||||
|
||||
while (tile_y < slice_dst.B) {
|
||||
float tile_x = slice_dst.L;
|
||||
|
||||
while (tile_x < slice_dst.R) {
|
||||
// Draw the texture in the appropriate destination rect
|
||||
// for this tile, and clip it to the clipping rect for
|
||||
// this slice.
|
||||
RectF tile_dst = RectF(PosF(tile_x, tile_y), tile_size);
|
||||
|
||||
getWindow().drawTexture(tile_dst, slice_clip,
|
||||
m_style.box.image, slice_src, m_style.box.tint);
|
||||
|
||||
tile_x += tile_size.W;
|
||||
}
|
||||
tile_y += tile_size.H;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Box::drawItems()
|
||||
{
|
||||
// The icon rect is computed while the box is being laid out, so we
|
||||
// just need to draw it with the fill color behind it.
|
||||
getWindow().drawRect(m_icon_rect, m_clip_rect, m_style.icon.fill);
|
||||
getWindow().drawTexture(m_icon_rect, m_clip_rect, m_style.icon.image,
|
||||
getLayerSource(m_style.icon), m_style.icon.tint);
|
||||
|
||||
// The window handles all the complicated text layout, so we can just
|
||||
// draw the text with all the appropriate styling.
|
||||
getWindow().drawText(m_content_rect, m_clip_rect, m_font, m_text,
|
||||
m_style.text.color, m_style.text.mark,
|
||||
m_style.text.align, m_style.text.valign);
|
||||
}
|
||||
|
||||
bool Box::isHovered() const
|
||||
{
|
||||
return m_elem.getHoveredBox() == getId();
|
||||
}
|
||||
|
||||
bool Box::isPressed() const
|
||||
{
|
||||
return m_elem.getPressedBox() == getId();
|
||||
}
|
||||
|
||||
void Box::setHovered(bool hovered)
|
||||
{
|
||||
if (hovered) {
|
||||
m_elem.setHoveredBox(getId());
|
||||
} else if (isHovered()) {
|
||||
m_elem.setHoveredBox(NO_ID);
|
||||
}
|
||||
}
|
||||
|
||||
void Box::setPressed(bool pressed)
|
||||
{
|
||||
if (pressed) {
|
||||
m_elem.setPressedBox(getId());
|
||||
} else if (isPressed()) {
|
||||
m_elem.setPressedBox(NO_ID);
|
||||
}
|
||||
}
|
||||
}
|
144
src/ui/box.h
Normal file
144
src/ui/box.h
Normal file
|
@ -0,0 +1,144 @@
|
|||
// Luanti
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ui/helpers.h"
|
||||
#include "ui/style.h"
|
||||
#include "util/basic_macros.h"
|
||||
|
||||
#include <array>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
union SDL_Event;
|
||||
|
||||
namespace ui
|
||||
{
|
||||
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;
|
||||
|
||||
// For groups that are standalone or not part of any particular group,
|
||||
// this box group can be used.
|
||||
static constexpr u32 NO_GROUP = -1;
|
||||
|
||||
// Represents a nonexistent box, i.e. a box with a group of NO_GROUP
|
||||
// and an item of -1, which no box should use.
|
||||
static constexpr u64 NO_ID = -1;
|
||||
|
||||
private:
|
||||
// Indicates that there is no style string for this state combination.
|
||||
static constexpr u32 NO_STYLE = -1;
|
||||
|
||||
// The element, group, and item are intrinsic to the box's identity, so
|
||||
// they are set by the constructor and aren't cleared in reset() or
|
||||
// changed in read().
|
||||
Elem &m_elem;
|
||||
|
||||
u32 m_group;
|
||||
u32 m_item;
|
||||
|
||||
std::vector<Box *> m_content;
|
||||
std::string_view m_label;
|
||||
|
||||
Style m_style;
|
||||
std::array<u32, NUM_STATES> m_style_refs;
|
||||
|
||||
// We cache the font and text content every time the box is restyled.
|
||||
std::wstring m_text;
|
||||
gui::IGUIFont *m_font;
|
||||
|
||||
// Cached information about the layout of the box, which is cleared in
|
||||
// restyle() and recomputed in resize() and relayout().
|
||||
SizeF m_min_layout;
|
||||
SizeF m_min_content;
|
||||
|
||||
RectF m_display_rect;
|
||||
RectF m_icon_rect;
|
||||
RectF m_content_rect;
|
||||
|
||||
RectF m_clip_rect;
|
||||
|
||||
public:
|
||||
Box(Elem &elem, u32 group, u32 item) :
|
||||
m_elem(elem),
|
||||
m_group(group),
|
||||
m_item(item)
|
||||
{
|
||||
reset();
|
||||
}
|
||||
|
||||
DISABLE_CLASS_COPY(Box)
|
||||
|
||||
Elem &getElem() { return m_elem; }
|
||||
const Elem &getElem() const { return m_elem; }
|
||||
|
||||
Window &getWindow();
|
||||
const Window &getWindow() const;
|
||||
|
||||
u32 getGroup() const { return m_group; }
|
||||
u32 getItem() const { return m_item; }
|
||||
u64 getId() const { return ((u64)m_group << 32) | (u64)m_item; }
|
||||
|
||||
const std::vector<Box *> &getContent() const { return m_content; }
|
||||
void setContent(std::vector<Box *> content) { m_content = std::move(content); }
|
||||
|
||||
std::string_view getLabel() const { return m_label; }
|
||||
void setLabel(std::string_view label) { m_label = label; }
|
||||
|
||||
void reset();
|
||||
void read(std::istream &is);
|
||||
|
||||
void restyle();
|
||||
void resize();
|
||||
void relayout(RectF layout_rect, RectF layout_clip);
|
||||
|
||||
void draw();
|
||||
|
||||
bool isPointed() const;
|
||||
bool isContentPointed() const;
|
||||
|
||||
bool processInput(const SDL_Event &event);
|
||||
bool processFullPress(const SDL_Event &event, void (*on_press)(Elem &));
|
||||
|
||||
private:
|
||||
static RectF getLayerSource(const Layer &layer);
|
||||
static SizeF getLayerSize(const Layer &layer);
|
||||
|
||||
DispF getMiddleEdges();
|
||||
|
||||
void resizeBox();
|
||||
void relayoutBox(RectF layout_rect, RectF layout_clip);
|
||||
|
||||
void resizePlace();
|
||||
void relayoutPlace();
|
||||
|
||||
void drawBox();
|
||||
void drawItems();
|
||||
|
||||
bool isHovered() const;
|
||||
bool isPressed() const;
|
||||
|
||||
void setPressed(bool pressed);
|
||||
void setHovered(bool hovered);
|
||||
};
|
||||
}
|
173
src/ui/clickable_elems.cpp
Normal file
173
src/ui/clickable_elems.cpp
Normal file
|
@ -0,0 +1,173 @@
|
|||
// Luanti
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// Copyright (C) 2024 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
#include "ui/clickable_elems.h"
|
||||
|
||||
#include "debug.h"
|
||||
#include "log.h"
|
||||
#include "ui/manager.h"
|
||||
#include "util/serialize.h"
|
||||
|
||||
namespace ui
|
||||
{
|
||||
void Button::reset()
|
||||
{
|
||||
Elem::reset();
|
||||
|
||||
m_disabled = false;
|
||||
}
|
||||
|
||||
void Button::read(std::istream &is)
|
||||
{
|
||||
auto super = newIs(readStr32(is));
|
||||
Elem::read(super);
|
||||
|
||||
u32 set_mask = readU32(is);
|
||||
|
||||
m_disabled = testShift(set_mask);
|
||||
|
||||
if (testShift(set_mask))
|
||||
enableEvent(ON_PRESS);
|
||||
}
|
||||
|
||||
bool Button::processInput(const SDL_Event &event)
|
||||
{
|
||||
return getMain().processFullPress(event, UI_CALLBACK(onPress));
|
||||
}
|
||||
|
||||
void Button::onPress()
|
||||
{
|
||||
if (!m_disabled && testEvent(ON_PRESS)) {
|
||||
g_manager.sendMessage(createEvent(ON_PRESS).str());
|
||||
}
|
||||
}
|
||||
|
||||
void Toggle::reset()
|
||||
{
|
||||
Elem::reset();
|
||||
|
||||
m_disabled = false;
|
||||
}
|
||||
|
||||
void Toggle::read(std::istream &is)
|
||||
{
|
||||
auto super = newIs(readStr32(is));
|
||||
Elem::read(super);
|
||||
|
||||
u32 set_mask = readU32(is);
|
||||
|
||||
m_disabled = testShift(set_mask);
|
||||
testShiftBool(set_mask, m_selected);
|
||||
|
||||
if (testShift(set_mask))
|
||||
enableEvent(ON_PRESS);
|
||||
if (testShift(set_mask))
|
||||
enableEvent(ON_CHANGE);
|
||||
}
|
||||
|
||||
bool Toggle::processInput(const SDL_Event &event)
|
||||
{
|
||||
return getMain().processFullPress(event, UI_CALLBACK(onPress));
|
||||
}
|
||||
|
||||
void Toggle::onPress()
|
||||
{
|
||||
if (m_disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_selected = !m_selected;
|
||||
|
||||
// Send both a press and a change event since both occurred.
|
||||
if (testEvent(ON_PRESS)) {
|
||||
g_manager.sendMessage(createEvent(ON_PRESS).str());
|
||||
}
|
||||
if (testEvent(ON_CHANGE)) {
|
||||
auto os = createEvent(ON_CHANGE);
|
||||
writeU8(os, m_selected);
|
||||
|
||||
g_manager.sendMessage(os.str());
|
||||
}
|
||||
}
|
||||
|
||||
void Option::reset()
|
||||
{
|
||||
Elem::reset();
|
||||
|
||||
m_disabled = false;
|
||||
m_family.clear();
|
||||
}
|
||||
|
||||
void Option::read(std::istream &is)
|
||||
{
|
||||
auto super = newIs(readStr32(is));
|
||||
Elem::read(super);
|
||||
|
||||
u32 set_mask = readU32(is);
|
||||
|
||||
m_disabled = testShift(set_mask);
|
||||
testShiftBool(set_mask, m_selected);
|
||||
|
||||
if (testShift(set_mask))
|
||||
m_family = readNullStr(is);
|
||||
|
||||
if (testShift(set_mask))
|
||||
enableEvent(ON_PRESS);
|
||||
if (testShift(set_mask))
|
||||
enableEvent(ON_CHANGE);
|
||||
}
|
||||
|
||||
bool Option::processInput(const SDL_Event &event)
|
||||
{
|
||||
return getMain().processFullPress(event, UI_CALLBACK(onPress));
|
||||
}
|
||||
|
||||
void Option::onPress()
|
||||
{
|
||||
if (m_disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Send a press event for this pressed option button.
|
||||
if (testEvent(ON_PRESS)) {
|
||||
g_manager.sendMessage(createEvent(ON_PRESS).str());
|
||||
}
|
||||
|
||||
// Select this option button unconditionally before deselecting the
|
||||
// others in the family.
|
||||
onChange(true);
|
||||
|
||||
// If this option button has no family, then don't do anything else
|
||||
// since there may be other buttons with the same empty family string.
|
||||
if (m_family.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If we find any other option buttons in this family, deselect them.
|
||||
for (Elem *elem : getWindow().getElems()) {
|
||||
if (elem->getType() != getType()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Option *option = (Option *)elem;
|
||||
if (option->m_family == m_family && option != this) {
|
||||
option->onChange(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Option::onChange(bool selected)
|
||||
{
|
||||
bool was_selected = m_selected;
|
||||
m_selected = selected;
|
||||
|
||||
// If the state of the option button changed, send a change event.
|
||||
if (was_selected != m_selected && testEvent(ON_CHANGE)) {
|
||||
auto os = createEvent(ON_CHANGE);
|
||||
writeU8(os, m_selected);
|
||||
|
||||
g_manager.sendMessage(os.str());
|
||||
}
|
||||
}
|
||||
}
|
102
src/ui/clickable_elems.h
Normal file
102
src/ui/clickable_elems.h
Normal file
|
@ -0,0 +1,102 @@
|
|||
// Luanti
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// Copyright (C) 2024 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ui/box.h"
|
||||
#include "ui/elem.h"
|
||||
#include "ui/helpers.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
namespace ui
|
||||
{
|
||||
class Button : public Elem
|
||||
{
|
||||
private:
|
||||
// Serialized constants; do not change values of entries.
|
||||
static constexpr u32 ON_PRESS = 0x00;
|
||||
|
||||
bool m_disabled;
|
||||
|
||||
public:
|
||||
Button(Window &window, std::string id) :
|
||||
Elem(window, std::move(id))
|
||||
{}
|
||||
|
||||
virtual Type getType() const override { return BUTTON; }
|
||||
|
||||
virtual void reset() override;
|
||||
virtual void read(std::istream &is) override;
|
||||
|
||||
virtual bool isBoxDisabled(const Box &box) const override { return m_disabled; }
|
||||
|
||||
virtual bool processInput(const SDL_Event &event) override;
|
||||
|
||||
private:
|
||||
void onPress();
|
||||
};
|
||||
|
||||
class Toggle : public Elem
|
||||
{
|
||||
private:
|
||||
// Serialized constants; do not change values of entries.
|
||||
static constexpr u32 ON_PRESS = 0x00;
|
||||
static constexpr u32 ON_CHANGE = 0x01;
|
||||
|
||||
bool m_disabled;
|
||||
bool m_selected = false; // Persistent
|
||||
|
||||
public:
|
||||
Toggle(Window &window, std::string id) :
|
||||
Elem(window, std::move(id))
|
||||
{}
|
||||
|
||||
virtual Type getType() const override { return TOGGLE; }
|
||||
|
||||
virtual void reset() override;
|
||||
virtual void read(std::istream &is) override;
|
||||
|
||||
virtual bool isBoxSelected(const Box &box) const override { return m_selected; }
|
||||
virtual bool isBoxDisabled(const Box &box) const override { return m_disabled; }
|
||||
|
||||
virtual bool processInput(const SDL_Event &event) override;
|
||||
|
||||
private:
|
||||
void onPress();
|
||||
};
|
||||
|
||||
class Option : public Elem
|
||||
{
|
||||
private:
|
||||
// Serialized constants; do not change values of entries.
|
||||
static constexpr u32 ON_PRESS = 0x00;
|
||||
static constexpr u32 ON_CHANGE = 0x01;
|
||||
|
||||
bool m_disabled;
|
||||
std::string m_family;
|
||||
|
||||
bool m_selected = false; // Persistent
|
||||
|
||||
public:
|
||||
Option(Window &window, std::string id) :
|
||||
Elem(window, std::move(id))
|
||||
{}
|
||||
|
||||
virtual Type getType() const override { return OPTION; }
|
||||
|
||||
virtual void reset() override;
|
||||
virtual void read(std::istream &is) override;
|
||||
|
||||
virtual bool isBoxSelected(const Box &box) const override { return m_selected; }
|
||||
virtual bool isBoxDisabled(const Box &box) const override { return m_disabled; }
|
||||
|
||||
virtual bool processInput(const SDL_Event &event) override;
|
||||
|
||||
private:
|
||||
void onPress();
|
||||
void onChange(bool selected);
|
||||
};
|
||||
}
|
152
src/ui/elem.cpp
Normal file
152
src/ui/elem.cpp
Normal file
|
@ -0,0 +1,152 @@
|
|||
// Luanti
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
#include "ui/elem.h"
|
||||
|
||||
#include "debug.h"
|
||||
#include "log.h"
|
||||
#include "ui/manager.h"
|
||||
#include "ui/window.h"
|
||||
#include "util/serialize.h"
|
||||
|
||||
// Include every element header for Elem::create()
|
||||
#include "ui/clickable_elems.h"
|
||||
#include "ui/static_elems.h"
|
||||
|
||||
#include <SDL2/SDL.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);
|
||||
CREATE(BUTTON, Button);
|
||||
CREATE(TOGGLE, Toggle);
|
||||
CREATE(OPTION, Option);
|
||||
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, Box::NO_GROUP, MAIN_BOX)
|
||||
{}
|
||||
|
||||
Elem::~Elem()
|
||||
{
|
||||
// Make sure we don't leave any dangling pointers in the window.
|
||||
m_window.clearElem(this);
|
||||
}
|
||||
|
||||
void Elem::reset()
|
||||
{
|
||||
m_order = (size_t)-1;
|
||||
|
||||
m_parent = nullptr;
|
||||
m_children.clear();
|
||||
|
||||
m_label = "";
|
||||
m_main_box.reset();
|
||||
|
||||
m_events = 0;
|
||||
}
|
||||
|
||||
void Elem::read(std::istream &is)
|
||||
{
|
||||
u32 set_mask = readU32(is);
|
||||
|
||||
if (testShift(set_mask))
|
||||
readChildren(is);
|
||||
if (testShift(set_mask))
|
||||
m_label = readStr16(is);
|
||||
if (testShift(set_mask))
|
||||
m_main_box.read(is);
|
||||
|
||||
std::vector<Box *> content;
|
||||
for (Elem *elem : m_children) {
|
||||
content.push_back(&elem->getMain());
|
||||
}
|
||||
|
||||
m_main_box.setContent(std::move(content));
|
||||
m_main_box.setLabel(m_label);
|
||||
}
|
||||
|
||||
bool Elem::isFocused() const
|
||||
{
|
||||
return m_window.isFocused() && m_window.getFocused() == this;
|
||||
}
|
||||
|
||||
void Elem::enableEvent(u32 event)
|
||||
{
|
||||
m_events |= (1 << event);
|
||||
}
|
||||
|
||||
bool Elem::testEvent(u32 event) const
|
||||
{
|
||||
return m_events & (1 << event);
|
||||
}
|
||||
|
||||
std::ostringstream Elem::createEvent(u32 event) const
|
||||
{
|
||||
auto os = newOs();
|
||||
|
||||
writeU8(os, Manager::ELEM_EVENT);
|
||||
writeU64(os, m_window.getId());
|
||||
writeU8(os, event);
|
||||
writeU8(os, getType());
|
||||
writeNullStr(os, m_id);
|
||||
|
||||
return os;
|
||||
}
|
||||
|
||||
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 at this
|
||||
* point. However, cycles including the root are impossible.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
114
src/ui/elem.h
Normal file
114
src/ui/elem.h
Normal file
|
@ -0,0 +1,114 @@
|
|||
// Luanti
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ui/box.h"
|
||||
#include "ui/helpers.h"
|
||||
#include "util/basic_macros.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
union SDL_Event;
|
||||
|
||||
namespace ui
|
||||
{
|
||||
class Window;
|
||||
|
||||
#define UI_CALLBACK(method) \
|
||||
[](Elem &elem) { \
|
||||
static_cast<decltype(*this)>(elem).method(); \
|
||||
}
|
||||
|
||||
class Elem
|
||||
{
|
||||
public:
|
||||
// Serialized enum; do not change values of entries.
|
||||
enum Type : u8
|
||||
{
|
||||
ELEM = 0x00,
|
||||
ROOT = 0x01,
|
||||
BUTTON = 0x02,
|
||||
TOGGLE = 0x03,
|
||||
OPTION = 0x04,
|
||||
};
|
||||
|
||||
// The main box is always the zeroth item in the Box::NO_GROUP group.
|
||||
static constexpr u32 MAIN_BOX = 0;
|
||||
|
||||
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;
|
||||
|
||||
std::string m_label;
|
||||
|
||||
Box m_main_box;
|
||||
u64 m_hovered_box = Box::NO_ID; // Persistent
|
||||
u64 m_pressed_box = Box::NO_ID; // Persistent
|
||||
|
||||
u32 m_events;
|
||||
|
||||
public:
|
||||
static std::unique_ptr<Elem> create(Type type, Window &window, std::string id);
|
||||
|
||||
Elem(Window &window, std::string id);
|
||||
|
||||
DISABLE_CLASS_COPY(Elem)
|
||||
|
||||
virtual ~Elem();
|
||||
|
||||
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() const { 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 &getMain() { return m_main_box; }
|
||||
|
||||
u64 getHoveredBox() const { return m_hovered_box; }
|
||||
u64 getPressedBox() const { return m_pressed_box; }
|
||||
|
||||
void setHoveredBox(u64 id) { m_hovered_box = id; }
|
||||
void setPressedBox(u64 id) { m_pressed_box = id; }
|
||||
|
||||
virtual void reset();
|
||||
virtual void read(std::istream &is);
|
||||
|
||||
bool isFocused() const;
|
||||
|
||||
virtual bool isBoxFocused (const Box &box) const { return isFocused(); }
|
||||
virtual bool isBoxSelected(const Box &box) const { return false; }
|
||||
virtual bool isBoxHovered (const Box &box) const { return box.getId() == m_hovered_box; }
|
||||
virtual bool isBoxPressed (const Box &box) const { return box.getId() == m_pressed_box; }
|
||||
virtual bool isBoxDisabled(const Box &box) const { return false; }
|
||||
|
||||
virtual bool processInput(const SDL_Event &event) { return false; }
|
||||
|
||||
protected:
|
||||
void enableEvent(u32 event);
|
||||
bool testEvent(u32 event) const;
|
||||
|
||||
std::ostringstream createEvent(u32 event) const;
|
||||
|
||||
private:
|
||||
void readChildren(std::istream &is);
|
||||
};
|
||||
}
|
516
src/ui/helpers.h
Normal file
516
src/ui/helpers.h
Normal file
|
@ -0,0 +1,516 @@
|
|||
// Luanti
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// Copyright (C) 2024 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "irrlichttypes.h"
|
||||
#include "util/serialize.h"
|
||||
|
||||
#include <dimension2d.h>
|
||||
#include <IGUIFont.h>
|
||||
#include <ITexture.h>
|
||||
#include <rect.h>
|
||||
#include <vector2d.h>
|
||||
#include <vector3d.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <sstream>
|
||||
|
||||
namespace ui
|
||||
{
|
||||
// Define some useful named colors.
|
||||
const video::SColor BLANK = 0x00000000;
|
||||
const video::SColor BLACK = 0xFF000000;
|
||||
const video::SColor WHITE = 0xFFFFFFFF;
|
||||
|
||||
/* UIs deal with tons of 2D positions, sizes, rectangles, and the like, but
|
||||
* Irrlicht's core::vector2d, core::dimension2d, and core::rect classes are
|
||||
* inadequate for the job. For instance, vectors use the mathematical
|
||||
* definition and hence have multiple special forms of multiplication.
|
||||
* Notably, they don't have component-wise multiplication, which is the
|
||||
* only one useful for UIs aside from scalar multiplication. Additionally,
|
||||
* the distinction between a position and a dimension are blurred since
|
||||
* vectors can perform operations that make no sense for absolute
|
||||
* positions, e.g. position + position. Dimensions are underpowered, and
|
||||
* rectangles are clunky to work with for multiple reasons.
|
||||
*
|
||||
* So, we create our own classes for use with 2D drawing and UIs. The Pos
|
||||
* class represents an absolute position, whereas the Size class represents
|
||||
* a relative displacement, size, or scaling factor. Similarly, the Rect
|
||||
* class represents an absolute rectangle defined by two Pos fields,
|
||||
* whereas the Disp class represents a double relative displacement or
|
||||
* scaling factor defined by two Size fields.
|
||||
*
|
||||
* All operations are component-wise, e.g. the multiplication operation for
|
||||
* two sizes is defined as `a * b == {a.W * b.W, a.H * b.H}`. Other useful
|
||||
* operations exist, like component-wise minimums and maximums, unions and
|
||||
* intersections of rectangles, offseting rectangles with displacements in
|
||||
* multiple ways, and so on. Lastly, functions never mutate the class in
|
||||
* place, so you can do
|
||||
*
|
||||
* doThing(a.intersectWith(b));
|
||||
*
|
||||
* rather than being forced to use the much more clunky
|
||||
*
|
||||
* core::recti c = a;
|
||||
* c.clipAgainst(b);
|
||||
* doThing(c);
|
||||
*
|
||||
* Implicit conversions between these classes and the corresponding
|
||||
* Irrlicht classes are defined for seamless interoperability with code
|
||||
* that still uses Irrlicht's types.
|
||||
*/
|
||||
template<typename E> struct Pos;
|
||||
template<typename E> struct Size;
|
||||
template<typename E> struct Rect;
|
||||
template<typename E> struct Disp;
|
||||
|
||||
template<typename E>
|
||||
struct Size
|
||||
{
|
||||
E W;
|
||||
E H;
|
||||
|
||||
Size() : W(), H() {}
|
||||
explicit Size(E n) : W(n), H(n) {}
|
||||
Size(E w, E h) : W(w), H(h) {}
|
||||
|
||||
template<typename K>
|
||||
explicit Size(Pos<K> pos) : W(pos.X), H(pos.Y) {}
|
||||
template<typename K>
|
||||
Size(Size<K> other) : W(other.W), H(other.H) {}
|
||||
|
||||
template<typename K>
|
||||
explicit Size(core::vector2d<K> vec) : W(vec.X), H(vec.Y) {}
|
||||
template<typename K>
|
||||
Size(core::dimension2d<K> dim) : W(dim.Width), H(dim.Height) {}
|
||||
|
||||
template<typename K>
|
||||
explicit operator core::vector2d<K>() const { return core::vector2d<K>(W, H); }
|
||||
template<typename K>
|
||||
operator core::dimension2d<K>() const { return core::dimension2d<K>(W, H); }
|
||||
|
||||
E area() const { return W * H; }
|
||||
bool empty() const { return area() == 0; }
|
||||
|
||||
bool operator==(Size<E> other) const { return W == other.W && H == other.H; }
|
||||
bool operator!=(Size<E> other) const { return !(*this == other); }
|
||||
|
||||
E &operator[](int index) { return index ? H : W; }
|
||||
const E &operator[](int index) const { return index ? H : W; }
|
||||
|
||||
Size<E> operator+() const { return Size<E>(+W, +H); }
|
||||
Size<E> operator-() const { return Size<E>(-W, -H); }
|
||||
|
||||
Size<E> operator+(Size<E> other) const { return Size<E>(W + other.W, H + other.H); }
|
||||
Size<E> operator-(Size<E> other) const { return Size<E>(W - other.W, H - other.H); }
|
||||
|
||||
Size<E> &operator+=(Size<E> other) { *this = *this + other; return *this; }
|
||||
Size<E> &operator-=(Size<E> other) { *this = *this - other; return *this; }
|
||||
|
||||
Size<E> operator*(Size<E> other) const { return Size<E>(W * other.W, H * other.H); }
|
||||
Size<E> operator/(Size<E> other) const { return Size<E>(W / other.W, H / other.H); }
|
||||
|
||||
Size<E> &operator*=(Size<E> other) { *this = *this * other; return *this; }
|
||||
Size<E> &operator/=(Size<E> other) { *this = *this / other; return *this; }
|
||||
|
||||
Size<E> operator*(E scalar) const { return Size<E>(W * scalar, H * scalar); }
|
||||
Size<E> operator/(E scalar) const { return Size<E>(W / scalar, H / scalar); }
|
||||
|
||||
Size<E> &operator*=(E scalar) { *this = *this * scalar; return *this; }
|
||||
Size<E> &operator/=(E scalar) { *this = *this / scalar; return *this; }
|
||||
|
||||
Size<E> min(Size<E> other) const
|
||||
{ return Size<E>(std::min(W, other.W), std::min(H, other.H)); }
|
||||
Size<E> max(Size<E> other) const
|
||||
{ return Size<E>(std::max(W, other.W), std::max(H, other.H)); }
|
||||
|
||||
Size<E> clamp(Size<E> lo, Size<E> hi) const
|
||||
{ return Size<E>(std::clamp(W, lo.W, hi.W), std::clamp(H, lo.H, hi.H)); }
|
||||
Size<E> clamp(Disp<E> disp) const
|
||||
{ return clamp(disp.TopLeft, disp.BottomRight); }
|
||||
|
||||
Size<E> clip() const { return max(Size<E>()); }
|
||||
|
||||
friend std::ostream &operator<<(std::ostream &os, Size<E> size)
|
||||
{
|
||||
os << "(" << size.W << ", " << size.H << ")";
|
||||
return os;
|
||||
}
|
||||
};
|
||||
|
||||
using SizeI = Size<s32>;
|
||||
using SizeU = Size<u32>;
|
||||
using SizeF = Size<f32>;
|
||||
|
||||
template<typename E>
|
||||
struct Pos
|
||||
{
|
||||
E X;
|
||||
E Y;
|
||||
|
||||
Pos() : X(), Y() {}
|
||||
explicit Pos(E n) : X(n), Y(n) {}
|
||||
Pos(E x, E y) : X(x), Y(y) {}
|
||||
|
||||
template<typename K>
|
||||
Pos(Pos<K> other) : X(other.X), Y(other.Y) {}
|
||||
template<typename K>
|
||||
explicit Pos(Size<K> size) : X(size.W), Y(size.H) {}
|
||||
|
||||
template<typename K>
|
||||
Pos(core::vector2d<K> vec) : X(vec.X), Y(vec.Y) {}
|
||||
template<typename K>
|
||||
explicit Pos(core::dimension2d<K> dim) : X(dim.Width), Y(dim.Height) {}
|
||||
|
||||
template<typename K>
|
||||
operator core::vector2d<K>() const { return core::vector2d<K>(X, Y); }
|
||||
template<typename K>
|
||||
explicit operator core::dimension2d<K>() const { return core::dimension2d<K>(X, Y); }
|
||||
|
||||
bool operator==(Pos<E> other) const { return X == other.X && Y == other.Y; }
|
||||
bool operator!=(Pos<E> other) const { return !(*this == other); }
|
||||
|
||||
E &operator[](int index) { return index ? Y : X; }
|
||||
const E &operator[](int index) const { return index ? Y : X; }
|
||||
|
||||
Pos<E> operator+(Size<E> size) const { return Pos<E>(X + size.W, Y + size.H); }
|
||||
Pos<E> operator-(Size<E> size) const { return Pos<E>(X - size.W, Y - size.H); }
|
||||
|
||||
Pos<E> &operator+=(Size<E> size) { *this = *this + size; return *this; }
|
||||
Pos<E> &operator-=(Size<E> size) { *this = *this - size; return *this; }
|
||||
|
||||
Pos<E> operator*(Size<E> size) const { return Pos<E>(X * size.W, Y * size.H); }
|
||||
Pos<E> operator/(Size<E> size) const { return Pos<E>(X / size.W, Y / size.H); }
|
||||
|
||||
Pos<E> &operator*=(Size<E> size) { *this = *this * size; return *this; }
|
||||
Pos<E> &operator/=(Size<E> size) { *this = *this / size; return *this; }
|
||||
|
||||
Pos<E> operator*(E scalar) const { return Pos<E>(X * scalar, Y * scalar); }
|
||||
Pos<E> operator/(E scalar) const { return Pos<E>(X / scalar, Y / scalar); }
|
||||
|
||||
Pos<E> &operator*=(E scalar) { *this = *this * scalar; return *this; }
|
||||
Pos<E> &operator/=(E scalar) { *this = *this / scalar; return *this; }
|
||||
|
||||
Size<E> operator-(Pos<E> other) const { return Size<E>(X - other.X, Y - other.Y); }
|
||||
Size<E> operator/(Pos<E> other) const { return Size<E>(X / other.X, Y / other.Y); }
|
||||
|
||||
Pos<E> min(Pos<E> other) const
|
||||
{ return Pos<E>(std::min(X, other.X), std::min(Y, other.Y)); }
|
||||
Pos<E> max(Pos<E> other) const
|
||||
{ return Pos<E>(std::max(X, other.X), std::max(Y, other.Y)); }
|
||||
|
||||
Pos<E> clamp(Pos<E> lo, Pos<E> hi) const
|
||||
{ return Pos<E>(std::clamp(X, lo.X, hi.X), std::clamp(Y, lo.Y, hi.Y)); }
|
||||
Pos<E> clamp(Rect<E> rect) const
|
||||
{ return clamp(rect.TopLeft, rect.BottomRight); }
|
||||
|
||||
friend std::ostream &operator<<(std::ostream &os, Pos<E> pos)
|
||||
{
|
||||
os << "(" << pos.X << ", " << pos.Y << ")";
|
||||
return os;
|
||||
}
|
||||
};
|
||||
|
||||
using PosI = Pos<s32>;
|
||||
using PosU = Pos<u32>;
|
||||
using PosF = Pos<f32>;
|
||||
|
||||
template<typename E>
|
||||
struct Disp
|
||||
{
|
||||
union {
|
||||
struct {
|
||||
E L;
|
||||
E T;
|
||||
};
|
||||
Size<E> TopLeft;
|
||||
};
|
||||
union {
|
||||
struct {
|
||||
E R;
|
||||
E B;
|
||||
};
|
||||
Size<E> BottomRight;
|
||||
};
|
||||
|
||||
Disp() : L(), T(), R(), B() {}
|
||||
explicit Disp(E n) : L(n), T(n), R(n), B(n) {}
|
||||
Disp(E x, E y) : L(x), T(y), R(x), B(y) {}
|
||||
Disp(E l, E t, E r, E b) : L(l), T(t), R(r), B(b) {}
|
||||
|
||||
explicit Disp(Size<E> size) : TopLeft(size), BottomRight(size) {}
|
||||
Disp(Size<E> tl, Size<E> br) : TopLeft(tl), BottomRight(br) {}
|
||||
|
||||
template<typename K>
|
||||
explicit Disp(Rect<K> rect) : TopLeft(rect.TopLeft), BottomRight(rect.BottomRight) {}
|
||||
template<typename K>
|
||||
Disp(Disp<K> other) : TopLeft(other.TopLeft), BottomRight(other.BottomRight) {}
|
||||
|
||||
template<typename K>
|
||||
explicit Disp(core::rect<K> rect) :
|
||||
TopLeft(rect.UpperLeftCorner), BottomRight(rect.LowerRightCorner) {}
|
||||
|
||||
template<typename K>
|
||||
explicit operator core::rect<K>() const { return core::rect<K>(Rect<K>(*this)); }
|
||||
|
||||
E X() const { return L + R; }
|
||||
E Y() const { return T + B; }
|
||||
Size<E> extents() const { return TopLeft + BottomRight; }
|
||||
|
||||
bool operator==(Disp<E> other) const
|
||||
{ return TopLeft == other.TopLeft && BottomRight == other.BottomRight; }
|
||||
bool operator!=(Disp<E> other) const { return !(*this == other); }
|
||||
|
||||
Disp<E> operator+() const { return Disp<E>(+TopLeft, +BottomRight); }
|
||||
Disp<E> operator-() const { return Disp<E>(-TopLeft, -BottomRight); }
|
||||
|
||||
Disp<E> operator+(Disp<E> other) const
|
||||
{ return Disp<E>(TopLeft + other.TopLeft, BottomRight + other.BottomRight); }
|
||||
Disp<E> operator-(Disp<E> other) const
|
||||
{ return Disp<E>(TopLeft - other.TopLeft, BottomRight - other.BottomRight); }
|
||||
|
||||
Disp<E> &operator+=(Disp<E> other) { *this = *this + other; return *this; }
|
||||
Disp<E> &operator-=(Disp<E> other) { *this = *this - other; return *this; }
|
||||
|
||||
Disp<E> operator*(Disp<E> other) const
|
||||
{ return Disp<E>(TopLeft * other.TopLeft, BottomRight * other.BottomRight); }
|
||||
Disp<E> operator/(Disp<E> other) const
|
||||
{ return Disp<E>(TopLeft / other.TopLeft, BottomRight / other.BottomRight); }
|
||||
|
||||
Disp<E> &operator*=(Disp<E> other) { *this = *this * other; return *this; }
|
||||
Disp<E> &operator/=(Disp<E> other) { *this = *this / other; return *this; }
|
||||
|
||||
Disp<E> operator*(E scalar) const
|
||||
{ return Disp<E>(TopLeft * scalar, BottomRight * scalar); }
|
||||
Disp<E> operator/(E scalar) const
|
||||
{ return Disp<E>(TopLeft / scalar, BottomRight / scalar); }
|
||||
|
||||
Disp<E> &operator*=(E scalar) { *this = *this * scalar; return *this; }
|
||||
Disp<E> &operator/=(E scalar) { *this = *this / scalar; return *this; }
|
||||
|
||||
Disp<E> clip() const { return Disp<E>(TopLeft.clip(), BottomRight.clip()); }
|
||||
|
||||
friend std::ostream &operator<<(std::ostream &os, Disp<E> disp)
|
||||
{
|
||||
os << "(" << disp.L << ", " << disp.T << ", " << disp.R << ", " << disp.B << ")";
|
||||
return os;
|
||||
}
|
||||
};
|
||||
|
||||
using DispI = Disp<s32>;
|
||||
using DispU = Disp<u32>;
|
||||
using DispF = Disp<f32>;
|
||||
|
||||
template<typename E>
|
||||
struct Rect
|
||||
{
|
||||
union {
|
||||
struct {
|
||||
E L;
|
||||
E T;
|
||||
};
|
||||
Pos<E> TopLeft;
|
||||
};
|
||||
union {
|
||||
struct {
|
||||
E R;
|
||||
E B;
|
||||
};
|
||||
Pos<E> BottomRight;
|
||||
};
|
||||
|
||||
Rect() : L(), T(), R(), B() {}
|
||||
Rect(E l, E t, E r, E b) : L(l), T(t), R(r), B(b) {}
|
||||
|
||||
explicit Rect(Pos<E> pos) : TopLeft(pos), BottomRight(pos) {}
|
||||
Rect(Pos<E> tl, Pos<E> br) : TopLeft(tl), BottomRight(br) {}
|
||||
|
||||
explicit Rect(Size<E> size) : TopLeft(), BottomRight(size) {}
|
||||
Rect(Pos<E> pos, Size<E> size) : TopLeft(pos), BottomRight(pos + size) {}
|
||||
|
||||
template<typename K>
|
||||
Rect(Rect<K> other) : TopLeft(other.TopLeft), BottomRight(other.BottomRight) {}
|
||||
template<typename K>
|
||||
explicit Rect(Disp<K> disp) : TopLeft(disp.TopLeft), BottomRight(disp.BottomRight) {}
|
||||
|
||||
template<typename K>
|
||||
Rect(core::rect<K> rect) :
|
||||
TopLeft(rect.UpperLeftCorner), BottomRight(rect.LowerRightCorner) {}
|
||||
|
||||
template<typename K>
|
||||
operator core::rect<K>() const { return core::rect<K>(TopLeft, BottomRight); }
|
||||
|
||||
E W() const { return R - L; }
|
||||
E H() const { return B - T; }
|
||||
Size<E> size() const { return BottomRight - TopLeft; }
|
||||
|
||||
E area() const { return size().area(); }
|
||||
bool empty() const { return size().empty(); }
|
||||
|
||||
bool operator==(Rect<E> other) const
|
||||
{ return TopLeft == other.TopLeft && BottomRight == other.BottomRight; }
|
||||
bool operator!=(Rect<E> other) const { return !(*this == other); }
|
||||
|
||||
Rect<E> operator+(Disp<E> disp) const
|
||||
{ return Rect<E>(TopLeft + disp.TopLeft, BottomRight + disp.BottomRight); }
|
||||
Rect<E> operator-(Disp<E> disp) const
|
||||
{ return Rect<E>(TopLeft - disp.TopLeft, BottomRight - disp.BottomRight); }
|
||||
|
||||
Rect<E> &operator+=(Disp<E> disp) { *this = *this + disp; return *this; }
|
||||
Rect<E> &operator-=(Disp<E> disp) { *this = *this - disp; return *this; }
|
||||
|
||||
Rect<E> operator*(Disp<E> disp) const
|
||||
{ return Rect<E>(TopLeft * disp.TopLeft, BottomRight * disp.BottomRight); }
|
||||
Rect<E> operator/(Disp<E> disp) const
|
||||
{ return Rect<E>(TopLeft / disp.TopLeft, BottomRight / disp.BottomRight); }
|
||||
|
||||
Rect<E> &operator*=(Disp<E> disp) { *this = *this * disp; return *this; }
|
||||
Rect<E> &operator/=(Disp<E> disp) { *this = *this / disp; return *this; }
|
||||
|
||||
Rect<E> operator*(E scalar) const
|
||||
{ return Rect<E>(TopLeft * scalar, BottomRight * scalar); }
|
||||
Rect<E> operator/(E scalar) const
|
||||
{ return Rect<E>(TopLeft / scalar, BottomRight / scalar); }
|
||||
|
||||
Rect<E> &operator*=(E scalar) { *this = *this * scalar; return *this; }
|
||||
Rect<E> &operator/=(E scalar) { *this = *this / scalar; return *this; }
|
||||
|
||||
Disp<E> operator-(Rect<E> other) const
|
||||
{ return Disp<E>(TopLeft - other.TopLeft, BottomRight - other.BottomRight); }
|
||||
Disp<E> operator/(Rect<E> other) const
|
||||
{ return Disp<E>(TopLeft / other.TopLeft, BottomRight / other.BottomRight); }
|
||||
|
||||
Rect<E> insetBy(Disp<E> disp) const
|
||||
{ return Rect<E>(TopLeft + disp.TopLeft, BottomRight - disp.BottomRight); }
|
||||
Rect<E> outsetBy(Disp<E> disp) const
|
||||
{ return Rect<E>(TopLeft - disp.TopLeft, BottomRight + disp.BottomRight); }
|
||||
|
||||
Rect<E> unionWith(Rect<E> other) const
|
||||
{ return Rect<E>(TopLeft.min(other.TopLeft), BottomRight.max(other.BottomRight)); }
|
||||
Rect<E> intersectWith(Rect<E> other) const
|
||||
{ return Rect<E>(TopLeft.max(other.TopLeft), BottomRight.min(other.BottomRight)); }
|
||||
|
||||
Rect<E> clip() const { return Rect<E>(TopLeft, size().clip()); }
|
||||
|
||||
bool contains(Pos<E> pos) const
|
||||
{ return pos.X >= L && pos.Y >= T && pos.X < R && pos.Y < B; }
|
||||
|
||||
friend std::ostream &operator<<(std::ostream &os, Rect<E> rect)
|
||||
{
|
||||
os << "(" << rect.L << ", " << rect.T << ", " << rect.R << ", " << rect.B << ")";
|
||||
return os;
|
||||
}
|
||||
};
|
||||
|
||||
using RectI = Rect<s32>;
|
||||
using RectU = Rect<u32>;
|
||||
using RectF = Rect<f32>;
|
||||
|
||||
// Define a few functions that are particularly useful for UI serialization
|
||||
// and deserialization. The testShift() function shifts out and returns the
|
||||
// lower bit of an integer containing flags, which is particularly useful
|
||||
// for testing whether an optional serialized field is present or not.
|
||||
inline bool testShift(u32 &mask)
|
||||
{
|
||||
bool test = mask & 1;
|
||||
mask >>= 1;
|
||||
return test;
|
||||
}
|
||||
|
||||
// Booleans are often stored directly in the flags value. However, we want
|
||||
// the bit position of each field to stay constant, so the mask needs to be
|
||||
// shifted regardless of whether the boolean is set.
|
||||
inline void testShiftBool(u32 &mask, bool &flag)
|
||||
{
|
||||
if (testShift(mask)) {
|
||||
flag = testShift(mask);
|
||||
} else {
|
||||
testShift(mask);
|
||||
}
|
||||
}
|
||||
|
||||
// Convenience functions for creating new binary streams.
|
||||
inline std::istringstream newIs(const std::string &str)
|
||||
{
|
||||
return std::istringstream(str, std::ios_base::binary);
|
||||
}
|
||||
|
||||
inline std::ostringstream newOs()
|
||||
{
|
||||
return std::ostringstream(std::ios_base::binary);
|
||||
}
|
||||
|
||||
// The UI purposefully avoids dealing with SerializationError, so it always
|
||||
// uses string functions that truncate gracefully. Hence, we make
|
||||
// convenience wrappers around the string functions in "serialize.h".
|
||||
inline std::string readStr16(std::istream &is) { return deSerializeString16(is, true); }
|
||||
inline std::string readStr32(std::istream &is) { return deSerializeString32(is, true); }
|
||||
|
||||
inline void writeStr16(std::ostream &os, std::string_view str)
|
||||
{ os << serializeString16(str, true); }
|
||||
inline void writeStr32(std::ostream &os, std::string_view str)
|
||||
{ os << serializeString32(str, true); }
|
||||
|
||||
// The UI also uses null-terminated strings for certain fields as well, so
|
||||
// define functions to work with them.
|
||||
inline std::string readNullStr(std::istream &is)
|
||||
{
|
||||
std::string str;
|
||||
std::getline(is, str, '\0');
|
||||
return str;
|
||||
}
|
||||
|
||||
inline void writeNullStr(std::ostream &os, std::string_view str)
|
||||
{
|
||||
os << std::string_view(str.data(), std::min(str.find('\0'), str.size())) << '\0';
|
||||
}
|
||||
|
||||
// Define serialization and deserialization functions that work with the
|
||||
// positioning types above. Brace initializer lists are used for the
|
||||
// constructors because they guarantee left-to-right argument evaluation.
|
||||
inline PosI readPosI(std::istream &is) { return PosI{readS32(is), readS32(is)}; }
|
||||
inline PosU readPosU(std::istream &is) { return PosU{readU32(is), readU32(is)}; }
|
||||
inline PosF readPosF(std::istream &is) { return PosF{readF32(is), readF32(is)}; }
|
||||
|
||||
inline void writePosI(std::ostream &os, PosI pos)
|
||||
{ writeS32(os, pos.X); writeS32(os, pos.Y); }
|
||||
inline void writePosU(std::ostream &os, PosU pos)
|
||||
{ writeU32(os, pos.X); writeU32(os, pos.Y); }
|
||||
inline void writePosF(std::ostream &os, PosF pos)
|
||||
{ writeF32(os, pos.X); writeF32(os, pos.Y); }
|
||||
|
||||
inline SizeI readSizeI(std::istream &is) { return SizeI{readS32(is), readS32(is)}; }
|
||||
inline SizeU readSizeU(std::istream &is) { return SizeU{readU32(is), readU32(is)}; }
|
||||
inline SizeF readSizeF(std::istream &is) { return SizeF{readF32(is), readF32(is)}; }
|
||||
|
||||
inline void writeSizeI(std::ostream &os, SizeI size)
|
||||
{ writeS32(os, size.W); writeS32(os, size.H); }
|
||||
inline void writeSizeU(std::ostream &os, SizeU size)
|
||||
{ writeU32(os, size.W); writeU32(os, size.H); }
|
||||
inline void writeSizeF(std::ostream &os, SizeF size)
|
||||
{ writeF32(os, size.W); writeF32(os, size.H); }
|
||||
|
||||
inline RectI readRectI(std::istream &is) { return RectI{readPosI(is), readPosI(is)}; }
|
||||
inline RectU readRectU(std::istream &is) { return RectU{readPosU(is), readPosU(is)}; }
|
||||
inline RectF readRectF(std::istream &is) { return RectF{readPosF(is), readPosF(is)}; }
|
||||
|
||||
inline void writeRectI(std::ostream &os, RectI rect)
|
||||
{ writePosI(os, rect.TopLeft); writePosI(os, rect.BottomRight); }
|
||||
inline void writeRectU(std::ostream &os, RectU rect)
|
||||
{ writePosU(os, rect.TopLeft); writePosU(os, rect.BottomRight); }
|
||||
inline void writeRectF(std::ostream &os, RectF rect)
|
||||
{ writePosF(os, rect.TopLeft); writePosF(os, rect.BottomRight); }
|
||||
|
||||
inline DispI readDispI(std::istream &is) { return DispI{readSizeI(is), readSizeI(is)}; }
|
||||
inline DispU readDispU(std::istream &is) { return DispU{readSizeU(is), readSizeU(is)}; }
|
||||
inline DispF readDispF(std::istream &is) { return DispF{readSizeF(is), readSizeF(is)}; }
|
||||
|
||||
inline void writeDispI(std::ostream &os, DispI disp)
|
||||
{ writeSizeI(os, disp.TopLeft); writeSizeI(os, disp.BottomRight); }
|
||||
inline void writeDispU(std::ostream &os, DispU disp)
|
||||
{ writeSizeU(os, disp.TopLeft); writeSizeU(os, disp.BottomRight); }
|
||||
inline void writeDispF(std::ostream &os, DispF disp)
|
||||
{ writeSizeF(os, disp.TopLeft); writeSizeF(os, disp.BottomRight); }
|
||||
}
|
172
src/ui/manager.cpp
Normal file
172
src/ui/manager.cpp
Normal file
|
@ -0,0 +1,172 @@
|
|||
// Luanti
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
#include "ui/manager.h"
|
||||
|
||||
#include "debug.h"
|
||||
#include "log.h"
|
||||
#include "settings.h"
|
||||
#include "client/client.h"
|
||||
#include "client/renderingengine.h"
|
||||
#include "client/texturesource.h"
|
||||
#include "client/tile.h"
|
||||
#include "gui/mainmenumanager.h"
|
||||
#include "util/serialize.h"
|
||||
|
||||
#include <SDL2/SDL.h>
|
||||
|
||||
namespace ui
|
||||
{
|
||||
SDL_Event createUiEvent(UiEvent type, void *data1, void *data2)
|
||||
{
|
||||
SDL_Event event;
|
||||
|
||||
event.user.type = type + SDL_USEREVENT;
|
||||
event.user.code = 0;
|
||||
event.user.data1 = data1;
|
||||
event.user.data2 = data2;
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
video::ITexture *Manager::getTexture(const std::string &name) const
|
||||
{
|
||||
return m_client->tsrc()->getTexture(name);
|
||||
}
|
||||
|
||||
float Manager::getScale(WindowType type) const
|
||||
{
|
||||
if (type == WindowType::GUI || type == WindowType::CHAT) {
|
||||
return m_gui_scale;
|
||||
}
|
||||
return m_hud_scale;
|
||||
}
|
||||
|
||||
void Manager::reset()
|
||||
{
|
||||
m_client = nullptr;
|
||||
|
||||
m_windows.clear();
|
||||
m_gui_windows.clear();
|
||||
}
|
||||
|
||||
void Manager::removeWindow(u64 id)
|
||||
{
|
||||
auto it = m_windows.find(id);
|
||||
if (it == m_windows.end()) {
|
||||
errorstream << "Window " << id << " is already closed" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
m_windows.erase(it);
|
||||
m_gui_windows.erase(id);
|
||||
}
|
||||
|
||||
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()) {
|
||||
errorstream << "Window " << id << " is already open" << std::endl;
|
||||
break;
|
||||
}
|
||||
|
||||
it = m_windows.emplace(id, id).first;
|
||||
if (!it->second.read(is, true)) {
|
||||
errorstream << "Fatal error when opening window " << id <<
|
||||
"; closing window" << std::endl;
|
||||
removeWindow(id);
|
||||
break;
|
||||
}
|
||||
|
||||
if (it->second.getType() == WindowType::GUI) {
|
||||
m_gui_windows.emplace(id, &it->second);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case UPDATE_WINDOW: {
|
||||
auto it = m_windows.find(id);
|
||||
if (it == m_windows.end()) {
|
||||
errorstream << "Window " << id << " does not exist" << std::endl;
|
||||
}
|
||||
|
||||
if (!it->second.read(is, false)) {
|
||||
errorstream << "Fatal error when updating window " << id <<
|
||||
"; closing window" << std::endl;
|
||||
removeWindow(id);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case CLOSE_WINDOW:
|
||||
removeWindow(id);
|
||||
break;
|
||||
|
||||
default:
|
||||
errorstream << "Invalid manager action: " << action << std::endl;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Manager::sendMessage(const std::string &data)
|
||||
{
|
||||
m_client->sendUiMessage(data.c_str(), data.size());
|
||||
}
|
||||
|
||||
void Manager::preDraw()
|
||||
{
|
||||
float base_scale = RenderingEngine::getDisplayDensity();
|
||||
m_gui_scale = base_scale * g_settings->getFloat("gui_scaling");
|
||||
m_hud_scale = base_scale * g_settings->getFloat("hud_scaling");
|
||||
}
|
||||
|
||||
void Manager::drawType(WindowType type)
|
||||
{
|
||||
for (auto &it : m_windows) {
|
||||
if (it.second.getType() == type) {
|
||||
it.second.drawAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Window *Manager::getFocused()
|
||||
{
|
||||
if (m_gui_windows.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
return m_gui_windows.rbegin()->second;
|
||||
}
|
||||
|
||||
bool Manager::isFocused() const
|
||||
{
|
||||
return g_menumgr.menuCount() == 0 && !m_gui_windows.empty();
|
||||
}
|
||||
|
||||
bool Manager::processInput(const SDL_Event &event)
|
||||
{
|
||||
Window *focused = getFocused();
|
||||
if (focused != nullptr) {
|
||||
return focused->processInput(event);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Manager g_manager;
|
||||
}
|
125
src/ui/manager.h
Normal file
125
src/ui/manager.h
Normal file
|
@ -0,0 +1,125 @@
|
|||
// Luanti
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ui/helpers.h"
|
||||
#include "ui/window.h"
|
||||
#include "util/basic_macros.h"
|
||||
|
||||
#include <IGUIElement.h>
|
||||
|
||||
#include <iostream>
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
class Client;
|
||||
|
||||
union SDL_Event;
|
||||
|
||||
namespace ui
|
||||
{
|
||||
/* Custom UI-specific event types for events of type SDL_USEREVENT. Create
|
||||
* the event structure with createUiEvent().
|
||||
*
|
||||
* Some events should always return false to give parent elements and other
|
||||
* boxes a chance to see the event. Other events return true to indicate
|
||||
* that the element may become focused or hovered.
|
||||
*/
|
||||
enum UiEvent
|
||||
{
|
||||
UI_FOCUS_REQUEST, // Return true to accept request.
|
||||
UI_FOCUS_CHANGED, // Never return true.
|
||||
UI_FOCUS_SUBVERTED, // Not sent to parent elements. Never return true.
|
||||
|
||||
UI_HOVER_REQUEST, // Return true to accept request.
|
||||
UI_HOVER_CHANGED,
|
||||
};
|
||||
|
||||
#define UI_USER(event) (UI_##event + SDL_USEREVENT)
|
||||
|
||||
SDL_Event createUiEvent(UiEvent type, void *data1 = nullptr, void *data2 = nullptr);
|
||||
|
||||
class Manager
|
||||
{
|
||||
public:
|
||||
// Serialized enum; do not change values of entries.
|
||||
enum ReceiveAction : u8
|
||||
{
|
||||
OPEN_WINDOW = 0x00,
|
||||
REOPEN_WINDOW = 0x01,
|
||||
UPDATE_WINDOW = 0x02,
|
||||
CLOSE_WINDOW = 0x03,
|
||||
};
|
||||
|
||||
// Serialized enum; do not change values of entries.
|
||||
enum SendAction : u8
|
||||
{
|
||||
WINDOW_EVENT = 0x00,
|
||||
ELEM_EVENT = 0x01,
|
||||
};
|
||||
|
||||
private:
|
||||
Client *m_client;
|
||||
|
||||
float m_gui_scale = 0.0f;
|
||||
float m_hud_scale = 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;
|
||||
|
||||
// Keep track of which GUI windows are currently open. We also use a
|
||||
// map so we can easily find the topmost window.
|
||||
std::map<u64, Window *> m_gui_windows;
|
||||
|
||||
public:
|
||||
Manager()
|
||||
{
|
||||
reset();
|
||||
}
|
||||
|
||||
DISABLE_CLASS_COPY(Manager)
|
||||
|
||||
Client *getClient() const { return m_client; }
|
||||
void setClient(Client *client) { m_client = client; }
|
||||
|
||||
video::ITexture *getTexture(const std::string &name) const;
|
||||
|
||||
float getScale(WindowType type) const;
|
||||
|
||||
void reset();
|
||||
void removeWindow(u64 id);
|
||||
|
||||
void receiveMessage(const std::string &data);
|
||||
void sendMessage(const std::string &data);
|
||||
|
||||
void preDraw();
|
||||
void drawType(WindowType type);
|
||||
|
||||
Window *getFocused();
|
||||
|
||||
bool isFocused() const;
|
||||
bool processInput(const SDL_Event &event);
|
||||
};
|
||||
|
||||
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, core::recti())
|
||||
{}
|
||||
|
||||
virtual void draw() override
|
||||
{
|
||||
g_manager.drawType(ui::WindowType::GUI);
|
||||
gui::IGUIElement::draw();
|
||||
}
|
||||
};
|
||||
}
|
38
src/ui/static_elems.cpp
Normal file
38
src/ui/static_elems.cpp
Normal file
|
@ -0,0 +1,38 @@
|
|||
// Luanti
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// Copyright (C) 2024 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
#include "ui/static_elems.h"
|
||||
|
||||
#include "debug.h"
|
||||
#include "log.h"
|
||||
#include "ui/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);
|
||||
|
||||
m_backdrop_box.setContent({&getMain()});
|
||||
}
|
||||
|
||||
bool Root::isBoxFocused(const Box &box) const
|
||||
{
|
||||
return box.getItem() == BACKDROP_BOX ? getWindow().isFocused() : isFocused();
|
||||
}
|
||||
}
|
38
src/ui/static_elems.h
Normal file
38
src/ui/static_elems.h
Normal file
|
@ -0,0 +1,38 @@
|
|||
// Luanti
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// Copyright (C) 2024 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ui/box.h"
|
||||
#include "ui/elem.h"
|
||||
#include "ui/helpers.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
namespace ui
|
||||
{
|
||||
class Root : public Elem
|
||||
{
|
||||
private:
|
||||
Box m_backdrop_box;
|
||||
|
||||
static constexpr u32 BACKDROP_BOX = 1;
|
||||
|
||||
public:
|
||||
Root(Window &window, std::string id) :
|
||||
Elem(window, std::move(id)),
|
||||
m_backdrop_box(*this, Box::NO_GROUP, BACKDROP_BOX)
|
||||
{}
|
||||
|
||||
virtual Type getType() const override { return ROOT; }
|
||||
|
||||
Box &getBackdrop() { return m_backdrop_box; }
|
||||
|
||||
virtual void reset() override;
|
||||
virtual void read(std::istream &is) override;
|
||||
|
||||
virtual bool isBoxFocused(const Box &box) const override;
|
||||
};
|
||||
}
|
242
src/ui/style.cpp
Normal file
242
src/ui/style.cpp
Normal file
|
@ -0,0 +1,242 @@
|
|||
// Luanti
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// Copyright (C) 2024 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
#include "ui/style.h"
|
||||
|
||||
#include "debug.h"
|
||||
#include "log.h"
|
||||
#include "ui/manager.h"
|
||||
#include "util/serialize.h"
|
||||
|
||||
namespace ui
|
||||
{
|
||||
static LayoutType toLayoutType(u8 type)
|
||||
{
|
||||
if (type > (u8)LayoutType::MAX) {
|
||||
return LayoutType::PLACE;
|
||||
}
|
||||
return (LayoutType)type;
|
||||
}
|
||||
|
||||
static DirFlags toDirFlags(u8 dir)
|
||||
{
|
||||
if (dir > (u8)DirFlags::MAX) {
|
||||
return DirFlags::NONE;
|
||||
}
|
||||
return (DirFlags)dir;
|
||||
}
|
||||
|
||||
static DisplayMode toDisplayMode(u8 mode)
|
||||
{
|
||||
if (mode > (u8)DisplayMode::MAX) {
|
||||
return DisplayMode::VISIBLE;
|
||||
}
|
||||
return (DisplayMode)mode;
|
||||
}
|
||||
|
||||
static IconPlace toIconPlace(u8 place)
|
||||
{
|
||||
if (place > (u8)IconPlace::MAX) {
|
||||
return IconPlace::CENTER;
|
||||
}
|
||||
return (IconPlace)place;
|
||||
}
|
||||
|
||||
static Align toAlign(u8 align)
|
||||
{
|
||||
if (align > (u8)Align::MAX) {
|
||||
return Align::CENTER;
|
||||
}
|
||||
return (Align)align;
|
||||
}
|
||||
|
||||
void Layout::reset()
|
||||
{
|
||||
type = LayoutType::PLACE;
|
||||
clip = DirFlags::NONE;
|
||||
|
||||
scale = 0.0f;
|
||||
}
|
||||
|
||||
void Layout::read(std::istream &full_is)
|
||||
{
|
||||
auto is = newIs(readStr16(full_is));
|
||||
u32 set_mask = readU32(is);
|
||||
|
||||
if (testShift(set_mask))
|
||||
type = toLayoutType(readU8(is));
|
||||
if (testShift(set_mask))
|
||||
clip = toDirFlags(readU8(is));
|
||||
|
||||
if (testShift(set_mask))
|
||||
scale = std::max(readF32(is), 0.0f);
|
||||
}
|
||||
|
||||
void Sizing::reset()
|
||||
{
|
||||
size = SizeF(0.0f, 0.0f);
|
||||
span = SizeF(1.0f, 1.0f);
|
||||
|
||||
pos = PosF(0.0f, 0.0f);
|
||||
anchor = PosF(0.0f, 0.0f);
|
||||
|
||||
margin = DispF(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
padding = DispF(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
}
|
||||
|
||||
void Sizing::read(std::istream &full_is)
|
||||
{
|
||||
auto is = newIs(readStr16(full_is));
|
||||
u32 set_mask = readU32(is);
|
||||
|
||||
if (testShift(set_mask))
|
||||
size = readSizeF(is).clip();
|
||||
if (testShift(set_mask))
|
||||
span = readSizeF(is).clip();
|
||||
|
||||
if (testShift(set_mask))
|
||||
pos = readPosF(is);
|
||||
if (testShift(set_mask))
|
||||
anchor = readPosF(is);
|
||||
|
||||
if (testShift(set_mask))
|
||||
margin = readDispF(is);
|
||||
if (testShift(set_mask))
|
||||
padding = readDispF(is);
|
||||
}
|
||||
|
||||
void Layer::reset()
|
||||
{
|
||||
image = nullptr;
|
||||
fill = BLANK;
|
||||
tint = WHITE;
|
||||
|
||||
scale = 1.0f;
|
||||
source = RectF(0.0f, 0.0f, 1.0f, 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))
|
||||
scale = std::max(readF32(is), 0.0f);
|
||||
if (testShift(set_mask))
|
||||
source = readRectF(is);
|
||||
|
||||
if (testShift(set_mask))
|
||||
num_frames = std::max(readU32(is), 1U);
|
||||
if (testShift(set_mask))
|
||||
frame_time = std::max(readU32(is), 1U);
|
||||
}
|
||||
|
||||
void Text::reset()
|
||||
{
|
||||
prepend = "";
|
||||
append = "";
|
||||
|
||||
color = WHITE;
|
||||
mark = BLANK;
|
||||
size = 16;
|
||||
|
||||
mono = false;
|
||||
italic = false;
|
||||
bold = false;
|
||||
|
||||
align = Align::CENTER;
|
||||
valign = Align::CENTER;
|
||||
}
|
||||
|
||||
void Text::read(std::istream &full_is)
|
||||
{
|
||||
auto is = newIs(readStr16(full_is));
|
||||
u32 set_mask = readU32(is);
|
||||
|
||||
if (testShift(set_mask))
|
||||
prepend = readStr16(is);
|
||||
if (testShift(set_mask))
|
||||
append = readStr16(is);
|
||||
|
||||
if (testShift(set_mask))
|
||||
color = readARGB8(is);
|
||||
if (testShift(set_mask))
|
||||
mark = readARGB8(is);
|
||||
if (testShift(set_mask))
|
||||
size = std::clamp(readU32(is), 1U, 999U);
|
||||
|
||||
testShiftBool(set_mask, mono);
|
||||
testShiftBool(set_mask, italic);
|
||||
testShiftBool(set_mask, bold);
|
||||
|
||||
if (testShift(set_mask))
|
||||
align = toAlign(readU8(is));
|
||||
if (testShift(set_mask))
|
||||
valign = toAlign(readU8(is));
|
||||
}
|
||||
|
||||
void Style::reset()
|
||||
{
|
||||
layout.reset();
|
||||
sizing.reset();
|
||||
|
||||
display = DisplayMode::VISIBLE;
|
||||
|
||||
box.reset();
|
||||
icon.reset();
|
||||
|
||||
box_middle = DispF(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
box_tile = DirFlags::NONE;
|
||||
|
||||
icon_place = IconPlace::CENTER;
|
||||
icon_gutter = 0.0f;
|
||||
icon_overlap = false;
|
||||
|
||||
text.reset();
|
||||
}
|
||||
|
||||
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))
|
||||
layout.read(is);
|
||||
if (testShift(set_mask))
|
||||
sizing.read(is);
|
||||
|
||||
if (testShift(set_mask))
|
||||
display = toDisplayMode(readU8(is));
|
||||
|
||||
if (testShift(set_mask))
|
||||
box.read(is);
|
||||
if (testShift(set_mask))
|
||||
icon.read(is);
|
||||
|
||||
if (testShift(set_mask))
|
||||
box_middle = readDispF(is).clip();
|
||||
if (testShift(set_mask))
|
||||
box_tile = toDirFlags(readU8(is));
|
||||
|
||||
if (testShift(set_mask))
|
||||
icon_place = toIconPlace(readU8(is));
|
||||
if (testShift(set_mask))
|
||||
icon_gutter = readF32(is);
|
||||
testShiftBool(set_mask, icon_overlap);
|
||||
|
||||
if (testShift(set_mask))
|
||||
text.read(is);
|
||||
}
|
||||
}
|
160
src/ui/style.h
Normal file
160
src/ui/style.h
Normal file
|
@ -0,0 +1,160 @@
|
|||
// Luanti
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// Copyright (C) 2024 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ui/helpers.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
namespace ui
|
||||
{
|
||||
// Serialized enum; do not change order of entries.
|
||||
enum class LayoutType : u8
|
||||
{
|
||||
PLACE,
|
||||
|
||||
MAX = PLACE,
|
||||
};
|
||||
|
||||
// Serialized enum; do not change order of entries.
|
||||
enum class DirFlags : u8
|
||||
{
|
||||
NONE,
|
||||
X,
|
||||
Y,
|
||||
BOTH,
|
||||
|
||||
MAX = BOTH,
|
||||
};
|
||||
|
||||
// Serialized enum; do not change order of entries.
|
||||
enum class DisplayMode : u8
|
||||
{
|
||||
VISIBLE,
|
||||
OVERFLOW,
|
||||
HIDDEN,
|
||||
CLIPPED,
|
||||
|
||||
MAX = CLIPPED,
|
||||
};
|
||||
|
||||
// Serialized enum; do not change order of entries.
|
||||
enum class IconPlace : u8
|
||||
{
|
||||
CENTER,
|
||||
LEFT,
|
||||
TOP,
|
||||
RIGHT,
|
||||
BOTTOM,
|
||||
|
||||
MAX = BOTTOM,
|
||||
};
|
||||
|
||||
// Serialized enum; do not change order of entries.
|
||||
enum class Align : u8
|
||||
{
|
||||
START,
|
||||
CENTER,
|
||||
END,
|
||||
|
||||
MAX = END,
|
||||
};
|
||||
|
||||
struct Layout
|
||||
{
|
||||
LayoutType type;
|
||||
DirFlags clip;
|
||||
|
||||
float scale;
|
||||
|
||||
Layout() { reset(); }
|
||||
|
||||
void reset();
|
||||
void read(std::istream &is);
|
||||
};
|
||||
|
||||
struct Sizing
|
||||
{
|
||||
SizeF size;
|
||||
SizeF span;
|
||||
|
||||
PosF pos;
|
||||
PosF anchor;
|
||||
|
||||
DispF margin;
|
||||
DispF padding;
|
||||
|
||||
Sizing() { reset(); }
|
||||
|
||||
void reset();
|
||||
void read(std::istream &is);
|
||||
};
|
||||
|
||||
struct Layer
|
||||
{
|
||||
video::ITexture *image;
|
||||
video::SColor fill;
|
||||
video::SColor tint;
|
||||
|
||||
float scale;
|
||||
RectF source;
|
||||
|
||||
u32 num_frames;
|
||||
u32 frame_time;
|
||||
|
||||
Layer() { reset(); }
|
||||
|
||||
void reset();
|
||||
void read(std::istream &is);
|
||||
};
|
||||
|
||||
struct Text
|
||||
{
|
||||
std::string prepend;
|
||||
std::string append;
|
||||
|
||||
video::SColor color;
|
||||
video::SColor mark;
|
||||
u32 size;
|
||||
|
||||
bool mono;
|
||||
bool italic;
|
||||
bool bold;
|
||||
|
||||
Align align;
|
||||
Align valign;
|
||||
|
||||
Text() { reset(); }
|
||||
|
||||
void reset();
|
||||
void read(std::istream &is);
|
||||
};
|
||||
|
||||
struct Style
|
||||
{
|
||||
Layout layout;
|
||||
Sizing sizing;
|
||||
|
||||
DisplayMode display;
|
||||
|
||||
Layer box;
|
||||
Layer icon;
|
||||
|
||||
DispF box_middle;
|
||||
DirFlags box_tile;
|
||||
|
||||
IconPlace icon_place;
|
||||
float icon_gutter;
|
||||
bool icon_overlap;
|
||||
|
||||
Text text;
|
||||
|
||||
Style() { reset(); }
|
||||
|
||||
void reset();
|
||||
void read(std::istream &is);
|
||||
};
|
||||
}
|
812
src/ui/window.cpp
Normal file
812
src/ui/window.cpp
Normal file
|
@ -0,0 +1,812 @@
|
|||
// Luanti
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
#include "ui/window.h"
|
||||
|
||||
#include "debug.h"
|
||||
#include "log.h"
|
||||
#include "settings.h"
|
||||
#include "client/client.h"
|
||||
#include "client/renderingengine.h"
|
||||
#include "client/tile.h"
|
||||
#include "ui/box.h"
|
||||
#include "ui/manager.h"
|
||||
#include "ui/static_elems.h"
|
||||
#include "util/serialize.h"
|
||||
#include "util/string.h"
|
||||
|
||||
#include <SDL2/SDL.h>
|
||||
|
||||
namespace ui
|
||||
{
|
||||
SizeI getTextureSize(video::ITexture *texture)
|
||||
{
|
||||
if (texture != nullptr) {
|
||||
return SizeI(texture->getOriginalSize());
|
||||
}
|
||||
return SizeI(0, 0);
|
||||
}
|
||||
|
||||
WindowType toWindowType(u8 type)
|
||||
{
|
||||
if (type > (u8)WindowType::MAX) {
|
||||
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;
|
||||
}
|
||||
|
||||
Elem *Window::getNextElem(Elem *elem, bool reverse)
|
||||
{
|
||||
size_t next = elem->getOrder();
|
||||
size_t last = m_ordered_elems.size() - 1;
|
||||
|
||||
if (!reverse) {
|
||||
next = (next == last) ? 0 : next + 1;
|
||||
} else {
|
||||
next = (next == 0) ? last : next - 1;
|
||||
}
|
||||
|
||||
return m_ordered_elems[next];
|
||||
}
|
||||
|
||||
void Window::clearElem(Elem *elem)
|
||||
{
|
||||
if (m_focused_elem == elem) {
|
||||
m_focused_elem = nullptr;
|
||||
}
|
||||
if (m_hovered_elem == elem) {
|
||||
m_hovered_elem = 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();
|
||||
|
||||
m_focused_elem = nullptr;
|
||||
m_hovered_elem = nullptr;
|
||||
|
||||
m_allow_close = true;
|
||||
m_events = 0;
|
||||
}
|
||||
|
||||
bool Window::read(std::istream &is, bool opening)
|
||||
{
|
||||
// Read in all the fundamental properties that must be unconditionally
|
||||
// provided for the window.
|
||||
std::unordered_map<Elem *, std::string> elem_contents;
|
||||
readElems(is, elem_contents);
|
||||
|
||||
if (!readRootElem(is)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
readStyles(is);
|
||||
|
||||
if (opening) {
|
||||
m_type = toWindowType(readU8(is));
|
||||
}
|
||||
|
||||
// After the unconditional properties, read the conditional ones.
|
||||
u32 set_mask = readU32(is);
|
||||
|
||||
bool set_focus = false;
|
||||
Elem *new_focused = nullptr;
|
||||
|
||||
if (testShift(set_mask)) {
|
||||
new_focused = getElem(readNullStr(is), false);
|
||||
set_focus = true;
|
||||
}
|
||||
if (opening) {
|
||||
m_allow_close = testShift(set_mask);
|
||||
} else {
|
||||
testShift(set_mask);
|
||||
}
|
||||
|
||||
if (testShift(set_mask))
|
||||
enableEvent(ON_SUBMIT);
|
||||
if (testShift(set_mask))
|
||||
enableEvent(ON_FOCUS_CHANGE);
|
||||
|
||||
return updateElems(elem_contents, set_focus, new_focused);
|
||||
}
|
||||
|
||||
float Window::getScale() const
|
||||
{
|
||||
return g_manager.getScale(m_type);
|
||||
}
|
||||
|
||||
SizeF Window::getScreenSize() const
|
||||
{
|
||||
SizeF size = RenderingEngine::get_video_driver()->getCurrentRenderTargetSize();
|
||||
return size / getScale();
|
||||
}
|
||||
|
||||
PosF Window::getPointerPos() const
|
||||
{
|
||||
int x, y;
|
||||
SDL_GetMouseState(&x, &y);
|
||||
|
||||
return PosF(x, y) / getScale();
|
||||
}
|
||||
|
||||
SizeF Window::getTextSize(gui::IGUIFont *font, std::wstring_view text)
|
||||
{
|
||||
// If we have an empty string, we want it to take up no space. IGUIFont
|
||||
// measures the dimensions of an empty string as having the normal line
|
||||
// height rather than no space.
|
||||
if (text.empty()) {
|
||||
return SizeF();
|
||||
}
|
||||
|
||||
// IGUIFont measures the height of text with newlines incorrectly, so
|
||||
// we have to measure each line in the string individually.
|
||||
SizeF text_size = SizeF();
|
||||
size_t start = 0;
|
||||
|
||||
while (start <= text.size()) {
|
||||
// Get the line spanning from the start of this line to the next
|
||||
// newline, or the end of the string if there are no more newlines.
|
||||
size_t end = std::min(text.find(L'\n', start), text.size());
|
||||
std::wstring line(text.substr(start, end - start));
|
||||
|
||||
// Get the dimensions of the line. Since fonts are already scaled,
|
||||
// we have to reverse the scaling factor to get the right size.
|
||||
SizeF line_size = SizeF(font->getDimension(
|
||||
unescape_enriched(line).c_str())) / getScale();
|
||||
|
||||
text_size.W = std::max(text_size.W, line_size.W);
|
||||
text_size.H += line_size.H;
|
||||
|
||||
// Move the start of the current line to after the end of this one.
|
||||
start = end + 1;
|
||||
}
|
||||
|
||||
return text_size;
|
||||
}
|
||||
|
||||
void Window::drawRect(RectF dst, RectF clip, video::SColor color)
|
||||
{
|
||||
if (dst.intersectWith(clip).empty() || color.getAlpha() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
core::recti scaled_clip = clip * getScale();
|
||||
|
||||
RenderingEngine::get_video_driver()->draw2DRectangle(
|
||||
color, dst * getScale(), &scaled_clip);
|
||||
}
|
||||
|
||||
void Window::drawTexture(RectF dst, RectF clip, video::ITexture *texture,
|
||||
RectF src, video::SColor tint)
|
||||
{
|
||||
if (dst.intersectWith(clip).empty() ||
|
||||
texture == nullptr || tint.getAlpha() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
core::recti scaled_clip = clip * getScale();
|
||||
video::SColor colors[] = {tint, tint, tint, tint};
|
||||
|
||||
RenderingEngine::get_video_driver()->draw2DImage(texture, dst * getScale(),
|
||||
src * DispF(getTextureSize(texture)), &scaled_clip, colors, true);
|
||||
}
|
||||
|
||||
void Window::drawText(RectF dst, RectF clip, gui::IGUIFont *font,
|
||||
std::wstring_view text, video::SColor color, video::SColor mark,
|
||||
Align align, Align valign)
|
||||
{
|
||||
if (dst.intersectWith(clip).empty() || font == nullptr || text.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We count the number of lines in the text to find the total height.
|
||||
size_t num_lines = std::count(text.begin(), text.end(), L'\n') + 1;
|
||||
|
||||
// Get the height of a single line, and use this with the vertical
|
||||
// alignment to find the vertical position of the first line.
|
||||
float height = font->getDimension(L"").Height / getScale();
|
||||
float top;
|
||||
|
||||
switch (valign) {
|
||||
case Align::START:
|
||||
top = dst.T;
|
||||
break;
|
||||
case Align::CENTER:
|
||||
top = (dst.T + dst.B - (height * num_lines)) / 2.0f;
|
||||
break;
|
||||
case Align::END:
|
||||
top = dst.B - (height * num_lines);
|
||||
break;
|
||||
}
|
||||
|
||||
core::recti scaled_clip = clip * getScale();
|
||||
|
||||
// Like getTextSize(), we loop over each line in the string.
|
||||
size_t start = 0;
|
||||
|
||||
while (start <= text.size()) {
|
||||
size_t end = std::min(text.find(L'\n', start), text.size());
|
||||
std::wstring line(text.substr(start, end - start));
|
||||
|
||||
// Get the width of this line of text. Just like the height, we use
|
||||
// the alignment to find the horizontal position for this line.
|
||||
float width = font->getDimension(
|
||||
unescape_enriched(line).c_str()).Width / getScale();
|
||||
float left;
|
||||
|
||||
switch (align) {
|
||||
case Align::START:
|
||||
left = dst.L;
|
||||
break;
|
||||
case Align::CENTER:
|
||||
left = (dst.L + dst.R - width) / 2.0f;
|
||||
break;
|
||||
case Align::END:
|
||||
left = dst.R - width;
|
||||
break;
|
||||
}
|
||||
|
||||
// This gives us the destination rect for this line of the text,
|
||||
// which we scale appropriately.
|
||||
RectF line_rect = RectF(PosF(left, top), SizeF(width, height)) * getScale();
|
||||
|
||||
// If we have a highlight color for the text, draw the highlight
|
||||
// before we draw the line.
|
||||
if (mark.getAlpha() != 0) {
|
||||
RenderingEngine::get_video_driver()->draw2DRectangle(
|
||||
mark, line_rect, &scaled_clip);
|
||||
}
|
||||
|
||||
// Then draw the text itself using the provided font.
|
||||
font->draw(line.c_str(), line_rect, color, false, false, &scaled_clip);
|
||||
|
||||
// Finally, advance to the next line.
|
||||
top += height;
|
||||
start = end + 1;
|
||||
}
|
||||
}
|
||||
|
||||
void Window::drawAll()
|
||||
{
|
||||
Box &backdrop = m_root_elem->getBackdrop();
|
||||
|
||||
// Since the elements, screen size, pixel size, or style properties
|
||||
// might have changed since the last frame, we need to recompute stuff
|
||||
// before drawing: restyle all the boxes, recompute the base sizes from
|
||||
// the leaves to the root, and then layout each element in the element
|
||||
// tree from the root to the leaves.
|
||||
backdrop.restyle();
|
||||
backdrop.resize();
|
||||
|
||||
RectF layout_rect(getScreenSize());
|
||||
backdrop.relayout(layout_rect, layout_rect);
|
||||
|
||||
// Find the current hovered element, which will be nothing if we're not
|
||||
// focused. This may have changed between draw calls due to window
|
||||
// resizes or element layouting.
|
||||
hoverPointedElem();
|
||||
|
||||
// If this window isn't focused, tell the currently focused element.
|
||||
if (!isFocused()) {
|
||||
SDL_Event notice = createUiEvent(UI_FOCUS_SUBVERTED);
|
||||
sendTreeInput(m_focused_elem, notice, true);
|
||||
}
|
||||
|
||||
// Draw all of the newly layouted and updated elements.
|
||||
backdrop.draw();
|
||||
}
|
||||
|
||||
bool Window::isFocused() const
|
||||
{
|
||||
return g_manager.isFocused() && g_manager.getFocused() == this;
|
||||
}
|
||||
|
||||
bool Window::processInput(const SDL_Event &event)
|
||||
{
|
||||
switch (event.type) {
|
||||
case SDL_KEYDOWN:
|
||||
case SDL_KEYUP: {
|
||||
// Send the keypresses off to the focused element for processing.
|
||||
// The hovered element never gets keypresses.
|
||||
if (sendFocusedInput(event) != nullptr) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.type == SDL_KEYDOWN) {
|
||||
u16 mod = event.key.keysym.mod;
|
||||
|
||||
switch (event.key.keysym.sym) {
|
||||
case SDLK_ESCAPE:
|
||||
// If we got an Escape keypress, close the window.
|
||||
destroyWindow();
|
||||
return true;
|
||||
|
||||
case SDLK_RETURN:
|
||||
case SDLK_RETURN2:
|
||||
case SDLK_KP_ENTER:
|
||||
// If the Enter key was pressed but not handled by any
|
||||
// elements, send a submit event to the server.
|
||||
if (testEvent(ON_SUBMIT)) {
|
||||
g_manager.sendMessage(createEvent(ON_SUBMIT).str());
|
||||
}
|
||||
return true;
|
||||
|
||||
case SDLK_TAB:
|
||||
case SDLK_KP_TAB:
|
||||
// If we got a Tab key press, but not a Ctrl + Tab (which
|
||||
// is reserved for use by elements), focus the next
|
||||
// element, or the previous element if Shift is pressed.
|
||||
if (!(mod & KMOD_CTRL)) {
|
||||
focusNextElem(mod & KMOD_SHIFT);
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
case SDL_MOUSEMOTION:
|
||||
case SDL_MOUSEBUTTONDOWN:
|
||||
case SDL_MOUSEBUTTONUP:
|
||||
case SDL_MOUSEWHEEL: {
|
||||
// Make sure that we have an updated hovered element so that the
|
||||
// hovered element is the one that receives the mouse motion event.
|
||||
if (event.type == SDL_MOUSEMOTION) {
|
||||
hoverPointedElem();
|
||||
}
|
||||
|
||||
// If we just clicked with the left mouse button, see if there's
|
||||
// any element at that position to focus.
|
||||
if (event.type == SDL_MOUSEBUTTONDOWN &&
|
||||
event.button.button == SDL_BUTTON_LEFT) {
|
||||
if (isPointerOutside()) {
|
||||
changeFocusedElem(nullptr, true);
|
||||
} else {
|
||||
focusPointedElem();
|
||||
}
|
||||
}
|
||||
|
||||
// First, give the focused element a chance to see the mouse event,
|
||||
// so it can e.g. unpress a button if a mouse button was released.
|
||||
if (sendFocusedInput(event) != nullptr) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Then, send the mouse input to the hovered element.
|
||||
if (sendPointedInput(event) != nullptr) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void Window::enableEvent(u32 event)
|
||||
{
|
||||
m_events |= (1 << event);
|
||||
}
|
||||
|
||||
bool Window::testEvent(u32 event) const
|
||||
{
|
||||
return m_events & (1 << event);
|
||||
}
|
||||
|
||||
std::ostringstream Window::createEvent(u32 event) const
|
||||
{
|
||||
auto os = newOs();
|
||||
|
||||
writeU8(os, Manager::WINDOW_EVENT);
|
||||
writeU64(os, m_id);
|
||||
writeU8(os, event);
|
||||
|
||||
return os;
|
||||
}
|
||||
|
||||
void Window::destroyWindow()
|
||||
{
|
||||
if (m_allow_close) {
|
||||
// Always send the close event so the server can update its
|
||||
// internal window tables, even if there's no on_close() handler.
|
||||
g_manager.sendMessage(createEvent(ON_CLOSE).str());
|
||||
|
||||
// This causes the window object to be destroyed. Do not run any
|
||||
// code after this!
|
||||
g_manager.removeWindow(m_id);
|
||||
}
|
||||
}
|
||||
|
||||
Elem *Window::sendTreeInput(Elem *elem, const SDL_Event &event, bool direct)
|
||||
{
|
||||
// Give the event to the element and all its parents for processing.
|
||||
while (elem != nullptr) {
|
||||
bool handled = elem->processInput(event);
|
||||
|
||||
if (handled) {
|
||||
// If we handled the event, return the element that handled it.
|
||||
return elem;
|
||||
} else if (direct) {
|
||||
// If this event is only intended directly for this element and
|
||||
// it didn't handle the event, then we're done.
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Otherwise, give the parent a chance to handle it.
|
||||
elem = elem->getParent();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Elem *Window::sendPointedInput(const SDL_Event &event)
|
||||
{
|
||||
// We want to get the topmost hovered element, so we have to iterate in
|
||||
// reverse draw order and check each element the mouse is inside.
|
||||
for (size_t i = m_ordered_elems.size(); i > 0; i--) {
|
||||
Elem *elem = m_ordered_elems[i - 1];
|
||||
if (elem->getMain().isContentPointed() && elem->processInput(event)) {
|
||||
return elem;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Elem *Window::sendFocusedInput(const SDL_Event &event)
|
||||
{
|
||||
if (m_focused_elem == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Send the event to the focused element and its parents.
|
||||
Elem *handled = sendTreeInput(m_focused_elem, event, false);
|
||||
|
||||
// If one of the focused element's parents handled the event, let the
|
||||
// focused element know that focus was subverted.
|
||||
if (handled != nullptr && handled != m_focused_elem) {
|
||||
SDL_Event notice = createUiEvent(UI_FOCUS_SUBVERTED);
|
||||
sendTreeInput(m_focused_elem, notice, true);
|
||||
}
|
||||
|
||||
return handled;
|
||||
}
|
||||
|
||||
void Window::changeFocusedElem(Elem *new_focused, bool send_event)
|
||||
{
|
||||
// If the same element is being focused, do nothing.
|
||||
if (new_focused == m_focused_elem) {
|
||||
return;
|
||||
}
|
||||
|
||||
Elem *old_focused = m_focused_elem;
|
||||
m_focused_elem = new_focused;
|
||||
|
||||
// Let the old and new focused elements know that things have
|
||||
// changed, and their parent elements too.
|
||||
SDL_Event notice = createUiEvent(UI_FOCUS_CHANGED, old_focused, new_focused);
|
||||
|
||||
sendTreeInput(old_focused, notice, false);
|
||||
sendTreeInput(new_focused, notice, false);
|
||||
|
||||
// If the server wants to know when focus changes, send it an event.
|
||||
if (send_event && testEvent(ON_FOCUS_CHANGE)) {
|
||||
auto os = createEvent(ON_FOCUS_CHANGE);
|
||||
|
||||
// If either the old or the new element was unfocused, send an
|
||||
// empty string. Otherwise, send the ID of the element.
|
||||
writeNullStr(os, old_focused == nullptr ? "" : old_focused->getId());
|
||||
writeNullStr(os, new_focused == nullptr ? "" : new_focused->getId());
|
||||
|
||||
g_manager.sendMessage(os.str());
|
||||
}
|
||||
}
|
||||
|
||||
bool Window::requestFocusedElem(Elem *new_focused, bool send_event)
|
||||
{
|
||||
// If this element is already focused, we don't need to do anything.
|
||||
if (new_focused == m_focused_elem) {
|
||||
return m_focused_elem;
|
||||
}
|
||||
|
||||
SDL_Event notice = createUiEvent(UI_FOCUS_REQUEST);
|
||||
|
||||
// Ask the new element if it can take user focus. If it can, make it
|
||||
// the focused element.
|
||||
if (sendTreeInput(new_focused, notice, true) == new_focused) {
|
||||
changeFocusedElem(new_focused, send_event);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Window::focusNextElem(bool reverse)
|
||||
{
|
||||
// Start tabbing from the focused element if there is one, or the root
|
||||
// element otherwise.
|
||||
Elem *start = m_focused_elem != nullptr ? m_focused_elem : m_root_elem;
|
||||
|
||||
// Loop through all the elements in order (not including the starting
|
||||
// element), trying to focus them, until we reach the place we started
|
||||
// again, which means that no element wanted to be focused.
|
||||
Elem *next = getNextElem(start, reverse);
|
||||
|
||||
while (next != start) {
|
||||
if (requestFocusedElem(next, true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
next = getNextElem(next, reverse);
|
||||
}
|
||||
}
|
||||
|
||||
void Window::focusPointedElem()
|
||||
{
|
||||
SDL_Event notice = createUiEvent(UI_FOCUS_REQUEST);
|
||||
|
||||
// Ask all elements that the mouse just clicked on if they want to
|
||||
// be the focused element.
|
||||
Elem *new_focused = sendPointedInput(notice);
|
||||
|
||||
// If an element responded to the request that is different from the
|
||||
// currently focused element, then update the focused element.
|
||||
if (new_focused != nullptr && m_focused_elem != new_focused) {
|
||||
changeFocusedElem(new_focused, true);
|
||||
}
|
||||
}
|
||||
|
||||
void Window::hoverPointedElem()
|
||||
{
|
||||
SDL_Event notice = createUiEvent(UI_HOVER_REQUEST);
|
||||
|
||||
// If the window is focused, ask all elements that the mouse is
|
||||
// currently inside if they want to be the hovered element. Otherwise,
|
||||
// make no element hovered.
|
||||
Elem *old_hovered = m_hovered_elem;
|
||||
Elem *new_hovered = nullptr;
|
||||
|
||||
if (isFocused()) {
|
||||
new_hovered = sendPointedInput(notice);
|
||||
}
|
||||
|
||||
// If a different element responded to the hover request (or no element
|
||||
// at all), then update the hovered element.
|
||||
if (old_hovered != new_hovered) {
|
||||
m_hovered_elem = new_hovered;
|
||||
|
||||
// Let the old and new hovered elements know that things have
|
||||
// changed, and their parent elements too.
|
||||
notice = createUiEvent(UI_HOVER_CHANGED, old_hovered, new_hovered);
|
||||
|
||||
sendTreeInput(old_hovered, notice, false);
|
||||
sendTreeInput(new_hovered, notice, false);
|
||||
}
|
||||
}
|
||||
|
||||
bool Window::isPointerOutside() const
|
||||
{
|
||||
// If the mouse is inside any element, it's not outside the window. We
|
||||
// have to check every element, not just the root, because elements may
|
||||
// have the noclip property set. However, the backdrop is not included.
|
||||
for (Elem *elem : m_ordered_elems) {
|
||||
if (elem->getMain().isContentPointed()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
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 non-empty IDs since that indicates
|
||||
// a nonexistent element in getElem(). 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();
|
||||
}
|
||||
|
||||
bool Window::readRootElem(std::istream &is)
|
||||
{
|
||||
// Get the root element of the window and make sure it's valid.
|
||||
Elem *root = getElem(readNullStr(is), true);
|
||||
|
||||
if (root == nullptr) {
|
||||
errorstream << "Window " << m_id << " has no root element" << std::endl;
|
||||
return false;
|
||||
} else if (root->getType() != Elem::ROOT) {
|
||||
errorstream << "Window " << m_id <<
|
||||
" has wrong type for root element" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
m_root_elem = static_cast<Root *>(root);
|
||||
return true;
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
bool Window::updateElems(std::unordered_map<Elem *, std::string> &elem_contents,
|
||||
bool set_focus, Elem *new_focused)
|
||||
{
|
||||
// Now that we have a fully updated window, we can update each element
|
||||
// with its contents and set up the parent-child relations. 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(contents.second);
|
||||
contents.first->read(is);
|
||||
}
|
||||
|
||||
// Check the depth of the element tree; if it's too deep, there's
|
||||
// potential for stack overflow. We also create the list of ordered
|
||||
// elements since we're already doing a preorder traversal.
|
||||
if (!updateTree(m_root_elem, 1)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the number of elements discovered by the tree traversal is less
|
||||
// than the total number of elements, orphaned elements must exist.
|
||||
if (m_elems.size() != m_ordered_elems.size()) {
|
||||
errorstream << "Window " << m_id << " has orphaned elements" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the user wants to focus a new element or unfocus the current
|
||||
// element, remove focus from the current element and request focus on
|
||||
// the new element.
|
||||
if (set_focus && new_focused != m_focused_elem) {
|
||||
if (new_focused != nullptr) {
|
||||
if (!requestFocusedElem(new_focused, false)) {
|
||||
changeFocusedElem(nullptr, false);
|
||||
}
|
||||
} else {
|
||||
changeFocusedElem(nullptr, false);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Window::updateTree(Elem *elem, size_t depth)
|
||||
{
|
||||
// The parent gets ordered before its children since the ordering of
|
||||
// elements follows draw order.
|
||||
elem->setOrder(m_ordered_elems.size());
|
||||
m_ordered_elems.push_back(elem);
|
||||
|
||||
if (depth > MAX_TREE_DEPTH) {
|
||||
errorstream << "Window " << m_id <<
|
||||
" exceeds the max tree depth of " << 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 (!updateTree(child, depth + 1)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
148
src/ui/window.h
Normal file
148
src/ui/window.h
Normal file
|
@ -0,0 +1,148 @@
|
|||
// Luanti
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ui/elem.h"
|
||||
#include "ui/helpers.h"
|
||||
#include "util/basic_macros.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
union SDL_Event;
|
||||
|
||||
namespace ui
|
||||
{
|
||||
class Root;
|
||||
|
||||
SizeI getTextureSize(video::ITexture *texture);
|
||||
|
||||
// Serialized enum; do not change order of entries.
|
||||
enum class WindowType : u8
|
||||
{
|
||||
FILTER,
|
||||
MASK,
|
||||
HUD,
|
||||
CHAT,
|
||||
GUI,
|
||||
|
||||
MAX = GUI,
|
||||
};
|
||||
|
||||
WindowType toWindowType(u8 type);
|
||||
|
||||
class Window
|
||||
{
|
||||
private:
|
||||
// Serialized constants; do not change values of entries.
|
||||
static constexpr u32 ON_CLOSE = 0x00;
|
||||
static constexpr u32 ON_SUBMIT = 0x01;
|
||||
static constexpr u32 ON_FOCUS_CHANGE = 0x02;
|
||||
|
||||
static constexpr size_t MAX_TREE_DEPTH = 64;
|
||||
|
||||
// The ID and type are intrinsic to the box's identity, so they aren't
|
||||
// cleared in reset(). The ID is set by the constructor, whereas the
|
||||
// type is deserialized when the window is first opened.
|
||||
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;
|
||||
|
||||
Root *m_root_elem;
|
||||
|
||||
std::vector<std::string> m_style_strs;
|
||||
|
||||
Elem *m_focused_elem;
|
||||
Elem *m_hovered_elem;
|
||||
|
||||
bool m_allow_close;
|
||||
u32 m_events;
|
||||
|
||||
public:
|
||||
Window(u64 id) :
|
||||
m_id(id)
|
||||
{
|
||||
reset();
|
||||
}
|
||||
|
||||
DISABLE_CLASS_COPY(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 *getNextElem(Elem *elem, bool reverse);
|
||||
|
||||
Root *getRoot() { return m_root_elem; }
|
||||
Elem *getFocused() { return m_focused_elem; }
|
||||
Elem *getHovered() { return m_hovered_elem; }
|
||||
|
||||
void clearElem(Elem *elem);
|
||||
|
||||
const std::string *getStyleStr(u32 index) const;
|
||||
|
||||
void reset();
|
||||
bool read(std::istream &is, bool opening);
|
||||
|
||||
float getScale() const;
|
||||
|
||||
SizeF getScreenSize() const;
|
||||
PosF getPointerPos() const;
|
||||
|
||||
SizeF getTextSize(gui::IGUIFont *font, std::wstring_view text);
|
||||
|
||||
void drawRect(RectF dst, RectF clip, video::SColor color);
|
||||
void drawTexture(RectF dst, RectF clip, video::ITexture *texture,
|
||||
RectF src = RectF(0.0f, 0.0f, 1.0f, 1.0f), video::SColor tint = WHITE);
|
||||
void drawText(RectF dst, RectF clip, gui::IGUIFont *font, std::wstring_view text,
|
||||
video::SColor color = WHITE, video::SColor mark = BLANK,
|
||||
Align align = Align::START, Align valign = Align::START);
|
||||
|
||||
void drawAll();
|
||||
|
||||
bool isFocused() const;
|
||||
bool processInput(const SDL_Event &event);
|
||||
|
||||
private:
|
||||
void enableEvent(u32 event);
|
||||
bool testEvent(u32 event) const;
|
||||
|
||||
std::ostringstream createEvent(u32 event) const;
|
||||
|
||||
// Warning: This method causes the window object to be destroyed.
|
||||
// Return immediately after use, and don't use the window object again.
|
||||
void destroyWindow();
|
||||
|
||||
Elem *sendTreeInput(Elem *elem, const SDL_Event &event, bool direct);
|
||||
Elem *sendPointedInput(const SDL_Event &event);
|
||||
Elem *sendFocusedInput(const SDL_Event &event);
|
||||
|
||||
void changeFocusedElem(Elem *new_focused, bool send_event);
|
||||
bool requestFocusedElem(Elem *new_focused, bool send_event);
|
||||
|
||||
void focusNextElem(bool reverse);
|
||||
|
||||
void focusPointedElem();
|
||||
void hoverPointedElem();
|
||||
|
||||
bool isPointerOutside() const;
|
||||
|
||||
void readElems(std::istream &is,
|
||||
std::unordered_map<Elem *, std::string> &elem_contents);
|
||||
bool readRootElem(std::istream &is);
|
||||
void readStyles(std::istream &is);
|
||||
|
||||
bool updateElems(std::unordered_map<Elem *, std::string> &elem_contents,
|
||||
bool set_focus, Elem *new_focused);
|
||||
bool updateTree(Elem *elem, size_t depth);
|
||||
};
|
||||
}
|
|
@ -19,40 +19,56 @@ FloatType g_serialize_f32_type = FLOATTYPE_UNKNOWN;
|
|||
//// String
|
||||
////
|
||||
|
||||
std::string serializeString16(std::string_view plain)
|
||||
std::string serializeString16(std::string_view plain, bool truncate)
|
||||
{
|
||||
std::string s;
|
||||
char buf[2];
|
||||
size_t size = plain.size();
|
||||
|
||||
static_assert(STRING_MAX_LEN <= U16_MAX);
|
||||
if (plain.size() > STRING_MAX_LEN)
|
||||
throw SerializationError("String too long for serializeString16");
|
||||
s.reserve(2 + plain.size());
|
||||
|
||||
writeU16((u8 *)&buf[0], plain.size());
|
||||
s.append(buf, 2);
|
||||
if (size > STRING_MAX_LEN) {
|
||||
if (truncate) {
|
||||
size = STRING_MAX_LEN;
|
||||
} else {
|
||||
throw SerializationError("String too long for serializeString16");
|
||||
}
|
||||
}
|
||||
|
||||
char size_buf[2];
|
||||
writeU16((u8 *)size_buf, size);
|
||||
|
||||
s.reserve(2 + size);
|
||||
s.append(size_buf, 2);
|
||||
s.append(plain.substr(0, size));
|
||||
|
||||
s.append(plain);
|
||||
return s;
|
||||
}
|
||||
|
||||
std::string deSerializeString16(std::istream &is)
|
||||
std::string deSerializeString16(std::istream &is, bool truncate)
|
||||
{
|
||||
std::string s;
|
||||
char buf[2];
|
||||
char size_buf[2];
|
||||
|
||||
is.read(buf, 2);
|
||||
if (is.gcount() != 2)
|
||||
is.read(size_buf, 2);
|
||||
if (is.gcount() != 2) {
|
||||
if (truncate) {
|
||||
return s;
|
||||
}
|
||||
throw SerializationError("deSerializeString16: size not read");
|
||||
}
|
||||
|
||||
u16 s_size = readU16((u8 *)buf);
|
||||
if (s_size == 0)
|
||||
u16 size = readU16((u8 *)size_buf);
|
||||
if (size == 0) {
|
||||
return s;
|
||||
}
|
||||
|
||||
s.resize(s_size);
|
||||
is.read(&s[0], s_size);
|
||||
if (is.gcount() != s_size)
|
||||
s.resize(size);
|
||||
is.read(&s[0], size);
|
||||
if (truncate) {
|
||||
s.resize(is.gcount());
|
||||
} else if (is.gcount() != size) {
|
||||
throw SerializationError("deSerializeString16: truncated");
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
@ -62,45 +78,74 @@ std::string deSerializeString16(std::istream &is)
|
|||
//// Long String
|
||||
////
|
||||
|
||||
std::string serializeString32(std::string_view plain)
|
||||
std::string serializeString32(std::string_view plain, bool truncate)
|
||||
{
|
||||
std::string s;
|
||||
char buf[4];
|
||||
size_t size = plain.size();
|
||||
|
||||
static_assert(LONG_STRING_MAX_LEN <= U32_MAX);
|
||||
if (plain.size() > LONG_STRING_MAX_LEN)
|
||||
throw SerializationError("String too long for serializeLongString");
|
||||
s.reserve(4 + plain.size());
|
||||
|
||||
writeU32((u8*)&buf[0], plain.size());
|
||||
s.append(buf, 4);
|
||||
s.append(plain);
|
||||
if (size > LONG_STRING_MAX_LEN) {
|
||||
if (truncate) {
|
||||
size = LONG_STRING_MAX_LEN;
|
||||
} else {
|
||||
throw SerializationError("String too long for serializeString32");
|
||||
}
|
||||
}
|
||||
|
||||
char size_buf[4];
|
||||
writeU32((u8 *)size_buf, size);
|
||||
|
||||
s.reserve(4 + size);
|
||||
s.append(size_buf, 4);
|
||||
s.append(plain.substr(0, size));
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
std::string deSerializeString32(std::istream &is)
|
||||
std::string deSerializeString32(std::istream &is, bool truncate)
|
||||
{
|
||||
std::string s;
|
||||
char buf[4];
|
||||
char size_buf[4];
|
||||
|
||||
is.read(buf, 4);
|
||||
if (is.gcount() != 4)
|
||||
throw SerializationError("deSerializeLongString: size not read");
|
||||
|
||||
u32 s_size = readU32((u8 *)buf);
|
||||
if (s_size == 0)
|
||||
return s;
|
||||
|
||||
// We don't really want a remote attacker to force us to allocate 4GB...
|
||||
if (s_size > LONG_STRING_MAX_LEN) {
|
||||
throw SerializationError("deSerializeLongString: "
|
||||
"string too long: " + itos(s_size) + " bytes");
|
||||
is.read(size_buf, 4);
|
||||
if (is.gcount() != 4) {
|
||||
if (truncate) {
|
||||
return s;
|
||||
}
|
||||
throw SerializationError("deSerializeString32: size not read");
|
||||
}
|
||||
|
||||
s.resize(s_size);
|
||||
is.read(&s[0], s_size);
|
||||
if ((u32)is.gcount() != s_size)
|
||||
throw SerializationError("deSerializeLongString: truncated");
|
||||
u32 size = readU32((u8 *)size_buf);
|
||||
u32 ignore = 0;
|
||||
if (size == 0) {
|
||||
return s;
|
||||
}
|
||||
|
||||
if (size > LONG_STRING_MAX_LEN) {
|
||||
if (truncate) {
|
||||
ignore = size - LONG_STRING_MAX_LEN;
|
||||
size = LONG_STRING_MAX_LEN;
|
||||
} else {
|
||||
// We don't really want a remote attacker to force us to allocate 4GB...
|
||||
throw SerializationError("deSerializeString32: "
|
||||
"string too long: " + itos(size) + " bytes");
|
||||
}
|
||||
}
|
||||
|
||||
s.resize(size);
|
||||
is.read(&s[0], size);
|
||||
if (truncate) {
|
||||
s.resize(is.gcount());
|
||||
} else if (is.gcount() != size) {
|
||||
throw SerializationError("deSerializeString32: truncated");
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
|
|
@ -446,16 +446,16 @@ MAKE_STREAM_WRITE_FXN(video::SColor, ARGB8, 4);
|
|||
}
|
||||
|
||||
// Creates a string with the length as the first two bytes
|
||||
std::string serializeString16(std::string_view plain);
|
||||
std::string serializeString16(std::string_view plain, bool truncate = false);
|
||||
|
||||
// Reads a string with the length as the first two bytes
|
||||
std::string deSerializeString16(std::istream &is);
|
||||
std::string deSerializeString16(std::istream &is, bool truncate = false);
|
||||
|
||||
// Creates a string with the length as the first four bytes
|
||||
std::string serializeString32(std::string_view plain);
|
||||
std::string serializeString32(std::string_view plain, bool truncate = false);
|
||||
|
||||
// Reads a string with the length as the first four bytes
|
||||
std::string deSerializeString32(std::istream &is);
|
||||
std::string deSerializeString32(std::istream &is, bool truncate = false);
|
||||
|
||||
// Creates a string encoded in JSON format (almost equivalent to a C string literal)
|
||||
std::string serializeJsonString(std::string_view plain);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue