revert the old based flutter-like approach

This commit is contained in:
PeaAshMeter 2026-05-02 23:39:54 +03:00
parent 790d63d37f
commit 85bb8e1d22
21 changed files with 472 additions and 654 deletions

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

@ -0,0 +1,76 @@
--- Объект, который отвечает за работу с элементами интерфейса одного экрана
--- @class UIBuilder
--- @field private _cache UIElement[]
--- @field elementTree UIElement
local builder = {}
builder.__index = builder
--- @return UIBuilder
local function new(from)
from._cache = {}
setmetatable(from, builder)
return from
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,109 +1,40 @@
local Rect = require "lib.simple_ui.rect"
local function makeGradientMesh(w, h, topColor, bottomColor)
local vertices = {
{ 0, 0, 0, 0, topColor[1], topColor[2], topColor[3], topColor[4] }, -- левый верх
{ w, 0, 1, 0, topColor[1], topColor[2], topColor[3], topColor[4] }, -- правый верх
{ w, h, 1, 1, bottomColor[1], bottomColor[2], bottomColor[3], bottomColor[4] }, -- правый низ
{ 0, h, 0, 1, bottomColor[1], bottomColor[2], bottomColor[3], bottomColor[4] }, -- левый низ
}
local mesh = love.graphics.newMesh(vertices, "fan", "static")
return mesh
end
local Constraints = require "lib.simple_ui.constraints"
local Vec3 = require "lib.utils.vec3"
--- @class UIElement
--- @field bounds Rect Прямоугольник, в границах которого размещается элемент. Размеры и положение в экранных координатах
--- @field overlayGradientMesh love.Mesh Общий градиент поверх элемента (интерполированный меш)
local uiElement = {}
uiElement.bounds = Rect {}
uiElement.overlayGradientMesh = makeGradientMesh(1, 1, { 0, 0, 0, 0 }, { 0, 0, 0, 0.4 });
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): UIElement?
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)
return self.bounds:hasPoint(screenX, screenY)
end
function element:draw() end
--- @generic T : UIElement
--- @param values table
--- @param values T
--- @param self T
--- @return T
function uiElement.new(self, values)
function element.new(self, values)
values.bounds = values.bounds or Rect {}
values.overlayGradientMesh = values.overlayGradientMesh or uiElement.overlayGradientMesh;
values.transform = values.transform or love.math.newTransform()
if values.child then values.child.parent = values end
return setmetatable(values, self)
end
--- Рисует границу вокруг элемента (с псевдо-затенением)
--- @param type "outer" | "inner"
--- @param width? number
function uiElement:drawBorder(type, width)
local w = width or 4
love.graphics.setLineWidth(w)
if type == "inner" then
love.graphics.setColor(0.2, 0.2, 0.2)
love.graphics.line({
self.bounds.x, self.bounds.y + self.bounds.height,
self.bounds.x, self.bounds.y,
self.bounds.x + self.bounds.width, self.bounds.y,
})
love.graphics.setColor(0.3, 0.3, 0.3)
love.graphics.line({
self.bounds.x + self.bounds.width, self.bounds.y,
self.bounds.x + self.bounds.width, self.bounds.y + self.bounds.height,
self.bounds.x, self.bounds.y + self.bounds.height,
})
else
love.graphics.setColor(0.3, 0.3, 0.3)
-- love.graphics.line({
-- self.bounds.x, self.bounds.y + self.bounds.height,
-- self.bounds.x, self.bounds.y,
-- self.bounds.x + self.bounds.width, self.bounds.y,
-- })
love.graphics.line({
self.bounds.x, self.bounds.y + self.bounds.height - w,
self.bounds.x, self.bounds.y + w,
})
love.graphics.line({
self.bounds.x + w, self.bounds.y,
self.bounds.x + self.bounds.width - w, self.bounds.y,
})
love.graphics.setColor(0.2, 0.2, 0.2)
-- love.graphics.line({
-- self.bounds.x + self.bounds.width, self.bounds.y,
-- self.bounds.x + self.bounds.width, self.bounds.y + self.bounds.height,
-- self.bounds.x, self.bounds.y + self.bounds.height,
-- })
love.graphics.line({
self.bounds.x + self.bounds.width, self.bounds.y + w,
self.bounds.x + self.bounds.width, self.bounds.y + self.bounds.height - w,
})
love.graphics.line({
self.bounds.x + self.bounds.width - w, self.bounds.y + self.bounds.height,
self.bounds.x + w, self.bounds.y + self.bounds.height,
})
end
love.graphics.setColor(1, 1, 1)
end
--- рисует градиент поверх элемента
function uiElement:drawGradientOverlay()
love.graphics.push()
love.graphics.translate(self.bounds.x, self.bounds.y)
love.graphics.scale(self.bounds.width, self.bounds.height)
love.graphics.setColor(1, 1, 1, 1)
love.graphics.draw(self.overlayGradientMesh)
love.graphics.pop()
end
return uiElement
return element

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

