minetest/builtin/ui/window.lua

309 lines
7.9 KiB
Lua

--[[
Minetest
Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
--]]
local open_windows = {}
local function build_window(id, param)
local info = open_windows[id]
if not info then
return nil, nil
end
local window = info.builder(id, info.player, info.context, param or {})
assert(core.is_instance(window, ui.Window),
"Expected ui.Window to be returned from builder function")
assert(not window._id, "Window object has already been returned")
window._id = id
info.window = window
return window, info.player
end
ui.Window = core.class()
ui._window_types = {
bg = 0,
mask = 1,
hud = 2,
message = 3,
gui = 4,
fg = 5,
}
function ui.Window:new(props)
self._id = nil -- Set by build_window()
self._type = props.type
self._theme = props.theme or ui.get_default_theme()
self._style = props.style or ui.Style{}
self._root = props.root
assert(ui._window_types[self._type], "Invalid window type")
assert(core.is_instance(self._root, ui.Root),
"Expected root of window to be ui.Root")
self._elems = self._root:_get_flat()
self._elems_by_id = {}
for _, elem in ipairs(self._elems) do
local id = elem._id
assert(not self._elems_by_id[id], "Element has duplicate ID: '" .. id .. "'")
self._elems_by_id[id] = elem
assert(elem._window == nil, "Element already has window")
elem._window = self
end
end
function ui.Window:_encode(player, opening)
local enc_styles = self:_encode_styles()
local enc_elems = self:_encode_elems()
local data = ui._encode("ZzZ", enc_elems, self._root._id, enc_styles)
if opening then
data = ui._encode("ZB", data, ui._window_types[self._type])
end
return data
end
function ui.Window:_encode_styles()
-- Clear out all the boxes in every element.
for _, elem in ipairs(self._elems) do
for box in pairs(elem._boxes) do
elem._boxes[box] = {n = 0}
end
end
-- Get a cascaded and flattened list of all the styles for this window.
local styles = self:_get_full_style():_get_flat()
-- Take each style and apply its properties to every box and state matched
-- by its selector.
self:_apply_styles(styles)
-- Take the styled boxes and encode their styles into a single table,
-- replacing the boxes' style property tables with indices into this table.
local enc_styles = self:_index_styles()
return ui._encode_array("Z", enc_styles)
end
function ui.Window:_get_full_style()
-- The full style contains the theme, global style, and inline element
-- styles as sub-styles, in that order, to ensure the correct precedence.
local styles = {self._theme, self._style}
for _, elem in ipairs(self._elems) do
-- Cascade the inline style with the element's ID, ensuring that the
-- inline style globally refers to this element only.
table.insert(styles, ui.Style{
sel = "#" .. elem._id,
nested = {elem._style},
})
end
-- Return all these styles wrapped up into a single style.
return ui.Style{
nested = styles,
}
end
local function apply_style(elem, boxes, style)
-- Loop through each box, applying the styles accordingly. The table of
-- boxes may be empty, in which case nothing happens.
for _, box in pairs(boxes) do
local name = box.name or "main"
-- If this style resets all properties, find all states that are a
-- subset of the state being styled and clear their property tables.
if style._reset then
for i = ui._STATE_NONE, ui._NUM_STATES - 1 do
if bit.band(box.states, i) == box.states then
elem._boxes[name][i] = nil
end
end
end
-- Get the existing style property table for this box if it exists.
local props = elem._boxes[name][box.states] or {}
-- Cascade the properties from this style onto the box.
elem._boxes[name][box.states] = ui._cascade_props(style._props, props)
end
end
function ui.Window:_apply_styles(styles)
-- Loop through each style and element and see if the style properties can
-- be applied to any boxes.
for _, style in ipairs(styles) do
for _, elem in ipairs(self._elems) do
-- Check if the selector for this style. If it matches, apply the
-- style to each of the applicable boxes.
local matches, boxes = style._sel(elem)
if matches then
apply_style(elem, boxes, style)
end
end
end
end
local function index_style(box, i, style_indices, enc_styles)
-- If we have a style for this state, serialize it to a string. Identical
-- styles have identical strings, so we use this to our advantage.
local enc = ui._encode_props(box[i])
-- If we haven't serialized a style identical to this one before, store
-- this as the latest index in the list of style strings.
if not style_indices[enc] then
style_indices[enc] = #enc_styles
table.insert(enc_styles, enc)
end
-- Set the index of our state to the index of its style string, and keep
-- count of how many states with valid indices we have for this box so far.
box[i] = style_indices[enc]
box.n = box.n + 1
end
function ui.Window:_index_styles()
local style_indices = {}
local enc_styles = {}
for _, elem in ipairs(self._elems) do
for _, box in pairs(elem._boxes) do
for i = ui._STATE_NONE, ui._NUM_STATES - 1 do
if box[i] then
-- If this box has a style, encode and index it.
index_style(box, i, style_indices, enc_styles)
else
-- Otherwise, this state has no style, so set it as such.
box[i] = ui._NO_STYLE
end
end
end
end
return enc_styles
end
function ui.Window:_encode_elems()
local enc_elems = {}
for _, elem in ipairs(self._elems) do
table.insert(enc_elems, elem:_encode())
end
return ui._encode_array("Z", enc_elems)
end
local OPEN_WINDOW = 0x00
local REOPEN_WINDOW = 0x01
local UPDATE_WINDOW = 0x02
local CLOSE_WINDOW = 0x03
local last_id = 0
function ui.open(builder, player, context, param)
local id = last_id
last_id = last_id + 1
open_windows[id] = {
builder = builder,
player = player,
context = context or {},
window = nil, -- Set by build_window()
}
local window = build_window(id, param)
local data = ui._encode("BL Z", OPEN_WINDOW, id,
window:_encode(player, true))
core.send_ui_message(player, data)
return id
end
function ui.reopen(close_id, param)
local new_id = last_id
last_id = last_id + 1
open_windows[new_id] = open_windows[close_id]
open_windows[close_id] = nil
local window, player = build_window(new_id, param)
if not window then
return nil
end
local data = ui._encode("BLL Z", REOPEN_WINDOW, new_id, close_id,
window:_encode(player, true))
core.send_ui_message(player, data)
return new_id
end
function ui.update(id, param)
local window, player = build_window(id, param)
if not window then
return
end
local data = ui._encode("BL Z", UPDATE_WINDOW, id,
window:_encode(player, false))
core.send_ui_message(player, data)
end
function ui.close(id)
local info = open_windows[id]
if not info then
return
end
local data = ui._encode("BL", CLOSE_WINDOW, id)
core.send_ui_message(info.player, data)
open_windows[id] = nil
end
function ui.get_window_info(id)
local info = open_windows[id]
if not info then
return nil
end
-- Only return a subset of the fields that are relevant for the caller.
return {
builder = info.builder,
player = info.player,
context = info.context,
}
end
function ui.get_open_windows()
local ids = {}
for id in pairs(open_windows) do
table.insert(ids, id)
end
return ids
end