From 44a71d98ae39c95280a8930ef56bec21b70abd5a Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Sat, 16 Aug 2025 07:09:29 +0300 Subject: [PATCH 1/5] implement core ui widget logic --- lib/ui/element.lua | 44 ++++++++++++++++++++++++++++ lib/ui/widgets.lua | 72 ++++++++++++++++++++++++++++++++++++++++++++++ main.lua | 27 +++++++++++++++-- 3 files changed, 140 insertions(+), 3 deletions(-) create mode 100644 lib/ui/element.lua create mode 100644 lib/ui/widgets.lua diff --git a/lib/ui/element.lua b/lib/ui/element.lua new file mode 100644 index 0000000..3a46b12 --- /dev/null +++ b/lib/ui/element.lua @@ -0,0 +1,44 @@ +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 diff --git a/lib/ui/widgets.lua b/lib/ui/widgets.lua new file mode 100644 index 0000000..084e569 --- /dev/null +++ b/lib/ui/widgets.lua @@ -0,0 +1,72 @@ +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 diff --git a/main.lua b/main.lua index 8d29dc6..53dcbcb 100644 --- a/main.lua +++ b/main.lua @@ -1,10 +1,10 @@ -- CameraLoader = require 'lib/camera' +local ui = require "lib.ui.widgets" local character = require "lib/character/character" require "lib/tree" - function love.conf(t) t.console = true end @@ -24,6 +24,16 @@ function love.load() end end + Widgets = ui.Root { + child = ui.Align { + alignment = "bottomCenter", + child = ui.Rectangle { + size = Vec3 { 100, 100 }, + color = { 0, 0, 1 } + } + } + } + -- PlayerFaction.characters = { Hero1, Hero2 } love.window.setMode(1080, 720, { resizable = true, msaa = 4, vsync = true }) end @@ -35,6 +45,11 @@ function love.update(dt) Tree.panning:update(dt) Tree.level:update(dt) Tree.controls:cache() + + Widgets:traverse(function(el) + el:update(dt) + return true + end) local t2 = love.timer.getTime() lt = string.format("%.3f", (t2 - t1) * 1000) end @@ -81,11 +96,17 @@ function love.draw() love.graphics.setColor(1, 1, 1) end - - Tree.level:draw() Tree.level.camera:detach() + Widgets:traverse( + function(el) + el:draw() + return true + end + ) + love.graphics.setColor(1, 1, 1) + local stats = "fps: " .. love.timer.getFPS() .. " lt: " .. lt .. " dt: " .. dt love.graphics.print(stats, 10, 10) From b34022f123f7003aa78a3ba6777f7e5e661fa8e1 Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Sun, 17 Aug 2025 06:18:24 +0300 Subject: [PATCH 2/5] add deep-copy method --- lib/utils/vec3.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/utils/vec3.lua b/lib/utils/vec3.lua index 72b4b16..ca9a10e 100644 --- a/lib/utils/vec3.lua +++ b/lib/utils/vec3.lua @@ -66,6 +66,10 @@ function __Vec3:subtract(other) return self:add(other:invert()) end +function __Vec3:copy() + return Vec3 { self.x, self.y, self.z } +end + --- @param other Vec3 function __Vec3:equalsTo(other) return From 017f971311a4c82e1e1e997c2c9d0996de14cff1 Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Sun, 17 Aug 2025 06:18:49 +0300 Subject: [PATCH 3/5] complete ui rewrite --- lib/ui/core.lua | 490 +++++++++++++++++++++++++++++++++++++++++++++ lib/ui/element.lua | 44 ---- lib/ui/widgets.lua | 72 ------- main.lua | 44 ++-- 4 files changed, 517 insertions(+), 133 deletions(-) create mode 100644 lib/ui/core.lua delete mode 100644 lib/ui/element.lua delete mode 100644 lib/ui/widgets.lua diff --git a/lib/ui/core.lua b/lib/ui/core.lua new file mode 100644 index 0000000..8bc3a75 --- /dev/null +++ b/lib/ui/core.lua @@ -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 diff --git a/lib/ui/element.lua b/lib/ui/element.lua deleted file mode 100644 index 3a46b12..0000000 --- a/lib/ui/element.lua +++ /dev/null @@ -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 diff --git a/lib/ui/widgets.lua b/lib/ui/widgets.lua deleted file mode 100644 index 084e569..0000000 --- a/lib/ui/widgets.lua +++ /dev/null @@ -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 diff --git a/main.lua b/main.lua index 53dcbcb..82d98e1 100644 --- a/main.lua +++ b/main.lua @@ -1,6 +1,6 @@ -- CameraLoader = require 'lib/camera' -local ui = require "lib.ui.widgets" +local ui = require "lib.ui.core" local character = require "lib/character/character" require "lib/tree" @@ -25,13 +25,31 @@ function love.load() end Widgets = ui.Root { - child = ui.Align { - alignment = "bottomCenter", - child = ui.Rectangle { - size = Vec3 { 100, 100 }, - color = { 0, 0, 1 } + child = + ui.Column { + children = { + 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 } }, + } + }, + 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 } @@ -46,10 +64,7 @@ function love.update(dt) Tree.level:update(dt) Tree.controls:cache() - Widgets:traverse(function(el) - el:update(dt) - return true - end) + Widgets:update(dt) local t2 = love.timer.getTime() lt = string.format("%.3f", (t2 - t1) * 1000) end @@ -99,12 +114,7 @@ function love.draw() Tree.level:draw() Tree.level.camera:detach() - Widgets:traverse( - function(el) - el:draw() - return true - end - ) + Widgets:draw() love.graphics.setColor(1, 1, 1) local stats = "fps: " .. love.timer.getFPS() .. " lt: " .. lt .. " dt: " .. dt From 9613e660c3c5dd8846f2c11234de0bd2e18061ed Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Sun, 17 Aug 2025 06:32:17 +0300 Subject: [PATCH 4/5] add padding widget --- lib/ui/core.lua | 57 +++++++++++++++++++++++++++++++++++++++++++++++++ main.lua | 2 +- 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/lib/ui/core.lua b/lib/ui/core.lua index 8bc3a75..0cecde3 100644 --- a/lib/ui/core.lua +++ b/lib/ui/core.lua @@ -471,6 +471,62 @@ local function mk_ctor(class) return class end +-- =============== Padding (single child) =============== + +---@class Padding: SingleChildElement +---@field padding {left?: number, right?: number, top?: number, bottom?: number} +local Padding = setmetatable({}, { __index = SingleChildElement }) +Padding.__index = Padding + +---@param props { padding?: {left?: number, right?: number, top?: number, bottom?: number}, child?: Element }|nil +---@return Padding +function Padding:new(props) + local o = SingleChildElement.new(self, props) + ---@cast o Padding + local p = o.padding or {} + o.padding = { + left = tonumber(p.left) or 0, + right = tonumber(p.right) or 0, + top = tonumber(p.top) or 0, + bottom = tonumber(p.bottom) or 0, + } + return o +end + +function Padding:layout(constraints) + local L = self.padding.left + local R = self.padding.right + local T = self.padding.top + local B = self.padding.bottom + local hp = L + R + local vp = T + B + + -- Вычитаем паддинги из ограничений для ребёнка + local child_min_w = math.max(0, constraints.min_w - hp) + local child_min_h = math.max(0, constraints.min_h - vp) + local child_max_w = math.max(0, constraints.max_w - hp) + local child_max_h = math.max(0, constraints.max_h - vp) + local child_c = make_constraints(child_min_w, child_min_h, child_max_w, child_max_h) + + if self.child then + self.child:layout(child_c) + local w = self.child.size.x + hp + local h = self.child.size.y + vp + self.size.x, self.size.y = clamp_size(w, h, constraints) + else + -- Нет ребёнка: размер равен сумме паддингов (с учётом ограничений) + self.size.x, self.size.y = clamp_size(hp, vp, constraints) + end +end + +function Padding:arrange(origin) + self.origin = Vec3 { origin.x, origin.y, origin.z or 0 } + if not self.child then return end + local L = self.padding.left + local T = self.padding.top + self.child:arrange(Vec3 { self.origin.x + L, self.origin.y + T, self.origin.z }) +end + ui.Element = Element ui.Root = mk_ctor(Root) ui.Align = mk_ctor(Align) @@ -478,6 +534,7 @@ ui.Row = mk_ctor(Row) ui.Column = mk_ctor(Column) ui.Rectangle = mk_ctor(Rectangle) ui.Expanded = mk_ctor(Expanded) +ui.Padding = mk_ctor(Padding) -- Экспорт вспомогательных, если пригодится ui.constraints = { diff --git a/main.lua b/main.lua index 82d98e1..650b868 100644 --- a/main.lua +++ b/main.lua @@ -33,7 +33,7 @@ function love.load() 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.Padding { padding = { left = 10, right = 10 }, child = ui.Rectangle { size = Vec3 { 100, 150 }, color = { 0, 1, 0 } } }, ui.Rectangle { size = Vec3 { 100, 100 }, color = { 1, 1, 0 } }, } }, From 1b92c0b344495d0909d1243d5a6bd20098c0ab4d Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Thu, 21 Aug 2025 20:54:25 +0300 Subject: [PATCH 5/5] implement reactive rectangle --- lib/ui/core.lua | 34 ++++++++++++++++++++-------------- lib/ui/layout.lua | 36 ++++++++++++++++++++++++++++++++++++ main.lua | 32 ++------------------------------ 3 files changed, 58 insertions(+), 44 deletions(-) create mode 100644 lib/ui/layout.lua diff --git a/lib/ui/core.lua b/lib/ui/core.lua index 0cecde3..3fe6fe5 100644 --- a/lib/ui/core.lua +++ b/lib/ui/core.lua @@ -73,7 +73,7 @@ end ---@param dt number function Element:update(dt) -- По умолчанию спускаем update вниз - for _, ch in ipairs(self.children) do + for _, ch in ipairs(self.children or {}) do ch:update(dt) end end @@ -201,7 +201,7 @@ end -- =============== Align (single child) =============== ---@class Align: SingleChildElement ----@field alignment string "center"|"top_left"|"top_right"|"bottom_left"|"bottom_right" +---@field alignment string "center_left" | "center" | "center_right" | "top_left" | "top_center" | "top_right" | "bottom_left" | "bottom_center" | "bottom_right" ---@field expand boolean если true — занимает все доступное от родителя local Align = setmetatable({}, { __index = SingleChildElement }) Align.__index = Align @@ -239,6 +239,7 @@ function Align:arrange(origin) local cw, ch = self.child.size.x, self.child.size.y local x, y = self.origin.x, self.origin.y + --- @todo сделать красиво if self.alignment == "center" then x = x + (pw - cw) / 2 y = y + (ph - ch) / 2 @@ -249,10 +250,15 @@ function Align:arrange(origin) y = y + (ph - ch) / 2 elseif self.alignment == "top_left" then -- x,y остаются + elseif self.alignment == "top_center" then + x = x + (pw - cw) / 2 elseif self.alignment == "top_right" then x = x + (pw - cw) elseif self.alignment == "bottom_left" then y = y + (ph - ch) + elseif self.alignment == "bottom_center" then + y = y + (ph - ch) + x = x + (pw - cw) / 2 elseif self.alignment == "bottom_right" then x = x + (pw - cw) y = y + (ph - ch) @@ -459,18 +465,6 @@ function Expanded:arrange(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 - -- =============== Padding (single child) =============== ---@class Padding: SingleChildElement @@ -527,6 +521,18 @@ function Padding:arrange(origin) self.child:arrange(Vec3 { self.origin.x + L, self.origin.y + T, self.origin.z }) 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) diff --git a/lib/ui/layout.lua b/lib/ui/layout.lua new file mode 100644 index 0000000..554df3e --- /dev/null +++ b/lib/ui/layout.lua @@ -0,0 +1,36 @@ +local Vec3 = require "lib.utils.vec3" +local ui = require "lib.ui.core" + +--- @type Rectangle +local ReactiveRectangle = ui.Rectangle { + size = Vec3 { 100, 100 }, + color = { 1, 0, 0 }, + state = { tick = 0 } +} +function ReactiveRectangle:update(dt) + getmetatable(self):update(dt) + + local mx, my = love.mouse.getPosition() + if mx > self.origin.x and mx < self.origin.x + self.size.x + and my > self.origin.y and my < self.origin.y + self.size.y + then + self.color = { 0, 1, 0 } + else + self.color = { 1, 0, 0 } + end + + self.state.tick = self.state.tick + 1 +end + +local Layout = ui.Root { + child = ui.Align { + alignment = "bottom_center", + child = ui.Row { + children = { + ReactiveRectangle + } + } + } +} + +return Layout diff --git a/main.lua b/main.lua index 650b868..384a675 100644 --- a/main.lua +++ b/main.lua @@ -1,6 +1,6 @@ -- CameraLoader = require 'lib/camera' -local ui = require "lib.ui.core" +local Widgets = require "lib.ui.layout" local character = require "lib/character/character" require "lib/tree" @@ -24,34 +24,6 @@ function love.load() end end - Widgets = ui.Root { - child = - ui.Column { - children = { - 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.Padding { padding = { left = 10, right = 10 }, 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 } love.window.setMode(1080, 720, { resizable = true, msaa = 4, vsync = true }) end @@ -60,11 +32,11 @@ local lt = "0" function love.update(dt) local t1 = love.timer.getTime() Tree.controls:poll() + Widgets:update(dt) -- логика UI-слоя должна отработать раньше всех, потому что нужно перехватить жесты и не пустить их дальше Tree.panning:update(dt) Tree.level:update(dt) Tree.controls:cache() - Widgets:update(dt) local t2 = love.timer.getTime() lt = string.format("%.3f", (t2 - t1) * 1000) end