@ -0,0 +1,71 @@
local Constraints = require "lib.simple_ui.constraints"
--- @class Flex : MultiChildElement
--- @field direction "horizontal" | "vertical"
--- @field mainAxisSize "max" | "min"
--- @field mainAxisAlignment "start" | "center" | "end"
local flex = setmetatable({}, require "lib.simple_ui.multi_child_element")
flex.__index = flex
flex.type = "Flex"
flex.direction = "horizontal"
flex.mainAxisSize = "max"
flex.mainAxisAlignment = "start"
function flex:layout()
local mainAxisSize = 0
local crossAxisSize = 0
if self.direction == "horizontal" then
for _, child in ipairs(self.children) do
child.constraints = Constraints { maxHeight = self.constraints.maxHeight }
child:layout()
if child.size.y > crossAxisSize then crossAxisSize = child.size.y end
mainAxisSize = mainAxisSize + child.size.x
end
local start = 0
if self.mainAxisAlignment == "center" then
start = self.constraints.maxWidth / 2 - mainAxisSize / 2
elseif self.mainAxisAlignment == "end" then
start = self.constraints.maxWidth - mainAxisSize
end
local shift = 0
for _, child in ipairs(self.children) do
child.offset = Vec3 { self.offset.x + start + shift, self.offset.y }
shift = shift + child.size.x
end
if self.mainAxisSize == "max" then
self.size = Vec3 { self.constraints.maxWidth, crossAxisSize }
else
self.size = Vec3 { mainAxisSize, crossAxisSize }
end
else
for _, child in ipairs(self.children) do
child.constraints = Constraints { maxWidth = self.constraints.maxWidth }
child:layout()
child.offset = Vec3 { self.offset.x, self.offset.y + mainAxisSize }
if child.size.x > crossAxisSize then crossAxisSize = child.size.x end
mainAxisSize = mainAxisSize + child.size.y
end
local start = 0
if self.mainAxisAlignment == "center" then
start = self.constraints.maxHeight / 2 - mainAxisSize / 2
elseif self.mainAxisAlignment == "end" then
start = self.constraints.maxHeight - mainAxisSize
end
local shift = 0
for _, child in ipairs(self.children) do
child.offset = Vec3 { self.offset.x, self.offset.y + start + shift }
shift = shift + child.size.y
end
if self.mainAxisSize == "max" then
self.size = Vec3 { crossAxisSize, self.constraints.maxHeight }
else
self.size = Vec3 { mainAxisSize, crossAxisSize }
end
end
end
return flex

View File

