From 85bb8e1d2261a4326d80f76c89f8893c5424cac6 Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Sat, 2 May 2026 23:39:54 +0300 Subject: [PATCH] revert the old based flutter-like approach --- lib/simple_ui/builder.lua | 76 +++++++++ lib/simple_ui/center.lua | 22 +++ lib/simple_ui/constraints.lua | 21 +++ lib/simple_ui/element.lua | 121 +++----------- lib/simple_ui/flex.lua | 71 +++++++++ lib/simple_ui/level/bar.lua | 71 --------- lib/simple_ui/level/bottom_bars.lua | 86 ---------- lib/simple_ui/level/cpanel.lua | 108 ------------- lib/simple_ui/level/end_turn.lua | 53 ------ lib/simple_ui/level/layout.lua | 23 --- lib/simple_ui/level/scale.lua | 2 - lib/simple_ui/level/skill_row.lua | 213 ------------------------- lib/simple_ui/level/test.lua | 101 ++++++++++++ lib/simple_ui/multi_child_element.lua | 19 +++ lib/simple_ui/padding.lua | 35 ++++ lib/simple_ui/placeholder.lua | 24 +++ lib/simple_ui/rect.lua | 2 +- lib/simple_ui/screen_area.lua | 26 +++ lib/simple_ui/single_child_element.lua | 20 +++ lib/simple_ui/sized_box.lua | 24 +++ main.lua | 8 +- 21 files changed, 472 insertions(+), 654 deletions(-) create mode 100644 lib/simple_ui/builder.lua create mode 100644 lib/simple_ui/center.lua create mode 100644 lib/simple_ui/constraints.lua create mode 100644 lib/simple_ui/flex.lua delete mode 100644 lib/simple_ui/level/bar.lua delete mode 100644 lib/simple_ui/level/bottom_bars.lua delete mode 100644 lib/simple_ui/level/cpanel.lua delete mode 100644 lib/simple_ui/level/end_turn.lua delete mode 100644 lib/simple_ui/level/layout.lua delete mode 100644 lib/simple_ui/level/scale.lua delete mode 100644 lib/simple_ui/level/skill_row.lua create mode 100644 lib/simple_ui/level/test.lua create mode 100644 lib/simple_ui/multi_child_element.lua create mode 100644 lib/simple_ui/padding.lua create mode 100644 lib/simple_ui/placeholder.lua create mode 100644 lib/simple_ui/screen_area.lua create mode 100644 lib/simple_ui/single_child_element.lua create mode 100644 lib/simple_ui/sized_box.lua diff --git a/lib/simple_ui/builder.lua b/lib/simple_ui/builder.lua new file mode 100644 index 0000000..748df2e --- /dev/null +++ b/lib/simple_ui/builder.lua @@ -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 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 440f7a6..a4c5a3f 100644 --- a/lib/simple_ui/element.lua +++ b/lib/simple_ui/element.lua @@ -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 diff --git a/lib/simple_ui/flex.lua b/lib/simple_ui/flex.lua new file mode 100644 index 0000000..7593612 --- /dev/null +++ b/lib/simple_ui/flex.lua @@ -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 diff --git a/lib/simple_ui/level/bar.lua b/lib/simple_ui/level/bar.lua deleted file mode 100644 index 35601af..0000000 --- a/lib/simple_ui/level/bar.lua +++ /dev/null @@ -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 diff --git a/lib/simple_ui/level/bottom_bars.lua b/lib/simple_ui/level/bottom_bars.lua deleted file mode 100644 index d7e3a91..0000000 --- a/lib/simple_ui/level/bottom_bars.lua +++ /dev/null @@ -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 diff --git a/lib/simple_ui/level/cpanel.lua b/lib/simple_ui/level/cpanel.lua deleted file mode 100644 index 2e489f9..0000000 --- a/lib/simple_ui/level/cpanel.lua +++ /dev/null @@ -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 diff --git a/lib/simple_ui/level/end_turn.lua b/lib/simple_ui/level/end_turn.lua deleted file mode 100644 index 21b95d7..0000000 --- a/lib/simple_ui/level/end_turn.lua +++ /dev/null @@ -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 diff --git a/lib/simple_ui/level/layout.lua b/lib/simple_ui/level/layout.lua deleted file mode 100644 index 0c9774a..0000000 --- a/lib/simple_ui/level/layout.lua +++ /dev/null @@ -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 diff --git a/lib/simple_ui/level/scale.lua b/lib/simple_ui/level/scale.lua deleted file mode 100644 index fb4f658..0000000 --- a/lib/simple_ui/level/scale.lua +++ /dev/null @@ -1,2 +0,0 @@ -local UI_SCALE = 0.75 -- выдуманное значение для dependency injection, надо подбирать так, чтобы UI_SCALE * 64 было целым числом -return UI_SCALE diff --git a/lib/simple_ui/level/skill_row.lua b/lib/simple_ui/level/skill_row.lua deleted file mode 100644 index 1c56059..0000000 --- a/lib/simple_ui/level/skill_row.lua +++ /dev/null @@ -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 diff --git a/lib/simple_ui/level/test.lua b/lib/simple_ui/level/test.lua new file mode 100644 index 0000000..d8a76f8 --- /dev/null +++ b/lib/simple_ui/level/test.lua @@ -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 {} + } +} diff --git a/lib/simple_ui/multi_child_element.lua b/lib/simple_ui/multi_child_element.lua new file mode 100644 index 0000000..37af6d0 --- /dev/null +++ b/lib/simple_ui/multi_child_element.lua @@ -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 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..6578e13 --- /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 = 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 diff --git a/lib/simple_ui/rect.lua b/lib/simple_ui/rect.lua index 932a0ad..3f3931f 100644 --- a/lib/simple_ui/rect.lua +++ b/lib/simple_ui/rect.lua @@ -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 diff --git a/lib/simple_ui/screen_area.lua b/lib/simple_ui/screen_area.lua new file mode 100644 index 0000000..816f6ba --- /dev/null +++ b/lib/simple_ui/screen_area.lua @@ -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 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/simple_ui/sized_box.lua b/lib/simple_ui/sized_box.lua new file mode 100644 index 0000000..4bb0a2e --- /dev/null +++ b/lib/simple_ui/sized_box.lua @@ -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 diff --git a/main.lua b/main.lua index 112fc59..5475284 100644 --- a/main.lua +++ b/main.lua @@ -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)