complete ui rewrite
This commit is contained in:
parent
b34022f123
commit
017f971311
490
lib/ui/core.lua
Normal file
490
lib/ui/core.lua
Normal file
@ -0,0 +1,490 @@
|
|||||||
|
---@class UIConstraints
|
||||||
|
---@field min_w number
|
||||||
|
---@field min_h number
|
||||||
|
---@field max_w number
|
||||||
|
---@field max_h number
|
||||||
|
|
||||||
|
local ui = {}
|
||||||
|
|
||||||
|
-- =============== Constraints helpers ===============
|
||||||
|
|
||||||
|
local function make_constraints(min_w, min_h, max_w, max_h)
|
||||||
|
return {
|
||||||
|
min_w = min_w or 0,
|
||||||
|
min_h = min_h or 0,
|
||||||
|
max_w = max_w or math.huge,
|
||||||
|
max_h = max_h or math.huge,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function clamp(v, lo, hi)
|
||||||
|
if v < lo then return lo end
|
||||||
|
if v > hi then return hi end
|
||||||
|
return v
|
||||||
|
end
|
||||||
|
|
||||||
|
local function clamp_size(w, h, c)
|
||||||
|
return clamp(w, c.min_w, c.max_w), clamp(h, c.min_h, c.max_h)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function loosen(c)
|
||||||
|
-- "loose" constraints (0..max), удобно для wrap-контейнеров/Align
|
||||||
|
return make_constraints(0, 0, c.max_w, c.max_h)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function tighten(c, w, h)
|
||||||
|
-- exact (tight) constraints — зажимают размер ребёнка
|
||||||
|
w, h = clamp_size(w, h, c)
|
||||||
|
return make_constraints(w, h, w, h)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- =============== Base elements ===============
|
||||||
|
|
||||||
|
--- @class Element
|
||||||
|
--- @field parent Element|nil
|
||||||
|
--- @field children Element[]
|
||||||
|
--- @field child Element|nil
|
||||||
|
--- @field origin Vec3
|
||||||
|
--- @field size Vec3
|
||||||
|
--- @field state table
|
||||||
|
local Element = {}
|
||||||
|
Element.__index = Element
|
||||||
|
|
||||||
|
function Element:new(props)
|
||||||
|
local o = setmetatable({}, self)
|
||||||
|
o.parent = nil
|
||||||
|
o.children = {}
|
||||||
|
o.origin = Vec3 { 0, 0, 0 }
|
||||||
|
o.size = Vec3 { 0, 0, 0 }
|
||||||
|
o.state = {}
|
||||||
|
if props then
|
||||||
|
-- Копируем "публичные" поля; Таблицы клонируем, если можно
|
||||||
|
for k, v in pairs(props) do
|
||||||
|
if type(v) == "table" then
|
||||||
|
o[k] = v.copy and v:copy() or v
|
||||||
|
else
|
||||||
|
o[k] = v
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return o
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param dt number
|
||||||
|
function Element:update(dt)
|
||||||
|
-- По умолчанию спускаем update вниз
|
||||||
|
for _, ch in ipairs(self.children) do
|
||||||
|
ch:update(dt)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param constraints UIConstraints
|
||||||
|
function Element:layout(constraints)
|
||||||
|
-- Базовое поведение: размер = объединённый bbox детей (wrap-content)
|
||||||
|
local max_w, max_h = 0, 0
|
||||||
|
for _, ch in ipairs(self.children) do
|
||||||
|
ch:layout(loosen(constraints)) -- по умолчанию не заставляем детей растягиваться
|
||||||
|
max_w = math.max(max_w, ch.size.x)
|
||||||
|
max_h = math.max(max_h, ch.size.y)
|
||||||
|
end
|
||||||
|
local w, h = clamp_size(max_w, max_h, constraints)
|
||||||
|
self.size.x, self.size.y = w, h
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param origin Vec3
|
||||||
|
function Element:arrange(origin)
|
||||||
|
-- Базово: ставим себя, а детей кладём в (0,0) внутри нас
|
||||||
|
self.origin = Vec3 { origin.x, origin.y, origin.z or 0 }
|
||||||
|
for _, ch in ipairs(self.children) do
|
||||||
|
ch:arrange(Vec3 { self.origin.x, self.origin.y, self.origin.z })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Element:draw()
|
||||||
|
-- По умолчанию — только рисуем детей
|
||||||
|
for _, ch in ipairs(self.children) do
|
||||||
|
ch:draw()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- =============== SingleChild / MultiChild базовые ===============
|
||||||
|
|
||||||
|
--- @class SingleChildElement: Element
|
||||||
|
local SingleChildElement = setmetatable({}, { __index = Element })
|
||||||
|
SingleChildElement.__index = SingleChildElement
|
||||||
|
|
||||||
|
function SingleChildElement:new(props)
|
||||||
|
local o = Element.new(self, props)
|
||||||
|
if o.child then
|
||||||
|
o.child.parent = o
|
||||||
|
o.children = { o.child }
|
||||||
|
end
|
||||||
|
return o
|
||||||
|
end
|
||||||
|
|
||||||
|
---@class MultiChildElement: Element
|
||||||
|
local MultiChildElement = setmetatable({}, { __index = Element })
|
||||||
|
MultiChildElement.__index = MultiChildElement
|
||||||
|
|
||||||
|
function MultiChildElement:new(props)
|
||||||
|
local o = Element.new(self, props)
|
||||||
|
o.children = o.children or o.children or {}
|
||||||
|
o.children = o.children -- ensure array
|
||||||
|
if props and props.children then
|
||||||
|
for _, ch in ipairs(props.children) do
|
||||||
|
ch.parent = o
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return o
|
||||||
|
end
|
||||||
|
|
||||||
|
-- =============== Root ===============
|
||||||
|
|
||||||
|
---@class Root: SingleChildElement
|
||||||
|
local Root = setmetatable({}, { __index = SingleChildElement })
|
||||||
|
Root.__index = Root
|
||||||
|
|
||||||
|
function Root:new(props)
|
||||||
|
return SingleChildElement.new(self, props)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Root:update(dt)
|
||||||
|
-- Root может делать глобальные обновления, но главное — спустить вниз
|
||||||
|
if self.child then self.child:update(dt) end
|
||||||
|
|
||||||
|
-- На каждом кадре делаем полный проход раскладки:
|
||||||
|
local w, h = love.graphics.getWidth(), love.graphics.getHeight()
|
||||||
|
self.size = Vec3 { w, h, 0 }
|
||||||
|
local constraints = make_constraints(w, h, w, h) -- tight: размер окна
|
||||||
|
if self.child then
|
||||||
|
-- layout с точными ограничениями родителя
|
||||||
|
self.child:layout(loosen(constraints)) -- разрешаем контенту быть меньше окна
|
||||||
|
-- arrange кладём ребёнка в начало (0,0), а Align/др. уже выровняет внутри себя
|
||||||
|
self.child:arrange(Vec3 { 0, 0, 0 })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Root:draw()
|
||||||
|
if self.child then self.child:draw() end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- =============== Rectangle (leaf) ===============
|
||||||
|
|
||||||
|
---@class Rectangle: Element
|
||||||
|
---@field color number[] @ {r,g,b} 0..1
|
||||||
|
---@field size Vec3
|
||||||
|
local Rectangle = setmetatable({}, { __index = Element })
|
||||||
|
Rectangle.__index = Rectangle
|
||||||
|
|
||||||
|
function Rectangle:new(props)
|
||||||
|
local o = Element.new(self, props)
|
||||||
|
--- @cast o Rectangle
|
||||||
|
o.color = o.color or { 1, 1, 1 }
|
||||||
|
-- size должен быть задан; если нет — size = 0,0
|
||||||
|
o.size = o.size or Vec3 { 0, 0, 0 }
|
||||||
|
return o
|
||||||
|
end
|
||||||
|
|
||||||
|
function Rectangle:layout(constraints)
|
||||||
|
local w = self.size.x or 0
|
||||||
|
local h = self.size.y or 0
|
||||||
|
w, h = clamp_size(w, h, constraints)
|
||||||
|
self.size.x, self.size.y = w, h
|
||||||
|
end
|
||||||
|
|
||||||
|
function Rectangle:draw()
|
||||||
|
love.graphics.setColor(self.color[1], self.color[2], self.color[3], 1)
|
||||||
|
love.graphics.rectangle("fill", self.origin.x, self.origin.y, self.size.x, self.size.y)
|
||||||
|
-- Сброс цвета — по вкусу
|
||||||
|
end
|
||||||
|
|
||||||
|
-- =============== Align (single child) ===============
|
||||||
|
|
||||||
|
---@class Align: SingleChildElement
|
||||||
|
---@field alignment string "center"|"top_left"|"top_right"|"bottom_left"|"bottom_right"
|
||||||
|
---@field expand boolean если true — занимает все доступное от родителя
|
||||||
|
local Align = setmetatable({}, { __index = SingleChildElement })
|
||||||
|
Align.__index = Align
|
||||||
|
|
||||||
|
function Align:new(props)
|
||||||
|
local o = SingleChildElement.new(self, props)
|
||||||
|
---@cast o Align
|
||||||
|
o.alignment = o.alignment or "center"
|
||||||
|
o.expand = (o.expand ~= nil) and o.expand or true -- по умолчанию растягиваемся под родителя
|
||||||
|
return o
|
||||||
|
end
|
||||||
|
|
||||||
|
function Align:layout(constraints)
|
||||||
|
if self.child then
|
||||||
|
-- Ребёнка считаем "loose" — пусть занимает сколько хочет
|
||||||
|
self.child:layout(loosen(constraints))
|
||||||
|
if self.expand then
|
||||||
|
-- Сам Align займет максимум
|
||||||
|
self.size.x, self.size.y = clamp_size(constraints.max_w, constraints.max_h, constraints)
|
||||||
|
else
|
||||||
|
-- Или же wrap по ребёнку
|
||||||
|
local w, h = self.child.size.x, self.child.size.y
|
||||||
|
self.size.x, self.size.y = clamp_size(w, h, constraints)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
self.size.x, self.size.y = clamp_size(0, 0, constraints)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Align:arrange(origin)
|
||||||
|
self.origin = Vec3 { origin.x, origin.y, origin.z or 0 }
|
||||||
|
if not self.child then return end
|
||||||
|
|
||||||
|
local pw, ph = self.size.x, self.size.y
|
||||||
|
local cw, ch = self.child.size.x, self.child.size.y
|
||||||
|
local x, y = self.origin.x, self.origin.y
|
||||||
|
|
||||||
|
if self.alignment == "center" then
|
||||||
|
x = x + (pw - cw) / 2
|
||||||
|
y = y + (ph - ch) / 2
|
||||||
|
elseif self.alignment == "center_left" then
|
||||||
|
y = y + (ph - ch) / 2
|
||||||
|
elseif self.alignment == "center_right" then
|
||||||
|
x = x + (pw - cw)
|
||||||
|
y = y + (ph - ch) / 2
|
||||||
|
elseif self.alignment == "top_left" then
|
||||||
|
-- x,y остаются
|
||||||
|
elseif self.alignment == "top_right" then
|
||||||
|
x = x + (pw - cw)
|
||||||
|
elseif self.alignment == "bottom_left" then
|
||||||
|
y = y + (ph - ch)
|
||||||
|
elseif self.alignment == "bottom_right" then
|
||||||
|
x = x + (pw - cw)
|
||||||
|
y = y + (ph - ch)
|
||||||
|
else
|
||||||
|
-- неизвестное — по центру
|
||||||
|
x = x + (pw - cw) / 2
|
||||||
|
y = y + (ph - ch) / 2
|
||||||
|
end
|
||||||
|
|
||||||
|
self.child:arrange(Vec3 { x, y, self.origin.z })
|
||||||
|
end
|
||||||
|
|
||||||
|
-- =============== Row / Column (multi) ===============
|
||||||
|
|
||||||
|
-- helper: прочитать flex у ребёнка (если есть)
|
||||||
|
local function get_flex_of(ch)
|
||||||
|
return (ch.get_flex and ch:get_flex()) or 0
|
||||||
|
end
|
||||||
|
|
||||||
|
---@class Row: MultiChildElement
|
||||||
|
---@field gap number расстояние между детьми
|
||||||
|
local Row = setmetatable({}, { __index = MultiChildElement })
|
||||||
|
Row.__index = Row
|
||||||
|
|
||||||
|
function Row:new(props)
|
||||||
|
local o = MultiChildElement.new(self, props)
|
||||||
|
---@cast o Row
|
||||||
|
o.gap = o.gap or 0
|
||||||
|
return o
|
||||||
|
end
|
||||||
|
|
||||||
|
function Row:layout(constraints)
|
||||||
|
local total_gap = (#self.children > 1) and (self.gap * (#self.children - 1)) or 0
|
||||||
|
|
||||||
|
local fixed_w, max_h = 0, 0
|
||||||
|
local flex_children, total_flex = {}, 0
|
||||||
|
local loose = loosen(constraints)
|
||||||
|
|
||||||
|
-- проход 1: меряем нефлекс-детей, собираем флекс-список
|
||||||
|
for _, ch in ipairs(self.children) do
|
||||||
|
local f = get_flex_of(ch)
|
||||||
|
if f > 0 then
|
||||||
|
total_flex = total_flex + f
|
||||||
|
flex_children[#flex_children + 1] = { node = ch, flex = f }
|
||||||
|
else
|
||||||
|
ch:layout(loose)
|
||||||
|
fixed_w = fixed_w + ch.size.x
|
||||||
|
if ch.size.y > max_h then max_h = ch.size.y end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local min_row_w = fixed_w + total_gap
|
||||||
|
local wants_expand = total_flex > 0
|
||||||
|
|
||||||
|
-- КЛЮЧ: если есть флекс → растягиваемся до доступного max_w
|
||||||
|
local candidate = wants_expand and constraints.max_w or min_row_w
|
||||||
|
local target_w = clamp(candidate, math.max(min_row_w, constraints.min_w), constraints.max_w)
|
||||||
|
local remaining = math.max(0, target_w - min_row_w)
|
||||||
|
|
||||||
|
-- проход 2: раздаём остаток флекс-детям (tight по ширине)
|
||||||
|
if total_flex > 0 and remaining > 0 then
|
||||||
|
local acc = 0
|
||||||
|
for i, item in ipairs(flex_children) do
|
||||||
|
local alloc = (i == #flex_children)
|
||||||
|
and (remaining - acc) -- последний добирает остаток для суммирования в точности в target_w
|
||||||
|
or (remaining * item.flex / total_flex)
|
||||||
|
acc = acc + alloc
|
||||||
|
local c = make_constraints(alloc, 0, alloc, loose.max_h)
|
||||||
|
item.node:layout(c)
|
||||||
|
if item.node.size.y > max_h then max_h = item.node.size.y end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
-- даже если remaining==0, флекс-детей всё равно надо промерить (tight нулевой шириной)
|
||||||
|
for _, item in ipairs(flex_children) do
|
||||||
|
local c = make_constraints(0, 0, 0, loose.max_h)
|
||||||
|
item.node:layout(c)
|
||||||
|
if item.node.size.y > max_h then max_h = item.node.size.y end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
self.size.x, self.size.y = target_w, clamp(max_h, constraints.min_h, constraints.max_h)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Row:arrange(origin)
|
||||||
|
self.origin = Vec3 { origin.x, origin.y, origin.z or 0 }
|
||||||
|
local cursor_x = self.origin.x
|
||||||
|
local y_base = self.origin.y
|
||||||
|
|
||||||
|
for i, ch in ipairs(self.children) do
|
||||||
|
-- Вертикальное выравнивание: по базовой линии (top). Можно прокачать до "center" / "bottom".
|
||||||
|
ch:arrange(Vec3 { cursor_x, y_base, self.origin.z })
|
||||||
|
cursor_x = cursor_x + ch.size.x + (i < #self.children and self.gap or 0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@class Column: MultiChildElement
|
||||||
|
---@field gap number
|
||||||
|
local Column = setmetatable({}, { __index = MultiChildElement })
|
||||||
|
Column.__index = Column
|
||||||
|
|
||||||
|
function Column:new(props)
|
||||||
|
local o = MultiChildElement.new(self, props)
|
||||||
|
--- @cast o Column
|
||||||
|
o.gap = o.gap or 0
|
||||||
|
return o
|
||||||
|
end
|
||||||
|
|
||||||
|
function Column:layout(constraints)
|
||||||
|
local total_gap = (#self.children > 1) and (self.gap * (#self.children - 1)) or 0
|
||||||
|
|
||||||
|
local fixed_h, max_w = 0, 0
|
||||||
|
local flex_children, total_flex = {}, 0
|
||||||
|
local loose = loosen(constraints)
|
||||||
|
|
||||||
|
for _, ch in ipairs(self.children) do
|
||||||
|
local f = get_flex_of(ch)
|
||||||
|
if f > 0 then
|
||||||
|
total_flex = total_flex + f
|
||||||
|
flex_children[#flex_children + 1] = { node = ch, flex = f }
|
||||||
|
else
|
||||||
|
ch:layout(loose)
|
||||||
|
fixed_h = fixed_h + ch.size.y
|
||||||
|
if ch.size.x > max_w then max_w = ch.size.x end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local min_col_h = fixed_h + total_gap
|
||||||
|
local wants_expand = (total_flex > 0)
|
||||||
|
local candidate = wants_expand and constraints.max_h or min_col_h
|
||||||
|
local target_h = clamp(candidate, math.max(min_col_h, constraints.min_h), constraints.max_h)
|
||||||
|
local remaining = math.max(0, target_h - min_col_h)
|
||||||
|
|
||||||
|
if total_flex > 0 and remaining > 0 then
|
||||||
|
local acc = 0
|
||||||
|
for i, item in ipairs(flex_children) do
|
||||||
|
local alloc = (i == #flex_children)
|
||||||
|
and (remaining - acc)
|
||||||
|
or (remaining * item.flex / total_flex)
|
||||||
|
acc = acc + alloc
|
||||||
|
local c = make_constraints(0, alloc, loose.max_w, alloc)
|
||||||
|
item.node:layout(c)
|
||||||
|
if item.node.size.x > max_w then max_w = item.node.size.x end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
for _, item in ipairs(flex_children) do
|
||||||
|
local c = make_constraints(0, 0, loose.max_w, 0)
|
||||||
|
item.node:layout(c)
|
||||||
|
if item.node.size.x > max_w then max_w = item.node.size.x end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
self.size.x, self.size.y = clamp(max_w, constraints.min_w, constraints.max_w), target_h
|
||||||
|
end
|
||||||
|
|
||||||
|
function Column:arrange(origin)
|
||||||
|
self.origin = Vec3 { origin.x, origin.y, origin.z or 0 }
|
||||||
|
local x_base = self.origin.x
|
||||||
|
local cursor_y = self.origin.y
|
||||||
|
|
||||||
|
for i, ch in ipairs(self.children) do
|
||||||
|
ch:arrange(Vec3 { x_base, cursor_y, self.origin.z })
|
||||||
|
cursor_y = cursor_y + ch.size.y + (i < #self.children and self.gap or 0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-----------------------------------------------------------------
|
||||||
|
|
||||||
|
---@class Expanded: SingleChildElement
|
||||||
|
---@field flex integer -- коэффициент флекса (>=1)
|
||||||
|
local Expanded = setmetatable({}, { __index = SingleChildElement })
|
||||||
|
Expanded.__index = Expanded
|
||||||
|
|
||||||
|
---@param props { flex?: integer, child?: Element }|nil
|
||||||
|
---@return Expanded
|
||||||
|
function Expanded:new(props)
|
||||||
|
local o = SingleChildElement.new(self, props)
|
||||||
|
---@cast o Expanded
|
||||||
|
o.flex = (o.flex and math.max(1, math.floor(o.flex))) or 1
|
||||||
|
return o
|
||||||
|
end
|
||||||
|
|
||||||
|
function Expanded:get_flex()
|
||||||
|
return self.flex
|
||||||
|
end
|
||||||
|
|
||||||
|
function Expanded:layout(constraints)
|
||||||
|
if self.child then
|
||||||
|
self.child:layout(constraints)
|
||||||
|
-- Становимся размером с ребёнка с учётом ограничений
|
||||||
|
local w, h = self.child.size.x, self.child.size.y
|
||||||
|
w, h = clamp_size(w, h, constraints)
|
||||||
|
self.size.x, self.size.y = w, h
|
||||||
|
else
|
||||||
|
-- Пустой Expanded — просто занимает выделенное родителем место (можно для Spacer)
|
||||||
|
local w, h = clamp_size(0, 0, constraints)
|
||||||
|
self.size.x, self.size.y = w, h
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Expanded:arrange(origin)
|
||||||
|
self.origin = Vec3 { origin.x, origin.y, origin.z or 0 }
|
||||||
|
if self.child then
|
||||||
|
self.child:arrange(self.origin)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- =============== Public constructors (callable) ===============
|
||||||
|
|
||||||
|
local function mk_ctor(class)
|
||||||
|
-- Сохраняем существующую метатаблицу (в ней уже есть __index на родителя!)
|
||||||
|
local mt = getmetatable(class) or {}
|
||||||
|
mt.__call = function(_, props)
|
||||||
|
return class:new(props or {})
|
||||||
|
end
|
||||||
|
setmetatable(class, mt)
|
||||||
|
return class
|
||||||
|
end
|
||||||
|
|
||||||
|
ui.Element = Element
|
||||||
|
ui.Root = mk_ctor(Root)
|
||||||
|
ui.Align = mk_ctor(Align)
|
||||||
|
ui.Row = mk_ctor(Row)
|
||||||
|
ui.Column = mk_ctor(Column)
|
||||||
|
ui.Rectangle = mk_ctor(Rectangle)
|
||||||
|
ui.Expanded = mk_ctor(Expanded)
|
||||||
|
|
||||||
|
-- Экспорт вспомогательных, если пригодится
|
||||||
|
ui.constraints = {
|
||||||
|
make = make_constraints,
|
||||||
|
loosen = loosen,
|
||||||
|
tighten = tighten,
|
||||||
|
clamp_size = clamp_size,
|
||||||
|
}
|
||||||
|
|
||||||
|
return ui
|
||||||
@ -1,44 +0,0 @@
|
|||||||
require "lib.utils.vec3"
|
|
||||||
--- Stateful UI element
|
|
||||||
--- @class Element
|
|
||||||
--- @field state table
|
|
||||||
--- @field update fun(self: Element, dt: number) called each logical frame, alters self.state
|
|
||||||
--- @field draw fun(self: Element) uses self.state to draw, should not alter anything
|
|
||||||
--- @field origin Vec3
|
|
||||||
--- @field size Vec3
|
|
||||||
--- @field parent Element | nil
|
|
||||||
--- @field child Element | nil
|
|
||||||
local Element = {}
|
|
||||||
Element.__index = Element
|
|
||||||
|
|
||||||
Element.state = {}
|
|
||||||
function Element:update(dt)
|
|
||||||
local parent = self.parent
|
|
||||||
if not parent then return end
|
|
||||||
|
|
||||||
self.origin = self.origin or parent.origin
|
|
||||||
self.size = self.size or parent.size
|
|
||||||
end
|
|
||||||
|
|
||||||
function Element:draw() end
|
|
||||||
|
|
||||||
--- Recursive depth-first traversal.
|
|
||||||
--- If `visit` returns false, traversal is stopped early.
|
|
||||||
--- @param visit fun(el: Element): boolean|nil
|
|
||||||
--- @return boolean
|
|
||||||
function Element:traverse(visit)
|
|
||||||
local cont = visit(self)
|
|
||||||
if not cont then return false end
|
|
||||||
if not self.child then return false end
|
|
||||||
self.child.parent = self
|
|
||||||
return not not self.child:traverse(visit)
|
|
||||||
end
|
|
||||||
|
|
||||||
--- template constructor
|
|
||||||
--- @param data {state: table, update: fun(dt: number), draw: fun(), [any]: any}
|
|
||||||
--- @return Element
|
|
||||||
function Element.new(data)
|
|
||||||
return setmetatable(data, Element)
|
|
||||||
end
|
|
||||||
|
|
||||||
return Element
|
|
||||||
@ -1,72 +0,0 @@
|
|||||||
require "lib.utils.vec3"
|
|
||||||
local baseElement = require "lib.ui.element"
|
|
||||||
|
|
||||||
local W = {}
|
|
||||||
--- @alias Alignment "topLeft" | "topCenter" | "topRight" | "centerLeft" | "center" | "centerRight" | "bottomLeft" | "bottomCenter" | "bottomRight"
|
|
||||||
--- @type {[Alignment]: Vec3}
|
|
||||||
local alignments = {
|
|
||||||
topLeft = Vec3 { 0, 0 },
|
|
||||||
topCenter = Vec3 { 0.5, 0 },
|
|
||||||
topRight = Vec3 { 1, 0 },
|
|
||||||
centerLeft = Vec3 { 0, 0.5 },
|
|
||||||
center = Vec3 { 0.5, 0.5 },
|
|
||||||
centerRight = Vec3 { 1, 0.5 },
|
|
||||||
bottomLeft = Vec3 { 0, 1 },
|
|
||||||
bottomCenter = Vec3 { 0.5, 1 },
|
|
||||||
bottomRight = Vec3 { 1, 1 }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
--- @class UIRoot : Element
|
|
||||||
local Root = {}
|
|
||||||
setmetatable(Root, { __index = baseElement })
|
|
||||||
|
|
||||||
function Root.new(data)
|
|
||||||
return setmetatable(data, { __index = Root })
|
|
||||||
end
|
|
||||||
|
|
||||||
function Root:update(dt)
|
|
||||||
self.size = Vec3 { love.graphics.getWidth(), love.graphics.getHeight() }
|
|
||||||
end
|
|
||||||
|
|
||||||
W.Root = Root.new
|
|
||||||
--------------------------------------------------
|
|
||||||
|
|
||||||
--- @class Align : Element
|
|
||||||
--- @field alignment Alignment
|
|
||||||
local Align = {}
|
|
||||||
setmetatable(Align, { __index = baseElement })
|
|
||||||
|
|
||||||
function Align.new(data)
|
|
||||||
data.alignment = data.alignment or "center"
|
|
||||||
return setmetatable(data, { __index = Align })
|
|
||||||
end
|
|
||||||
|
|
||||||
function Align:update(dt)
|
|
||||||
local parent = self.parent --[[@as Element]]
|
|
||||||
local shift = alignments[self.alignment]
|
|
||||||
self.origin = Vec3 { parent.size.x * shift.x, parent.size.y * shift.y }
|
|
||||||
end
|
|
||||||
|
|
||||||
W.Align = Align.new
|
|
||||||
--------------------------------------------------
|
|
||||||
|
|
||||||
--- @class Rectangle : Element
|
|
||||||
--- @field color number[]
|
|
||||||
local Rectangle = {}
|
|
||||||
setmetatable(Rectangle, { __index = baseElement })
|
|
||||||
|
|
||||||
function Rectangle.new(data)
|
|
||||||
return setmetatable(data, { __index = Rectangle })
|
|
||||||
end
|
|
||||||
|
|
||||||
function Rectangle:draw()
|
|
||||||
love.graphics.setColor(self.color or { 1, 1, 1 })
|
|
||||||
love.graphics.rectangle("fill", self.origin.x - self.size.x / 2, self.origin.y - self.size.y / 2, self.size.x,
|
|
||||||
self.size.y)
|
|
||||||
end
|
|
||||||
|
|
||||||
W.Rectangle = Rectangle.new
|
|
||||||
---------------------------------------------------
|
|
||||||
|
|
||||||
return W
|
|
||||||
44
main.lua
44
main.lua
@ -1,6 +1,6 @@
|
|||||||
-- CameraLoader = require 'lib/camera'
|
-- CameraLoader = require 'lib/camera'
|
||||||
|
|
||||||
local ui = require "lib.ui.widgets"
|
local ui = require "lib.ui.core"
|
||||||
local character = require "lib/character/character"
|
local character = require "lib/character/character"
|
||||||
require "lib/tree"
|
require "lib/tree"
|
||||||
|
|
||||||
@ -25,13 +25,31 @@ function love.load()
|
|||||||
end
|
end
|
||||||
|
|
||||||
Widgets = ui.Root {
|
Widgets = ui.Root {
|
||||||
child = ui.Align {
|
child =
|
||||||
alignment = "bottomCenter",
|
ui.Column {
|
||||||
child = ui.Rectangle {
|
children = {
|
||||||
size = Vec3 { 100, 100 },
|
ui.Row {
|
||||||
color = { 0, 0, 1 }
|
gap = 8,
|
||||||
|
children = {
|
||||||
|
ui.Rectangle { size = Vec3 { 100, 100 }, color = { 0, 0, 1 } },
|
||||||
|
ui.Rectangle { size = Vec3 { 200, 100 }, color = { 1, 0, 0 } },
|
||||||
|
ui.Expanded { flex = 1, child = ui.Rectangle { size = Vec3 { 100, 150 }, color = { 0, 1, 0 } } },
|
||||||
|
ui.Rectangle { size = Vec3 { 100, 100 }, color = { 1, 1, 0 } },
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ui.Expanded {},
|
||||||
|
ui.Row {
|
||||||
|
gap = 8,
|
||||||
|
children = {
|
||||||
|
ui.Rectangle { size = Vec3 { 100, 100 }, color = { 0, 0, 1 } },
|
||||||
|
ui.Rectangle { size = Vec3 { 200, 100 }, color = { 1, 0, 0 } },
|
||||||
|
ui.Expanded { flex = 1, child = ui.Rectangle { size = Vec3 { 100, 150 }, color = { 0, 1, 0 } } },
|
||||||
|
ui.Rectangle { size = Vec3 { 100, 100 }, color = { 1, 1, 0 } },
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
-- PlayerFaction.characters = { Hero1, Hero2 }
|
-- PlayerFaction.characters = { Hero1, Hero2 }
|
||||||
@ -46,10 +64,7 @@ function love.update(dt)
|
|||||||
Tree.level:update(dt)
|
Tree.level:update(dt)
|
||||||
Tree.controls:cache()
|
Tree.controls:cache()
|
||||||
|
|
||||||
Widgets:traverse(function(el)
|
Widgets:update(dt)
|
||||||
el:update(dt)
|
|
||||||
return true
|
|
||||||
end)
|
|
||||||
local t2 = love.timer.getTime()
|
local t2 = love.timer.getTime()
|
||||||
lt = string.format("%.3f", (t2 - t1) * 1000)
|
lt = string.format("%.3f", (t2 - t1) * 1000)
|
||||||
end
|
end
|
||||||
@ -99,12 +114,7 @@ function love.draw()
|
|||||||
Tree.level:draw()
|
Tree.level:draw()
|
||||||
Tree.level.camera:detach()
|
Tree.level.camera:detach()
|
||||||
|
|
||||||
Widgets:traverse(
|
Widgets:draw()
|
||||||
function(el)
|
|
||||||
el:draw()
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
)
|
|
||||||
love.graphics.setColor(1, 1, 1)
|
love.graphics.setColor(1, 1, 1)
|
||||||
|
|
||||||
local stats = "fps: " .. love.timer.getFPS() .. " lt: " .. lt .. " dt: " .. dt
|
local stats = "fps: " .. love.timer.getFPS() .. " lt: " .. lt .. " dt: " .. dt
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user