reinvent the wheel

This commit is contained in:
PeaAshMeter 2025-11-18 23:48:18 +03:00
parent 411c435e7a
commit c7ee957c8c
13 changed files with 309 additions and 15 deletions

77
lib/simple_ui/builder.lua Normal file
View File

@ -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

22
lib/simple_ui/center.lua Normal file
View File

@ -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

View File

@ -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

View File

@ -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

14
lib/simple_ui/flex.lua Normal file
View File

@ -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

View File

@ -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
}

View File

@ -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

35
lib/simple_ui/padding.lua Normal file
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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)