@ -1,71 +0,0 @@
local Element = require "lib.simple_ui.element"
--- @class BarElement : UIElement
--- @field getter fun() : number
--- @field value number
--- @field maxValue number
--- @field color Color
--- @field useDividers boolean
--- @field drawText boolean
local barElement = setmetatable({}, Element)
barElement.__index = barElement
barElement.useDividers = false
barElement.drawText = false
function barElement:update(dt)
local val = self.getter()
self.value = val < 0 and 0 or val > self.maxValue and self.maxValue or val
end
function barElement:draw()
local valueWidth = self.bounds.width * self.value / self.maxValue
local emptyWidth = self.bounds.width - valueWidth
--- шум
love.graphics.setShader(Tree.assets.files.shaders.soft_uniform_noise)
love.graphics.rectangle("fill", self.bounds.x, self.bounds.y, self.bounds.width, self.bounds.height)
love.graphics.setShader()
--- закраска пустой части
love.graphics.setColor(0.05, 0.05, 0.05)
love.graphics.setBlendMode("multiply", "premultiplied")
love.graphics.rectangle("fill", self.bounds.x + valueWidth, self.bounds.y, emptyWidth,
self.bounds.height)
love.graphics.setBlendMode("alpha")
--- закраска значимой части её цветом
love.graphics.setColor(self.color.r, self.color.g, self.color.b)
love.graphics.setBlendMode("multiply", "premultiplied")
love.graphics.rectangle("fill", self.bounds.x, self.bounds.y, valueWidth,
self.bounds.height)
love.graphics.setBlendMode("alpha")
--- мерки
love.graphics.setColor(38 / 255, 50 / 255, 56 / 255)
if self.useDividers then
local count = self.maxValue - 1
local measureWidth = self.bounds.width / self.maxValue
for i = 1, count, 1 do
love.graphics.line(self.bounds.x + i * measureWidth, self.bounds.y, self.bounds.x + i * measureWidth,
self.bounds.y + self.bounds.height)
end
end
love.graphics.setColor(1, 1, 1)
--- текст поверх
if self.drawText then
local font = Tree.fonts:getDefaultTheme():getVariant("small")
local t = love.graphics.newText(font, tostring(self.value) .. "/" .. tostring(self.maxValue))
love.graphics.draw(t, math.floor(self.bounds.x + self.bounds.width / 2 - t:getWidth() / 2),
math.floor(self.bounds.y + self.bounds.height / 2 - t:getHeight() / 2))
end
self:drawBorder("inner")
self:drawGradientOverlay()
end
return function(values) return barElement:new(values) end

View File

@ -1,86 +0,0 @@
local Element = require "lib.simple_ui.element"
local Rect = require "lib.simple_ui.rect"
local Color = require "lib.simple_ui.color"
local Bar = require "lib.simple_ui.level.bar"
--- @class BottomBars : UIElement
--- @field hpBar BarElement
--- @field manaBar BarElement
local bottomBars = setmetatable({}, Element)
bottomBars.__index = bottomBars;
--- @param cid Id
function bottomBars.new(cid)
local t = setmetatable({}, bottomBars)
t.hpBar =
Bar {
getter = function()
local char = Tree.level.characters[cid]
return char:try(Tree.behaviors.stats, function(stats)
return stats.hp or 0
end)
end,
color = Color { r = 130 / 255, g = 8 / 255, b = 8 / 255 },
drawText = true,
maxValue = 20
}
t.manaBar =
Bar {
getter = function()
local char = Tree.level.characters[cid]
return char:try(Tree.behaviors.stats, function(stats)
return stats.mana or 0
end)
end,
color = Color { r = 51 / 255, g = 105 / 255, b = 30 / 255 },
useDividers = true,
maxValue = 10
}
return t
end
function bottomBars:update(dt)
local height = 16
local margin = 2
self.bounds.height = height
self.bounds.y = self.bounds.y - height
self.hpBar.bounds = Rect {
width = -2 * margin + self.bounds.width / 2,
height = height - margin,
x = self.bounds.x + margin,
y = self.bounds.y + margin
}
self.manaBar.bounds = Rect {
width = -2 * margin + self.bounds.width / 2,
height = height - margin,
x = self.bounds.x + margin + self.bounds.width / 2,
y = self.bounds.y + margin
}
self.hpBar:update(dt)
self.manaBar:update(dt)
end
function bottomBars:draw()
-- шум
love.graphics.setShader(Tree.assets.files.shaders.soft_uniform_noise)
love.graphics.rectangle("fill", self.bounds.x, self.bounds.y, self.bounds.width, self.bounds.height)
love.graphics.setShader()
love.graphics.setColor(38 / 255, 50 / 255, 56 / 255)
love.graphics.setBlendMode("multiply", "premultiplied")
love.graphics.rectangle("fill", self.bounds.x, self.bounds.y, self.bounds.width, self.bounds.height)
love.graphics.setBlendMode("alpha")
self.hpBar:draw()
self.manaBar:draw()
end
return bottomBars.new

View File

