diff --git a/lib/simple_ui/builder.lua b/lib/simple_ui/builder.lua new file mode 100644 index 0000000..096d0d8 --- /dev/null +++ b/lib/simple_ui/builder.lua @@ -0,0 +1,77 @@ +--- Объект, который отвечает за работу с элементами интерфейса одного экрана +--- @class UIBuilder +--- @field private _cache UIElement[] +--- @field elementTree UIElement +local builder = {} +builder.__index = builder + +local function new(elementTree) + local l = {} + l.elementTree = elementTree + l._cache = {} + + setmetatable(l, builder) + return l +end + +--- @param element? UIElement +--- @private +function builder:_get(element) + if not element then return nil end + + local key = builder:_makeKey(element) + if not key then return element end + + local cached = self._cache[key] + if cached then return cached end + + self._cache[key] = element + return element +end + +--- @param element UIElement +--- @private +function builder:_makeKey(element) + if not element.key then return nil end + if type(element.key) == "string" then return element.key end + element.key = element.type .. "<" .. tostring(element.key) .. ">" + return element.key +end + +--- @private +function builder:build_step(cur) + if not cur then return end + if cur.build then + cur.child = self:_get(cur:build()) + self:build_step(cur.child) + elseif cur.children then + for _, child in ipairs(cur.children) do + self:build_step(self:_get(child)) + end + else + cur.child = self:_get(cur.child) + self:build_step(cur.child) + end +end + +--- Этот метод раскрывает всех отложенных (через build) детей в дереве и хитро их кэширует, чтобы не перестраивались постоянно +--- +--- Благодаря этому можно каждый раз создавать новые элементы в верстке, а получать старые :) +function builder:build() + local root = self:_get(self.elementTree) + self:build_step(root) +end + +function builder:layout() + self.elementTree:layout() +end + +function builder:update(dt) + self.elementTree:update(dt) +end + +function builder:draw() + self.elementTree:draw() +end + +return new diff --git a/lib/simple_ui/center.lua b/lib/simple_ui/center.lua new file mode 100644 index 0000000..994c831 --- /dev/null +++ b/lib/simple_ui/center.lua @@ -0,0 +1,22 @@ +local Constraints = require "lib.simple_ui.constraints" +local SingleChildElement = require "lib.simple_ui.single_child_element" + +--- @class Center : SingleChildElement +local element = setmetatable({}, SingleChildElement) +element.__index = element +element.__type = "Center" + +function element:layout() + self.size = Vec3 { self.constraints.maxWidth, self.constraints.maxHeight } + + if not self.child then return end + self.child.constraints = Constraints(self.constraints) + self.child:layout() + + self.child.offset = Vec3 { + self.offset.x + (self.size.x - self.child.size.x) / 2, + self.offset.y + (self.size.y - self.child.size.y) / 2, + } +end + +return element diff --git a/lib/simple_ui/constraints.lua b/lib/simple_ui/constraints.lua new file mode 100644 index 0000000..ab54dff --- /dev/null +++ b/lib/simple_ui/constraints.lua @@ -0,0 +1,21 @@ +--- @class Constraints +--- @field minWidth number +--- @field maxWidth number +--- @field minHeight number +--- @field maxHeight number +local constraints = { + minWidth = 0, + maxWidth = math.huge, + minHeight = 0, + maxHeight = math.huge +} + +constraints.__index = constraints + +--- @param from {minWidth?: number, maxWidth?: number, minHeight?: number, maxHeight?: number} +--- @return Constraints +local function new(from) + return setmetatable(from, constraints) +end + +return new diff --git a/lib/simple_ui/element.lua b/lib/simple_ui/element.lua index b2433c0..cc2099b 100644 --- a/lib/simple_ui/element.lua +++ b/lib/simple_ui/element.lua @@ -1,28 +1,40 @@ local Rect = require "lib.simple_ui.rect" +local Constraints = require "lib.simple_ui.constraints" +local Vec3 = require "lib.utils.vec3" --- @class UIElement ---- @field bounds Rect Прямоугольник, в границах которого размещается элемент. Размеры и положение в *локальных* координатах ---- @field transform love.Transform Преобразование из локальных координат элемента (bounds) в экранные координаты -local uiElement = {} -uiElement.__index = uiElement +--- @field type string +--- @field key? any Must be convertible to string +--- @field parent? UIElement +--- @field constraints Constraints +--- @field offset Vec3 Положение левого верхнего угла элемента в экранных координатах {x, y}. Устанавливается родительским элементом. +--- @field size Vec3 Размеры элемента в экранных координатах {x, y} +--- @field build? fun(self) +local element = {} +element.__index = element +element.type = "Element" +element.constraints = Constraints {} +element.offset = Vec3 {} +element.size = Vec3 {} -function uiElement:update(dt) end +--- "Constraints go down. Sizes go up. Parent sets position." +--- +--- Karl Marx, probably. +function element:layout() end -function uiElement:draw() end +function element:update(dt) end -function uiElement:hitTest(screenX, screenY) - local lx, ly = self.transform:inverseTransformPoint(screenX, screenY) - return self.bounds:hasPoint(lx, ly) -end +function element:draw() end --- @generic T : UIElement --- @param values table --- @param self T --- @return T -function uiElement.new(self, values) +function element.new(self, values) values.bounds = values.bounds or Rect {} values.transform = values.transform or love.math.newTransform() + if values.child then values.child.parent = values end return setmetatable(values, self) end -return uiElement +return element diff --git a/lib/simple_ui/flex.lua b/lib/simple_ui/flex.lua new file mode 100644 index 0000000..1c072f3 --- /dev/null +++ b/lib/simple_ui/flex.lua @@ -0,0 +1,14 @@ +--- @class Flex : MultiChildElement +--- @field direction "horizontal" | "vertical" +local flex = setmetatable({}, require "lib.simple_ui.multi_child_element") +flex.__index = flex +flex.type = "Flex" +flex.direction = "horizontal" + +function flex:layout() + for _, child in ipairs(self.children) do + + end +end + +return flex diff --git a/lib/simple_ui/level/test.lua b/lib/simple_ui/level/test.lua new file mode 100644 index 0000000..4dd5237 --- /dev/null +++ b/lib/simple_ui/level/test.lua @@ -0,0 +1,24 @@ +local ScreenArea = require "lib.simple_ui.screen_area" +local Center = require "lib.simple_ui.center" +local Placeholder = require "lib.simple_ui.placeholder" +local Padding = require "lib.simple_ui.padding" + + +return ScreenArea:new { + build = function(self) + return + (love.timer.getTime() / 2) % 2 < 1 and + Center:new { + child = Padding:new { + left = 8, + right = 8, + top = 8, + bottom = 8, + child = Placeholder:new { + key = "const :)", + } + } + or nil + } + end +} diff --git a/lib/simple_ui/multi_child_element.lua b/lib/simple_ui/multi_child_element.lua new file mode 100644 index 0000000..25ad1d9 --- /dev/null +++ b/lib/simple_ui/multi_child_element.lua @@ -0,0 +1,13 @@ +--- @class MultiChildElement : UIElement +--- @field children UIElement[] +local element = setmetatable({}, require "lib.simple_ui.element") +element.__index = element +element.children = {} + +function element:update(dt) + for _, child in ipairs(self.children) do + child:update(dt) + end +end + +return element diff --git a/lib/simple_ui/padding.lua b/lib/simple_ui/padding.lua new file mode 100644 index 0000000..05c809e --- /dev/null +++ b/lib/simple_ui/padding.lua @@ -0,0 +1,35 @@ +local Constraints = require "lib.simple_ui.constraints" +local SingleChildElement = require "lib.simple_ui.single_child_element" + +--- @class Padding : SingleChildElement +--- @field left number +--- @field right number +--- @field top number +--- @field bottom number +local element = setmetatable({}, SingleChildElement) +element.__index = element +element.type = "Placeholder" +element.left = 0 +element.right = 0 +element.top = 0 +element.bottom = 0 + +--- "When passing layout constraints to its child, padding shrinks the constraints by the given padding, causing the child to layout at a smaller size. +--- Padding then sizes itself to its child's size, inflated by the padding, effectively creating empty space around the child." +--- +--- as in https://api.flutter.dev/flutter/widgets/Padding-class.html +function element:layout() + if not self.child then return end + local c = Constraints(self.constraints) + c.maxWidth = c.maxWidth - self.left - self.right + c.maxHeight = c.maxHeight - self.top - self.bottom + c.maxWidth = c.maxWidth > 0 and c.maxWidth or 0 + c.maxHeight = c.maxHeight > 0 and c.maxHeight or 0 + self.child.constraints = c + + self.child:layout() + self.size = Vec3 { self.child.size.x + self.left + self.right, self.constraints.maxHeight + self.top + self.bottom } + self.child.offset = self.offset + Vec3 { self.left, self.top } +end + +return element diff --git a/lib/simple_ui/placeholder.lua b/lib/simple_ui/placeholder.lua new file mode 100644 index 0000000..37cf069 --- /dev/null +++ b/lib/simple_ui/placeholder.lua @@ -0,0 +1,24 @@ +local Constraints = require "lib.simple_ui.constraints" +local SingleChildElement = require "lib.simple_ui.single_child_element" + +--- @class Placeholder : SingleChildElement +local element = setmetatable({}, SingleChildElement) +element.__index = element +element.type = "Placeholder" + +function element:layout() + self.size = Vec3 { self.constraints.maxWidth, self.constraints.maxHeight } + + if not self.child then return end + self.child.constraints = Constraints(self.constraints) + self.child:layout() + self.child.offset = Vec3 {} +end + +function element:draw() + love.graphics.rectangle("line", self.offset.x, self.offset.y, self.size.x, self.size.y) + love.graphics.line(self.offset.x, self.offset.y, self.offset.x + self.size.x, self.offset.y + self.size.y) + love.graphics.line(self.offset.x, self.offset.y + self.size.y, self.offset.x + self.size.x, self.offset.y) +end + +return element diff --git a/lib/simple_ui/screen_area.lua b/lib/simple_ui/screen_area.lua new file mode 100644 index 0000000..f253f57 --- /dev/null +++ b/lib/simple_ui/screen_area.lua @@ -0,0 +1,28 @@ +local Constraints = require "lib.simple_ui.constraints" +local SingleChildElement = require "lib.simple_ui.single_child_element" + +--- @class ScreenArea : SingleChildElement +local element = setmetatable({}, SingleChildElement) +element.__index = element +element.type = "ScreenArea" + +function element:layout() + local screenW, screenH = love.graphics.getWidth(), love.graphics.getHeight() + self.constraints = Constraints { + maxWidth = screenW, + maxHeight = screenH + } + self.size = Vec3 { screenW, screenH } + + if not self.child then return end + self.child.constraints = Constraints { -- force a child to be the same size as the screen + minWidth = screenW, + maxWidth = screenW, + minHeight = screenH, + maxHeight = screenH, + } + self.child:layout() + self.child.offset = Vec3 {} +end + +return element diff --git a/lib/simple_ui/single_child_element.lua b/lib/simple_ui/single_child_element.lua new file mode 100644 index 0000000..45602f3 --- /dev/null +++ b/lib/simple_ui/single_child_element.lua @@ -0,0 +1,20 @@ +--- @class SingleChildElement : UIElement +--- @field child? UIElement +local element = setmetatable({}, require "lib.simple_ui.element") +element.__index = element + +function element:layout() + if not self.child then return end + self.child.constraints = self.constraints + self.child:layout() +end + +function element:update(dt) + if self.child then self.child:update(dt) end +end + +function element:draw() + if self.child then self.child:draw() end +end + +return element diff --git a/lib/utils/vec3.lua b/lib/utils/vec3.lua index ca9a10e..504ad39 100644 --- a/lib/utils/vec3.lua +++ b/lib/utils/vec3.lua @@ -78,8 +78,9 @@ function __Vec3:equalsTo(other) and self.z == other.z end ----Vec3 constructor ----@param vec number[] +--- Vec3 constructor +--- @param vec number[] +--- @return Vec3 function Vec3(vec) return setmetatable({ x = vec[1] or 0, diff --git a/main.lua b/main.lua index 26825d5..a898089 100644 --- a/main.lua +++ b/main.lua @@ -2,7 +2,8 @@ local character = require "lib/character/character" require "lib/tree" -local testLayout = require "lib.simple_ui.level.layout" +local UIBuilder = require("lib.simple_ui.builder") +local testLayout = UIBuilder(require "lib.simple_ui.level.test") function love.conf(t) t.console = true @@ -25,6 +26,8 @@ local lt = "0" function love.update(dt) local t1 = love.timer.getTime() Tree.controls:poll() + testLayout:build() + testLayout:layout() testLayout:update(dt) -- логика UI-слоя должна отработать раньше всех, потому что нужно перехватить жесты и не пустить их дальше Tree.panning:update(dt) Tree.level:update(dt)