@ -1,108 +0,0 @@
local task = require "lib.utils.task"
local easing = require "lib.utils.easing"
local Element = require "lib.simple_ui.element"
local Rect = require "lib.simple_ui.rect"
local SkillRow = require "lib.simple_ui.level.skill_row"
local Bars = require "lib.simple_ui.level.bottom_bars"
local EndTurnButton = require "lib.simple_ui.level.end_turn"
--- @class CharacterPanel : UIElement
--- @field animationTask Task
--- @field alpha number
--- @field state "show" | "idle" | "hide"
--- @field skillRow SkillRow
--- @field bars BottomBars
--- @field endTurnButton EndTurnButton
local characterPanel = setmetatable({}, Element)
characterPanel.__index = characterPanel
function characterPanel.new(characterId)
local t = {}
t.state = "show"
t.skillRow = SkillRow(characterId)
t.bars = Bars(characterId)
t.endTurnButton = EndTurnButton {}
t.alpha = 0 -- starts hidden/animating
return setmetatable(t, characterPanel)
end
function characterPanel:show()
self.state = "show"
self.animationTask = task.tween(self, { alpha = 1 }, 300, easing.easeOutCubic)
self.animationTask(function() self.state = "idle" end)
end
function characterPanel:hide()
self.state = "hide"
self.animationTask = task.tween(self, { alpha = 0 }, 300, easing.easeOutCubic)
end
--- @type love.Canvas
local characterPanelCanvas;
function characterPanel:update(dt)
-- Tasks update automatically via task.update(dt) in main.lua
self.skillRow:update(dt)
self.bars.bounds = Rect {
width = self.skillRow.bounds.width,
x = self.skillRow.bounds.x,
y = self.skillRow.bounds.y
}
self.bars:update(dt)
self.bounds = Rect {
x = self.bars.bounds.x,
y = self.bars.bounds.y,
width = self.bars.bounds.width,
height = self.bars.bounds.height + self.skillRow.bounds.height
}
self.endTurnButton:layout()
self.endTurnButton.bounds.x = self.bounds.x + self.bounds.width + 32
self.endTurnButton.bounds.y = self.bounds.y + self.bounds.height / 2 - self.endTurnButton.bounds.height / 2
self.endTurnButton:update(dt)
if not characterPanelCanvas then
characterPanelCanvas = love.graphics.newCanvas(self.bounds.width, self.bounds.height)
end
--- анимация появления
local revealShader = Tree.assets.files.shaders.reveal
revealShader:send("t", self.alpha)
end
function characterPanel:draw()
self.skillRow:draw()
--- @TODO: переписать этот ужас с жонглированием координатами, а то слишком хардкод (skillRow рисуется относительно нуля и не закрывает канвас)
love.graphics.push()
local canvas = love.graphics.getCanvas()
love.graphics.translate(0, self.bars.bounds.height)
love.graphics.setCanvas(characterPanelCanvas)
love.graphics.clear()
love.graphics.draw(canvas)
love.graphics.pop()
love.graphics.push()
love.graphics.translate(-self.bounds.x, -self.bounds.y)
self.bars:draw()
self:drawBorder("outer")
love.graphics.pop()
--- рисуем текстуру шейдером появления
love.graphics.setCanvas()
love.graphics.setShader(Tree.assets.files.shaders.reveal)
love.graphics.setColor(1, 1, 1, 1)
self.endTurnButton:draw()
love.graphics.push()
love.graphics.translate(self.bounds.x, self.bounds.y)
love.graphics.draw(characterPanelCanvas)
love.graphics.setColor(1, 1, 1)
love.graphics.pop()
love.graphics.setShader()
end
return characterPanel.new

View File

@ -1,53 +0,0 @@
local Element = require "lib.simple_ui.element"
local task = require "lib.utils.task"
local easing = require "lib.utils.easing"
--- @class EndTurnButton : UIElement
--- @field hovered boolean
--- @field onClick function?
local endTurnButton = setmetatable({}, Element)
endTurnButton.__index = endTurnButton
function endTurnButton:update(dt)
local mx, my = love.mouse.getPosition()
if self:hitTest(mx, my) then
self.hovered = true
if Tree.controls:isJustPressed("select") then
if self.onClick then self.onClick() end
Tree.controls:consume("select")
end
else
self.hovered = false
end
end
function endTurnButton:layout()
local font = Tree.fonts:getDefaultTheme():getVariant("large")
self.text = love.graphics.newText(font, "Завершить ход")
self.bounds.width = self.text:getWidth() + 32
self.bounds.height = self.text:getHeight() + 16
end
function endTurnButton:draw()
love.graphics.setColor(38 / 255, 50 / 255, 56 / 255, 0.9)
love.graphics.rectangle("fill", self.bounds.x, self.bounds.y, self.bounds.width, self.bounds.height)
if self.hovered then
love.graphics.setColor(0.1, 0.1, 0.1)
love.graphics.rectangle("fill", self.bounds.x, self.bounds.y, self.bounds.width, self.bounds.height)
end
love.graphics.setColor(0.95, 0.95, 0.95)
love.graphics.draw(self.text, self.bounds.x + 16, self.bounds.y + 8)
self:drawBorder("outer")
love.graphics.setColor(1, 1, 1)
end
function endTurnButton:onClick()
Tree.level.turnOrder:next()
end
return function(values)
return endTurnButton:new(values)
end

View File

@ -1,23 +0,0 @@
local CPanel = require "lib.simple_ui.level.cpanel"
local build
local layout = {}
function layout:update(dt)
if self.characterPanel then self.characterPanel:update(dt) end
local cid = Tree.level.selector:selected()
if cid then
self.characterPanel = CPanel(cid)
self.characterPanel:show()
self.characterPanel:update(dt)
elseif Tree.level.selector:deselected() then
self.characterPanel:hide()
end
end
function layout:draw()
if self.characterPanel then self.characterPanel:draw() end
end
return layout

View File

@ -1,2 +0,0 @@
local UI_SCALE = 0.75 -- выдуманное значение для dependency injection, надо подбирать так, чтобы UI_SCALE * 64 было целым числом
return UI_SCALE

View File

@ -1,213 +0,0 @@
local icons = require("lib.utils.sprite_atlas").load(Tree.assets.files.dev_icons)
local Element = require "lib.simple_ui.element"
local Rect = require "lib.simple_ui.rect"
local UI_SCALE = require "lib.simple_ui.level.scale"
--- @class SkillButton : UIElement
--- @field hovered boolean
--- @field selected boolean
--- @field onClick function?
--- @field getCooldown function?
--- @field icon? string
local skillButton = setmetatable({}, Element)
skillButton.__index = skillButton
function skillButton:update(dt)
if not self.icon then return end
local mx, my = love.mouse.getPosition()
if self:hitTest(mx, my) then
self.hovered = true
if Tree.controls:isJustPressed("select") then
local cd = self.getCooldown and self.getCooldown() or 0
if cd == 0 then
if self.onClick then self.onClick() end
end
Tree.controls:consume("select")
end
else
self.hovered = false
end
end
function skillButton:draw()
love.graphics.setLineWidth(2)
local cd = self.getCooldown and self.getCooldown() or 0
if not self.icon then
love.graphics.setColor(0.05, 0.05, 0.05)
love.graphics.rectangle("fill", self.bounds.x, self.bounds.y, self.bounds.width, self.bounds.height)
self:drawBorder("inner")
return
end
local quad = icons:pickQuad(self.icon)
love.graphics.push()
love.graphics.translate(self.bounds.x, self.bounds.y)
love.graphics.scale(self.bounds.width / icons.tileSize, self.bounds.height / icons.tileSize)
love.graphics.draw(icons.atlas, quad)
love.graphics.pop()
self:drawBorder("inner")
if self.selected then
love.graphics.setColor(0.3, 1, 0.3, 0.5)
love.graphics.rectangle("fill", self.bounds.x, self.bounds.y, self.bounds.width, self.bounds.height)
elseif self.hovered then
love.graphics.setColor(0.7, 1, 0.7, 0.5)
love.graphics.rectangle("fill", self.bounds.x, self.bounds.y, self.bounds.width, self.bounds.height)
end
if cd > 0 then
love.graphics.setColor(0, 0, 0, 0.5)
love.graphics.rectangle("fill", self.bounds.x, self.bounds.y, self.bounds.width, self.bounds.height)
local font = Tree.fonts:getDefaultTheme():getVariant("headline")
love.graphics.setColor(0, 0, 0)
local t = love.graphics.newText(font, tostring(cd))
love.graphics.draw(t, math.floor(self.bounds.x + 2 + self.bounds.width / 2 - t:getWidth() / 2),
math.floor(self.bounds.y + 2 + self.bounds.height / 2 - t:getHeight() / 2))
love.graphics.setColor(1, 1, 1)
love.graphics.draw(t, math.floor(self.bounds.x + self.bounds.width / 2 - t:getWidth() / 2),
math.floor(self.bounds.y + self.bounds.height / 2 - t:getHeight() / 2))
else
end
love.graphics.setColor(1, 1, 1)
end
--------------------------------------------------------------------------------
--- @class SkillRow : UIElement
--- @field characterId Id
--- @field selected SkillButton?
--- @field children SkillButton[]
local skillRow = setmetatable({}, Element)
skillRow.__index = skillRow
--- @param characterId Id
--- @return SkillRow
function skillRow.new(characterId)
local t = {
characterId = characterId,
children = {}
}
setmetatable(t, skillRow)
local char = Tree.level.characters[characterId]
char:try(Tree.behaviors.spellcaster, function(behavior)
for i, spell in ipairs(behavior.spellbook) do
local skb = skillButton:new { icon = spell.tag }
skb.onClick = function()
skb.selected = not skb.selected
if t.selected then t.selected.selected = false end
t.selected = skb
if not behavior.cast then
behavior.cast = behavior.spellbook[i]
behavior.state = "casting"
behavior.spellbook[i]:onSelected(char)
else
behavior.state = "idle"
behavior.cast = nil
end
end
skb.getCooldown = function()
return behavior.cooldowns[spell.tag] or 0
end
t.children[i] = skb
end
end)
for i = #t.children + 1, 7, 1 do
t.children[i] = skillButton:new {}
end
return t
end
--- @type love.Canvas
local c;
function skillRow:update(dt)
local iconSize = math.floor(64 * UI_SCALE)
local screenW, screenH = love.graphics.getDimensions()
local padding, margin = 8, 4
local count = #self.children -- слоты под скиллы
self.bounds = Rect {
width = iconSize * count + (count + 1) * margin,
height = iconSize + 2 * margin,
}
self.bounds.y = screenH - self.bounds.height - padding -- отступ снизу
self.bounds.x = screenW / 2 - self.bounds.width / 2
for i, skb in ipairs(self.children) do
skb.bounds = Rect { x = self.bounds.x + margin + (i - 1) * (iconSize + margin), -- друг за другом, включая первый отступ от границы
y = self.bounds.y + margin, height = iconSize, width = iconSize }
skb:update(dt)
end
if not c then
c = love.graphics.newCanvas(self.bounds.width, self.bounds.height)
end
end
function skillRow:draw()
love.graphics.setCanvas({ c, stencil = true })
love.graphics.clear()
love.graphics.setColor(1, 1, 1)
do
--- рисуем в локальных координатах текстурки
love.graphics.push()
love.graphics.translate(-self.bounds.x, -self.bounds.y)
-- сначала иконки скиллов
for _, skb in ipairs(self.children) do
skb:draw()
end
-- маска для вырезов под иконки
love.graphics.setShader(Tree.assets.files.shaders.alpha_mask)
love.graphics.stencil(function()
local mask = Tree.assets.files.masks.rrect32
local maskSize = mask:getWidth()
for _, skb in ipairs(self.children) do
love.graphics.draw(mask, skb.bounds.x, skb.bounds.y, 0,
skb.bounds.width / maskSize, skb.bounds.height / maskSize)
end
end, "replace", 1)
love.graphics.setShader()
-- дальше рисуем панель, перекрывая иконки
love.graphics.setStencilTest("less", 1)
-- шум
love.graphics.setShader(Tree.assets.files.shaders.soft_uniform_noise)
love.graphics.rectangle("fill", self.bounds.x, self.bounds.y, self.bounds.width, self.bounds.height)
love.graphics.setShader()
-- фон
love.graphics.setColor(38 / 255, 50 / 255, 56 / 255)
love.graphics.setBlendMode("multiply", "premultiplied")
love.graphics.rectangle("fill", self.bounds.x, self.bounds.y, self.bounds.width, self.bounds.height)
love.graphics.setBlendMode("alpha")
love.graphics.setStencilTest()
--затенение
self:drawGradientOverlay()
love.graphics.pop()
end
love.graphics.setColor(1, 1, 1)
end
return skillRow.new

View File

@ -0,0 +1,101 @@
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"
local Builder = require "lib.simple_ui.builder"
local Flex = require "lib.simple_ui.flex"
local SizedBox = require "lib.simple_ui.sized_box"
local SingleChildElement = require "lib.simple_ui.single_child_element"
local MyWidget = setmetatable({}, SingleChildElement)
MyWidget.__index = MyWidget
--- comment
--- @return Flex
function MyWidget:build()
return Flex:new {
key = "my_flex",
direction = "vertical",
children = {
Flex:new {
key = "inner_flex",
mainAxisAlignment = "start",
children = {
SizedBox:new {
width = 100,
height = 100,
child = Placeholder:new {}
},
SizedBox:new {
width = 100,
height = 100,
child = Placeholder:new {}
},
},
},
Flex:new {
key = "inner_flex2",
mainAxisAlignment = "center",
children = {
SizedBox:new {
width = 100,
height = 100,
child = Placeholder:new {}
},
SizedBox:new {
width = 100,
height = 100,
child = Placeholder:new {}
},
},
},
Flex:new {
key = "inner_flex3",
mainAxisAlignment = "end",
children = {
SizedBox:new {
width = 100,
height = 100,
child = Placeholder:new {}
},
SizedBox:new {
width = 100,
height = 100,
child = Placeholder:new {}
},
},
},
-- SizedBox:new {
-- width = 100,
-- height = 100,
-- child = Placeholder:new {}
-- },
-- SizedBox:new {
-- width = 100,
-- height = 100,
-- child = Placeholder:new {}
-- },
}
}
end
-- function MyWidget:layout()
-- if not self.child then return end
-- self.child.constraints = self.constraints
-- self.child:layout()
-- end
return Builder {
elementTree = ScreenArea:new {
child = MyWidget:new {}
}
}

View File

@ -0,0 +1,19 @@
--- @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
function element:draw()
for _, child in ipairs(self.children) do
child:draw()
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 = self.offset:copy()
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

@ -18,7 +18,7 @@ function rect.new(table)
end
function rect:hasPoint(x, y)
return x >= self.x and x < self.x + self.width and y >= self.y and y < self.y + self.height
return x >= self.x and x < self.width and y >= self.y and y < self.height
end
return rect.new

View File

@ -0,0 +1,26 @@
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 {
maxWidth = screenW,
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

@ -0,0 +1,24 @@
--- @class SizedBox : SingleChildElement
local element = setmetatable({}, require "lib.simple_ui.single_child_element")
local Constraints = require("lib.simple_ui.constraints")
element.type = "SizedBox"
element.__index = element
function element:layout()
self.size = Vec3 { self.width, self.height }
if not self.child then return end
self.child.constraints = Constraints {
maxWidth = self.width,
maxHeight = self.height,
}
self.child:layout()
self.child.offset = self.offset:copy()
end
-- function element:draw()
-- print('hello2')
-- if self.child then self.child:draw() end
-- end
return element

View File

@ -12,7 +12,7 @@ end
function love.load()
love.window.setMode(1280, 720, { resizable = true, msaa = 4, vsync = true })
require "lib/tree" -- важно это сделать после настройки окна
testLayout = require "lib.simple_ui.level.layout"
testLayout = require "lib.simple_ui.level.test"
local chars = {
character.spawn("Foodor")
@ -83,7 +83,11 @@ function love.update(dt)
require('lib.utils.task').update(dt)
Tree.controls:poll()
Tree.level.camera:update(dt) -- сначала логика камеры, потому что на нее завязан UI
testLayout:update(dt) -- потом UI, потому что нужно перехватить жесты и не пустить их дальше
testLayout:build()
testLayout:layout()
testLayout:update(dt) -- потом UI, потому что нужно перехватить жесты и не пустить их дальше
Tree.panning:update(dt)
Tree.level:update(dt)
Tree.audio:update(dt)