From 552ccff0ec31213d5d0b3b29f4af2450725abbd8 Mon Sep 17 00:00:00 2001 From: Endaris Date: Sat, 10 May 2025 21:52:31 +0200 Subject: [PATCH 01/49] add layouting logic and adapt it for UiElement --- client/src/scenes/Scene.lua | 1 + client/src/ui/Layouts/FlexLayout.lua | 63 +++++++ .../src/ui/Layouts/HorizontalFlexLayout.lua | 155 ++++++++++++++++++ client/src/ui/Layouts/Layout.lua | 34 ++++ client/src/ui/Layouts/VerticalFlexLayout.lua | 155 ++++++++++++++++++ client/src/ui/Menu.lua | 10 +- client/src/ui/UIElement.lua | 98 +++++++---- client/src/ui/init.lua | 8 + 8 files changed, 489 insertions(+), 35 deletions(-) create mode 100644 client/src/ui/Layouts/FlexLayout.lua create mode 100644 client/src/ui/Layouts/HorizontalFlexLayout.lua create mode 100644 client/src/ui/Layouts/Layout.lua create mode 100644 client/src/ui/Layouts/VerticalFlexLayout.lua diff --git a/client/src/scenes/Scene.lua b/client/src/scenes/Scene.lua index d2e7fb07..78e713b5 100644 --- a/client/src/scenes/Scene.lua +++ b/client/src/scenes/Scene.lua @@ -20,6 +20,7 @@ local Scene = class( ---@param self Scene function (self, sceneParams) self.uiRoot = ui.UiElement({x = 0, y = 0, width = consts.CANVAS_WIDTH, height = consts.CANVAS_HEIGHT}) + self.uiRoot.controlsWindow = true -- scenes may specify theme music to use that is played once they are switched to -- eligible labels: -- main diff --git a/client/src/ui/Layouts/FlexLayout.lua b/client/src/ui/Layouts/FlexLayout.lua new file mode 100644 index 00000000..bceaddba --- /dev/null +++ b/client/src/ui/Layouts/FlexLayout.lua @@ -0,0 +1,63 @@ +local PATH = (...):gsub('%.[^%.]+$', '') +local Layout = require(PATH ..".Layout") + +---@class FlexLayout : Layout +local FlexLayout = setmetatable({}, {__index = Layout}) + +---@param uiElement UiElement +function FlexLayout.fitSizeWidth(uiElement) + for _, child in ipairs(uiElement.children) do + child.layout.fitSizeWidth(child) + end + local w = uiElement.layout.getMinWidth(uiElement) + uiElement.width = math.max(w, uiElement.minWidth) +end + +---@param uiElement UiElement +function FlexLayout.fitSizeHeight(uiElement) + for _, child in ipairs(uiElement.children) do + child.layout.fitSizeHeight(child) + end + local h = uiElement.layout.getMinHeight(uiElement) + uiElement.height = math.max(h, uiElement.minHeight) +end + +function FlexLayout.updateWidths(uiElement, width) + uiElement.layout.fitSizeWidth(uiElement) + if width then + uiElement.width = math.max(width, uiElement.width) + end + uiElement.layout.growChildrenWidth(uiElement) +end + +function FlexLayout.updateHeights(uiElement, height) + uiElement.layout.fitSizeHeight(uiElement) + if height then + uiElement.height = math.max(height, uiElement.height) + end + uiElement.layout.growChildrenHeight(uiElement) +end + +---@param uiElement UiElement +---@return number +function FlexLayout.getMinWidth(uiElement) + error("FlexLayout does not implement getMinWidth") +end + +---@param uiElement UiElement +---@return number +function FlexLayout.getMinHeight(uiElement) + error("FlexLayout does not implement getMinHeight") +end + +---@param uiElement UiElement +function FlexLayout.growChildrenWidth(uiElement) + error("FlexLayout does not implement growChildrenWidth") +end + +---@param uiElement UiElement +function FlexLayout.growChildrenHeight(uiElement) + error("FlexLayout does not implement growChildrenHeight") +end + +return FlexLayout \ No newline at end of file diff --git a/client/src/ui/Layouts/HorizontalFlexLayout.lua b/client/src/ui/Layouts/HorizontalFlexLayout.lua new file mode 100644 index 00000000..418a1c1b --- /dev/null +++ b/client/src/ui/Layouts/HorizontalFlexLayout.lua @@ -0,0 +1,155 @@ +local PATH = (...):gsub('%.[^%.]+$', '') +local FlexLayout = require(PATH ..".FlexLayout") + +---@class HorizontalFlexLayout : FlexLayout +local HorizontalFlexLayout = setmetatable({}, {__index = FlexLayout}) + +---@param uiElement UiElement +function HorizontalFlexLayout.getMinWidth(uiElement) + local w = uiElement.padding * 2 + uiElement.childGap * (#uiElement.children - 1) + + for _, child in ipairs(uiElement.children) do + w = w + child.width + end + + return w +end + +---@param uiElement UiElement +function HorizontalFlexLayout.getMinHeight(uiElement) + local h = uiElement.padding * 2 + local maxHeight = 0 + + for _, child in ipairs(uiElement.children) do + maxHeight = math.max(maxHeight, child.height) + end + + return h + maxHeight +end + +---@param uiElement UiElement +function HorizontalFlexLayout.growChildrenWidth(uiElement) + if #uiElement.children == 0 then + return + end + + local growables = {} + + for i, child in ipairs(uiElement.children) do + if child.hFill and child.width < child.maxWidth then + growables[#growables+1] = child + child.newWidth = child.width + end + end + + if #growables > 0 then + local remainingWidth = uiElement.width - uiElement.layout.getMinWidth(uiElement) + + while #growables > 0 and remainingWidth > 0 do + local smallest = growables[1].newWidth + local secondSmallest = math.huge + -- if growables[1] is already the smallest, it will increment the counter at some point within the loop so we can't count it yet + local smallestCount = 1 + + for i = 2, #growables do + local growable = growables[i] + if growable.newWidth < smallest then + secondSmallest = smallest + smallest = growable.newWidth + smallestCount = 1 + elseif growable.newWidth > smallest then + secondSmallest = math.min(secondSmallest, growable.newWidth) + else + smallestCount = smallestCount + 1 + end + end + + local delta = secondSmallest - smallest + local widthToAdd = math.min(delta, remainingWidth / smallestCount) + + if delta * smallestCount >= remainingWidth then + for _, growable in ipairs(growables) do + if growable.newWidth == smallest then + local toAdd = math.min(widthToAdd, growable.maxWidth - growable.newWidth) + growable.newWidth = growable.newWidth + toAdd + remainingWidth = remainingWidth - toAdd + end + end + if remainingWidth < 1 then + -- we could run into floating point shenanigans here + remainingWidth = 0 + end + else + for _, growable in ipairs(growables) do + if growable.newWidth == smallest then + local toAdd = math.min(widthToAdd, growable.maxWidth - growable.newWidth) + growable.newWidth = growable.newWidth + toAdd + remainingWidth = remainingWidth - toAdd + end + end + end + + for i = #growables, 1, -1 do + local growable = growables[i] + if growable.newWidth >= growable.maxWidth then + growable.width = growable.newWidth + table.remove(growables, i) + end + end + end + + for _, growable in ipairs(growables) do + growable.width = growable.newWidth + end + end + + for _, child in ipairs(uiElement.children) do + child.layout.growChildrenWidth(child) + end +end + +---@param uiElement UiElement +function HorizontalFlexLayout.growChildrenHeight(uiElement) + for _, child in ipairs(uiElement.children) do + if child.vFill then + child.height = math.min(uiElement.height - uiElement.padding * 2, child.maxHeight) + end + child.layout.growChildrenHeight(child) + end +end + +---@param uiElement UiElement +function HorizontalFlexLayout.positionChildren(uiElement) + local remainingWidth = uiElement.width - (uiElement.padding * 2 + uiElement.childGap * (#uiElement.children - 1)) + for _, child in ipairs(uiElement.children) do + remainingWidth = remainingWidth - child.width + end + + local x = uiElement.padding + for _, child in ipairs(uiElement.children) do + if child.hAlign == "left" then + child.x = x + elseif child.hAlign == "center" then + child.x = x + remainingWidth / 2 + elseif child.hAlign == "right" then + child.x = x + remainingWidth + end + + if child.vAlign == "top" then + child.y = uiElement.padding + elseif child.vAlign == "center" then + child.y = (uiElement.height - child.height) / 2 + elseif child.vAlign == "bottom" then + child.y = (uiElement.height - child.height) - uiElement.padding + end + child.x = math.round(child.x) + child.y = math.round(child.y) + x = x + uiElement.childGap + child.width + end + + for _, child in ipairs(uiElement.children) do + child.layout.positionChildren(child) + end +end + +return HorizontalFlexLayout \ No newline at end of file diff --git a/client/src/ui/Layouts/Layout.lua b/client/src/ui/Layouts/Layout.lua new file mode 100644 index 00000000..0fcd32d4 --- /dev/null +++ b/client/src/ui/Layouts/Layout.lua @@ -0,0 +1,34 @@ +---@class Layout +local Layout = {} + +---@param uiElement UiElement +---@param width number? +---@param height number? +function Layout.resize(uiElement, width, height) + uiElement.layout.updateWidths(uiElement, width) + + -- transform width to height for width-to-height supporting uiElements based on the width pass + uiElement:setMinHeightForWidth() + + uiElement.layout.updateHeights(uiElement, height) + + uiElement.layout.positionChildren(uiElement) +end + +---@param uiElement UiElement +function Layout.updateWidths(uiElement, width) + error("Layout does not implement positionChildren") +end + +---@param uiElement UiElement +function Layout.updateHeights(uiElement, height) + error("Layout does not implement positionChildren") +end + +---@param uiElement UiElement +function Layout.positionChildren(uiElement) + error("Layout does not implement positionChildren") +end + + +return Layout \ No newline at end of file diff --git a/client/src/ui/Layouts/VerticalFlexLayout.lua b/client/src/ui/Layouts/VerticalFlexLayout.lua new file mode 100644 index 00000000..75d6a4c8 --- /dev/null +++ b/client/src/ui/Layouts/VerticalFlexLayout.lua @@ -0,0 +1,155 @@ +local PATH = (...):gsub('%.[^%.]+$', '') +local FlexLayout = require(PATH ..".FlexLayout") + +---@class VerticalFlexLayout : FlexLayout +local VerticalFlexLayout = setmetatable({}, {__index = FlexLayout}) + +---@param uiElement UiElement +function VerticalFlexLayout.getMinWidth(uiElement) + local w = uiElement.padding * 2 + local maxWidth = 0 + + for _, child in ipairs(uiElement.children) do + maxWidth = math.max(maxWidth, child.width) + end + + return w + maxWidth +end + +---@param uiElement UiElement +function VerticalFlexLayout.getMinHeight(uiElement) + local h = uiElement.padding * 2 + uiElement.childGap * (#uiElement.children - 1) + + for _, child in ipairs(uiElement.children) do + h = h + child.height + end + + return h +end + +---@param uiElement UiElement +function VerticalFlexLayout.growChildrenHeight(uiElement) + if #uiElement.children == 0 then + return + end + + local growables = {} + + for i, child in ipairs(uiElement.children) do + if child.vFill and child.height < child.maxHeight then + growables[#growables+1] = child + child.newHeight = child.height + end + end + + if #growables > 0 then + local remainingHeight = uiElement.height - uiElement.layout.getMinHeight(uiElement) + + while #growables > 0 and remainingHeight > 0 do + local smallest = growables[1].newHeight + local secondSmallest = math.huge + -- if growables[1] is already the smallest, it will increment the counter at some point within the loop so we can't count it yet + local smallestCount = 1 + + for i = 2, #growables do + local growable = growables[i] + if growable.newHeight < smallest then + secondSmallest = smallest + smallest = growable.newHeight + smallestCount = 1 + elseif growable.newHeight > smallest then + secondSmallest = math.min(secondSmallest, growable.newHeight) + else + smallestCount = smallestCount + 1 + end + end + + local delta = secondSmallest - smallest + local heightToAdd = math.min(delta, remainingHeight / smallestCount) + + if delta * smallestCount >= remainingHeight then + for _, growable in ipairs(growables) do + if growable.newHeight == smallest then + local toAdd = math.min(heightToAdd, growable.maxHeight - growable.newHeight) + growable.newHeight = growable.newHeight + toAdd + remainingHeight = remainingHeight - toAdd + end + end + if remainingHeight < 1 then + -- we could run into floating point shenanigans here + remainingHeight = 0 + end + else + for _, growable in ipairs(growables) do + if growable.newHeight == smallest then + local toAdd = math.min(heightToAdd, growable.maxHeight - growable.newHeight) + growable.newHeight = growable.newHeight + toAdd + remainingHeight = remainingHeight - toAdd + end + end + end + + for i = #growables, 1, -1 do + local growable = growables[i] + if growable.newHeight >= growable.maxHeight then + growable.height = growable.newHeight + table.remove(growables, i) + end + end + end + + for _, growable in ipairs(growables) do + growable.height = growable.newHeight + end + end + + for _, child in ipairs(uiElement.children) do + child.layout.growChildrenHeight(child) + end +end + +---@param uiElement UiElement +function VerticalFlexLayout.growChildrenWidth(uiElement) + for _, child in ipairs(uiElement.children) do + if child.hFill then + child.width = math.min(uiElement.width - uiElement.padding * 2, child.maxWidth) + end + child.layout.growChildrenWidth(child) + end +end + +---@param uiElement UiElement +function VerticalFlexLayout.positionChildren(uiElement) + local remainingHeight = uiElement.height - (uiElement.padding * 2 + uiElement.childGap * (#uiElement.children - 1)) + for _, child in ipairs(uiElement.children) do + remainingHeight = remainingHeight - child.height + end + + local y = uiElement.padding + for _, child in ipairs(uiElement.children) do + if child.vAlign == "top" then + child.y = y + elseif child.vAlign == "center" then + child.y = y + remainingHeight / 2 + elseif child.vAlign == "right" then + child.y = y + remainingHeight + end + + if child.hAlign == "left" then + child.x = uiElement.padding + elseif child.hAlign == "center" then + child.x = (uiElement.width - child.width) / 2 + elseif child.hAlign == "right" then + child.x = (uiElement.width - child.width) - uiElement.padding + end + child.x = math.round(child.x) + child.y = math.round(child.y) + y = y + uiElement.childGap + child.height + end + + for _, child in ipairs(uiElement.children) do + child.layout.positionChildren(child) + end +end + +return VerticalFlexLayout \ No newline at end of file diff --git a/client/src/ui/Menu.lua b/client/src/ui/Menu.lua index 0bc6fbab..b6d11c4f 100644 --- a/client/src/ui/Menu.lua +++ b/client/src/ui/Menu.lua @@ -80,7 +80,7 @@ function Menu:setMenuItems(menuItems) self:setSelectedIndex(1) end -function Menu:layout() +function Menu:updateLayout() self.upIndicator:setVisibility(false) self.downIndicator:setVisibility(false) self.allContentShowing = self.yOffset == 0 @@ -143,7 +143,7 @@ function Menu:addMenuItem(index, menuItem) if needsIncreasedIndex then self:setSelectedIndex(self.selectedIndex + 1) end - self:layout() + self:updateLayout() end function Menu:removeMenuItemAtIndex(index) @@ -184,7 +184,7 @@ function Menu:removeMenuItem(menuItemId) self:setSelectedIndex(self.selectedIndex - 1) end - self:layout() + self:updateLayout() return menuItem end @@ -215,7 +215,7 @@ function Menu:setSelectedIndex(index) if #self.menuItems > 0 then self.menuItems[self.selectedIndex]:setSelected(true) end - self:layout() + self:updateLayout() end function Menu:scrollUp() @@ -291,7 +291,7 @@ function Menu:onDrag(x, y) else self.yOffset = math.min(self.totalHeight - self.height + 50, self.originalY - yOffset) end - self:layout() + self:updateLayout() end else self.touchedChild:onDrag(x, y) diff --git a/client/src/ui/UIElement.lua b/client/src/ui/UIElement.lua index 927e84c5..6a8af84d 100644 --- a/client/src/ui/UIElement.lua +++ b/client/src/ui/UIElement.lua @@ -1,11 +1,17 @@ local class = require("common.lib.class") -local GraphicsUtil = require("client.src.graphics.graphics_util") + +---@alias color { [1]: number, [2]: number, [3]: number, [4]: number } ---@class UiElement ---@field x number relative x offset to the parent element (canvas if no parent) ---@field y number relative y offset to the parent element (canvas if no parent) +---@field minWidth number ---@field width number width of the element for the sake of resizing children and touch hitboxes +---@field maxWidth number +---@field minHeight number ---@field height number height of the element for the sake of resizing children and touch hitboxes +---@field maxHeight number +---@field fixedHeight boolean ---@field hAlign ("left" | "center" | "right") determines the horizontal alignment relative to the parent ---@field vAlign ("top" | "center" | "bottom") determines the vertical alignment relative to the parent ---@field hFill boolean if the element's width should fill out the entire parent's width @@ -14,7 +20,11 @@ local GraphicsUtil = require("client.src.graphics.graphics_util") ---@field isEnabled boolean if the element is currently eligible for touch interaction ---@field parent UiElement the element's parent it is getting position relative to via its properties ---@field children UiElement[] the element's children that get positioned relative to it +---@field backgroundColor color +---@field padding number +---@field childGap number ---@field id integer unique identifier of the element +---@field layout FlexLayout ---@field onTouch function? touch callback for when the element is being touched ---@field onDrag function? touch callback for when the mouse touching the element is dragged across the screen ---@field onRelease function? touch callback for when the mouse touching the element is released @@ -24,14 +34,22 @@ local GraphicsUtil = require("client.src.graphics.graphics_util") ---@class UiElementOptions ---@field x number? relative x offset to the parent element (canvas if no parent) ---@field y number? relative y offset to the parent element (canvas if no parent) +---@field minWidth number? ---@field width number? width of the element for the sake of resizing children and touch hitboxes +---@field maxWidth number? +---@field minHeight number? ---@field height number? height of the element for the sake of resizing children and touch hitboxes +---@field maxHeight number? ---@field hAlign ("left" | "center" | "right")? determines the horizontal alignment relative to the parent ---@field vAlign ("top" | "center" | "bottom")? determines the vertical alignment relative to the parent ---@field hFill boolean? if the element's width should fill out the entire parent's width ---@field vFill boolean? if the element's height should fill out the entire parent's height ---@field isVisible boolean? if the element is currently visible for rendering (also disables touch interaction) ---@field isEnabled boolean? if the element is currently eligible for touch interaction +---@field layout FlexLayout? +---@field backgroundColor color? +---@field padding number? +---@field childGap number? local uniqueId = 0 @@ -47,9 +65,22 @@ local UIElement = class( self.x = options.x or 0 self.y = options.y or 0 + self.backgroundColor = options.backgroundColor or {1, 1, 1, 0} + self.layout = options.layout + self.padding = options.padding or 0 + self.childGap = options.childGap or 0 + -- ui dimensions + self.minWidth = options.minWidth or options.width or 0 self.width = options.width or 0 + self.maxWidth = options.maxWidth or options.width or math.huge + self.minHeight = options.minHeight or options.height or 0 self.height = options.height or 0 + self.maxHeight = options.maxHeight or options.height or math.huge + + if self.padding * 2 > self.maxHeight or self.padding * 2 > self.maxWidth then + error("The size of the padding cannot exceed the UIElements maximum dimensions") + end -- how to align the element inside the parent element self.hAlign = options.hAlign or "left" @@ -78,34 +109,34 @@ local UIElement = class( UIElement.TYPE = "UIElement" -function UIElement:addChild(uiElement) - if uiElement.parent ~= self then - self.children[#self.children + 1] = uiElement - uiElement.parent = self - uiElement:resize() - end -end - -function UIElement:resize() - if self.hFill and self.parent then - self.width = self.parent.width - end - - if self.vFill and self.parent then - self.height = self.parent.height - end - - self:onResize() - - if self.hFill or self.vFill then - for _, child in ipairs(self.children) do - child:resize() +local function onChildrenChanged(uiElement) + if uiElement.parent then + -- resizing segments does not make much sense if the segments have to be resized later anyway + -- so bubble just straight up + onChildrenChanged(uiElement.parent) + else + -- and then only resize from the root element + local oWidth = uiElement.width + local oHeight = uiElement.height + + uiElement.layout.resize(uiElement) + + if uiElement.controlsWindow then + if oWidth ~= uiElement.width or oHeight ~= uiElement.height then + love.window.updateMode(uiElement.width, uiElement.height, {}) + end end end end --- overridable function to define extra behaviour to the element itself on resize -function UIElement:onResize() +function UIElement:addChild(uiElement) + if uiElement.parent then + error("Tried to give a uiElement more than one parent") + else + self.children[#self.children + 1] = uiElement + uiElement.parent = self + onChildrenChanged(uiElement.parent) + end end function UIElement:detach() @@ -114,6 +145,7 @@ function UIElement:detach() if child.id == self.id then table.remove(self.parent.children, i) self:onDetach() + onChildrenChanged(self.parent) self.parent = nil break end @@ -127,13 +159,11 @@ end function UIElement:getScreenPos() local x, y = 0, 0 - local xOffset, yOffset = 0, 0 if self.parent then x, y = self.parent:getScreenPos() - xOffset, yOffset = GraphicsUtil.getAlignmentOffset(self.parent, self) end - return x + self.x + xOffset, y + self.y + yOffset + return x + self.x, y + self.y end -- passes a retranslation request through the tree to reach all Labels @@ -159,14 +189,16 @@ end -- UiElements containing children draw the children-independent part in this function -- implementation is optional so layout elements don't have to function UIElement:drawSelf() + love.graphics.setColor(self.backgroundColor) + love.graphics.rectangle("fill", self.x, self.y, self.width, self.height) + love.graphics.setColor(1, 1, 1, 1) + --love.graphics.print(self.width .. ", " .. self.height, self.x + 5, self.y + 5) end function UIElement:drawChildren() for _, uiElement in ipairs(self.children) do if uiElement.isVisible then - GraphicsUtil.applyAlignment(self, uiElement) uiElement:draw() - GraphicsUtil.resetAlignment() end end end @@ -217,4 +249,10 @@ function UIElement:getTouchedElement(x, y) end end +function UIElement:setMinHeightForWidth() + for i, child in ipairs(self.children) do + child:setMinHeightForWidth() + end +end + return UIElement \ No newline at end of file diff --git a/client/src/ui/init.lua b/client/src/ui/init.lua index 6cf18ae4..da6e2720 100644 --- a/client/src/ui/init.lua +++ b/client/src/ui/init.lua @@ -18,6 +18,10 @@ local ui = { ---@see Label ---@type fun(options: LabelOptions): Label Label = require(PATH .. ".Label"), + Layouts = { + HorizontalFlexLayout = require(PATH .. ".Layouts.HorizontalFlexLayout"), + VerticalFlexLayout = require(PATH .. ".Layouts.VerticalFlexLayout"), + }, Leaderboard = require(PATH .. ".Leaderboard"), ---@see LevelSlider ---@type fun(options: SliderOptions): LevelSlider @@ -45,8 +49,12 @@ local ui = { TextButton = require(PATH .. ".TextButton"), ---@see UiElement ---@type fun(options:UiElementOptions): UiElement + ---@class UiElement UiElement = require(PATH .. ".UIElement"), ValueLabel = require(PATH .. ".ValueLabel"), } +-- the default layout +ui.UiElement.layout = ui.Layouts.HorizontalFlexLayout + return ui \ No newline at end of file From 130bae50197abb2ba4183b3e4fc168494d0c2ef7 Mon Sep 17 00:00:00 2001 From: Endaris Date: Sun, 11 May 2025 00:22:06 +0200 Subject: [PATCH 02/49] make specialized scrollcontainer to replace Menu --- client/src/Game.lua | 1 + client/src/scenes/InputConfigMenu.lua | 2 +- client/src/scenes/MainMenu.lua | 189 ++++++++++-------- client/src/scenes/Scene.lua | 3 +- client/src/ui/FocusDirector.lua | 4 + client/src/ui/Focusable.lua | 6 + client/src/ui/Label.lua | 151 ++++++-------- .../src/ui/Layouts/HorizontalFlexLayout.lua | 3 +- client/src/ui/Layouts/Layout.lua | 4 + client/src/ui/Layouts/VerticalFlexLayout.lua | 3 +- client/src/ui/Menu.lua | 6 +- client/src/ui/MenuItem.lua | 35 ++-- client/src/ui/ScrollContainer.lua | 41 +++- client/src/ui/TextButton.lua | 6 - client/src/ui/UIElement.lua | 2 +- client/src/ui/VerticalMenu.lua | 102 ++++++++++ client/src/ui/init.lua | 5 +- common/lib/stringExtensions.lua | 17 ++ common/lib/util.lua | 18 +- 19 files changed, 376 insertions(+), 222 deletions(-) create mode 100644 client/src/ui/VerticalMenu.lua diff --git a/client/src/Game.lua b/client/src/Game.lua index d15a1d49..bd725929 100644 --- a/client/src/Game.lua +++ b/client/src/Game.lua @@ -1,3 +1,4 @@ +require("common.lib.stringExtensions") require("client.src.localization") require("common.lib.Queue") require("client.src.server_queue") diff --git a/client/src/scenes/InputConfigMenu.lua b/client/src/scenes/InputConfigMenu.lua index ab9187a0..e56acbce 100644 --- a/client/src/scenes/InputConfigMenu.lua +++ b/client/src/scenes/InputConfigMenu.lua @@ -53,7 +53,7 @@ end function InputConfigMenu:getKeyDisplayName(key) local keyDisplayName = key if key and string.match(key, ":") then - local controllerKeySplit = util.split(key, ":") + local controllerKeySplit = string.split(key, ":") local controllerName = shortenControllerName(joystickManager.guidToName[controllerKeySplit[1]] or "Unplugged Controller") keyDisplayName = string.format("%s (%s-%s)", controllerKeySplit[3], controllerName, controllerKeySplit[2]) end diff --git a/client/src/scenes/MainMenu.lua b/client/src/scenes/MainMenu.lua index bf13cb74..c2743588 100644 --- a/client/src/scenes/MainMenu.lua +++ b/client/src/scenes/MainMenu.lua @@ -40,92 +40,123 @@ local function switchToScene(sceneName, transition) end function MainMenu:createMainMenu() + local menuContainer = ui.VerticalMenu({ + hAlign = "center", + minHeight = 480, + maxHeight = 540, + childGap = 8, + padding = 32, + backgroundColor = {1, 0, 0, 0.2} + }) + + local endless = ui.MenuItem.createButtonMenuItem("mm_1_endless", nil, nil, function() + GAME.battleRoom = BattleRoom.createLocalFromGameMode(GameModes.getPreset("ONE_PLAYER_ENDLESS"), EndlessGame) + if GAME.battleRoom then + switchToScene(EndlessMenu({battleRoom = GAME.battleRoom})) + end + end) - local menuItems = {ui.MenuItem.createButtonMenuItem("mm_1_endless", nil, nil, function() - GAME.battleRoom = BattleRoom.createLocalFromGameMode(GameModes.getPreset("ONE_PLAYER_ENDLESS"), EndlessGame) - if GAME.battleRoom then - switchToScene(EndlessMenu({battleRoom = GAME.battleRoom})) - end - end), - ui.MenuItem.createButtonMenuItem("mm_1_puzzle", nil, nil, function() - GAME.battleRoom = BattleRoom.createLocalFromGameMode(GameModes.getPreset("ONE_PLAYER_PUZZLE"), PuzzleGame) - if GAME.battleRoom then - switchToScene(PuzzleMenu({battleRoom = GAME.battleRoom})) - end - end), - ui.MenuItem.createButtonMenuItem("mm_1_time", nil, nil, function() - GAME.battleRoom = BattleRoom.createLocalFromGameMode(GameModes.getPreset("ONE_PLAYER_TIME_ATTACK"), TimeAttackGame) - if GAME.battleRoom then - switchToScene(TimeAttackMenu({battleRoom = GAME.battleRoom})) - end - end), - ui.MenuItem.createButtonMenuItem("mm_1_vs", nil, nil, function() - GAME.battleRoom = BattleRoom.createLocalFromGameMode(GameModes.getPreset("ONE_PLAYER_VS_SELF"), VsSelfGame) - if GAME.battleRoom then - switchToScene(CharacterSelectVsSelf({battleRoom = GAME.battleRoom})) - end - end), - ui.MenuItem.createButtonMenuItem("mm_1_training", nil, nil, function() - switchToScene(TrainingMenu()) - end), - ui.MenuItem.createButtonMenuItem("mm_1_challenge_mode", nil, nil, function() - switchToScene(ChallengeModeMenu()) - end), - ui.MenuItem.createButtonMenuItem("mm_2_vs_online", {""}, nil, function() - switchToScene(Lobby({serverIp = "panelattack.com"})) - end), - ui.MenuItem.createButtonMenuItem("mm_2_vs_local", nil, nil, function() - GAME.battleRoom = BattleRoom.createLocalFromGameMode(GameModes.getPreset("TWO_PLAYER_VS"), GameBase) - if GAME.battleRoom then - switchToScene(CharacterSelect2p({battleRoom = GAME.battleRoom})) - end - end), - ui.MenuItem.createButtonMenuItem("mm_replay_browser", nil, nil, function() - switchToScene(ReplayBrowser()) - end), - ui.MenuItem.createButtonMenuItem("mm_configure", nil, nil, function() - switchToScene(InputConfigMenu()) - end), - ui.MenuItem.createButtonMenuItem("mm_set_name", nil, nil, function() - switchToScene(SetNameMenu()) - end), - ui.MenuItem.createButtonMenuItem("mm_options", nil, nil, function() - switchToScene(OptionsMenu()) - end), - ui.MenuItem.createButtonMenuItem("mm_fullscreen", {"\n(Alt+Enter)"}, nil, function() - GAME.theme:playValidationSfx() - GAME:toggleFullscreen() - end), - ui.MenuItem.createButtonMenuItem("mm_quit", nil, nil, function() love.event.quit() end ) - } - - local menu = ui.Menu.createCenteredMenu(menuItems) - - local debugMenuItems = {ui.MenuItem.createButtonMenuItem("Beta Server", nil, nil, function() switchToScene(Lobby({serverIp = "betaserver.panelattack.com", serverPort = 59569})) end), - ui.MenuItem.createButtonMenuItem("Localhost Server", nil, nil, function() switchToScene(Lobby({serverIp = "Localhost"})) end) - } - - local function addDebugMenuItems() - if config.debugShowServers then - for i, menuItem in ipairs(debugMenuItems) do - menu:addMenuItem(i + 7, menuItem) - end + local puzzle = ui.MenuItem.createButtonMenuItem("mm_1_puzzle", nil, nil, function() + GAME.battleRoom = BattleRoom.createLocalFromGameMode(GameModes.getPreset("ONE_PLAYER_PUZZLE"), PuzzleGame) + if GAME.battleRoom then + switchToScene(PuzzleMenu({battleRoom = GAME.battleRoom})) end - if config.debugShowDesignHelper then - menu:addMenuItem(#menu.menuItems, ui.MenuItem.createButtonMenuItem("Design Helper", nil, nil, function() - switchToScene(DesignHelper()) - end)) + end) + + local time = ui.MenuItem.createButtonMenuItem("mm_1_time", nil, nil, function() + GAME.battleRoom = BattleRoom.createLocalFromGameMode(GameModes.getPreset("ONE_PLAYER_TIME_ATTACK"), TimeAttackGame) + if GAME.battleRoom then + switchToScene(TimeAttackMenu({battleRoom = GAME.battleRoom})) end - end + end) - local function removeDebugMenuItems() - for i, menuItem in ipairs(debugMenuItems) do - menu:removeMenuItem(menuItem[1].id) + local vsSelf = ui.MenuItem.createButtonMenuItem("mm_1_vs", nil, nil, function() + GAME.battleRoom = BattleRoom.createLocalFromGameMode(GameModes.getPreset("ONE_PLAYER_VS_SELF"), VsSelfGame) + if GAME.battleRoom then + switchToScene(CharacterSelectVsSelf({battleRoom = GAME.battleRoom})) end + end) + local training = ui.MenuItem.createButtonMenuItem("mm_1_training", nil, nil, function() + switchToScene(TrainingMenu()) + end) + + local challenge = ui.MenuItem.createButtonMenuItem("mm_1_challenge_mode", nil, nil, function() + switchToScene(ChallengeModeMenu()) + end) + + local vsOnline = ui.MenuItem.createButtonMenuItem("mm_2_vs_online", {""}, nil, function() + switchToScene(Lobby({serverIp = "panelattack.com"})) + end) + + local vsLocal = ui.MenuItem.createButtonMenuItem("mm_2_vs_local", nil, nil, function() + GAME.battleRoom = BattleRoom.createLocalFromGameMode(GameModes.getPreset("TWO_PLAYER_VS"), GameBase) + if GAME.battleRoom then + switchToScene(CharacterSelect2p({battleRoom = GAME.battleRoom})) + end + end) + + local betaServer = ui.MenuItem.createButtonMenuItem("Beta Server", nil, false, function() + switchToScene(Lobby({serverIp = "betaserver.panelattack.com", serverPort = 59569})) + end) + + local localServer = ui.MenuItem.createButtonMenuItem("Localhost Server", nil, false, function() + switchToScene(Lobby({serverIp = "Localhost"})) + end) + + local replayBrowser = ui.MenuItem.createButtonMenuItem("mm_replay_browser", nil, nil, function() + switchToScene(ReplayBrowser()) + end) + + local inputConfig = ui.MenuItem.createButtonMenuItem("mm_configure", nil, nil, function() + switchToScene(InputConfigMenu()) + end) + + local setName = ui.MenuItem.createButtonMenuItem("mm_set_name", nil, nil, function() + switchToScene(SetNameMenu()) + end) + + local options = ui.MenuItem.createButtonMenuItem("mm_options", nil, nil, function() + switchToScene(OptionsMenu()) + end) + + local fullscreenToggle = ui.MenuItem.createButtonMenuItem("mm_fullscreen", {"\n(Alt+Enter)"}, nil, function() + GAME.theme:playValidationSfx() + GAME:toggleFullscreen() + end) + + local quit = ui.MenuItem.createButtonMenuItem("mm_quit", nil, nil, function() + love.event.quit() + end) + + local designHelper = ui.MenuItem.createButtonMenuItem("Design Helper", nil, false, function() + switchToScene(DesignHelper()) + end) + + menuContainer:addChild(endless) + menuContainer:addChild(puzzle) + menuContainer:addChild(time) + menuContainer:addChild(vsSelf) + menuContainer:addChild(training) + menuContainer:addChild(challenge) + menuContainer:addChild(vsOnline) + menuContainer:addChild(vsLocal) + if config.debugShowServers then + menuContainer:addChild(betaServer) + menuContainer:addChild(localServer) + end + menuContainer:addChild(replayBrowser) + menuContainer:addChild(inputConfig) + if config.name == "" then + menuContainer:addChild(setName) + end + menuContainer:addChild(options) + menuContainer:addChild(fullscreenToggle) + menuContainer:addChild(quit) + if config.debugShowDesignHelper then + menuContainer:addChild(designHelper) end - addDebugMenuItems() - return menu + return menuContainer end local nextUpdate = 900 diff --git a/client/src/scenes/Scene.lua b/client/src/scenes/Scene.lua index 78e713b5..6bb16eec 100644 --- a/client/src/scenes/Scene.lua +++ b/client/src/scenes/Scene.lua @@ -19,7 +19,7 @@ local SoundController = require("client.src.music.SoundController") local Scene = class( ---@param self Scene function (self, sceneParams) - self.uiRoot = ui.UiElement({x = 0, y = 0, width = consts.CANVAS_WIDTH, height = consts.CANVAS_HEIGHT}) + self.uiRoot = ui.UiElement({x = 0, y = 0, width = consts.CANVAS_WIDTH, height = consts.CANVAS_HEIGHT, padding = 16}) self.uiRoot.controlsWindow = true -- scenes may specify theme music to use that is played once they are switched to -- eligible labels: @@ -72,6 +72,7 @@ end function Scene:refreshLocalization() self.uiRoot:refreshLocalization() + self.uiRoot.layout.resize(self.uiRoot, self.uiRoot.width, self.uiRoot.height) end function Scene:drawCommunityMessage() diff --git a/client/src/ui/FocusDirector.lua b/client/src/ui/FocusDirector.lua index 18568ce8..62e0155d 100644 --- a/client/src/ui/FocusDirector.lua +++ b/client/src/ui/FocusDirector.lua @@ -1,5 +1,9 @@ -- use in tandem with Focusable.lua +---@class FocusDirector +---@field setFocus fun(director: table, focusable: table, callback: function?) +---@field focused table? + local function directsFocus(uiElement) uiElement.focused = nil uiElement.setFocus = function(table, focusable, callback) diff --git a/client/src/ui/Focusable.lua b/client/src/ui/Focusable.lua index c61c6d23..6734fbc4 100644 --- a/client/src/ui/Focusable.lua +++ b/client/src/ui/Focusable.lua @@ -1,5 +1,11 @@ -- use in tandem with FocusDirector.lua +---@class Focusable +---@field receiveInputs fun(any, table, number?) +---@field isFocusable boolean +---@field hasFocus boolean? +---@field yieldFocus fun()? + local function focusable(uiElement) uiElement.isFocusable = true uiElement.hasFocus = false diff --git a/client/src/ui/Label.lua b/client/src/ui/Label.lua index 88a45b57..f54024c0 100644 --- a/client/src/ui/Label.lua +++ b/client/src/ui/Label.lua @@ -4,37 +4,78 @@ local class = require("common.lib.class") local GraphicsUtil = require("client.src.graphics.graphics_util") ---@class LabelOptions : UiElementOptions ----@field text string The raw text or localization key ----@field translate boolean? Whether the game looks for a localization for text or not +---@field id string? The localization key; nil if there should be no translation +---@field text string? The raw text; ignored if there is a localization key ---@field replacements string[]? Additional strings to perform string format on a localized key with parts marked for replacement ---@field fontSize integer? The size of the font ---@field wrap boolean? If the font should wrap around ---@field wrapRatio number? By which % of the unwrapped text the text should wrap ---@class Label : UiElement ----@field text string The raw text or localization key ----@field translate boolean Whether the game looks for a localization for text or not +---@field id string? The localization key ---@field replacementTable string[]? Additional strings to perform string format on a localized key with parts marked for replacement +---@field text string The raw text or localization key +---@field font love.Font Cached font for recreating the love.Text on changes ---@field fontSize integer The size of the font ---@field wrap boolean If the font should wrap around ----@field wrapRatio number By which % of the unwrapped text the text should wrap ----@field font love.Font Cached font for recreating the love.Text on changes ----@field drawable love.Text Cached love.Text for redrawing ---@overload fun(options: LabelOptions): Label local Label = class( function(self, options) + self.id = options.id + + if self.id then + self.replacementTable = options.replacements or {} + self.text = loc(self.id, unpack(self.replacementTable)) + else + self.text = options.text or "" + end + self.hAlign = options.hAlign or "left" self.vAlign = options.vAlign or "top" - self.hFill = options.hFill or true + self.font = options.font or love.graphics.getFont() + self.fontSize = options.fontSize or GraphicsUtil.fontSize - self.wrap = options.wrap or false - self.wrapRatio = options.wrapRatio or 1 + local totalWidth = self.font:getWidth(self.text) + if options.wrap ~= nil then + self.wrap = options.wrap + else + self.wrap = true + end - self.fontSize = options.fontSize or GraphicsUtil.fontSize + local words = self.text:split() + local maxWordWidth = self.font:getWidth(words[1]) + for i = 2, #words do + maxWordWidth = math.max(maxWordWidth, self.font:getWidth(words[i])) + end + + if options.minWidth ~= nil then + self.minWidth = options.minWidth + else + if self.wrap then + self.minWidth = maxWordWidth + else + self.minWidth = totalWidth + end + end - self:setText(options.text, options.replacements, options.translate) + self.width = options.width or totalWidth + self.maxWidth = options.maxWidth or totalWidth + self.minHeight = options.minHeight or self.font:getHeight() + self.height = options.height or self.font:getHeight() + + if options.maxHeight ~= nil then + self.maxHeight = options.maxHeight + else + if self.wrap then + self.maxHeight = (#words * self.font:getHeight()) + else + self.maxHeight = self.font:getHeight() + end + end + self.hFill = true + self.vFill = false end, UIElement ) @@ -44,88 +85,20 @@ function Label:getEffectiveDimensions() return self.drawable:getDimensions() end -function Label:setText(text, replacementTable, translate) - if text == self.text and replacementTable == self.replacementTable and self.translate == translate then - return - end - - -- whether we should translate the label or not - if translate ~= nil then - self.translate = translate - elseif self.translate == nil then - self.translate = true - end - - if replacementTable then - -- list of parameters for translating the label (e.g. numbers/names to replace placeholders with) - self.replacementTable = replacementTable - elseif not self.replacementTable then - self.replacementTable = {} - end - - if text then - self.text = text - end - - if self.translate then - -- always need a new text cause the font might have changed - self.drawable = GraphicsUtil.newText(GraphicsUtil.getGlobalFontWithSize(self.fontSize), loc(self.text, unpack(self.replacementTable))) - else - if not self.drawable then - self.drawable = GraphicsUtil.newText(GraphicsUtil.getGlobalFontWithSize(self.fontSize), self.text) - end - end - - self:refreshFormatting() - - self.width = self.drawable:getWidth() - self.height = self.drawable:getHeight() -end - -function Label:setWrap(wrapRatio, hAlign) - self.wrap = not not wrapRatio - self.wrapRatio = wrapRatio - self.hAlign = hAlign or self.hAlign - self:refreshFormatting() -end - -function Label:refreshFormatting() - local text = self.text - - if self.translate then - text = loc(self.text, unpack(self.replacementTable)) - end - - if self.wrap then - self.drawable:setf(text, self.wrapRatio * self.width, self.hAlign) - self.height = self.drawable:getHeight() - else - self.drawable:set(text) - end -end - -function Label:onResize() - if self.wrap then - self.width = math.max(self.width, self.drawable:getWidth()) - else - self.width = self.drawable:getWidth() - end - self.height = self.drawable:getHeight() - self:refreshFormatting() -end - function Label:refreshLocalization() - if self.translate then - local font = GraphicsUtil.getGlobalFontWithSize(self.fontSize) - - -- always need a new text cause the font might have changed - self.drawable = GraphicsUtil.newText(font, loc(self.text, unpack(self.replacementTable))) - self.width, self.height = self.drawable:getDimensions() + if self.id then + self.text = loc(self.text, unpack(self.replacementTable)) end end function Label:drawSelf() - GraphicsUtil.drawClearText(self.drawable, math.round(self.x), math.round(self.y)) + love.graphics.printf(self.text, self.x, self.y, self.width) +end + +function Label:setMinHeightForWidth() + local refText = GraphicsUtil.newText(GraphicsUtil.getGlobalFontWithSize(self.fontSize)) + refText:setf(self.text, self.width, "left") + self.minHeight = refText:getHeight() end return Label \ No newline at end of file diff --git a/client/src/ui/Layouts/HorizontalFlexLayout.lua b/client/src/ui/Layouts/HorizontalFlexLayout.lua index 418a1c1b..70fc29a7 100644 --- a/client/src/ui/Layouts/HorizontalFlexLayout.lua +++ b/client/src/ui/Layouts/HorizontalFlexLayout.lua @@ -1,5 +1,6 @@ local PATH = (...):gsub('%.[^%.]+$', '') local FlexLayout = require(PATH ..".FlexLayout") +local util = require("common.lib.util") ---@class HorizontalFlexLayout : FlexLayout local HorizontalFlexLayout = setmetatable({}, {__index = FlexLayout}) @@ -12,7 +13,7 @@ function HorizontalFlexLayout.getMinWidth(uiElement) w = w + child.width end - return w + return util.bound(uiElement.minWidth, w, uiElement.maxWidth) end ---@param uiElement UiElement diff --git a/client/src/ui/Layouts/Layout.lua b/client/src/ui/Layouts/Layout.lua index 0fcd32d4..d858edeb 100644 --- a/client/src/ui/Layouts/Layout.lua +++ b/client/src/ui/Layouts/Layout.lua @@ -13,6 +13,10 @@ function Layout.resize(uiElement, width, height) uiElement.layout.updateHeights(uiElement, height) uiElement.layout.positionChildren(uiElement) + + if uiElement.onResize then + uiElement:onResize() + end end ---@param uiElement UiElement diff --git a/client/src/ui/Layouts/VerticalFlexLayout.lua b/client/src/ui/Layouts/VerticalFlexLayout.lua index 75d6a4c8..d32befb2 100644 --- a/client/src/ui/Layouts/VerticalFlexLayout.lua +++ b/client/src/ui/Layouts/VerticalFlexLayout.lua @@ -1,5 +1,6 @@ local PATH = (...):gsub('%.[^%.]+$', '') local FlexLayout = require(PATH ..".FlexLayout") +local util = require("common.lib.util") ---@class VerticalFlexLayout : FlexLayout local VerticalFlexLayout = setmetatable({}, {__index = FlexLayout}) @@ -24,7 +25,7 @@ function VerticalFlexLayout.getMinHeight(uiElement) h = h + child.height end - return h + return util.bound(uiElement.minHeight, h, uiElement.maxHeight) end ---@param uiElement UiElement diff --git a/client/src/ui/Menu.lua b/client/src/ui/Menu.lua index b6d11c4f..73572645 100644 --- a/client/src/ui/Menu.lua +++ b/client/src/ui/Menu.lua @@ -17,7 +17,6 @@ local Menu = class( function(self, options) ---@class Menu self = self - self.TYPE = "VerticalScrollingButtonMenu" self.selectedIndex = 1 self.yMin = self.y @@ -42,6 +41,8 @@ local Menu = class( UIElement ) +Menu.TYPE = "VerticalScrollingButtonMenu" + Menu.NAVIGATION_BUTTON_WIDTH = NAVIGATION_BUTTON_WIDTH Menu.BUTTON_HORIZONTAL_PADDING = 0 Menu.BUTTON_VERTICAL_PADDING = 8 @@ -53,7 +54,8 @@ function Menu.createCenteredMenu(items) hAlign = "center", vAlign = "center", menuItems = items, - height = themes[config.theme].main_menu_max_height + height = themes[config.theme].main_menu_max_height, + layout = require(PATH .. ".Layouts.VerticalFlexLayout") }) return menu diff --git a/client/src/ui/MenuItem.lua b/client/src/ui/MenuItem.lua index 508fe210..9c389350 100644 --- a/client/src/ui/MenuItem.lua +++ b/client/src/ui/MenuItem.lua @@ -1,6 +1,7 @@ local PATH = (...):gsub('%.[^%.]+$', '') local UiElement = require(PATH .. ".UIElement") local Label = require(PATH .. ".Label") +local Button = require(PATH .. ".Button") local TextButton = require(PATH .. ".TextButton") local class = require("common.lib.class") local GraphicsUtil = require("client.src.graphics.graphics_util") @@ -53,25 +54,27 @@ end -- Creates a menu item with just a button function MenuItem.createButtonMenuItem(text, replacements, translate, onClick) assert(text ~= nil) - local BUTTON_WIDTH = 140 - if translate == nil then - translate = true + local id + if translate == nil or translate then + id = text + text = nil end - local textButton = TextButton({ - label = Label({ - text = text, - replacements = replacements, - translate = translate, - hAlign = "center", - vAlign = "center" - }), - onClick = onClick, width = BUTTON_WIDTH + local label = Label({ + id = id, + text = text, + replacements = replacements, + hAlign = "center", + vAlign = "center" }) + local button = Button({ + width = 140, + maxWidth = 300, + padding = 8, + onClick = onClick + }) + button:addChild(label) - local menuItem = MenuItem.createMenuItem(textButton) - menuItem.textButton = textButton - - return menuItem + return button end -- Creates a menu item with a label followed by a button diff --git a/client/src/ui/ScrollContainer.lua b/client/src/ui/ScrollContainer.lua index fe674170..79cac434 100644 --- a/client/src/ui/ScrollContainer.lua +++ b/client/src/ui/ScrollContainer.lua @@ -2,6 +2,7 @@ local PATH = (...):gsub('%.[^%.]+$', '') local UiElement = require(PATH .. ".UIElement") local class = require("common.lib.class") local util = require("common.lib.util") +local GraphicsUtil = require("client.src.graphics.graphics_util") ---@class ScrollContainerOptions : UiElementOptions ---@field scrollOrientation ("vertical" | "horizontal" | nil) @@ -76,13 +77,13 @@ function ScrollContainer:onTouch(x, y) end function ScrollContainer:onDrag(x, y) - if not self.touchedChild then + if not self.touchedChild or not self.touchedChild.onDrag then if self.scrollOrientation == "vertical" then self:setScrollOffset(self.originalOffset + (y - self.initialTouchY)) elseif self.scrollOrientation == "horizontal" then self:setScrollOffset(self.originalOffset + (x - self.initialTouchX)) end - elseif self.touchedChild.onDrag then + else x, y = getTranslatedOffset(self, x, y) self.touchedChild:onDrag(x, y) end @@ -105,6 +106,7 @@ local loveMajor = love.getVersion() function ScrollContainer:draw() if self.isVisible then + UiElement.drawSelf(self) -- make a stencil according to width/height if loveMajor >= 12 then love.graphics.setStencilMode("draw", 1) @@ -136,6 +138,26 @@ function ScrollContainer:draw() else love.graphics.setStencilTest() end + + if self.scrollOrientation == "vertical" then + if self.maxScrollOffset > 0 then + if self.scrollOffset < 0 then + GraphicsUtil.print("^", self.x + self.width / 2 - GraphicsUtil.fontSize / 2, self.y - 20) + end + if math.abs(self.scrollOffset) < self.maxScrollOffset then + GraphicsUtil.print("v", self.x + self.width / 2 - GraphicsUtil.fontSize / 2, self.y + self.height + 8) + end + end + else + if self.maxScrollOffset > 0 then + if self.scrollOffset < 0 then + GraphicsUtil.print("<", self.x - 20, self.y + self.height / 2 - GraphicsUtil.fontSize / 2) + end + if math.abs(self.scrollOffset) < self.maxScrollOffset then + GraphicsUtil.print(">", self.x + self.width + 8, self.y + self.height / 2 - GraphicsUtil.fontSize / 2) + end + end + end end end @@ -159,13 +181,16 @@ function ScrollContainer:getTouchedChildElement(x, y) end end --- adds the uiElement and updates the maxScrollOffset if the addition extends the sensible scroll area -function ScrollContainer:addChild(uiElement) - UiElement.addChild(self, uiElement) - if self.scrollOrientation == "vertical" then - self.maxScrollOffset = math.max(self.maxScrollOffset, (uiElement.y + uiElement.height) - self.height) +function ScrollContainer:onResize() + local lastChild = self.children[#self.children] + if lastChild then + if self.scrollOrientation == "vertical" then + self.maxScrollOffset = lastChild.y + lastChild.height - self.height + self.padding + else + self.maxScrollOffset = lastChild.x + lastChild.width - self.width + self.padding + end else - self.maxScrollOffset = math.max(self.maxScrollOffset, (uiElement.x + uiElement.width) - self.width) + self.maxScrollOffset = 0 end end diff --git a/client/src/ui/TextButton.lua b/client/src/ui/TextButton.lua index f4c7bb4c..82744dcc 100644 --- a/client/src/ui/TextButton.lua +++ b/client/src/ui/TextButton.lua @@ -17,12 +17,6 @@ local TextButton = class(function(self, options) self.label.hAlign = "center" self.label.vAlign = "center" self:addChild(self.label) - - -- stretch to fit text - local width, height = self.label:getEffectiveDimensions() - self.width = math.max(width + TEXT_WIDTH_PADDING, self.width) - self.height = math.max(height + TEXT_HEIGHT_PADDING, self.height) - end, Button) TextButton.TYPE = "TextButton" diff --git a/client/src/ui/UIElement.lua b/client/src/ui/UIElement.lua index 6a8af84d..65100092 100644 --- a/client/src/ui/UIElement.lua +++ b/client/src/ui/UIElement.lua @@ -192,7 +192,7 @@ function UIElement:drawSelf() love.graphics.setColor(self.backgroundColor) love.graphics.rectangle("fill", self.x, self.y, self.width, self.height) love.graphics.setColor(1, 1, 1, 1) - --love.graphics.print(self.width .. ", " .. self.height, self.x + 5, self.y + 5) + love.graphics.print(self.width .. ", " .. self.height, self.x + 5, self.y + 5) end function UIElement:drawChildren() diff --git a/client/src/ui/VerticalMenu.lua b/client/src/ui/VerticalMenu.lua new file mode 100644 index 00000000..0717b3ef --- /dev/null +++ b/client/src/ui/VerticalMenu.lua @@ -0,0 +1,102 @@ +local PATH = (...):gsub('%.[^%.]+$', '') +local ScrollContainer = require(PATH .. ".ScrollContainer") +local class = require("common.lib.class") +local util = require("common.lib.util") +local GraphicsUtil = require("client.src.graphics.graphics_util") +local Focusable = require(PATH .. ".Focusable") +local FocusDirector = require(PATH .. ".FocusDirector") +local input = require("client.src.inputManager") + +---@class VerticalMenu : ScrollContainer, Focusable +local VerticalMenu = class( +function(self, options) + self.selectedIndex = nil + self.scrollOrientation = "vertical" +end, +ScrollContainer) + +Focusable(VerticalMenu) +FocusDirector(VerticalMenu) + +function VerticalMenu:setInitialFocus() + for i, child in ipairs(self.children) do + if child.receiveInputs and child.isEnabled and child.isVisible then + self.selectedIndex = i + break + end + end +end + +function VerticalMenu:selectPrevious() + for i = self.selectedIndex - 1, self.selectedIndex - #self.children, -1 do + local index = wrap(1, i, #self.children) + local child = self.children[index] + if child.receiveInputs and child.isEnabled and child.isVisible then + self.selectedIndex = index + break + end + end + self:keepVisible(-self.children[self.selectedIndex].y, self.children[self.selectedIndex].height) + GAME.theme:playMoveSfx() +end + +function VerticalMenu:selectNext() + for i = self.selectedIndex + 1, self.selectedIndex + #self.children do + local index = wrap(1, i, #self.children) + local child = self.children[index] + if child.receiveInputs and child.isEnabled and child.isVisible then + self.selectedIndex = index + break + end + end + self:keepVisible(-self.children[self.selectedIndex].y, self.children[self.selectedIndex].height) + GAME.theme:playMoveSfx() +end + +function VerticalMenu:receiveInputs(inputs, dt) + if not self.selectedIndex then + self:setInitialFocus() + end + + if not self.isEnabled then + return + end + + if not inputs then + -- if we don't get inputs passed, use the global input table + inputs = input + end + + local selectedElement = self.children[self.selectedIndex] + + if self.focused then + self.focused:receiveInputs(inputs, dt) + elseif inputs.isDown["MenuEsc"] then + if self.selectedIndex ~= #self.children then + self:setSelectedIndex(#self.children) + GAME.theme:playCancelSfx() + else + selectedElement:receiveInputs(inputs, dt) + end + elseif inputs:isPressedWithRepeat("MenuUp") then + self:selectPrevious() + elseif inputs:isPressedWithRepeat("MenuDown") then + self:selectNext() + else + if inputs.isDown["MenuSelect"] and selectedElement.isFocusable then + self:setFocus(selectedElement) + else + selectedElement:receiveInputs(inputs, dt) + end + end +end + +function VerticalMenu:draw() + ScrollContainer.draw(self) + if self.selectedIndex then + local selected = self.children[self.selectedIndex] + love.graphics.print(">", self.x + selected.x - 10, self.y + self.scrollOffset + selected.y) + end +end + +return VerticalMenu \ No newline at end of file diff --git a/client/src/ui/init.lua b/client/src/ui/init.lua index da6e2720..dcb0f878 100644 --- a/client/src/ui/init.lua +++ b/client/src/ui/init.lua @@ -52,9 +52,12 @@ local ui = { ---@class UiElement UiElement = require(PATH .. ".UIElement"), ValueLabel = require(PATH .. ".ValueLabel"), + ---@see VerticalMenu + ---@type fun(options: ScrollContainerOptions): VerticalMenu + VerticalMenu = require(PATH .. ".VerticalMenu"), } -- the default layout -ui.UiElement.layout = ui.Layouts.HorizontalFlexLayout +ui.UiElement.layout = ui.Layouts.VerticalFlexLayout return ui \ No newline at end of file diff --git a/common/lib/stringExtensions.lua b/common/lib/stringExtensions.lua index bd364abe..30711725 100644 --- a/common/lib/stringExtensions.lua +++ b/common/lib/stringExtensions.lua @@ -1,5 +1,7 @@ local utf8 = require("common.lib.utf8Additions") +---@param self string +---@return string[] function string.toCharTable(self) local t = {} for _, codePoint in utf8.codes(self) do @@ -7,4 +9,19 @@ function string.toCharTable(self) t[#t+1] = character end return t +end + +---@param inputstr string +---@param separator string? can be a pattern +---@return string[]? +---@diagnostic disable-next-line: duplicate-set-field +function string.split(inputstr, separator) + separator = separator or "%s" + local t = {} + for field, s in string.gmatch(inputstr, "([^" .. separator .. "]*)(" .. separator .. "?)") do + t[#t+1] = field + if s == "" then + return t + end + end end \ No newline at end of file diff --git a/common/lib/util.lua b/common/lib/util.lua index 7b75aa6b..c583882a 100644 --- a/common/lib/util.lua +++ b/common/lib/util.lua @@ -1,3 +1,4 @@ +require("common.lib.stringExtensions") local tableUtils = require("common.lib.tableUtils") local pairs, type, setmetatable, getmetatable = pairs, type, setmetatable, getmetatable @@ -193,21 +194,6 @@ function frames_to_time_string(frame_count, include_centiseconds) return ret end --- split the input string on some separator, returns table ----@param inputstr string ----@param sep string? ----@return string[]? -function util.split(inputstr, sep) - sep = sep or "%s" - local t = {} - for field, s in string.gmatch(inputstr, "([^" .. sep .. "]*)(" .. sep .. "?)") do - table.insert(t, field) - if s == "" then - return t - end - end -end - -- Remove white space from the ends of a string ---@param s string ---@return string trimmed @@ -281,7 +267,7 @@ function util.addToCPath(path) -- Example: luasocket's dynamic core library's entry point is -- luaopen_socket_core -- meaning it has to be required as "socket.core", otherwise it cannot be opened - local cPathDirs = util.split(package.cpath, ";") + local cPathDirs = string.split(package.cpath, ";") local fileExtension = string.sub(cPathDirs[1], -4) if fileExtension == "?.so" then path = path:gsub("%?%?", "?.so") From 28d3541a9e75ddf13f0af22a29a0a98611498687 Mon Sep 17 00:00:00 2001 From: Endaris Date: Sun, 11 May 2025 03:05:27 +0200 Subject: [PATCH 03/49] some layouting adjustments for options menu --- client/src/scenes/ModManagement.lua | 2 +- client/src/scenes/OptionsMenu.lua | 390 +++++++++++------- client/src/ui/ButtonGroup.lua | 9 + client/src/ui/Label.lua | 31 +- .../src/ui/Layouts/HorizontalFlexLayout.lua | 55 ++- client/src/ui/Layouts/VerticalFlexLayout.lua | 55 ++- client/src/ui/MenuItem.lua | 108 +++-- client/src/ui/ScrollContainer.lua | 34 +- client/src/ui/Slider.lua | 8 +- client/src/ui/StackPanel.lua | 1 - client/src/ui/Stepper.lua | 15 +- client/src/ui/TextButton.lua | 2 + client/src/ui/UIElement.lua | 14 +- client/src/ui/VerticalMenu.lua | 16 +- client/src/ui/touchHandler.lua | 1 - 15 files changed, 477 insertions(+), 264 deletions(-) diff --git a/client/src/scenes/ModManagement.lua b/client/src/scenes/ModManagement.lua index e9724e5d..f4ef410e 100644 --- a/client/src/scenes/ModManagement.lua +++ b/client/src/scenes/ModManagement.lua @@ -55,7 +55,7 @@ function ModManagement:load() self.cursor.onMove = function(c) local newOffset = c.target.unitSize * (c.selectedGridPos.y - 1) if self.scrollContainer then - self.scrollContainer:keepVisible(-newOffset, c.target.unitSize) + self.scrollContainer:keepVisible(newOffset, c.target.unitSize) end end diff --git a/client/src/scenes/OptionsMenu.lua b/client/src/scenes/OptionsMenu.lua index 2238b124..35df80f3 100644 --- a/client/src/scenes/OptionsMenu.lua +++ b/client/src/scenes/OptionsMenu.lua @@ -45,8 +45,8 @@ function OptionsMenu:loadScreens() menus.aboutAttackFiles = self:loadInfoScreen(save.read_txt_file("docs/training.txt")) menus.installingMods = self:loadInfoScreen(save.read_txt_file("docs/installMods.md")) - if #menus.modifyUserIdMenu.menuItems == 1 then - menus.baseMenu:removeMenuItemAtIndex(7) + if #menus.modifyUserIdMenu.children == 1 then + menus.modifyUserIdMenu:detach() end return menus @@ -83,7 +83,7 @@ end local function createToggleButtonGroup(configField, onChangeFn) return ui.ButtonGroup({ - buttons = {ui.TextButton({width = 60, label = ui.Label({text = "op_off"})}), ui.TextButton({width = 60, label = ui.Label({text = "op_on"})})}, + buttons = {ui.TextButton({width = 60, label = ui.Label({id = "op_off"})}), ui.TextButton({width = 60, label = ui.Label({id = "op_on"})})}, values = {false, true}, selectedIndex = config[configField] and 2 or 1, onChange = function(group, value) @@ -175,40 +175,56 @@ function OptionsMenu:loadBaseMenu() end }) - local baseMenuOptions = { - ui.MenuItem.createStepperMenuItem("op_language", nil, nil, languageStepper), - ui.MenuItem.createButtonMenuItem("op_general", nil, nil, function() - GAME.theme:playValidationSfx() - self:switchToScreen("generalMenu") - end), - ui.MenuItem.createButtonMenuItem("op_graphics", nil, nil, function() - GAME.theme:playValidationSfx() - self:switchToScreen("graphicsMenu") - end), - ui.MenuItem.createButtonMenuItem("op_audio", nil, nil, function() - GAME.theme:playValidationSfx() - self:switchToScreen("audioMenu") - end), - ui.MenuItem.createButtonMenuItem("op_debug", nil, nil, function() - GAME.theme:playValidationSfx() - self:switchToScreen("debugMenu") - end), - ui.MenuItem.createButtonMenuItem("op_about", nil, nil, function() - GAME.theme:playValidationSfx() - self:switchToScreen("aboutMenu") - end), - ui.MenuItem.createButtonMenuItem("Modify User ID", nil, false, function() - GAME.theme:playValidationSfx() - self:switchToScreen("modifyUserIdMenu") - end), - ui.MenuItem.createButtonMenuItem("Manage Mods", nil, false, function() - GAME.theme:playValidationSfx() - GAME.navigationStack:push(ModManagement()) - end), - ui.MenuItem.createButtonMenuItem("back", nil, nil, self.exit) - } + local languageSelection = ui.MenuItem.createStepperMenuItem("op_language", nil, nil, languageStepper) + local generalMenu = ui.MenuItem.createButtonMenuItem("op_general", nil, nil, function() + GAME.theme:playValidationSfx() + self:switchToScreen("generalMenu") + end) + local graphicsMenu = ui.MenuItem.createButtonMenuItem("op_graphics", nil, nil, function() + GAME.theme:playValidationSfx() + self:switchToScreen("graphicsMenu") + end) + local audioMenu = ui.MenuItem.createButtonMenuItem("op_audio", nil, nil, function() + GAME.theme:playValidationSfx() + self:switchToScreen("audioMenu") + end) + local debugMenu = ui.MenuItem.createButtonMenuItem("op_debug", nil, nil, function() + GAME.theme:playValidationSfx() + self:switchToScreen("debugMenu") + end) + local aboutMenu = ui.MenuItem.createButtonMenuItem("op_about", nil, nil, function() + GAME.theme:playValidationSfx() + self:switchToScreen("aboutMenu") + end) + local modifyUserId = ui.MenuItem.createButtonMenuItem("Modify User ID", nil, false, function() + GAME.theme:playValidationSfx() + self:switchToScreen("modifyUserIdMenu") + end) + local modManagement = ui.MenuItem.createButtonMenuItem("Manage Mods", nil, false, function() + GAME.theme:playValidationSfx() + GAME.navigationStack:push(ModManagement()) + end) + local back = ui.MenuItem.createButtonMenuItem("back", nil, nil, self.exit) + + local menu = ui.VerticalMenu({ + hAlign = "center", + minHeight = 480, + maxHeight = 540, + childGap = 8, + padding = 32, + width = 600, + }) + + menu:addChild(languageSelection) + menu:addChild(generalMenu) + menu:addChild(graphicsMenu) + menu:addChild(audioMenu) + menu:addChild(debugMenu) + menu:addChild(aboutMenu) + menu:addChild(modifyUserId) + menu:addChild(modManagement) + menu:addChild(back) - local menu = ui.Menu.createCenteredMenu(baseMenuOptions) return menu end @@ -216,8 +232,8 @@ function OptionsMenu:loadGeneralMenu() local saveReplaysPubliclyIndexMap = {["with my name"] = 1, ["anonymously"] = 2, ["not at all"] = 3} local publicReplayButtonGroup = ui.ButtonGroup({ buttons = { - ui.TextButton({label = ui.Label({text = "op_replay_public_with_name"})}), - ui.TextButton({label = ui.Label({text = "op_replay_public_anonymously"})}), ui.TextButton({label = ui.Label({text = "op_replay_public_no"})}) + ui.TextButton({label = ui.Label({id = "op_replay_public_with_name"})}), + ui.TextButton({label = ui.Label({id = "op_replay_public_anonymously"})}), ui.TextButton({label = ui.Label({id = "op_replay_public_no"})}) }, values = {"with my name", "anonymously", "not at all"}, selectedIndex = saveReplaysPubliclyIndexMap[config.save_replays_publicly], @@ -290,20 +306,15 @@ function OptionsMenu:loadGeneralMenu() }) end - local generalMenuOptions = { - ui.MenuItem.createToggleButtonGroupMenuItem("op_fps", nil, nil, createToggleButtonGroup("show_fps")), - ui.MenuItem.createToggleButtonGroupMenuItem("op_ingame_infos", nil, nil, createToggleButtonGroup("show_ingame_infos")), - ui.MenuItem.createToggleButtonGroupMenuItem("op_analytics", nil, nil, createToggleButtonGroup("enable_analytics", function() - analytics.init() - end)), - ui.MenuItem.createToggleButtonGroupMenuItem("op_replay_public", nil, nil, publicReplayButtonGroup), - } + local showFps = ui.MenuItem.createToggleButtonGroupMenuItem("op_fps", nil, nil, createToggleButtonGroup("show_fps")) + local showIngameInfos = ui.MenuItem.createToggleButtonGroupMenuItem("op_ingame_infos", nil, nil, createToggleButtonGroup("show_ingame_infos")) + local enableAnalytics = ui.MenuItem.createToggleButtonGroupMenuItem("op_analytics", nil, nil, createToggleButtonGroup("enable_analytics", function() + analytics.init() + end)) + local saveReplaysPublicly = ui.MenuItem.createToggleButtonGroupMenuItem("op_replay_public", nil, nil, publicReplayButtonGroup) - if releaseStreamSelection then - generalMenuOptions[#generalMenuOptions+1] = ui.MenuItem.createToggleButtonGroupMenuItem("Release Stream", nil, false, releaseStreamSelection) - end - generalMenuOptions[#generalMenuOptions + 1] = ui.MenuItem.createButtonMenuItem("back", nil, nil, + local back = ui.MenuItem.createButtonMenuItem("back", nil, nil, function() GAME.theme:playCancelSfx() self:switchToScreen("baseMenu") @@ -314,7 +325,26 @@ function OptionsMenu:loadGeneralMenu() end end) - local menu = ui.Menu.createCenteredMenu(generalMenuOptions) + local menu = ui.VerticalMenu({ + hAlign = "center", + minHeight = 480, + maxHeight = 540, + childGap = 8, + padding = 32, + width = 600, + }) + + menu:addChild(showFps) + menu:addChild(showIngameInfos) + menu:addChild(enableAnalytics) + menu:addChild(saveReplaysPublicly) + + if releaseStreamSelection then + menu:addChild(ui.MenuItem.createToggleButtonGroupMenuItem("Release Stream", nil, false, releaseStreamSelection)) + end + + menu:addChild(back) + return menu end @@ -384,7 +414,7 @@ function OptionsMenu:loadGraphicsMenu() end local scaleTypeData = { - {value = "auto", text = "op_scale_auto"}, {value = "fit", text = "op_scale_fit"}, {value = "fixed", text = "op_scale_fixed"} + {value = "auto", id = "op_scale_auto"}, {value = "fit", id = "op_scale_fit"}, {value = "fixed", id = "op_scale_fixed"} } for index, value in ipairs(scaleTypeData) do value.index = index @@ -392,7 +422,7 @@ function OptionsMenu:loadGraphicsMenu() local scaleButtonGroup = ui.ButtonGroup({ buttons = tableUtils.map(scaleTypeData, function(scaleType) - return ui.TextButton({label = ui.Label({text = scaleType.text})}) + return ui.TextButton({label = ui.Label({id = scaleType.id})}) end), values = tableUtils.map(scaleTypeData, function(scaleType) return scaleType.value @@ -422,25 +452,42 @@ function OptionsMenu:loadGraphicsMenu() return slider end - local graphicsMenuOptions = { - ui.MenuItem.createStepperMenuItem("op_theme", nil, nil, themeStepper), - ui.MenuItem.createToggleButtonGroupMenuItem("op_scale", nil, nil, scaleButtonGroup), - ui.MenuItem.createSliderMenuItem("op_portrait_darkness", nil, nil, createConfigSlider("portrait_darkness", 0, 100)), - ui.MenuItem.createToggleButtonGroupMenuItem("op_popfx", nil, nil, createToggleButtonGroup("popfx")), - ui.MenuItem.createToggleButtonGroupMenuItem("op_renderTelegraph", nil, nil, createToggleButtonGroup("renderTelegraph")), - ui.MenuItem.createToggleButtonGroupMenuItem("op_renderAttacks", nil, nil, createToggleButtonGroup("renderAttacks")), - ui.MenuItem.createSliderMenuItem("op_shakeIntensity", nil, nil, getShakeIntensitySlider()), - ui.MenuItem.createButtonMenuItem("back", nil, nil, function() + local themeSelection = ui.MenuItem.createStepperMenuItem("op_theme", nil, nil, themeStepper) + local scaleType = ui.MenuItem.createToggleButtonGroupMenuItem("op_scale", nil, nil, scaleButtonGroup) + local portraitDarkness = ui.MenuItem.createSliderMenuItem("op_portrait_darkness", nil, nil, createConfigSlider("portrait_darkness", 0, 100)) + local popFx = ui.MenuItem.createToggleButtonGroupMenuItem("op_popfx", nil, nil, createToggleButtonGroup("popfx")) + local telegraph = ui.MenuItem.createToggleButtonGroupMenuItem("op_renderTelegraph", nil, nil, createToggleButtonGroup("renderTelegraph")) + local attacks = ui.MenuItem.createToggleButtonGroupMenuItem("op_renderAttacks", nil, nil, createToggleButtonGroup("renderAttacks")) + local shakeIntensity = ui.MenuItem.createSliderMenuItem("op_shakeIntensity", nil, nil, getShakeIntensitySlider()) + local back = ui.MenuItem.createButtonMenuItem("back", nil, nil, function() GAME.showGameScaleUntil = GAME.timer GAME.theme:playCancelSfx() self:switchToScreen("baseMenu") end) - } - local menu = ui.Menu.createCenteredMenu(graphicsMenuOptions) + local menu = ui.VerticalMenu({ + hAlign = "center", + minHeight = 480, + maxHeight = 540, + childGap = 8, + padding = 32, + width = 600, + }) + + menu:addChild(themeSelection) + menu:addChild(scaleType) + if config.gameScaleType == "fixed" then menu:addMenuItem(3, fixedScaleSlider) end + + menu:addChild(portraitDarkness) + menu:addChild(popFx) + menu:addChild(telegraph) + menu:addChild(attacks) + menu:addChild(shakeIntensity) + menu:addChild(back) + return menu end @@ -448,8 +495,8 @@ function OptionsMenu:loadSoundMenu() local musicFrequencyIndexMap = {["stage"] = 1, ["often_stage"] = 2, ["either"] = 3, ["often_characters"] = 4, ["characters"] = 5} local musicFrequencyStepper = ui.Stepper({ labels = { - ui.Label({text = "op_only_stage"}), ui.Label({text = "op_often_stage"}), ui.Label({text = "op_stage_characters"}), - ui.Label({text = "op_often_characters"}), ui.Label({text = "op_only_characters"}) + ui.Label({id = "op_only_stage"}), ui.Label({id = "op_often_stage"}), ui.Label({id = "op_stage_characters"}), + ui.Label({id = "op_often_characters"}), ui.Label({id = "op_only_characters"}) }, values = {"stage", "often_stage", "either", "often_characters", "characters"}, selectedIndex = musicFrequencyIndexMap[config.use_music_from], @@ -459,115 +506,170 @@ function OptionsMenu:loadSoundMenu() end }) - local audioMenuOptions = { - ui.MenuItem.createSliderMenuItem("op_vol", nil, nil, createConfigSlider("master_volume", 0, 100, function(slider) + local masterVolume = ui.MenuItem.createSliderMenuItem("op_vol", nil, nil, createConfigSlider("master_volume", 0, 100, function(slider) SoundController:setMasterVolume(slider.value) - end)), - ui.MenuItem.createSliderMenuItem("op_vol_sfx", nil, nil, createConfigSlider("SFX_volume", 0, 100, function() + end)) + local sfxVolume = ui.MenuItem.createSliderMenuItem("op_vol_sfx", nil, nil, createConfigSlider("SFX_volume", 0, 100, function() SoundController:applyConfigVolumes() - end)), - ui.MenuItem.createSliderMenuItem("op_vol_music", nil, nil, createConfigSlider("music_volume", 0, 100, function() + end)) + local musicVolume = ui.MenuItem.createSliderMenuItem("op_vol_music", nil, nil, createConfigSlider("music_volume", 0, 100, function() SoundController:applyConfigVolumes() - end)), - ui.MenuItem.createToggleButtonGroupMenuItem("op_menu_music", nil, nil, createToggleButtonGroup("enableMenuMusic", function() self:applyMusic() end)), - ui.MenuItem.createStepperMenuItem("op_use_music_from", nil, nil, musicFrequencyStepper), - ui.MenuItem.createToggleButtonGroupMenuItem("op_music_delay", nil, nil, createToggleButtonGroup("danger_music_changeback_delay")), - ui.MenuItem.createButtonMenuItem("mm_music_test", nil, nil, function() + end)) + local menuMusic = ui.MenuItem.createToggleButtonGroupMenuItem("op_menu_music", nil, nil, createToggleButtonGroup("enableMenuMusic", function() self:applyMusic() end)) + local musicSource = ui.MenuItem.createStepperMenuItem("op_use_music_from", nil, nil, musicFrequencyStepper) + local dangerChangeBackDelay = ui.MenuItem.createToggleButtonGroupMenuItem("op_music_delay", nil, nil, createToggleButtonGroup("danger_music_changeback_delay")) + local musicTest = ui.MenuItem.createButtonMenuItem("mm_music_test", nil, nil, function() GAME.navigationStack:push(SoundTest()) - end), - ui.MenuItem.createButtonMenuItem("back", nil, nil, function() + end) + local back = ui.MenuItem.createButtonMenuItem("back", nil, nil, function() GAME.theme:playCancelSfx() self:switchToScreen("baseMenu") end) - } - local menu = ui.Menu.createCenteredMenu(audioMenuOptions) + local menu = ui.VerticalMenu({ + hAlign = "center", + minHeight = 480, + maxHeight = 540, + childGap = 8, + padding = 32, + width = 600, + }) + + menu:addChild(masterVolume) + menu:addChild(sfxVolume) + menu:addChild(musicVolume) + menu:addChild(menuMusic) + menu:addChild(musicSource) + menu:addChild(dangerChangeBackDelay) + menu:addChild(musicTest) + menu:addChild(back) + return menu end function OptionsMenu:loadDebugMenu() - local debugMenuOptions = { - ui.MenuItem.createToggleButtonGroupMenuItem("op_debug", nil, nil, createToggleButtonGroup("debug_mode")), - ui.MenuItem.createSliderMenuItem("VS Frames Behind", nil, false, createConfigSlider("debug_vsFramesBehind", 0, 200)), - ui.MenuItem.createToggleButtonGroupMenuItem("Show Debug Servers", nil, false, createToggleButtonGroup("debugShowServers")), - ui.MenuItem.createToggleButtonGroupMenuItem("Show Design Helper", nil, false, createToggleButtonGroup("debugShowDesignHelper")), - ui.MenuItem.createButtonMenuItem("Window Size Tester", nil, false, function() - GAME.navigationStack:push(require("client.src.scenes.WindowSizeTester")()) - end), - ui.MenuItem.createToggleButtonGroupMenuItem("Profile frame times", nil, false, createToggleButtonGroup("debugProfile", - function() - prof.enable(config.debugProfile) - prof.setDurationFilter(config.debugProfileThreshold / 1000) - end)), - ui.MenuItem.createSliderMenuItem("Discard frames below duration (ms)", nil, false, createConfigSlider("debugProfileThreshold", 0, 100, - function() - prof.setDurationFilter(config.debugProfileThreshold / 1000) - end)), - ui.MenuItem.createButtonMenuItem("back", nil, nil, function() - GAME.theme:playCancelSfx() - self:switchToScreen("baseMenu") - end), - } + local debugMode = ui.MenuItem.createToggleButtonGroupMenuItem("op_debug", nil, nil, createToggleButtonGroup("debug_mode")) + local vsFramesBehind = ui.MenuItem.createSliderMenuItem("VS Frames Behind", nil, false, createConfigSlider("debug_vsFramesBehind", 0, 200)) + local debugServers = ui.MenuItem.createToggleButtonGroupMenuItem("Show Debug Servers", nil, false, createToggleButtonGroup("debugShowServers")) + local designHelper = ui.MenuItem.createToggleButtonGroupMenuItem("Show Design Helper", nil, false, createToggleButtonGroup("debugShowDesignHelper")) + local windowSizeTester = ui.MenuItem.createButtonMenuItem("Window Size Tester", nil, false, function() + GAME.navigationStack:push(require("client.src.scenes.WindowSizeTester")()) + end) + local profilingFilter = ui.MenuItem.createSliderMenuItem("Discard frames below duration (ms)", nil, false, createConfigSlider("debugProfileThreshold", 0, 100, + function() + prof.setDurationFilter(config.debugProfileThreshold / 1000) + end)) + local profiling = ui.MenuItem.createToggleButtonGroupMenuItem("Profile frame times", nil, false, createToggleButtonGroup("debugProfile", + function() + prof.enable(config.debugProfile) + prof.setDurationFilter(config.debugProfileThreshold / 1000) + profilingFilter:setEnabled(config.debugProfile) + end)) + local back = ui.MenuItem.createButtonMenuItem("back", nil, nil, function() + GAME.theme:playCancelSfx() + self:switchToScreen("baseMenu") + end) + + local menu = ui.VerticalMenu({ + hAlign = "center", + minHeight = 480, + maxHeight = 540, + childGap = 8, + padding = 32, + width = 600, + }) + + menu:addChild(debugMode) + menu:addChild(vsFramesBehind) + menu:addChild(debugServers) + menu:addChild(designHelper) + menu:addChild(windowSizeTester) + menu:addChild(profiling) + menu:addChild(profilingFilter) + menu:addChild(back) - return ui.Menu.createCenteredMenu(debugMenuOptions) + return menu end function OptionsMenu:loadAboutMenu() - local aboutMenuOptions = { - ui.MenuItem.createButtonMenuItem("op_about_themes", nil, nil, function() - GAME.theme:playValidationSfx() - love.system.openURL("https://github.com/panel-attack/panel-game/blob/beta/docs/themes.md") - end), - ui.MenuItem.createButtonMenuItem("op_about_characters", nil, nil, function() - GAME.theme:playValidationSfx() - love.system.openURL("https://github.com/panel-attack/panel-game/blob/beta/docs/characters.md") - end), - ui.MenuItem.createButtonMenuItem("op_about_stages", nil, nil, function() - GAME.theme:playValidationSfx() - love.system.openURL("https://github.com/panel-attack/panel-game/blob/beta/docs/stages.md") - end), - ui.MenuItem.createButtonMenuItem("op_about_panels", nil, nil, function() - GAME.theme:playValidationSfx() - love.system.openURL("https://github.com/panel-attack/panel-game/blob/beta/docs/panels.md") - end), - ui.MenuItem.createButtonMenuItem("About Attack Files", nil, nil, function() - GAME.theme:playValidationSfx() - love.system.openURL("https://github.com/panel-attack/panel-game/blob/beta/docs/training.txt") - end), - ui.MenuItem.createButtonMenuItem("Installing Mods", nil, nil, function() - GAME.theme:playValidationSfx() - love.system.openURL("https://github.com/panel-attack/panel-game/blob/beta/docs/installMods.md") - end), - ui.MenuItem.createButtonMenuItem("System Info", nil, nil, function() - GAME.theme:playValidationSfx() - self:switchToScreen("systemInfo") - end), - ui.MenuItem.createButtonMenuItem("back", nil, nil, function() - GAME.theme:playCancelSfx() - self:switchToScreen("baseMenu") - end) - } + local aboutThemes = ui.MenuItem.createButtonMenuItem("op_about_themes", nil, nil, function() + GAME.theme:playValidationSfx() + love.system.openURL("https://github.com/panel-attack/panel-game/blob/beta/docs/themes.md") + end) + local aboutCharacters = ui.MenuItem.createButtonMenuItem("op_about_characters", nil, nil, function() + GAME.theme:playValidationSfx() + love.system.openURL("https://github.com/panel-attack/panel-game/blob/beta/docs/characters.md") + end) + local aboutStages = ui.MenuItem.createButtonMenuItem("op_about_stages", nil, nil, function() + GAME.theme:playValidationSfx() + love.system.openURL("https://github.com/panel-attack/panel-game/blob/beta/docs/stages.md") + end) + local aboutPanels = ui.MenuItem.createButtonMenuItem("op_about_panels", nil, nil, function() + GAME.theme:playValidationSfx() + love.system.openURL("https://github.com/panel-attack/panel-game/blob/beta/docs/panels.md") + end) + local aboutAttackFiles = ui.MenuItem.createButtonMenuItem("About Attack Files", nil, nil, function() + GAME.theme:playValidationSfx() + love.system.openURL("https://github.com/panel-attack/panel-game/blob/beta/docs/training.txt") + end) + local aboutInstallingMods = ui.MenuItem.createButtonMenuItem("Installing Mods", nil, nil, function() + GAME.theme:playValidationSfx() + love.system.openURL("https://github.com/panel-attack/panel-game/blob/beta/docs/installMods.md") + end) + local systemInfo = ui.MenuItem.createButtonMenuItem("System Info", nil, nil, function() + GAME.theme:playValidationSfx() + self:switchToScreen("systemInfo") + end) + local back = ui.MenuItem.createButtonMenuItem("back", nil, nil, function() + GAME.theme:playCancelSfx() + self:switchToScreen("baseMenu") + end) + + local menu = ui.VerticalMenu({ + hAlign = "center", + minHeight = 480, + maxHeight = 540, + childGap = 8, + padding = 32, + width = 600, + }) + + menu:addChild(aboutThemes) + menu:addChild(aboutCharacters) + menu:addChild(aboutStages) + menu:addChild(aboutPanels) + menu:addChild(aboutAttackFiles) + menu:addChild(aboutInstallingMods) + menu:addChild(systemInfo) + menu:addChild(back) - local menu = ui.Menu.createCenteredMenu(aboutMenuOptions) return menu end function OptionsMenu:loadModifyUserIdMenu() - local modifyUserIdOptions = {} + local menu = ui.VerticalMenu({ + hAlign = "center", + minHeight = 480, + maxHeight = 540, + childGap = 8, + padding = 32, + width = 600, + }) local userIDDirectories = fileUtils.getFilteredDirectoryItems("servers") for i = 1, #userIDDirectories do if love.filesystem.getInfo("servers/" .. userIDDirectories[i] .. "/user_id.txt", "file") then - modifyUserIdOptions[#modifyUserIdOptions + 1] = ui.MenuItem.createButtonMenuItem(userIDDirectories[i], nil, false, function() + menu:addChild(ui.MenuItem.createButtonMenuItem(userIDDirectories[i], nil, false, function() GAME.navigationStack:push(SetUserIdMenu({serverIp = userIDDirectories[i]})) - end) + end)) end end - modifyUserIdOptions[#modifyUserIdOptions + 1] = ui.MenuItem.createButtonMenuItem("back", nil, nil, function() + local back = ui.MenuItem.createButtonMenuItem("back", nil, nil, function() GAME.theme:playCancelSfx() self:switchToScreen("baseMenu") end) - return ui.Menu.createCenteredMenu(modifyUserIdOptions) + menu:addChild(back) + return menu end function OptionsMenu:load() diff --git a/client/src/ui/ButtonGroup.lua b/client/src/ui/ButtonGroup.lua index 72ca5af8..1a5dba96 100644 --- a/client/src/ui/ButtonGroup.lua +++ b/client/src/ui/ButtonGroup.lua @@ -3,6 +3,7 @@ local UIElement = require(PATH .. ".UIElement") local class = require("common.lib.class") local util = require("common.lib.util") local tableUtils = require("common.lib.tableUtils") +local HorizontalFlexLayout = require(PATH .. ".Layouts.HorizontalFlexLayout") local BUTTON_PADDING = 5 @@ -40,6 +41,7 @@ local function setButtons(self, buttons, values, selectedIndex) overallWidth = overallWidth + BUTTON_PADDING end button.onClick = genButtonGroupFn(self, button) + button.vAlign = "center" self:addChild(button) overallHeight = math.max(overallHeight, button.height) end @@ -66,6 +68,12 @@ local ButtonGroup = class( function(self, options) self.selectedIndex = options.selectedIndex or 1 + self.padding = options.padding or 4 + self.childGap = options.childGap or 8 + if options.hFill == nil then + self.hFill = true + end + self.onChange = options.onChange or function() end setButtons(self, options.buttons, options.values, self.selectedIndex) @@ -73,6 +81,7 @@ local ButtonGroup = class( UIElement ) ButtonGroup.TYPE = "ButtonGroup" +ButtonGroup.layout = HorizontalFlexLayout -- changes state for the button group -- updates the color of the selected button diff --git a/client/src/ui/Label.lua b/client/src/ui/Label.lua index f54024c0..31a0ffff 100644 --- a/client/src/ui/Label.lua +++ b/client/src/ui/Label.lua @@ -33,8 +33,8 @@ local Label = class( self.hAlign = options.hAlign or "left" self.vAlign = options.vAlign or "top" - self.font = options.font or love.graphics.getFont() self.fontSize = options.fontSize or GraphicsUtil.fontSize + self.font = options.font or GraphicsUtil.getGlobalFontWithSize(self.fontSize) local totalWidth = self.font:getWidth(self.text) if options.wrap ~= nil then @@ -60,7 +60,7 @@ local Label = class( end self.width = options.width or totalWidth - self.maxWidth = options.maxWidth or totalWidth + self.maxWidth = options.maxWidth or math.huge self.minHeight = options.minHeight or self.font:getHeight() self.height = options.height or self.font:getHeight() @@ -81,23 +81,38 @@ local Label = class( ) Label.TYPE = "Label" -function Label:getEffectiveDimensions() - return self.drawable:getDimensions() +function Label:setFontSize(fontSize) + self.fontSize = fontSize + self.font = GraphicsUtil.getGlobalFontWithSize(self.fontSize) + local totalWidth = self.font:getWidth(self.text) + self.width = totalWidth + self.maxWidth = math.huge + if not self.wrap then + self.minWidth = totalWidth + end end function Label:refreshLocalization() if self.id then - self.text = loc(self.text, unpack(self.replacementTable)) + self.text = loc(self.id, unpack(self.replacementTable)) + end + self.font = GraphicsUtil.getGlobalFontWithSize(self.fontSize) + local totalWidth = self.font:getWidth(self.text) + self.width = totalWidth + self.maxWidth = totalWidth + if not self.wrap then + self.minWidth = totalWidth end end function Label:drawSelf() - love.graphics.printf(self.text, self.x, self.y, self.width) + love.graphics.setFont(self.font) + love.graphics.printf(self.text, self.x, self.y, self.width, self.hAlign) end function Label:setMinHeightForWidth() - local refText = GraphicsUtil.newText(GraphicsUtil.getGlobalFontWithSize(self.fontSize)) - refText:setf(self.text, self.width, "left") + local refText = GraphicsUtil.newText(self.font) + refText:setf(self.text, self.width, self.hAlign) self.minHeight = refText:getHeight() end diff --git a/client/src/ui/Layouts/HorizontalFlexLayout.lua b/client/src/ui/Layouts/HorizontalFlexLayout.lua index 70fc29a7..8d06d04d 100644 --- a/client/src/ui/Layouts/HorizontalFlexLayout.lua +++ b/client/src/ui/Layouts/HorizontalFlexLayout.lua @@ -10,7 +10,9 @@ function HorizontalFlexLayout.getMinWidth(uiElement) local w = uiElement.padding * 2 + uiElement.childGap * (#uiElement.children - 1) for _, child in ipairs(uiElement.children) do - w = w + child.width + if child.isVisible then + w = w + child.width + end end return util.bound(uiElement.minWidth, w, uiElement.maxWidth) @@ -22,7 +24,9 @@ function HorizontalFlexLayout.getMinHeight(uiElement) local maxHeight = 0 for _, child in ipairs(uiElement.children) do - maxHeight = math.max(maxHeight, child.height) + if child.isVisible then + maxHeight = math.max(maxHeight, child.height) + end end return h + maxHeight @@ -37,7 +41,7 @@ function HorizontalFlexLayout.growChildrenWidth(uiElement) local growables = {} for i, child in ipairs(uiElement.children) do - if child.hFill and child.width < child.maxWidth then + if child.isVisible and child.hFill and child.width < child.maxWidth then growables[#growables+1] = child child.newWidth = child.width end @@ -123,33 +127,42 @@ end function HorizontalFlexLayout.positionChildren(uiElement) local remainingWidth = uiElement.width - (uiElement.padding * 2 + uiElement.childGap * (#uiElement.children - 1)) for _, child in ipairs(uiElement.children) do - remainingWidth = remainingWidth - child.width + if child.isVisible then + remainingWidth = remainingWidth - child.width + else + -- we subtracted the childgap for invisible children earlier + remainingWidth = remainingWidth + uiElement.childGap + end end local x = uiElement.padding for _, child in ipairs(uiElement.children) do - if child.hAlign == "left" then - child.x = x - elseif child.hAlign == "center" then - child.x = x + remainingWidth / 2 - elseif child.hAlign == "right" then - child.x = x + remainingWidth - end + if child.isVisible then + if child.hAlign == "left" then + child.x = x + elseif child.hAlign == "center" then + child.x = x + remainingWidth / 2 + elseif child.hAlign == "right" then + child.x = x + remainingWidth + end - if child.vAlign == "top" then - child.y = uiElement.padding - elseif child.vAlign == "center" then - child.y = (uiElement.height - child.height) / 2 - elseif child.vAlign == "bottom" then - child.y = (uiElement.height - child.height) - uiElement.padding + if child.vAlign == "top" then + child.y = uiElement.padding + elseif child.vAlign == "center" then + child.y = (uiElement.height - child.height) / 2 + elseif child.vAlign == "bottom" then + child.y = (uiElement.height - child.height) - uiElement.padding + end + child.x = math.round(child.x) + child.y = math.round(child.y) + x = x + uiElement.childGap + child.width end - child.x = math.round(child.x) - child.y = math.round(child.y) - x = x + uiElement.childGap + child.width end for _, child in ipairs(uiElement.children) do - child.layout.positionChildren(child) + if child.isVisible then + child.layout.positionChildren(child) + end end end diff --git a/client/src/ui/Layouts/VerticalFlexLayout.lua b/client/src/ui/Layouts/VerticalFlexLayout.lua index d32befb2..c8e6a2de 100644 --- a/client/src/ui/Layouts/VerticalFlexLayout.lua +++ b/client/src/ui/Layouts/VerticalFlexLayout.lua @@ -11,7 +11,9 @@ function VerticalFlexLayout.getMinWidth(uiElement) local maxWidth = 0 for _, child in ipairs(uiElement.children) do - maxWidth = math.max(maxWidth, child.width) + if child.isVisible then + maxWidth = math.max(maxWidth, child.width) + end end return w + maxWidth @@ -22,7 +24,9 @@ function VerticalFlexLayout.getMinHeight(uiElement) local h = uiElement.padding * 2 + uiElement.childGap * (#uiElement.children - 1) for _, child in ipairs(uiElement.children) do - h = h + child.height + if child.isVisible then + h = h + child.height + end end return util.bound(uiElement.minHeight, h, uiElement.maxHeight) @@ -37,7 +41,7 @@ function VerticalFlexLayout.growChildrenHeight(uiElement) local growables = {} for i, child in ipairs(uiElement.children) do - if child.vFill and child.height < child.maxHeight then + if child.isVisible and child.vFill and child.height < child.maxHeight then growables[#growables+1] = child child.newHeight = child.height end @@ -123,33 +127,42 @@ end function VerticalFlexLayout.positionChildren(uiElement) local remainingHeight = uiElement.height - (uiElement.padding * 2 + uiElement.childGap * (#uiElement.children - 1)) for _, child in ipairs(uiElement.children) do - remainingHeight = remainingHeight - child.height + if child.isVisible then + remainingHeight = remainingHeight - child.height + else + -- we subtracted the childgap for this child earlier + remainingHeight = remainingHeight + uiElement.childGap + end end local y = uiElement.padding for _, child in ipairs(uiElement.children) do - if child.vAlign == "top" then - child.y = y - elseif child.vAlign == "center" then - child.y = y + remainingHeight / 2 - elseif child.vAlign == "right" then - child.y = y + remainingHeight - end + if child.isVisible then + if child.vAlign == "top" then + child.y = y + elseif child.vAlign == "center" then + child.y = y + remainingHeight / 2 + elseif child.vAlign == "right" then + child.y = y + remainingHeight + end - if child.hAlign == "left" then - child.x = uiElement.padding - elseif child.hAlign == "center" then - child.x = (uiElement.width - child.width) / 2 - elseif child.hAlign == "right" then - child.x = (uiElement.width - child.width) - uiElement.padding + if child.hAlign == "left" then + child.x = uiElement.padding + elseif child.hAlign == "center" then + child.x = (uiElement.width - child.width) / 2 + elseif child.hAlign == "right" then + child.x = (uiElement.width - child.width) - uiElement.padding + end + child.x = math.round(child.x) + child.y = math.round(child.y) + y = y + uiElement.childGap + child.height end - child.x = math.round(child.x) - child.y = math.round(child.y) - y = y + uiElement.childGap + child.height end for _, child in ipairs(uiElement.children) do - child.layout.positionChildren(child) + if child.isVisible then + child.layout.positionChildren(child) + end end end diff --git a/client/src/ui/MenuItem.lua b/client/src/ui/MenuItem.lua index 9c389350..b3fac8fd 100644 --- a/client/src/ui/MenuItem.lua +++ b/client/src/ui/MenuItem.lua @@ -6,6 +6,7 @@ local TextButton = require(PATH .. ".TextButton") local class = require("common.lib.class") local GraphicsUtil = require("client.src.graphics.graphics_util") local system = require("client.src.system") +local HorizontalFlexLayout = require(PATH .. ".Layouts.HorizontalFlexLayout") -- MenuItem is a specific UIElement that all children of Menu should be local MenuItem = class(function(self, options) @@ -21,32 +22,28 @@ MenuItem.PADDING = 2 function MenuItem.createMenuItem(label, item) assert(label ~= nil) - label.vAlign = "center" - label.x = MenuItem.PADDING - - local menuItem = MenuItem({x = 0, y = 0}) + local menuItem = UiElement({hAlign = "center", layout = HorizontalFlexLayout, childGap = 16, hFill = true}) menuItem.width = label.width + (2 * MenuItem.PADDING) if system.isMobileOS() or DEBUG_ENABLED then - label.height = math.max(30, label.height + (2 * MenuItem.PADDING)) - menuItem.height = math.max(30, label.height, item and item.height or 0) - else - menuItem.height = math.max(menuItem.height, math.max(label.height, item and item.height or 0) + (2 * MenuItem.PADDING)) + label:setFontSize(18) end + label.vAlign = "center" + label.hAlign = "right" + label.hFill = true + menuItem:addChild(label) if item ~= nil then - local spaceBetween = 16 - item.x = label.width + spaceBetween item.vAlign = "center" - if system.isMobileOS() or DEBUG_ENABLED then - item.height = math.max(30, item.height) - end - menuItem.width = item.x + item.width + MenuItem.PADDING + item.hAlign = "left" + item.hFill = true menuItem:addChild(item) end - menuItem:addChild(label) + menuItem.receiveInputs = function(i, inputs) + item:receiveInputs(inputs) + end return menuItem end @@ -59,18 +56,26 @@ function MenuItem.createButtonMenuItem(text, replacements, translate, onClick) id = text text = nil end + local fontSize = 12 + if system.isMobileOS() or DEBUG_ENABLED then + fontSize = 18 + end local label = Label({ id = id, text = text, replacements = replacements, hAlign = "center", - vAlign = "center" + vAlign = "center", + wrap = false, + fontSize = fontSize }) local button = Button({ width = 140, maxWidth = 300, padding = 8, - onClick = onClick + onClick = onClick, + hAlign = "center", + vAlign = "center", }) button:addChild(label) @@ -89,8 +94,18 @@ function MenuItem.createLabeledButtonMenuItem(labelText, labelTextReplacements, if buttonTextTranslate == nil then buttonTextTranslate = true end - - local label = Label({text = labelText, replacements = labelTextReplacements, translate = labelTextTranslate, vAlign = "center"}) + assert(text ~= nil) + local id + if labelTextTranslate == nil or labelTextTranslate then + id = text + text = nil + end + local label = Label({ + id = id, + text = text, + replacements = labelTextReplacements, + vAlign = "center" + }) local textButton = TextButton({label = Label({text = buttonText, replacements = buttonTextReplacements, translate = buttonTextTranslate, hAlign = "center", vAlign = "center"}), onClick = buttonOnClick, width = BUTTON_WIDTH}) local menuItem = MenuItem.createMenuItem(label, textButton) @@ -101,38 +116,51 @@ end function MenuItem.createStepperMenuItem(text, replacements, translate, stepper) assert(text ~= nil) - assert(stepper ~= nil) - if translate == nil then - translate = true + local id + if translate == nil or translate then + id = text + text = nil end - local label = Label({text = text, replacements = replacements, translate = translate, vAlign = "center"}) - local menuItem = MenuItem.createMenuItem(label, stepper) - - return menuItem + local label = Label({ + id = id, + text = text, + replacements = replacements, + vAlign = "center" + }) + + return MenuItem.createMenuItem(label, stepper) end function MenuItem.createToggleButtonGroupMenuItem(text, replacements, translate, toggleButtonGroup) assert(text ~= nil) - assert(toggleButtonGroup ~= nil) - if translate == nil then - translate = true + local id + if translate == nil or translate then + id = text + text = nil end - local label = Label({text = text, replacements = replacements, translate = translate, vAlign = "center"}) - local menuItem = MenuItem.createMenuItem(label, toggleButtonGroup) - - return menuItem + local label = Label({ + id = id, + text = text, + replacements = replacements, + vAlign = "center" + }) + return MenuItem.createMenuItem(label, toggleButtonGroup) end function MenuItem.createSliderMenuItem(text, replacements, translate, slider) assert(text ~= nil) - assert(slider ~= nil) - if translate == nil then - translate = true + local id + if translate == nil or translate then + id = text + text = nil end - local label = Label({text = text, replacements = replacements, translate = translate, vAlign = "center"}) - local menuItem = MenuItem.createMenuItem(label, slider) - - return menuItem + local label = Label({ + id = id, + text = text, + replacements = replacements, + vAlign = "center" + }) + return MenuItem.createMenuItem(label, slider) end function MenuItem:setSelected(selected) diff --git a/client/src/ui/ScrollContainer.lua b/client/src/ui/ScrollContainer.lua index 79cac434..d975b92d 100644 --- a/client/src/ui/ScrollContainer.lua +++ b/client/src/ui/ScrollContainer.lua @@ -3,6 +3,7 @@ local UiElement = require(PATH .. ".UIElement") local class = require("common.lib.class") local util = require("common.lib.util") local GraphicsUtil = require("client.src.graphics.graphics_util") +local logger = require("common.lib.logger") ---@class ScrollContainerOptions : UiElementOptions ---@field scrollOrientation ("vertical" | "horizontal" | nil) @@ -27,6 +28,8 @@ ScrollContainer.TYPE = "ScrollContainer" -- bounds the scrollOffset of the container to the desired value between 0 and -maxScrollOffset ---@param value number desired scrollOffset value function ScrollContainer:setScrollOffset(value) + logger.debug("Firing ScrollContainer.setScrollOffset " .. tostring(value)) + logger.debug("maxScrollOffset " .. tostring(self.maxScrollOffset)) self.scrollOffset = util.bound(-self.maxScrollOffset, value, 0) end @@ -35,6 +38,11 @@ end ---@param offset number the offset of the element to be kept visible ---@param size number the size of the element to be kept visible function ScrollContainer:keepVisible(offset, size) + logger.debug("Firing ScrollContainer.keepVisible") + -- weird implementation detail that with scrolling scrolloffset goes negative + -- childGap is added to guarantee some whitespace if the container is built around it + offset = -offset + self.childGap / 2 + size = size + self.childGap -- with increasing negative value we scroll further down/right local refSize if self.scrollOrientation == "vertical" then @@ -61,6 +69,13 @@ local function getTranslatedOffset(scrollContainer, x, y) end function ScrollContainer:onTouch(x, y) + logger.debug("Firing ScrollContainer.onTouch") + self.initialTouchX = x + self.initialTouchY = y + logger.debug("initialTouchY: " .. self.initialTouchY) + self.originalOffset = self.scrollOffset + logger.debug("originalOffset: " .. self.originalOffset) + local realTouchedElement = self:getTouchedChildElement(x, y) if realTouchedElement then self.touchedChild = realTouchedElement @@ -68,15 +83,12 @@ function ScrollContainer:onTouch(x, y) x, y = getTranslatedOffset(self, x, y) self.touchedChild:onTouch(x, y) end - else - self.scrolling = true - self.initialTouchX = x - self.initialTouchY = y - self.originalOffset = self.scrollOffset end end function ScrollContainer:onDrag(x, y) + logger.debug("Firing ScrollContainer.onDrag") + logger.debug("y: " .. y) if not self.touchedChild or not self.touchedChild.onDrag then if self.scrollOrientation == "vertical" then self:setScrollOffset(self.originalOffset + (y - self.initialTouchY)) @@ -90,10 +102,10 @@ function ScrollContainer:onDrag(x, y) end function ScrollContainer:onRelease(x, y, duration) - if not self.touchedChild then - self:onDrag(x, y) - self.scrolling = false - else + logger.debug("Firing ScrollContainer.onRelease") + self:onDrag(x, y) + + if self.touchedChild then if self.touchedChild.onRelease then x, y = getTranslatedOffset(self, x, y) self.touchedChild:onRelease(x, y) @@ -185,9 +197,9 @@ function ScrollContainer:onResize() local lastChild = self.children[#self.children] if lastChild then if self.scrollOrientation == "vertical" then - self.maxScrollOffset = lastChild.y + lastChild.height - self.height + self.padding + self.maxScrollOffset = math.max(0, lastChild.y + lastChild.height - self.height + self.padding) else - self.maxScrollOffset = lastChild.x + lastChild.width - self.width + self.padding + self.maxScrollOffset = math.max(0, lastChild.x + lastChild.width - self.width + self.padding) end else self.maxScrollOffset = 0 diff --git a/client/src/ui/Slider.lua b/client/src/ui/Slider.lua index ed01953f..642eae45 100644 --- a/client/src/ui/Slider.lua +++ b/client/src/ui/Slider.lua @@ -6,7 +6,7 @@ local GraphicsUtil = require("client.src.graphics.graphics_util") local handleRadius = 7.5 local xPadding = 8 -local yPadding = 2 +local yPadding = -18 local valueBackgroundPaddingX = 2 local valueBackgroundPaddingY = -1 -- textHeight isn't a tight bounds local sliderBarThickness = 6 @@ -51,6 +51,10 @@ local Slider = class( self.value = self:getBoundedValue(value) -- don't use set value as not everything is setup yet self.onlyChangeOnRelease = options.onlyChangeOnRelease or false + if options.hFill == nil then + self.hFill = true + end + self.minText = GraphicsUtil.newText(love.graphics.getFont(), tostring(self.min)) self.maxText = GraphicsUtil.newText(love.graphics.getFont(), tostring(self.max)) self.valueText = GraphicsUtil.newText(love.graphics.getFont(), tostring(self.value)) @@ -58,7 +62,7 @@ local Slider = class( local valueTextWidth, valueTextHeight = self.valueText:getDimensions() local textWidth, textHeight = self.maxText:getDimensions() self.width = self.tickLength * self:tickCount() + xPadding + math.max(xPadding, textWidth / 2) - self.height = yPadding * 2 + handleRadius * 2 + valueTextHeight + textHeight + self.height = 4 * 2 + handleRadius * 2 + valueTextHeight + textHeight end, UIElement ) diff --git a/client/src/ui/StackPanel.lua b/client/src/ui/StackPanel.lua index e1b22697..5c7e723b 100644 --- a/client/src/ui/StackPanel.lua +++ b/client/src/ui/StackPanel.lua @@ -51,7 +51,6 @@ end function StackPanel:addElement(uiElement) self:applyStackPanelSettings(uiElement) self:addChild(uiElement) - self:resize() end diff --git a/client/src/ui/Stepper.lua b/client/src/ui/Stepper.lua index a342bb64..3b72204c 100644 --- a/client/src/ui/Stepper.lua +++ b/client/src/ui/Stepper.lua @@ -5,6 +5,7 @@ local Label = require(PATH .. ".Label") local class = require("common.lib.class") local util = require("common.lib.util") local GraphicsUtil = require("client.src.graphics.graphics_util") +local HorizontalFlexLayout = require(PATH .. ".Layouts.HorizontalFlexLayout") local NAV_BUTTON_WIDTH = 25 local EMPTY_STEPPER_WIDTH = 160 @@ -58,11 +59,13 @@ local Stepper = class( function(self, options) self.onChange = options.onChange or function() end self.selectedIndex = options.selectedIndex or 1 + self.childGap = options.childGap or 8 local navButtonWidth = 25 self.leftButton = TextButton({ width = navButtonWidth, vAlign = "center", + hAlign = "center", label = Label({text = "<", translate = false}), onClick = function(selfElement, inputSource, holdTime) setState(self, self.selectedIndex - 1) @@ -71,24 +74,26 @@ local Stepper = class( self.rightButton = TextButton({ width = navButtonWidth, vAlign = "center", + hAlign = "center", label = Label({text = ">", translate = false}), onClick = function(selfElement, inputSource, holdTime) setState(self, self.selectedIndex + 1) end }) self:addChild(self.leftButton) - self:addChild(self.rightButton) - + self.color = {.5, .5, 1, .7} self.borderColor = {.7, .7, 1, .7} - + setLabels(self, options.labels, options.values, self.selectedIndex) + self:addChild(self.rightButton) - self.TYPE = "Stepper" end, UIElement ) +Stepper.TYPE = "Stepper" +Stepper.layout = HorizontalFlexLayout Stepper.setLabels = setLabels Stepper.setState = setState @@ -121,7 +126,7 @@ end -- Remove all attached labels, preserving the navigation buttons function Stepper:removeLabelChildren() - for i = #self.children, 3, -1 do + for i = #self.children - 1, 2, -1 do self.children[i]:detach() end end diff --git a/client/src/ui/TextButton.lua b/client/src/ui/TextButton.lua index 82744dcc..7d3bc092 100644 --- a/client/src/ui/TextButton.lua +++ b/client/src/ui/TextButton.lua @@ -16,6 +16,8 @@ local TextButton = class(function(self, options) self.label = options.label self.label.hAlign = "center" self.label.vAlign = "center" + self.hFill = true + self.padding = options.padding or 8 self:addChild(self.label) end, Button) TextButton.TYPE = "TextButton" diff --git a/client/src/ui/UIElement.lua b/client/src/ui/UIElement.lua index 65100092..dd17c833 100644 --- a/client/src/ui/UIElement.lua +++ b/client/src/ui/UIElement.lua @@ -192,7 +192,7 @@ function UIElement:drawSelf() love.graphics.setColor(self.backgroundColor) love.graphics.rectangle("fill", self.x, self.y, self.width, self.height) love.graphics.setColor(1, 1, 1, 1) - love.graphics.print(self.width .. ", " .. self.height, self.x + 5, self.y + 5) + --love.graphics.print(self.width .. ", " .. self.height, self.x + 5, self.y + 5) end function UIElement:drawChildren() @@ -207,18 +207,20 @@ end -- if you want to stop drawing an element, e.g. due to changing a subscreen, -- the more opportune method is to simply remove it from the ui tree via detach() function UIElement:setVisibility(isVisible) - self.isVisible = isVisible - self:onVisibilityChanged() + if self.isVisible ~= isVisible then + self.isVisible = isVisible + self:onVisibilityChanged() + end end function UIElement:onVisibilityChanged() + if self.parent then + onChildrenChanged(self.parent) + end end function UIElement:setEnabled(isEnabled) self.isEnabled = isEnabled - for _, uiElement in ipairs(self.children) do - uiElement:setEnabled(isEnabled) - end end function UIElement:inBounds(x, y) diff --git a/client/src/ui/VerticalMenu.lua b/client/src/ui/VerticalMenu.lua index 0717b3ef..0ddcba7e 100644 --- a/client/src/ui/VerticalMenu.lua +++ b/client/src/ui/VerticalMenu.lua @@ -36,7 +36,7 @@ function VerticalMenu:selectPrevious() break end end - self:keepVisible(-self.children[self.selectedIndex].y, self.children[self.selectedIndex].height) + self:keepVisible(self.children[self.selectedIndex].y, self.children[self.selectedIndex].height) GAME.theme:playMoveSfx() end @@ -49,10 +49,20 @@ function VerticalMenu:selectNext() break end end - self:keepVisible(-self.children[self.selectedIndex].y, self.children[self.selectedIndex].height) + self:keepVisible(self.children[self.selectedIndex].y, self.children[self.selectedIndex].height) GAME.theme:playMoveSfx() end +function VerticalMenu:selectLast() + for i = #self.children, 1, -1 do + local child = self.children[i] + if child.receiveInputs and child.isEnabled and child.isVisible then + self.selectedIndex = i + break + end + end +end + function VerticalMenu:receiveInputs(inputs, dt) if not self.selectedIndex then self:setInitialFocus() @@ -73,7 +83,7 @@ function VerticalMenu:receiveInputs(inputs, dt) self.focused:receiveInputs(inputs, dt) elseif inputs.isDown["MenuEsc"] then if self.selectedIndex ~= #self.children then - self:setSelectedIndex(#self.children) + self:selectLast() GAME.theme:playCancelSfx() else selectedElement:receiveInputs(inputs, dt) diff --git a/client/src/ui/touchHandler.lua b/client/src/ui/touchHandler.lua index e297c264..b5977e8b 100644 --- a/client/src/ui/touchHandler.lua +++ b/client/src/ui/touchHandler.lua @@ -1,6 +1,5 @@ local input = require("client.src.inputManager") -- handles all touch interactions --- all elements that implement touch interactions must register themselves with the touch handler on construction local touchHandler = { ---@type UiElement? From 91bf458e6adb9645c15e701496b10b96965c0790 Mon Sep 17 00:00:00 2001 From: Endaris Date: Sun, 11 May 2025 20:38:54 +0200 Subject: [PATCH 04/49] fully replace Menu with new ScrollContainer-based VerticalMenu --- client/src/scenes/ChallengeModeMenu.lua | 28 +- client/src/scenes/CharacterSelect.lua | 14 +- client/src/scenes/DesignHelper.lua | 4 +- client/src/scenes/GameBase.lua | 47 ++- client/src/scenes/InputConfigMenu.lua | 36 +- client/src/scenes/Lobby.lua | 67 ++-- client/src/scenes/ModManagement.lua | 26 +- client/src/scenes/PortraitGame.lua | 4 +- client/src/scenes/PuzzleMenu.lua | 41 ++- client/src/scenes/SetNameMenu.lua | 4 +- client/src/scenes/SetUserIdMenu.lua | 2 +- client/src/scenes/SoundTest.lua | 60 ++-- client/src/scenes/TrainingMenu.lua | 43 ++- client/src/scenes/WindowSizeTester.lua | 4 +- .../src/ui/Layouts/HorizontalScrollLayout.lua | 34 ++ .../src/ui/Layouts/VerticalScrollLayout.lua | 34 ++ client/src/ui/Menu.lua | 338 ------------------ client/src/ui/MenuItem.lua | 1 - client/src/ui/ScrollContainer.lua | 7 + client/src/ui/VerticalMenu.lua | 2 + client/src/ui/init.lua | 1 - 21 files changed, 300 insertions(+), 497 deletions(-) create mode 100644 client/src/ui/Layouts/HorizontalScrollLayout.lua create mode 100644 client/src/ui/Layouts/VerticalScrollLayout.lua delete mode 100644 client/src/ui/Menu.lua diff --git a/client/src/scenes/ChallengeModeMenu.lua b/client/src/scenes/ChallengeModeMenu.lua index c124e70e..0be885c3 100644 --- a/client/src/scenes/ChallengeModeMenu.lua +++ b/client/src/scenes/ChallengeModeMenu.lua @@ -32,7 +32,7 @@ function ChallengeModeMenu:load(sceneParams) local difficultyLabels = {} local challengeModes = {} for i = 1, ChallengeMode.numDifficulties do - table.insert(difficultyLabels, ui.Label({text = "challenge_difficulty_" .. i})) + table.insert(difficultyLabels, ui.Label({id = "challenge_difficulty_" .. i})) table.insert(challengeModes, i) end @@ -48,15 +48,25 @@ function ChallengeModeMenu:load(sceneParams) } ) - local menuItems = { - ui.MenuItem.createStepperMenuItem("difficulty", nil, nil, difficultyStepper), - ui.MenuItem.createButtonMenuItem("go_", nil, nil, function() - self:goToCharacterSelect(difficultyStepper.value) - end), - ui.MenuItem.createButtonMenuItem("back", nil, nil, exitMenu) - } + local challengeModeDifficulty = ui.MenuItem.createStepperMenuItem("difficulty", nil, nil, difficultyStepper) + local go = ui.MenuItem.createButtonMenuItem("go_", nil, nil, function() + self:goToCharacterSelect(difficultyStepper.value) + end) + local back = ui.MenuItem.createButtonMenuItem("back", nil, nil, exitMenu) + + self.menu = ui.VerticalMenu({ + hAlign = "center", + minHeight = 480, + maxHeight = 540, + childGap = 8, + padding = 32, + width = 600, + }) + + self.menu:addChild(challengeModeDifficulty) + self.menu:addChild(go) + self.menu:addChild(back) - self.menu = ui.Menu.createCenteredMenu(menuItems) self.uiRoot:addChild(self.menu) end diff --git a/client/src/scenes/CharacterSelect.lua b/client/src/scenes/CharacterSelect.lua index c06b0c82..5adf18cb 100644 --- a/client/src/scenes/CharacterSelect.lua +++ b/client/src/scenes/CharacterSelect.lua @@ -159,7 +159,7 @@ function CharacterSelect:createReadyButton() local readyButton = ui.TextButton({ hFill = true, vFill = true, - label = ui.Label({text = "ready"}), + label = ui.Label({id = "ready"}), backgroundColor = {1, 1, 1, 0}, outlineColor = {1, 1, 1, 1} }) @@ -184,7 +184,7 @@ function CharacterSelect:createLeaveButton() leaveButton = ui.TextButton({ hFill = true, vFill = true, - label = ui.Label({text = "leave"}), + label = ui.Label({id = "leave"}), backgroundColor = {1, 1, 1, 0}, outlineColor = {1, 1, 1, 1}, onClick = function() @@ -760,7 +760,7 @@ function CharacterSelect:createPlayerInfo(player) stackPanel.winrateLabel = ui.Label({ x = 4, - text = "ss_winrate" + id = "ss_winrate" }) stackPanel.winrateValueLabel = ui.Label({ @@ -877,10 +877,10 @@ end function CharacterSelect:createDifficultyCarousel(player, height) local passengers = { - { id = 1, uiElement = ui.Label({text = "easy", vAlign = "center", hAlign = "center"})}, - { id = 2, uiElement = ui.Label({text = "normal", vAlign = "center", hAlign = "center"})}, - { id = 3, uiElement = ui.Label({text = "hard", vAlign = "center", hAlign = "center"})}, - { id = 4, uiElement = ui.Label({text = "ss_ex_mode", vAlign = "center", hAlign = "center"})}, + { id = 1, uiElement = ui.Label({id = "easy", vAlign = "center", hAlign = "center"})}, + { id = 2, uiElement = ui.Label({id = "normal", vAlign = "center", hAlign = "center"})}, + { id = 3, uiElement = ui.Label({id = "hard", vAlign = "center", hAlign = "center"})}, + { id = 4, uiElement = ui.Label({id = "ss_ex_mode", vAlign = "center", hAlign = "center"})}, } local difficultyCarousel = ui.Carousel({ isEnabled = player.isLocal, diff --git a/client/src/scenes/DesignHelper.lua b/client/src/scenes/DesignHelper.lua index 3bd1ac31..7b9b99fb 100644 --- a/client/src/scenes/DesignHelper.lua +++ b/client/src/scenes/DesignHelper.lua @@ -15,8 +15,8 @@ function DesignHelper:load() --self:loadStages() --self.grid:createElementAt(1, 2, 2, 1, "stage", self.stageCarousel) self.rankedSelection = ui.StackPanel({vFill = true, alignment = "left", hAlign = "center", vAlign = "center"}) - local trueLabel = ui.Label({text = "ss_ranked", vAlign = "top", hAlign = "center"}) - local falseLabel = ui.Label({text = "ss_casual", vAlign = "bottom", hAlign = "center"}) + local trueLabel = ui.Label({id = "ss_ranked", vAlign = "top", hAlign = "center"}) + local falseLabel = ui.Label({id = "ss_casual", vAlign = "bottom", hAlign = "center"}) self.rankedSelection:addChild(trueLabel) self.rankedSelection:addChild(falseLabel) self.grid:createElementAt(3, 2, 2, 1, "ranked", self.rankedSelection) diff --git a/client/src/scenes/GameBase.lua b/client/src/scenes/GameBase.lua index 3b648611..eda96a14 100644 --- a/client/src/scenes/GameBase.lua +++ b/client/src/scenes/GameBase.lua @@ -178,32 +178,31 @@ function GameBase:load() self.backgroundImage = UpdatingImage(self.stage.images.background, false, 0, 0, consts.CANVAS_WIDTH, consts.CANVAS_HEIGHT) self.stageTrack = self:getStageTrack() - local pauseMenuItems = { - ui.MenuItem.createButtonMenuItem("pause_resume", nil, true, function() - GAME.theme:playValidationSfx() - self.pauseMenu:setVisibility(false) - self.match:togglePause() - if self.stageTrack and self.pauseState.musicWasPlaying then - SoundController:playMusic(self.stageTrack) - end - self:initializeFrameInfo() - end), - ui.MenuItem.createButtonMenuItem("back", nil, true, function() - GAME.theme:playCancelSfx() - self.match:abort() - self:startNextScene() - end), - } - - self.pauseMenu = ui.Menu({ - x = 0, - y = 0, + local resume = ui.MenuItem.createButtonMenuItem("pause_resume", nil, true, function() + GAME.theme:playValidationSfx() + self.pauseMenu:setVisibility(false) + self.match:togglePause() + if self.stageTrack and self.pauseState.musicWasPlaying then + SoundController:playMusic(self.stageTrack) + end + self:initializeFrameInfo() + end) + local back = ui.MenuItem.createButtonMenuItem("back", nil, true, function() + GAME.theme:playCancelSfx() + self.match:abort() + self:startNextScene() + end) + + self.pauseMenu = ui.VerticalMenu({ hAlign = "center", - vAlign = "center", - menuItems = pauseMenuItems, - height = 200 + minHeight = 480, + maxHeight = 540, + childGap = 8, + padding = 32, + isVisible = false, }) - self.pauseMenu:setVisibility(false) + self.pauseMenu:addChild(resume) + self.pauseMenu:addChild(back) self.uiRoot:addChild(self.pauseMenu) leftover_time = 1 / 120 diff --git a/client/src/scenes/InputConfigMenu.lua b/client/src/scenes/InputConfigMenu.lua index e56acbce..f58d60cb 100644 --- a/client/src/scenes/InputConfigMenu.lua +++ b/client/src/scenes/InputConfigMenu.lua @@ -162,29 +162,43 @@ end function InputConfigMenu:load(sceneParams) self.configIndex = 1 - local menuOptions = {} self.slider = ui.Slider({ min = 1, max = input.maxConfigurations, value = 1, tickLength = 10, - onValueChange = function(slider) self:updateInputConfigMenuLabels(slider.value) end}) - menuOptions[1] = ui.MenuItem.createSliderMenuItem("configuration", nil, nil, self.slider) + onValueChange = function(slider) self:updateInputConfigMenuLabels(slider.value) end + }) + + local configSelection = ui.MenuItem.createSliderMenuItem("configuration", nil, nil, self.slider) + local setAllKeys = ui.MenuItem.createButtonMenuItem("op_all_keys", nil, nil, function() self:setAllKeysStart() end) + local clearAllKeys = ui.MenuItem.createButtonMenuItem("Clear All Inputs", nil, false, function() self:clearAllInputs() end) + local resetToDefault = ui.MenuItem.createButtonMenuItem("Reset Keys To Default", nil, false, function() self:resetToDefault(menuOptions) end) + local back = ui.MenuItem.createButtonMenuItem("back", nil, nil, exitMenu) + + self.menu = ui.VerticalMenu({ + hAlign = "center", + minHeight = 480, + maxHeight = 840, + childGap = 8, + padding = 32, + width = 600, + }) + + self.menu:addChild(configSelection) for i, key in ipairs(consts.KEY_NAMES) do - local clickFunction = function() + local clickFunction = function() if not self.settingKey then self:setKeyStart(key) end end local keyName = self:getKeyDisplayName(GAME.input.inputConfigurations[self.configIndex][key]) - menuOptions[#menuOptions + 1] = ui.MenuItem.createLabeledButtonMenuItem(key, nil, false, keyName, nil, false, clickFunction) + self.menu:addChild(ui.MenuItem.createLabeledButtonMenuItem(key, nil, false, keyName, nil, false, clickFunction)) end - menuOptions[#menuOptions + 1] = ui.MenuItem.createButtonMenuItem("op_all_keys", nil, nil, function() self:setAllKeysStart() end) - menuOptions[#menuOptions + 1] = ui.MenuItem.createButtonMenuItem("Clear All Inputs", nil, false, function() self:clearAllInputs() end) - menuOptions[#menuOptions + 1] = ui.MenuItem.createButtonMenuItem("Reset Keys To Default", nil, false, function() self:resetToDefault(menuOptions) end) - menuOptions[#menuOptions + 1] = ui.MenuItem.createButtonMenuItem("back", nil, nil, exitMenu) - - self.menu = ui.Menu.createCenteredMenu(menuOptions) + self.menu:addChild(setAllKeys) + self.menu:addChild(clearAllKeys) + self.menu:addChild(resetToDefault) + self.menu:addChild(back) self.uiRoot:addChild(self.menu) end diff --git a/client/src/scenes/Lobby.lua b/client/src/scenes/Lobby.lua index 48455457..50813850 100644 --- a/client/src/scenes/Lobby.lua +++ b/client/src/scenes/Lobby.lua @@ -13,7 +13,7 @@ local Lobby = class(function(self, sceneParams) -- ui self.leaderboard = ui.Leaderboard({isVisible = false, x = 200, hAlign = "center", vAlign = "center"}) - self.lobbyMessage = ui.Label({text = "lb_select_player"}) + self.lobbyMessage = ui.Label({id = "lb_select_player"}) self.backgroundImg = themes[config.theme].images.bg_main self.lobbyMenu = nil self.lobbyMenuXoffsetMap = { @@ -58,34 +58,47 @@ function Lobby:load(sceneParams) end function Lobby:initLobbyMenu() - local menuItems = { - ui.MenuItem.createMenuItem(self.lobbyMessage), - ui.MenuItem.createButtonMenuItem("mm_1_endless", nil, nil, function() - GAME.netClient:requestRoom(GameModes.getPreset("ONE_PLAYER_ENDLESS")) - end), - ui.MenuItem.createButtonMenuItem("mm_1_time", nil, nil, function() - GAME.netClient:requestRoom(GameModes.getPreset("ONE_PLAYER_TIME_ATTACK")) - end), - ui.MenuItem.createButtonMenuItem("mm_1_vs", nil, nil, function() - if GAME.localPlayer.settings.style ~= GameModes.Styles.MODERN then - GAME.localPlayer:setStyle(GameModes.Styles.MODERN) - GAME.netClient:sendPlayerSettings(GAME.localPlayer) - end - GAME.netClient:requestRoom(GameModes.getPreset("ONE_PLAYER_VS_SELF")) - end), - ui.MenuItem.createButtonMenuItem("lb_show_board", nil, nil, function() - if self.leaderboard.hasFocus then - self.leaderboard:yieldFocus() - else - self:toggleLeaderboard() - end - end), - ui.MenuItem.createButtonMenuItem("lb_back", nil, nil, exitMenu) - } - self.leaderboardToggleLabel = menuItems[5].textButton.children[1] + local lobbyMessage = ui.MenuItem.createMenuItem(self.lobbyMessage) + local endless = ui.MenuItem.createButtonMenuItem("mm_1_endless", nil, nil, function() + GAME.netClient:requestRoom(GameModes.getPreset("ONE_PLAYER_ENDLESS")) + end) + local time = ui.MenuItem.createButtonMenuItem("mm_1_time", nil, nil, function() + GAME.netClient:requestRoom(GameModes.getPreset("ONE_PLAYER_TIME_ATTACK")) + end) + local vsSelf = ui.MenuItem.createButtonMenuItem("mm_1_vs", nil, nil, function() + if GAME.localPlayer.settings.style ~= GameModes.Styles.MODERN then + GAME.localPlayer:setStyle(GameModes.Styles.MODERN) + GAME.netClient:sendPlayerSettings(GAME.localPlayer) + end + GAME.netClient:requestRoom(GameModes.getPreset("ONE_PLAYER_VS_SELF")) + end) + local showLeaderboard = ui.MenuItem.createButtonMenuItem("lb_show_board", nil, nil, function() + if self.leaderboard.hasFocus then + self.leaderboard:yieldFocus() + else + self:toggleLeaderboard() + end + end) + local back = ui.MenuItem.createButtonMenuItem("lb_back", nil, nil, exitMenu) + self.leaderboardToggleLabel = showLeaderboard.children[1] self.lobbyMenuStartingUp = true - self.lobbyMenu = ui.Menu.createCenteredMenu(menuItems) + self.lobbyMenu = ui.VerticalMenu({ + hAlign = "center", + minHeight = 480, + maxHeight = 840, + childGap = 8, + padding = 32, + width = 600, + }) + + self.lobbyMenu:addChild(lobbyMessage) + self.lobbyMenu:addChild(endless) + self.lobbyMenu:addChild(time) + self.lobbyMenu:addChild(vsSelf) + self.lobbyMenu:addChild(showLeaderboard) + self.lobbyMenu:addChild(back) + self.lobbyMenu.x = self.lobbyMenuXoffsetMap[false] self.uiRoot:addChild(self.lobbyMenu) diff --git a/client/src/scenes/ModManagement.lua b/client/src/scenes/ModManagement.lua index f4ef410e..664ab20d 100644 --- a/client/src/scenes/ModManagement.lua +++ b/client/src/scenes/ModManagement.lua @@ -115,26 +115,24 @@ function ModManagement:load() end ) - local menuItems = { - self.manageCharactersButton, - self.manageStagesButton, - self.backButton - } - - if system.supportsFileBrowserOpen() then - table.insert(menuItems, 3, self.openSaveDirectoryButton) - end - - self.menu = ui.Menu({ - menuItems = menuItems, + self.menu = ui.VerticalMenu({ x = 100, y = 0, hAlign = "left", vAlign = "center", - width = 200, - height = 300, + minHeight = 480, + maxHeight = 540, + childGap = 8, + padding = 32, }) + self.menu:addChild(self.manageCharactersButton) + self.menu:addChild(self.manageStagesButton) + if system.supportsFileBrowserOpen() then + self.menu:addChild(self.openSaveDirectoryButton) + end + self.menu:addChild(self.backButton) + self.uiRoot:addChild(self.menu) end diff --git a/client/src/scenes/PortraitGame.lua b/client/src/scenes/PortraitGame.lua index 056a88ee..7baf046f 100644 --- a/client/src/scenes/PortraitGame.lua +++ b/client/src/scenes/PortraitGame.lua @@ -36,7 +36,7 @@ function PortraitGame:customLoad() self.uiRoot.height = consts.CANVAS_WIDTH local communityMessage = ui.Label({ - text = "join_community", + id = "join_community", replacements = {"\ndiscord." .. consts.SERVER_LOCATION}, translate = true, hAlign = "center", @@ -214,7 +214,7 @@ function PortraitGame:flipToPortrait() stack.origin_x = stack.frameOriginX / stack.gfxScale -- create a raise button that interacts with the touch controller - local raiseButton = ui.TextButton({label = ui.Label({text = "raise", fontSize = 20}), hAlign = "right", vAlign = "bottom", height = player.stack:canvasHeight() / 2}) + local raiseButton = ui.TextButton({label = ui.Label({id = "raise", fontSize = 20}), hAlign = "right", vAlign = "bottom", height = player.stack:canvasHeight() / 2}) raiseButton.onTouch = function(button, x, y) button.backgroundColor[4] = 1 stack.touchInputDetector.touchingRaise = true diff --git a/client/src/scenes/PuzzleMenu.lua b/client/src/scenes/PuzzleMenu.lua index 80ac4f42..cad74b82 100644 --- a/client/src/scenes/PuzzleMenu.lua +++ b/client/src/scenes/PuzzleMenu.lua @@ -7,7 +7,7 @@ local LevelPresets = require("common.data.LevelPresets") -- Scene for the puzzle selection menu ---@class PuzzleMenu : Scene ----@field menu Menu +---@field menu VerticalMenu ---@field puzzleLabel Label ---@field levelSlider LevelSlider ---@field randomColorButtons ButtonGroup @@ -70,8 +70,8 @@ function PuzzleMenu:load(sceneParams) self.randomColorsButtons = ui.ButtonGroup( { buttons = { - ui.TextButton({label = ui.Label({text = "op_off"}), width = BUTTON_WIDTH, height = BUTTON_HEIGHT}), - ui.TextButton({label = ui.Label({text = "op_on"}), width = BUTTON_WIDTH, height = BUTTON_HEIGHT}), + ui.TextButton({label = ui.Label({id = "op_off"}), width = BUTTON_WIDTH, height = BUTTON_HEIGHT}), + ui.TextButton({label = ui.Label({id = "op_on"}), width = BUTTON_WIDTH, height = BUTTON_HEIGHT}), }, values = {false, true}, selectedIndex = config.puzzle_randomColors and 2 or 1, @@ -85,8 +85,8 @@ function PuzzleMenu:load(sceneParams) self.randomlyFlipPuzzleButtons = ui.ButtonGroup( { buttons = { - ui.TextButton({label = ui.Label({text = "op_off"}), width = BUTTON_WIDTH, height = BUTTON_HEIGHT}), - ui.TextButton({label = ui.Label({text = "op_on"}), width = BUTTON_WIDTH, height = BUTTON_HEIGHT}), + ui.TextButton({label = ui.Label({id = "op_off"}), width = BUTTON_WIDTH, height = BUTTON_HEIGHT}), + ui.TextButton({label = ui.Label({id = "op_on"}), width = BUTTON_WIDTH, height = BUTTON_HEIGHT}), }, values = {false, true}, selectedIndex = config.puzzle_randomFlipped and 2 or 1, @@ -97,21 +97,30 @@ function PuzzleMenu:load(sceneParams) } ) - local menuOptions = { - ui.MenuItem.createSliderMenuItem("level", nil, nil, self.levelSlider), - ui.MenuItem.createToggleButtonGroupMenuItem("randomColors", nil, nil, self.randomColorsButtons), - ui.MenuItem.createToggleButtonGroupMenuItem("randomHorizontalFlipped", nil, nil, self.randomlyFlipPuzzleButtons), - } - + local levelSelection = ui.MenuItem.createSliderMenuItem("level", nil, nil, self.levelSlider) + local randomizeColors = ui.MenuItem.createToggleButtonGroupMenuItem("randomColors", nil, nil, self.randomColorsButtons) + local randomFlip = ui.MenuItem.createToggleButtonGroupMenuItem("randomHorizontalFlipped", nil, nil, self.randomlyFlipPuzzleButtons) + local back = ui.MenuItem.createButtonMenuItem("back", nil, nil, function() self:exit() end) + + self.menu = ui.VerticalMenu({ + hAlign = "center", + minHeight = 480, + maxHeight = 840, + childGap = 8, + padding = 32, + width = 600, + }) + + self.menu:addChild(levelSelection) + self.menu:addChild(randomizeColors) + self.menu:addChild(randomFlip) for puzzleSetName, puzzleSet in pairsSortedByKeys(GAME.puzzleSets) do - menuOptions[#menuOptions + 1] = ui.MenuItem.createButtonMenuItem(puzzleSetName, nil, false, function() self:startGame(puzzleSet) end) + self.menu:addChild(ui.MenuItem.createButtonMenuItem(puzzleSetName, nil, false, function() self:startGame(puzzleSet) end)) end - menuOptions[#menuOptions + 1] = ui.MenuItem.createButtonMenuItem("back", nil, nil, function() self:exit() end) - - self.menu = ui.Menu.createCenteredMenu(menuOptions) + self.menu:addChild(back) local x, y = unpack(themes[config.theme].main_menu_screen_pos) - self.puzzleLabel = ui.Label({text = "pz_puzzles", x = x - 10, y = y - 40}) + self.puzzleLabel = ui.Label({id = "pz_puzzles", x = x - 10, y = y - 40}) self.uiRoot:addChild(self.menu) self.uiRoot:addChild(self.puzzleLabel) diff --git a/client/src/scenes/SetNameMenu.lua b/client/src/scenes/SetNameMenu.lua index 208a2ad4..253d6305 100644 --- a/client/src/scenes/SetNameMenu.lua +++ b/client/src/scenes/SetNameMenu.lua @@ -18,7 +18,7 @@ SetNameMenu.name = "SetNameMenu" function SetNameMenu:load() local x, y = unpack(themes[config.theme].main_menu_screen_pos) self.promptLabel = ui.Label({ - text = "op_enter_name", + id = "op_enter_name", vAlign = "top", hAlign = "center", y = y @@ -57,7 +57,7 @@ function SetNameMenu:load() self.uiRoot:addChild(self.nameLengthLabel) self.confirmationButton = ui.TextButton({ - label = ui.Label({text = "mm_set_name"}), + label = ui.Label({id = "mm_set_name"}), y = y + 100, vAlign = "top", hAlign = "center", diff --git a/client/src/scenes/SetUserIdMenu.lua b/client/src/scenes/SetUserIdMenu.lua index f01bdac1..46b8b122 100644 --- a/client/src/scenes/SetUserIdMenu.lua +++ b/client/src/scenes/SetUserIdMenu.lua @@ -30,7 +30,7 @@ function SetUserIdMenu:load(sceneParams) }) self.confirmationButton = ui.TextButton({ - label = ui.Label({text = "go_"}), + label = ui.Label({id = "go_"}), x = menuX, y = menuY + 60, vAlign = "top", diff --git a/client/src/scenes/SoundTest.lua b/client/src/scenes/SoundTest.lua index 325d941a..f061a556 100644 --- a/client/src/scenes/SoundTest.lua +++ b/client/src/scenes/SoundTest.lua @@ -144,9 +144,9 @@ function SoundTest:load() playButtonGroup = ui.ButtonGroup( { buttons = { - ui.TextButton({label = ui.Label({text = "op_off"})}), - ui.TextButton({label = ui.Label({text = "character"})}), - ui.TextButton({label = ui.Label({text = "stage"})}), + ui.TextButton({label = ui.Label({id = "op_off"})}), + ui.TextButton({label = ui.Label({id = "character"})}), + ui.TextButton({label = ui.Label({id = "stage"})}), }, values = {"", "character", "stage"}, selectedIndex = 1, @@ -162,9 +162,9 @@ function SoundTest:load() end } ) - + local labels, values = createSfxMenuInfo(characterStepper.value) - + sfxStepper = ui.Stepper( { labels = labels, @@ -175,33 +175,45 @@ function SoundTest:load() end } ) - + local playCharacterSFXFn = function() if #sfxStepper.labels > 0 then love.audio.play(love.audio.newSource(characters[characterStepper.value].path.."/"..sfxStepper.value, "static")) end end - local menuLabelWidth = 120 - local soundTestMenuOptions = { - ui.MenuItem.createStepperMenuItem("character", nil, nil, characterStepper), - ui.MenuItem.createStepperMenuItem("stage", nil, nil, stageStepper), - ui.MenuItem.createToggleButtonGroupMenuItem("op_music_type", nil, nil, musicTypeButtonGroup), - ui.MenuItem.createToggleButtonGroupMenuItem("Background", nil, false, playButtonGroup), - ui.MenuItem.createStepperMenuItem("op_music_sfx", nil, nil, sfxStepper), - ui.MenuItem.createButtonMenuItem("op_music_play", nil, nil, playCharacterSFXFn), - ui.MenuItem.createButtonMenuItem("back", nil, nil, function() - SoundController:stopMusic() - love.audio.stop() - themes[config.theme].sounds.menu_validate = menuValidateSound - GAME.navigationStack:pop() - end) - } - - self.soundTestMenu = ui.Menu.createCenteredMenu(soundTestMenuOptions) + local characterSelection = ui.MenuItem.createStepperMenuItem("character", nil, nil, characterStepper) + local stageSelection = ui.MenuItem.createStepperMenuItem("stage", nil, nil, stageStepper) + local musicType = ui.MenuItem.createToggleButtonGroupMenuItem("op_music_type", nil, nil, musicTypeButtonGroup) + local musicPlayback = ui.MenuItem.createToggleButtonGroupMenuItem("Background", nil, false, playButtonGroup) + local sfxSelection = ui.MenuItem.createStepperMenuItem("op_music_sfx", nil, nil, sfxStepper) + local sfxPlayback = ui.MenuItem.createButtonMenuItem("op_music_play", nil, nil, playCharacterSFXFn) + local back = ui.MenuItem.createButtonMenuItem("back", nil, nil, function() + SoundController:stopMusic() + love.audio.stop() + themes[config.theme].sounds.menu_validate = menuValidateSound + GAME.navigationStack:pop() + end) + + self.soundTestMenu = ui.VerticalMenu({ + hAlign = "center", + minHeight = 480, + maxHeight = 540, + childGap = 8, + padding = 32, + width = 600, + }) + + self.soundTestMenu:addChild(characterSelection) + self.soundTestMenu:addChild(stageSelection) + self.soundTestMenu:addChild(musicType) + self.soundTestMenu:addChild(musicPlayback) + self.soundTestMenu:addChild(sfxSelection) + self.soundTestMenu:addChild(sfxPlayback) + self.soundTestMenu:addChild(back) self.uiRoot:addChild(self.soundTestMenu) - + self.backgroundImg = themes[config.theme].images.bg_main -- stop main music diff --git a/client/src/scenes/TrainingMenu.lua b/client/src/scenes/TrainingMenu.lua index b6445db9..98945e51 100644 --- a/client/src/scenes/TrainingMenu.lua +++ b/client/src/scenes/TrainingMenu.lua @@ -9,9 +9,7 @@ local save = require("client.src.save") local TrainingMenu = class( function (self, sceneParams) - self.backgroundImg = themes[config.theme].images.bg_main self.keepMusic = true - self.menu = nil -- set in load self:load(sceneParams) end, Scene @@ -84,39 +82,52 @@ function TrainingMenu:load(sceneParams) ) local widthSlider = ui.Slider({ - min = 1, - max = 6, + min = 1, + max = 6, value = 1, tickLength = 15, onValueChange = function() garbagePatternStepper:setState(1) end }) local heightSlider = ui.Slider({ - min = 1, - max = 99, + min = 1, + max = 99, value = 1, onValueChange = function() garbagePatternStepper:setState(1) end }) - local menuItems = { - ui.MenuItem.createStepperMenuItem("Garbage Pattern", nil, false, garbagePatternStepper), - ui.MenuItem.createSliderMenuItem("width", nil, nil, widthSlider), - ui.MenuItem.createSliderMenuItem("height", nil, nil, heightSlider), - ui.MenuItem.createButtonMenuItem("go_", nil, nil, function() self:goToCharacterSelect(garbagePatternStepper.value, widthSlider.value, heightSlider.value) end), - ui.MenuItem.createButtonMenuItem("back", nil, nil, exitMenu) - } - self.menu = ui.Menu.createCenteredMenu(menuItems) + local presets = ui.MenuItem.createStepperMenuItem("Garbage Pattern", nil, false, garbagePatternStepper) + local width = ui.MenuItem.createSliderMenuItem("width", nil, nil, widthSlider) + local height = ui.MenuItem.createSliderMenuItem("height", nil, nil, heightSlider) + local go = ui.MenuItem.createButtonMenuItem("go_", nil, nil, function() self:goToCharacterSelect(garbagePatternStepper.value, widthSlider.value, heightSlider.value) end) + local back = ui.MenuItem.createButtonMenuItem("back", nil, nil, exitMenu) + + self.menu = ui.VerticalMenu({ + hAlign = "center", + minHeight = 480, + maxHeight = 540, + childGap = 8, + padding = 32, + width = 600, + }) + + self.menu:addChild(presets) + self.menu:addChild(width) + self.menu:addChild(height) + self.menu:addChild(go) + self.menu:addChild(back) + self.uiRoot:addChild(self.menu) end function TrainingMenu:update(dt) - self.backgroundImg:update(dt) + GAME.theme.images.bg_main:update(dt) self.menu:receiveInputs() end function TrainingMenu:draw() - self.backgroundImg:draw() + GAME.theme.images.bg_main:draw() self.uiRoot:draw() end diff --git a/client/src/scenes/WindowSizeTester.lua b/client/src/scenes/WindowSizeTester.lua index 5e4aa029..5fed33a6 100644 --- a/client/src/scenes/WindowSizeTester.lua +++ b/client/src/scenes/WindowSizeTester.lua @@ -43,8 +43,8 @@ function WindowSizeTester:load() local maximizedLabel = ui.ValueLabel({valueFunction = function() return "Maximized: " .. tostring(love.window.isMaximized()) end}) self.uiRoot.fullscreenSelection = ui.ButtonGroup({ buttons = { - ui.TextButton({width = 60, label = ui.Label({text = "op_off"})}), - ui.TextButton({width = 60, label = ui.Label({text = "op_on"})}) + ui.TextButton({width = 60, label = ui.Label({id = "op_off"})}), + ui.TextButton({width = 60, label = ui.Label({id = "op_on"})}) }, values = {false, true}, selectedIndex = flags.fullscreen and 2 or 1, diff --git a/client/src/ui/Layouts/HorizontalScrollLayout.lua b/client/src/ui/Layouts/HorizontalScrollLayout.lua new file mode 100644 index 00000000..8ea8b658 --- /dev/null +++ b/client/src/ui/Layouts/HorizontalScrollLayout.lua @@ -0,0 +1,34 @@ +local PATH = (...):gsub('%.[^%.]+$', '') +local HorizontalFlexLayout = require(PATH ..".HorizontalFlexLayout") + +---@class HorizontalScrollLayout : HorizontalFlexLayout +local HorizontalScrollLayout = setmetatable({}, {__index = HorizontalFlexLayout}) + +---@param uiElement UiElement +function HorizontalScrollLayout.positionChildren(uiElement) + local x = uiElement.padding + for _, child in ipairs(uiElement.children) do + if child.isVisible then + child.x = x + + if child.vAlign == "top" then + child.y = uiElement.padding + elseif child.vAlign == "center" then + child.y = (uiElement.height - child.height) / 2 + elseif child.vAlign == "bottom" then + child.y = (uiElement.height - child.height) - uiElement.padding + end + child.x = math.round(child.x) + child.y = math.round(child.y) + x = x + uiElement.childGap + child.width + end + end + + for _, child in ipairs(uiElement.children) do + if child.isVisible then + child.layout.positionChildren(child) + end + end +end + +return HorizontalScrollLayout \ No newline at end of file diff --git a/client/src/ui/Layouts/VerticalScrollLayout.lua b/client/src/ui/Layouts/VerticalScrollLayout.lua new file mode 100644 index 00000000..60374f62 --- /dev/null +++ b/client/src/ui/Layouts/VerticalScrollLayout.lua @@ -0,0 +1,34 @@ +local PATH = (...):gsub('%.[^%.]+$', '') +local VerticalFlexLayout = require(PATH ..".VerticalFlexLayout") + +---@class VerticalScrollLayout : VerticalFlexLayout +local VerticalScrollLayout = setmetatable({}, {__index = VerticalFlexLayout}) + +---@param uiElement UiElement +function VerticalScrollLayout.positionChildren(uiElement) + local y = uiElement.padding + for _, child in ipairs(uiElement.children) do + if child.isVisible then + child.y = y + + if child.hAlign == "left" then + child.x = uiElement.padding + elseif child.hAlign == "center" then + child.x = (uiElement.width - child.width) / 2 + elseif child.hAlign == "right" then + child.x = (uiElement.width - child.width) - uiElement.padding + end + child.x = math.round(child.x) + child.y = math.round(child.y) + y = y + uiElement.childGap + child.height + end + end + + for _, child in ipairs(uiElement.children) do + if child.isVisible then + child.layout.positionChildren(child) + end + end +end + +return VerticalScrollLayout \ No newline at end of file diff --git a/client/src/ui/Menu.lua b/client/src/ui/Menu.lua deleted file mode 100644 index 73572645..00000000 --- a/client/src/ui/Menu.lua +++ /dev/null @@ -1,338 +0,0 @@ -local table = table - -local PATH = (...):gsub('%.[^%.]+$', '') -local UIElement = require(PATH .. ".UIElement") -local Label = require(PATH .. ".Label") -local directsFocus = require(PATH .. ".FocusDirector") -local class = require("common.lib.class") -local input = require("client.src.inputManager") - -local NAVIGATION_BUTTON_WIDTH = 30 - --- Menu is a collection of buttons that stack vertically and supports scrolling and keyboard navigation. --- It requires the passed in menu items to have valid widths and adds padding between each. The height also must be passed in --- and the width is the maximum of all buttons. ----@class Menu : UiElement -local Menu = class( - function(self, options) - ---@class Menu - self = self - - self.selectedIndex = 1 - self.yMin = self.y - self.totalHeight = 0 - self.menuItemYOffsets = {} - self.allContentShowing = true - - self.upIndicator = Label({text = "^", translate = false, isVisible = false, vAlign = "top", hAlign = "center", y = -14}) - self.downIndicator = Label({text = "v", translate = false, isVisible = false, vAlign = "bottom", hAlign = "center"}) - self:addChild(self.upIndicator) - self:addChild(self.downIndicator) - - -- bogus this should be passed in? - self.centerVertically = themes[config.theme].centerMenusVertically - - self.yOffset = 0 - self.firstActiveIndex = 1 - self.lastActiveIndex = 1 - self:setMenuItems(options.menuItems) - directsFocus(self) - end, - UIElement -) - -Menu.TYPE = "VerticalScrollingButtonMenu" - -Menu.NAVIGATION_BUTTON_WIDTH = NAVIGATION_BUTTON_WIDTH -Menu.BUTTON_HORIZONTAL_PADDING = 0 -Menu.BUTTON_VERTICAL_PADDING = 8 - -function Menu.createCenteredMenu(items) - local menu = Menu({ - x = 0, - y = 0, - hAlign = "center", - vAlign = "center", - menuItems = items, - height = themes[config.theme].main_menu_max_height, - layout = require(PATH .. ".Layouts.VerticalFlexLayout") - }) - - return menu -end - --- Sets the menu items for this menu --- menuItems: a list of UIElement tuples of the form: --- {{Label/Button, ButtonGroup/Stepper/Slider}, ...} --- the actual self.menuItems list is formated slightly differently, consisting of a list of Labels or Buttons --- each of which may have a ButtonGroup, Stepper, or Slider child element which controls the action for that item -function Menu:setMenuItems(menuItems) - if self.menuItems then - for i, menuItem in ipairs(self.menuItems) do - menuItem:detach() - end - end - - self.menuItems = {} - - for i, menuItem in ipairs(menuItems) do - self:addChild(menuItem) - self.menuItems[#self.menuItems + 1] = menuItem - end - self:setSelectedIndex(1) -end - -function Menu:updateLayout() - self.upIndicator:setVisibility(false) - self.downIndicator:setVisibility(false) - self.allContentShowing = self.yOffset == 0 - self.firstActiveIndex = nil - self.lastActiveIndex = nil - self.width = 0 - self.totalHeight = 0 - - if #self.menuItems == 0 then - return - end - - local currentY = 0 - local totalMenuHeight = 0 - local menuFull = false - for i, menuItem in ipairs(self.menuItems) do - self.menuItemYOffsets[i] = currentY - menuItem:setVisibility(false) - local realY = currentY - self.yOffset - if realY < 0 then - self.upIndicator:setVisibility(true) - end - if menuFull == false and realY >= 0 then - if realY + menuItem.height < self.height then - if self.firstActiveIndex == nil then - self.firstActiveIndex = i - end - menuItem.x = Menu.BUTTON_HORIZONTAL_PADDING - menuItem.y = realY - menuItem:setVisibility(true) - else - self.allContentShowing = false - self.downIndicator:setVisibility(true) - menuFull = true - end - end - currentY = currentY + menuItem.height + Menu.BUTTON_VERTICAL_PADDING - if menuFull == false then - self.lastActiveIndex = i - totalMenuHeight = realY + menuItem.height - end - self.width = math.max(self.width, menuItem.width) - self.totalHeight = self.totalHeight + menuItem.height + Menu.BUTTON_VERTICAL_PADDING - end - - if self.centerVertically then - self.y = self.yMin + (self.height / 2) - (totalMenuHeight / 2) - else - self.y = self.yMin - end -end - -function Menu:addMenuItem(index, menuItem) - local needsIncreasedIndex = false - if index <= self.selectedIndex then - needsIncreasedIndex = true - end - table.insert(self.menuItems, index, menuItem) - self:addChild(menuItem) - if needsIncreasedIndex then - self:setSelectedIndex(self.selectedIndex + 1) - end - self:updateLayout() -end - -function Menu:removeMenuItemAtIndex(index) - return self:removeMenuItem(self.menuItems[index].id) -end - -function Menu:indexOfMenuItemID(menuItemId) - local menuItemIndex = nil - for i, menuItem in ipairs(self.menuItems) do - if menuItemId == menuItem.id then - menuItemIndex = i - break - end - end - return menuItemIndex -end - -function Menu:containsMenuItemID(menuItemId) - return self:indexOfMenuItemID(menuItemId) ~= nil -end - -function Menu:removeMenuItem(menuItemId) - local menuItemIndex = self:indexOfMenuItemID(menuItemId) - - if menuItemIndex == nil then - return - end - - local needsDecreasedIndex = false - if menuItemIndex <= self.selectedIndex then - needsDecreasedIndex = true - end - - local menuItem = table.remove(self.menuItems, menuItemIndex) - menuItem:detach() - - if needsDecreasedIndex then - self:setSelectedIndex(self.selectedIndex - 1) - end - - self:updateLayout() - return menuItem -end - --- Updates the selected index of the menu --- Also updates the scroll state to show the button if off screen -function Menu:setSelectedIndex(index) - if index <= 0 then - index = 1 -- 1 index is the default if no items - end - - if #self.menuItems >= self.selectedIndex then - self.menuItems[self.selectedIndex]:setSelected(false) - end - if self.firstActiveIndex > index then - self.yOffset = self.menuItemYOffsets[index] - elseif self.lastActiveIndex < index then - local currentIndex = 1 - local bottomOfDesiredIndex = self.menuItemYOffsets[index] + self.menuItems[index].height - while self.menuItemYOffsets[currentIndex] + self.height < bottomOfDesiredIndex do - currentIndex = currentIndex + 1 - if currentIndex >= #self.menuItems then - break - end - end - self.yOffset = self.menuItemYOffsets[currentIndex] - end - self.selectedIndex = index - if #self.menuItems > 0 then - self.menuItems[self.selectedIndex]:setSelected(true) - end - self:updateLayout() -end - -function Menu:scrollUp() - self:setSelectedIndex(wrap(1, self.selectedIndex - 1, #self.menuItems)) - GAME.theme:playMoveSfx() -end - -function Menu:scrollDown() - self:setSelectedIndex(wrap(1, self.selectedIndex + 1, #self.menuItems)) - GAME.theme:playMoveSfx() -end - -function Menu:receiveInputs(inputs, dt) - if not self.isEnabled then - return - end - - if not inputs then - -- if we don't get inputs passed, use the global input table - inputs = input - end - - local selectedElement = self.menuItems[self.selectedIndex] - - if self.focused then - self.focused:receiveInputs(inputs, dt) - elseif inputs.isDown["MenuEsc"] then - if self.selectedIndex ~= #self.menuItems then - self:setSelectedIndex(#self.menuItems) - GAME.theme:playCancelSfx() - else - selectedElement:receiveInputs(inputs, dt) - end - elseif inputs:isPressedWithRepeat("MenuUp") then - self:scrollUp() - elseif inputs:isPressedWithRepeat("MenuDown") then - self:scrollDown() - else - if inputs.isDown["MenuSelect"] and selectedElement.isFocusable then - self:setFocus(selectedElement) - else - selectedElement:receiveInputs(inputs, dt) - end - end -end - -function Menu:update(dt) - -end - -function Menu:drawSelf() - -end - -function Menu:onTouch(x, y) - self.swiping = true - self.initialTouchX = x - self.initialTouchY = y - self.originalY = self.yOffset - local realTouchedElement = UIElement.getTouchedElement(self, x, y) - if realTouchedElement and realTouchedElement ~= self then - self.touchedChild = realTouchedElement - self.touchedChild:onTouch(x, y) - end -end - -function Menu:onDrag(x, y) - if not self.touchedChild or not self.touchedChild.onDrag then - local yOffset = y - self.initialTouchY - if self.height < self.totalHeight then - if yOffset > 0 then - self.yOffset = math.max(self.originalY - yOffset, -50)-- - 2 * NAVIGATION_BUTTON_WIDTH) - else - self.yOffset = math.min(self.totalHeight - self.height + 50, self.originalY - yOffset) - end - self:updateLayout() - end - else - self.touchedChild:onDrag(x, y) - end -end - -function Menu:onRelease(x, y) - if not self.touchedChild or not self.touchedChild.onRelease then - self:onDrag(x, y) - else - if self.yOffset ~= self.originalY then - -- we dragged so trigger with the original touch coordinates - -- that way the button will only trigger its on-click if it still touches the start coords - self.touchedChild:onRelease(self.initialTouchX, self.initialTouchY) - else - self.touchedChild:onRelease(x, y) - end - end - - self.swiping = false - self.touchedChild = nil -end - --- overwrite the default callback to always return itself --- while keeping a reference to the really touched element -function Menu:getTouchedElement(x, y) - if self.isVisible and self.isEnabled and self:inBounds(x, y) then - if self.allContentShowing then - local touchedElement - for i = 1, #self.children do - touchedElement = self.children[i]:getTouchedElement(x, y) - if touchedElement then - return touchedElement - end - end - else - return self - end - end -end - -return Menu \ No newline at end of file diff --git a/client/src/ui/MenuItem.lua b/client/src/ui/MenuItem.lua index b3fac8fd..4db3c0ab 100644 --- a/client/src/ui/MenuItem.lua +++ b/client/src/ui/MenuItem.lua @@ -94,7 +94,6 @@ function MenuItem.createLabeledButtonMenuItem(labelText, labelTextReplacements, if buttonTextTranslate == nil then buttonTextTranslate = true end - assert(text ~= nil) local id if labelTextTranslate == nil or labelTextTranslate then id = text diff --git a/client/src/ui/ScrollContainer.lua b/client/src/ui/ScrollContainer.lua index d975b92d..5afd6264 100644 --- a/client/src/ui/ScrollContainer.lua +++ b/client/src/ui/ScrollContainer.lua @@ -4,6 +4,8 @@ local class = require("common.lib.class") local util = require("common.lib.util") local GraphicsUtil = require("client.src.graphics.graphics_util") local logger = require("common.lib.logger") +local VerticalScrollLayout = require(PATH .. ".Layouts.VerticalScrollLayout") +local HorizontalScrollLayout = require(PATH .. ".Layouts.HorizontalScrollLayout") ---@class ScrollContainerOptions : UiElementOptions ---@field scrollOrientation ("vertical" | "horizontal" | nil) @@ -19,6 +21,11 @@ local ScrollContainer = class( ---@param self ScrollContainer function(self, options) self.scrollOrientation = "vertical" or options.scrollOrientation + if self.scrollOrientation == "vertical" then + self.layout = VerticalScrollLayout + else + self.layout = HorizontalScrollLayout + end self.scrollOffset = 0 self.maxScrollOffset = 0 end, diff --git a/client/src/ui/VerticalMenu.lua b/client/src/ui/VerticalMenu.lua index 0ddcba7e..2a38d9de 100644 --- a/client/src/ui/VerticalMenu.lua +++ b/client/src/ui/VerticalMenu.lua @@ -6,12 +6,14 @@ local GraphicsUtil = require("client.src.graphics.graphics_util") local Focusable = require(PATH .. ".Focusable") local FocusDirector = require(PATH .. ".FocusDirector") local input = require("client.src.inputManager") +local VerticalScrollLayout = require(PATH .. ".Layouts.VerticalScrollLayout") ---@class VerticalMenu : ScrollContainer, Focusable local VerticalMenu = class( function(self, options) self.selectedIndex = nil self.scrollOrientation = "vertical" + self.layout = VerticalScrollLayout end, ScrollContainer) diff --git a/client/src/ui/init.lua b/client/src/ui/init.lua index dcb0f878..5adbe7a1 100644 --- a/client/src/ui/init.lua +++ b/client/src/ui/init.lua @@ -26,7 +26,6 @@ local ui = { ---@see LevelSlider ---@type fun(options: SliderOptions): LevelSlider LevelSlider = require(PATH .. ".LevelSlider"), - Menu = require(PATH .. ".Menu"), MenuItem = require(PATH .. ".MenuItem"), MultiPlayerSelectionWrapper = require(PATH .. ".MultiPlayerSelectionWrapper"), PagedUniGrid = require(PATH .. ".PagedUniGrid"), From 2187846e070b4e81bfe5e09a77fbcaf73add12be Mon Sep 17 00:00:00 2001 From: Endaris Date: Sun, 11 May 2025 21:31:23 +0200 Subject: [PATCH 05/49] rework font handling to work with discrete key sizes --- client/src/ClientStack.lua | 17 ++--- client/src/Game.lua | 12 +-- client/src/PlayerStack.lua | 2 +- client/src/graphics/graphics_util.lua | 75 +++++++++++++++---- client/src/mods/Theme.lua | 2 +- client/src/scenes/ChallengeModeRecapScene.lua | 6 +- client/src/scenes/Game1pChallenge.lua | 10 +-- client/src/scenes/ModManagement.lua | 2 +- client/src/scenes/OptionsMenu.lua | 2 +- client/src/scenes/PortraitGame.lua | 2 +- client/src/scenes/ReplayGame.lua | 2 +- client/src/scenes/SetUserIdMenu.lua | 2 +- client/src/scenes/StartUp.lua | 2 +- client/src/scenes/TitleScreen.lua | 2 +- client/src/ui/Label.lua | 36 +++++---- client/src/ui/MenuItem.lua | 6 +- client/src/ui/ScrollContainer.lua | 17 ++--- main.lua | 2 +- 18 files changed, 120 insertions(+), 79 deletions(-) diff --git a/client/src/ClientStack.lua b/client/src/ClientStack.lua index 0683cdd1..0eee1290 100644 --- a/client/src/ClientStack.lua +++ b/client/src/ClientStack.lua @@ -200,7 +200,11 @@ function ClientStack:drawNumber(number, themePositionOffset, scale, cameFromLega GraphicsUtil.drawPixelFont(number, self.assets.numberPixelFont, x, y, scale, scale, "center", 0) end -function ClientStack:drawString(string, themePositionOffset, cameFromLegacyScoreOffset, fontSize) +---@param str string +---@param themePositionOffset number[] +---@param cameFromLegacyScoreOffset boolean +---@param fontSize FontSize +function ClientStack:drawString(str, themePositionOffset, cameFromLegacyScoreOffset, fontSize) if cameFromLegacyScoreOffset == nil then cameFromLegacyScoreOffset = false end @@ -217,12 +221,7 @@ function ClientStack:drawString(string, themePositionOffset, cameFromLegacyScore end end - if fontSize == nil then - fontSize = GraphicsUtil.fontSize - end - local fontDelta = fontSize - GraphicsUtil.fontSize - - GraphicsUtil.printf(string, x, y, limit, alignment, nil, nil, fontDelta) + GraphicsUtil.printf(str, x, y, limit, alignment, nil, nil, fontSize) end -- Positions the stack draw position for the given player @@ -396,14 +395,14 @@ function ClientStack:drawAbsoluteMultibar(stop_time, shake_time, pre_stop_time) end if remainingSeconds > 0 then - self:drawString(string.format("%." .. themes[config.theme].multibar_LeftoverTime_Decimals .. "f", remainingSeconds), overtimePos, false, 20) + self:drawString(string.format("%." .. themes[config.theme].multibar_LeftoverTime_Decimals .. "f", remainingSeconds), overtimePos, false, "big") end end end function ClientStack:drawPlayerName() local username = (self.player.name or "") - self:drawString(username, themes[config.theme].name_Pos, true, themes[config.theme].name_Font_Size) + self:drawString(username, themes[config.theme].name_Pos, true, "big") end function ClientStack:drawWinCount() diff --git a/client/src/Game.lua b/client/src/Game.lua index bd725929..6dc46a1c 100644 --- a/client/src/Game.lua +++ b/client/src/Game.lua @@ -429,7 +429,7 @@ function Game:drawScaleInfo() if consts.CANVAS_WIDTH * self.canvasXScale > newPixelWidth then scaleString = scaleString .. " Clipped " end - love.graphics.printf(scaleString, GraphicsUtil.getGlobalFontWithSize(30), 5, 5, 2000, "left") + love.graphics.printf(scaleString, GraphicsUtil.getGlobalFontWithSize("huge"), 5, 5, 2000, "left") end end @@ -643,7 +643,7 @@ function Game:drawLoadingString(loadingString) local y = consts.CANVAS_HEIGHT/2 - textHeight/2 local backgroundPadding = 10 GraphicsUtil.drawRectangle("fill", consts.CANVAS_WIDTH / 2 - (textMaxWidth / 2) , y - backgroundPadding, textMaxWidth, textHeight, 0, 0, 0, 0.5) - GraphicsUtil.printf(loadingString, x, y, consts.CANVAS_WIDTH, "center", nil, nil, 10) + GraphicsUtil.printf(loadingString, x, y, consts.CANVAS_WIDTH, "center", nil, nil, "big") end function Game:setLanguage(lang_code) @@ -656,13 +656,13 @@ function Game:setLanguage(lang_code) config.language_code = Localization.codes[Localization.lang_index] if themes[config.theme] and themes[config.theme].font and themes[config.theme].font.path then - GraphicsUtil.setGlobalFont(themes[config.theme].font.path, themes[config.theme].font.size, self:newCanvasSnappedScale()) + GraphicsUtil.setGlobalFont(themes[config.theme].font.path, (themes[config.theme].font.size or 12) - 12, self:newCanvasSnappedScale()) elseif config.language_code == "JP" then - GraphicsUtil.setGlobalFont("client/assets/fonts/jp.ttf", 14, self:newCanvasSnappedScale()) + GraphicsUtil.setGlobalFont("client/assets/fonts/jp.ttf", 2, self:newCanvasSnappedScale()) elseif config.language_code == "TH" then - GraphicsUtil.setGlobalFont("client/assets/fonts/th.otf", 14, self:newCanvasSnappedScale()) + GraphicsUtil.setGlobalFont("client/assets/fonts/th.otf", 2, self:newCanvasSnappedScale()) else - GraphicsUtil.setGlobalFont(nil, 12, self:newCanvasSnappedScale()) + GraphicsUtil.setGlobalFont(nil, 0, self:newCanvasSnappedScale()) end Localization:refresh_global_strings() diff --git a/client/src/PlayerStack.lua b/client/src/PlayerStack.lua index 7d45b0c1..a388116a 100644 --- a/client/src/PlayerStack.lua +++ b/client/src/PlayerStack.lua @@ -1116,7 +1116,7 @@ function PlayerStack:drawAnalyticData() local icon_width local icon_height - local font = GraphicsUtil.getGlobalFontWithSize(GraphicsUtil.fontSize + fontIncrement) + local font = GraphicsUtil.getGlobalFontWithSize("big") GraphicsUtil.setFont(font) -- Background GraphicsUtil.drawRectangle("fill", x - backgroundPadding , y - backgroundPadding, width, height, 0, 0, 0, 0.5) diff --git a/client/src/graphics/graphics_util.lua b/client/src/graphics/graphics_util.lua index 10e8ec65..78d948a6 100644 --- a/client/src/graphics/graphics_util.lua +++ b/client/src/graphics/graphics_util.lua @@ -1,13 +1,26 @@ local consts = require("common.engine.consts") local logger = require("common.lib.logger") local FileUtils = require("client.src.FileUtils") +local util = require("common.lib.util") + +---@enum (key) FontSize +local fontSizes = { + small = 8, + normal = 12, + medium = 16, + big = 20, + huge = 30, + gigantic = 42, +} -- Utility methods for drawing local GraphicsUtil = { fontFile = nil, - fontSize = 12, + fontSizeDelta = 0, fontDpiScale = 1, fontCache = {}, + ---@type table + textCache = util.getWeaklyKeyedTable(), quadPool = {} } @@ -233,24 +246,36 @@ local function privateMakeFont(fontPath, size, dpiScale) end -- Creates a new font based on the current font and a delta +---@param fontSize FontSize function GraphicsUtil.getGlobalFontWithSize(fontSize) - local f = GraphicsUtil.fontCache[fontSize] + local realSize = GraphicsUtil.getEffectiveFontSize(fontSize) + local f = GraphicsUtil.fontCache[realSize] if not f or f:getDPIScale() ~= GraphicsUtil.fontDpiScale then - f = privateMakeFont(GraphicsUtil.fontFile, fontSize, GraphicsUtil.fontDpiScale) - GraphicsUtil.fontCache[fontSize] = f + f = privateMakeFont(GraphicsUtil.fontFile, realSize, GraphicsUtil.fontDpiScale) + GraphicsUtil.fontCache[realSize] = f end return f end -function GraphicsUtil.setGlobalFont(filepath, size, dpiScale) +---@param fontSize FontSize +---@return integer +function GraphicsUtil.getEffectiveFontSize(fontSize) + return fontSizes[fontSize] + GraphicsUtil.fontSizeDelta +end + +---@param filepath string +---@param fontSizeDelta integer? +---@param dpiScale number? +function GraphicsUtil.setGlobalFont(filepath, fontSizeDelta, dpiScale) GraphicsUtil.setFontDpiScale(dpiScale) GraphicsUtil.fontCache = {} GraphicsUtil.fontFile = filepath - GraphicsUtil.fontSize = size - local createdFont = GraphicsUtil.getGlobalFontWithSize(size) + GraphicsUtil.fontSizeDelta = fontSizeDelta or 0 + local createdFont = GraphicsUtil.getGlobalFontWithSize("normal") love.graphics.setFont(createdFont) end +---@param dpiScale number? function GraphicsUtil.setFontDpiScale(dpiScale) if dpiScale and tonumber(dpiScale) then GraphicsUtil.fontDpiScale = dpiScale @@ -259,7 +284,21 @@ end -- Returns the current global font function GraphicsUtil.getGlobalFont() - return GraphicsUtil.getGlobalFontWithSize(GraphicsUtil.fontSize) + return GraphicsUtil.getGlobalFontWithSize("normal") +end + +---@param fontSize FontSize +---@param text string +---@param limit number +---@param hAlign ("left" | "center" | "right") +function GraphicsUtil.getTextHeightForWidth(fontSize, text, limit, hAlign) + local font = GraphicsUtil.getGlobalFontWithSize(fontSize) + if not GraphicsUtil.textCache[font] then + GraphicsUtil.textCache[font] = GraphicsUtil.newText(font) + end + local t = GraphicsUtil.textCache[font] + t:setf(text, limit, hAlign) + return t:getHeight() end function GraphicsUtil.setFont(font) @@ -289,17 +328,26 @@ function GraphicsUtil.print(str, x, y, color, scale) end -- Draws a font with a given font delta from the standard font -function GraphicsUtil.printf(str, x, y, limit, halign, color, scale, font_delta_size) +---@param str string +---@param x number +---@param y number +---@param limit number? +---@param halign string? +---@param color number[]? +---@param scale number? +---@param fontSize FontSize +function GraphicsUtil.printf(str, x, y, limit, halign, color, scale, fontSize) x = x or 0 y = y or 0 scale = scale or 1 color = color or nil limit = limit or consts.CANVAS_WIDTH - font_delta_size = font_delta_size or 0 + fontSize = fontSize or "normal" halign = halign or "left" GraphicsUtil.setColor(0, 0, 0, 1) - if font_delta_size ~= 0 then - GraphicsUtil.setFont(GraphicsUtil.getGlobalFontWithSize(GraphicsUtil.fontSize + font_delta_size)) + local font = GraphicsUtil.getGlobalFontWithSize(fontSize) + if font ~= love.graphics.getFont() then + GraphicsUtil.setFont(font) end love.graphics.printf(str, x+1, y+1, limit, halign, 0, scale) @@ -310,9 +358,6 @@ function GraphicsUtil.printf(str, x, y, limit, halign, color, scale, font_delta_ GraphicsUtil.setColor(r,g,b,a) love.graphics.printf(str, x, y, limit, halign, 0, scale) - if font_delta_size ~= 0 then - GraphicsUtil.setFont(GraphicsUtil.getGlobalFont()) - end GraphicsUtil.setColor(1,1,1,1) end diff --git a/client/src/mods/Theme.lua b/client/src/mods/Theme.lua index 2147f536..2dda2a22 100644 --- a/client/src/mods/Theme.lua +++ b/client/src/mods/Theme.lua @@ -873,7 +873,7 @@ function theme_init() themes[config.theme]:load() if themes[config.theme].font.path then - GraphicsUtil.setGlobalFont(themes[config.theme].font.path, themes[config.theme].font.size) + GraphicsUtil.setGlobalFont(themes[config.theme].font.path, (themes[config.theme].font.size or 12) - 12) end end diff --git a/client/src/scenes/ChallengeModeRecapScene.lua b/client/src/scenes/ChallengeModeRecapScene.lua index 84420fe7..e7bc8e3d 100644 --- a/client/src/scenes/ChallengeModeRecapScene.lua +++ b/client/src/scenes/ChallengeModeRecapScene.lua @@ -54,14 +54,14 @@ function ChallengeModeRecapScene:draw() local limit = consts.CANVAS_WIDTH local message = "Congratulations!\n You beat " .. self.challengeMode.difficultyName .. "!" - GraphicsUtil.printf(message, 0, drawY, limit, "center", nil, nil, 30) + GraphicsUtil.printf(message, 0, drawY, limit, "center", nil, nil, "gigantic") self.uiRoot:draw() local limit = 400 drawY = drawY + 120 - GraphicsUtil.printf("Continues", drawX - limit / 2, drawY, limit, "center", nil, nil, 4) + GraphicsUtil.printf("Continues", drawX - limit / 2, drawY, limit, "center", nil, nil, "medium") drawY = drawY + 20 - GraphicsUtil.printf(self.challengeMode.continues, drawX - limit / 2, drawY, limit, "center", nil, nil, 4) + GraphicsUtil.printf(self.challengeMode.continues, drawX - limit / 2, drawY, limit, "center", nil, nil, "medium") local font = GraphicsUtil.getGlobalFont() GraphicsUtil.print(loc("continue_button"), (consts.CANVAS_WIDTH - font:getWidth(loc("continue_button"))) / 2, consts.CANVAS_HEIGHT - 60) diff --git a/client/src/scenes/Game1pChallenge.lua b/client/src/scenes/Game1pChallenge.lua index 39fb0656..cb2cfdf9 100644 --- a/client/src/scenes/Game1pChallenge.lua +++ b/client/src/scenes/Game1pChallenge.lua @@ -78,21 +78,21 @@ end function Game1pChallenge:drawDifficultyName(drawX, drawY) local limit = 400 - GraphicsUtil.printf(loc("difficulty"), drawX - limit / 2, drawY, limit, "center", nil, nil, 10) - GraphicsUtil.printf(GAME.battleRoom.difficultyName, drawX - limit / 2, drawY + 26, limit, "center", nil, nil, 10) + GraphicsUtil.printf(loc("difficulty"), drawX - limit / 2, drawY, limit, "center", nil, nil, "big") + GraphicsUtil.printf(GAME.battleRoom.difficultyName, drawX - limit / 2, drawY + 26, limit, "center", nil, nil, "big") end function Game1pChallenge:drawStageInfo(drawX, drawY) local limit = 400 - GraphicsUtil.printf("Stage", drawX - limit / 2, drawY, limit, "center", nil, nil, 10) + GraphicsUtil.printf("Stage", drawX - limit / 2, drawY, limit, "center", nil, nil, "big") GraphicsUtil.drawPixelFont(self.stageIndex, themes[config.theme].fontMaps.numbers[2], drawX, drawY + 26, themes[config.theme].win_Scale, themes[config.theme].win_Scale, "center", 0) end function Game1pChallenge:drawContinueInfo(drawX, drawY) local limit = 400 - GraphicsUtil.printf("Continues", drawX - limit / 2, drawY, limit, "center", nil, nil, 4) - GraphicsUtil.printf(GAME.battleRoom.continues, drawX - limit / 2, drawY + 20, limit, "center", nil, nil, 4) + GraphicsUtil.printf("Continues", drawX - limit / 2, drawY, limit, "center", nil, nil, "medium") + GraphicsUtil.printf(GAME.battleRoom.continues, drawX - limit / 2, drawY + 20, limit, "center", nil, nil, "medium") end return Game1pChallenge diff --git a/client/src/scenes/ModManagement.lua b/client/src/scenes/ModManagement.lua index 664ab20d..73af7a75 100644 --- a/client/src/scenes/ModManagement.lua +++ b/client/src/scenes/ModManagement.lua @@ -32,7 +32,7 @@ function ModManagement:load() self.headerLabel = ui.Label({ text = "placeholder", hAlign = "center", - fontSize = 16, + fontSize = "medium", }) self.headLine = self:loadGridHeader() diff --git a/client/src/scenes/OptionsMenu.lua b/client/src/scenes/OptionsMenu.lua index 35df80f3..dad7a77f 100644 --- a/client/src/scenes/OptionsMenu.lua +++ b/client/src/scenes/OptionsMenu.lua @@ -367,7 +367,7 @@ function OptionsMenu:loadGraphicsMenu() config.theme = value GAME.theme = themes[value] SoundController:stopMusic() - GraphicsUtil.setGlobalFont(themes[config.theme].font.path, themes[config.theme].font.size) + GraphicsUtil.setGlobalFont(themes[config.theme].font.path, (themes[config.theme].font.size or 12) - 12) self:updateMenuLanguage() self.backgroundImage = themes[config.theme].images.bg_main self:applyMusic() diff --git a/client/src/scenes/PortraitGame.lua b/client/src/scenes/PortraitGame.lua index 7baf046f..43a625b9 100644 --- a/client/src/scenes/PortraitGame.lua +++ b/client/src/scenes/PortraitGame.lua @@ -214,7 +214,7 @@ function PortraitGame:flipToPortrait() stack.origin_x = stack.frameOriginX / stack.gfxScale -- create a raise button that interacts with the touch controller - local raiseButton = ui.TextButton({label = ui.Label({id = "raise", fontSize = 20}), hAlign = "right", vAlign = "bottom", height = player.stack:canvasHeight() / 2}) + local raiseButton = ui.TextButton({label = ui.Label({id = "raise", fontSize = "big"}), hAlign = "right", vAlign = "bottom", height = player.stack:canvasHeight() / 2}) raiseButton.onTouch = function(button, x, y) button.backgroundColor[4] = 1 stack.touchInputDetector.touchingRaise = true diff --git a/client/src/scenes/ReplayGame.lua b/client/src/scenes/ReplayGame.lua index 0c0f3c75..9bfb5b34 100644 --- a/client/src/scenes/ReplayGame.lua +++ b/client/src/scenes/ReplayGame.lua @@ -114,7 +114,7 @@ ReplayGame.runGameOver = ReplayGame.runGame function ReplayGame:customDraw() local textPos = themes[config.theme].gameover_text_Pos local playbackText = self.playbackSpeeds[self.playbackSpeedIndex] .. "x" - GraphicsUtil.printf(playbackText, textPos[0], textPos[1], consts.CANVAS_WIDTH, "center", nil, 1, 10) + GraphicsUtil.printf(playbackText, textPos[0], textPos[1], consts.CANVAS_WIDTH, "center", nil, 1, "big") end function ReplayGame:drawHUD() diff --git a/client/src/scenes/SetUserIdMenu.lua b/client/src/scenes/SetUserIdMenu.lua index 46b8b122..e9174121 100644 --- a/client/src/scenes/SetUserIdMenu.lua +++ b/client/src/scenes/SetUserIdMenu.lua @@ -48,7 +48,7 @@ function SetUserIdMenu:load(sceneParams) hAlign = "center", vAlign = "bottom", y = -50, - fontSize = 20, + fontSize = "big", }) self.idInputField:setFocus(0, 0) diff --git a/client/src/scenes/StartUp.lua b/client/src/scenes/StartUp.lua index dc764174..8d04ce7a 100644 --- a/client/src/scenes/StartUp.lua +++ b/client/src/scenes/StartUp.lua @@ -21,7 +21,7 @@ local StartUp = class(function(scene, sceneParams) logger.debug(scene.migrationMessage) end - love.graphics.setFont(GraphicsUtil.getGlobalFontWithSize(GraphicsUtil.fontSize + 10)) + love.graphics.setFont(GraphicsUtil.getGlobalFontWithSize("big")) end, Scene) StartUp.name = "StartUp" diff --git a/client/src/scenes/TitleScreen.lua b/client/src/scenes/TitleScreen.lua index 05fc9897..fdb4e4da 100644 --- a/client/src/scenes/TitleScreen.lua +++ b/client/src/scenes/TitleScreen.lua @@ -22,7 +22,7 @@ local function titleDrawPressStart(percent) local textHeight = 40 local x = (consts.CANVAS_WIDTH / 2) - (textMaxWidth / 2) local y = consts.CANVAS_HEIGHT * 0.75 - GraphicsUtil.printf(loc("continue_button"), x, y, textMaxWidth, "center", {1,1,1,percent}, nil, 16) + GraphicsUtil.printf(loc("continue_button"), x, y, textMaxWidth, "center", {1,1,1,percent}, nil, "huge") end function TitleScreen:update(dt) diff --git a/client/src/ui/Label.lua b/client/src/ui/Label.lua index 31a0ffff..f97598d5 100644 --- a/client/src/ui/Label.lua +++ b/client/src/ui/Label.lua @@ -16,7 +16,7 @@ local GraphicsUtil = require("client.src.graphics.graphics_util") ---@field replacementTable string[]? Additional strings to perform string format on a localized key with parts marked for replacement ---@field text string The raw text or localization key ---@field font love.Font Cached font for recreating the love.Text on changes ----@field fontSize integer The size of the font +---@field fontSize FontSize The size of the font ---@field wrap boolean If the font should wrap around ---@overload fun(options: LabelOptions): Label local Label = class( @@ -33,10 +33,10 @@ local Label = class( self.hAlign = options.hAlign or "left" self.vAlign = options.vAlign or "top" - self.fontSize = options.fontSize or GraphicsUtil.fontSize - self.font = options.font or GraphicsUtil.getGlobalFontWithSize(self.fontSize) + self.fontSize = options.fontSize or "normal" + local font = GraphicsUtil.getGlobalFontWithSize(self.fontSize) - local totalWidth = self.font:getWidth(self.text) + local totalWidth = font:getWidth(self.text) if options.wrap ~= nil then self.wrap = options.wrap else @@ -44,9 +44,9 @@ local Label = class( end local words = self.text:split() - local maxWordWidth = self.font:getWidth(words[1]) + local maxWordWidth = font:getWidth(words[1]) for i = 2, #words do - maxWordWidth = math.max(maxWordWidth, self.font:getWidth(words[i])) + maxWordWidth = math.max(maxWordWidth, font:getWidth(words[i])) end if options.minWidth ~= nil then @@ -61,16 +61,16 @@ local Label = class( self.width = options.width or totalWidth self.maxWidth = options.maxWidth or math.huge - self.minHeight = options.minHeight or self.font:getHeight() - self.height = options.height or self.font:getHeight() + self.minHeight = options.minHeight or font:getHeight() + self.height = options.height or font:getHeight() if options.maxHeight ~= nil then self.maxHeight = options.maxHeight else if self.wrap then - self.maxHeight = (#words * self.font:getHeight()) + self.maxHeight = (#words * font:getHeight()) else - self.maxHeight = self.font:getHeight() + self.maxHeight = font:getHeight() end end @@ -81,10 +81,11 @@ local Label = class( ) Label.TYPE = "Label" +---@param fontSize FontSize function Label:setFontSize(fontSize) self.fontSize = fontSize - self.font = GraphicsUtil.getGlobalFontWithSize(self.fontSize) - local totalWidth = self.font:getWidth(self.text) + local font = GraphicsUtil.getGlobalFontWithSize(self.fontSize) + local totalWidth = font:getWidth(self.text) self.width = totalWidth self.maxWidth = math.huge if not self.wrap then @@ -96,8 +97,8 @@ function Label:refreshLocalization() if self.id then self.text = loc(self.id, unpack(self.replacementTable)) end - self.font = GraphicsUtil.getGlobalFontWithSize(self.fontSize) - local totalWidth = self.font:getWidth(self.text) + local font = GraphicsUtil.getGlobalFontWithSize(self.fontSize) + local totalWidth = font:getWidth(self.text) self.width = totalWidth self.maxWidth = totalWidth if not self.wrap then @@ -106,14 +107,11 @@ function Label:refreshLocalization() end function Label:drawSelf() - love.graphics.setFont(self.font) - love.graphics.printf(self.text, self.x, self.y, self.width, self.hAlign) + GraphicsUtil.printf(self.text, self.x, self.y, self.width, self.hAlign, nil, nil, self.fontSize) end function Label:setMinHeightForWidth() - local refText = GraphicsUtil.newText(self.font) - refText:setf(self.text, self.width, self.hAlign) - self.minHeight = refText:getHeight() + self.minHeight = GraphicsUtil.getTextHeightForWidth(self.fontSize, self.text, self.width, self.hAlign) end return Label \ No newline at end of file diff --git a/client/src/ui/MenuItem.lua b/client/src/ui/MenuItem.lua index 4db3c0ab..64f4ac46 100644 --- a/client/src/ui/MenuItem.lua +++ b/client/src/ui/MenuItem.lua @@ -27,7 +27,7 @@ function MenuItem.createMenuItem(label, item) menuItem.width = label.width + (2 * MenuItem.PADDING) if system.isMobileOS() or DEBUG_ENABLED then - label:setFontSize(18) + label:setFontSize("big") end label.vAlign = "center" @@ -56,9 +56,9 @@ function MenuItem.createButtonMenuItem(text, replacements, translate, onClick) id = text text = nil end - local fontSize = 12 + local fontSize = "normal" if system.isMobileOS() or DEBUG_ENABLED then - fontSize = 18 + fontSize = "big" end local label = Label({ id = id, diff --git a/client/src/ui/ScrollContainer.lua b/client/src/ui/ScrollContainer.lua index 5afd6264..5c93a0f5 100644 --- a/client/src/ui/ScrollContainer.lua +++ b/client/src/ui/ScrollContainer.lua @@ -158,22 +158,21 @@ function ScrollContainer:draw() love.graphics.setStencilTest() end - if self.scrollOrientation == "vertical" then - if self.maxScrollOffset > 0 then + if self.maxScrollOffset > 0 then + local fontSize = GraphicsUtil.getEffectiveFontSize("normal") + if self.scrollOrientation == "vertical" then if self.scrollOffset < 0 then - GraphicsUtil.print("^", self.x + self.width / 2 - GraphicsUtil.fontSize / 2, self.y - 20) + GraphicsUtil.print("^", self.x + self.width / 2 - fontSize / 2, self.y - 20) end if math.abs(self.scrollOffset) < self.maxScrollOffset then - GraphicsUtil.print("v", self.x + self.width / 2 - GraphicsUtil.fontSize / 2, self.y + self.height + 8) + GraphicsUtil.print("v", self.x + self.width / 2 - fontSize / 2, self.y + self.height + 8) end - end - else - if self.maxScrollOffset > 0 then + else if self.scrollOffset < 0 then - GraphicsUtil.print("<", self.x - 20, self.y + self.height / 2 - GraphicsUtil.fontSize / 2) + GraphicsUtil.print("<", self.x - 20, self.y + self.height / 2 - fontSize / 2) end if math.abs(self.scrollOffset) < self.maxScrollOffset then - GraphicsUtil.print(">", self.x + self.width + 8, self.y + self.height / 2 - GraphicsUtil.fontSize / 2) + GraphicsUtil.print(">", self.x + self.width + 8, self.y + self.height / 2 - fontSize / 2) end end end diff --git a/main.lua b/main.lua index ac86dbb5..c033430d 100644 --- a/main.lua +++ b/main.lua @@ -265,7 +265,7 @@ function love.errorhandler(msg) end love.graphics.reset() - local s, font = pcall(GraphicsUtil.getGlobalFontWithSize, GraphicsUtil.fontSize + 4) + local s, font = pcall(GraphicsUtil.getGlobalFontWithSize, "medium") if s then love.graphics.setFont(font) else From 8f7ea945d1ce92f1811739f62eabf1781566efcf Mon Sep 17 00:00:00 2001 From: Endaris Date: Sun, 11 May 2025 23:08:26 +0200 Subject: [PATCH 06/49] add adaptive layout and fix some more font stuff --- client/src/Game.lua | 13 ++- client/src/graphics/graphics_util.lua | 2 +- client/src/scenes/Scene.lua | 9 +- client/src/ui/Label.lua | 5 + client/src/ui/Layouts/AdaptiveFlexLayout.lua | 30 +++++ client/src/ui/MenuItem.lua | 13 +-- client/src/ui/Slider.lua | 6 +- client/src/ui/Stepper.lua | 113 ++++++++----------- client/src/ui/init.lua | 1 + 9 files changed, 109 insertions(+), 83 deletions(-) create mode 100644 client/src/ui/Layouts/AdaptiveFlexLayout.lua diff --git a/client/src/Game.lua b/client/src/Game.lua index 6dc46a1c..65c15343 100644 --- a/client/src/Game.lua +++ b/client/src/Game.lua @@ -655,14 +655,19 @@ function Game:setLanguage(lang_code) end config.language_code = Localization.codes[Localization.lang_index] + local baseOffset = 0 + if system.isMobileOS() or DEBUG_ENABLED then + baseOffset = 4 + end + if themes[config.theme] and themes[config.theme].font and themes[config.theme].font.path then - GraphicsUtil.setGlobalFont(themes[config.theme].font.path, (themes[config.theme].font.size or 12) - 12, self:newCanvasSnappedScale()) + GraphicsUtil.setGlobalFont(themes[config.theme].font.path, (themes[config.theme].font.size or 12) - 12 + baseOffset, self:newCanvasSnappedScale()) elseif config.language_code == "JP" then - GraphicsUtil.setGlobalFont("client/assets/fonts/jp.ttf", 2, self:newCanvasSnappedScale()) + GraphicsUtil.setGlobalFont("client/assets/fonts/jp.ttf", 2 + baseOffset, self:newCanvasSnappedScale()) elseif config.language_code == "TH" then - GraphicsUtil.setGlobalFont("client/assets/fonts/th.otf", 2, self:newCanvasSnappedScale()) + GraphicsUtil.setGlobalFont("client/assets/fonts/th.otf", 2 + baseOffset, self:newCanvasSnappedScale()) else - GraphicsUtil.setGlobalFont(nil, 0, self:newCanvasSnappedScale()) + GraphicsUtil.setGlobalFont(nil, baseOffset, self:newCanvasSnappedScale()) end Localization:refresh_global_strings() diff --git a/client/src/graphics/graphics_util.lua b/client/src/graphics/graphics_util.lua index 78d948a6..5d3e2d5f 100644 --- a/client/src/graphics/graphics_util.lua +++ b/client/src/graphics/graphics_util.lua @@ -335,7 +335,7 @@ end ---@param halign string? ---@param color number[]? ---@param scale number? ----@param fontSize FontSize +---@param fontSize FontSize? function GraphicsUtil.printf(str, x, y, limit, halign, color, scale, fontSize) x = x or 0 y = y or 0 diff --git a/client/src/scenes/Scene.lua b/client/src/scenes/Scene.lua index 6bb16eec..10a2ff16 100644 --- a/client/src/scenes/Scene.lua +++ b/client/src/scenes/Scene.lua @@ -19,7 +19,14 @@ local SoundController = require("client.src.music.SoundController") local Scene = class( ---@param self Scene function (self, sceneParams) - self.uiRoot = ui.UiElement({x = 0, y = 0, width = consts.CANVAS_WIDTH, height = consts.CANVAS_HEIGHT, padding = 16}) + self.uiRoot = ui.UiElement({ + x = 0, + y = 0, + width = consts.CANVAS_WIDTH, + height = consts.CANVAS_HEIGHT, + padding = 16, + layout = ui.Layouts.AdaptiveFlexLayout + }) self.uiRoot.controlsWindow = true -- scenes may specify theme music to use that is played once they are switched to -- eligible labels: diff --git a/client/src/ui/Label.lua b/client/src/ui/Label.lua index f97598d5..5155fdb6 100644 --- a/client/src/ui/Label.lua +++ b/client/src/ui/Label.lua @@ -110,6 +110,11 @@ function Label:drawSelf() GraphicsUtil.printf(self.text, self.x, self.y, self.width, self.hAlign, nil, nil, self.fontSize) end +function Label:getPreferredWidth() + local font = GraphicsUtil.getGlobalFontWithSize(self.fontSize) + return font:getWidth(self.text) +end + function Label:setMinHeightForWidth() self.minHeight = GraphicsUtil.getTextHeightForWidth(self.fontSize, self.text, self.width, self.hAlign) end diff --git a/client/src/ui/Layouts/AdaptiveFlexLayout.lua b/client/src/ui/Layouts/AdaptiveFlexLayout.lua new file mode 100644 index 00000000..f6034137 --- /dev/null +++ b/client/src/ui/Layouts/AdaptiveFlexLayout.lua @@ -0,0 +1,30 @@ +local PATH = (...):gsub('%.[^%.]+$', '') +local VerticalFlexLayout = require(PATH ..".VerticalFlexLayout") +local HorizontalFlexLayout = require(PATH ..".HorizontalFlexLayout") + +---@class AdaptiveFlexLayout : FlexLayout +local AdaptiveFlexLayout = setmetatable({}, {__index = HorizontalFlexLayout}) + +function AdaptiveFlexLayout.resize(uiElement, width, height) + local mt = getmetatable(AdaptiveFlexLayout) + if width < height and mt.__index ~= VerticalFlexLayout then + mt.__index = VerticalFlexLayout + elseif height < width and mt.__index ~= HorizontalFlexLayout then + mt.__index = HorizontalFlexLayout + end + + uiElement.layout.updateWidths(uiElement, width) + + -- transform width to height for width-to-height supporting uiElements based on the width pass + uiElement:setMinHeightForWidth() + + uiElement.layout.updateHeights(uiElement, height) + + uiElement.layout.positionChildren(uiElement) + + if uiElement.onResize then + uiElement:onResize() + end +end + +return AdaptiveFlexLayout \ No newline at end of file diff --git a/client/src/ui/MenuItem.lua b/client/src/ui/MenuItem.lua index 64f4ac46..48ee046a 100644 --- a/client/src/ui/MenuItem.lua +++ b/client/src/ui/MenuItem.lua @@ -5,7 +5,6 @@ local Button = require(PATH .. ".Button") local TextButton = require(PATH .. ".TextButton") local class = require("common.lib.class") local GraphicsUtil = require("client.src.graphics.graphics_util") -local system = require("client.src.system") local HorizontalFlexLayout = require(PATH .. ".Layouts.HorizontalFlexLayout") -- MenuItem is a specific UIElement that all children of Menu should be @@ -26,10 +25,6 @@ function MenuItem.createMenuItem(label, item) menuItem.width = label.width + (2 * MenuItem.PADDING) - if system.isMobileOS() or DEBUG_ENABLED then - label:setFontSize("big") - end - label.vAlign = "center" label.hAlign = "right" label.hFill = true @@ -56,18 +51,14 @@ function MenuItem.createButtonMenuItem(text, replacements, translate, onClick) id = text text = nil end - local fontSize = "normal" - if system.isMobileOS() or DEBUG_ENABLED then - fontSize = "big" - end + local label = Label({ id = id, text = text, replacements = replacements, hAlign = "center", vAlign = "center", - wrap = false, - fontSize = fontSize + wrap = false }) local button = Button({ width = 140, diff --git a/client/src/ui/Slider.lua b/client/src/ui/Slider.lua index 642eae45..b2afd7b6 100644 --- a/client/src/ui/Slider.lua +++ b/client/src/ui/Slider.lua @@ -55,9 +55,9 @@ local Slider = class( self.hFill = true end - self.minText = GraphicsUtil.newText(love.graphics.getFont(), tostring(self.min)) - self.maxText = GraphicsUtil.newText(love.graphics.getFont(), tostring(self.max)) - self.valueText = GraphicsUtil.newText(love.graphics.getFont(), tostring(self.value)) + self.minText = GraphicsUtil.newText(GraphicsUtil.getGlobalFontWithSize("small"), tostring(self.min)) + self.maxText = GraphicsUtil.newText(GraphicsUtil.getGlobalFontWithSize("small"), tostring(self.max)) + self.valueText = GraphicsUtil.newText(GraphicsUtil.getGlobalFontWithSize("small"), tostring(self.value)) local valueTextWidth, valueTextHeight = self.valueText:getDimensions() local textWidth, textHeight = self.maxText:getDimensions() diff --git a/client/src/ui/Stepper.lua b/client/src/ui/Stepper.lua index 3b72204c..fbdaad37 100644 --- a/client/src/ui/Stepper.lua +++ b/client/src/ui/Stepper.lua @@ -8,51 +8,6 @@ local GraphicsUtil = require("client.src.graphics.graphics_util") local HorizontalFlexLayout = require(PATH .. ".Layouts.HorizontalFlexLayout") local NAV_BUTTON_WIDTH = 25 -local EMPTY_STEPPER_WIDTH = 160 - -local function setLabels(self, labels, values, selectedIndex) - self.selectedIndex = selectedIndex - self.values = values - self.labels = labels - - self:removeLabelChildren() - - if (#labels == 0) then - self.rightButton:setVisibility(false); - self.leftButton:setVisibility(false); - self.width = EMPTY_STEPPER_WIDTH - return - end - - for _, label in ipairs(labels) do - label.hAlign = "center" - label.vAlign = "center" - self.width = math.max(label.width + 10 + NAV_BUTTON_WIDTH * 2, self.width) - self.height = math.max(label.height + 4, self.height) - - self:addChild(label) - label:setVisibility(false) - end - - self.labels[self.selectedIndex]:setVisibility(true) - self.value = self.values[self.selectedIndex] - self.rightButton.x = self.width - NAV_BUTTON_WIDTH - self.rightButton:setVisibility(true); - self.leftButton:setVisibility(true); -end - -local function setState(self, i) - local new_index = util.bound(1, i, #self.labels) - if i ~= new_index then - return - end - - self.labels[self.selectedIndex]:setVisibility(false) - self.selectedIndex = new_index - self.value = self.values[new_index] - self.labels[new_index]:setVisibility(true) - self.onChange(self.value) -end -- UIElement representing a scrolling list of options local Stepper = class( @@ -66,36 +21,75 @@ local Stepper = class( width = navButtonWidth, vAlign = "center", hAlign = "center", - label = Label({text = "<", translate = false}), + label = Label({text = "<"}), onClick = function(selfElement, inputSource, holdTime) - setState(self, self.selectedIndex - 1) + self:setState(self.selectedIndex - 1) end }) + self.labelContainer = UIElement({ + vAlign = "center", + hAlign = "center", + }) self.rightButton = TextButton({ width = navButtonWidth, vAlign = "center", hAlign = "center", - label = Label({text = ">", translate = false}), + label = Label({text = ">"}), onClick = function(selfElement, inputSource, holdTime) - setState(self, self.selectedIndex + 1) + self:setState(self.selectedIndex + 1) end }) self:addChild(self.leftButton) - - self.color = {.5, .5, 1, .7} - self.borderColor = {.7, .7, 1, .7} - - setLabels(self, options.labels, options.values, self.selectedIndex) + self:addChild(self.labelContainer) self:addChild(self.rightButton) + self:setLabels(options.labels, options.values, self.selectedIndex) + self.color = {.5, .5, 1, .7} + self.borderColor = {.7, .7, 1, .7} end, UIElement ) Stepper.TYPE = "Stepper" Stepper.layout = HorizontalFlexLayout -Stepper.setLabels = setLabels -Stepper.setState = setState +function Stepper:setLabels(labels, values, selectedIndex) + self.selectedIndex = selectedIndex + self.values = values + self.labels = labels + + for i = #self.labelContainer.children, 1, -1 do + self.labelContainer.children[i]:detach() + end + + local minWidth = 0 + for _, label in ipairs(labels) do + label.hAlign = "center" + label.vAlign = "center" + label:setVisibility(false) + minWidth = math.max(minWidth, label:getPreferredWidth()) + + self.labelContainer:addChild(label) + end + self.labelContainer.minWidth = minWidth + self.labelContainer.width = minWidth + self.labelContainer.maxWidth = minWidth + + self.labels[self.selectedIndex]:setVisibility(true) + self.value = self.values[self.selectedIndex] +end + +function Stepper.setState(self, i) + local new_index = util.bound(1, i, #self.labels) + if i ~= new_index then + return + end + + self.labels[self.selectedIndex]:setVisibility(false) + self.selectedIndex = new_index + self.value = self.values[new_index] + self.labels[new_index]:setVisibility(true) + self.onChange(self.value) +end function Stepper:receiveInputs(input) if input:isPressedWithRepeat("Left") then @@ -124,11 +118,4 @@ function Stepper:drawSelf() end end --- Remove all attached labels, preserving the navigation buttons -function Stepper:removeLabelChildren() - for i = #self.children - 1, 2, -1 do - self.children[i]:detach() - end -end - return Stepper \ No newline at end of file diff --git a/client/src/ui/init.lua b/client/src/ui/init.lua index 5adbe7a1..d13dc450 100644 --- a/client/src/ui/init.lua +++ b/client/src/ui/init.lua @@ -19,6 +19,7 @@ local ui = { ---@type fun(options: LabelOptions): Label Label = require(PATH .. ".Label"), Layouts = { + AdaptiveFlexLayout = require(PATH .. ".Layouts.AdaptiveFlexLayout"), HorizontalFlexLayout = require(PATH .. ".Layouts.HorizontalFlexLayout"), VerticalFlexLayout = require(PATH .. ".Layouts.VerticalFlexLayout"), }, From 13efcce3bfab5b8fb90e4fab8010c53be4186c4e Mon Sep 17 00:00:00 2001 From: Endaris Date: Sun, 11 May 2025 23:21:51 +0200 Subject: [PATCH 07/49] fix a crash, fix graphics menu adjustment, fix slider alignment --- client/src/scenes/OptionsMenu.lua | 26 ++++++++++---------- client/src/ui/Layouts/AdaptiveFlexLayout.lua | 10 +++++--- client/src/ui/Slider.lua | 5 ++-- client/src/ui/UIElement.lua | 10 ++++++-- 4 files changed, 30 insertions(+), 21 deletions(-) diff --git a/client/src/scenes/OptionsMenu.lua b/client/src/scenes/OptionsMenu.lua index dad7a77f..c81827d2 100644 --- a/client/src/scenes/OptionsMenu.lua +++ b/client/src/scenes/OptionsMenu.lua @@ -349,6 +349,15 @@ function OptionsMenu:loadGeneralMenu() end function OptionsMenu:loadGraphicsMenu() + local menu = ui.VerticalMenu({ + hAlign = "center", + minHeight = 480, + maxHeight = 540, + childGap = 8, + padding = 32, + width = 600, + }) + local themeIndex local themeLabels = {} for i, v in ipairs(themeIds) do @@ -405,10 +414,10 @@ function OptionsMenu:loadGraphicsMenu() getFixedScaleSlider()) local function updateFixedButtonGroupVisibility() if config.gameScaleType ~= "fixed" then - self.menus.graphicsMenu:removeMenuItem(fixedScaleSlider.id) + fixedScaleSlider:detach() else - if self.menus.graphicsMenu:containsMenuItemID(fixedScaleSlider.id) == false then - self.menus.graphicsMenu:addMenuItem(3, fixedScaleSlider) + if not fixedScaleSlider.parent then + menu:addChild(fixedScaleSlider, 3) end end end @@ -465,20 +474,11 @@ function OptionsMenu:loadGraphicsMenu() self:switchToScreen("baseMenu") end) - local menu = ui.VerticalMenu({ - hAlign = "center", - minHeight = 480, - maxHeight = 540, - childGap = 8, - padding = 32, - width = 600, - }) - menu:addChild(themeSelection) menu:addChild(scaleType) if config.gameScaleType == "fixed" then - menu:addMenuItem(3, fixedScaleSlider) + menu:addChild(fixedScaleSlider, 3) end menu:addChild(portraitDarkness) diff --git a/client/src/ui/Layouts/AdaptiveFlexLayout.lua b/client/src/ui/Layouts/AdaptiveFlexLayout.lua index f6034137..07354d79 100644 --- a/client/src/ui/Layouts/AdaptiveFlexLayout.lua +++ b/client/src/ui/Layouts/AdaptiveFlexLayout.lua @@ -7,10 +7,12 @@ local AdaptiveFlexLayout = setmetatable({}, {__index = HorizontalFlexLayout}) function AdaptiveFlexLayout.resize(uiElement, width, height) local mt = getmetatable(AdaptiveFlexLayout) - if width < height and mt.__index ~= VerticalFlexLayout then - mt.__index = VerticalFlexLayout - elseif height < width and mt.__index ~= HorizontalFlexLayout then - mt.__index = HorizontalFlexLayout + if width and height then + if width < height and mt.__index ~= VerticalFlexLayout then + mt.__index = VerticalFlexLayout + elseif height < width and mt.__index ~= HorizontalFlexLayout then + mt.__index = HorizontalFlexLayout + end end uiElement.layout.updateWidths(uiElement, width) diff --git a/client/src/ui/Slider.lua b/client/src/ui/Slider.lua index b2afd7b6..8126b1ba 100644 --- a/client/src/ui/Slider.lua +++ b/client/src/ui/Slider.lua @@ -6,7 +6,7 @@ local GraphicsUtil = require("client.src.graphics.graphics_util") local handleRadius = 7.5 local xPadding = 8 -local yPadding = -18 +local yPadding = 4 local valueBackgroundPaddingX = 2 local valueBackgroundPaddingY = -1 -- textHeight isn't a tight bounds local sliderBarThickness = 6 @@ -62,7 +62,8 @@ local Slider = class( local valueTextWidth, valueTextHeight = self.valueText:getDimensions() local textWidth, textHeight = self.maxText:getDimensions() self.width = self.tickLength * self:tickCount() + xPadding + math.max(xPadding, textWidth / 2) - self.height = 4 * 2 + handleRadius * 2 + valueTextHeight + textHeight + self.height = handleRadius * 2 + valueTextHeight + textHeight + self.minHeight = self.height end, UIElement ) diff --git a/client/src/ui/UIElement.lua b/client/src/ui/UIElement.lua index dd17c833..f37e3cb7 100644 --- a/client/src/ui/UIElement.lua +++ b/client/src/ui/UIElement.lua @@ -129,11 +129,17 @@ local function onChildrenChanged(uiElement) end end -function UIElement:addChild(uiElement) +---@param uiElement UiElement +---@param index integer? +function UIElement:addChild(uiElement, index) if uiElement.parent then error("Tried to give a uiElement more than one parent") else - self.children[#self.children + 1] = uiElement + if index then + table.insert(self.children, index, uiElement) + else + self.children[#self.children + 1] = uiElement + end uiElement.parent = self onChildrenChanged(uiElement.parent) end From e9c22b6424d778e3caafa489f4dfa19932c465bf Mon Sep 17 00:00:00 2001 From: Endaris Date: Sun, 11 May 2025 23:45:08 +0200 Subject: [PATCH 08/49] refix slider positioning fix an issue where the top level children would not grow in the direction of the layout orientation --- client/src/scenes/OptionsMenu.lua | 5 ++++- client/src/ui/Label.lua | 1 - client/src/ui/Layouts/HorizontalFlexLayout.lua | 3 +-- client/src/ui/Layouts/HorizontalScrollLayout.lua | 11 +++++++++++ client/src/ui/Layouts/VerticalFlexLayout.lua | 3 +-- client/src/ui/Layouts/VerticalScrollLayout.lua | 11 +++++++++++ client/src/ui/MenuItem.lua | 8 +++++++- client/src/ui/Slider.lua | 2 +- client/src/ui/UIElement.lua | 2 +- 9 files changed, 37 insertions(+), 9 deletions(-) diff --git a/client/src/scenes/OptionsMenu.lua b/client/src/scenes/OptionsMenu.lua index c81827d2..a15ad46a 100644 --- a/client/src/scenes/OptionsMenu.lua +++ b/client/src/scenes/OptionsMenu.lua @@ -355,7 +355,10 @@ function OptionsMenu:loadGraphicsMenu() maxHeight = 540, childGap = 8, padding = 32, - width = 600, + minWidth = 600, + maxWidth = 900, + hFill = true, + backgroundColor = {1, 0, 0, 0.4} }) local themeIndex diff --git a/client/src/ui/Label.lua b/client/src/ui/Label.lua index 5155fdb6..a9bb53e1 100644 --- a/client/src/ui/Label.lua +++ b/client/src/ui/Label.lua @@ -100,7 +100,6 @@ function Label:refreshLocalization() local font = GraphicsUtil.getGlobalFontWithSize(self.fontSize) local totalWidth = font:getWidth(self.text) self.width = totalWidth - self.maxWidth = totalWidth if not self.wrap then self.minWidth = totalWidth end diff --git a/client/src/ui/Layouts/HorizontalFlexLayout.lua b/client/src/ui/Layouts/HorizontalFlexLayout.lua index 8d06d04d..13d2a0d6 100644 --- a/client/src/ui/Layouts/HorizontalFlexLayout.lua +++ b/client/src/ui/Layouts/HorizontalFlexLayout.lua @@ -1,6 +1,5 @@ local PATH = (...):gsub('%.[^%.]+$', '') local FlexLayout = require(PATH ..".FlexLayout") -local util = require("common.lib.util") ---@class HorizontalFlexLayout : FlexLayout local HorizontalFlexLayout = setmetatable({}, {__index = FlexLayout}) @@ -15,7 +14,7 @@ function HorizontalFlexLayout.getMinWidth(uiElement) end end - return util.bound(uiElement.minWidth, w, uiElement.maxWidth) + return w end ---@param uiElement UiElement diff --git a/client/src/ui/Layouts/HorizontalScrollLayout.lua b/client/src/ui/Layouts/HorizontalScrollLayout.lua index 8ea8b658..7847cc56 100644 --- a/client/src/ui/Layouts/HorizontalScrollLayout.lua +++ b/client/src/ui/Layouts/HorizontalScrollLayout.lua @@ -1,9 +1,20 @@ local PATH = (...):gsub('%.[^%.]+$', '') local HorizontalFlexLayout = require(PATH ..".HorizontalFlexLayout") +local util = require("common.lib.util") ---@class HorizontalScrollLayout : HorizontalFlexLayout local HorizontalScrollLayout = setmetatable({}, {__index = HorizontalFlexLayout}) +function HorizontalScrollLayout.getMinWidth(uiElement) + return util.bound(uiElement.minWidth, HorizontalFlexLayout.getMinWidth(uiElement), uiElement.maxWidth) +end + +function HorizontalScrollLayout.growChildrenWidth(uiElement) + for _, child in ipairs(uiElement.children) do + child.layout.growChildrenWidth(child) + end +end + ---@param uiElement UiElement function HorizontalScrollLayout.positionChildren(uiElement) local x = uiElement.padding diff --git a/client/src/ui/Layouts/VerticalFlexLayout.lua b/client/src/ui/Layouts/VerticalFlexLayout.lua index c8e6a2de..8625111a 100644 --- a/client/src/ui/Layouts/VerticalFlexLayout.lua +++ b/client/src/ui/Layouts/VerticalFlexLayout.lua @@ -1,6 +1,5 @@ local PATH = (...):gsub('%.[^%.]+$', '') local FlexLayout = require(PATH ..".FlexLayout") -local util = require("common.lib.util") ---@class VerticalFlexLayout : FlexLayout local VerticalFlexLayout = setmetatable({}, {__index = FlexLayout}) @@ -29,7 +28,7 @@ function VerticalFlexLayout.getMinHeight(uiElement) end end - return util.bound(uiElement.minHeight, h, uiElement.maxHeight) + return h end ---@param uiElement UiElement diff --git a/client/src/ui/Layouts/VerticalScrollLayout.lua b/client/src/ui/Layouts/VerticalScrollLayout.lua index 60374f62..81d0a56a 100644 --- a/client/src/ui/Layouts/VerticalScrollLayout.lua +++ b/client/src/ui/Layouts/VerticalScrollLayout.lua @@ -1,9 +1,20 @@ local PATH = (...):gsub('%.[^%.]+$', '') local VerticalFlexLayout = require(PATH ..".VerticalFlexLayout") +local util = require("common.lib.util") ---@class VerticalScrollLayout : VerticalFlexLayout local VerticalScrollLayout = setmetatable({}, {__index = VerticalFlexLayout}) +function VerticalScrollLayout.getMinHeight(uiElement) + return util.bound(uiElement.minHeight, VerticalFlexLayout.getMinHeight(uiElement), uiElement.maxHeight) +end + +function VerticalScrollLayout.growChildrenHeight(uiElement) + for _, child in ipairs(uiElement.children) do + child.layout.growChildrenHeight(child) + end +end + ---@param uiElement UiElement function VerticalScrollLayout.positionChildren(uiElement) local y = uiElement.padding diff --git a/client/src/ui/MenuItem.lua b/client/src/ui/MenuItem.lua index 48ee046a..aed43218 100644 --- a/client/src/ui/MenuItem.lua +++ b/client/src/ui/MenuItem.lua @@ -21,7 +21,13 @@ MenuItem.PADDING = 2 function MenuItem.createMenuItem(label, item) assert(label ~= nil) - local menuItem = UiElement({hAlign = "center", layout = HorizontalFlexLayout, childGap = 16, hFill = true}) + local menuItem = UiElement({ + hAlign = "center", + layout = HorizontalFlexLayout, + childGap = 16, + hFill = true, + backgroundColor = {math.random(), math.random(), math.random(), 0.4} + }) menuItem.width = label.width + (2 * MenuItem.PADDING) diff --git a/client/src/ui/Slider.lua b/client/src/ui/Slider.lua index 8126b1ba..facdd6b0 100644 --- a/client/src/ui/Slider.lua +++ b/client/src/ui/Slider.lua @@ -6,7 +6,7 @@ local GraphicsUtil = require("client.src.graphics.graphics_util") local handleRadius = 7.5 local xPadding = 8 -local yPadding = 4 +local yPadding = 2 local valueBackgroundPaddingX = 2 local valueBackgroundPaddingY = -1 -- textHeight isn't a tight bounds local sliderBarThickness = 6 diff --git a/client/src/ui/UIElement.lua b/client/src/ui/UIElement.lua index f37e3cb7..daac46be 100644 --- a/client/src/ui/UIElement.lua +++ b/client/src/ui/UIElement.lua @@ -198,7 +198,7 @@ function UIElement:drawSelf() love.graphics.setColor(self.backgroundColor) love.graphics.rectangle("fill", self.x, self.y, self.width, self.height) love.graphics.setColor(1, 1, 1, 1) - --love.graphics.print(self.width .. ", " .. self.height, self.x + 5, self.y + 5) + love.graphics.print(self.width .. ", " .. self.height, self.x + 5, self.y + 5) end function UIElement:drawChildren() From ed0dcce8b85bccac97446d41e6a63d8184f35225 Mon Sep 17 00:00:00 2001 From: Endaris Date: Mon, 12 May 2025 01:07:53 +0200 Subject: [PATCH 09/49] vaguely resolve most crashes for a slightly broken layout (lobby/vsSelf) --- client/src/ClientMatch.lua | 2 +- client/src/scenes/CharacterSelect.lua | 10 +- client/src/scenes/DesignHelper.lua | 95 +++++++++++++------ client/src/scenes/Lobby.lua | 28 +++--- client/src/scenes/ModManagement.lua | 4 +- client/src/scenes/OptionsMenu.lua | 12 +-- client/src/scenes/SetNameMenu.lua | 4 +- client/src/ui/Grid.lua | 5 +- client/src/ui/GridCursor.lua | 4 +- client/src/ui/Label.lua | 46 +++++++-- client/src/ui/Layouts/FlexLayout.lua | 20 ++-- .../src/ui/Layouts/HorizontalFlexLayout.lua | 8 +- client/src/ui/Layouts/Layout.lua | 12 +++ client/src/ui/Layouts/StaticLayout.lua | 31 ++++++ client/src/ui/Layouts/VerticalFlexLayout.lua | 8 +- client/src/ui/MenuItem.lua | 7 +- client/src/ui/MultiPlayerSelectionWrapper.lua | 1 - client/src/ui/UIElement.lua | 4 +- client/src/ui/VerticalMenu.lua | 4 + 19 files changed, 211 insertions(+), 94 deletions(-) create mode 100644 client/src/ui/Layouts/StaticLayout.lua diff --git a/client/src/ClientMatch.lua b/client/src/ClientMatch.lua index 7ddf3a6c..ed3d9c1e 100644 --- a/client/src/ClientMatch.lua +++ b/client/src/ClientMatch.lua @@ -672,7 +672,7 @@ function ClientMatch:draw_pause() GraphicsUtil.draw(image, x, y, 0, scale, scale, xOffset, yOffset) end local y = 260 - GraphicsUtil.printf(loc("pause"), 0, y, consts.CANVAS_WIDTH, "center", nil, 1, 10) + GraphicsUtil.printf(loc("pause"), 0, y, consts.CANVAS_WIDTH, "center", nil, 1, "big") GraphicsUtil.printf(loc("pl_pause_help"), 0, y + 30, consts.CANVAS_WIDTH, "center", nil, 1) end diff --git a/client/src/scenes/CharacterSelect.lua b/client/src/scenes/CharacterSelect.lua index 5adf18cb..643ff01b 100644 --- a/client/src/scenes/CharacterSelect.lua +++ b/client/src/scenes/CharacterSelect.lua @@ -812,9 +812,9 @@ function CharacterSelect:createRankedStatusPanel() vAlign = "top" }) if self.battleRoom.ranked then - rankedStatus.rankedLabel:setText("ss_ranked") + rankedStatus.rankedLabel:setId("ss_ranked") else - rankedStatus.rankedLabel:setText("ss_casual") + rankedStatus.rankedLabel:setId("ss_casual") end rankedStatus.commentLabel = ui.Label({ text = self.battleRoom.rankedComments or "", @@ -827,11 +827,11 @@ function CharacterSelect:createRankedStatusPanel() rankedStatus.update = function(self, ranked, comments) if ranked then - rankedStatus.rankedLabel:setText("ss_ranked") + rankedStatus.rankedLabel:setId("ss_ranked") else - rankedStatus.rankedLabel:setText("ss_casual") + rankedStatus.rankedLabel:setId("ss_casual") end - rankedStatus.commentLabel:setText(comments, nil, false) + rankedStatus.commentLabel:setText(comments) end self.battleRoom:connectSignal("rankedStatusChanged", rankedStatus, rankedStatus.update) diff --git a/client/src/scenes/DesignHelper.lua b/client/src/scenes/DesignHelper.lua index 7b9b99fb..2101e8cd 100644 --- a/client/src/scenes/DesignHelper.lua +++ b/client/src/scenes/DesignHelper.lua @@ -4,40 +4,77 @@ local ui = require("client.src.ui") local input = require("client.src.inputManager") local DesignHelper = class(function(self, sceneParams) - self:load(sceneParams) + --self:load(sceneParams) end, Scene) DesignHelper.name = "DesignHelper" function DesignHelper:load() - self:loadGrid() - --self:loadPanels() - --self:loadStages() - --self.grid:createElementAt(1, 2, 2, 1, "stage", self.stageCarousel) - self.rankedSelection = ui.StackPanel({vFill = true, alignment = "left", hAlign = "center", vAlign = "center"}) - local trueLabel = ui.Label({id = "ss_ranked", vAlign = "top", hAlign = "center"}) - local falseLabel = ui.Label({id = "ss_casual", vAlign = "bottom", hAlign = "center"}) - self.rankedSelection:addChild(trueLabel) - self.rankedSelection:addChild(falseLabel) - self.grid:createElementAt(3, 2, 2, 1, "ranked", self.rankedSelection) - self.rankedSelection:addElement(self:loadRankedSelection(96)) - self.rankedSelection:addElement(self:loadRankedSelection(96)) -end + self.uiRoot.layout = ui.Layouts.VerticalFlexLayout + + local roomMode = ui.ButtonGroup({ + childGap = 8, + padding = 8, + hAlign = "center", + --hFill = true, + backgroundColor = {1, 0, 0, 0.5}, + }) + + roomMode:addChild(ui.Text({text = "Battle", hAlign = "center", vAlign = "center"})) + roomMode:addChild(ui.Text({text = "Arcade", hAlign = "center", vAlign = "center"})) + + self.uiRoot:addChild(roomMode) + + local gameMode = ui.HorizontalRadioSelector({ + childGap = 8, + padding = 8, + backgroundColor = {0, 0, 1, 0.5}, + hAlign = "center", + -- hFill = true, + }) + + gameMode:addChild(ui.Text({text = "VS", wrap = false})) + gameMode:addChild(ui.Text({text = "VS Self", wrap = false})) + gameMode:addChild(ui.Text({text = "Time Attack", wrap = false})) + gameMode:addChild(ui.Text({text = "Endless", wrap = false})) + gameMode:addChild(ui.Text({text = "Puzzle", wrap = false})) + gameMode:addChild(ui.Text({text = "Training", wrap = false})) + gameMode:addChild(ui.Text({text = "Line Clear", wrap = false})) + + self.uiRoot:addChild(gameMode) + + local subSelectionSelector = ui.UIElement({ + childGap = 8, + padding = 8, + hFill = true, + backgroundColor = {0, 1, 0, 0.5} + }) + + subSelectionSelector:addChild(ui.Text({text = "Character", wrap = false})) + subSelectionSelector:addChild(ui.Text({text = "Stage", wrap = false})) + subSelectionSelector:addChild(ui.Text({text = "Panels", wrap = false})) + subSelectionSelector:addChild(ui.Text({text = "Ranked", wrap = false})) + subSelectionSelector:addChild(ui.Text({text = "Level", wrap = false})) + subSelectionSelector:addChild(ui.Text({text = "Input Selection", wrap = false})) + subSelectionSelector:addChild(ui.Text({text = "Puzzle", wrap = false})) + subSelectionSelector:addChild(ui.Text({text = "Attack File", wrap = false})) + + self.uiRoot:addChild(subSelectionSelector) + + local subSelection = ui.UIElement({ + hFill = true, + minHeight = 400, + vFill = true, + padding = 8, + backgroundColor = {0.7, 0, 0.5, 1}, + }) + + subSelection:addChild(ui.Text({})) + + self.uiRoot:addChild(subSelection) -function DesignHelper:loadGrid() - self.grid = ui.Grid({x = 180, y = 60, unitSize = 108, gridWidth = 9, gridHeight = 6, unitMargin = 6}) - self.uiRoot:addChild(self.grid) - -- self.cursor = GridCursor({ - -- grid = self.grid, - -- activeArea = {x1 = 1, y1 = 2, x2 = 9, y2 = 5}, - -- translateSubGrids = true, - -- startPosition = {x = 9, y = 2}, - -- playerNumber = 1 - -- }) - -- self.uiRoot:addChild(self.cursor) - -- self.cursor.escapeCallback = function() - -- SoundController:playSfx(themes[config.theme].sounds.menu_cancel) - -- end + self.uiRoot:addChild(ui.Text({text = "Ready", hAlign = "center"})) + self.uiRoot:addChild(ui.Text({text = "Leave", hAlign = "center"})) end function DesignHelper:loadRankedSelection(width) @@ -63,7 +100,7 @@ function DesignHelper:update() end function DesignHelper:draw() - self.grid:draw() + self.uiRoot:draw() end return DesignHelper diff --git a/client/src/scenes/Lobby.lua b/client/src/scenes/Lobby.lua index 50813850..558d68be 100644 --- a/client/src/scenes/Lobby.lua +++ b/client/src/scenes/Lobby.lua @@ -111,11 +111,11 @@ end function Lobby:toggleLeaderboard() GAME.theme:playMoveSfx() if not self.leaderboard.isVisible then - self.leaderboardToggleLabel:setText("lb_hide_board") + self.leaderboardToggleLabel:setId("lb_hide_board") GAME.netClient:requestLeaderboard() self.lobbyMenu:setFocus(self.leaderboard, function() self:toggleLeaderboard() end) else - self.leaderboardToggleLabel:setText("lb_show_board") + self.leaderboardToggleLabel:setId("lb_show_board") end self.leaderboard:setVisibility(not self.leaderboard.isVisible) self.lobbyMenu.x = self.lobbyMenuXoffsetMap[self.leaderboard.isVisible] @@ -157,13 +157,13 @@ end -- rebuilds the UI based on the new lobby information function Lobby:onLobbyStateUpdate(lobbyState) local previousText - if self.lobbyMenu.menuItems[self.lobbyMenu.selectedIndex].textButton then - previousText = self.lobbyMenu.menuItems[self.lobbyMenu.selectedIndex].textButton.children[1].text + if self.lobbyMenu.children[self.lobbyMenu.selectedIndex].children[1] then + previousText = self.lobbyMenu.children[self.lobbyMenu.selectedIndex].children[1].text end local desiredIndex = self.lobbyMenu.selectedIndex -- cleanup previous lobby menu - while #self.lobbyMenu.menuItems > 6 do + while #self.lobbyMenu.children > 6 do self.lobbyMenu:removeMenuItemAtIndex(2) end self.lobbyMenu:setSelectedIndex(1) @@ -177,7 +177,7 @@ function Lobby:onLobbyStateUpdate(lobbyState) if lobbyState.willingPlayers[v] then unmatchedPlayer = unmatchedPlayer .. " " .. loc("lb_received") end - self.lobbyMenu:addMenuItem(2, ui.MenuItem.createButtonMenuItem(unmatchedPlayer, nil, false, self:requestGameFunction(v))) + self.lobbyMenu:addChild(ui.MenuItem.createButtonMenuItem(unmatchedPlayer, nil, false, self:requestGameFunction(v)), 2) end end for _, room in ipairs(lobbyState.spectatableRooms) do @@ -185,10 +185,10 @@ function Lobby:onLobbyStateUpdate(lobbyState) local playerA = room.a .. self:playerRatingString(room.a) local playerB = room.b .. self:playerRatingString(room.b) local roomName = loc("lb_spectate") .. " " .. playerA .. " vs " .. playerB .. " (" .. room.state .. ")" - self.lobbyMenu:addMenuItem(2, ui.MenuItem.createButtonMenuItem(roomName, nil, false, self:requestSpectateFunction(room))) + self.lobbyMenu:addChild(ui.MenuItem.createButtonMenuItem(roomName, nil, false, self:requestSpectateFunction(room)), 2) else local roomName = loc("lb_spectate") .. " " .. room.name .. " (" .. room.state .. ")" - self.lobbyMenu:addMenuItem(2, ui.MenuItem.createButtonMenuItem(roomName, nil, false, self:requestSpectateFunction(room))) + self.lobbyMenu:addChild(ui.MenuItem.createButtonMenuItem(roomName, nil, false, self:requestSpectateFunction(room)), 2) end end @@ -196,13 +196,13 @@ function Lobby:onLobbyStateUpdate(lobbyState) self.lobbyMenu:setSelectedIndex(2) self.lobbyMenuStartingUp = false else - for i = 1, #self.lobbyMenu.menuItems do - if self.lobbyMenu.menuItems[i].textButton and self.lobbyMenu.menuItems[i].textButton.children[1].text == previousText then + for i = 1, #self.lobbyMenu.children do + if self.lobbyMenu.children[i].children[1] and self.lobbyMenu.children[i].children[1].text == previousText then desiredIndex = i break end end - self.lobbyMenu:setSelectedIndex(util.bound(2, desiredIndex, #self.lobbyMenu.menuItems)) + self.lobbyMenu:setSelectedIndex(util.bound(2, desiredIndex, #self.lobbyMenu.children)) end end @@ -218,9 +218,9 @@ function Lobby:update(dt) else if GAME.timer > GAME.netClient.loginTime + 5 then if #GAME.netClient.lobbyData.players == 1 then - self.lobbyMessage:setText("lb_alone", nil, true) + self.lobbyMessage:setId("lb_alone") else - self.lobbyMessage:setText("lb_select_player", nil, true) + self.lobbyMessage:setId("lb_select_player") end end self.lobbyMenu:receiveInputs() @@ -246,7 +246,7 @@ end function Lobby:onLoginFinish(result) if result.loggedIn then - self.lobbyMessage:setText(result.message, nil, false) + self.lobbyMessage:setText(result.message) else local messageTransition = MessageTransition(love.timer.getTime(), 5, result.message) GAME.navigationStack:pop(messageTransition) diff --git a/client/src/scenes/ModManagement.lua b/client/src/scenes/ModManagement.lua index 73af7a75..9481501a 100644 --- a/client/src/scenes/ModManagement.lua +++ b/client/src/scenes/ModManagement.lua @@ -75,7 +75,7 @@ function ModManagement:load() if self.scrollContainer then self.stackPanel:remove(self.scrollContainer) end - self.headerLabel:setText("characters") + self.headerLabel:setId("characters") self.scrollContainer = self:newScrollContainer() self.scrollContainer:addChild(self.characterGrid) self.stackPanel:addElement(self.scrollContainer) @@ -92,7 +92,7 @@ function ModManagement:load() if self.scrollContainer then self.stackPanel:remove(self.scrollContainer) end - self.headerLabel:setText("stages") + self.headerLabel:setId("stages") self.scrollContainer = self:newScrollContainer() self.scrollContainer:addChild(self.stageGrid) self.stackPanel:addElement(self.scrollContainer) diff --git a/client/src/scenes/OptionsMenu.lua b/client/src/scenes/OptionsMenu.lua index a15ad46a..bb8aceb0 100644 --- a/client/src/scenes/OptionsMenu.lua +++ b/client/src/scenes/OptionsMenu.lua @@ -83,7 +83,7 @@ end local function createToggleButtonGroup(configField, onChangeFn) return ui.ButtonGroup({ - buttons = {ui.TextButton({width = 60, label = ui.Label({id = "op_off"})}), ui.TextButton({width = 60, label = ui.Label({id = "op_on"})})}, + buttons = {ui.TextButton({width = 60, label = ui.Label({id = "op_off"})}), ui.TextButton({width = 60, label = ui.Label({id = "op_on"})})}, values = {false, true}, selectedIndex = config[configField] and 2 or 1, onChange = function(group, value) @@ -232,8 +232,8 @@ function OptionsMenu:loadGeneralMenu() local saveReplaysPubliclyIndexMap = {["with my name"] = 1, ["anonymously"] = 2, ["not at all"] = 3} local publicReplayButtonGroup = ui.ButtonGroup({ buttons = { - ui.TextButton({label = ui.Label({id = "op_replay_public_with_name"})}), - ui.TextButton({label = ui.Label({id = "op_replay_public_anonymously"})}), ui.TextButton({label = ui.Label({id = "op_replay_public_no"})}) + ui.TextButton({label = ui.Label({id = "op_replay_public_with_name"})}), + ui.TextButton({label = ui.Label({id = "op_replay_public_anonymously"})}), ui.TextButton({label = ui.Label({id = "op_replay_public_no"})}) }, values = {"with my name", "anonymously", "not at all"}, selectedIndex = saveReplaysPubliclyIndexMap[config.save_replays_publicly], @@ -426,7 +426,7 @@ function OptionsMenu:loadGraphicsMenu() end local scaleTypeData = { - {value = "auto", id = "op_scale_auto"}, {value = "fit", id = "op_scale_fit"}, {value = "fixed", id = "op_scale_fixed"} + {value = "auto", id = "op_scale_auto"}, {value = "fit", id = "op_scale_fit"}, {value = "fixed", id = "op_scale_fixed"} } for index, value in ipairs(scaleTypeData) do value.index = index @@ -498,8 +498,8 @@ function OptionsMenu:loadSoundMenu() local musicFrequencyIndexMap = {["stage"] = 1, ["often_stage"] = 2, ["either"] = 3, ["often_characters"] = 4, ["characters"] = 5} local musicFrequencyStepper = ui.Stepper({ labels = { - ui.Label({id = "op_only_stage"}), ui.Label({id = "op_often_stage"}), ui.Label({id = "op_stage_characters"}), - ui.Label({id = "op_often_characters"}), ui.Label({id = "op_only_characters"}) + ui.Label({id = "op_only_stage"}), ui.Label({id = "op_often_stage"}), ui.Label({id = "op_stage_characters"}), + ui.Label({id = "op_often_characters"}), ui.Label({id = "op_only_characters"}) }, values = {"stage", "often_stage", "either", "often_characters", "characters"}, selectedIndex = musicFrequencyIndexMap[config.use_music_from], diff --git a/client/src/scenes/SetNameMenu.lua b/client/src/scenes/SetNameMenu.lua index 253d6305..c01eec03 100644 --- a/client/src/scenes/SetNameMenu.lua +++ b/client/src/scenes/SetNameMenu.lua @@ -87,7 +87,7 @@ end function SetNameMenu:update(dt) self.backgroundImg:update(dt) if self.validationLabel.text ~= "" and self.nameField.value ~= "" then - self.validationLabel:setText("", nil, false) + self.validationLabel:setText("") end if input.allKeys.isDown["return"] then @@ -102,7 +102,7 @@ function SetNameMenu:update(dt) if self.nameField.hasFocus then self.nameLengthLabel:setText("(" .. self.nameField.value:len() .. "/" .. NAME_LENGTH_LIMIT .. ")") if self.nameField.value == "" then - self.validationLabel:setText("op_username_blank_warning", nil, true) + self.validationLabel:setId("op_username_blank_warning") end self.confirmationButton:setEnabled(self.nameField.value ~= "") end diff --git a/client/src/ui/Grid.lua b/client/src/ui/Grid.lua index 9e44c3e3..1776e02e 100644 --- a/client/src/ui/Grid.lua +++ b/client/src/ui/Grid.lua @@ -3,6 +3,7 @@ local UiElement = require(PATH .. ".UIElement") local GridElement = require(PATH .. ".GridElement") local class = require("common.lib.class") local GraphicsUtil = require("client.src.graphics.graphics_util") +local StaticLayout = require(PATH .. ".Layouts.StaticLayout") local Grid = class(function(self, options) self.unitSize = options.unitSize @@ -19,9 +20,11 @@ local Grid = class(function(self, options) -- self.grid[row][col] = {} -- end end - self.TYPE = "Grid" end, UiElement) +Grid.TYPE = "Grid" +Grid.layout = StaticLayout + -- width and height are sizes relative to the unitSize of the grid -- id is a string identificator to indiate what kind of uiElement resides here -- uiElement is the actual element on display that will perform user interaction when selected diff --git a/client/src/ui/GridCursor.lua b/client/src/ui/GridCursor.lua index b4c2f6f7..26bf9639 100644 --- a/client/src/ui/GridCursor.lua +++ b/client/src/ui/GridCursor.lua @@ -4,6 +4,7 @@ local class = require("common.lib.class") local directsFocus = require(PATH .. ".FocusDirector") local consts = require("common.engine.consts") local GraphicsUtil = require("client.src.graphics.graphics_util") +local StaticLayout = require(PATH .. ".Layouts.StaticLayout") -- create a new cursor that can navigate on the specified grid -- grid: the target grid that is navigated on @@ -38,9 +39,10 @@ local GridCursor = class(function(self, options) self.trapped = false self.drawClock = 0 - self.TYPE = "GridCursor" end, UiElement) +GridCursor.TYPE = "GridCursor" +GridCursor.layout = StaticLayout GridCursor.directions = {up = {x = 0, y = -1}, down = {x = 0, y = 1}, left = {x = -1, y = 0}, right = {x = 1, y = 0}} function GridCursor:setTarget(grid, startPosition, activeArea) diff --git a/client/src/ui/Label.lua b/client/src/ui/Label.lua index a9bb53e1..a085fb99 100644 --- a/client/src/ui/Label.lua +++ b/client/src/ui/Label.lua @@ -81,28 +81,56 @@ local Label = class( ) Label.TYPE = "Label" ----@param fontSize FontSize -function Label:setFontSize(fontSize) - self.fontSize = fontSize +function Label:recalculateSizes() local font = GraphicsUtil.getGlobalFontWithSize(self.fontSize) + local words = self.text:split() + local maxWordWidth = font:getWidth(words[1]) + for i = 2, #words do + maxWordWidth = math.max(maxWordWidth, font:getWidth(words[i])) + end + + self.minHeight = font:getHeight() + if self.wrap then + self.maxHeight = (#words * font:getHeight()) + else + self.maxHeight = self.minHeight + end local totalWidth = font:getWidth(self.text) self.width = totalWidth self.maxWidth = math.huge if not self.wrap then + self.minWidth = maxWordWidth + else self.minWidth = totalWidth end end +---@param fontSize FontSize +function Label:setFontSize(fontSize) + self.fontSize = fontSize + self:recalculateSizes() +end + +---@param id string +---@param replacements table? +function Label:setId(id, replacements) + self.id = id + self.replacementTable = replacements or {} + self.text = loc(self.id, unpack(self.replacementTable)) + self:recalculateSizes() +end + +---@param text string +function Label:setText(text) + self.text = text + self:recalculateSizes() +end + function Label:refreshLocalization() if self.id then self.text = loc(self.id, unpack(self.replacementTable)) end - local font = GraphicsUtil.getGlobalFontWithSize(self.fontSize) - local totalWidth = font:getWidth(self.text) - self.width = totalWidth - if not self.wrap then - self.minWidth = totalWidth - end + self:recalculateSizes() end function Label:drawSelf() diff --git a/client/src/ui/Layouts/FlexLayout.lua b/client/src/ui/Layouts/FlexLayout.lua index bceaddba..7fb79c95 100644 --- a/client/src/ui/Layouts/FlexLayout.lua +++ b/client/src/ui/Layouts/FlexLayout.lua @@ -7,7 +7,9 @@ local FlexLayout = setmetatable({}, {__index = Layout}) ---@param uiElement UiElement function FlexLayout.fitSizeWidth(uiElement) for _, child in ipairs(uiElement.children) do - child.layout.fitSizeWidth(child) + if child.layout.fitSizeWidth then + child.layout.fitSizeWidth(child) + end end local w = uiElement.layout.getMinWidth(uiElement) uiElement.width = math.max(w, uiElement.minWidth) @@ -16,7 +18,9 @@ end ---@param uiElement UiElement function FlexLayout.fitSizeHeight(uiElement) for _, child in ipairs(uiElement.children) do - child.layout.fitSizeHeight(child) + if child.layout.fitSizeHeight then + child.layout.fitSizeHeight(child) + end end local h = uiElement.layout.getMinHeight(uiElement) uiElement.height = math.max(h, uiElement.minHeight) @@ -38,18 +42,6 @@ function FlexLayout.updateHeights(uiElement, height) uiElement.layout.growChildrenHeight(uiElement) end ----@param uiElement UiElement ----@return number -function FlexLayout.getMinWidth(uiElement) - error("FlexLayout does not implement getMinWidth") -end - ----@param uiElement UiElement ----@return number -function FlexLayout.getMinHeight(uiElement) - error("FlexLayout does not implement getMinHeight") -end - ---@param uiElement UiElement function FlexLayout.growChildrenWidth(uiElement) error("FlexLayout does not implement growChildrenWidth") diff --git a/client/src/ui/Layouts/HorizontalFlexLayout.lua b/client/src/ui/Layouts/HorizontalFlexLayout.lua index 13d2a0d6..02f8300d 100644 --- a/client/src/ui/Layouts/HorizontalFlexLayout.lua +++ b/client/src/ui/Layouts/HorizontalFlexLayout.lua @@ -108,7 +108,9 @@ function HorizontalFlexLayout.growChildrenWidth(uiElement) end for _, child in ipairs(uiElement.children) do - child.layout.growChildrenWidth(child) + if child.layout.growChildrenWidth then + child.layout.growChildrenWidth(child) + end end end @@ -118,7 +120,9 @@ function HorizontalFlexLayout.growChildrenHeight(uiElement) if child.vFill then child.height = math.min(uiElement.height - uiElement.padding * 2, child.maxHeight) end - child.layout.growChildrenHeight(child) + if child.layout.growChildrenHeight then + child.layout.growChildrenHeight(child) + end end end diff --git a/client/src/ui/Layouts/Layout.lua b/client/src/ui/Layouts/Layout.lua index d858edeb..c8c17369 100644 --- a/client/src/ui/Layouts/Layout.lua +++ b/client/src/ui/Layouts/Layout.lua @@ -34,5 +34,17 @@ function Layout.positionChildren(uiElement) error("Layout does not implement positionChildren") end +---@param uiElement UiElement +---@return number +function Layout.getMinWidth(uiElement) + error("FlexLayout does not implement getMinWidth") +end + +---@param uiElement UiElement +---@return number +function Layout.getMinHeight(uiElement) + error("FlexLayout does not implement getMinHeight") +end + return Layout \ No newline at end of file diff --git a/client/src/ui/Layouts/StaticLayout.lua b/client/src/ui/Layouts/StaticLayout.lua new file mode 100644 index 00000000..a22e4f47 --- /dev/null +++ b/client/src/ui/Layouts/StaticLayout.lua @@ -0,0 +1,31 @@ +local PATH = (...):gsub('%.[^%.]+$', '') +local Layout = require(PATH ..".Layout") + +---@class StaticLayout : Layout +local StaticLayout = setmetatable({}, {__index = Layout}) + +---@param uiElement UiElement +function StaticLayout.updateWidths(uiElement, width) +end + +---@param uiElement UiElement +function StaticLayout.updateHeights(uiElement, height) +end + +---@param uiElement UiElement +function StaticLayout.positionChildren(uiElement) +end + +---@param uiElement UiElement +---@return number +function StaticLayout.getMinWidth(uiElement) + return uiElement.width +end + +---@param uiElement UiElement +---@return number +function StaticLayout.getMinHeight(uiElement) + return uiElement.height +end + +return StaticLayout \ No newline at end of file diff --git a/client/src/ui/Layouts/VerticalFlexLayout.lua b/client/src/ui/Layouts/VerticalFlexLayout.lua index 8625111a..c2508c97 100644 --- a/client/src/ui/Layouts/VerticalFlexLayout.lua +++ b/client/src/ui/Layouts/VerticalFlexLayout.lua @@ -108,7 +108,9 @@ function VerticalFlexLayout.growChildrenHeight(uiElement) end for _, child in ipairs(uiElement.children) do - child.layout.growChildrenHeight(child) + if child.layout.growChildrenHeight then + child.layout.growChildrenHeight(child) + end end end @@ -118,7 +120,9 @@ function VerticalFlexLayout.growChildrenWidth(uiElement) if child.hFill then child.width = math.min(uiElement.width - uiElement.padding * 2, child.maxWidth) end - child.layout.growChildrenWidth(child) + if child.layout.growChildrenWidth then + child.layout.growChildrenWidth(child) + end end end diff --git a/client/src/ui/MenuItem.lua b/client/src/ui/MenuItem.lua index aed43218..d77dc93a 100644 --- a/client/src/ui/MenuItem.lua +++ b/client/src/ui/MenuItem.lua @@ -40,10 +40,9 @@ function MenuItem.createMenuItem(label, item) item.hAlign = "left" item.hFill = true menuItem:addChild(item) - end - - menuItem.receiveInputs = function(i, inputs) - item:receiveInputs(inputs) + menuItem.receiveInputs = function(i, inputs) + item:receiveInputs(inputs) + end end return menuItem diff --git a/client/src/ui/MultiPlayerSelectionWrapper.lua b/client/src/ui/MultiPlayerSelectionWrapper.lua index f2f2ce44..fa749358 100644 --- a/client/src/ui/MultiPlayerSelectionWrapper.lua +++ b/client/src/ui/MultiPlayerSelectionWrapper.lua @@ -23,7 +23,6 @@ function MultiPlayerSelectionWrapper:addElement(uiElement, player) end self:applyStackPanelSettings(uiElement) self:addChild(uiElement) - self:resize() end function MultiPlayerSelectionWrapper:insertElementAtIndex(uiElement, index, player) diff --git a/client/src/ui/UIElement.lua b/client/src/ui/UIElement.lua index daac46be..350205ab 100644 --- a/client/src/ui/UIElement.lua +++ b/client/src/ui/UIElement.lua @@ -133,7 +133,9 @@ end ---@param index integer? function UIElement:addChild(uiElement, index) if uiElement.parent then - error("Tried to give a uiElement more than one parent") + if uiElement.parent ~= self then + error("Tried to give a uiElement more than one parent") + end else if index then table.insert(self.children, index, uiElement) diff --git a/client/src/ui/VerticalMenu.lua b/client/src/ui/VerticalMenu.lua index 2a38d9de..2b4089ec 100644 --- a/client/src/ui/VerticalMenu.lua +++ b/client/src/ui/VerticalMenu.lua @@ -103,6 +103,10 @@ function VerticalMenu:receiveInputs(inputs, dt) end end +function VerticalMenu:setSelectedIndex(index) + self.selectedIndex = util.bound(1, index, #self.children) +end + function VerticalMenu:draw() ScrollContainer.draw(self) if self.selectedIndex then From b8f5b529dc270cd7e4b7238ef85cfce471f48265 Mon Sep 17 00:00:00 2001 From: Endaris Date: Sat, 17 May 2025 13:47:44 +0200 Subject: [PATCH 10/49] add a static layout that blocks flex propagation --- client/src/ui/GridElement.lua | 3 +++ client/src/ui/ScrollContainer.lua | 6 +++++- client/src/ui/UIElement.lua | 20 ++++++++++---------- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/client/src/ui/GridElement.lua b/client/src/ui/GridElement.lua index d0725fdf..607e4359 100644 --- a/client/src/ui/GridElement.lua +++ b/client/src/ui/GridElement.lua @@ -2,6 +2,7 @@ local PATH = (...):gsub('%.[^%.]+$', '') local UiElement = require(PATH .. ".UIElement") local class = require("common.lib.class") local GraphicsUtil = require("client.src.graphics.graphics_util") +local StaticLayout = require(PATH .. ".Layouts.StaticLayout") local GridElement = class(function(gridElement, options) if options.content then @@ -24,6 +25,8 @@ local GridElement = class(function(gridElement, options) gridElement.TYPE = "GridElement" end, UiElement) +GridElement.layout = StaticLayout + function GridElement:drawSelf() if self.drawBorders then GraphicsUtil.drawRectangle("line", self.x, self.y, self.width, self.height) diff --git a/client/src/ui/ScrollContainer.lua b/client/src/ui/ScrollContainer.lua index 5c93a0f5..aeb66c88 100644 --- a/client/src/ui/ScrollContainer.lua +++ b/client/src/ui/ScrollContainer.lua @@ -199,7 +199,7 @@ function ScrollContainer:getTouchedChildElement(x, y) end end -function ScrollContainer:onResize() +function ScrollContainer:recalculateMaxScrollOffset() local lastChild = self.children[#self.children] if lastChild then if self.scrollOrientation == "vertical" then @@ -212,4 +212,8 @@ function ScrollContainer:onResize() end end +function ScrollContainer:onResize() + self:recalculateMaxScrollOffset() +end + return ScrollContainer \ No newline at end of file diff --git a/client/src/ui/UIElement.lua b/client/src/ui/UIElement.lua index 350205ab..9b28bc41 100644 --- a/client/src/ui/UIElement.lua +++ b/client/src/ui/UIElement.lua @@ -109,21 +109,21 @@ local UIElement = class( UIElement.TYPE = "UIElement" -local function onChildrenChanged(uiElement) - if uiElement.parent then +function UIElement:onChildrenChanged() + if self.parent then -- resizing segments does not make much sense if the segments have to be resized later anyway -- so bubble just straight up - onChildrenChanged(uiElement.parent) + self.parent:onChildrenChanged() else -- and then only resize from the root element - local oWidth = uiElement.width - local oHeight = uiElement.height + local oWidth = self.width + local oHeight = self.height - uiElement.layout.resize(uiElement) + self.layout.resize(self) - if uiElement.controlsWindow then - if oWidth ~= uiElement.width or oHeight ~= uiElement.height then - love.window.updateMode(uiElement.width, uiElement.height, {}) + if self.controlsWindow then + if oWidth ~= self.width or oHeight ~= self.height then + love.window.updateMode(self.width, self.height, {}) end end end @@ -143,7 +143,7 @@ function UIElement:addChild(uiElement, index) self.children[#self.children + 1] = uiElement end uiElement.parent = self - onChildrenChanged(uiElement.parent) + self:onChildrenChanged() end end From 8dcd4ff231d7a8b87a805fdfa3ebb16bb4a2d713 Mon Sep 17 00:00:00 2001 From: Endaris Date: Sat, 17 May 2025 16:56:07 +0200 Subject: [PATCH 11/49] adjust layouting to use a UIElement specific baseWidth/Height --- client/src/ui/Label.lua | 48 ++++--------------- client/src/ui/Layouts/AdaptiveFlexLayout.lua | 14 +----- client/src/ui/Layouts/FlexLayout.lua | 22 ++++++--- .../src/ui/Layouts/HorizontalFlexLayout.lua | 23 +++------ .../src/ui/Layouts/HorizontalScrollLayout.lua | 4 +- client/src/ui/Layouts/Layout.lua | 4 ++ client/src/ui/Layouts/VerticalFlexLayout.lua | 28 ++--------- .../src/ui/Layouts/VerticalScrollLayout.lua | 4 +- client/src/ui/UIElement.lua | 12 ++++- 9 files changed, 54 insertions(+), 105 deletions(-) diff --git a/client/src/ui/Label.lua b/client/src/ui/Label.lua index a085fb99..6e2ed885 100644 --- a/client/src/ui/Label.lua +++ b/client/src/ui/Label.lua @@ -36,43 +36,18 @@ local Label = class( self.fontSize = options.fontSize or "normal" local font = GraphicsUtil.getGlobalFontWithSize(self.fontSize) - local totalWidth = font:getWidth(self.text) if options.wrap ~= nil then self.wrap = options.wrap else self.wrap = true end + -- min sizes are set with this function + self:recalculateSizes() - local words = self.text:split() - local maxWordWidth = font:getWidth(words[1]) - for i = 2, #words do - maxWordWidth = math.max(maxWordWidth, font:getWidth(words[i])) - end - - if options.minWidth ~= nil then - self.minWidth = options.minWidth - else - if self.wrap then - self.minWidth = maxWordWidth - else - self.minWidth = totalWidth - end - end - - self.width = options.width or totalWidth + self.width = options.width or self.preferredWidth self.maxWidth = options.maxWidth or math.huge - self.minHeight = options.minHeight or font:getHeight() self.height = options.height or font:getHeight() - - if options.maxHeight ~= nil then - self.maxHeight = options.maxHeight - else - if self.wrap then - self.maxHeight = (#words * font:getHeight()) - else - self.maxHeight = font:getHeight() - end - end + self.maxHeight = options.maxHeight or math.huge self.hFill = true self.vFill = false @@ -90,18 +65,11 @@ function Label:recalculateSizes() end self.minHeight = font:getHeight() + self.preferredWidth = font:getWidth(self.text) if self.wrap then - self.maxHeight = (#words * font:getHeight()) - else - self.maxHeight = self.minHeight - end - local totalWidth = font:getWidth(self.text) - self.width = totalWidth - self.maxWidth = math.huge - if not self.wrap then self.minWidth = maxWordWidth else - self.minWidth = totalWidth + self.minWidth = self.preferredWidth end end @@ -146,4 +114,8 @@ function Label:setMinHeightForWidth() self.minHeight = GraphicsUtil.getTextHeightForWidth(self.fontSize, self.text, self.width, self.hAlign) end +function Label:getBaseWidth() + return self.preferredWidth +end + return Label \ No newline at end of file diff --git a/client/src/ui/Layouts/AdaptiveFlexLayout.lua b/client/src/ui/Layouts/AdaptiveFlexLayout.lua index 07354d79..99b59efc 100644 --- a/client/src/ui/Layouts/AdaptiveFlexLayout.lua +++ b/client/src/ui/Layouts/AdaptiveFlexLayout.lua @@ -1,4 +1,5 @@ local PATH = (...):gsub('%.[^%.]+$', '') +local Layout = require(PATH .. ".Layout") local VerticalFlexLayout = require(PATH ..".VerticalFlexLayout") local HorizontalFlexLayout = require(PATH ..".HorizontalFlexLayout") @@ -15,18 +16,7 @@ function AdaptiveFlexLayout.resize(uiElement, width, height) end end - uiElement.layout.updateWidths(uiElement, width) - - -- transform width to height for width-to-height supporting uiElements based on the width pass - uiElement:setMinHeightForWidth() - - uiElement.layout.updateHeights(uiElement, height) - - uiElement.layout.positionChildren(uiElement) - - if uiElement.onResize then - uiElement:onResize() - end + Layout.resize(uiElement, width, height) end return AdaptiveFlexLayout \ No newline at end of file diff --git a/client/src/ui/Layouts/FlexLayout.lua b/client/src/ui/Layouts/FlexLayout.lua index 7fb79c95..15503108 100644 --- a/client/src/ui/Layouts/FlexLayout.lua +++ b/client/src/ui/Layouts/FlexLayout.lua @@ -12,7 +12,7 @@ function FlexLayout.fitSizeWidth(uiElement) end end local w = uiElement.layout.getMinWidth(uiElement) - uiElement.width = math.max(w, uiElement.minWidth) + uiElement.newWidth = math.max(w, uiElement:getBaseWidth()) end ---@param uiElement UiElement @@ -23,22 +23,32 @@ function FlexLayout.fitSizeHeight(uiElement) end end local h = uiElement.layout.getMinHeight(uiElement) - uiElement.height = math.max(h, uiElement.minHeight) + uiElement.newHeight = math.max(h, uiElement:getBaseHeight()) end function FlexLayout.updateWidths(uiElement, width) - uiElement.layout.fitSizeWidth(uiElement) + if not uiElement.newWidth then + uiElement.layout.fitSizeWidth(uiElement) + end if width then - uiElement.width = math.max(width, uiElement.width) + uiElement.width = math.max(width, uiElement.newWidth) + else + uiElement.width = uiElement.newWidth end + uiElement.newWidth = nil uiElement.layout.growChildrenWidth(uiElement) end function FlexLayout.updateHeights(uiElement, height) - uiElement.layout.fitSizeHeight(uiElement) + if not uiElement.newHeight then + uiElement.layout.fitSizeHeight(uiElement) + end if height then - uiElement.height = math.max(height, uiElement.height) + uiElement.height = math.max(height, uiElement.newHeight) + else + uiElement.height = uiElement.newHeight end + uiElement.newHeight = nil uiElement.layout.growChildrenHeight(uiElement) end diff --git a/client/src/ui/Layouts/HorizontalFlexLayout.lua b/client/src/ui/Layouts/HorizontalFlexLayout.lua index 02f8300d..5eff57eb 100644 --- a/client/src/ui/Layouts/HorizontalFlexLayout.lua +++ b/client/src/ui/Layouts/HorizontalFlexLayout.lua @@ -10,7 +10,7 @@ function HorizontalFlexLayout.getMinWidth(uiElement) for _, child in ipairs(uiElement.children) do if child.isVisible then - w = w + child.width + w = w + child.newWidth end end @@ -24,7 +24,7 @@ function HorizontalFlexLayout.getMinHeight(uiElement) for _, child in ipairs(uiElement.children) do if child.isVisible then - maxHeight = math.max(maxHeight, child.height) + maxHeight = math.max(maxHeight, child.newHeight) end end @@ -37,17 +37,17 @@ function HorizontalFlexLayout.growChildrenWidth(uiElement) return end + local remainingWidth = uiElement.width - uiElement.layout.getMinWidth(uiElement) + local growables = {} for i, child in ipairs(uiElement.children) do - if child.isVisible and child.hFill and child.width < child.maxWidth then + if child.isVisible and child.hFill and child.newWidth < child.maxWidth then growables[#growables+1] = child - child.newWidth = child.width end end if #growables > 0 then - local remainingWidth = uiElement.width - uiElement.layout.getMinWidth(uiElement) while #growables > 0 and remainingWidth > 0 do local smallest = growables[1].newWidth @@ -96,21 +96,10 @@ function HorizontalFlexLayout.growChildrenWidth(uiElement) for i = #growables, 1, -1 do local growable = growables[i] if growable.newWidth >= growable.maxWidth then - growable.width = growable.newWidth table.remove(growables, i) end end end - - for _, growable in ipairs(growables) do - growable.width = growable.newWidth - end - end - - for _, child in ipairs(uiElement.children) do - if child.layout.growChildrenWidth then - child.layout.growChildrenWidth(child) - end end end @@ -118,7 +107,7 @@ end function HorizontalFlexLayout.growChildrenHeight(uiElement) for _, child in ipairs(uiElement.children) do if child.vFill then - child.height = math.min(uiElement.height - uiElement.padding * 2, child.maxHeight) + child.newHeight = math.min(uiElement.height - uiElement.padding * 2, child.maxHeight) end if child.layout.growChildrenHeight then child.layout.growChildrenHeight(child) diff --git a/client/src/ui/Layouts/HorizontalScrollLayout.lua b/client/src/ui/Layouts/HorizontalScrollLayout.lua index 7847cc56..88cbde0a 100644 --- a/client/src/ui/Layouts/HorizontalScrollLayout.lua +++ b/client/src/ui/Layouts/HorizontalScrollLayout.lua @@ -10,9 +10,7 @@ function HorizontalScrollLayout.getMinWidth(uiElement) end function HorizontalScrollLayout.growChildrenWidth(uiElement) - for _, child in ipairs(uiElement.children) do - child.layout.growChildrenWidth(child) - end + -- no growing because we scroll in this direction end ---@param uiElement UiElement diff --git a/client/src/ui/Layouts/Layout.lua b/client/src/ui/Layouts/Layout.lua index c8c17369..bd79f1b2 100644 --- a/client/src/ui/Layouts/Layout.lua +++ b/client/src/ui/Layouts/Layout.lua @@ -12,6 +12,10 @@ function Layout.resize(uiElement, width, height) uiElement.layout.updateHeights(uiElement, height) + for i, child in ipairs(uiElement.children) do + child.layout.resize(child, child.newWidth, child.newHeight) + end + uiElement.layout.positionChildren(uiElement) if uiElement.onResize then diff --git a/client/src/ui/Layouts/VerticalFlexLayout.lua b/client/src/ui/Layouts/VerticalFlexLayout.lua index c2508c97..704e4166 100644 --- a/client/src/ui/Layouts/VerticalFlexLayout.lua +++ b/client/src/ui/Layouts/VerticalFlexLayout.lua @@ -11,7 +11,7 @@ function VerticalFlexLayout.getMinWidth(uiElement) for _, child in ipairs(uiElement.children) do if child.isVisible then - maxWidth = math.max(maxWidth, child.width) + maxWidth = math.max(maxWidth, child.newWidth) end end @@ -24,7 +24,7 @@ function VerticalFlexLayout.getMinHeight(uiElement) for _, child in ipairs(uiElement.children) do if child.isVisible then - h = h + child.height + h = h + child.newHeight end end @@ -40,9 +40,8 @@ function VerticalFlexLayout.growChildrenHeight(uiElement) local growables = {} for i, child in ipairs(uiElement.children) do - if child.isVisible and child.vFill and child.height < child.maxHeight then + if child.isVisible and child.vFill and child.newHeight < child.maxHeight then growables[#growables+1] = child - child.newHeight = child.height end end @@ -101,16 +100,6 @@ function VerticalFlexLayout.growChildrenHeight(uiElement) end end end - - for _, growable in ipairs(growables) do - growable.height = growable.newHeight - end - end - - for _, child in ipairs(uiElement.children) do - if child.layout.growChildrenHeight then - child.layout.growChildrenHeight(child) - end end end @@ -118,10 +107,7 @@ end function VerticalFlexLayout.growChildrenWidth(uiElement) for _, child in ipairs(uiElement.children) do if child.hFill then - child.width = math.min(uiElement.width - uiElement.padding * 2, child.maxWidth) - end - if child.layout.growChildrenWidth then - child.layout.growChildrenWidth(child) + child.newWidth = math.min(uiElement.width - uiElement.padding * 2, child.maxWidth) end end end @@ -161,12 +147,6 @@ function VerticalFlexLayout.positionChildren(uiElement) y = y + uiElement.childGap + child.height end end - - for _, child in ipairs(uiElement.children) do - if child.isVisible then - child.layout.positionChildren(child) - end - end end return VerticalFlexLayout \ No newline at end of file diff --git a/client/src/ui/Layouts/VerticalScrollLayout.lua b/client/src/ui/Layouts/VerticalScrollLayout.lua index 81d0a56a..62604f63 100644 --- a/client/src/ui/Layouts/VerticalScrollLayout.lua +++ b/client/src/ui/Layouts/VerticalScrollLayout.lua @@ -10,9 +10,7 @@ function VerticalScrollLayout.getMinHeight(uiElement) end function VerticalScrollLayout.growChildrenHeight(uiElement) - for _, child in ipairs(uiElement.children) do - child.layout.growChildrenHeight(child) - end + -- no growing because we scroll in this direction end ---@param uiElement UiElement diff --git a/client/src/ui/UIElement.lua b/client/src/ui/UIElement.lua index 9b28bc41..24b1ddd7 100644 --- a/client/src/ui/UIElement.lua +++ b/client/src/ui/UIElement.lua @@ -153,7 +153,7 @@ function UIElement:detach() if child.id == self.id then table.remove(self.parent.children, i) self:onDetach() - onChildrenChanged(self.parent) + self.parent:onChildrenChanged() self.parent = nil break end @@ -223,7 +223,7 @@ end function UIElement:onVisibilityChanged() if self.parent then - onChildrenChanged(self.parent) + self.parent:onChildrenChanged() end end @@ -265,4 +265,12 @@ function UIElement:setMinHeightForWidth() end end +function UIElement:getBaseWidth() + return self.minWidth +end + +function UIElement:getBaseHeight() + return self.minHeight +end + return UIElement \ No newline at end of file From 1a53bdc8587a401389e9b00c448bc89195127402 Mon Sep 17 00:00:00 2001 From: Endaris Date: Sat, 17 May 2025 17:08:45 +0200 Subject: [PATCH 12/49] add shrinking to horizontalFlexLayout --- .../src/ui/Layouts/HorizontalFlexLayout.lua | 157 +++++++++++++----- 1 file changed, 112 insertions(+), 45 deletions(-) diff --git a/client/src/ui/Layouts/HorizontalFlexLayout.lua b/client/src/ui/Layouts/HorizontalFlexLayout.lua index 5eff57eb..38a740dd 100644 --- a/client/src/ui/Layouts/HorizontalFlexLayout.lua +++ b/client/src/ui/Layouts/HorizontalFlexLayout.lua @@ -31,6 +31,9 @@ function HorizontalFlexLayout.getMinHeight(uiElement) return h + maxHeight end +local growables = {} +local shrinkables = {} + ---@param uiElement UiElement function HorizontalFlexLayout.growChildrenWidth(uiElement) if #uiElement.children == 0 then @@ -39,64 +42,128 @@ function HorizontalFlexLayout.growChildrenWidth(uiElement) local remainingWidth = uiElement.width - uiElement.layout.getMinWidth(uiElement) - local growables = {} + if remainingWidth >= 1 then + table.clear(growables) - for i, child in ipairs(uiElement.children) do - if child.isVisible and child.hFill and child.newWidth < child.maxWidth then - growables[#growables+1] = child + for i, child in ipairs(uiElement.children) do + if child.isVisible and child.hFill and child.newWidth < child.maxWidth then + growables[#growables+1] = child + end end - end - if #growables > 0 then - - while #growables > 0 and remainingWidth > 0 do - local smallest = growables[1].newWidth - local secondSmallest = math.huge - -- if growables[1] is already the smallest, it will increment the counter at some point within the loop so we can't count it yet - local smallestCount = 1 - - for i = 2, #growables do - local growable = growables[i] - if growable.newWidth < smallest then - secondSmallest = smallest - smallest = growable.newWidth - smallestCount = 1 - elseif growable.newWidth > smallest then - secondSmallest = math.min(secondSmallest, growable.newWidth) + if #growables > 0 then + + while #growables > 0 and remainingWidth > 0 do + local smallest = growables[1].newWidth + local secondSmallest = math.huge + -- if growables[1] is already the smallest, it will increment the counter at some point within the loop so we can't count it yet + local smallestCount = 1 + + for i = 2, #growables do + local growable = growables[i] + if growable.newWidth < smallest then + secondSmallest = smallest + smallest = growable.newWidth + smallestCount = 1 + elseif growable.newWidth > smallest then + secondSmallest = math.min(secondSmallest, growable.newWidth) + else + smallestCount = smallestCount + 1 + end + end + + local delta = secondSmallest - smallest + local widthToAdd = math.min(delta, remainingWidth / smallestCount) + + if delta * smallestCount >= remainingWidth then + for _, growable in ipairs(growables) do + if growable.newWidth == smallest then + local toAdd = math.min(widthToAdd, growable.maxWidth - growable.newWidth) + growable.newWidth = growable.newWidth + toAdd + remainingWidth = remainingWidth - toAdd + end + end + if remainingWidth < 1 then + -- we could run into floating point shenanigans here + remainingWidth = 0 + end else - smallestCount = smallestCount + 1 + for _, growable in ipairs(growables) do + if growable.newWidth == smallest then + local toAdd = math.min(widthToAdd, growable.maxWidth - growable.newWidth) + growable.newWidth = growable.newWidth + toAdd + remainingWidth = remainingWidth - toAdd + end + end + end + + for i = #growables, 1, -1 do + local growable = growables[i] + if growable.newWidth >= growable.maxWidth then + table.remove(growables, i) + end end end + end + elseif remainingWidth <= 1 then + table.clear(shrinkables) - local delta = secondSmallest - smallest - local widthToAdd = math.min(delta, remainingWidth / smallestCount) + for i, child in ipairs(uiElement.children) do + if child.isVisible and child.hFill and child.newWidth > child.minWidth then + shrinkables[#shrinkables+1] = child + end + end - if delta * smallestCount >= remainingWidth then - for _, growable in ipairs(growables) do - if growable.newWidth == smallest then - local toAdd = math.min(widthToAdd, growable.maxWidth - growable.newWidth) - growable.newWidth = growable.newWidth + toAdd - remainingWidth = remainingWidth - toAdd + if #shrinkables > 0 then + while #shrinkables > 0 and remainingWidth < 0 do + local biggest = shrinkables[1].newWidth + local secondBiggest = 0 + -- if growables[1] is already the smallest, it will increment the counter at some point within the loop so we can't count it yet + local biggestCount = 1 + + for i = 2, #shrinkables do + local shrinkable = shrinkables[i] + if shrinkable.newWidth > biggest then + secondBiggest = biggest + biggest = shrinkable.newWidth + biggestCount = 1 + elseif shrinkable.newWidth < biggest then + secondBiggest = math.max(secondBiggest, shrinkable.newWidth) + else + biggestCount = biggestCount + 1 end end - if remainingWidth < 1 then - -- we could run into floating point shenanigans here - remainingWidth = 0 - end - else - for _, growable in ipairs(growables) do - if growable.newWidth == smallest then - local toAdd = math.min(widthToAdd, growable.maxWidth - growable.newWidth) - growable.newWidth = growable.newWidth + toAdd - remainingWidth = remainingWidth - toAdd + + local delta = secondBiggest - biggest + local widthToSubtract = math.min(delta, math.abs(remainingWidth / biggestCount)) + + if delta * biggestCount >= -remainingWidth then + for _, shrinkable in ipairs(shrinkables) do + if shrinkable.newWidth == biggest then + local toSubtract = math.min(widthToSubtract, shrinkable.newWidth - shrinkable.minWidth) + shrinkable.newWidth = shrinkable.newWidth - toSubtract + remainingWidth = remainingWidth + toSubtract + end + end + if remainingWidth < 1 then + -- we could run into floating point shenanigans here + remainingWidth = 0 + end + else + for _, shrinkable in ipairs(shrinkables) do + if shrinkable.newWidth == biggest then + local toSubtract = math.min(widthToSubtract, shrinkable.newWidth - shrinkable.minWidth) + shrinkable.newWidth = shrinkable.newWidth - toSubtract + remainingWidth = remainingWidth + toSubtract + end end end - end - for i = #growables, 1, -1 do - local growable = growables[i] - if growable.newWidth >= growable.maxWidth then - table.remove(growables, i) + for i = #shrinkables, 1, -1 do + local shrinkable = shrinkables[i] + if shrinkable.newWidth <= shrinkable.minWidth then + table.remove(shrinkables, i) + end end end end From 1493e6f776f7b66b58dbe1aa0eba6dcb2ecb7f2a Mon Sep 17 00:00:00 2001 From: Endaris Date: Sat, 17 May 2025 22:02:41 +0200 Subject: [PATCH 13/49] abandon fixed canvas resolution, embrace dynamic window size --- client/src/ClientMatch.lua | 17 +- client/src/ClientStack.lua | 4 +- client/src/Game.lua | 36 +--- client/src/PlayerStack.lua | 21 +-- client/src/RunTimeGraph.lua | 4 +- client/src/graphics/graphics_util.lua | 2 +- client/src/mods/Stage.lua | 2 +- client/src/mods/StageLoader.lua | 10 +- client/src/mods/Theme.lua | 21 +-- client/src/scenes/ChallengeModeRecapScene.lua | 10 +- client/src/scenes/DesignHelper.lua | 168 +++++++++++++++--- client/src/scenes/Game1pChallenge.lua | 6 +- client/src/scenes/GameBase.lua | 2 +- client/src/scenes/GameCatchUp.lua | 7 +- client/src/scenes/MainMenu.lua | 7 +- client/src/scenes/PortraitGame.lua | 12 +- client/src/scenes/ReplayGame.lua | 2 +- client/src/scenes/Scene.lua | 6 +- client/src/scenes/StartUp.lua | 4 +- client/src/scenes/TitleScreen.lua | 6 +- .../Transitions/BlackFadeTransition.lua | 2 +- client/src/scenes/Transitions/Transition.lua | 2 +- client/src/ui/ImageContainer.lua | 6 +- client/src/ui/Label.lua | 6 +- .../src/ui/Layouts/HorizontalFlexLayout.lua | 6 - .../src/ui/Layouts/HorizontalWrapLayout.lua | 71 ++++++++ client/src/ui/Layouts/Layout.lua | 2 +- client/src/ui/Layouts/VerticalFlexLayout.lua | 3 +- client/src/ui/ScrollContainer.lua | 10 +- client/src/ui/UIElement.lua | 43 ++++- client/src/ui/init.lua | 1 + 31 files changed, 355 insertions(+), 144 deletions(-) create mode 100644 client/src/ui/Layouts/HorizontalWrapLayout.lua diff --git a/client/src/ClientMatch.lua b/client/src/ClientMatch.lua index ed3d9c1e..c94aa18a 100644 --- a/client/src/ClientMatch.lua +++ b/client/src/ClientMatch.lua @@ -562,7 +562,7 @@ end function ClientMatch:drawCommunityMessage() -- Draw the community message if not config.debug_mode then - GraphicsUtil.printf(join_community_msg or "", 0, 668, consts.CANVAS_WIDTH, "center") + GraphicsUtil.printf(join_community_msg or "", 0, 668, love.graphics.getWidth(), "center") end end @@ -593,8 +593,8 @@ function ClientMatch:render() -- let the spectator know the game is about to die local iconSize = 60 local icon_width, icon_height = themes[config.theme].images.IMG_bug:getDimensions() - local x = (consts.CANVAS_WIDTH / 2) - (iconSize / 2) - local y = (consts.CANVAS_HEIGHT / 2) - (iconSize / 2) + local x = (love.graphics.getWidth() / 2) - (iconSize / 2) + local y = (love.graphics.getHeight() / 2) - (iconSize / 2) GraphicsUtil.draw(themes[config.theme].images.IMG_bug, x, y, 0, iconSize / icon_width, iconSize / icon_height) end end @@ -660,20 +660,21 @@ end -- Draw the pause menu function ClientMatch:draw_pause() + local width, height = love.graphics.getDimensions() if not self.renderDuringPause then local image = themes[config.theme].images.pause - local scale = consts.CANVAS_WIDTH / math.max(image:getWidth(), image:getHeight()) -- keep image ratio + local scale = width / math.max(image:getWidth(), image:getHeight()) -- keep image ratio -- adjust coordinates to be centered - local x = consts.CANVAS_WIDTH / 2 - local y = consts.CANVAS_HEIGHT / 2 + local x = width / 2 + local y = height / 2 local xOffset = math.floor(image:getWidth() * 0.5) local yOffset = math.floor(image:getHeight() * 0.5) GraphicsUtil.draw(image, x, y, 0, scale, scale, xOffset, yOffset) end local y = 260 - GraphicsUtil.printf(loc("pause"), 0, y, consts.CANVAS_WIDTH, "center", nil, 1, "big") - GraphicsUtil.printf(loc("pl_pause_help"), 0, y + 30, consts.CANVAS_WIDTH, "center", nil, 1) + GraphicsUtil.printf(loc("pause"), 0, y, width, "center", nil, 1, "big") + GraphicsUtil.printf(loc("pl_pause_help"), 0, y + 30, width, "center", nil, 1) end function ClientMatch:getWinners() diff --git a/client/src/ClientStack.lua b/client/src/ClientStack.lua index 0eee1290..58d0bf0e 100644 --- a/client/src/ClientStack.lua +++ b/client/src/ClientStack.lua @@ -211,7 +211,7 @@ function ClientStack:drawString(str, themePositionOffset, cameFromLegacyScoreOff local x = self:elementOriginXWithOffset(themePositionOffset, cameFromLegacyScoreOffset) local y = self:elementOriginYWithOffset(themePositionOffset, cameFromLegacyScoreOffset) - local limit = consts.CANVAS_WIDTH - x + local limit = love.graphics.getWidth() - x local alignment = "left" if themes[config.theme]:offsetsAreFixed() then if self.renderIndex == 1 then @@ -235,7 +235,7 @@ function ClientStack:moveForRenderIndex(renderIndex) self.mirror_x = -1 self.multiplication = 1 end - local centerX = (GAME.globalCanvas:getWidth() / 2) + local centerX = love.graphics.getWidth() / 2 local stackWidth = self:canvasWidth() local innerStackXMovement = 100 local outerStackXMovement = stackWidth + innerStackXMovement diff --git a/client/src/Game.lua b/client/src/Game.lua index 65c15343..db0b6456 100644 --- a/client/src/Game.lua +++ b/client/src/Game.lua @@ -40,7 +40,6 @@ end ---@class PanelAttack ---@field netClient NetClient ---@field battleRoom BattleRoom? ----@field globalCanvas love.Canvas ---@field muteSound boolean ---@field rich_presence table ---@field input table @@ -65,7 +64,7 @@ local Game = class( self.puzzleSets = {} -- all the puzzles loaded into the game self.netClient = NetClient() self.server_queue = ServerQueue() - self.main_menu_screen_pos = {consts.CANVAS_WIDTH / 2 - 108 + 50, consts.CANVAS_HEIGHT / 2 - 111} + self.main_menu_screen_pos = {love.graphics.getWidth() / 2 - 108 + 50, love.graphics.getHeight() / 2 - 111} self.config = config self.localization = Localization self.replay = {} @@ -80,9 +79,6 @@ local Game = class( self.canvasYScale = 1 self.backgroundColor = { 0.0, 0.0, 0.0 } - -- depends on canvasXScale - self.globalCanvas = love.graphics.newCanvas(consts.CANVAS_WIDTH, consts.CANVAS_HEIGHT, {dpiscale=newCanvasSnappedScale(self)}) - self.automaticScales = {1, 1.5, 2, 2.5, 3} -- specifies a time that is compared against self.timer to determine if GameScale should be shown self.showGameScaleUntil = 0 @@ -127,7 +123,6 @@ function Game:load() self.navigationStack = require("client.src.NavigationStack") self.navigationStack:push(StartUp({setupRoutine = self.setupRoutine})) - self.globalCanvas = love.graphics.newCanvas(consts.CANVAS_WIDTH, consts.CANVAS_HEIGHT, {dpiscale=GAME:newCanvasSnappedScale()}) end local function detectHardwareProblems() @@ -393,25 +388,13 @@ function Game:update(dt) end function Game:draw() - -- Setting the canvas means everything we draw is drawn to the canvas instead of the screen - love.graphics.setCanvas({self.globalCanvas, stencil = true}) love.graphics.setBackgroundColor(unpack(self.backgroundColor)) love.graphics.clear() - -- With this, self.globalCanvas is clear and set as our active canvas everything is being drawn to self.navigationStack:draw() self:drawFPS() self:drawScaleInfo() - - -- resetting the canvas means everything we draw is drawn to the screen - love.graphics.setCanvas() - - love.graphics.setBlendMode("alpha", "premultiplied") - -- now we draw the finished canvas at scale - -- this way we don't have to worry about scaling singular elements, just draw everything at 1280x720 to the canvas - love.graphics.draw(self.globalCanvas, self.canvasX, self.canvasY, 0, self.canvasXScale, self.canvasYScale, self.globalCanvas:getWidth() / 2, self.globalCanvas:getHeight() / 2) - love.graphics.setBlendMode("alpha", "alphamultiply") end function Game:drawFPS() @@ -423,10 +406,10 @@ end function Game:drawScaleInfo() if self.showGameScaleUntil > self.timer then - local scaleString = "Scale: " .. self.canvasXScale .. " (" .. consts.CANVAS_WIDTH * self.canvasXScale .. " x " .. consts.CANVAS_HEIGHT * self.canvasYScale .. ")" + local scaleString = "Scale: " .. self.canvasXScale .. " (" .. love.graphics.getWidth() * self.canvasXScale .. " x " .. love.graphics.getHeight() * self.canvasYScale .. ")" local newPixelWidth = love.graphics.getWidth() - if consts.CANVAS_WIDTH * self.canvasXScale > newPixelWidth then + if love.graphics.getWidth() * self.canvasXScale > newPixelWidth then scaleString = scaleString .. " Clipped " end love.graphics.printf(scaleString, GraphicsUtil.getGlobalFontWithSize("huge"), 5, 5, 2000, "left") @@ -572,7 +555,7 @@ function Game:updateCanvasPositionAndScale(newWindowWidth, newWindowHeight) if config.gameScaleType == "fit" then local w, h - local canvasWidth, canvasHeight = self.globalCanvas:getDimensions() + local canvasWidth, canvasHeight = love.graphics.getDimensions() if newWindowHeight / canvasHeight > newWindowWidth / canvasWidth then w = newWindowWidth h = canvasHeight * newWindowWidth / canvasWidth @@ -591,7 +574,7 @@ function Game:updateCanvasPositionAndScale(newWindowWidth, newWindowHeight) local newScale = 0.5 for i= #availableScales, 1, -1 do local scale = availableScales[i] - if (newWindowWidth >= self.globalCanvas:getWidth() * scale and newWindowHeight >= self.globalCanvas:getHeight() * scale) then + if (newWindowWidth >= love.graphics.getWidth() * scale and newWindowHeight >= love.graphics.getHeight() * scale) then newScale = scale break end @@ -612,7 +595,6 @@ function Game:refreshCanvasAndImagesForNewScale() self:drawLoadingString(loc("ld_characters")) coroutine.yield() - self.globalCanvas = love.graphics.newCanvas(GAME.globalCanvas:getWidth(), GAME.globalCanvas:getHeight(), {dpiscale=self:newCanvasSnappedScale()}) -- We need to reload all assets and fonts to get the new scaling info and filters -- Reload theme to get the new resolution assets @@ -631,7 +613,7 @@ end -- Transform from window coordinates to game coordinates function Game:transform_coordinates(x, y) - local newX, newY = (x - self.canvasX) / self.canvasXScale + self.globalCanvas:getWidth() / 2, (y - self.canvasY) / self.canvasYScale + self.globalCanvas:getHeight() / 2 + local newX, newY = (x - self.canvasX) / self.canvasXScale + love.graphics.getWidth() / 2, (y - self.canvasY) / self.canvasYScale + love.graphics.getHeight() / 2 return newX, newY end @@ -640,10 +622,10 @@ function Game:drawLoadingString(loadingString) local textMaxWidth = 300 local textHeight = 40 local x = 0 - local y = consts.CANVAS_HEIGHT/2 - textHeight/2 + local y = love.graphics.getHeight()/2 - textHeight/2 local backgroundPadding = 10 - GraphicsUtil.drawRectangle("fill", consts.CANVAS_WIDTH / 2 - (textMaxWidth / 2) , y - backgroundPadding, textMaxWidth, textHeight, 0, 0, 0, 0.5) - GraphicsUtil.printf(loadingString, x, y, consts.CANVAS_WIDTH, "center", nil, nil, "big") + GraphicsUtil.drawRectangle("fill", love.graphics.getWidth() / 2 - (textMaxWidth / 2) , y - backgroundPadding, textMaxWidth, textHeight, 0, 0, 0, 0.5) + GraphicsUtil.printf(loadingString, x, y, love.graphics.getWidth(), "center", nil, nil, "big") end function Game:setLanguage(lang_code) diff --git a/client/src/PlayerStack.lua b/client/src/PlayerStack.lua index a388116a..1e00ea6e 100644 --- a/client/src/PlayerStack.lua +++ b/client/src/PlayerStack.lua @@ -1101,9 +1101,10 @@ function PlayerStack:drawAnalyticData() local paddingToAnalytics = 16 local width = 160 local height = 600 + local screenWidth = love.graphics.getWidth() local x = paddingToAnalytics + backgroundPadding if self.renderIndex == 2 then - x = consts.CANVAS_WIDTH - paddingToAnalytics - width + backgroundPadding + x = screenWidth - paddingToAnalytics - width + backgroundPadding end local y = self.frameOriginY * self.gfxScale + backgroundPadding @@ -1123,7 +1124,7 @@ function PlayerStack:drawAnalyticData() -- Panels cleared panels[self.panels_dir]:drawPanelFrame(1, "face", x, y, iconSize) - GraphicsUtil.printf(analytic.data.destroyed_panels, x + iconToTextSpacing, y - 2, consts.CANVAS_WIDTH, "left", nil, 1) + GraphicsUtil.printf(analytic.data.destroyed_panels, x + iconToTextSpacing, y - 2, screenWidth, "left", nil, 1) y = y + nextIconIncrement @@ -1132,21 +1133,21 @@ function PlayerStack:drawAnalyticData() -- Garbage sent icon_width, icon_height = self.character.images.face:getDimensions() GraphicsUtil.draw(self.character.images.face, x, y, 0, iconSize / icon_width, iconSize / icon_height) - GraphicsUtil.printf(analytic.data.sent_garbage_lines, x + iconToTextSpacing, y - 2, consts.CANVAS_WIDTH, "left", nil, 1) + GraphicsUtil.printf(analytic.data.sent_garbage_lines, x + iconToTextSpacing, y - 2, screenWidth, "left", nil, 1) y = y + nextIconIncrement -- GPM icon_width, icon_height = self.theme.images.IMG_gpm:getDimensions() GraphicsUtil.draw(self.theme.images.IMG_gpm, x, y, 0, iconSize / icon_width, iconSize / icon_height) - GraphicsUtil.printf(analytic.lastGPM .. "/m", x + iconToTextSpacing, y - 2, consts.CANVAS_WIDTH, "left", nil, 1) + GraphicsUtil.printf(analytic.lastGPM .. "/m", x + iconToTextSpacing, y - 2, screenWidth, "left", nil, 1) y = y + nextIconIncrement -- Moves icon_width, icon_height = self.theme.images.IMG_cursorCount:getDimensions() GraphicsUtil.draw(self.theme.images.IMG_cursorCount, x, y, 0, iconSize / icon_width, iconSize / icon_height) - GraphicsUtil.printf(analytic.data.move_count, x + iconToTextSpacing, y - 2, consts.CANVAS_WIDTH, "left", nil, 1) + GraphicsUtil.printf(analytic.data.move_count, x + iconToTextSpacing, y - 2, screenWidth, "left", nil, 1) y = y + nextIconIncrement @@ -1155,7 +1156,7 @@ function PlayerStack:drawAnalyticData() icon_width, icon_height = self.theme.images.IMG_swap:getDimensions() GraphicsUtil.draw(self.theme.images.IMG_swap, x, y, 0, iconSize / icon_width, iconSize / icon_height) end - GraphicsUtil.printf(analytic.data.swap_count, x + iconToTextSpacing, y - 2, consts.CANVAS_WIDTH, "left", nil, 1) + GraphicsUtil.printf(analytic.data.swap_count, x + iconToTextSpacing, y - 2, screenWidth, "left", nil, 1) y = y + nextIconIncrement @@ -1164,7 +1165,7 @@ function PlayerStack:drawAnalyticData() icon_width, icon_height = self.theme.images.IMG_apm:getDimensions() GraphicsUtil.draw(self.theme.images.IMG_apm, x, y, 0, iconSize / icon_width, iconSize / icon_height) end - GraphicsUtil.printf(analytic.lastAPM .. "/m", x + iconToTextSpacing, y - 2, consts.CANVAS_WIDTH, "left", nil, 1) + GraphicsUtil.printf(analytic.lastAPM .. "/m", x + iconToTextSpacing, y - 2, screenWidth, "left", nil, 1) y = y + nextIconIncrement @@ -1182,7 +1183,7 @@ function PlayerStack:drawAnalyticData() if cardImage then icon_width, icon_height = cardImage:getDimensions() GraphicsUtil.draw(cardImage, x, y, 0, iconSize / icon_width, iconSize / icon_height) - GraphicsUtil.printf(analytic.data.reached_chains[i], x + iconToTextSpacing, y - 2, consts.CANVAS_WIDTH, "left", nil, 1) + GraphicsUtil.printf(analytic.data.reached_chains[i], x + iconToTextSpacing, y - 2, screenWidth, "left", nil, 1) y = y + nextIconIncrement end end @@ -1191,7 +1192,7 @@ function PlayerStack:drawAnalyticData() if chainCountAboveLimit > 0 then local cardImage = self.theme:chainImage(0) GraphicsUtil.draw(cardImage, x, y, 0, iconSize / icon_width, iconSize / icon_height) - GraphicsUtil.printf(chainCountAboveLimit, x + iconToTextSpacing, y - 2, consts.CANVAS_WIDTH, "left", nil, 1) + GraphicsUtil.printf(chainCountAboveLimit, x + iconToTextSpacing, y - 2, screenWidth, "left", nil, 1) end -- Draw the combo images @@ -1203,7 +1204,7 @@ function PlayerStack:drawAnalyticData() if cardImage then icon_width, icon_height = cardImage:getDimensions() GraphicsUtil.draw(cardImage, xCombo, yCombo, 0, iconSize / icon_width, iconSize / icon_height) - GraphicsUtil.printf(analytic.data.used_combos[i], xCombo + iconToTextSpacing, yCombo - 2, consts.CANVAS_WIDTH, "left", nil, 1) + GraphicsUtil.printf(analytic.data.used_combos[i], xCombo + iconToTextSpacing, yCombo - 2, screenWidth, "left", nil, 1) yCombo = yCombo + nextIconIncrement end end diff --git a/client/src/RunTimeGraph.lua b/client/src/RunTimeGraph.lua index a01c84b0..bcb6c1fa 100644 --- a/client/src/RunTimeGraph.lua +++ b/client/src/RunTimeGraph.lua @@ -7,7 +7,7 @@ local RunTimeGraph = class(function(self) local valueCount = 60 local width = valueCount * 8 local height = 50 - local x = consts.CANVAS_WIDTH - width + local x = love.graphics.getWidth() - width local y = 4 local padding = 80 self.graphs = {} @@ -76,7 +76,7 @@ function RunTimeGraph:draw() -- in order to not sully the draw data of the actual game, the RunTimeGraph is drawn separately -- these transformations assure it uses the same game coordinates as love.draw - love.graphics.translate(GAME.canvasX - GAME.globalCanvas:getWidth() / 2 * GAME.canvasXScale, GAME.canvasY - GAME.globalCanvas:getHeight() / 2 * GAME.canvasYScale) + love.graphics.translate(GAME.canvasX - love.graphics.getWidth() / 2 * GAME.canvasXScale, GAME.canvasY - love.graphics.getHeight() / 2 * GAME.canvasYScale) love.graphics.scale(GAME.canvasXScale, GAME.canvasYScale) BarGraph.drawGraphs(self.graphs) diff --git a/client/src/graphics/graphics_util.lua b/client/src/graphics/graphics_util.lua index 5d3e2d5f..0a65742e 100644 --- a/client/src/graphics/graphics_util.lua +++ b/client/src/graphics/graphics_util.lua @@ -341,7 +341,7 @@ function GraphicsUtil.printf(str, x, y, limit, halign, color, scale, fontSize) y = y or 0 scale = scale or 1 color = color or nil - limit = limit or consts.CANVAS_WIDTH + limit = limit or love.graphics.getWidth() fontSize = fontSize or "normal" halign = halign or "left" GraphicsUtil.setColor(0, 0, 0, 1) diff --git a/client/src/mods/Stage.lua b/client/src/mods/Stage.lua index 27832e8a..9de4a318 100644 --- a/client/src/mods/Stage.lua +++ b/client/src/mods/Stage.lua @@ -129,7 +129,7 @@ function stages_reload_graphics() stages[match.stageId]:graphics_init(true, false) -- for reasons, this is not drawn directly from the stage but from background image -- so override this while in a match - GAME.backgroundImage = UpdatingImage(stages[match.stageId].images.background, false, 0, 0, consts.CANVAS_WIDTH, consts.CANVAS_HEIGHT) + GAME.backgroundImage = UpdatingImage(stages[match.stageId].images.background, false, 0, 0, love.graphics.getDimensions()) end end end diff --git a/client/src/mods/StageLoader.lua b/client/src/mods/StageLoader.lua index 3850d2d3..b04b65e1 100644 --- a/client/src/mods/StageLoader.lua +++ b/client/src/mods/StageLoader.lua @@ -8,7 +8,15 @@ local StageLoader = {} -- initializes the stage class function StageLoader.initStages() - allStages, stageIds, stages, visibleStages = ModLoader.initMods(Stage) + local all, ids, filtered, visible = ModLoader.initMods(Stage) + ---@type table + allStages = all + ---@type string[] + stageIds = ids + ---@type table + stages = filtered +---@type string[] + visibleStages = visible StageLoader.loadBundleThumbnails() end diff --git a/client/src/mods/Theme.lua b/client/src/mods/Theme.lua index 2dda2a22..aecc64ae 100644 --- a/client/src/mods/Theme.lua +++ b/client/src/mods/Theme.lua @@ -313,17 +313,18 @@ function Theme:loadFont() end function Theme:loadMenuGraphics() - self.images.bg_main = UpdatingImage(self:load_theme_img("background/main"), self.bg_main_is_tiled, self.bg_main_speed_x, self.bg_main_speed_y, consts.CANVAS_WIDTH, consts.CANVAS_HEIGHT) + local width, height = love.graphics.getDimensions() + self.images.bg_main = UpdatingImage(self:load_theme_img("background/main"), self.bg_main_is_tiled, self.bg_main_speed_x, self.bg_main_speed_y, width, height) self:loadFont() local titleImage = self:load_theme_img("background/title", false) if titleImage then - self.images.bg_title = UpdatingImage(titleImage, self.bg_title_is_tiled, self.bg_title_speed_x, self.bg_title_speed_y, consts.CANVAS_WIDTH, consts.CANVAS_HEIGHT) + self.images.bg_title = UpdatingImage(titleImage, self.bg_title_is_tiled, self.bg_title_speed_x, self.bg_title_speed_y, width, height) end - self.images.bg_select_screen = UpdatingImage(self:load_theme_img("background/select_screen"), self.bg_select_screen_is_tiled, self.bg_select_screen_speed_x, self.bg_select_screen_speed_y, consts.CANVAS_WIDTH, consts.CANVAS_HEIGHT) - self.images.bg_readme = UpdatingImage(self:load_theme_img("background/readme"), self.bg_readme_is_tiled, self.bg_readme_speed_x, self.bg_readme_speed_y, consts.CANVAS_WIDTH, consts.CANVAS_HEIGHT) + self.images.bg_select_screen = UpdatingImage(self:load_theme_img("background/select_screen"), self.bg_select_screen_is_tiled, self.bg_select_screen_speed_x, self.bg_select_screen_speed_y, width, height) + self.images.bg_readme = UpdatingImage(self:load_theme_img("background/readme"), self.bg_readme_is_tiled, self.bg_readme_speed_x, self.bg_readme_speed_y, width, height) self.images.IMG_bug = self:load_theme_img("bug") end @@ -393,6 +394,7 @@ function Theme:loadSelectionGraphics() end function Theme:loadIngameGraphics() + local width, height = love.graphics.getDimensions() local bgOverlay = self:load_theme_img("background/bg_overlay") local fgOverlay = self:load_theme_img("background/fg_overlay") if bgOverlay then @@ -400,8 +402,8 @@ function Theme:loadIngameGraphics() image = bgOverlay, hAlign = "center", vAlign = "center", - width = consts.CANVAS_WIDTH, - height = consts.CANVAS_HEIGHT + width = width, + height = height }) end @@ -410,8 +412,8 @@ function Theme:loadIngameGraphics() image = fgOverlay, hAlign = "center", vAlign = "center", - width = consts.CANVAS_WIDTH, - height = consts.CANVAS_HEIGHT + width = width, + height = height }) end @@ -774,12 +776,11 @@ function Theme:final_init() if self.images.bg_title then menuYPadding = 100 self.main_menu_screen_pos = {532, menuYPadding} - self.main_menu_y_max = consts.CANVAS_HEIGHT - menuYPadding else self.main_menu_screen_pos = {532, 249} - self.main_menu_y_max = consts.CANVAS_HEIGHT - menuYPadding self.centerMenusVertically = false end + self.main_menu_y_max = love.graphics.getHeight() - menuYPadding self.main_menu_max_height = (self.main_menu_y_max - self.main_menu_screen_pos[2]) self.main_menu_y_center = self.main_menu_screen_pos[2] + (self.main_menu_max_height / 2) diff --git a/client/src/scenes/ChallengeModeRecapScene.lua b/client/src/scenes/ChallengeModeRecapScene.lua index e7bc8e3d..9a60212a 100644 --- a/client/src/scenes/ChallengeModeRecapScene.lua +++ b/client/src/scenes/ChallengeModeRecapScene.lua @@ -20,7 +20,7 @@ local ChallengeModeRecapScene = class( function(self, sceneParams) self.backgroundImg = GAME.theme.images.bg_main self.challengeMode = sceneParams.challengeMode - self.timeSplitElement = ChallengeModeTimeSplitsUIElement({x = consts.CANVAS_WIDTH / 2, y = 200}, self.challengeMode) + self.timeSplitElement = ChallengeModeTimeSplitsUIElement({x = love.graphics.getWidth() / 2, y = 200}, self.challengeMode) self.uiRoot:addChild(self.timeSplitElement) self.recapStartTime = love.timer.getTime() self.minDisplayTime = 2 -- the minimum amount of seconds the scene will be displayed for @@ -48,11 +48,11 @@ end function ChallengeModeRecapScene:draw() self.backgroundImg:draw() - - local drawX = consts.CANVAS_WIDTH / 2 + local width, height = love.graphics.getDimensions() + local drawX = width / 2 local drawY = 20 - local limit = consts.CANVAS_WIDTH + local limit = width local message = "Congratulations!\n You beat " .. self.challengeMode.difficultyName .. "!" GraphicsUtil.printf(message, 0, drawY, limit, "center", nil, nil, "gigantic") self.uiRoot:draw() @@ -64,7 +64,7 @@ function ChallengeModeRecapScene:draw() GraphicsUtil.printf(self.challengeMode.continues, drawX - limit / 2, drawY, limit, "center", nil, nil, "medium") local font = GraphicsUtil.getGlobalFont() - GraphicsUtil.print(loc("continue_button"), (consts.CANVAS_WIDTH - font:getWidth(loc("continue_button"))) / 2, consts.CANVAS_HEIGHT - 60) + GraphicsUtil.print(loc("continue_button"), (width - font:getWidth(loc("continue_button"))) / 2, height - 60) end return ChallengeModeRecapScene diff --git a/client/src/scenes/DesignHelper.lua b/client/src/scenes/DesignHelper.lua index 2101e8cd..d7eaaa94 100644 --- a/client/src/scenes/DesignHelper.lua +++ b/client/src/scenes/DesignHelper.lua @@ -2,79 +2,189 @@ local class = require("common.lib.class") local Scene = require("client.src.scenes.Scene") local ui = require("client.src.ui") local input = require("client.src.inputManager") +local prof = require("common.lib.zoneProfiler") local DesignHelper = class(function(self, sceneParams) - --self:load(sceneParams) + self:load(sceneParams) end, Scene) DesignHelper.name = "DesignHelper" function DesignHelper:load() self.uiRoot.layout = ui.Layouts.VerticalFlexLayout + self.uiRoot.childGap = 8 - local roomMode = ui.ButtonGroup({ + local roomMode = ui.UiElement({ childGap = 8, padding = 8, hAlign = "center", --hFill = true, backgroundColor = {1, 0, 0, 0.5}, + layout = ui.Layouts.HorizontalFlexLayout, }) - roomMode:addChild(ui.Text({text = "Battle", hAlign = "center", vAlign = "center"})) - roomMode:addChild(ui.Text({text = "Arcade", hAlign = "center", vAlign = "center"})) + roomMode:addChild(ui.Label({text = "Battle", hAlign = "center", vAlign = "center"})) + roomMode:addChild(ui.Label({text = "Arcade", hAlign = "center", vAlign = "center"})) self.uiRoot:addChild(roomMode) - local gameMode = ui.HorizontalRadioSelector({ + local gameMode = ui.UiElement({ childGap = 8, padding = 8, backgroundColor = {0, 0, 1, 0.5}, hAlign = "center", + layout = ui.Layouts.HorizontalFlexLayout, -- hFill = true, }) - gameMode:addChild(ui.Text({text = "VS", wrap = false})) - gameMode:addChild(ui.Text({text = "VS Self", wrap = false})) - gameMode:addChild(ui.Text({text = "Time Attack", wrap = false})) - gameMode:addChild(ui.Text({text = "Endless", wrap = false})) - gameMode:addChild(ui.Text({text = "Puzzle", wrap = false})) - gameMode:addChild(ui.Text({text = "Training", wrap = false})) - gameMode:addChild(ui.Text({text = "Line Clear", wrap = false})) + gameMode:addChild(ui.Label({text = "VS"})) + gameMode:addChild(ui.Label({text = "VS Self"})) + gameMode:addChild(ui.Label({text = "Time Attack"})) + gameMode:addChild(ui.Label({text = "Endless"})) + gameMode:addChild(ui.Label({text = "Puzzle"})) + gameMode:addChild(ui.Label({text = "Training"})) + gameMode:addChild(ui.Label({text = "Line Clear"})) self.uiRoot:addChild(gameMode) - local subSelectionSelector = ui.UIElement({ - childGap = 8, + local subSelectionSelector = ui.ScrollContainer({ + childGap = 32, padding = 8, hFill = true, - backgroundColor = {0, 1, 0, 0.5} + backgroundColor = {0, 1, 0, 0.5}, + scrollOrientation = "horizontal", + }) + + local characterButton = ui.Button({ + hAlign = "center", + vAlign = "center", + minWidth = 64, + minHeight = 64, + backgroundColor = {1, 1, 1, 0}, + }) + local characterImage = ui.ImageContainer({ + image = characters[GAME.localPlayer.settings.selectedCharacterId].images.icon, + drawBorders = true, + outlineColor = {1, 1, 1, 1}, + hFill = true, + vFill = true, + }) + characterButton:addChild(characterImage) + local characterSelectionSelector = ui.UiElement({ + layout = ui.Layouts.VerticalFlexLayout, + vAlign = "center", + vFill = true, + }) + characterSelectionSelector:addChild(characterButton) + characterSelectionSelector:addChild(ui.Label({id = "character", hAlign = "center", vAlign = "bottom"})) + + local stageSelectionSelector = ui.UiElement({ + layout = ui.Layouts.VerticalFlexLayout, + vAlign = "center", + vFill = true, + }) + local stageButton = ui.Button({ + hAlign = "center", + vAlign = "center", + minWidth = 64, + minHeight = 64, + backgroundColor = {1, 1, 1, 0}, + }) + local stageImage = ui.ImageContainer({ + image = stages[GAME.localPlayer.settings.selectedStageId].images.thumbnail, + drawBorders = true, + outlineColor = {1, 1, 1, 1}, + hFill = true, + vFill = true, }) + stageButton:addChild(stageImage) + stageSelectionSelector:addChild(stageButton) + stageSelectionSelector:addChild(ui.Label({id = "stage", hAlign = "center", vAlign = "bottom"})) - subSelectionSelector:addChild(ui.Text({text = "Character", wrap = false})) - subSelectionSelector:addChild(ui.Text({text = "Stage", wrap = false})) - subSelectionSelector:addChild(ui.Text({text = "Panels", wrap = false})) - subSelectionSelector:addChild(ui.Text({text = "Ranked", wrap = false})) - subSelectionSelector:addChild(ui.Text({text = "Level", wrap = false})) - subSelectionSelector:addChild(ui.Text({text = "Input Selection", wrap = false})) - subSelectionSelector:addChild(ui.Text({text = "Puzzle", wrap = false})) - subSelectionSelector:addChild(ui.Text({text = "Attack File", wrap = false})) + + local panelSelectionSelector = ui.UiElement({ + layout = ui.Layouts.VerticalFlexLayout, + vAlign = "center", + vFill = true, + }) + local panelButton = ui.Button({ + hAlign = "center", + vAlign = "center", + minWidth = 64, + minHeight = 64, + backgroundColor = {1, 1, 1, 0}, + }) + + local panelSize = 24 + local panelContainer = ui.UiElement({ + layout = ui.Layouts.HorizontalWrapLayout, + maxWidth = 3 * panelSize + }) + + for color = 1, 8 do + local panelImage = ui.ImageContainer({ + image = panels[GAME.localPlayer.settings.panelId].displayIcons[color], + width = panelSize, + height = panelSize, + }) + panelContainer:addChild(panelImage) + end + panelContainer:addChild(ui.ImageContainer({ + image = panels[GAME.localPlayer.settings.panelId].greyPanel, + width = panelSize, + height = panelSize, + })) + panelButton:addChild(panelContainer) + panelSelectionSelector:addChild(panelButton) + panelSelectionSelector:addChild(ui.Label({id = "panels", hAlign = "center", vAlign = "bottom"})) + + local levelSelectionSelector = ui.UiElement({ + layout = ui.Layouts.VerticalFlexLayout, + vAlign = "center", + vFill = true, + }) + + local levelButton = ui.Button({ + hAlign = "center", + vAlign = "center", + minWidth = 64, + minHeight = 64, + backgroundColor = {1, 1, 1, 0}, + }) + local levelImage = ui.ImageContainer({ + image = GAME.theme.images.IMG_levels[GAME.localPlayer.settings.level or 1], + hFill = true, + vFill = true, + }) + levelButton:addChild(levelImage) + levelSelectionSelector:addChild(levelButton) + levelSelectionSelector:addChild(ui.Label({id = "level", hAlign = "center", vAlign = "bottom"})) + + subSelectionSelector:addChild(characterSelectionSelector) + subSelectionSelector:addChild(stageSelectionSelector) + subSelectionSelector:addChild(panelSelectionSelector) + subSelectionSelector:addChild(ui.Label({text = "Ranked"})) + subSelectionSelector:addChild(levelSelectionSelector) + subSelectionSelector:addChild(ui.Label({text = "Input Selection"})) + subSelectionSelector:addChild(ui.Label({text = "Puzzle"})) + subSelectionSelector:addChild(ui.Label({text = "Attack File"})) self.uiRoot:addChild(subSelectionSelector) - local subSelection = ui.UIElement({ + local subSelection = ui.UiElement({ hFill = true, - minHeight = 400, + minHeight = 200, vFill = true, padding = 8, backgroundColor = {0.7, 0, 0.5, 1}, }) - subSelection:addChild(ui.Text({})) + --subSelection:addChild(ui.Label({})) self.uiRoot:addChild(subSelection) - self.uiRoot:addChild(ui.Text({text = "Ready", hAlign = "center"})) - self.uiRoot:addChild(ui.Text({text = "Leave", hAlign = "center"})) + self.uiRoot:addChild(ui.Label({text = "Ready", hAlign = "center"})) + self.uiRoot:addChild(ui.Label({text = "Leave", hAlign = "center"})) end function DesignHelper:loadRankedSelection(width) @@ -94,7 +204,7 @@ function DesignHelper:loadStages() end function DesignHelper:update() - if input.allKeys.isDown["MenuEsc"] then + if input.isDown["MenuEsc"] then GAME.navigationStack:pop() end end diff --git a/client/src/scenes/Game1pChallenge.lua b/client/src/scenes/Game1pChallenge.lua index cb2cfdf9..4cdaa480 100644 --- a/client/src/scenes/Game1pChallenge.lua +++ b/client/src/scenes/Game1pChallenge.lua @@ -9,7 +9,7 @@ local ChallengeModeRecapScene = require("client.src.scenes.ChallengeModeRecapSce local Game1pChallenge = class(function(self, sceneParams) self.totalTimeQuads = {} self.stageIndex = GAME.battleRoom.stageIndex - self.timeSplitElement = ChallengeModeTimeSplitsUIElement({x = consts.CANVAS_WIDTH / 2, y = 280}, GAME.battleRoom, GAME.battleRoom.stageIndex) + self.timeSplitElement = ChallengeModeTimeSplitsUIElement({x = love.graphics.getWidth() / 2, y = 280}, GAME.battleRoom, GAME.battleRoom.stageIndex) self.uiRoot:addChild(self.timeSplitElement) end, GameBase) @@ -42,10 +42,10 @@ end function Game1pChallenge:drawHUD() if GAME.battleRoom then - local drawX = consts.CANVAS_WIDTH / 2 + local drawX = love.graphics.getWidth() / 2 local drawY = 110 local width = 200 - local height = consts.CANVAS_HEIGHT - drawY + local height = love.graphics.getHeight() - drawY -- Background GraphicsUtil.drawRectangle("fill", drawX - width / 2, drawY, width, height, 0, 0, 0, 0.5) diff --git a/client/src/scenes/GameBase.lua b/client/src/scenes/GameBase.lua index eda96a14..25b89a73 100644 --- a/client/src/scenes/GameBase.lua +++ b/client/src/scenes/GameBase.lua @@ -175,7 +175,7 @@ function GameBase:load() self.match:connectSignal("countdownEnded", self, self.onGameStart) self.stage = stages[self.match.stageId] - self.backgroundImage = UpdatingImage(self.stage.images.background, false, 0, 0, consts.CANVAS_WIDTH, consts.CANVAS_HEIGHT) + self.backgroundImage = UpdatingImage(self.stage.images.background, false, 0, 0, love.graphics.getDimensions()) self.stageTrack = self:getStageTrack() local resume = ui.MenuItem.createButtonMenuItem("pause_resume", nil, true, function() diff --git a/client/src/scenes/GameCatchUp.lua b/client/src/scenes/GameCatchUp.lua index 0724e24e..0c679ecc 100644 --- a/client/src/scenes/GameCatchUp.lua +++ b/client/src/scenes/GameCatchUp.lua @@ -102,11 +102,12 @@ function GameCatchUp:update(dt) end function GameCatchUp:draw() + local w, h = love.graphics.getDimensions() local match = self.match GraphicsUtil.setColor(1, 1, 1, 1) - GraphicsUtil.drawRectangle("line", consts.CANVAS_WIDTH / 4 - 5, consts.CANVAS_HEIGHT / 2 - 25, consts.CANVAS_WIDTH / 2 + 10, 50) - GraphicsUtil.drawRectangle("fill", consts.CANVAS_WIDTH / 4, consts.CANVAS_HEIGHT / 2 - 20, consts.CANVAS_WIDTH / 2 * self.progress, 40) - GraphicsUtil.printf("Catching up: " .. match.engine.clock .. " out of " .. #match.stacks[1].engine.confirmedInput .. " frames", 0, 500, consts.CANVAS_WIDTH, "center") + GraphicsUtil.drawRectangle("line", w / 4 - 5, h / 2 - 25, w / 2 + 10, 50) + GraphicsUtil.drawRectangle("fill", w / 4, h / 2 - 20, w / 2 * self.progress, 40) + GraphicsUtil.printf("Catching up: " .. match.engine.clock .. " out of " .. #match.stacks[1].engine.confirmedInput .. " frames", 0, 500, w, "center") end return GameCatchUp \ No newline at end of file diff --git a/client/src/scenes/MainMenu.lua b/client/src/scenes/MainMenu.lua index c2743588..2832250d 100644 --- a/client/src/scenes/MainMenu.lua +++ b/client/src/scenes/MainMenu.lua @@ -192,10 +192,11 @@ function MainMenu:draw() self.uiRoot:draw() local fontHeight = GraphicsUtil.getGlobalFont():getHeight() local infoYPosition = 705 - fontHeight / 2 + local screenWidth = love.graphics.getWidth() local loveString = system.loveVersionString() if loveString == "11.3.0" then - GraphicsUtil.printf(loc("love_version_warning"), -5, infoYPosition, consts.CANVAS_WIDTH, "right") + GraphicsUtil.printf(loc("love_version_warning"), -5, infoYPosition, screenWidth, "right") infoYPosition = infoYPosition - fontHeight end @@ -210,7 +211,7 @@ function MainMenu:draw() version = "PA Version: " .. GAME.updater.activeReleaseStream.name .. " " .. (GAME.updater.activeVersion and GAME.updater.activeVersion.version or "dev") end end - GraphicsUtil.printf(version, -5, infoYPosition, consts.CANVAS_WIDTH, "right") + GraphicsUtil.printf(version, -5, infoYPosition, screenWidth, "right") infoYPosition = infoYPosition - fontHeight @@ -227,7 +228,7 @@ function MainMenu:draw() end if showUpdaterUpdateWarning then - GraphicsUtil.printf(loc("auto_updater_version_warning") .. " https://panelattack.com", -5, infoYPosition, consts.CANVAS_WIDTH, "right") + GraphicsUtil.printf(loc("auto_updater_version_warning") .. " https://panelattack.com", -5, infoYPosition, screenWidth, "right") infoYPosition = infoYPosition - fontHeight end end diff --git a/client/src/scenes/PortraitGame.lua b/client/src/scenes/PortraitGame.lua index 43a625b9..224e98c6 100644 --- a/client/src/scenes/PortraitGame.lua +++ b/client/src/scenes/PortraitGame.lua @@ -138,7 +138,7 @@ function PortraitGame:drawMultibar(stack) local x = math.floor((stack.frameOriginX + stack.panelOriginXOffset + overtimePos[1] / 3) * stack.gfxScale) local y = stack.panelOriginY * stack.gfxScale - local limit = GAME.globalCanvas:getWidth() - x + local limit = love.graphics.getWidth() - x local alignment = "right" limit = x - GraphicsUtil.getGlobalFont():getWidth(formattedSeconds) / 2 x = 0 @@ -189,8 +189,6 @@ function PortraitGame:draw() end function PortraitGame:flipToPortrait() - -- recreate the global canvas in portrait dimensions - GAME.globalCanvas = love.graphics.newCanvas(consts.CANVAS_HEIGHT, consts.CANVAS_WIDTH, {dpiscale=GAME:newCanvasSnappedScale()}) local width, height, _ = love.window.getMode() if system.isMobileOS() or DEBUG_ENABLED then @@ -206,9 +204,9 @@ function PortraitGame:flipToPortrait() local stack = player.stack stack.gfxScale = 5 -- force center it horizontally - stack.frameOriginX = (GAME.globalCanvas:getWidth() / 2 - stack:canvasWidth() / 2) / stack.gfxScale + stack.frameOriginX = (love.graphics.getWidth() / 2 - stack:canvasWidth() / 2) / stack.gfxScale -- and anchor at the bottom - stack.frameOriginY = (GAME.globalCanvas:getHeight() - stack:canvasHeight()) / stack.gfxScale + stack.frameOriginY = (love.graphics.getHeight() - stack:canvasHeight()) / stack.gfxScale stack.panelOriginX = stack.frameOriginX + stack.panelOriginXOffset stack.panelOriginY = stack.frameOriginY + stack.panelOriginYOffset stack.origin_x = stack.frameOriginX / stack.gfxScale @@ -233,7 +231,7 @@ function PortraitGame:flipToPortrait() local stack = player.stack stack.gfxScale = 1 stack.canvas = true - stack.frameOriginX = (GAME.globalCanvas:getWidth() - stack:canvasWidth()) - 12 + stack.frameOriginX = (love.graphics.getWidth() - stack:canvasWidth()) - 12 stack.frameOriginY = 10 stack.panelOriginX = stack.frameOriginX + stack.panelOriginXOffset stack.panelOriginY = stack.frameOriginY + stack.panelOriginYOffset @@ -243,8 +241,6 @@ function PortraitGame:flipToPortrait() end function PortraitGame:returnToLandscape() - -- recreate the global canvas in landscape dimensions - GAME.globalCanvas = love.graphics.newCanvas(consts.CANVAS_WIDTH, consts.CANVAS_HEIGHT, {dpiscale=GAME:newCanvasSnappedScale()}) -- flip the window dimensions to landscape local width, height, _ = love.window.getMode() if system.isMobileOS() or DEBUG_ENABLED then diff --git a/client/src/scenes/ReplayGame.lua b/client/src/scenes/ReplayGame.lua index 9bfb5b34..861a6c8d 100644 --- a/client/src/scenes/ReplayGame.lua +++ b/client/src/scenes/ReplayGame.lua @@ -114,7 +114,7 @@ ReplayGame.runGameOver = ReplayGame.runGame function ReplayGame:customDraw() local textPos = themes[config.theme].gameover_text_Pos local playbackText = self.playbackSpeeds[self.playbackSpeedIndex] .. "x" - GraphicsUtil.printf(playbackText, textPos[0], textPos[1], consts.CANVAS_WIDTH, "center", nil, 1, "big") + GraphicsUtil.printf(playbackText, textPos[0], textPos[1], love.graphics.getWidth(), "center", nil, 1, "big") end function ReplayGame:drawHUD() diff --git a/client/src/scenes/Scene.lua b/client/src/scenes/Scene.lua index 10a2ff16..7f85e461 100644 --- a/client/src/scenes/Scene.lua +++ b/client/src/scenes/Scene.lua @@ -22,8 +22,8 @@ local Scene = class( self.uiRoot = ui.UiElement({ x = 0, y = 0, - width = consts.CANVAS_WIDTH, - height = consts.CANVAS_HEIGHT, + width = love.graphics.getWidth(), + height = love.graphics.getHeight(), padding = 16, layout = ui.Layouts.AdaptiveFlexLayout }) @@ -85,7 +85,7 @@ end function Scene:drawCommunityMessage() -- Draw the community message if not config.debug_mode then - GraphicsUtil.printf(join_community_msg or "", 0, (668 / 720) * GAME.globalCanvas:getHeight(), GAME.globalCanvas:getWidth(), "center") + GraphicsUtil.printf(join_community_msg or "", 0, (668 / 720) * love.graphics.getHeight(), love.graphics.getWidth(), "center") end end diff --git a/client/src/scenes/StartUp.lua b/client/src/scenes/StartUp.lua index 8d04ce7a..1694fb28 100644 --- a/client/src/scenes/StartUp.lua +++ b/client/src/scenes/StartUp.lua @@ -67,9 +67,9 @@ end function StartUp:drawLoadingString(loadingString) local textHeight = 40 local x = 0 - local y = consts.CANVAS_HEIGHT / 2 - textHeight / 2 + local y = love.graphics.getHeight() / 2 - textHeight / 2 GraphicsUtil.setColor(1, 1, 1, 1) - love.graphics.printf(loadingString, x, y, consts.CANVAS_WIDTH, "center", 0, 1) + love.graphics.printf(loadingString, x, y, love.graphics.getWidth(), "center", 0, 1) end function StartUp:draw() diff --git a/client/src/scenes/TitleScreen.lua b/client/src/scenes/TitleScreen.lua index fdb4e4da..0c8521b0 100644 --- a/client/src/scenes/TitleScreen.lua +++ b/client/src/scenes/TitleScreen.lua @@ -18,10 +18,10 @@ local TitleScreen = class( TitleScreen.name = "TitleScreen" local function titleDrawPressStart(percent) - local textMaxWidth = consts.CANVAS_WIDTH - 40 + local textMaxWidth = love.graphics.getWidth() - 40 local textHeight = 40 - local x = (consts.CANVAS_WIDTH / 2) - (textMaxWidth / 2) - local y = consts.CANVAS_HEIGHT * 0.75 + local x = (love.graphics.getWidth() / 2) - (textMaxWidth / 2) + local y = love.graphics.getHeight() * 0.75 GraphicsUtil.printf(loc("continue_button"), x, y, textMaxWidth, "center", {1,1,1,percent}, nil, "huge") end diff --git a/client/src/scenes/Transitions/BlackFadeTransition.lua b/client/src/scenes/Transitions/BlackFadeTransition.lua index 11e2de2c..9250101e 100644 --- a/client/src/scenes/Transitions/BlackFadeTransition.lua +++ b/client/src/scenes/Transitions/BlackFadeTransition.lua @@ -18,7 +18,7 @@ function BlackFadeTransition:draw() self.newScene:draw() end GraphicsUtil.setColor(0, 0, 0, alpha) - GraphicsUtil.drawRectangle("fill", 0, 0, consts.CANVAS_WIDTH, consts.CANVAS_HEIGHT) + GraphicsUtil.drawRectangle("fill", 0, 0, love.graphics.getDimensions()) GraphicsUtil.setColor(1, 1, 1, 1) end diff --git a/client/src/scenes/Transitions/Transition.lua b/client/src/scenes/Transitions/Transition.lua index 89fdfa30..6c2b282d 100644 --- a/client/src/scenes/Transitions/Transition.lua +++ b/client/src/scenes/Transitions/Transition.lua @@ -21,7 +21,7 @@ local Transition = class(function(transition, startTime, duration) -- these are set by the navigationStack transition.oldScene = nil transition.newScene = nil - transition.uiRoot = UiElement({x = 0, y = 0, width = consts.CANVAS_WIDTH, height = consts.CANVAS_HEIGHT}) + transition.uiRoot = UiElement({x = 0, y = 0, width = love.graphics.getWidth(), height = love.graphics.getWidth()}) end) diff --git a/client/src/ui/ImageContainer.lua b/client/src/ui/ImageContainer.lua index 3be6db1c..e0e7d9f3 100644 --- a/client/src/ui/ImageContainer.lua +++ b/client/src/ui/ImageContainer.lua @@ -13,6 +13,8 @@ end, UiElement) function ImageContainer:setImage(image, width, height, scale) self.image = image self.imageWidth, self.imageHeight = self.image:getDimensions() + self.minWidth = math.max(self.imageWidth, self.minWidth) + self.minHeight = math.max(self.imageHeight, self.minHeight) if self.hFill and self.vFill then self.scale = math.min(self.width / self.imageWidth, self.height / self.imageHeight) @@ -38,8 +40,8 @@ end function ImageContainer:onResize() self.scale = math.min(self.width / self.imageWidth, self.height / self.imageHeight) - self.width = self.imageWidth * self.scale - self.height = self.imageHeight * self.scale + -- self.width = self.imageWidth * self.scale + -- self.height = self.imageHeight * self.scale end function ImageContainer:drawSelf() diff --git a/client/src/ui/Label.lua b/client/src/ui/Label.lua index 6e2ed885..83c07318 100644 --- a/client/src/ui/Label.lua +++ b/client/src/ui/Label.lua @@ -111,11 +111,15 @@ function Label:getPreferredWidth() end function Label:setMinHeightForWidth() - self.minHeight = GraphicsUtil.getTextHeightForWidth(self.fontSize, self.text, self.width, self.hAlign) + --self.minHeight = GraphicsUtil.getTextHeightForWidth(self.fontSize, self.text, self.width, self.hAlign) end function Label:getBaseWidth() return self.preferredWidth end +function Label:getBaseHeight() + return GraphicsUtil.getTextHeightForWidth(self.fontSize, self.text, self.width, self.hAlign) +end + return Label \ No newline at end of file diff --git a/client/src/ui/Layouts/HorizontalFlexLayout.lua b/client/src/ui/Layouts/HorizontalFlexLayout.lua index 38a740dd..cd28d7db 100644 --- a/client/src/ui/Layouts/HorizontalFlexLayout.lua +++ b/client/src/ui/Layouts/HorizontalFlexLayout.lua @@ -217,12 +217,6 @@ function HorizontalFlexLayout.positionChildren(uiElement) x = x + uiElement.childGap + child.width end end - - for _, child in ipairs(uiElement.children) do - if child.isVisible then - child.layout.positionChildren(child) - end - end end return HorizontalFlexLayout \ No newline at end of file diff --git a/client/src/ui/Layouts/HorizontalWrapLayout.lua b/client/src/ui/Layouts/HorizontalWrapLayout.lua new file mode 100644 index 00000000..1b49b222 --- /dev/null +++ b/client/src/ui/Layouts/HorizontalWrapLayout.lua @@ -0,0 +1,71 @@ +local PATH = (...):gsub('%.[^%.]+$', '') +local HorizontalFlexLayout = require(PATH ..".HorizontalFlexLayout") +local util = require("common.lib.util") + +---@class HorizontalWrapLayout : HorizontalFlexLayout +local HorizontalWrapLayout = setmetatable({}, {__index = HorizontalFlexLayout}) + +function HorizontalWrapLayout.getMinWidth(uiElement) + return util.bound(uiElement.minWidth, HorizontalFlexLayout.getMinWidth(uiElement), uiElement.maxWidth) +end + +---@param uiElement UiElement +function HorizontalWrapLayout.getMinHeight(uiElement) + local h = uiElement.padding * 2 + local maxHeight = 0 + uiElement.tempRows = {} + + local childrenInCurrentRow = 0 + local rowCount = 1 + local width = uiElement.padding + for i, child in ipairs(uiElement.children) do + if child.isVisible then + maxHeight = math.max(maxHeight, child.newHeight) + if width + child.newWidth + uiElement.padding + childrenInCurrentRow * uiElement.childGap > uiElement.width then + uiElement.tempRows[rowCount] = maxHeight + rowCount = rowCount + 1 + width = uiElement.padding + child.newWidth + childrenInCurrentRow = 1 + h = h + maxHeight + maxHeight = 0 + else + childrenInCurrentRow = childrenInCurrentRow + 1 + width = width + child.newWidth + end + child.tempRow = rowCount + end + end + + uiElement.tempRows[rowCount] = maxHeight + + return h +end + +---@param uiElement UiElement +function HorizontalWrapLayout.positionChildren(uiElement) + if #uiElement.tempRows == 1 then + HorizontalFlexLayout.positionChildren(uiElement) + else + local x = uiElement.padding + local y = uiElement.padding + local row = 1 + for _, child in ipairs(uiElement.children) do + if child.isVisible then + if child.tempRow > row then + x = uiElement.padding + y = y + uiElement.tempRows[row] + uiElement.childGap + row = child.tempRow + end + + child.x = math.round(x) + child.y = math.round(y) + x = x + uiElement.childGap + child.width + child.tempRow = nil + end + end + uiElement.tempRows = nil + end + +end + +return HorizontalWrapLayout \ No newline at end of file diff --git a/client/src/ui/Layouts/Layout.lua b/client/src/ui/Layouts/Layout.lua index bd79f1b2..b2fe28ac 100644 --- a/client/src/ui/Layouts/Layout.lua +++ b/client/src/ui/Layouts/Layout.lua @@ -1,3 +1,4 @@ + ---@class Layout local Layout = {} @@ -6,7 +7,6 @@ local Layout = {} ---@param height number? function Layout.resize(uiElement, width, height) uiElement.layout.updateWidths(uiElement, width) - -- transform width to height for width-to-height supporting uiElements based on the width pass uiElement:setMinHeightForWidth() diff --git a/client/src/ui/Layouts/VerticalFlexLayout.lua b/client/src/ui/Layouts/VerticalFlexLayout.lua index 704e4166..a15eec0e 100644 --- a/client/src/ui/Layouts/VerticalFlexLayout.lua +++ b/client/src/ui/Layouts/VerticalFlexLayout.lua @@ -95,7 +95,6 @@ function VerticalFlexLayout.growChildrenHeight(uiElement) for i = #growables, 1, -1 do local growable = growables[i] if growable.newHeight >= growable.maxHeight then - growable.height = growable.newHeight table.remove(growables, i) end end @@ -131,7 +130,7 @@ function VerticalFlexLayout.positionChildren(uiElement) child.y = y elseif child.vAlign == "center" then child.y = y + remainingHeight / 2 - elseif child.vAlign == "right" then + elseif child.vAlign == "bottom" then child.y = y + remainingHeight end diff --git a/client/src/ui/ScrollContainer.lua b/client/src/ui/ScrollContainer.lua index aeb66c88..c03d65b7 100644 --- a/client/src/ui/ScrollContainer.lua +++ b/client/src/ui/ScrollContainer.lua @@ -20,7 +20,7 @@ local HorizontalScrollLayout = require(PATH .. ".Layouts.HorizontalScrollLayout" local ScrollContainer = class( ---@param self ScrollContainer function(self, options) - self.scrollOrientation = "vertical" or options.scrollOrientation + self.scrollOrientation = options.scrollOrientation or "vertical" if self.scrollOrientation == "vertical" then self.layout = VerticalScrollLayout else @@ -216,4 +216,12 @@ function ScrollContainer:onResize() self:recalculateMaxScrollOffset() end +function ScrollContainer:getBaseHeight() + return self.minHeight +end + +function ScrollContainer:getBaseWidth() + return self.minWidth +end + return ScrollContainer \ No newline at end of file diff --git a/client/src/ui/UIElement.lua b/client/src/ui/UIElement.lua index 24b1ddd7..cf839e88 100644 --- a/client/src/ui/UIElement.lua +++ b/client/src/ui/UIElement.lua @@ -24,7 +24,7 @@ local class = require("common.lib.class") ---@field padding number ---@field childGap number ---@field id integer unique identifier of the element ----@field layout FlexLayout +---@field layout Layout ---@field onTouch function? touch callback for when the element is being touched ---@field onDrag function? touch callback for when the mouse touching the element is dragged across the screen ---@field onRelease function? touch callback for when the mouse touching the element is released @@ -46,7 +46,7 @@ local class = require("common.lib.class") ---@field vFill boolean? if the element's height should fill out the entire parent's height ---@field isVisible boolean? if the element is currently visible for rendering (also disables touch interaction) ---@field isEnabled boolean? if the element is currently eligible for touch interaction ----@field layout FlexLayout? +---@field layout Layout? ---@field backgroundColor color? ---@field padding number? ---@field childGap number? @@ -69,6 +69,7 @@ local UIElement = class( self.layout = options.layout self.padding = options.padding or 0 self.childGap = options.childGap or 0 + self.wraps = options.wraps or false -- ui dimensions self.minWidth = options.minWidth or options.width or 0 @@ -260,9 +261,21 @@ function UIElement:getTouchedElement(x, y) end function UIElement:setMinHeightForWidth() - for i, child in ipairs(self.children) do - child:setMinHeightForWidth() - end + -- local childrenInCurrentRow = 0 + -- local rowCount = 1 + -- local width = self.padding + -- for i, child in ipairs(self.children) do + -- if width + child.width + self.padding + childrenInCurrentRow * self.childGap > self.width then + -- rowCount = rowCount + 1 + -- width = self.padding + child.width + -- childrenInCurrentRow = 1 + -- else + -- childrenInCurrentRow = childrenInCurrentRow + 1 + -- width = width + child.width + -- end + -- end + + -- return self.minHeight * rowCount end function UIElement:getBaseWidth() @@ -270,7 +283,25 @@ function UIElement:getBaseWidth() end function UIElement:getBaseHeight() - return self.minHeight + -- if self.wraps then + -- local childrenInCurrentRow = 0 + -- local rowCount = 1 + -- local width = self.padding + -- for i, child in ipairs(self.children) do + -- if width + child.width + self.padding + childrenInCurrentRow * self.childGap > self.width then + -- rowCount = rowCount + 1 + -- width = self.padding + child.width + -- childrenInCurrentRow = 1 + -- else + -- childrenInCurrentRow = childrenInCurrentRow + 1 + -- width = width + child.width + -- end + -- end + + -- return self.minHeight * rowCount + -- else + return self.minHeight + --end end return UIElement \ No newline at end of file diff --git a/client/src/ui/init.lua b/client/src/ui/init.lua index d13dc450..aa813f4b 100644 --- a/client/src/ui/init.lua +++ b/client/src/ui/init.lua @@ -21,6 +21,7 @@ local ui = { Layouts = { AdaptiveFlexLayout = require(PATH .. ".Layouts.AdaptiveFlexLayout"), HorizontalFlexLayout = require(PATH .. ".Layouts.HorizontalFlexLayout"), + HorizontalWrapLayout = require(PATH .. ".Layouts.HorizontalWrapLayout"), VerticalFlexLayout = require(PATH .. ".Layouts.VerticalFlexLayout"), }, Leaderboard = require(PATH .. ".Leaderboard"), From 9e6c1222cc0addec08b9da7c09d7ee200e893d34 Mon Sep 17 00:00:00 2001 From: Endaris Date: Sat, 17 May 2025 23:57:18 +0200 Subject: [PATCH 14/49] add a very basic standard cursor for whatever needs to kickstart a nested focuse navigation --- client/src/scenes/DesignHelper.lua | 45 +++-- client/src/scenes/MainMenu.lua | 6 +- client/src/ui/Cursor.lua | 158 ++++++++++++++++++ .../src/ui/Layouts/HorizontalFlexLayout.lua | 2 +- client/src/ui/Layouts/Layout.lua | 7 +- client/src/ui/Layouts/VerticalFlexLayout.lua | 2 +- client/src/ui/VerticalMenu.lua | 16 +- client/src/ui/init.lua | 3 + 8 files changed, 214 insertions(+), 25 deletions(-) create mode 100644 client/src/ui/Cursor.lua diff --git a/client/src/scenes/DesignHelper.lua b/client/src/scenes/DesignHelper.lua index d7eaaa94..8ab5a43a 100644 --- a/client/src/scenes/DesignHelper.lua +++ b/client/src/scenes/DesignHelper.lua @@ -86,8 +86,8 @@ function DesignHelper:load() local stageButton = ui.Button({ hAlign = "center", vAlign = "center", - minWidth = 64, - minHeight = 64, + minWidth = 84, + minHeight = 84, backgroundColor = {1, 1, 1, 0}, }) local stageImage = ui.ImageContainer({ @@ -110,12 +110,12 @@ function DesignHelper:load() local panelButton = ui.Button({ hAlign = "center", vAlign = "center", - minWidth = 64, - minHeight = 64, + minWidth = 84, + minHeight = 84, backgroundColor = {1, 1, 1, 0}, }) - local panelSize = 24 + local panelSize = 28 local panelContainer = ui.UiElement({ layout = ui.Layouts.HorizontalWrapLayout, maxWidth = 3 * panelSize @@ -147,8 +147,8 @@ function DesignHelper:load() local levelButton = ui.Button({ hAlign = "center", vAlign = "center", - minWidth = 64, - minHeight = 64, + minWidth = 84, + minHeight = 84, backgroundColor = {1, 1, 1, 0}, }) local levelImage = ui.ImageContainer({ @@ -163,12 +163,30 @@ function DesignHelper:load() subSelectionSelector:addChild(characterSelectionSelector) subSelectionSelector:addChild(stageSelectionSelector) subSelectionSelector:addChild(panelSelectionSelector) - subSelectionSelector:addChild(ui.Label({text = "Ranked"})) + --subSelectionSelector:addChild(ui.Label({text = "Ranked"})) subSelectionSelector:addChild(levelSelectionSelector) - subSelectionSelector:addChild(ui.Label({text = "Input Selection"})) - subSelectionSelector:addChild(ui.Label({text = "Puzzle"})) - subSelectionSelector:addChild(ui.Label({text = "Attack File"})) + --subSelectionSelector:addChild(ui.Label({text = "Input Selection"})) + --subSelectionSelector:addChild(ui.Label({text = "Puzzle"})) + --subSelectionSelector:addChild(ui.Label({text = "Attack File"})) + + local readyButton = ui.Button({ + hAlign = "center", + vAlign = "center", + minWidth = 84, + minHeight = 84, + }) + readyButton:addChild(ui.Label({id = "ready", hAlign = "center", vAlign = "center"})) + local leaveButton = ui.Button({ + hAlign = "center", + vAlign = "center", + minWidth = 84, + minHeight = 84, + }) + leaveButton:addChild(ui.Label({id = "leave", hAlign = "center", vAlign = "center"})) + + subSelectionSelector:addChild(readyButton) + subSelectionSelector:addChild(leaveButton) self.uiRoot:addChild(subSelectionSelector) local subSelection = ui.UiElement({ @@ -179,12 +197,7 @@ function DesignHelper:load() backgroundColor = {0.7, 0, 0.5, 1}, }) - --subSelection:addChild(ui.Label({})) - self.uiRoot:addChild(subSelection) - - self.uiRoot:addChild(ui.Label({text = "Ready", hAlign = "center"})) - self.uiRoot:addChild(ui.Label({text = "Leave", hAlign = "center"})) end function DesignHelper:loadRankedSelection(width) diff --git a/client/src/scenes/MainMenu.lua b/client/src/scenes/MainMenu.lua index 2832250d..81de615e 100644 --- a/client/src/scenes/MainMenu.lua +++ b/client/src/scenes/MainMenu.lua @@ -29,6 +29,10 @@ local PuzzleGame = require("client.src.scenes.PuzzleGame") local MainMenu = class(function(self, sceneParams) self.music = "main" self.menu = self:createMainMenu() + self.cursor = ui.Cursor({ + target = self.menu, + cursorImage = GAME.theme:getGridCursor(1) + }) self.uiRoot:addChild(self.menu) end, Scene) @@ -182,7 +186,7 @@ end function MainMenu:update(dt) GAME.theme.images.bg_main:update(dt) - self.menu:receiveInputs() + self.cursor:receiveInputs() self:checkForUpdates() end diff --git a/client/src/ui/Cursor.lua b/client/src/ui/Cursor.lua new file mode 100644 index 00000000..b73d87e1 --- /dev/null +++ b/client/src/ui/Cursor.lua @@ -0,0 +1,158 @@ +local PATH = (...):gsub('%.[^%.]+$', '') +local UiElement = require(PATH .. ".UIElement") +local class = require("common.lib.class") +local FocusDirector = require(PATH .. ".FocusDirector") +local consts = require("common.engine.consts") + +---@class Cursor : FocusDirector, UiElement +---@field hovered UiElement +---@field target UiElement +---@field hoveredIndex integer +---@field cursorImage love.Texture +---@field escapeCallback fun(self: Cursor) +local Cursor = class( +function(self, options) + if options.escapeCallback then + self.escapeCallback = options.escapeCallback + end + self:setTarget(options.target) + if options.hoveredIndex then + self.hoveredIndex = options.hoveredIndex + else + self.hoveredIndex = 0 + self:moveToNext() + end + self.cursorImage = options.cursorImage +end, +UiElement) + +FocusDirector(Cursor) + +function Cursor:moveToNext() + if self.target then + for i = self.hoveredIndex + 1, self.hoveredIndex + #self.target.children do + local index = wrap(1, i, #self.target.children) + local child = self.target.children[index] + if child.receiveInputs and child.isEnabled and child.isVisible then + self.hoveredIndex = index + self.hovered = self.target.children[self.hoveredIndex] + break + end + end + end +end + +function Cursor:moveToPrevious() + if self.target then + for i = self.hoveredIndex - 1, self.hoveredIndex - #self.target.children, -1 do + local index = wrap(1, i, #self.target.children) + local child = self.target.children[index] + if child.receiveInputs and child.isEnabled and child.isVisible then + self.hoveredIndex = index + self.hovered = self.target.children[self.hoveredIndex] + break + end + end + end +end + +function Cursor:getLastIndex() + for i = #self.target.children, 1, -1 do + local child = self.target.children[i] + if child.receiveInputs and child.isEnabled and child.isVisible then + return i + end + end +end + +function Cursor:moveToLast() + self.hoveredIndex = self:getLastIndex() + self.hovered = self.target.children[self.hoveredIndex] +end + +function Cursor:receiveInputs(inputs, dt) + if self.target then + if self.focused then + self.focused:receiveInputs(inputs, dt, self.player) + elseif inputs.isDown.Swap2 then + GAME.theme:playCancelSfx() + self:escapeCallback() + elseif inputs:isPressedWithRepeat("Left", consts.KEY_DELAY, consts.KEY_REPEAT_PERIOD) then + if self.target.layout.characteristic == "horizontal" then + GAME.theme:playMoveSfx() + self:moveToPrevious() + elseif self.hovered.receiveInputs then + self.hovered:receiveInputs(inputs, dt) + else + GAME.theme:playCancelSfx() + end + elseif inputs:isPressedWithRepeat("Right", consts.KEY_DELAY, consts.KEY_REPEAT_PERIOD) then + if self.target.layout.characteristic == "horizontal" then + GAME.theme:playMoveSfx() + self:moveToNext() + elseif self.hovered.receiveInputs then + self.hovered:receiveInputs(inputs, dt) + else + GAME.theme:playCancelSfx() + end + elseif inputs:isPressedWithRepeat("Up", consts.KEY_DELAY, consts.KEY_REPEAT_PERIOD) then + if self.target.layout.characteristic == "vertical" then + GAME.theme:playMoveSfx() + self:moveToPrevious() + elseif self.hovered.receiveInputs then + self.hovered:receiveInputs(inputs, dt) + else + GAME.theme:playCancelSfx() + end + elseif inputs:isPressedWithRepeat("Down", consts.KEY_DELAY, consts.KEY_REPEAT_PERIOD) then + if self.target.layout.characteristic == "vertical" then + GAME.theme:playMoveSfx() + self:moveToNext() + elseif self.hovered.receiveInputs then + self.hovered:receiveInputs(inputs, dt) + else + GAME.theme:playCancelSfx() + end + elseif inputs.isDown.Swap1 or inputs.isDown.Start then + if self.hovered.isFocusable then + GAME.theme:playValidationSfx() + self:setFocus(self.hovered) + elseif self.hovered.receiveInputs then + self.hovered:receiveInputs(inputs, dt) + else + GAME.theme:playCancelSfx() + end + end + end +end + +function Cursor:setTarget(uiElement) + self.target = uiElement + if uiElement.isFocusable then + self:setFocus(self.target) + end +end + +function Cursor:drawSelf() + if not self.focused then + local x, y = self.hovered:getScreenPos() + local imageWidth, imageHeight = self.cursorImage:getDimensions() + local xScale = self.hovered.width / imageWidth + local yScale = self.hovered.height / imageHeight + love.graphics.draw(self.cursorImage, x, y, 0, xScale, yScale) + end +end + +function Cursor:escapeCallback() + if self.hoveredIndex == self:getLastIndex() then + if self.focused then + self.focused:yieldFocus() + else + GAME.navigationStack:pop() + end + else + self:moveToLast() + end +end + +return Cursor \ No newline at end of file diff --git a/client/src/ui/Layouts/HorizontalFlexLayout.lua b/client/src/ui/Layouts/HorizontalFlexLayout.lua index cd28d7db..33d58486 100644 --- a/client/src/ui/Layouts/HorizontalFlexLayout.lua +++ b/client/src/ui/Layouts/HorizontalFlexLayout.lua @@ -2,7 +2,7 @@ local PATH = (...):gsub('%.[^%.]+$', '') local FlexLayout = require(PATH ..".FlexLayout") ---@class HorizontalFlexLayout : FlexLayout -local HorizontalFlexLayout = setmetatable({}, {__index = FlexLayout}) +local HorizontalFlexLayout = setmetatable({characteristic = "horizontal"}, {__index = FlexLayout}) ---@param uiElement UiElement function HorizontalFlexLayout.getMinWidth(uiElement) diff --git a/client/src/ui/Layouts/Layout.lua b/client/src/ui/Layouts/Layout.lua index b2fe28ac..108f16ea 100644 --- a/client/src/ui/Layouts/Layout.lua +++ b/client/src/ui/Layouts/Layout.lua @@ -1,6 +1,11 @@ +---@enum LayoutCharacteristic +local LayoutCharacteristics = { none = "none", horizontal = "horizontal", vertical = "vertical"} ---@class Layout -local Layout = {} +---@field characteristic LayoutCharacteristic +local Layout = { + characteristic = LayoutCharacteristics.none +} ---@param uiElement UiElement ---@param width number? diff --git a/client/src/ui/Layouts/VerticalFlexLayout.lua b/client/src/ui/Layouts/VerticalFlexLayout.lua index a15eec0e..74fffbe0 100644 --- a/client/src/ui/Layouts/VerticalFlexLayout.lua +++ b/client/src/ui/Layouts/VerticalFlexLayout.lua @@ -2,7 +2,7 @@ local PATH = (...):gsub('%.[^%.]+$', '') local FlexLayout = require(PATH ..".FlexLayout") ---@class VerticalFlexLayout : FlexLayout -local VerticalFlexLayout = setmetatable({}, {__index = FlexLayout}) +local VerticalFlexLayout = setmetatable({characteristic = "vertical"}, {__index = FlexLayout}) ---@param uiElement UiElement function VerticalFlexLayout.getMinWidth(uiElement) diff --git a/client/src/ui/VerticalMenu.lua b/client/src/ui/VerticalMenu.lua index 2b4089ec..1443f121 100644 --- a/client/src/ui/VerticalMenu.lua +++ b/client/src/ui/VerticalMenu.lua @@ -63,6 +63,7 @@ function VerticalMenu:selectLast() break end end + self:keepVisible(self.children[self.selectedIndex].y, self.children[self.selectedIndex].height) end function VerticalMenu:receiveInputs(inputs, dt) @@ -107,11 +108,16 @@ function VerticalMenu:setSelectedIndex(index) self.selectedIndex = util.bound(1, index, #self.children) end -function VerticalMenu:draw() - ScrollContainer.draw(self) - if self.selectedIndex then - local selected = self.children[self.selectedIndex] - love.graphics.print(">", self.x + selected.x - 10, self.y + self.scrollOffset + selected.y) +function VerticalMenu:drawChildren() + for i, uiElement in ipairs(self.children) do + if uiElement.isVisible then + if i == self.selectedIndex then + GraphicsUtil.setColor(0.6, 0.6, 1, 0.5) + love.graphics.rectangle("fill", uiElement.x, uiElement.y, uiElement.width, uiElement.height) + love.graphics.rectangle("line", uiElement.x, uiElement.y, uiElement.width, uiElement.height) + end + uiElement:draw() + end end end diff --git a/client/src/ui/init.lua b/client/src/ui/init.lua index aa813f4b..4c45b73a 100644 --- a/client/src/ui/init.lua +++ b/client/src/ui/init.lua @@ -9,6 +9,9 @@ local ui = { Button = require(PATH .. ".Button"), ButtonGroup = require(PATH .. ".ButtonGroup"), Carousel = require(PATH .. ".Carousel"), + ---@see Cursor + ---@type fun(options: UiElementOptions): Cursor + Cursor = require(PATH .. ".Cursor"), Focusable = require(PATH .. ".Focusable"), FocusDirector = require(PATH .. ".FocusDirector"), Grid = require(PATH .. ".Grid"), From 6bbb45b83e60106cfe1cada35e2a0fd6ed184da3 Mon Sep 17 00:00:00 2001 From: Endaris Date: Sun, 18 May 2025 02:33:38 +0200 Subject: [PATCH 15/49] initial cursor display for designhelper --- client/src/scenes/DesignHelper.lua | 21 ++++++++++++++++++--- client/src/scenes/MainMenu.lua | 4 +++- client/src/ui/Cursor.lua | 17 ++++++++--------- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/client/src/scenes/DesignHelper.lua b/client/src/scenes/DesignHelper.lua index 8ab5a43a..153128ec 100644 --- a/client/src/scenes/DesignHelper.lua +++ b/client/src/scenes/DesignHelper.lua @@ -1,7 +1,7 @@ local class = require("common.lib.class") local Scene = require("client.src.scenes.Scene") local ui = require("client.src.ui") -local input = require("client.src.inputManager") +local inputs = require("client.src.inputManager") local prof = require("common.lib.zoneProfiler") local DesignHelper = class(function(self, sceneParams) @@ -26,6 +26,8 @@ function DesignHelper:load() roomMode:addChild(ui.Label({text = "Battle", hAlign = "center", vAlign = "center"})) roomMode:addChild(ui.Label({text = "Arcade", hAlign = "center", vAlign = "center"})) + roomMode.receiveInputs = function() end + self.uiRoot:addChild(roomMode) local gameMode = ui.UiElement({ @@ -45,6 +47,8 @@ function DesignHelper:load() gameMode:addChild(ui.Label({text = "Training"})) gameMode:addChild(ui.Label({text = "Line Clear"})) + gameMode.receiveInputs = function() end + self.uiRoot:addChild(gameMode) local subSelectionSelector = ui.ScrollContainer({ @@ -197,7 +201,15 @@ function DesignHelper:load() backgroundColor = {0.7, 0, 0.5, 1}, }) + subSelectionSelector.receiveInputs = function() end + subSelection.receiveInputs = function() end + self.uiRoot:addChild(subSelection) + + self.cursor = ui.Cursor({ + target = self.uiRoot, + hoveredIndex = 3, + }) end function DesignHelper:loadRankedSelection(width) @@ -216,13 +228,16 @@ function DesignHelper:loadStages() self.stageCarousel:loadCurrentStages() end -function DesignHelper:update() - if input.isDown["MenuEsc"] then +function DesignHelper:update(dt) + self.cursor:receiveInputs(inputs, dt) + + if inputs.isDown["MenuEsc"] then GAME.navigationStack:pop() end end function DesignHelper:draw() + self.cursor:draw() self.uiRoot:draw() end diff --git a/client/src/scenes/MainMenu.lua b/client/src/scenes/MainMenu.lua index 81de615e..1546b825 100644 --- a/client/src/scenes/MainMenu.lua +++ b/client/src/scenes/MainMenu.lua @@ -18,6 +18,7 @@ local SetNameMenu = require("client.src.scenes.SetNameMenu") local OptionsMenu = require("client.src.scenes.OptionsMenu") local DesignHelper = require("client.src.scenes.DesignHelper") local system = require("client.src.system") +local inputs = require("client.src.inputManager") local TimeAttackGame = require("client.src.scenes.TimeAttackGame") local EndlessGame = require("client.src.scenes.EndlessGame") @@ -34,6 +35,7 @@ local MainMenu = class(function(self, sceneParams) cursorImage = GAME.theme:getGridCursor(1) }) self.uiRoot:addChild(self.menu) + self.uiRoot:addChild(self.cursor) end, Scene) MainMenu.name = "MainMenu" @@ -186,7 +188,7 @@ end function MainMenu:update(dt) GAME.theme.images.bg_main:update(dt) - self.cursor:receiveInputs() + self.cursor:receiveInputs(inputs) self:checkForUpdates() end diff --git a/client/src/ui/Cursor.lua b/client/src/ui/Cursor.lua index b73d87e1..6f365a02 100644 --- a/client/src/ui/Cursor.lua +++ b/client/src/ui/Cursor.lua @@ -3,12 +3,12 @@ local UiElement = require(PATH .. ".UIElement") local class = require("common.lib.class") local FocusDirector = require(PATH .. ".FocusDirector") local consts = require("common.engine.consts") +local GraphicsUtil = require("client.src.graphics.graphics_util") ---@class Cursor : FocusDirector, UiElement ---@field hovered UiElement ---@field target UiElement ---@field hoveredIndex integer ----@field cursorImage love.Texture ---@field escapeCallback fun(self: Cursor) local Cursor = class( function(self, options) @@ -18,11 +18,11 @@ function(self, options) self:setTarget(options.target) if options.hoveredIndex then self.hoveredIndex = options.hoveredIndex + self.hovered = self.target.children[self.hoveredIndex] else self.hoveredIndex = 0 self:moveToNext() end - self.cursorImage = options.cursorImage end, UiElement) @@ -134,13 +134,12 @@ function Cursor:setTarget(uiElement) end function Cursor:drawSelf() - if not self.focused then - local x, y = self.hovered:getScreenPos() - local imageWidth, imageHeight = self.cursorImage:getDimensions() - local xScale = self.hovered.width / imageWidth - local yScale = self.hovered.height / imageHeight - love.graphics.draw(self.cursorImage, x, y, 0, xScale, yScale) - end + GraphicsUtil.setColor(0, 0, 0, 0.2) + love.graphics.rectangle("fill", self.target.x, self.target.y, self.target.width, self.target.height) + GraphicsUtil.setColor(1, 1, 1, 0.2) + local x, y = self.hovered:getScreenPos() + love.graphics.rectangle("fill", x, y, self.hovered.width, self.hovered.height) + GraphicsUtil.setColor(1, 1, 1, 1) end function Cursor:escapeCallback() From c91091098aced999b29f5fe1f5ae8c71e8ee0a6d Mon Sep 17 00:00:00 2001 From: Endaris Date: Sun, 18 May 2025 16:58:09 +0200 Subject: [PATCH 16/49] create a container that unifies child size allows grid size navigation and automatically wraps for the given width --- client/src/Game.lua | 6 + client/src/scenes/DesignHelper.lua | 142 ++++++------- client/src/ui/Cursor.lua | 4 +- .../src/ui/Layouts/HorizontalWrapLayout.lua | 9 +- client/src/ui/UniSizedContainer.lua | 188 ++++++++++++++++++ client/src/ui/init.lua | 3 + 6 files changed, 262 insertions(+), 90 deletions(-) create mode 100644 client/src/ui/UniSizedContainer.lua diff --git a/client/src/Game.lua b/client/src/Game.lua index db0b6456..f891d4dc 100644 --- a/client/src/Game.lua +++ b/client/src/Game.lua @@ -351,6 +351,12 @@ function Game:updateMouseVisibility(dt) end function Game:handleResize(newWidth, newHeight) + local activeScene = GAME.navigationStack:getActiveScene() + if activeScene then + activeScene.uiRoot.minWidth = newWidth + activeScene.uiRoot.minHeight = newHeight + activeScene.uiRoot.layout.resize(activeScene.uiRoot, newWidth, newHeight) + end self:updateCanvasPositionAndScale(newWidth, newHeight) if self.battleRoom and self.battleRoom.match then self.needsAssetReload = true diff --git a/client/src/scenes/DesignHelper.lua b/client/src/scenes/DesignHelper.lua index 153128ec..d3b49638 100644 --- a/client/src/scenes/DesignHelper.lua +++ b/client/src/scenes/DesignHelper.lua @@ -10,6 +10,30 @@ end, Scene) DesignHelper.name = "DesignHelper" +local function getSelectorTemplate(id) + local selector = ui.UiElement({ + layout = ui.Layouts.VerticalFlexLayout, + vAlign = "center", + vFill = true, + }) + local button = ui.Button({ + hAlign = "center", + vAlign = "center", + minWidth = 84, + minHeight = 84, + backgroundColor = {1, 1, 1, 0}, + }) + local label = ui.Label({ + id = id, + hAlign = "center", + vAlign = "bottom" + }) + selector:addChild(button) + selector:addChild(label) + + return selector, button +end + function DesignHelper:load() self.uiRoot.layout = ui.Layouts.VerticalFlexLayout self.uiRoot.childGap = 8 @@ -51,21 +75,16 @@ function DesignHelper:load() self.uiRoot:addChild(gameMode) - local subSelectionSelector = ui.ScrollContainer({ + local subSelectionSelector = ui.UniSizedContainer({ childGap = 32, padding = 8, hFill = true, backgroundColor = {0, 1, 0, 0.5}, - scrollOrientation = "horizontal", + childrenWidth = 84, + childrenHeight = 112, + --scrollOrientation = "horizontal", }) - local characterButton = ui.Button({ - hAlign = "center", - vAlign = "center", - minWidth = 64, - minHeight = 64, - backgroundColor = {1, 1, 1, 0}, - }) local characterImage = ui.ImageContainer({ image = characters[GAME.localPlayer.settings.selectedCharacterId].images.icon, drawBorders = true, @@ -73,27 +92,9 @@ function DesignHelper:load() hFill = true, vFill = true, }) + local characterSelectionSelector, characterButton = getSelectorTemplate("character") characterButton:addChild(characterImage) - local characterSelectionSelector = ui.UiElement({ - layout = ui.Layouts.VerticalFlexLayout, - vAlign = "center", - vFill = true, - }) - characterSelectionSelector:addChild(characterButton) - characterSelectionSelector:addChild(ui.Label({id = "character", hAlign = "center", vAlign = "bottom"})) - local stageSelectionSelector = ui.UiElement({ - layout = ui.Layouts.VerticalFlexLayout, - vAlign = "center", - vFill = true, - }) - local stageButton = ui.Button({ - hAlign = "center", - vAlign = "center", - minWidth = 84, - minHeight = 84, - backgroundColor = {1, 1, 1, 0}, - }) local stageImage = ui.ImageContainer({ image = stages[GAME.localPlayer.settings.selectedStageId].images.thumbnail, drawBorders = true, @@ -101,68 +102,40 @@ function DesignHelper:load() hFill = true, vFill = true, }) + local stageSelectionSelector, stageButton = getSelectorTemplate("stage") stageButton:addChild(stageImage) - stageSelectionSelector:addChild(stageButton) - stageSelectionSelector:addChild(ui.Label({id = "stage", hAlign = "center", vAlign = "bottom"})) - - - local panelSelectionSelector = ui.UiElement({ - layout = ui.Layouts.VerticalFlexLayout, - vAlign = "center", - vFill = true, - }) - local panelButton = ui.Button({ - hAlign = "center", - vAlign = "center", - minWidth = 84, - minHeight = 84, - backgroundColor = {1, 1, 1, 0}, - }) - local panelSize = 28 - local panelContainer = ui.UiElement({ - layout = ui.Layouts.HorizontalWrapLayout, - maxWidth = 3 * panelSize - }) - - for color = 1, 8 do - local panelImage = ui.ImageContainer({ - image = panels[GAME.localPlayer.settings.panelId].displayIcons[color], - width = panelSize, - height = panelSize, - }) - panelContainer:addChild(panelImage) - end - panelContainer:addChild(ui.ImageContainer({ - image = panels[GAME.localPlayer.settings.panelId].greyPanel, - width = panelSize, - height = panelSize, - })) - panelButton:addChild(panelContainer) - panelSelectionSelector:addChild(panelButton) - panelSelectionSelector:addChild(ui.Label({id = "panels", hAlign = "center", vAlign = "bottom"})) - - local levelSelectionSelector = ui.UiElement({ - layout = ui.Layouts.VerticalFlexLayout, - vAlign = "center", - vFill = true, - }) + local panelSelectionSelector, panelButton = getSelectorTemplate("panels") + + -- local panelSize = 28 + -- local panelContainer = ui.UiElement({ + -- layout = ui.Layouts.HorizontalWrapLayout, + -- maxWidth = 3 * panelSize + -- }) + + -- for color = 1, 8 do + -- local panelImage = ui.ImageContainer({ + -- image = panels[GAME.localPlayer.settings.panelId].displayIcons[color], + -- width = panelSize, + -- height = panelSize, + -- }) + -- panelContainer:addChild(panelImage) + -- end + -- panelContainer:addChild(ui.ImageContainer({ + -- image = panels[GAME.localPlayer.settings.panelId].greyPanel, + -- width = panelSize, + -- height = panelSize, + -- })) + --panelButton:addChild(panelContainer) - local levelButton = ui.Button({ - hAlign = "center", - vAlign = "center", - minWidth = 84, - minHeight = 84, - backgroundColor = {1, 1, 1, 0}, - }) local levelImage = ui.ImageContainer({ image = GAME.theme.images.IMG_levels[GAME.localPlayer.settings.level or 1], hFill = true, vFill = true, }) + local levelSelectionSelector, levelButton = getSelectorTemplate("level") levelButton:addChild(levelImage) - levelSelectionSelector:addChild(levelButton) - levelSelectionSelector:addChild(ui.Label({id = "level", hAlign = "center", vAlign = "bottom"})) + subSelectionSelector:addChild(characterSelectionSelector) subSelectionSelector:addChild(stageSelectionSelector) @@ -172,7 +145,7 @@ function DesignHelper:load() --subSelectionSelector:addChild(ui.Label({text = "Input Selection"})) --subSelectionSelector:addChild(ui.Label({text = "Puzzle"})) --subSelectionSelector:addChild(ui.Label({text = "Attack File"})) - + local readyButton = ui.Button({ hAlign = "center", vAlign = "center", @@ -198,10 +171,9 @@ function DesignHelper:load() minHeight = 200, vFill = true, padding = 8, - backgroundColor = {0.7, 0, 0.5, 1}, + backgroundColor = {0, 1, 0, 0.5}--{0.7, 0, 0.5, 1}, }) - subSelectionSelector.receiveInputs = function() end subSelection.receiveInputs = function() end self.uiRoot:addChild(subSelection) @@ -231,7 +203,7 @@ end function DesignHelper:update(dt) self.cursor:receiveInputs(inputs, dt) - if inputs.isDown["MenuEsc"] then + if inputs.isDown["MenuEsc"] and not self.cursor.focused then GAME.navigationStack:pop() end end diff --git a/client/src/ui/Cursor.lua b/client/src/ui/Cursor.lua index 6f365a02..1bc4ca87 100644 --- a/client/src/ui/Cursor.lua +++ b/client/src/ui/Cursor.lua @@ -134,8 +134,8 @@ function Cursor:setTarget(uiElement) end function Cursor:drawSelf() - GraphicsUtil.setColor(0, 0, 0, 0.2) - love.graphics.rectangle("fill", self.target.x, self.target.y, self.target.width, self.target.height) + -- GraphicsUtil.setColor(0, 0, 0, 0.2) + -- love.graphics.rectangle("fill", self.target.x, self.target.y, self.target.width, self.target.height) GraphicsUtil.setColor(1, 1, 1, 0.2) local x, y = self.hovered:getScreenPos() love.graphics.rectangle("fill", x, y, self.hovered.width, self.hovered.height) diff --git a/client/src/ui/Layouts/HorizontalWrapLayout.lua b/client/src/ui/Layouts/HorizontalWrapLayout.lua index 1b49b222..6afb3851 100644 --- a/client/src/ui/Layouts/HorizontalWrapLayout.lua +++ b/client/src/ui/Layouts/HorizontalWrapLayout.lua @@ -6,7 +6,7 @@ local util = require("common.lib.util") local HorizontalWrapLayout = setmetatable({}, {__index = HorizontalFlexLayout}) function HorizontalWrapLayout.getMinWidth(uiElement) - return util.bound(uiElement.minWidth, HorizontalFlexLayout.getMinWidth(uiElement), uiElement.maxWidth) + return util.bound(uiElement.minWidth, uiElement:getBaseWidth(), uiElement.maxWidth) end ---@param uiElement UiElement @@ -26,8 +26,7 @@ function HorizontalWrapLayout.getMinHeight(uiElement) rowCount = rowCount + 1 width = uiElement.padding + child.newWidth childrenInCurrentRow = 1 - h = h + maxHeight - maxHeight = 0 + maxHeight = child.newHeight else childrenInCurrentRow = childrenInCurrentRow + 1 width = width + child.newWidth @@ -37,6 +36,10 @@ function HorizontalWrapLayout.getMinHeight(uiElement) end uiElement.tempRows[rowCount] = maxHeight + h = h + (#uiElement.tempRows - 1) * uiElement.childGap + for i = 1, #uiElement.tempRows do + h = h + uiElement.tempRows[i] + end return h end diff --git a/client/src/ui/UniSizedContainer.lua b/client/src/ui/UniSizedContainer.lua new file mode 100644 index 00000000..b2f2813d --- /dev/null +++ b/client/src/ui/UniSizedContainer.lua @@ -0,0 +1,188 @@ +local PATH = (...):gsub('%.[^%.]+$', '') +local UiElement = require(PATH .. ".UIElement") +local class = require("common.lib.class") +local HorizontalWrapLayout = require(PATH .. ".Layouts.HorizontalWrapLayout") +local Focusable = require(PATH .. ".Focusable") +local consts = require("common.engine.consts") +local GraphicsUtil = require("client.src.graphics.graphics_util") + +---@class UniSizedContainer : UiElement +---@field childrenWidth integer +---@field childrenHeight integer +---@field selectedRow integer +---@field selectedColumn integer +---@field rows UiElement[][] +local UniSizedContainer = class( +function(self, options) + assert(options.childrenHeight and options.childrenWidth) + + self.childrenWidth = options.childrenWidth + self.childrenHeight = options.childrenHeight + self.minHeight = self.padding * 2 + self.childrenHeight + + self.hAlignChildren = "left" + self.vAlignChildren = "top" + + self.selectedRow = 1 + self.selectedColumn = 1 +end, +UiElement) + +Focusable(UniSizedContainer) +UniSizedContainer.layout = HorizontalWrapLayout + +---@param uiElement UiElement +---@param index integer? +function UniSizedContainer:addChild(uiElement, index) + uiElement.minWidth = self.childrenWidth + uiElement.maxWidth = self.childrenWidth + uiElement.minHeight = self.childrenHeight + uiElement.maxHeight = math.min(self.childrenHeight, uiElement.maxHeight) + uiElement.hAlign = self.hAlignChildren + uiElement.vAlign = self.vAlignChildren + + UiElement.addChild(self, uiElement, index) +end + +---@return boolean? # if the movement was successful +function UniSizedContainer:moveToPreviousRow() + if not self.rows or #self.rows == 1 then + return false + end + + local nextRow = wrap(1, self.selectedRow - 1, #self.rows) + if self.rows[nextRow][self.selectedColumn] then + self.selectedRow = nextRow + return true + else + return false + end +end + +---@return boolean? # if the movement was successful +function UniSizedContainer:moveToNextRow() + if not self.rows or #self.rows == 1 then + return false + end + + local nextRow = wrap(1, self.selectedRow + 1, #self.rows) + if self.rows[nextRow][self.selectedColumn] then + self.selectedRow = nextRow + return true + else + return false + end +end + +---@return boolean? # if the movement was successful +function UniSizedContainer:moveToPrevious() + if not self.rows or not self.rows[self.selectedRow] or #self.rows[self.selectedRow] == 1 then + return false + end + + self.selectedColumn = wrap(1, self.selectedColumn - 1, #self.rows[self.selectedRow]) + return true +end + +---@return boolean? # if the movement was successful +function UniSizedContainer:moveToNext() + if not self.rows or not self.rows[self.selectedRow] or #self.rows[self.selectedRow] == 1 then + return false + end + + self.selectedColumn = wrap(1, self.selectedColumn + 1, #self.rows[self.selectedRow]) + return true +end + +function UniSizedContainer:receiveInputs(inputs, dt) + if self.focused then + self.focused:receiveInputs(inputs, dt, self.player) + elseif inputs.isDown.Swap2 then + if self.yieldFocus then + GAME.theme:playCancelSfx() + self:yieldFocus() + end + elseif inputs:isPressedWithRepeat("Left", consts.KEY_DELAY, consts.KEY_REPEAT_PERIOD) then + if self:moveToPrevious() then + GAME.theme:playMoveSfx() + end + elseif inputs:isPressedWithRepeat("Right", consts.KEY_DELAY, consts.KEY_REPEAT_PERIOD) then + if self:moveToNext() then + GAME.theme:playMoveSfx() + end + elseif inputs:isPressedWithRepeat("Up", consts.KEY_DELAY, consts.KEY_REPEAT_PERIOD) then + if self:moveToPreviousRow() then + GAME.theme:playMoveSfx() + end + elseif inputs:isPressedWithRepeat("Down", consts.KEY_DELAY, consts.KEY_REPEAT_PERIOD) then + if self:moveToNextRow() then + GAME.theme:playMoveSfx() + end + elseif inputs.isDown.Swap1 or inputs.isDown.Start then + if self.hovered.isFocusable then + GAME.theme:playValidationSfx() + self:setFocus(self.hovered) + elseif self.hovered.receiveInputs then + self.hovered:receiveInputs(inputs, dt) + else + GAME.theme:playCancelSfx() + end + end +end + +function UniSizedContainer:onResize() + if #self.children == 0 then + return + end + + local selectedChild + if self.rows then + selectedChild = self.rows[self.selectedRow][self.selectedColumn] + end + + local rowIndex = 1 + local rows = {{}} + local yOffset = self.padding + for i, child in ipairs(self.children) do + if child.y > yOffset then + yOffset = child.y + rowIndex = rowIndex + 1 + rows[rowIndex] = {} + end + table.insert(rows[rowIndex], child) + end + + self.rows = rows + + if selectedChild then + for row = 1, #self.rows do + for col = 1, #self.rows[row] do + if self.rows[row][col] == selectedChild then + self.selectedRow = row + self.selectedColumn = col + break + end + end + end + end +end + +function UniSizedContainer:drawSelf() + if self.hasFocus then + local selectedChild = self.rows[self.selectedRow][self.selectedColumn] + local x, y = selectedChild:getScreenPos() + GraphicsUtil.setColor(1, 1, 1, 0.2) + love.graphics.rectangle("fill", x, y, selectedChild.width, selectedChild.height) + GraphicsUtil.setColor(1, 1, 1, 1) + end +end + +function UniSizedContainer:getBaseWidth() + return self.padding * 2 + self.childrenWidth +end + +function UniSizedContainer:getBaseHeight() + +end + +return UniSizedContainer \ No newline at end of file diff --git a/client/src/ui/init.lua b/client/src/ui/init.lua index 4c45b73a..34c5ef00 100644 --- a/client/src/ui/init.lua +++ b/client/src/ui/init.lua @@ -55,6 +55,9 @@ local ui = { ---@type fun(options:UiElementOptions): UiElement ---@class UiElement UiElement = require(PATH .. ".UIElement"), + ---@see UniSizedContainer + ---@type fun(options:UiElementOptions): UniSizedContainer) + UniSizedContainer = require(PATH .. ".UniSizedContainer"), ValueLabel = require(PATH .. ".ValueLabel"), ---@see VerticalMenu ---@type fun(options: ScrollContainerOptions): VerticalMenu From 0728309aa3062f4762a678fe3453e9db34886497 Mon Sep 17 00:00:00 2001 From: Endaris Date: Mon, 26 May 2025 18:45:39 +0200 Subject: [PATCH 17/49] fix most things about wrapping containers with children --- client/src/Game.lua | 7 +- client/src/graphics/graphics_util.lua | 8 ++ client/src/scenes/DesignHelper.lua | 13 ++- client/src/scenes/MainMenu.lua | 2 +- client/src/scenes/PortraitGame.lua | 4 +- client/src/scenes/StartUp.lua | 2 +- client/src/ui/ImageContainer.lua | 2 +- client/src/ui/Label.lua | 14 +--- client/src/ui/Layouts/FlexLayout.lua | 31 ++++--- .../src/ui/Layouts/HorizontalFlexLayout.lua | 34 +++++--- .../src/ui/Layouts/HorizontalScrollLayout.lua | 6 +- .../src/ui/Layouts/HorizontalWrapLayout.lua | 43 +++------- client/src/ui/Layouts/Layout.lua | 81 +++++++++++++++---- client/src/ui/Layouts/StaticLayout.lua | 8 +- client/src/ui/Layouts/VerticalFlexLayout.lua | 25 +++++- .../src/ui/Layouts/VerticalScrollLayout.lua | 2 +- client/src/ui/PassThroughElement.lua | 30 +++++++ client/src/ui/ScrollContainer.lua | 8 +- client/src/ui/UIElement.lua | 57 +++---------- client/src/ui/UniSizedContainer.lua | 56 +++++++++++-- client/src/ui/init.lua | 3 + main.lua | 2 +- 22 files changed, 281 insertions(+), 157 deletions(-) create mode 100644 client/src/ui/PassThroughElement.lua diff --git a/client/src/Game.lua b/client/src/Game.lua index 510dd7f0..13c68357 100644 --- a/client/src/Game.lua +++ b/client/src/Game.lua @@ -355,14 +355,17 @@ function Game:handleResize(newWidth, newHeight) activeScene.uiRoot.minWidth = newWidth activeScene.uiRoot.minHeight = newHeight activeScene.uiRoot.layout.resize(activeScene.uiRoot, newWidth, newHeight) + if activeScene.uiRoot.width ~= newWidth or activeScene.uiRoot.height ~= newHeight then + GraphicsUtil.updateMode(activeScene.uiRoot.width, activeScene.uiRoot.height) + end end self:updateCanvasPositionAndScale(newWidth, newHeight) if self.battleRoom and self.battleRoom.match then self.needsAssetReload = true else - self:refreshCanvasAndImagesForNewScale() + --self:refreshCanvasAndImagesForNewScale() end - self.showGameScaleUntil = self.timer + 5 + --self.showGameScaleUntil = self.timer + 5 end -- Called every few fractions of a second to update the game diff --git a/client/src/graphics/graphics_util.lua b/client/src/graphics/graphics_util.lua index bcae14dd..5c625786 100644 --- a/client/src/graphics/graphics_util.lua +++ b/client/src/graphics/graphics_util.lua @@ -432,4 +432,12 @@ else GraphicsUtil.newText = love.graphics.newText end +---@param width number # Window width. +---@param height number # Window height. +---@param settings {fullscreen: boolean, fullscreentype: love.FullscreenType, vsync: boolean, msaa: number, resizable: boolean, borderless: boolean, centered: boolean, display: number, minwidth: number, minheight: number, highdpi: boolean, x: number, y: number}? # The settings table with the following optional fields. Any field not filled in will use the current value that would be returned by love.window.getMode. +---@return boolean success # True if successful, false otherwise. +function GraphicsUtil.updateMode(width, height, settings) + return love.window.updateMode(width, height, settings or {}) +end + return GraphicsUtil diff --git a/client/src/scenes/DesignHelper.lua b/client/src/scenes/DesignHelper.lua index d3b49638..f20de8d1 100644 --- a/client/src/scenes/DesignHelper.lua +++ b/client/src/scenes/DesignHelper.lua @@ -75,13 +75,19 @@ function DesignHelper:load() self.uiRoot:addChild(gameMode) + local passThroughSelector = ui.PassThroughElement({ + backgroundColor = {0, 1, 0, 0.5}, + hAlign = "center", + hFill = true, + }) + local subSelectionSelector = ui.UniSizedContainer({ childGap = 32, padding = 8, - hFill = true, backgroundColor = {0, 1, 0, 0.5}, childrenWidth = 84, childrenHeight = 112, + hAlign = "center", --scrollOrientation = "horizontal", }) @@ -164,6 +170,8 @@ function DesignHelper:load() subSelectionSelector:addChild(readyButton) subSelectionSelector:addChild(leaveButton) + --passThroughSelector:addChild(subSelectionSelector) + --self.uiRoot:addChild(passThroughSelector) self.uiRoot:addChild(subSelectionSelector) local subSelection = ui.UiElement({ @@ -201,11 +209,10 @@ function DesignHelper:loadStages() end function DesignHelper:update(dt) - self.cursor:receiveInputs(inputs, dt) - if inputs.isDown["MenuEsc"] and not self.cursor.focused then GAME.navigationStack:pop() end + self.cursor:receiveInputs(inputs, dt) end function DesignHelper:draw() diff --git a/client/src/scenes/MainMenu.lua b/client/src/scenes/MainMenu.lua index 1546b825..72338b45 100644 --- a/client/src/scenes/MainMenu.lua +++ b/client/src/scenes/MainMenu.lua @@ -157,10 +157,10 @@ function MainMenu:createMainMenu() end menuContainer:addChild(options) menuContainer:addChild(fullscreenToggle) - menuContainer:addChild(quit) if config.debugShowDesignHelper then menuContainer:addChild(designHelper) end + menuContainer:addChild(quit) return menuContainer end diff --git a/client/src/scenes/PortraitGame.lua b/client/src/scenes/PortraitGame.lua index 073bd328..36bb95c4 100644 --- a/client/src/scenes/PortraitGame.lua +++ b/client/src/scenes/PortraitGame.lua @@ -193,7 +193,7 @@ function PortraitGame:flipToPortrait() local width, height, _ = love.window.getMode() if system.isMobileOS() or DEBUG_ENABLED then -- flip the window dimensions to portrait - love.window.updateMode(height, width, {}) + GraphicsUtil.updateMode(height, width, {}) love.window.setFullscreen(true) --GAME:updateCanvasPositionAndScale(width, height) end @@ -240,7 +240,7 @@ function PortraitGame:returnToLandscape() -- flip the window dimensions to landscape local width, height, _ = love.window.getMode() if system.isMobileOS() or DEBUG_ENABLED then - love.window.updateMode(height, width, {}) + GraphicsUtil.updateMode(height, width, {}) love.window.setFullscreen(false) --GAME:updateCanvasPositionAndScale(width, height) end diff --git a/client/src/scenes/StartUp.lua b/client/src/scenes/StartUp.lua index 1694fb28..c133d56b 100644 --- a/client/src/scenes/StartUp.lua +++ b/client/src/scenes/StartUp.lua @@ -115,7 +115,7 @@ function StartUp:migrate() self.migrationPath = nil self.migrationMessage = nil readConfigFile(config) - love.window.updateMode(config.windowWidth, config.windowHeight, + GraphicsUtil.updateMode(config.windowWidth, config.windowHeight, { x = config.windowX, y = config.windowY, diff --git a/client/src/ui/ImageContainer.lua b/client/src/ui/ImageContainer.lua index e0e7d9f3..94c7bbad 100644 --- a/client/src/ui/ImageContainer.lua +++ b/client/src/ui/ImageContainer.lua @@ -38,7 +38,7 @@ function ImageContainer:setImage(image, width, height, scale) end end -function ImageContainer:onResize() +function ImageContainer:onResized() self.scale = math.min(self.width / self.imageWidth, self.height / self.imageHeight) -- self.width = self.imageWidth * self.scale -- self.height = self.imageHeight * self.scale diff --git a/client/src/ui/Label.lua b/client/src/ui/Label.lua index 83c07318..eb5d41e9 100644 --- a/client/src/ui/Label.lua +++ b/client/src/ui/Label.lua @@ -2,6 +2,7 @@ local PATH = (...):gsub('%.[^%.]+$', '') local UIElement = require(PATH .. ".UIElement") local class = require("common.lib.class") local GraphicsUtil = require("client.src.graphics.graphics_util") +local HorizontalWrapLayout = require(PATH .. ".Layouts.HorizontalWrapLayout") ---@class LabelOptions : UiElementOptions ---@field id string? The localization key; nil if there should be no translation @@ -54,7 +55,9 @@ local Label = class( end, UIElement ) + Label.TYPE = "Label" +Label.layout = HorizontalWrapLayout function Label:recalculateSizes() local font = GraphicsUtil.getGlobalFontWithSize(self.fontSize) @@ -106,19 +109,10 @@ function Label:drawSelf() end function Label:getPreferredWidth() - local font = GraphicsUtil.getGlobalFontWithSize(self.fontSize) - return font:getWidth(self.text) -end - -function Label:setMinHeightForWidth() - --self.minHeight = GraphicsUtil.getTextHeightForWidth(self.fontSize, self.text, self.width, self.hAlign) -end - -function Label:getBaseWidth() return self.preferredWidth end -function Label:getBaseHeight() +function Label:getMinHeight() return GraphicsUtil.getTextHeightForWidth(self.fontSize, self.text, self.width, self.hAlign) end diff --git a/client/src/ui/Layouts/FlexLayout.lua b/client/src/ui/Layouts/FlexLayout.lua index 15503108..35c34c00 100644 --- a/client/src/ui/Layouts/FlexLayout.lua +++ b/client/src/ui/Layouts/FlexLayout.lua @@ -1,5 +1,6 @@ local PATH = (...):gsub('%.[^%.]+$', '') local Layout = require(PATH ..".Layout") +local util = require("common.lib.util") ---@class FlexLayout : Layout local FlexLayout = setmetatable({}, {__index = Layout}) @@ -11,8 +12,8 @@ function FlexLayout.fitSizeWidth(uiElement) child.layout.fitSizeWidth(child) end end - local w = uiElement.layout.getMinWidth(uiElement) - uiElement.newWidth = math.max(w, uiElement:getBaseWidth()) + local w = uiElement.layout.getPreferredWidth(uiElement) + uiElement.newWidth = math.max(w, uiElement:getPreferredWidth(), uiElement.minWidth) end ---@param uiElement UiElement @@ -23,23 +24,28 @@ function FlexLayout.fitSizeHeight(uiElement) end end local h = uiElement.layout.getMinHeight(uiElement) - uiElement.newHeight = math.max(h, uiElement:getBaseHeight()) + uiElement.newHeight = math.max(h, uiElement.minHeight) end -function FlexLayout.updateWidths(uiElement, width) +function FlexLayout.setWidth(uiElement, width) if not uiElement.newWidth then uiElement.layout.fitSizeWidth(uiElement) end + if width then - uiElement.width = math.max(width, uiElement.newWidth) + local minWidth = uiElement.layout.getMinWidth(uiElement) + if not uiElement.controlsWindow then + minWidth = math.max(minWidth, uiElement.minWidth) + end + uiElement.width = util.bound(minWidth, uiElement.newWidth, width) else - uiElement.width = uiElement.newWidth + uiElement.width = util.bound(uiElement.width, uiElement.newWidth, uiElement.maxWidth) end + uiElement.newWidth = nil - uiElement.layout.growChildrenWidth(uiElement) end -function FlexLayout.updateHeights(uiElement, height) +function FlexLayout.setHeight(uiElement, height) if not uiElement.newHeight then uiElement.layout.fitSizeHeight(uiElement) end @@ -49,17 +55,16 @@ function FlexLayout.updateHeights(uiElement, height) uiElement.height = uiElement.newHeight end uiElement.newHeight = nil - uiElement.layout.growChildrenHeight(uiElement) end ---@param uiElement UiElement -function FlexLayout.growChildrenWidth(uiElement) - error("FlexLayout does not implement growChildrenWidth") +function FlexLayout.finalizeChildrenWidths(uiElement) + error("FlexLayout does not implement finalizeChildrenWidths") end ---@param uiElement UiElement -function FlexLayout.growChildrenHeight(uiElement) - error("FlexLayout does not implement growChildrenHeight") +function FlexLayout.finalizeChildrenHeights(uiElement) + error("FlexLayout does not implement finalizeChildrenHeights") end return FlexLayout \ No newline at end of file diff --git a/client/src/ui/Layouts/HorizontalFlexLayout.lua b/client/src/ui/Layouts/HorizontalFlexLayout.lua index 33d58486..234b231d 100644 --- a/client/src/ui/Layouts/HorizontalFlexLayout.lua +++ b/client/src/ui/Layouts/HorizontalFlexLayout.lua @@ -5,12 +5,25 @@ local FlexLayout = require(PATH ..".FlexLayout") local HorizontalFlexLayout = setmetatable({characteristic = "horizontal"}, {__index = FlexLayout}) ---@param uiElement UiElement +---@return number # the minimum width of the element as dictated by its children function HorizontalFlexLayout.getMinWidth(uiElement) local w = uiElement.padding * 2 + uiElement.childGap * (#uiElement.children - 1) for _, child in ipairs(uiElement.children) do if child.isVisible then - w = w + child.newWidth + w = w + math.max(child.layout.getMinWidth(child), child.minWidth) + end + end + + return w +end + +function HorizontalFlexLayout.getPreferredWidth(uiElement) + local w = uiElement.padding * 2 + uiElement.childGap * (#uiElement.children - 1) + + for _, child in ipairs(uiElement.children) do + if child.isVisible then + w = w + math.max(child.layout.getMinWidth(child), child:getPreferredWidth()) end end @@ -35,12 +48,16 @@ local growables = {} local shrinkables = {} ---@param uiElement UiElement -function HorizontalFlexLayout.growChildrenWidth(uiElement) +function HorizontalFlexLayout.finalizeChildrenWidths(uiElement) if #uiElement.children == 0 then return end - local remainingWidth = uiElement.width - uiElement.layout.getMinWidth(uiElement) + local remainingWidth = uiElement.width + + for _, child in ipairs(uiElement.children) do + remainingWidth = remainingWidth - child.newWidth + end if remainingWidth >= 1 then table.clear(growables) @@ -105,11 +122,11 @@ function HorizontalFlexLayout.growChildrenWidth(uiElement) end end end - elseif remainingWidth <= 1 then + elseif remainingWidth <= -1 then table.clear(shrinkables) for i, child in ipairs(uiElement.children) do - if child.isVisible and child.hFill and child.newWidth > child.minWidth then + if child.isVisible and child.newWidth > child.minWidth then shrinkables[#shrinkables+1] = child end end @@ -134,7 +151,7 @@ function HorizontalFlexLayout.growChildrenWidth(uiElement) end end - local delta = secondBiggest - biggest + local delta = math.abs(secondBiggest - biggest) local widthToSubtract = math.min(delta, math.abs(remainingWidth / biggestCount)) if delta * biggestCount >= -remainingWidth then @@ -171,14 +188,11 @@ function HorizontalFlexLayout.growChildrenWidth(uiElement) end ---@param uiElement UiElement -function HorizontalFlexLayout.growChildrenHeight(uiElement) +function HorizontalFlexLayout.finalizeChildrenHeights(uiElement) for _, child in ipairs(uiElement.children) do if child.vFill then child.newHeight = math.min(uiElement.height - uiElement.padding * 2, child.maxHeight) end - if child.layout.growChildrenHeight then - child.layout.growChildrenHeight(child) - end end end diff --git a/client/src/ui/Layouts/HorizontalScrollLayout.lua b/client/src/ui/Layouts/HorizontalScrollLayout.lua index 88cbde0a..0be5692c 100644 --- a/client/src/ui/Layouts/HorizontalScrollLayout.lua +++ b/client/src/ui/Layouts/HorizontalScrollLayout.lua @@ -9,7 +9,11 @@ function HorizontalScrollLayout.getMinWidth(uiElement) return util.bound(uiElement.minWidth, HorizontalFlexLayout.getMinWidth(uiElement), uiElement.maxWidth) end -function HorizontalScrollLayout.growChildrenWidth(uiElement) +function HorizontalScrollLayout.getPreferredWidth(uiElement) + return util.bound(uiElement.minWidth, HorizontalFlexLayout.getPreferredWidth(uiElement), uiElement.maxWidth) +end + +function HorizontalScrollLayout.finalizeChildrenWidths(uiElement) -- no growing because we scroll in this direction end diff --git a/client/src/ui/Layouts/HorizontalWrapLayout.lua b/client/src/ui/Layouts/HorizontalWrapLayout.lua index 6afb3851..c17af5d5 100644 --- a/client/src/ui/Layouts/HorizontalWrapLayout.lua +++ b/client/src/ui/Layouts/HorizontalWrapLayout.lua @@ -5,48 +5,31 @@ local util = require("common.lib.util") ---@class HorizontalWrapLayout : HorizontalFlexLayout local HorizontalWrapLayout = setmetatable({}, {__index = HorizontalFlexLayout}) -function HorizontalWrapLayout.getMinWidth(uiElement) - return util.bound(uiElement.minWidth, uiElement:getBaseWidth(), uiElement.maxWidth) +function HorizontalWrapLayout.resize(uiElement, width, height) + HorizontalFlexLayout.resize(uiElement, width, height) end ----@param uiElement UiElement -function HorizontalWrapLayout.getMinHeight(uiElement) - local h = uiElement.padding * 2 - local maxHeight = 0 - uiElement.tempRows = {} +function HorizontalWrapLayout.getMinWidth(uiElement) + local w = uiElement.padding * 2 + local maxWidth = 0 - local childrenInCurrentRow = 0 - local rowCount = 1 - local width = uiElement.padding - for i, child in ipairs(uiElement.children) do + for _, child in ipairs(uiElement.children) do if child.isVisible then - maxHeight = math.max(maxHeight, child.newHeight) - if width + child.newWidth + uiElement.padding + childrenInCurrentRow * uiElement.childGap > uiElement.width then - uiElement.tempRows[rowCount] = maxHeight - rowCount = rowCount + 1 - width = uiElement.padding + child.newWidth - childrenInCurrentRow = 1 - maxHeight = child.newHeight - else - childrenInCurrentRow = childrenInCurrentRow + 1 - width = width + child.newWidth - end - child.tempRow = rowCount + maxWidth = math.max(maxWidth, child.newWidth, child.minWidth) end end - uiElement.tempRows[rowCount] = maxHeight - h = h + (#uiElement.tempRows - 1) * uiElement.childGap - for i = 1, #uiElement.tempRows do - h = h + uiElement.tempRows[i] - end + return w + maxWidth +end - return h +---@param uiElement UiElement +function HorizontalWrapLayout.getMinHeight(uiElement) + return uiElement:getMinHeight() end ---@param uiElement UiElement function HorizontalWrapLayout.positionChildren(uiElement) - if #uiElement.tempRows == 1 then + if not uiElement.tempRows or #uiElement.tempRows == 1 then HorizontalFlexLayout.positionChildren(uiElement) else local x = uiElement.padding diff --git a/client/src/ui/Layouts/Layout.lua b/client/src/ui/Layouts/Layout.lua index 108f16ea..6aad694e 100644 --- a/client/src/ui/Layouts/Layout.lua +++ b/client/src/ui/Layouts/Layout.lua @@ -4,38 +4,79 @@ local LayoutCharacteristics = { none = "none", horizontal = "horizontal", vertic ---@class Layout ---@field characteristic LayoutCharacteristic local Layout = { - characteristic = LayoutCharacteristics.none + characteristic = LayoutCharacteristics.none, + Characteristics = LayoutCharacteristics } ---@param uiElement UiElement ---@param width number? ---@param height number? function Layout.resize(uiElement, width, height) - uiElement.layout.updateWidths(uiElement, width) - -- transform width to height for width-to-height supporting uiElements based on the width pass - uiElement:setMinHeightForWidth() + Layout.updateWidths(uiElement, width) + Layout.updateHeights(uiElement, height) + Layout.updatePositions(uiElement) + Layout.runResizedCallbacks(uiElement) +end - uiElement.layout.updateHeights(uiElement, height) +function Layout.runResizedCallbacks(uiElement) + if uiElement.onResized then + uiElement:onResized() + end - for i, child in ipairs(uiElement.children) do - child.layout.resize(child, child.newWidth, child.newHeight) + for _, child in ipairs(uiElement.children) do + Layout.runResizedCallbacks(child) end +end - uiElement.layout.positionChildren(uiElement) +---@param uiElement UiElement +function Layout.updateWidths(uiElement, width) + uiElement.layout.setWidth(uiElement, width) + uiElement.layout.finalizeChildrenWidths(uiElement) - if uiElement.onResize then - uiElement:onResize() + for i, child in ipairs(uiElement.children) do + if child.isVisible then + child.layout.updateWidths(child, child.newWidth) + end end end ---@param uiElement UiElement -function Layout.updateWidths(uiElement, width) - error("Layout does not implement positionChildren") +---@param width integer? +function Layout.setWidth(uiElement, width) + error("Layout does not implement setWidth") +end + +function Layout.finalizeChildrenWidths(uiElement) + error("Layout does not implement finalizeChildrenWidths") end ---@param uiElement UiElement function Layout.updateHeights(uiElement, height) - error("Layout does not implement positionChildren") + uiElement.layout.setHeight(uiElement, height) + uiElement.layout.finalizeChildrenHeights(uiElement) + + for i, child in ipairs(uiElement.children) do + if child.isVisible then + child.layout.updateHeights(child, child.newHeight) + end + end +end + +function Layout.setHeight(uiElement, height) + error("Layout does not implement setHeight") +end + +function Layout.finalizeChildrenHeights(uiElement) + error("Layout does not implement finalizeChildrenHeights") +end + +function Layout.updatePositions(uiElement) + uiElement.layout.positionChildren(uiElement) + for _, child in ipairs(uiElement.children) do + if child.isVisible then + child.layout.updatePositions(child) + end + end end ---@param uiElement UiElement @@ -44,15 +85,21 @@ function Layout.positionChildren(uiElement) end ---@param uiElement UiElement ----@return number +---@return number # the minimum width of the element as dictated by its children function Layout.getMinWidth(uiElement) - error("FlexLayout does not implement getMinWidth") + error("Layout does not implement getMinWidth") end ---@param uiElement UiElement ----@return number +---@return number # the minimum height of the element as dictated by its children function Layout.getMinHeight(uiElement) - error("FlexLayout does not implement getMinHeight") + error("Layout does not implement getMinHeight") +end + +---@param uiElement UiElement +---@return number # the preferred width of the element based on the preferred widths of its children +function Layout.getPreferredWidth(uiElement) + error("Layout does not implement getPreferredWidth") end diff --git a/client/src/ui/Layouts/StaticLayout.lua b/client/src/ui/Layouts/StaticLayout.lua index a22e4f47..7647a161 100644 --- a/client/src/ui/Layouts/StaticLayout.lua +++ b/client/src/ui/Layouts/StaticLayout.lua @@ -5,11 +5,11 @@ local Layout = require(PATH ..".Layout") local StaticLayout = setmetatable({}, {__index = Layout}) ---@param uiElement UiElement -function StaticLayout.updateWidths(uiElement, width) +function StaticLayout.setWidth(uiElement, width) end ---@param uiElement UiElement -function StaticLayout.updateHeights(uiElement, height) +function StaticLayout.setHeight(uiElement, height) end ---@param uiElement UiElement @@ -22,6 +22,10 @@ function StaticLayout.getMinWidth(uiElement) return uiElement.width end +function StaticLayout.getPreferredWidth(uiElement) + return uiElement.width +end + ---@param uiElement UiElement ---@return number function StaticLayout.getMinHeight(uiElement) diff --git a/client/src/ui/Layouts/VerticalFlexLayout.lua b/client/src/ui/Layouts/VerticalFlexLayout.lua index 74fffbe0..1ffbf090 100644 --- a/client/src/ui/Layouts/VerticalFlexLayout.lua +++ b/client/src/ui/Layouts/VerticalFlexLayout.lua @@ -5,13 +5,27 @@ local FlexLayout = require(PATH ..".FlexLayout") local VerticalFlexLayout = setmetatable({characteristic = "vertical"}, {__index = FlexLayout}) ---@param uiElement UiElement +---@return number # the minimum width of the element as dictated by its children function VerticalFlexLayout.getMinWidth(uiElement) local w = uiElement.padding * 2 local maxWidth = 0 for _, child in ipairs(uiElement.children) do if child.isVisible then - maxWidth = math.max(maxWidth, child.newWidth) + maxWidth = math.max(maxWidth, child.layout.getMinWidth(child)) + end + end + + return w + maxWidth +end + +function VerticalFlexLayout.getPreferredWidth(uiElement) + local w = uiElement.padding * 2 + local maxWidth = 0 + + for _, child in ipairs(uiElement.children) do + if child.isVisible then + maxWidth = math.max(maxWidth, child.layout.getMinWidth(child), child:getPreferredWidth()) end end @@ -32,7 +46,7 @@ function VerticalFlexLayout.getMinHeight(uiElement) end ---@param uiElement UiElement -function VerticalFlexLayout.growChildrenHeight(uiElement) +function VerticalFlexLayout.finalizeChildrenHeights(uiElement) if #uiElement.children == 0 then return end @@ -103,10 +117,13 @@ function VerticalFlexLayout.growChildrenHeight(uiElement) end ---@param uiElement UiElement -function VerticalFlexLayout.growChildrenWidth(uiElement) +function VerticalFlexLayout.finalizeChildrenWidths(uiElement) + local maxWidth = uiElement.width - uiElement.padding * 2 for _, child in ipairs(uiElement.children) do if child.hFill then - child.newWidth = math.min(uiElement.width - uiElement.padding * 2, child.maxWidth) + child.newWidth = math.min(maxWidth, child.maxWidth) + else + child.newWidth = math.min(maxWidth, child.newWidth) end end end diff --git a/client/src/ui/Layouts/VerticalScrollLayout.lua b/client/src/ui/Layouts/VerticalScrollLayout.lua index 62604f63..6b93de79 100644 --- a/client/src/ui/Layouts/VerticalScrollLayout.lua +++ b/client/src/ui/Layouts/VerticalScrollLayout.lua @@ -9,7 +9,7 @@ function VerticalScrollLayout.getMinHeight(uiElement) return util.bound(uiElement.minHeight, VerticalFlexLayout.getMinHeight(uiElement), uiElement.maxHeight) end -function VerticalScrollLayout.growChildrenHeight(uiElement) +function VerticalScrollLayout.finalizeChildrenHeights(uiElement) -- no growing because we scroll in this direction end diff --git a/client/src/ui/PassThroughElement.lua b/client/src/ui/PassThroughElement.lua new file mode 100644 index 00000000..06f80d57 --- /dev/null +++ b/client/src/ui/PassThroughElement.lua @@ -0,0 +1,30 @@ +local PATH = (...):gsub('%.[^%.]+$', '') +local UIElement = require(PATH .. ".UIElement") +local class = require("common.lib.class") +local Focusable = require(PATH .. ".Focusable") + +--- A plain element with the main purpose of giving a container child an alignment without disrupting cursor focus +---@class PassThroughElement : UiElement, Focusable +local PassThroughElement = class( +function (self, options) +end, +UIElement) + +Focusable(PassThroughElement) + +function PassThroughElement:addChild(uiElement) + if self.children[1] then + error("A PassThroughElement can only have one child") + end + uiElement.yieldFocus = function() + self.yieldFocus() + end + UIElement.addChild(self, uiElement) +end + +function PassThroughElement:receiveInputs(inputs, dt) + self.children[1]:receiveInputs(inputs, dt) +end + + +return PassThroughElement \ No newline at end of file diff --git a/client/src/ui/ScrollContainer.lua b/client/src/ui/ScrollContainer.lua index c03d65b7..75c9a00d 100644 --- a/client/src/ui/ScrollContainer.lua +++ b/client/src/ui/ScrollContainer.lua @@ -212,15 +212,11 @@ function ScrollContainer:recalculateMaxScrollOffset() end end -function ScrollContainer:onResize() +function ScrollContainer:onResized() self:recalculateMaxScrollOffset() end -function ScrollContainer:getBaseHeight() - return self.minHeight -end - -function ScrollContainer:getBaseWidth() +function ScrollContainer:getPreferredWidth() return self.minWidth end diff --git a/client/src/ui/UIElement.lua b/client/src/ui/UIElement.lua index cf839e88..ac4cca64 100644 --- a/client/src/ui/UIElement.lua +++ b/client/src/ui/UIElement.lua @@ -1,17 +1,17 @@ local class = require("common.lib.class") +local GraphicsUtil = require("client.src.graphics.graphics_util") ---@alias color { [1]: number, [2]: number, [3]: number, [4]: number } ---@class UiElement ---@field x number relative x offset to the parent element (canvas if no parent) ---@field y number relative y offset to the parent element (canvas if no parent) ----@field minWidth number +---@field minWidth number the minimum width of the element, independent of children and layout ---@field width number width of the element for the sake of resizing children and touch hitboxes ---@field maxWidth number ----@field minHeight number +---@field minHeight number the minimum height of the element, independent of children and layout ---@field height number height of the element for the sake of resizing children and touch hitboxes ---@field maxHeight number ----@field fixedHeight boolean ---@field hAlign ("left" | "center" | "right") determines the horizontal alignment relative to the parent ---@field vAlign ("top" | "center" | "bottom") determines the vertical alignment relative to the parent ---@field hFill boolean if the element's width should fill out the entire parent's width @@ -29,6 +29,7 @@ local class = require("common.lib.class") ---@field onDrag function? touch callback for when the mouse touching the element is dragged across the screen ---@field onRelease function? touch callback for when the mouse touching the element is released ---@field onHold function? touch callback for when a touch is held on the element for a longer duration +---@field onResized function? layout callback for when a UIElement and all of its children have been resized and positioned ---@field [any] any ---@class UiElementOptions @@ -120,11 +121,11 @@ function UIElement:onChildrenChanged() local oWidth = self.width local oHeight = self.height - self.layout.resize(self) - + if self.controlsWindow then + self.layout.resize(self) if oWidth ~= self.width or oHeight ~= self.height then - love.window.updateMode(self.width, self.height, {}) + GraphicsUtil.updateMode(self.width, self.height, {}) end end end @@ -260,48 +261,10 @@ function UIElement:getTouchedElement(x, y) end end -function UIElement:setMinHeightForWidth() - -- local childrenInCurrentRow = 0 - -- local rowCount = 1 - -- local width = self.padding - -- for i, child in ipairs(self.children) do - -- if width + child.width + self.padding + childrenInCurrentRow * self.childGap > self.width then - -- rowCount = rowCount + 1 - -- width = self.padding + child.width - -- childrenInCurrentRow = 1 - -- else - -- childrenInCurrentRow = childrenInCurrentRow + 1 - -- width = width + child.width - -- end - -- end - - -- return self.minHeight * rowCount -end - -function UIElement:getBaseWidth() +--- defaults to minWidth; elements that can wrap have to override this getter with a function returning their preferred width +---@return integer # the width the UIElement would prefer to take up if any space is available +function UIElement:getPreferredWidth() return self.minWidth end -function UIElement:getBaseHeight() - -- if self.wraps then - -- local childrenInCurrentRow = 0 - -- local rowCount = 1 - -- local width = self.padding - -- for i, child in ipairs(self.children) do - -- if width + child.width + self.padding + childrenInCurrentRow * self.childGap > self.width then - -- rowCount = rowCount + 1 - -- width = self.padding + child.width - -- childrenInCurrentRow = 1 - -- else - -- childrenInCurrentRow = childrenInCurrentRow + 1 - -- width = width + child.width - -- end - -- end - - -- return self.minHeight * rowCount - -- else - return self.minHeight - --end -end - return UIElement \ No newline at end of file diff --git a/client/src/ui/UniSizedContainer.lua b/client/src/ui/UniSizedContainer.lua index b2f2813d..aee63202 100644 --- a/client/src/ui/UniSizedContainer.lua +++ b/client/src/ui/UniSizedContainer.lua @@ -5,6 +5,7 @@ local HorizontalWrapLayout = require(PATH .. ".Layouts.HorizontalWrapLayout") local Focusable = require(PATH .. ".Focusable") local consts = require("common.engine.consts") local GraphicsUtil = require("client.src.graphics.graphics_util") +local input = require("client.src.inputManager") ---@class UniSizedContainer : UiElement ---@field childrenWidth integer @@ -53,6 +54,7 @@ function UniSizedContainer:moveToPreviousRow() local nextRow = wrap(1, self.selectedRow - 1, #self.rows) if self.rows[nextRow][self.selectedColumn] then self.selectedRow = nextRow + self.hovered = self.rows[self.selectedRow][self.selectedColumn] return true else return false @@ -68,6 +70,7 @@ function UniSizedContainer:moveToNextRow() local nextRow = wrap(1, self.selectedRow + 1, #self.rows) if self.rows[nextRow][self.selectedColumn] then self.selectedRow = nextRow + self.hovered = self.rows[self.selectedRow][self.selectedColumn] return true else return false @@ -81,6 +84,7 @@ function UniSizedContainer:moveToPrevious() end self.selectedColumn = wrap(1, self.selectedColumn - 1, #self.rows[self.selectedRow]) + self.hovered = self.rows[self.selectedRow][self.selectedColumn] return true end @@ -91,6 +95,7 @@ function UniSizedContainer:moveToNext() end self.selectedColumn = wrap(1, self.selectedColumn + 1, #self.rows[self.selectedRow]) + self.hovered = self.rows[self.selectedRow][self.selectedColumn] return true end @@ -130,7 +135,7 @@ function UniSizedContainer:receiveInputs(inputs, dt) end end -function UniSizedContainer:onResize() +function UniSizedContainer:onResized() if #self.children == 0 then return end @@ -168,21 +173,62 @@ function UniSizedContainer:onResize() end function UniSizedContainer:drawSelf() + UiElement.drawSelf(self) if self.hasFocus then local selectedChild = self.rows[self.selectedRow][self.selectedColumn] local x, y = selectedChild:getScreenPos() GraphicsUtil.setColor(1, 1, 1, 0.2) love.graphics.rectangle("fill", x, y, selectedChild.width, selectedChild.height) GraphicsUtil.setColor(1, 1, 1, 1) + + if input.isDown["Left"] or input.isPressed["Left"] then + GraphicsUtil.printf("Left", 0, 0) + elseif input.isDown["Right"] or input.isPressed["Right"] then + GraphicsUtil.printf("Right", 0, 0) + elseif input.isDown["Up"] or input.isPressed["Up"] then + GraphicsUtil.printf("Up", 0, 0) + elseif input.isDown["Down"] or input.isPressed["Down"] then + GraphicsUtil.printf("Down", 0, 0) + end end end -function UniSizedContainer:getBaseWidth() - return self.padding * 2 + self.childrenWidth -end +function UniSizedContainer:getMinHeight() + local h = self.padding * 2 + local maxHeight = 0 + self.tempRows = {} -function UniSizedContainer:getBaseHeight() + local childrenInCurrentRow = 0 + local rowCount = 1 + local width = self.padding + for i, child in ipairs(self.children) do + if child.isVisible then + maxHeight = math.max(maxHeight, child.minHeight, child.layout.getMinHeight(child)) + if width + child.width + self.padding + childrenInCurrentRow * self.childGap > self.width then + self.tempRows[rowCount] = maxHeight + rowCount = rowCount + 1 + width = self.padding + child.width + childrenInCurrentRow = 1 + maxHeight = child.newHeight + else + childrenInCurrentRow = childrenInCurrentRow + 1 + width = width + child.width + end + child.tempRow = rowCount + end + end + + self.tempRows[rowCount] = maxHeight + h = h + (#self.tempRows - 1) * self.childGap + for i = 1, #self.tempRows do + h = h + self.tempRows[i] + end + + return math.max(h, self.minHeight) +end +function UniSizedContainer:getPreferredWidth() + return self.padding * 2 + (self.childrenWidth + self.childGap) * #self.children - self.childGap end return UniSizedContainer \ No newline at end of file diff --git a/client/src/ui/init.lua b/client/src/ui/init.lua index 34c5ef00..0740b1f7 100644 --- a/client/src/ui/init.lua +++ b/client/src/ui/init.lua @@ -35,6 +35,9 @@ local ui = { MultiPlayerSelectionWrapper = require(PATH .. ".MultiPlayerSelectionWrapper"), PagedUniGrid = require(PATH .. ".PagedUniGrid"), PanelCarousel = require(PATH .. ".PanelCarousel"), + ---@see PassThroughElement + ---@type fun(options: UiElementOptions): PassThroughElement + PassThroughElement = require(PATH .. ".PassThroughElement"), ---@see PixelFontLabel ---@type fun(options: PixelFontLabelOptions): PixelFontLabel PixelFontLabel = require(PATH .. ".PixelFontLabel"), diff --git a/main.lua b/main.lua index c033430d..eaa187ff 100644 --- a/main.lua +++ b/main.lua @@ -43,7 +43,7 @@ function love.load(args, rawArgs) love.window.restore() end local offset = math.ceil(desktopHeight / 32) - love.window.updateMode(desktopWidth, desktopHeight - offset, flags) + GraphicsUtil.updateMode(desktopWidth, desktopHeight - offset, flags) love.window.setPosition(x, offset, displayIndex) end From a84081b01010f3081b9205a0e43c5cf5f7252c4c Mon Sep 17 00:00:00 2001 From: Endaris Date: Mon, 26 May 2025 19:54:44 +0200 Subject: [PATCH 18/49] fix scene.uiRoot maximum dimensions fix childGaps not being subtracted when calculating remaining width to distribute --- client/src/scenes/DesignHelper.lua | 47 ++++++++++--------- client/src/scenes/Scene.lua | 4 ++ client/src/ui/Label.lua | 1 - client/src/ui/Layouts/FlexLayout.lua | 14 ++++-- .../src/ui/Layouts/HorizontalFlexLayout.lua | 9 +++- 5 files changed, 44 insertions(+), 31 deletions(-) diff --git a/client/src/scenes/DesignHelper.lua b/client/src/scenes/DesignHelper.lua index f20de8d1..40ed8a1e 100644 --- a/client/src/scenes/DesignHelper.lua +++ b/client/src/scenes/DesignHelper.lua @@ -113,26 +113,27 @@ function DesignHelper:load() local panelSelectionSelector, panelButton = getSelectorTemplate("panels") - -- local panelSize = 28 - -- local panelContainer = ui.UiElement({ - -- layout = ui.Layouts.HorizontalWrapLayout, - -- maxWidth = 3 * panelSize - -- }) - - -- for color = 1, 8 do - -- local panelImage = ui.ImageContainer({ - -- image = panels[GAME.localPlayer.settings.panelId].displayIcons[color], - -- width = panelSize, - -- height = panelSize, - -- }) - -- panelContainer:addChild(panelImage) - -- end - -- panelContainer:addChild(ui.ImageContainer({ - -- image = panels[GAME.localPlayer.settings.panelId].greyPanel, - -- width = panelSize, - -- height = panelSize, - -- })) - --panelButton:addChild(panelContainer) + local panelSize = 28 + local panelContainer = ui.UniSizedContainer({ + maxWidth = 3 * panelSize, + childrenWidth = panelSize, + childrenHeight = panelSize, + }) + + for color = 1, 8 do + local panelImage = ui.ImageContainer({ + image = panels[GAME.localPlayer.settings.panelId].displayIcons[color], + width = panelSize, + height = panelSize, + }) + panelContainer:addChild(panelImage) + end + panelContainer:addChild(ui.ImageContainer({ + image = panels[GAME.localPlayer.settings.panelId].greyPanel, + width = panelSize, + height = panelSize, + })) + panelButton:addChild(panelContainer) local levelImage = ui.ImageContainer({ image = GAME.theme.images.IMG_levels[GAME.localPlayer.settings.level or 1], @@ -170,9 +171,9 @@ function DesignHelper:load() subSelectionSelector:addChild(readyButton) subSelectionSelector:addChild(leaveButton) - --passThroughSelector:addChild(subSelectionSelector) - --self.uiRoot:addChild(passThroughSelector) - self.uiRoot:addChild(subSelectionSelector) + passThroughSelector:addChild(subSelectionSelector) + self.uiRoot:addChild(passThroughSelector) + --self.uiRoot:addChild(subSelectionSelector) local subSelection = ui.UiElement({ hFill = true, diff --git a/client/src/scenes/Scene.lua b/client/src/scenes/Scene.lua index 7f85e461..7d3f40ca 100644 --- a/client/src/scenes/Scene.lua +++ b/client/src/scenes/Scene.lua @@ -19,11 +19,15 @@ local SoundController = require("client.src.music.SoundController") local Scene = class( ---@param self Scene function (self, sceneParams) + local _, _, flags = love.window.getMode() + local maxWidth, maxHeight = love.window.getDesktopDimensions(flags.display) self.uiRoot = ui.UiElement({ x = 0, y = 0, width = love.graphics.getWidth(), + maxWidth = maxWidth, height = love.graphics.getHeight(), + maxHeight = maxHeight, padding = 16, layout = ui.Layouts.AdaptiveFlexLayout }) diff --git a/client/src/ui/Label.lua b/client/src/ui/Label.lua index eb5d41e9..227fdbd3 100644 --- a/client/src/ui/Label.lua +++ b/client/src/ui/Label.lua @@ -10,7 +10,6 @@ local HorizontalWrapLayout = require(PATH .. ".Layouts.HorizontalWrapLayout") ---@field replacements string[]? Additional strings to perform string format on a localized key with parts marked for replacement ---@field fontSize integer? The size of the font ---@field wrap boolean? If the font should wrap around ----@field wrapRatio number? By which % of the unwrapped text the text should wrap ---@class Label : UiElement ---@field id string? The localization key diff --git a/client/src/ui/Layouts/FlexLayout.lua b/client/src/ui/Layouts/FlexLayout.lua index 35c34c00..d97e517a 100644 --- a/client/src/ui/Layouts/FlexLayout.lua +++ b/client/src/ui/Layouts/FlexLayout.lua @@ -32,14 +32,18 @@ function FlexLayout.setWidth(uiElement, width) uiElement.layout.fitSizeWidth(uiElement) end + local minWidth = uiElement.layout.getMinWidth(uiElement) + if not uiElement.controlsWindow then + minWidth = math.max(minWidth, uiElement.minWidth) + end if width then - local minWidth = uiElement.layout.getMinWidth(uiElement) - if not uiElement.controlsWindow then - minWidth = math.max(minWidth, uiElement.minWidth) + if width > minWidth then + uiElement.width = util.bound(minWidth, uiElement.newWidth, width) + else + uiElement.width = math.max(minWidth, uiElement.newWidth) end - uiElement.width = util.bound(minWidth, uiElement.newWidth, width) else - uiElement.width = util.bound(uiElement.width, uiElement.newWidth, uiElement.maxWidth) + uiElement.width = util.bound(minWidth, uiElement.newWidth, uiElement.maxWidth) end uiElement.newWidth = nil diff --git a/client/src/ui/Layouts/HorizontalFlexLayout.lua b/client/src/ui/Layouts/HorizontalFlexLayout.lua index 234b231d..cac7be0e 100644 --- a/client/src/ui/Layouts/HorizontalFlexLayout.lua +++ b/client/src/ui/Layouts/HorizontalFlexLayout.lua @@ -53,12 +53,17 @@ function HorizontalFlexLayout.finalizeChildrenWidths(uiElement) return end - local remainingWidth = uiElement.width + local remainingWidth = uiElement.width - uiElement.padding * 2 for _, child in ipairs(uiElement.children) do - remainingWidth = remainingWidth - child.newWidth + if uiElement.isVisible then + remainingWidth = remainingWidth - child.newWidth + remainingWidth = remainingWidth - uiElement.childGap + end end + remainingWidth = remainingWidth + uiElement.childGap + if remainingWidth >= 1 then table.clear(growables) From f02c6e57bcdaf6655f75e571247202cca29be4b9 Mon Sep 17 00:00:00 2001 From: Endaris Date: Mon, 26 May 2025 20:51:13 +0200 Subject: [PATCH 19/49] change alignment options to be parent based fix annotations for ui/init.lua --- client/src/scenes/DesignHelper.lua | 12 +--- client/src/scenes/MainMenu.lua | 3 +- client/src/scenes/Scene.lua | 4 +- client/src/ui/BoolSelector.lua | 1 + client/src/ui/Button.lua | 1 + client/src/ui/Cursor.lua | 5 ++ client/src/ui/Label.lua | 1 + .../src/ui/Layouts/HorizontalFlexLayout.lua | 24 +++---- .../src/ui/Layouts/HorizontalScrollLayout.lua | 6 +- .../src/ui/Layouts/HorizontalWrapLayout.lua | 20 +++++- client/src/ui/Layouts/VerticalFlexLayout.lua | 22 +++--- .../src/ui/Layouts/VerticalScrollLayout.lua | 6 +- client/src/ui/LevelSlider.lua | 1 + client/src/ui/MenuItem.lua | 1 + client/src/ui/PassThroughElement.lua | 1 + client/src/ui/PixelFontLabel.lua | 1 + client/src/ui/ScrollContainer.lua | 3 +- client/src/ui/Slider.lua | 1 + client/src/ui/TextButton.lua | 4 +- client/src/ui/UIElement.lua | 1 + client/src/ui/UniSizedContainer.lua | 8 ++- client/src/ui/VerticalMenu.lua | 2 + client/src/ui/init.lua | 67 ++++++++++++------- 23 files changed, 120 insertions(+), 75 deletions(-) diff --git a/client/src/scenes/DesignHelper.lua b/client/src/scenes/DesignHelper.lua index 40ed8a1e..763ca872 100644 --- a/client/src/scenes/DesignHelper.lua +++ b/client/src/scenes/DesignHelper.lua @@ -75,12 +75,6 @@ function DesignHelper:load() self.uiRoot:addChild(gameMode) - local passThroughSelector = ui.PassThroughElement({ - backgroundColor = {0, 1, 0, 0.5}, - hAlign = "center", - hFill = true, - }) - local subSelectionSelector = ui.UniSizedContainer({ childGap = 32, padding = 8, @@ -171,9 +165,9 @@ function DesignHelper:load() subSelectionSelector:addChild(readyButton) subSelectionSelector:addChild(leaveButton) - passThroughSelector:addChild(subSelectionSelector) - self.uiRoot:addChild(passThroughSelector) - --self.uiRoot:addChild(subSelectionSelector) + --passThroughSelector:addChild(subSelectionSelector) + --self.uiRoot:addChild(passThroughSelector) + self.uiRoot:addChild(subSelectionSelector) local subSelection = ui.UiElement({ hFill = true, diff --git a/client/src/scenes/MainMenu.lua b/client/src/scenes/MainMenu.lua index 72338b45..16895e0d 100644 --- a/client/src/scenes/MainMenu.lua +++ b/client/src/scenes/MainMenu.lua @@ -48,11 +48,12 @@ end function MainMenu:createMainMenu() local menuContainer = ui.VerticalMenu({ hAlign = "center", + vAlign = "center", minHeight = 480, maxHeight = 540, childGap = 8, padding = 32, - backgroundColor = {1, 0, 0, 0.2} + backgroundColor = {1, 0, 0, 0.2}, }) local endless = ui.MenuItem.createButtonMenuItem("mm_1_endless", nil, nil, function() diff --git a/client/src/scenes/Scene.lua b/client/src/scenes/Scene.lua index 7d3f40ca..303b187b 100644 --- a/client/src/scenes/Scene.lua +++ b/client/src/scenes/Scene.lua @@ -29,7 +29,9 @@ local Scene = class( height = love.graphics.getHeight(), maxHeight = maxHeight, padding = 16, - layout = ui.Layouts.AdaptiveFlexLayout + layout = ui.Layouts.AdaptiveFlexLayout, + hAlign = "center", + vAlign = "center", }) self.uiRoot.controlsWindow = true -- scenes may specify theme music to use that is played once they are switched to diff --git a/client/src/ui/BoolSelector.lua b/client/src/ui/BoolSelector.lua index 5d4d7e83..e0130c78 100644 --- a/client/src/ui/BoolSelector.lua +++ b/client/src/ui/BoolSelector.lua @@ -9,6 +9,7 @@ local GraphicsUtil = require("client.src.graphics.graphics_util") --- A BoolSelector is a UIElement that shows if a setting is on or off and lets you toggle it. ---@class BoolSelector : UiElement +---@operator call(BoolSelectorOptions): BoolSelector ---@field value boolean ---@field vertical boolean local BoolSelector = class(function(boolSelector, options) diff --git a/client/src/ui/Button.lua b/client/src/ui/Button.lua index 766ab7ba..2c9bf930 100644 --- a/client/src/ui/Button.lua +++ b/client/src/ui/Button.lua @@ -10,6 +10,7 @@ local input = require("client.src.inputManager") ---@field onClick fun(button: Button?, input: table?, timeHeld: number?)? ---@class Button : UiElement +---@operator call(ButtonOptions): Button ---@field backgroundColor number[] ---@field outlineColor number [] ---@field onClick fun(button: Button?, input: table?, timeHeld: number?) diff --git a/client/src/ui/Cursor.lua b/client/src/ui/Cursor.lua index 1bc4ca87..928a65a4 100644 --- a/client/src/ui/Cursor.lua +++ b/client/src/ui/Cursor.lua @@ -5,7 +5,12 @@ local FocusDirector = require(PATH .. ".FocusDirector") local consts = require("common.engine.consts") local GraphicsUtil = require("client.src.graphics.graphics_util") +---@class CursorOptions +---@field target UiElement +---@field hoveredIndex integer? + ---@class Cursor : FocusDirector, UiElement +---@operator call(CursorOptions): Cursor ---@field hovered UiElement ---@field target UiElement ---@field hoveredIndex integer diff --git a/client/src/ui/Label.lua b/client/src/ui/Label.lua index 227fdbd3..be105229 100644 --- a/client/src/ui/Label.lua +++ b/client/src/ui/Label.lua @@ -12,6 +12,7 @@ local HorizontalWrapLayout = require(PATH .. ".Layouts.HorizontalWrapLayout") ---@field wrap boolean? If the font should wrap around ---@class Label : UiElement +---@operator call(LabelOptions): Label ---@field id string? The localization key ---@field replacementTable string[]? Additional strings to perform string format on a localized key with parts marked for replacement ---@field text string The raw text or localization key diff --git a/client/src/ui/Layouts/HorizontalFlexLayout.lua b/client/src/ui/Layouts/HorizontalFlexLayout.lua index cac7be0e..5c7af85c 100644 --- a/client/src/ui/Layouts/HorizontalFlexLayout.lua +++ b/client/src/ui/Layouts/HorizontalFlexLayout.lua @@ -214,26 +214,26 @@ function HorizontalFlexLayout.positionChildren(uiElement) end local x = uiElement.padding + + if uiElement.hAlign == "left" then + elseif uiElement.hAlign == "center" then + x = x + remainingWidth / 2 + elseif uiElement.hAlign == "right" then + x = x + remainingWidth + end for _, child in ipairs(uiElement.children) do if child.isVisible then - if child.hAlign == "left" then - child.x = x - elseif child.hAlign == "center" then - child.x = x + remainingWidth / 2 - elseif child.hAlign == "right" then - child.x = x + remainingWidth - end + child.x = math.round(x) + x = x + uiElement.childGap + child.width - if child.vAlign == "top" then + if uiElement.vAlign == "top" then child.y = uiElement.padding - elseif child.vAlign == "center" then + elseif uiElement.vAlign == "center" then child.y = (uiElement.height - child.height) / 2 - elseif child.vAlign == "bottom" then + elseif uiElement.vAlign == "bottom" then child.y = (uiElement.height - child.height) - uiElement.padding end - child.x = math.round(child.x) child.y = math.round(child.y) - x = x + uiElement.childGap + child.width end end end diff --git a/client/src/ui/Layouts/HorizontalScrollLayout.lua b/client/src/ui/Layouts/HorizontalScrollLayout.lua index 0be5692c..c8980b14 100644 --- a/client/src/ui/Layouts/HorizontalScrollLayout.lua +++ b/client/src/ui/Layouts/HorizontalScrollLayout.lua @@ -24,11 +24,11 @@ function HorizontalScrollLayout.positionChildren(uiElement) if child.isVisible then child.x = x - if child.vAlign == "top" then + if uiElement.vAlign == "top" then child.y = uiElement.padding - elseif child.vAlign == "center" then + elseif uiElement.vAlign == "center" then child.y = (uiElement.height - child.height) / 2 - elseif child.vAlign == "bottom" then + elseif uiElement.vAlign == "bottom" then child.y = (uiElement.height - child.height) - uiElement.padding end child.x = math.round(child.x) diff --git a/client/src/ui/Layouts/HorizontalWrapLayout.lua b/client/src/ui/Layouts/HorizontalWrapLayout.lua index c17af5d5..039b45cb 100644 --- a/client/src/ui/Layouts/HorizontalWrapLayout.lua +++ b/client/src/ui/Layouts/HorizontalWrapLayout.lua @@ -32,13 +32,29 @@ function HorizontalWrapLayout.positionChildren(uiElement) if not uiElement.tempRows or #uiElement.tempRows == 1 then HorizontalFlexLayout.positionChildren(uiElement) else - local x = uiElement.padding + -- subtracting a childGap for every child, so compensating one in advance + local remainingWidth = uiElement.width - uiElement.padding * 2 + uiElement.childGap + for _, child in ipairs(uiElement.children) do + if child.isVisible and child.tempRow == 1 then + remainingWidth = remainingWidth - child.width - uiElement.childGap + end + end + + local startX = uiElement.padding + if uiElement.hAlign == "left" then + elseif uiElement.hAlign == "center" then + startX = startX + remainingWidth / 2 + elseif uiElement.hAlign == "right" then + startX = startX + remainingWidth + end + + local x = startX local y = uiElement.padding local row = 1 for _, child in ipairs(uiElement.children) do if child.isVisible then if child.tempRow > row then - x = uiElement.padding + x = startX y = y + uiElement.tempRows[row] + uiElement.childGap row = child.tempRow end diff --git a/client/src/ui/Layouts/VerticalFlexLayout.lua b/client/src/ui/Layouts/VerticalFlexLayout.lua index 1ffbf090..33080f1a 100644 --- a/client/src/ui/Layouts/VerticalFlexLayout.lua +++ b/client/src/ui/Layouts/VerticalFlexLayout.lua @@ -141,21 +141,23 @@ function VerticalFlexLayout.positionChildren(uiElement) end local y = uiElement.padding + + if uiElement.vAlign == "top" then + elseif uiElement.vAlign == "center" then + y = y + remainingHeight / 2 + elseif uiElement.vAlign == "bottom" then + y = y + remainingHeight + end + for _, child in ipairs(uiElement.children) do if child.isVisible then - if child.vAlign == "top" then - child.y = y - elseif child.vAlign == "center" then - child.y = y + remainingHeight / 2 - elseif child.vAlign == "bottom" then - child.y = y + remainingHeight - end + child.y = y - if child.hAlign == "left" then + if uiElement.hAlign == "left" then child.x = uiElement.padding - elseif child.hAlign == "center" then + elseif uiElement.hAlign == "center" then child.x = (uiElement.width - child.width) / 2 - elseif child.hAlign == "right" then + elseif uiElement.hAlign == "right" then child.x = (uiElement.width - child.width) - uiElement.padding end child.x = math.round(child.x) diff --git a/client/src/ui/Layouts/VerticalScrollLayout.lua b/client/src/ui/Layouts/VerticalScrollLayout.lua index 6b93de79..7b2436df 100644 --- a/client/src/ui/Layouts/VerticalScrollLayout.lua +++ b/client/src/ui/Layouts/VerticalScrollLayout.lua @@ -20,11 +20,11 @@ function VerticalScrollLayout.positionChildren(uiElement) if child.isVisible then child.y = y - if child.hAlign == "left" then + if uiElement.hAlign == "left" then child.x = uiElement.padding - elseif child.hAlign == "center" then + elseif uiElement.hAlign == "center" then child.x = (uiElement.width - child.width) / 2 - elseif child.hAlign == "right" then + elseif uiElement.hAlign == "right" then child.x = (uiElement.width - child.width) - uiElement.padding end child.x = math.round(child.x) diff --git a/client/src/ui/LevelSlider.lua b/client/src/ui/LevelSlider.lua index 7d68f87c..873708bf 100644 --- a/client/src/ui/LevelSlider.lua +++ b/client/src/ui/LevelSlider.lua @@ -5,6 +5,7 @@ local util = require("common.lib.util") local GraphicsUtil = require("client.src.graphics.graphics_util") ---@class LevelSlider : Slider +---@operator call(UiElementOptions): LevelSlider local LevelSlider = class( function(self, options) self.min = 1 diff --git a/client/src/ui/MenuItem.lua b/client/src/ui/MenuItem.lua index 7722d3fe..8967f83c 100644 --- a/client/src/ui/MenuItem.lua +++ b/client/src/ui/MenuItem.lua @@ -8,6 +8,7 @@ local GraphicsUtil = require("client.src.graphics.graphics_util") local HorizontalFlexLayout = require(PATH .. ".Layouts.HorizontalFlexLayout") -- MenuItem is a specific UIElement that all children of Menu should be +---@class MenuItem local MenuItem = class(function(self, options) self.selected = false self.TYPE = "MenuItem" diff --git a/client/src/ui/PassThroughElement.lua b/client/src/ui/PassThroughElement.lua index 06f80d57..bff60fb9 100644 --- a/client/src/ui/PassThroughElement.lua +++ b/client/src/ui/PassThroughElement.lua @@ -5,6 +5,7 @@ local Focusable = require(PATH .. ".Focusable") --- A plain element with the main purpose of giving a container child an alignment without disrupting cursor focus ---@class PassThroughElement : UiElement, Focusable +---@operator call(UiElementOptions):PassThroughElement local PassThroughElement = class( function (self, options) end, diff --git a/client/src/ui/PixelFontLabel.lua b/client/src/ui/PixelFontLabel.lua index 2b513200..8db0b268 100644 --- a/client/src/ui/PixelFontLabel.lua +++ b/client/src/ui/PixelFontLabel.lua @@ -11,6 +11,7 @@ local GraphicsUtil = require("client.src.graphics.graphics_util") ---@field fontMap PixelFontMap? ---@class PixelFontLabel : UiElement +---@operator call(PixelFontLabelOptions): PixelFontLabel ---@field text string ---@field charSpacing integer ---@field xScale number diff --git a/client/src/ui/ScrollContainer.lua b/client/src/ui/ScrollContainer.lua index 75c9a00d..5e2ded3d 100644 --- a/client/src/ui/ScrollContainer.lua +++ b/client/src/ui/ScrollContainer.lua @@ -11,11 +11,10 @@ local HorizontalScrollLayout = require(PATH .. ".Layouts.HorizontalScrollLayout" ---@field scrollOrientation ("vertical" | "horizontal" | nil) ---@class ScrollContainer : UiElement +---@operator call(ScrollContainerOptions): ScrollContainer ---@field scrollOrientation string "vertical" or "horizontal" ---@field scrollOffset number by how many pixels the children are translated in the orientation ---@field maxScrollOffset number maximum allowed value for scrollOffset the object will bound to - ----@class ScrollContainer ---@overload fun(options: ScrollContainerOptions): ScrollContainer local ScrollContainer = class( ---@param self ScrollContainer diff --git a/client/src/ui/Slider.lua b/client/src/ui/Slider.lua index facdd6b0..94d1462f 100644 --- a/client/src/ui/Slider.lua +++ b/client/src/ui/Slider.lua @@ -24,6 +24,7 @@ local sliderBarThickness = 6 -- A horizontal Slider element ---@class Slider: UiElement +---@operator call(SliderOptions): Slider ---@field min number minimum value ---@field max number maximum value ---@field tickLength integer how many pixels represent a value change of tickAmount diff --git a/client/src/ui/TextButton.lua b/client/src/ui/TextButton.lua index 7d3bc092..e8a5d2f0 100644 --- a/client/src/ui/TextButton.lua +++ b/client/src/ui/TextButton.lua @@ -2,15 +2,13 @@ local PATH = (...):gsub('%.[^%.]+$', '') local Button = require(PATH .. ".Button") local class = require("common.lib.class") -local TEXT_WIDTH_PADDING = 6 -local TEXT_HEIGHT_PADDING = 6 - ---@class TextButtonOptions : ButtonOptions ---@field label Label -- A TextButton is a button that sets itself apart from Button by automatically scaling its own size to fit the text inside -- This is different from the regular button that scales its content to fit inside itself ---@class TextButton : Button +---@operator call(TextButtonOptions): TextButton ---@field label Label local TextButton = class(function(self, options) self.label = options.label diff --git a/client/src/ui/UIElement.lua b/client/src/ui/UIElement.lua index ac4cca64..54d1c794 100644 --- a/client/src/ui/UIElement.lua +++ b/client/src/ui/UIElement.lua @@ -58,6 +58,7 @@ local uniqueId = 0 -- takes in a options table for setting default values -- all valid base options are defined in the constructor ---@class UiElement +---@operator call(UiElementOptions): UiElement ---@overload fun(options: UiElementOptions): UiElement local UIElement = class( ---@param self UiElement diff --git a/client/src/ui/UniSizedContainer.lua b/client/src/ui/UniSizedContainer.lua index aee63202..99a8590f 100644 --- a/client/src/ui/UniSizedContainer.lua +++ b/client/src/ui/UniSizedContainer.lua @@ -7,7 +7,12 @@ local consts = require("common.engine.consts") local GraphicsUtil = require("client.src.graphics.graphics_util") local input = require("client.src.inputManager") +---@class UniSizedContainerOptions : UiElementOptions +---@field childrenWidth integer +---@field childrenHeight integer + ---@class UniSizedContainer : UiElement +---@operator call(UniSizedContainerOptions): UniSizedContainer ---@field childrenWidth integer ---@field childrenHeight integer ---@field selectedRow integer @@ -21,9 +26,6 @@ function(self, options) self.childrenHeight = options.childrenHeight self.minHeight = self.padding * 2 + self.childrenHeight - self.hAlignChildren = "left" - self.vAlignChildren = "top" - self.selectedRow = 1 self.selectedColumn = 1 end, diff --git a/client/src/ui/VerticalMenu.lua b/client/src/ui/VerticalMenu.lua index 1443f121..ffe632b8 100644 --- a/client/src/ui/VerticalMenu.lua +++ b/client/src/ui/VerticalMenu.lua @@ -9,6 +9,8 @@ local input = require("client.src.inputManager") local VerticalScrollLayout = require(PATH .. ".Layouts.VerticalScrollLayout") ---@class VerticalMenu : ScrollContainer, Focusable +---@operator call(ScrollContainerOptions): VerticalMenu +---@overload fun(options: ScrollContainerOptions): VerticalMenu local VerticalMenu = class( function(self, options) self.selectedIndex = nil diff --git a/client/src/ui/init.lua b/client/src/ui/init.lua index 0740b1f7..4ba54810 100644 --- a/client/src/ui/init.lua +++ b/client/src/ui/init.lua @@ -1,16 +1,29 @@ local PATH = (...):gsub('%.init$', '') +--[[ +tag each with +---@source relative path +otherwise F12 on an import of ui elsewhere will lead to this file instead of the respective source file +the "./" is assumed given for relative paths but it's still a path so adding the extension is necessary +when addressing files in subdirectories (layouts) use forward slashes as the path separator +https://luals.github.io/wiki/annotations/#source + +also tag with +---@type +so that you get intellisense +]] + local ui = { - ---@see BoolSelector - ---@type fun(options: BoolSelectorOptions): BoolSelector + ---@source BoolSelector.lua + ---@type BoolSelector BoolSelector = require(PATH .. ".BoolSelector"), - ---@see Button - ---@type fun(options: ButtonOptions): Button + ---@source Button.lua + ---@type Button Button = require(PATH .. ".Button"), ButtonGroup = require(PATH .. ".ButtonGroup"), Carousel = require(PATH .. ".Carousel"), - ---@see Cursor - ---@type fun(options: UiElementOptions): Cursor + ---@source Cursor.lua + ---@type Cursor Cursor = require(PATH .. ".Cursor"), Focusable = require(PATH .. ".Focusable"), FocusDirector = require(PATH .. ".FocusDirector"), @@ -18,8 +31,8 @@ local ui = { GridCursor = require(PATH .. ".GridCursor"), ImageContainer = require(PATH .. ".ImageContainer"), InputField = require(PATH .. ".InputField"), - ---@see Label - ---@type fun(options: LabelOptions): Label + ---@source Label.lua + ---@type Label Label = require(PATH .. ".Label"), Layouts = { AdaptiveFlexLayout = require(PATH .. ".Layouts.AdaptiveFlexLayout"), @@ -28,42 +41,44 @@ local ui = { VerticalFlexLayout = require(PATH .. ".Layouts.VerticalFlexLayout"), }, Leaderboard = require(PATH .. ".Leaderboard"), - ---@see LevelSlider - ---@type fun(options: SliderOptions): LevelSlider + ---@source LevelSlider.lua + ---@type LevelSlider LevelSlider = require(PATH .. ".LevelSlider"), + ---@source MenuItem.lua + ---@type MenuItem MenuItem = require(PATH .. ".MenuItem"), MultiPlayerSelectionWrapper = require(PATH .. ".MultiPlayerSelectionWrapper"), PagedUniGrid = require(PATH .. ".PagedUniGrid"), PanelCarousel = require(PATH .. ".PanelCarousel"), - ---@see PassThroughElement - ---@type fun(options: UiElementOptions): PassThroughElement + ---@source PassThroughElement.lua + ---@type PassThroughElement PassThroughElement = require(PATH .. ".PassThroughElement"), - ---@see PixelFontLabel - ---@type fun(options: PixelFontLabelOptions): PixelFontLabel + ---@source PixelFontLabel.lua + ---@type PixelFontLabel PixelFontLabel = require(PATH .. ".PixelFontLabel"), - ---@see ScrollContainer - ---@type fun(options: ScrollContainerOptions): ScrollContainer + ---@source ScrollContainer.lua + ---@type ScrollContainer ScrollContainer = require(PATH .. ".ScrollContainer"), ScrollText = require(PATH .. ".ScrollText"), - ---@see Slider - ---@type fun(options: SliderOptions): Slider + ---@source Slider.lua + ---@type Slider Slider = require(PATH .. ".Slider"), StackPanel = require(PATH .. ".StackPanel"), StageCarousel = require(PATH .. ".StageCarousel"), Stepper = require(PATH .. ".Stepper"), - ---@see TextButton - ---@type fun(options: TextButtonOptions): TextButton + ---@source TextButton.lua + ---@type TextButton TextButton = require(PATH .. ".TextButton"), - ---@see UiElement - ---@type fun(options:UiElementOptions): UiElement + ---@source UiElement.lua + ---@type UiElement ---@class UiElement UiElement = require(PATH .. ".UIElement"), - ---@see UniSizedContainer - ---@type fun(options:UiElementOptions): UniSizedContainer) + ---@source UniSizedContainer.lua + ---@type UniSizedContainer UniSizedContainer = require(PATH .. ".UniSizedContainer"), ValueLabel = require(PATH .. ".ValueLabel"), - ---@see VerticalMenu - ---@type fun(options: ScrollContainerOptions): VerticalMenu + ---@source VerticalMenu.lua + ---@type VerticalMenu VerticalMenu = require(PATH .. ".VerticalMenu"), } From d4f47d5eefac605da049cf4e48b876a0842684f9 Mon Sep 17 00:00:00 2001 From: Endaris Date: Mon, 26 May 2025 22:32:10 +0200 Subject: [PATCH 20/49] fix alignment for unisizedcontainer children and default to minWidth if the window is squeezed too small instead of the preferred width --- client/src/scenes/DesignHelper.lua | 23 +++++++++++++---------- client/src/ui/Label.lua | 4 ++-- client/src/ui/Layouts/FlexLayout.lua | 2 +- client/src/ui/UIElement.lua | 17 ++++++++++++++--- client/src/ui/UniSizedContainer.lua | 2 -- 5 files changed, 30 insertions(+), 18 deletions(-) diff --git a/client/src/scenes/DesignHelper.lua b/client/src/scenes/DesignHelper.lua index 763ca872..0875bbab 100644 --- a/client/src/scenes/DesignHelper.lua +++ b/client/src/scenes/DesignHelper.lua @@ -13,7 +13,9 @@ DesignHelper.name = "DesignHelper" local function getSelectorTemplate(id) local selector = ui.UiElement({ layout = ui.Layouts.VerticalFlexLayout, + childGap = 4, vAlign = "center", + hAlign = "center", vFill = true, }) local button = ui.Button({ @@ -23,11 +25,7 @@ local function getSelectorTemplate(id) minHeight = 84, backgroundColor = {1, 1, 1, 0}, }) - local label = ui.Label({ - id = id, - hAlign = "center", - vAlign = "bottom" - }) + local label = ui.Label({id = id}) selector:addChild(button) selector:addChild(label) @@ -79,9 +77,10 @@ function DesignHelper:load() childGap = 32, padding = 8, backgroundColor = {0, 1, 0, 0.5}, - childrenWidth = 84, + childrenWidth = 112, childrenHeight = 112, hAlign = "center", + vAlign = "center", --scrollOrientation = "horizontal", }) @@ -147,21 +146,25 @@ function DesignHelper:load() --subSelectionSelector:addChild(ui.Label({text = "Puzzle"})) --subSelectionSelector:addChild(ui.Label({text = "Attack File"})) - local readyButton = ui.Button({ + local readyButton = ui.TextButton({ + label = ui.Label({id = "ready"}), hAlign = "center", vAlign = "center", minWidth = 84, minHeight = 84, + maxWidth = 84, + maxHeight = 84, }) - readyButton:addChild(ui.Label({id = "ready", hAlign = "center", vAlign = "center"})) - local leaveButton = ui.Button({ + local leaveButton = ui.TextButton({ + label = ui.Label({id = "leave"}), hAlign = "center", vAlign = "center", minWidth = 84, minHeight = 84, + maxWidth = 84, + maxHeight = 84, }) - leaveButton:addChild(ui.Label({id = "leave", hAlign = "center", vAlign = "center"})) subSelectionSelector:addChild(readyButton) subSelectionSelector:addChild(leaveButton) diff --git a/client/src/ui/Label.lua b/client/src/ui/Label.lua index be105229..566cf76d 100644 --- a/client/src/ui/Label.lua +++ b/client/src/ui/Label.lua @@ -31,8 +31,8 @@ local Label = class( self.text = options.text or "" end - self.hAlign = options.hAlign or "left" - self.vAlign = options.vAlign or "top" + self.hAlign = options.hAlign or "center" + self.vAlign = options.vAlign or "center" self.fontSize = options.fontSize or "normal" local font = GraphicsUtil.getGlobalFontWithSize(self.fontSize) diff --git a/client/src/ui/Layouts/FlexLayout.lua b/client/src/ui/Layouts/FlexLayout.lua index d97e517a..60642e43 100644 --- a/client/src/ui/Layouts/FlexLayout.lua +++ b/client/src/ui/Layouts/FlexLayout.lua @@ -40,7 +40,7 @@ function FlexLayout.setWidth(uiElement, width) if width > minWidth then uiElement.width = util.bound(minWidth, uiElement.newWidth, width) else - uiElement.width = math.max(minWidth, uiElement.newWidth) + uiElement.width = minWidth end else uiElement.width = util.bound(minWidth, uiElement.newWidth, uiElement.maxWidth) diff --git a/client/src/ui/UIElement.lua b/client/src/ui/UIElement.lua index 54d1c794..4cb9e71c 100644 --- a/client/src/ui/UIElement.lua +++ b/client/src/ui/UIElement.lua @@ -122,9 +122,14 @@ function UIElement:onChildrenChanged() local oWidth = self.width local oHeight = self.height - + -- if DEBUG_ENABLED then + -- self.layout.resize(self) + -- end + if self.controlsWindow then - self.layout.resize(self) + --if not DEBUG_ENABLED then + self.layout.resize(self) + --end if oWidth ~= self.width or oHeight ~= self.height then GraphicsUtil.updateMode(self.width, self.height, {}) end @@ -203,7 +208,13 @@ function UIElement:drawSelf() love.graphics.setColor(self.backgroundColor) love.graphics.rectangle("fill", self.x, self.y, self.width, self.height) love.graphics.setColor(1, 1, 1, 1) - love.graphics.print(self.width .. ", " .. self.height, self.x + 5, self.y + 5) + --if self.padding > 0 then + -- love.graphics.rectangle("line", self.x + self.padding, self.y + self.padding, 1, self.height - self.padding * 2) + -- love.graphics.rectangle("line", self.x + self.padding, self.y + self.padding, self.width - self.padding * 2, 1) + -- love.graphics.rectangle("line", self.x + self.width - self.padding, self.y + self.padding, 1, self.height - self.padding * 2) + -- love.graphics.rectangle("line", self.x + self.padding, self.y + self.height - self.padding, self.width - self.padding *2, 1) + --end + --love.graphics.print(self.width .. ", " .. self.height, self.x + 5, self.y + 5) end function UIElement:drawChildren() diff --git a/client/src/ui/UniSizedContainer.lua b/client/src/ui/UniSizedContainer.lua index 99a8590f..7083b0c0 100644 --- a/client/src/ui/UniSizedContainer.lua +++ b/client/src/ui/UniSizedContainer.lua @@ -41,8 +41,6 @@ function UniSizedContainer:addChild(uiElement, index) uiElement.maxWidth = self.childrenWidth uiElement.minHeight = self.childrenHeight uiElement.maxHeight = math.min(self.childrenHeight, uiElement.maxHeight) - uiElement.hAlign = self.hAlignChildren - uiElement.vAlign = self.vAlignChildren UiElement.addChild(self, uiElement, index) end From 1bfabb3c40cf87b1ef200ab4f948a592d3a971ee Mon Sep 17 00:00:00 2001 From: Endaris Date: Mon, 26 May 2025 23:54:28 +0200 Subject: [PATCH 21/49] fix a navigation crash when unisizedcontainer used vAlign different from top --- client/src/scenes/DesignHelper.lua | 5 +++++ client/src/ui/MenuItem.lua | 1 + client/src/ui/UniSizedContainer.lua | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/client/src/scenes/DesignHelper.lua b/client/src/scenes/DesignHelper.lua index 0875bbab..ea2af7e8 100644 --- a/client/src/scenes/DesignHelper.lua +++ b/client/src/scenes/DesignHelper.lua @@ -57,6 +57,7 @@ function DesignHelper:load() padding = 8, backgroundColor = {0, 0, 1, 0.5}, hAlign = "center", + vAlign = "center", layout = ui.Layouts.HorizontalFlexLayout, -- hFill = true, }) @@ -111,6 +112,8 @@ function DesignHelper:load() maxWidth = 3 * panelSize, childrenWidth = panelSize, childrenHeight = panelSize, + hAlign = "center", + vAlign = "center", }) for color = 1, 8 do @@ -126,6 +129,8 @@ function DesignHelper:load() width = panelSize, height = panelSize, })) + panelButton.padding = 4 + panelSelectionSelector.childGap = 0 panelButton:addChild(panelContainer) local levelImage = ui.ImageContainer({ diff --git a/client/src/ui/MenuItem.lua b/client/src/ui/MenuItem.lua index 8967f83c..527db2bc 100644 --- a/client/src/ui/MenuItem.lua +++ b/client/src/ui/MenuItem.lua @@ -24,6 +24,7 @@ function MenuItem.createMenuItem(label, item) local menuItem = UiElement({ hAlign = "center", + vAlign = "center", layout = HorizontalFlexLayout, childGap = 16, hFill = true, diff --git a/client/src/ui/UniSizedContainer.lua b/client/src/ui/UniSizedContainer.lua index 7083b0c0..9193510d 100644 --- a/client/src/ui/UniSizedContainer.lua +++ b/client/src/ui/UniSizedContainer.lua @@ -147,7 +147,7 @@ function UniSizedContainer:onResized() local rowIndex = 1 local rows = {{}} - local yOffset = self.padding + local yOffset = self.children[1].y for i, child in ipairs(self.children) do if child.y > yOffset then yOffset = child.y From 869658fb3a02b5f8a531925a482effa86732d984 Mon Sep 17 00:00:00 2001 From: Endaris Date: Tue, 27 May 2025 15:30:00 +0200 Subject: [PATCH 22/49] update Understanding the Codebase doc with info about the new layout mechanism and new GameMode related enums --- docs/Understanding the Codebase.md | 164 +++++++++++++++++++++++------ 1 file changed, 132 insertions(+), 32 deletions(-) diff --git a/docs/Understanding the Codebase.md b/docs/Understanding the Codebase.md index bd6c8045..cb5a52de 100644 --- a/docs/Understanding the Codebase.md +++ b/docs/Understanding the Codebase.md @@ -85,6 +85,8 @@ The exact startup mechanism differs a bit based on the system due to specific is You can find the current updater at https://github.com/panel-attack/panel-updater with its own documentation. +For developing or troubleshooting updater related things, copy the updater's `updater` directory into this directory and pass `updaterTest` as an extra argument to love. + ## Broad Client Structure Panel Attack has many files, maybe too many and not all of them are in an intuitive place. @@ -201,40 +203,61 @@ In the current iteration GameModes are thought to be mainly defined by a set of #### Stack Interactions Defining how the stacks interact with each other. The currently possible settings are + - NONE, if garbage is not sent anywhere, nor is any received - VERSUS, if garbage is sent to another stack and received from another stack - SELF, if garbage is sent to yourself - ATTACK_ENGINE, if garbage is sent by an attack engine -#### Game Win Conditions -These define a set of zero or more conditions that cause a `Stack` to stop running as soon as one is met. +#### Stack Win Conditions +These define a set of zero or more conditions that cause a `Stack` to stop running in a winning state as soon as one is met. The currently possible settings are -- NO_MATCHABLE_PANELS, if there are no remaining panels of color 1-8 -- NO_MATCHABLE_GARBAGE, if there is no unmatched garbage left on the board -#### Game Over Conditions -These define a set of zero or more conditions that cause a `Stack` to go game over as soon as one is met. +- MATCHABLE_PANELS, which can be set to an integer. If that number of matchable panels is left on the board, the stack wins. +- MATCHABLE_GARBAGE_PANELS, same as MATCHABLE_PANELS, except it refers to only unmatched panels making up garbage. +- SCORE, this is not implemented yet but the idea is that once the Stack reaches a certain score it wins, e.g. for 99999 point racing. + +#### Stack Over Conditions +These define a set of zero or more conditions that cause a `Stack` to stop running in a losing state as soon as one is met. The currently possible settings are -- NEGATIVE_HEALTH, results in game over if `health` reaches 0 -- NO_MOVES_LEFT, results in game over if the amount of available moves for a puzzle has been met -- CHAIN_DROPPED, results in game over if the active chain was dropped + +- HEALTH, which can be set to an integer. If the stack's `health` is equal or smaller, the stack loses. +- SWAPS, which can be set to an integer. If the stack's `swapCount` property equals or exceeds that number, the stack loses. +- CHAIN, which can be set to a boolean. When set to false, the stack loses upon dropping its chain. When set to true, the stack loses upon forming a chain. #### Match Win Conditions + These define a set of zero or more conditions to determine a winner between multiple Stacks inside a Match. +All match win conditions are defined through comparative statements between all players in a game. + +- HIGHEST means that the stack with the highest number wins and generally higher is better if there are more than 2 players. +- LOWEST means that the stack with the lowest number wins and generally lower is better if there are more than 2 players. + The currently possible settings are -- LAST_ALIVE, the last stack alive wins, typically used with no conditions for game win on the Stack level -- SCORE, the stack with the highest score wins -- TIME, the stack with the lowest clock time wins + +- GAME_OVER_CLOCK, this is suitable for elimination and VS game modes; not having went game over is considered as infinity +- TIME, this is suitable for non-elimination modes like score races where generally all stacks are expected to survive +- SCORE Match win conditions are order sensitive and are evaluated until the last one while a tie is present. Example: -In a hypothetical 99999 point race capped to 10 minutes with the match win condition { SCORE, LAST_ALIVE, TIME }, player 1 and player 2 manage to reach 99999 points before time is up, player 3 self destructs at 72000 points before time is up while player 4 only manages to reach 43000 points when time is up. +In a hypothetical 99999 point race capped to 10 minutes with the match win condition { SCORE, GAME_OVER_CLOCK, TIME }, player 1 and player 2 manage to reach 99999 points before time is up, player 3 self destructs at 72000 points before time is up while player 4 only manages to reach 43000 points when time is up. First SCORE is evaluated. Player 3 and player 4 are both eliminated as potential winners because player 1 and 2 beat them in points. -Second LAST_ALIVE is evaluated. Both player 1 and 2 finished in a winning state so they aren't game over, they are still tied. +Second GAME_OVER_CLOCK is evaluated. Both player 1 and 2 finished in a winning state so they aren't game over, they are still tied. Finally TIME is evaluated, player 2 has a lower clock time than player 1 so they are determined winner. In the future each condition should also act as a tiebreaker so that player 3 would be determined to beat player 4 because they win in the score win condition. -Besides the LAST_ALIVE they're not in use so the existing implementations may actually not work. +Besides the GAME_OVER_CLOCK they're not in use so the existing implementations may actually not work. + +#### Match End Conditions + +These define a set of conditions that cause the `Match` to conclude the moment one of them is met. + +- STACKS_ACTIVE, which can be set to an integer. The moment the amount of Stack's still playing equals or falls below that number, the game finishes. +A stack is considered active when it has neither met a Stack Win, nor a Stack Over condition. +- TIME_LIMIT, which can be set to an integer. This causes the match to stop simulating Stacks the moment they reach that frame number. Once all have reached it, the match ends. + +If you have a TIME_LIMIT you usually want at least STACKS_ACTIVE = 0 as an extra condition, otherwise if everyone loses before the time limit is up you will get a bricked match that never ends. #### Stack behaviours Stack possesses certain behaviour flags under its `behaviours` table that can toggle major functions. @@ -259,12 +282,13 @@ If you read the comments for `Stack.simulate`, you may notice a suspicious absen ### Replays and Interoperability -common/data defines some classes and functions that serve as interop between server and client. +common/data defines some classes and functions that create, represent or modify data that is used to exchange information between server and client. Currently these are: -Replay, ReplayPlayer, LevelData, LevelPresets and TouchDataEncoding and (yet to be moved there) GameModes. +GameModes, StackBehaviours, ReplayV3, LevelData, LevelPresets, InputCompression, KeyDataEncoding and TouchDataEncoding and (yet to be moved there) GameModes. -If client or server have further needs than these classes provide, they should generally aim to create their own components that do not inherit from these to guarantee that client and server internals can be edited and refactored without impacting networking. +If client or server have domain specific needs that go beyond what these classes provide, they should generally aim to create their own components that do not inherit from these to guarantee that client and server internals can be edited and refactored without impacting networking. +It's a lot of boilerplate but ultimately it guarantees that you don't change something on the client and suddenly have to update the server for a completely unrelated client feature and lets the maintainer sleep at night. Besides these standard formats that are used in communication, both client and server implement an abstraction layer called ClientMessages or ServerMessages respectively whose sole responsibility it is to convert incoming messages to a format the recipient understands. @@ -288,26 +312,100 @@ All scenes have an `uiRoot` that is traversed by the touch handler through `UIEl ### UIElement composite For general menu design, UIElements should be used. -There is a lot to say about UIElements and nothing at the same time. +There is a lot to say about UIElements and nothing at the same time because the implementations are...rather individual and sometimes quirky. There are some generic UIElements with very basic functionality such as Button, Label, Slider, Stepper. -There are some UIElements for layouting such as Grid, StackPanel or ScrollContainer. +There are some UIElements for organizing a layout such as UniSizedContainer or ScrollContainer. There are some rather specific UIElements for certain purposes such as LevelSlider, StageCarousel or MultiPlayerSelectionWrapper. You may find some of these to be rather unfit for the general purposes their names imply and some are in need of a rewrite. -Via UIElement the composite tree supports the use of alignments and horizontal and vertical fill flags to automatically position and size a UIElement relative to its parent. -For example to have an element centered within its parent you would choose `hAlign = "center"`, `vAlign = "center"`, `x = 0`, `y = 0`. -Note that this comes with some limitations, e.g. if your top level element does not specify a width and the child wants to horizontally fill its parent, this won't work, even if children of the child have a size they would want to fill out. Meaning to say, the top level element in a tree needs sane settings for x, y, width, height and may not use any of the align/fill settings. -This is realised on `Scene`s by having a standard `uiRoot` which is just a basic UIElement with canvas size that provides a container to add all other UIElements to. +### Layout + +The standard layouts for Panel Attack are akin to a FlexBox and the goal of layouts is to provide a way to design UI for scenes so that they provide a good experience on desktop while still being a functional compromise in portrait mode dimensions on mobile. + +To achieve this, layouting is implemented as mostly separated from UIElements themselves and the layout of most UIElements can be changed just by assigning it a different static layout table. + +There are some exceptions to this as some UIElements are built with a specific orientation in mind while providing extra functionality. +Examples of this include: + +- ScrollContainers +You can still change the orientation and layout but usually the contents are designed with the dimensions in mind and thus changing layout orientation will look bad +- Horizontally oriented containers that automatically wrap around as their width reduces +As they already adjust to portrait dimensions on their own, there is no good reason to change the layout + +Layouting always originates from the UIElement at the root and only if it is marked with `controlsWindow = true`. + +The biggest design points to note are the following: + +1. `x` and `y` are generally managed by the Layout. Placement of children depends on their order within the parent's `children` table. +There are some exceptions to this, some layouts will not perform any placement so you can still manually place but these Layouts will never be the default for any UIElement so you have to go out of your way to do it. +2. Alignment of children is based on the parent. That means to center a Button within a UIElement, the UIElement needs to be center aligned, not the Button. The previous iteration based alignment on the children so don't trip over this! + +#### General layout idea + +A general idea for organizing menu layout is the following: -If you can, don't use `Menu` for now, a menu redesign is planned and it will likely die with it. +The root level UIElement that controls the window dimensions and reacts to resizes uses an `AdaptiveFlexLayout` that automatically changes its layout between horizontal and vertical orientation based on its dimensions. +The root level UIElement has two children that use a `VerticalFlexLayout` each. +The first child is used for navigation and controls, the second displays info / preview information. +When in portrait dimensions, the root UIElement utilizes a vertical layout so that the display is a little akin to the Nintendo DS with two "screens". -#### Internal working +#### Layout mechanism + +For future reference and the creation of new layouts, a brief overview on how the Layout/FlexLayout works. + +The first core problem of automatic layout with an arbitrary resolution is that you have to know how big your widgets are before you can start placing them. +The second core problem is wrapping. Text and other tailor-made widgets can trade width for height. + +To address these problems, layouting follows a multi-step process in which each step traverses and works the entire UI tree before going to the next step. + +The first core problem has led to the introduction of a bunch more fields and functions to determine size: + +- minWidth, minHeight, defaults to 0 +- maxWidth, maxHeight, defaults to math.inf +- getPreferredWidth() +- hFill, vFill + +There are some more subtle problems hidden in this but the baseline is that the width of an element does not depend on its height but as per core problem #2 the height may depend on its width. +That means we first need to find out how wide an element is. +The only true answer at the start is "we don't know yet" so each UIElement is initialized with a temporary `newWidth` property that is not necessarily the final result but forms a meaningful basis for following calculations. +For any type of container that contains more than one child, the size also depends on the children so `newWidth` is assigned by recursively drilling down to the leaf nodes to set their `newWidth` first and calculating the parent with the children's information as it returns from the recursion until `newWidth` is assigned for the entire tree. + +At first each element's `newWidth` defaults to the highest of the following values: +- the UIElement's `minWidth` property +- the preferred width required by the children in accordance with the layout +in a vertical layout it would depend on the `newWidth` of the widest element only but in a horizontal it would be a sum +- the preferred width of the UIElement itself; this defaults to the UIElement's `minWidth` property but in particular wrappable elements like Labels will instead return their width in an unwrapped state. + +Now each element has a valid `newWidth` that makes sense but does not have a relation to the window size yet. + +The root element also has preliminary `newWidth` and with the delta of that value to the actual window width we can adjust the existing `newWidth` values in a top-down traversal: + +If the delta is positive, it is distributed between all children with the `hFill` property that have not reached their `maxWidth` yet. +If the delta is negative, it is distributed between all children that have a lower `minWidth` than `newWidth`, starting with the widest element. This will typically hit wrappables that return a preferred width much higher than their minimum width. (Note: it should also consider minWidth based on children on top of the property which it does not do right now). + +By traversing downwards, the extra width compared to the initial estimate is thus spent where possible and `newWidth` is finalized as `width`. + +For height the same steps are essentially repeated with the exception that there is no corresponding `getPreferredHeight()` function and instead all elements start at their minimum height. For wrappable elements their width-based minimum height is accessed through a `getMinHeight()` function that is only required by the HorizontalWrapLayout. +Correspondingly, the delta for `newHeight` to `height` will always be positive. + +To sum up, a valid and desirable estimate is first made for widths, than corrected with the true width using only operations that are known to be valid so the final outcome is guaranteed to be valid and related to the window size. +The same happens for height with consideration to the width values. + +In the following positioning step, each parent assigns `x` and `y` to its children according to its layout. The values are relative to the parent. + +As the final step the tree is once more traversed recursively to call optional callbacks on each UIElement to signify that resizing finished. +This is so that layout oriented UIElements that provide navigation options can make adjustments to how inputs affect selection. + + +#### Drawing + +The layout logic assigns `x` and `y` positions that are relative to its parent. + +Each element is called via `UIElement.draw` which first calls `drawSelf`, then performs a coordinate translation and finally calls the predefined `UIElement.drawChildren` that just calls `draw` on all visible children. +This mechanism is mainly intended for the children to be able to be drawn directly using `self.x` and `self.y` without relying on their parent. +While this is the default, both `draw` and `drawChildren` can be overwritten for any class to have it exert more fine control over how its children are drawn. +Notably, since all of these functions reside on metatables, they can also be shadowed on a per-instance basis for tailor-made behaviour. -The way the relative positioning works is that each element has its `drawSelf` function. -Each element is called via `UIElement.draw` however which first calls `drawSelf` and then calls the predefined `UIElement.drawChildren` that manages the offset for each child with the alignment before calling `draw` on it. -This mechanism is mainly intended for the children to be able to be drawn on their own without relying on their parent. -While this is the default, both `draw` and `drawChildren` can be overwritten for a certain element to have it exert more fine control over how its children are drawn. -Given that functions are overridable on ANY table, even individual ones, it is however also possible to simply overwrite the respective child's `drawSelf` function to rely on its parent instead which can be the easier approach as it allows use of alignments + relative offset without extra setup. ## Localization @@ -318,6 +416,7 @@ If there are placeholders in a localized string, the `replacements` field can be If you add new localization entries please make sure to **always** add them at the bottom. There is a google doc we pull from where non-developers can submit changes / new localizations and it spoils any syncing attempt if we get new entries in the middle of the file. ## Mods and Assets + The default assets can be found in the `client/assets folder`. `client/assets/default_data` contains mods that ship with the game while the other directories contain fallbacks for user mods that don't provide certain assets. Each graphic asset type has its own file for managing the loading process in `client/src/mods`: @@ -326,6 +425,7 @@ Loading of characters and stages is described in the next section. Panels always For characters, panels and stages, a table with id by index and a table with the actual mod by id is created for global access. ### Mod loading + PA has a (still experimental) `ModController` component that tries to automatically load balance the loaded mods. `ModController:loadModFor` is a function that lazy loads a mod for a certain user (this can be a player or match) and holds a table with mods that have been loaded. Additionally each mod also holds by who it is loaded via weak tables. @@ -353,7 +453,7 @@ Draws a bar graph used for per frame value display in debug mode with FPS counte ### [batteries](https://github.com/1bardesign/batteries) A powerful library seeking to fill in the huge gaps in Lua's own standard library. -In Panel Attack we only use the standalone `manual_gc.lua` which offers some functionality for manually collecting garbage on the client. +In Panel Attack we used to use the standalone `manual_gc.lua` which offers some functionality for manually collecting garbage on the client. ### common From 26cdfece00784156d30bc0f1fff807acd613c4a1 Mon Sep 17 00:00:00 2001 From: Endaris Date: Tue, 27 May 2025 23:40:11 +0200 Subject: [PATCH 23/49] extract CharacterButton from CharacterSelect into its own UIElement --- client/src/mods/Theme.lua | 20 ++- client/src/scenes/DesignHelper.lua | 38 +++++- client/src/ui/CharacterButton.lua | 199 ++++++++++++++++++++++++++++ client/src/ui/Cursor.lua | 24 +++- client/src/ui/Label.lua | 4 + client/src/ui/UniSizedContainer.lua | 15 +-- client/src/ui/init.lua | 5 +- 7 files changed, 286 insertions(+), 19 deletions(-) create mode 100644 client/src/ui/CharacterButton.lua diff --git a/client/src/mods/Theme.lua b/client/src/mods/Theme.lua index faadd067..dfc5fce1 100644 --- a/client/src/mods/Theme.lua +++ b/client/src/mods/Theme.lua @@ -357,11 +357,15 @@ local function loadPlayerNumberIcons(theme) return theme.images.IMG_players end -function Theme:loadSelectionGraphics() +function Theme:loadFlags() self.images.flags = {} for _, flag in ipairs(flags) do self.images.flags[flag] = self:load_theme_img("flags/" .. flag) end +end + +function Theme:loadSelectionGraphics() + self:loadFlags() self.images.IMG_level_cursor = self:load_theme_img("level/level_cursor") self.images.IMG_levels = {} @@ -1100,6 +1104,20 @@ function Theme:getSelectionAssetPack(index) return pack end +---@param flagName string? +---@return love.Texture? +function Theme:getFlag(flagName) + if not flagName then + return nil + end + + if not self.images.flags then + self:loadFlags() + end + + return self.images.flags[flagName] +end + function Theme:reload() self:deinitializeGraphics() self:json_init() diff --git a/client/src/scenes/DesignHelper.lua b/client/src/scenes/DesignHelper.lua index ea2af7e8..e82b969a 100644 --- a/client/src/scenes/DesignHelper.lua +++ b/client/src/scenes/DesignHelper.lua @@ -28,11 +28,30 @@ local function getSelectorTemplate(id) local label = ui.Label({id = id}) selector:addChild(button) selector:addChild(label) + selector.receiveInputs = function(selector, input, dt) + button:receiveInputs(input, dt) + end return selector, button end +local function createCharacterSelect() + local characterSelect = ui.UniSizedContainer({ + childrenWidth = 84, + childrenHeight = 84, + childGap = 16 + }) + + for i, characterId in ipairs(visibleCharacters) do + local button = ui.CharacterButton({character = characters[characterId]}) + characterSelect:addChild(button) + end + + return characterSelect +end + function DesignHelper:load() + self.characterSelect = createCharacterSelect() self.uiRoot.layout = ui.Layouts.VerticalFlexLayout self.uiRoot.childGap = 8 @@ -94,6 +113,9 @@ function DesignHelper:load() }) local characterSelectionSelector, characterButton = getSelectorTemplate("character") characterButton:addChild(characterImage) + characterButton.onClick = function() + self:focusCharacterSelect() + end local stageImage = ui.ImageContainer({ image = stages[GAME.localPlayer.settings.selectedStageId].images.thumbnail, @@ -177,7 +199,7 @@ function DesignHelper:load() --self.uiRoot:addChild(passThroughSelector) self.uiRoot:addChild(subSelectionSelector) - local subSelection = ui.UiElement({ + self.subSelection = ui.UiElement({ hFill = true, minHeight = 200, vFill = true, @@ -185,9 +207,9 @@ function DesignHelper:load() backgroundColor = {0, 1, 0, 0.5}--{0.7, 0, 0.5, 1}, }) - subSelection.receiveInputs = function() end + self.subSelection.receiveInputs = function() end - self.uiRoot:addChild(subSelection) + self.uiRoot:addChild(self.subSelection) self.cursor = ui.Cursor({ target = self.uiRoot, @@ -218,6 +240,16 @@ function DesignHelper:update(dt) self.cursor:receiveInputs(inputs, dt) end +function DesignHelper:focusCharacterSelect() + self.characterSelect.yieldFocus = function(characterSelect) + self.cursor:setTarget(self.uiRoot, 3) + characterSelect.yieldFocus = nil + end + + self.subSelection:addChild(self.characterSelect) + self.cursor:setTarget(self.characterSelect) +end + function DesignHelper:draw() self.cursor:draw() self.uiRoot:draw() diff --git a/client/src/ui/CharacterButton.lua b/client/src/ui/CharacterButton.lua new file mode 100644 index 00000000..06607099 --- /dev/null +++ b/client/src/ui/CharacterButton.lua @@ -0,0 +1,199 @@ +local PATH = (...):gsub('%.[^%.]+$', '') +local class = require("common.lib.class") +local Button = require(PATH .. ".Button") +local consts = require("common.engine.consts") +local GraphicsUtil = require("client.src.graphics.graphics_util") + +---@class CharacterButtonOptions : ButtonOptions +---@field character Character + +---@class CharacterButton : Button +---@operator call(CharacterButtonOptions): CharacterButton +---@field character Character +---@field characterId string +---@field displayName string +---@field characterIcon love.Texture +---@field flagIcon love.Texture? +---@field stageIcon love.Texture? +---@field panelIcon love.Texture? +---@field holdTime number +---@field superSelectVisible boolean +---@overload fun(options: CharacterButtonOptions): CharacterButton +local CharacterButton = class( +---@param self CharacterButton +---@param options CharacterButtonOptions +function (self, options) + assert(options.character) + self.character = options.character + self.characterId = self.character.id + if self.characterId == consts.RANDOM_CHARACTER_SPECIAL_VALUE then + self.displayName = loc("random") + else + self.displayName = self.character.display_name + end + + self.characterIcon = self.character.images.icon + self.flagIcon = GAME.theme:getFlag(self.character.flag) + if self.character.stage and stages[self.character.stage] then + self.stageIcon = stages[self.character.stage].images.thumbnail + end + if self.character.panels and panels[self.character.panels] then + self.panelIcon = panels[self.character.panels].displayIcons[1] + end + + self.holdTime = 0 + self.holding = false + self.superSelectVisible = false +end, +Button) + +local super_select_pixelcode = [[ + uniform float percent; + vec4 effect( vec4 color, Image tex, vec2 texture_coords, vec2 screen_coords ) + { + vec4 c = Texel(tex, texture_coords) * color; + if( texture_coords.x < percent ) + { + return c; + } + float ret = (c.x+c.y+c.z)/3.0; + return vec4(ret, ret, ret, c.a); + } + ]] + +CharacterButton.padding = 2 +CharacterButton.superSelectShader = love.graphics.newShader(super_select_pixelcode) + +function CharacterButton:updateSuperSelectShader(timer) + if timer > consts.SUPER_SELECTION_START then + if self.superSelectVisible == false then + self.superSelectVisible = true + end + local progress = (timer - consts.SUPER_SELECTION_START) / consts.SUPER_SELECTION_DURATION + if progress <= 1 then + CharacterButton.superSelectShader:send("percent", progress) + end + else + if self.superSelectVisible then + self.superSelectVisible = false + end + CharacterButton.superSelectShader:send("percent", 0) + end +end + +function CharacterButton:addChild() + error("CharacterButtons cannot have children") +end + +function CharacterButton:action(inputSource, holdTime) + local character = self.character + character:playSelectionSfx() + + if not inputSource or not inputSource.player then + return + else + local player = inputSource.player + GAME.theme:playValidationSfx() + if character:canSuperSelect() and holdTime > consts.SUPER_SELECTION_START + consts.SUPER_SELECTION_DURATION then + -- super select + if character.panels and panels[character.panels] then + player:setPanels(character.panels) + end + if character.stage and stages[character.stage] then + player:setStage(character.stage) + end + end + player:setCharacter(self.characterId) + end +end + +-- touch interaction +-- by implementing onHold we can provide updates to the shader +function CharacterButton:onHold(timer) + self:updateSuperSelectShader(timer) +end + + -- we need to override the standard onRelease to reset the shader +function CharacterButton:onRelease(x, y, timeHeld) + self:updateSuperSelectShader(0) + if self:inBounds(x, y) then + self:action(input.mouse, timeHeld) + end + self.holdTime = 0 +end + +function CharacterButton:receiveInputs(inputs, dt) + if not self.holding then + if inputs.isDown.Swap1 then + self.holding = true + end + else + if inputs.isPressed.Swap1 then + -- measure the time the press is held for + self.holdTime = inputs.isPressed.Swap1 + else + -- apply the actual click on release with the held time and reset it afterwards + self:action(inputs, self.holdTime) + self.holdTime = 0 + self.holding = false + end + end + self:updateSuperSelectShader(self.holdTime) +end + +---@param scale number +---@return FontSize +local function getFontSizeByScale(scale) + if scale < 0.8 then + return "small" + elseif scale < 1.2 then + return "normal" + elseif scale < 1.8 then + return "medium" + else + return "big" + end +end + +function CharacterButton:drawSelf() + GraphicsUtil.setColor(1, 1, 1, 1) + local imageWidth, imageHeight = self.characterIcon:getDimensions() + local scale = math.min(self.width / imageWidth, self.height / imageHeight) + + GraphicsUtil.draw(self.characterIcon, self.x, self.y, 0, scale, scale) + GraphicsUtil.printf(self.displayName, self.x, self.y, self.width, "center", nil, nil, getFontSizeByScale(scale)) + + local bottomOffset = self.height - CharacterButton.padding * scale + + if self.flagIcon then + imageWidth, imageHeight = self.flagIcon:getDimensions() + GraphicsUtil.draw(self.flagIcon, self.width - CharacterButton.padding * scale, bottomOffset, 0, scale, scale, imageWidth, imageHeight) + end + + if self.panelIcon then + imageWidth, imageHeight = self.panelIcon:getDimensions() + GraphicsUtil.draw(self.panelIcon, self.x + CharacterButton.padding * scale, bottomOffset, 0, scale, scale, imageWidth, imageHeight) + end + + if self.stageIcon then + imageWidth, imageHeight = self.stageIcon:getDimensions() + local leftOffset = self.width / 2 - imageWidth / 2 + love.graphics.push("transform") + love.graphics.translate(leftOffset, bottomOffset) + GraphicsUtil.draw(self.stageIcon, 0, 0, 0, scale, scale, imageWidth / 2, imageHeight) + love.graphics.rectangle("line", - imageWidth / 2 * scale, -imageHeight * scale, imageWidth * scale, imageHeight * scale) + love.graphics.pop() + end + + if self.superSelectVisible then + imageWidth, imageHeight = GAME.theme.images.IMG_super:getDimensions() + scale = math.min(self.width / imageWidth, self.height / imageHeight) + GraphicsUtil.setShader(CharacterButton.superSelectShader) + GraphicsUtil.draw(GAME.theme.images.IMG_super, self.x + self.width / 2, self.y + self.height / 2, 0, scale, scale, imageWidth / 2, imageHeight / 2) + GraphicsUtil.setShader() + end + + GraphicsUtil.drawRectangle("line", self.x, self.y, self.width, self.height) +end + +return CharacterButton \ No newline at end of file diff --git a/client/src/ui/Cursor.lua b/client/src/ui/Cursor.lua index 928a65a4..426df6a0 100644 --- a/client/src/ui/Cursor.lua +++ b/client/src/ui/Cursor.lua @@ -131,20 +131,34 @@ function Cursor:receiveInputs(inputs, dt) end end -function Cursor:setTarget(uiElement) +---@param uiElement UiElement +---@param hoveredIndex integer? +function Cursor:setTarget(uiElement, hoveredIndex) + if self.target ~= uiElement then + self.hovered = nil + end self.target = uiElement if uiElement.isFocusable then self:setFocus(self.target) + if hoveredIndex then + self.hoveredIndex = hoveredIndex + self.hovered = self.target.children[self.hoveredIndex] + else + self.hoveredIndex = 0 + self:moveToNext() + end end end function Cursor:drawSelf() -- GraphicsUtil.setColor(0, 0, 0, 0.2) -- love.graphics.rectangle("fill", self.target.x, self.target.y, self.target.width, self.target.height) - GraphicsUtil.setColor(1, 1, 1, 0.2) - local x, y = self.hovered:getScreenPos() - love.graphics.rectangle("fill", x, y, self.hovered.width, self.hovered.height) - GraphicsUtil.setColor(1, 1, 1, 1) + if self.hovered then + GraphicsUtil.setColor(1, 1, 1, 0.2) + local x, y = self.hovered:getScreenPos() + love.graphics.rectangle("fill", x, y, self.hovered.width, self.hovered.height) + GraphicsUtil.setColor(1, 1, 1, 1) + end end function Cursor:escapeCallback() diff --git a/client/src/ui/Label.lua b/client/src/ui/Label.lua index 566cf76d..3e6a8fa3 100644 --- a/client/src/ui/Label.lua +++ b/client/src/ui/Label.lua @@ -116,4 +116,8 @@ function Label:getMinHeight() return GraphicsUtil.getTextHeightForWidth(self.fontSize, self.text, self.width, self.hAlign) end +function Label:addChild() + error("Labels cannot have children") +end + return Label \ No newline at end of file diff --git a/client/src/ui/UniSizedContainer.lua b/client/src/ui/UniSizedContainer.lua index 9193510d..1a082fb7 100644 --- a/client/src/ui/UniSizedContainer.lua +++ b/client/src/ui/UniSizedContainer.lua @@ -18,6 +18,7 @@ local input = require("client.src.inputManager") ---@field selectedRow integer ---@field selectedColumn integer ---@field rows UiElement[][] +---@overload fun(options: UniSizedContainerOptions): UniSizedContainer local UniSizedContainer = class( function(self, options) assert(options.childrenHeight and options.childrenWidth) @@ -54,7 +55,6 @@ function UniSizedContainer:moveToPreviousRow() local nextRow = wrap(1, self.selectedRow - 1, #self.rows) if self.rows[nextRow][self.selectedColumn] then self.selectedRow = nextRow - self.hovered = self.rows[self.selectedRow][self.selectedColumn] return true else return false @@ -70,7 +70,6 @@ function UniSizedContainer:moveToNextRow() local nextRow = wrap(1, self.selectedRow + 1, #self.rows) if self.rows[nextRow][self.selectedColumn] then self.selectedRow = nextRow - self.hovered = self.rows[self.selectedRow][self.selectedColumn] return true else return false @@ -84,7 +83,6 @@ function UniSizedContainer:moveToPrevious() end self.selectedColumn = wrap(1, self.selectedColumn - 1, #self.rows[self.selectedRow]) - self.hovered = self.rows[self.selectedRow][self.selectedColumn] return true end @@ -95,7 +93,6 @@ function UniSizedContainer:moveToNext() end self.selectedColumn = wrap(1, self.selectedColumn + 1, #self.rows[self.selectedRow]) - self.hovered = self.rows[self.selectedRow][self.selectedColumn] return true end @@ -123,12 +120,12 @@ function UniSizedContainer:receiveInputs(inputs, dt) if self:moveToNextRow() then GAME.theme:playMoveSfx() end - elseif inputs.isDown.Swap1 or inputs.isDown.Start then - if self.hovered.isFocusable then + elseif inputs.isDown.Swap1 or inputs.isDown.Start or inputs.isPressed.Swap1 or inputs.isPressed.Start then + if self.rows[self.selectedRow][self.selectedColumn].isFocusable then GAME.theme:playValidationSfx() - self:setFocus(self.hovered) - elseif self.hovered.receiveInputs then - self.hovered:receiveInputs(inputs, dt) + self:setFocus(self.rows[self.selectedRow][self.selectedColumn]) + elseif self.rows[self.selectedRow][self.selectedColumn].receiveInputs then + self.rows[self.selectedRow][self.selectedColumn]:receiveInputs(inputs, dt) else GAME.theme:playCancelSfx() end diff --git a/client/src/ui/init.lua b/client/src/ui/init.lua index 4ba54810..4eca6689 100644 --- a/client/src/ui/init.lua +++ b/client/src/ui/init.lua @@ -3,7 +3,7 @@ local PATH = (...):gsub('%.init$', '') --[[ tag each with ---@source relative path -otherwise F12 on an import of ui elsewhere will lead to this file instead of the respective source file +that way "Go to source" on an import of ui elsewhere will lead to the respective source instead of this one the "./" is assumed given for relative paths but it's still a path so adding the extension is necessary when addressing files in subdirectories (layouts) use forward slashes as the path separator https://luals.github.io/wiki/annotations/#source @@ -22,6 +22,9 @@ local ui = { Button = require(PATH .. ".Button"), ButtonGroup = require(PATH .. ".ButtonGroup"), Carousel = require(PATH .. ".Carousel"), + ---@source CharacterButton.lua + ---@type CharacterButton + CharacterButton = require(PATH .. ".CharacterButton"), ---@source Cursor.lua ---@type Cursor Cursor = require(PATH .. ".Cursor"), From a0b814a3f70c08a3e2a2821589cc6474479ccc61 Mon Sep 17 00:00:00 2001 From: Endaris Date: Wed, 28 May 2025 01:11:38 +0200 Subject: [PATCH 24/49] update doc how localization is implemented in the Label UIElement --- docs/Understanding the Codebase.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/Understanding the Codebase.md b/docs/Understanding the Codebase.md index cb5a52de..d7fd40fd 100644 --- a/docs/Understanding the Codebase.md +++ b/docs/Understanding the Codebase.md @@ -411,7 +411,8 @@ Notably, since all of these functions reside on metatables, they can also be sha We have a cool localization.csv file. In the first column is the codename of a string, then the traductions into the different languages. When adding text to the game, we can reference it by `loc(codename)` so that the loc function can automatically fetch the correct string based on the language configuration. -For text that is properly embedded within the new UI structure, `Label`s are used for display. `Label` possesses a `translate` attribute that defaults to `true` if not explicitly passed as `false` so that the codenames for the strings can often be passed directly. +For text that is properly embedded within the new UI structure, `Label`s are used for display. `Label` have to be initialized with either an `id` or a `text`. +When using an `id` the `loc` function is used to fill the `Label`'s `text` property. Otherwise the text is filled directly and is not being translated. If there are placeholders in a localized string, the `replacements` field can be passed with the values that should be used as replacements. If you add new localization entries please make sure to **always** add them at the bottom. There is a google doc we pull from where non-developers can submit changes / new localizations and it spoils any syncing attempt if we get new entries in the middle of the file. From 5f7084590f1cd682602fff83492506f289271185 Mon Sep 17 00:00:00 2001 From: Endaris Date: Wed, 28 May 2025 10:46:59 +0200 Subject: [PATCH 25/49] add a new command line flag to simulate mobile OS instead of doing it by default in debug mode --- .vscode/launch.json.template | 3 +++ client/src/BattleRoom.lua | 2 +- client/src/Game.lua | 2 +- client/src/developer.lua | 2 ++ client/src/scenes/PortraitGame.lua | 4 ++-- client/src/system.lua | 4 ++++ 6 files changed, 13 insertions(+), 4 deletions(-) diff --git a/.vscode/launch.json.template b/.vscode/launch.json.template index e6cfd20e..c9933f13 100644 --- a/.vscode/launch.json.template +++ b/.vscode/launch.json.template @@ -24,6 +24,9 @@ // the profiler itself preallocates a lot of memory to reduce the odds of it causing spikes in frame time / memory alloc on its own //,"profileFrameTimes" //,"profileMemory" + + // to make the game take code branches intended specifically for mobile devices (e.g. increased font size) + //,"simulateMobileOS" ] }, // using the testLauncher.lua file to only run tests diff --git a/client/src/BattleRoom.lua b/client/src/BattleRoom.lua index 207b825a..ee3dd1ae 100644 --- a/client/src/BattleRoom.lua +++ b/client/src/BattleRoom.lua @@ -374,7 +374,7 @@ end function BattleRoom:createScene(match) -- for touch android players load a different scene - if (system.isMobileOS() or DEBUG_ENABLED) and self.gameScene.name ~= "PuzzleGame" and + if system.isMobileOS() and self.gameScene.name ~= "PuzzleGame" and --but only if they are the only local player cause for 2p vs local using portrait mode would be bad tableUtils.count(self.players, function(p) return p.isLocal and p.human end) == 1 then for _, player in ipairs(self.players) do diff --git a/client/src/Game.lua b/client/src/Game.lua index 13c68357..cce08590 100644 --- a/client/src/Game.lua +++ b/client/src/Game.lua @@ -646,7 +646,7 @@ function Game:setLanguage(lang_code) config.language_code = Localization.codes[Localization.lang_index] local baseOffset = 0 - if system.isMobileOS() or DEBUG_ENABLED then + if system.isMobileOS() then baseOffset = 4 end diff --git a/client/src/developer.lua b/client/src/developer.lua index ff4cc477..9bbcea9c 100644 --- a/client/src/developer.lua +++ b/client/src/developer.lua @@ -35,6 +35,8 @@ function developerTools.processArgs(args) -- drop the updater directory of the updater in for debugging purposes GAME_UPDATER_STATES = { idle = 0, checkingForUpdates = 1, downloading = 2} GAME_UPDATER = require("updater.gameUpdater") + elseif value == "simulateMobileOS" then + SIMULATE_MOBILE_OS = true else for match in string.gmatch(value, "user%-id=(.*)") do CUSTOM_USER_ID = match diff --git a/client/src/scenes/PortraitGame.lua b/client/src/scenes/PortraitGame.lua index 36bb95c4..97d7d8b3 100644 --- a/client/src/scenes/PortraitGame.lua +++ b/client/src/scenes/PortraitGame.lua @@ -191,7 +191,7 @@ end function PortraitGame:flipToPortrait() local width, height, _ = love.window.getMode() - if system.isMobileOS() or DEBUG_ENABLED then + if system.isMobileOS() then -- flip the window dimensions to portrait GraphicsUtil.updateMode(height, width, {}) love.window.setFullscreen(true) @@ -239,7 +239,7 @@ end function PortraitGame:returnToLandscape() -- flip the window dimensions to landscape local width, height, _ = love.window.getMode() - if system.isMobileOS() or DEBUG_ENABLED then + if system.isMobileOS() then GraphicsUtil.updateMode(height, width, {}) love.window.setFullscreen(false) --GAME:updateCanvasPositionAndScale(width, height) diff --git a/client/src/system.lua b/client/src/system.lua index 9dde9fcb..fab2ee17 100644 --- a/client/src/system.lua +++ b/client/src/system.lua @@ -12,6 +12,10 @@ function system.isMobileOS() return true end + if SIMULATE_MOBILE_OS then + return true + end + return false end From 344c09c5634936bfb3ad05902e10469ae4cfe345 Mon Sep 17 00:00:00 2001 From: Endaris Date: Wed, 28 May 2025 11:19:46 +0200 Subject: [PATCH 26/49] no layouting --- docs/Understanding the Codebase.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/Understanding the Codebase.md b/docs/Understanding the Codebase.md index d7fd40fd..82e673fa 100644 --- a/docs/Understanding the Codebase.md +++ b/docs/Understanding the Codebase.md @@ -322,7 +322,7 @@ You may find some of these to be rather unfit for the general purposes their nam The standard layouts for Panel Attack are akin to a FlexBox and the goal of layouts is to provide a way to design UI for scenes so that they provide a good experience on desktop while still being a functional compromise in portrait mode dimensions on mobile. -To achieve this, layouting is implemented as mostly separated from UIElements themselves and the layout of most UIElements can be changed just by assigning it a different static layout table. +To achieve this, the layout logic is implemented as mostly separated from UIElements themselves and the layout of most UIElements can be changed just by assigning it a different static layout table. There are some exceptions to this as some UIElements are built with a specific orientation in mind while providing extra functionality. Examples of this include: @@ -332,7 +332,7 @@ You can still change the orientation and layout but usually the contents are des - Horizontally oriented containers that automatically wrap around as their width reduces As they already adjust to portrait dimensions on their own, there is no good reason to change the layout -Layouting always originates from the UIElement at the root and only if it is marked with `controlsWindow = true`. +Layout updates always originate from the UIElement at the root and only if it is marked with `controlsWindow = true`. The biggest design points to note are the following: @@ -356,7 +356,7 @@ For future reference and the creation of new layouts, a brief overview on how th The first core problem of automatic layout with an arbitrary resolution is that you have to know how big your widgets are before you can start placing them. The second core problem is wrapping. Text and other tailor-made widgets can trade width for height. -To address these problems, layouting follows a multi-step process in which each step traverses and works the entire UI tree before going to the next step. +To address these problems, the layout logic follows a multi-step process in which each step traverses and works the entire UI tree before going to the next step. The first core problem has led to the introduction of a bunch more fields and functions to determine size: From ef9a7af4ba5c057391a5c3b19494ed893195b998 Mon Sep 17 00:00:00 2001 From: Endaris Date: Wed, 28 May 2025 17:37:23 +0200 Subject: [PATCH 27/49] start working on an integrated cursor system and fix some luaLS intellisense stuff --- .luarc.json | 8 +- client/src/inputManager.lua | 34 +++++- client/src/ui/CharacterButton.lua | 1 + client/src/ui/Cursor.lua | 158 +++++++++++---------------- client/src/ui/CursorNavigable.lua | 121 ++++++++++++++++++++ client/src/ui/Focusable.lua | 16 +++ client/src/ui/Label.lua | 1 + client/src/ui/PassThroughElement.lua | 31 ------ client/src/ui/PixelFontLabel.lua | 1 + client/src/ui/ScrollContainer.lua | 1 + client/src/ui/Slider.lua | 1 + client/src/ui/UIElement.lua | 6 + client/src/ui/UniSizedContainer.lua | 36 +++--- client/src/ui/VerticalMenu.lua | 5 + client/src/ui/init.lua | 77 +++++++------ common/lib/import.lua | 64 +++++++++++ common/lib/luaLsPlugin.lua | 80 ++++++++++++++ 17 files changed, 456 insertions(+), 185 deletions(-) create mode 100644 client/src/ui/CursorNavigable.lua delete mode 100644 client/src/ui/PassThroughElement.lua create mode 100644 common/lib/import.lua create mode 100644 common/lib/luaLsPlugin.lua diff --git a/.luarc.json b/.luarc.json index 1484447a..8f435faa 100644 --- a/.luarc.json +++ b/.luarc.json @@ -21,6 +21,9 @@ "hint.semicolon": "Disable", + "misc.parameters": ["--develop=true"], + + "runtime.plugin": "common/lib/luaLsPlugin.lua", "workspace.checkThirdParty": false, "workspace.ignoreDir": [ ".vscode", @@ -34,6 +37,9 @@ "${3rd}/love2d/library", "common/lib/socket.lua", "common/lib/dkjson.lua", - "common/lib/csprng.lua" + "common/lib/csprng.lua", + "common/lib/luaLsPlugin.lua", + "common/lib/import.lua", + "${addons}/lsqlite3/module/library" ] } \ No newline at end of file diff --git a/client/src/inputManager.lua b/client/src/inputManager.lua index d5aaf35c..44211827 100644 --- a/client/src/inputManager.lua +++ b/client/src/inputManager.lua @@ -3,6 +3,38 @@ local joystickManager = require("common.lib.joystickManager") local consts = require("common.engine.consts") local logger = require("common.lib.logger") +---@alias InputKeys ("Up" | "Down" | "Left" | "Right" | "Swap1" | "Swap2" | "TauntUp" | "TauntDown" | "Raise1" | "Raise2" | "Start" | "MenuUp" | "MenuDown" | "MenuLeft" | "MenuRight" | "MenuEsc" | "MenuSelect") + +-----@enum InputKeys +local inputKeys = { + Up = "Up", + Down = "Down", + Left = "Left", + Right = "Right", + Swap1 = "Swap1", + Swap2 = "Swap2", + TauntUp = "TauntUp", + TauntDown = "TauntDown", + Raise1 = "Raise1", + Raise2 = "Raise2", + Start = "Start", + MenuUp = "MenuUp", + MenuDown = "MenuDown", + MenuLeft = "MenuLeft", + MenuRight = "MenuRight", + MenuEsc = "MenuEsc", + MenuNextPage = "MenuNextPage", + MenuPrevPage = "MenuPrevPage", + MenuBack = "MenuBack", + MenuSelect = "MenuSelect", +} + +---@class KeyConfiguration +---@field isDown table +---@field isPressed table +---@field isUp table +---@field isPressedWithRepeat fun(inputs: KeyConfiguration, key: InputKeys, delay: number, repeatPeriod: number): boolean + -- table containing the set of keys in various states -- base structure: -- isDown: table of {key: true} pairs if the key was pressed in the current frame @@ -21,6 +53,7 @@ local inputManager = { isUp = {}, allKeys = {isDown = {}, isPressed = {}, isUp = {}}, mouse = {isDown = {}, isPressed = {}, isUp = {}, x = 0, y = 0}, + ---@type KeyConfiguration[] inputConfigurations = {}, maxConfigurations = 8, defaultKeys = { @@ -58,7 +91,6 @@ local menuReservedKeysMap = { MenuPrevPage = {{"pagedown"}, {"Raise2"}}, MenuBack = {{"backspace"}, {}}, MenuSelect = {{"return", "kpenter", "z"}, {"Swap1", "Start"}}, - FrameAdvance = {{"\\"}, {"TauntUp"}} } -- useful alternate representations of the above information diff --git a/client/src/ui/CharacterButton.lua b/client/src/ui/CharacterButton.lua index 06607099..6b0756e8 100644 --- a/client/src/ui/CharacterButton.lua +++ b/client/src/ui/CharacterButton.lua @@ -19,6 +19,7 @@ local GraphicsUtil = require("client.src.graphics.graphics_util") ---@field holdTime number ---@field superSelectVisible boolean ---@overload fun(options: CharacterButtonOptions): CharacterButton +---@type CharacterButton local CharacterButton = class( ---@param self CharacterButton ---@param options CharacterButtonOptions diff --git a/client/src/ui/Cursor.lua b/client/src/ui/Cursor.lua index 426df6a0..39b4de9c 100644 --- a/client/src/ui/Cursor.lua +++ b/client/src/ui/Cursor.lua @@ -1,69 +1,76 @@ -local PATH = (...):gsub('%.[^%.]+$', '') -local UiElement = require(PATH .. ".UIElement") local class = require("common.lib.class") -local FocusDirector = require(PATH .. ".FocusDirector") local consts = require("common.engine.consts") local GraphicsUtil = require("client.src.graphics.graphics_util") ---@class CursorOptions ---@field target UiElement +---@field keyInput KeyConfiguration ---@field hoveredIndex integer? ---@class Cursor : FocusDirector, UiElement ---@operator call(CursorOptions): Cursor +---@field keyInput KeyConfiguration +---@field focusStack (UiElement | CursorNavigable)[] ---@field hovered UiElement ----@field target UiElement +---@field focused UiElement | CursorNavigable ---@field hoveredIndex integer ---@field escapeCallback fun(self: Cursor) local Cursor = class( function(self, options) + self.keyInput = options.keyInput + self.focusStack = {} + if options.escapeCallback then self.escapeCallback = options.escapeCallback end - self:setTarget(options.target) - if options.hoveredIndex then - self.hoveredIndex = options.hoveredIndex - self.hovered = self.target.children[self.hoveredIndex] - else - self.hoveredIndex = 0 - self:moveToNext() - end -end, -UiElement) - -FocusDirector(Cursor) - -function Cursor:moveToNext() - if self.target then - for i = self.hoveredIndex + 1, self.hoveredIndex + #self.target.children do - local index = wrap(1, i, #self.target.children) - local child = self.target.children[index] - if child.receiveInputs and child.isEnabled and child.isVisible then - self.hoveredIndex = index - self.hovered = self.target.children[self.hoveredIndex] - break - end - end + self:setFocus(options.target) +end) + + +function Cursor:moveFocus(currentFocus, newFocus) + self:releaseFocus(currentFocus) + self:deepenFocus(newFocus) +end + +---@param uiElement UiElement | CursorNavigable +function Cursor:setFocus(uiElement, callback) + if self.focused then + self.focused.cursor = nil end + uiElement:receiveFocus(self) + self.focused = uiElement + self.focusStack = {} end -function Cursor:moveToPrevious() - if self.target then - for i = self.hoveredIndex - 1, self.hoveredIndex - #self.target.children, -1 do - local index = wrap(1, i, #self.target.children) - local child = self.target.children[index] - if child.receiveInputs and child.isEnabled and child.isVisible then - self.hoveredIndex = index - self.hovered = self.target.children[self.hoveredIndex] - break - end +---@param uiElement UiElement | CursorNavigable +function Cursor:deepenFocus(uiElement) + self.focusStack[#self.focusStack+1] = self.focused + self.focused.cursor = nil + self.focused = uiElement + self.focused.cursor = self +end + +---@param uiElement UiElement +function Cursor:releaseFocus(uiElement) + for i = #self.focusStack, 2, -1 do + local focused = self.focusStack[i] + self.focusStack[i] = nil + if focused.cursor then + focused.cursor = nil + focused.hoveredElement = nil + end + if focused == uiElement then + break end end + + self.focused = table.remove(self.focusStack, #self.focusStack) end + function Cursor:getLastIndex() - for i = #self.target.children, 1, -1 do - local child = self.target.children[i] + for i = #self.focused.children, 1, -1 do + local child = self.focused.children[i] if child.receiveInputs and child.isEnabled and child.isVisible then return i end @@ -72,61 +79,18 @@ end function Cursor:moveToLast() self.hoveredIndex = self:getLastIndex() - self.hovered = self.target.children[self.hoveredIndex] + self.hovered = self.focused.children[self.hoveredIndex] end -function Cursor:receiveInputs(inputs, dt) - if self.target then - if self.focused then - self.focused:receiveInputs(inputs, dt, self.player) - elseif inputs.isDown.Swap2 then - GAME.theme:playCancelSfx() - self:escapeCallback() - elseif inputs:isPressedWithRepeat("Left", consts.KEY_DELAY, consts.KEY_REPEAT_PERIOD) then - if self.target.layout.characteristic == "horizontal" then - GAME.theme:playMoveSfx() - self:moveToPrevious() - elseif self.hovered.receiveInputs then - self.hovered:receiveInputs(inputs, dt) - else - GAME.theme:playCancelSfx() - end - elseif inputs:isPressedWithRepeat("Right", consts.KEY_DELAY, consts.KEY_REPEAT_PERIOD) then - if self.target.layout.characteristic == "horizontal" then - GAME.theme:playMoveSfx() - self:moveToNext() - elseif self.hovered.receiveInputs then - self.hovered:receiveInputs(inputs, dt) - else - GAME.theme:playCancelSfx() - end - elseif inputs:isPressedWithRepeat("Up", consts.KEY_DELAY, consts.KEY_REPEAT_PERIOD) then - if self.target.layout.characteristic == "vertical" then - GAME.theme:playMoveSfx() - self:moveToPrevious() - elseif self.hovered.receiveInputs then - self.hovered:receiveInputs(inputs, dt) - else - GAME.theme:playCancelSfx() - end - elseif inputs:isPressedWithRepeat("Down", consts.KEY_DELAY, consts.KEY_REPEAT_PERIOD) then - if self.target.layout.characteristic == "vertical" then - GAME.theme:playMoveSfx() - self:moveToNext() - elseif self.hovered.receiveInputs then - self.hovered:receiveInputs(inputs, dt) - else - GAME.theme:playCancelSfx() - end - elseif inputs.isDown.Swap1 or inputs.isDown.Start then - if self.hovered.isFocusable then - GAME.theme:playValidationSfx() - self:setFocus(self.hovered) - elseif self.hovered.receiveInputs then - self.hovered:receiveInputs(inputs, dt) - else - GAME.theme:playCancelSfx() - end + +function Cursor:receiveInputs(dt) + local focused = self.focused + local hoveredElement = self.focused.hoveredElement + self.focused:processCursorInput(dt) + if self.focused.hoveredElement ~= hoveredElement then + hoveredElement.cursorFocus = false + if focused == self.focused then + --self.focused.hoveredElement. end end end @@ -134,15 +98,15 @@ end ---@param uiElement UiElement ---@param hoveredIndex integer? function Cursor:setTarget(uiElement, hoveredIndex) - if self.target ~= uiElement then + if self.focused ~= uiElement then self.hovered = nil end - self.target = uiElement + self.focused = uiElement if uiElement.isFocusable then - self:setFocus(self.target) + self:setFocus(self.focused) if hoveredIndex then self.hoveredIndex = hoveredIndex - self.hovered = self.target.children[self.hoveredIndex] + self.hovered = self.focused.children[self.hoveredIndex] else self.hoveredIndex = 0 self:moveToNext() diff --git a/client/src/ui/CursorNavigable.lua b/client/src/ui/CursorNavigable.lua new file mode 100644 index 00000000..3c0afd90 --- /dev/null +++ b/client/src/ui/CursorNavigable.lua @@ -0,0 +1,121 @@ +local consts = require("common.engine.consts") +local tableUtils = require("common.lib.tableUtils") + +---@class CursorNavigable +---@field receiveFocus fun(cursorNavigable: CursorNavigable | UiElement, cursor: Cursor) +---@field processCursorInput fun(cursorNavigable: CursorNavigable | UiElement, dt) +---@field receiveInputs fun(cursorNavigable: CursorNavigable | UiElement, inputs: KeyConfiguration, dt: number?) +---@field moveToNext fun(cursorNavigable: CursorNavigable | UiElement) +---@field moveToPrevious fun(cursorNavigable: CursorNavigable | UiElement) +---@field interactsWithCursor boolean +---@field isFocusable boolean +---@field cursor Cursor? +---@field hoveredElement UiElement? + + +local function receiveFocus(cursorNavigable, cursor) + cursorNavigable.cursor = cursor + if not cursorNavigable.hoveredElement then + for i, child in ipairs(cursorNavigable.children) do + if child.isVisible and child.isEnabled then + cursorNavigable.hoveredElement = child + break + end + end + end +end + +---@param cursorNavigable CursorNavigable | UiElement +local function moveToNext(cursorNavigable) + local hoveredIndex = tableUtils.indexOf(cursorNavigable.children, cursorNavigable.hoveredElement) + for i = hoveredIndex + 1, hoveredIndex + #cursorNavigable.children do + local index = wrap(1, i, #cursorNavigable.children) + local child = cursorNavigable.children[index] + if child.receiveInputs and child.isEnabled and child.isVisible then + hoveredIndex = index + cursorNavigable.hoveredElement = cursorNavigable.children[hoveredIndex] + break + end + end +end + +---@param cursorNavigable CursorNavigable | UiElement +local function moveToPrevious(cursorNavigable) + local hoveredIndex = tableUtils.indexOf(cursorNavigable.children, cursorNavigable.hoveredElement) + for i = hoveredIndex - 1, hoveredIndex - #cursorNavigable.children, -1 do + local index = wrap(1, i, #cursorNavigable.children) + local child = cursorNavigable.children[index] + if child.receiveInputs and child.isEnabled and child.isVisible then + hoveredIndex = index + cursorNavigable.hoveredElement = cursorNavigable.children[hoveredIndex] + break + end + end +end + +---@param cursorNavigable CursorNavigable | UiElement +---@param inputs KeyConfiguration +---@param dt number +local function defaultReceiveInputs(cursorNavigable, inputs, dt) + if inputs.isDown.Swap2 then + GAME.theme:playCancelSfx() + cursorNavigable:escapeCallback() + elseif cursorNavigable.layout.characteristic == "horizontal" then + if inputs:isPressedWithRepeat("Left", consts.KEY_DELAY, consts.KEY_REPEAT_PERIOD) then + GAME.theme:playMoveSfx() + cursorNavigable:moveToPrevious() + elseif inputs:isPressedWithRepeat("Right", consts.KEY_DELAY, consts.KEY_REPEAT_PERIOD) then + GAME.theme:playMoveSfx() + cursorNavigable:moveToNext() + end + elseif cursorNavigable.layout.characteristic == "vertical" then + if inputs:isPressedWithRepeat("Up", consts.KEY_DELAY, consts.KEY_REPEAT_PERIOD) then + GAME.theme:playMoveSfx() + cursorNavigable:moveToPrevious() + elseif inputs:isPressedWithRepeat("Down", consts.KEY_DELAY, consts.KEY_REPEAT_PERIOD) then + GAME.theme:playMoveSfx() + cursorNavigable:moveToNext() + end + elseif cursorNavigable.hoveredElement.isFocusable then + if inputs.isDown.Swap1 or inputs.isDown.Start then + GAME.theme:playValidationSfx() + cursorNavigable:setFocus(cursorNavigable.hovered) + end + elseif cursorNavigable.hovered.receiveInputs then + cursorNavigable.hovered:receiveInputs(inputs, dt) + else + GAME.theme:playCancelSfx() + end +end + +---@param cursorNavigable CursorNavigable | UiElement +---@param dt number +local function processCursorInput(cursorNavigable, dt) + if cursorNavigable.cursor then + cursorNavigable:receiveInputs(cursorNavigable.cursor.keyInput, dt) + else + error("Tried to process cursor inputs without a cursor") + end +end + +--[[ +when adding it to a class instead of a single element:
+- make sure to annotate the class table as its own type using ---@type to get rid of the warning
+- have the class definition inherit CursorNavigable
+- you can discard the return value; it is only returned so that when doing it for an instance LuaLS can easily infer the new union type +]] +---@param uiElement UiElement +---@return UiElement | CursorNavigable +local function addCursorNavigationInterface(uiElement) + uiElement.processCursorInput = processCursorInput + uiElement.receiveFocus = receiveFocus + uiElement.moveToNext = moveToNext + uiElement.moveToPrevious = moveToPrevious + uiElement.receiveInputs = defaultReceiveInputs + + ---@cast uiElement +CursorNavigable + return uiElement +end + + +return addCursorNavigationInterface \ No newline at end of file diff --git a/client/src/ui/Focusable.lua b/client/src/ui/Focusable.lua index 6734fbc4..6eb8b146 100644 --- a/client/src/ui/Focusable.lua +++ b/client/src/ui/Focusable.lua @@ -6,6 +6,10 @@ ---@field hasFocus boolean? ---@field yieldFocus fun()? +---@class CursorInteractable +---@field interactsWithCursor boolean +---@field receiveInputs fun(any, Cursor, number?) + local function focusable(uiElement) uiElement.isFocusable = true uiElement.hasFocus = false @@ -23,4 +27,16 @@ local function focusable(uiElement) -- end end +local function receiveFocus(cursorNavigable, cursor) + cursorNavigable.cursor = cursor + if not cursorNavigable.hoveredElement then + for i, child in ipairs(cursorNavigable.children) do + if child.isVisible and child.isEnabled then + cursorNavigable.hoveredElement = child + break + end + end + end +end + return focusable \ No newline at end of file diff --git a/client/src/ui/Label.lua b/client/src/ui/Label.lua index 3e6a8fa3..148060f8 100644 --- a/client/src/ui/Label.lua +++ b/client/src/ui/Label.lua @@ -20,6 +20,7 @@ local HorizontalWrapLayout = require(PATH .. ".Layouts.HorizontalWrapLayout") ---@field fontSize FontSize The size of the font ---@field wrap boolean If the font should wrap around ---@overload fun(options: LabelOptions): Label +---@type Label local Label = class( function(self, options) self.id = options.id diff --git a/client/src/ui/PassThroughElement.lua b/client/src/ui/PassThroughElement.lua deleted file mode 100644 index bff60fb9..00000000 --- a/client/src/ui/PassThroughElement.lua +++ /dev/null @@ -1,31 +0,0 @@ -local PATH = (...):gsub('%.[^%.]+$', '') -local UIElement = require(PATH .. ".UIElement") -local class = require("common.lib.class") -local Focusable = require(PATH .. ".Focusable") - ---- A plain element with the main purpose of giving a container child an alignment without disrupting cursor focus ----@class PassThroughElement : UiElement, Focusable ----@operator call(UiElementOptions):PassThroughElement -local PassThroughElement = class( -function (self, options) -end, -UIElement) - -Focusable(PassThroughElement) - -function PassThroughElement:addChild(uiElement) - if self.children[1] then - error("A PassThroughElement can only have one child") - end - uiElement.yieldFocus = function() - self.yieldFocus() - end - UIElement.addChild(self, uiElement) -end - -function PassThroughElement:receiveInputs(inputs, dt) - self.children[1]:receiveInputs(inputs, dt) -end - - -return PassThroughElement \ No newline at end of file diff --git a/client/src/ui/PixelFontLabel.lua b/client/src/ui/PixelFontLabel.lua index 8db0b268..c439441f 100644 --- a/client/src/ui/PixelFontLabel.lua +++ b/client/src/ui/PixelFontLabel.lua @@ -19,6 +19,7 @@ local GraphicsUtil = require("client.src.graphics.graphics_util") ---@field fontMap PixelFontMap ---@field charDistanceScaled number ---@overload fun(options: PixelFontLabelOptions): PixelFontLabel +---@type PixelFontLabel local PixelFontLabel = class( ---@param self PixelFontLabel ---@param options PixelFontLabelOptions diff --git a/client/src/ui/ScrollContainer.lua b/client/src/ui/ScrollContainer.lua index 5e2ded3d..6092c376 100644 --- a/client/src/ui/ScrollContainer.lua +++ b/client/src/ui/ScrollContainer.lua @@ -16,6 +16,7 @@ local HorizontalScrollLayout = require(PATH .. ".Layouts.HorizontalScrollLayout" ---@field scrollOffset number by how many pixels the children are translated in the orientation ---@field maxScrollOffset number maximum allowed value for scrollOffset the object will bound to ---@overload fun(options: ScrollContainerOptions): ScrollContainer +---@type ScrollContainer local ScrollContainer = class( ---@param self ScrollContainer function(self, options) diff --git a/client/src/ui/Slider.lua b/client/src/ui/Slider.lua index 94d1462f..1999d4cd 100644 --- a/client/src/ui/Slider.lua +++ b/client/src/ui/Slider.lua @@ -38,6 +38,7 @@ local sliderBarThickness = 6 ---@field isFocusable boolean? only present if the individual object has been marked as focusable ---@field yieldFocus fun()? only present if the individual object has been marked as focusable, yields focus back to the parent element ---@overload fun(options: SliderOptions): Slider +---@type Slider local Slider = class( ---@param self Slider ---@param options SliderOptions diff --git a/client/src/ui/UIElement.lua b/client/src/ui/UIElement.lua index 4cb9e71c..d96f8adc 100644 --- a/client/src/ui/UIElement.lua +++ b/client/src/ui/UIElement.lua @@ -30,6 +30,8 @@ local GraphicsUtil = require("client.src.graphics.graphics_util") ---@field onRelease function? touch callback for when the mouse touching the element is released ---@field onHold function? touch callback for when a touch is held on the element for a longer duration ---@field onResized function? layout callback for when a UIElement and all of its children have been resized and positioned +---@field cursorFocus boolean? +---@field mouseFocus boolean? ---@field [any] any ---@class UiElementOptions @@ -257,6 +259,10 @@ function UIElement:isTouchable() or self.onRelease end +function UIElement:isFocused() + return self.cursorFocus or self.mouseFocus +end + function UIElement:getTouchedElement(x, y) if self.isVisible and self.isEnabled and self:inBounds(x, y) then local touchedElement diff --git a/client/src/ui/UniSizedContainer.lua b/client/src/ui/UniSizedContainer.lua index 1a082fb7..9b769f15 100644 --- a/client/src/ui/UniSizedContainer.lua +++ b/client/src/ui/UniSizedContainer.lua @@ -6,12 +6,13 @@ local Focusable = require(PATH .. ".Focusable") local consts = require("common.engine.consts") local GraphicsUtil = require("client.src.graphics.graphics_util") local input = require("client.src.inputManager") +local CursorNavigable = require("client.src.ui.CursorNavigable") ---@class UniSizedContainerOptions : UiElementOptions ---@field childrenWidth integer ---@field childrenHeight integer ----@class UniSizedContainer : UiElement +---@class UniSizedContainer : UiElement, CursorNavigable ---@operator call(UniSizedContainerOptions): UniSizedContainer ---@field childrenWidth integer ---@field childrenHeight integer @@ -19,6 +20,7 @@ local input = require("client.src.inputManager") ---@field selectedColumn integer ---@field rows UiElement[][] ---@overload fun(options: UniSizedContainerOptions): UniSizedContainer +---@type UniSizedContainer local UniSizedContainer = class( function(self, options) assert(options.childrenHeight and options.childrenWidth) @@ -32,7 +34,8 @@ function(self, options) end, UiElement) -Focusable(UniSizedContainer) +CursorNavigable(UniSizedContainer) +UniSizedContainer.TYPE = "UniSizedContainer" UniSizedContainer.layout = HorizontalWrapLayout ---@param uiElement UiElement @@ -97,13 +100,9 @@ function UniSizedContainer:moveToNext() end function UniSizedContainer:receiveInputs(inputs, dt) - if self.focused then - self.focused:receiveInputs(inputs, dt, self.player) - elseif inputs.isDown.Swap2 then - if self.yieldFocus then - GAME.theme:playCancelSfx() - self:yieldFocus() - end + if inputs.isDown.Swap2 then + GAME.theme:playCancelSfx() + self.cursor:releaseFocus(self) elseif inputs:isPressedWithRepeat("Left", consts.KEY_DELAY, consts.KEY_REPEAT_PERIOD) then if self:moveToPrevious() then GAME.theme:playMoveSfx() @@ -120,18 +119,23 @@ function UniSizedContainer:receiveInputs(inputs, dt) if self:moveToNextRow() then GAME.theme:playMoveSfx() end - elseif inputs.isDown.Swap1 or inputs.isDown.Start or inputs.isPressed.Swap1 or inputs.isPressed.Start then - if self.rows[self.selectedRow][self.selectedColumn].isFocusable then + elseif self.rows[self.selectedRow][self.selectedColumn].isFocusable then + if inputs.isDown.Swap1 or inputs.isDown.Start then GAME.theme:playValidationSfx() - self:setFocus(self.rows[self.selectedRow][self.selectedColumn]) - elseif self.rows[self.selectedRow][self.selectedColumn].receiveInputs then - self.rows[self.selectedRow][self.selectedColumn]:receiveInputs(inputs, dt) - else - GAME.theme:playCancelSfx() + self.cursor:deepenFocus(self.rows[self.selectedRow][self.selectedColumn]) end + elseif self.rows[self.selectedRow][self.selectedColumn].receiveInputs then + self.rows[self.selectedRow][self.selectedColumn]:receiveInputs(inputs, dt) + else + GAME.theme:playCancelSfx() end end +function UniSizedContainer:processCursorInput(dt) + local inputs = self.cursor.keyInput + self:receiveInputs(inputs, dt) +end + function UniSizedContainer:onResized() if #self.children == 0 then return diff --git a/client/src/ui/VerticalMenu.lua b/client/src/ui/VerticalMenu.lua index ffe632b8..f691e54b 100644 --- a/client/src/ui/VerticalMenu.lua +++ b/client/src/ui/VerticalMenu.lua @@ -1,4 +1,5 @@ local PATH = (...):gsub('%.[^%.]+$', '') +PATH = "client.src.ui" local ScrollContainer = require(PATH .. ".ScrollContainer") local class = require("common.lib.class") local util = require("common.lib.util") @@ -7,10 +8,12 @@ local Focusable = require(PATH .. ".Focusable") local FocusDirector = require(PATH .. ".FocusDirector") local input = require("client.src.inputManager") local VerticalScrollLayout = require(PATH .. ".Layouts.VerticalScrollLayout") +local CursorNavigable = require(PATH .. ".CursorNavigable") ---@class VerticalMenu : ScrollContainer, Focusable ---@operator call(ScrollContainerOptions): VerticalMenu ---@overload fun(options: ScrollContainerOptions): VerticalMenu +---@type VerticalMenu local VerticalMenu = class( function(self, options) self.selectedIndex = nil @@ -19,6 +22,8 @@ function(self, options) end, ScrollContainer) +CursorNavigable(VerticalMenu) + Focusable(VerticalMenu) FocusDirector(VerticalMenu) diff --git a/client/src/ui/init.lua b/client/src/ui/init.lua index 4eca6689..0fe4352c 100644 --- a/client/src/ui/init.lua +++ b/client/src/ui/init.lua @@ -1,4 +1,6 @@ -local PATH = (...):gsub('%.init$', '') +local PATH = (...):gsub('%.[^%.]+$', '') +---@diagnostic disable-next-line: unused-local +local import = require("common.lib.import") --[[ tag each with @@ -16,73 +18,70 @@ so that you get intellisense local ui = { ---@source BoolSelector.lua ---@type BoolSelector - BoolSelector = require(PATH .. ".BoolSelector"), + BoolSelector = import("./BoolSelector"), ---@source Button.lua ---@type Button - Button = require(PATH .. ".Button"), - ButtonGroup = require(PATH .. ".ButtonGroup"), - Carousel = require(PATH .. ".Carousel"), + Button = import("./Button"), + ButtonGroup = import(PATH .. ".ButtonGroup"), + Carousel = import("./Carousel"), ---@source CharacterButton.lua ---@type CharacterButton - CharacterButton = require(PATH .. ".CharacterButton"), + CharacterButton = import("./CharacterButton"), ---@source Cursor.lua ---@type Cursor - Cursor = require(PATH .. ".Cursor"), - Focusable = require(PATH .. ".Focusable"), - FocusDirector = require(PATH .. ".FocusDirector"), - Grid = require(PATH .. ".Grid"), - GridCursor = require(PATH .. ".GridCursor"), - ImageContainer = require(PATH .. ".ImageContainer"), - InputField = require(PATH .. ".InputField"), + Cursor = import("./Cursor"), + Focusable = import("./Focusable"), + FocusDirector = import("./FocusDirector"), + Grid = import("./Grid"), + GridCursor = import("./GridCursor"), + ImageContainer = import("./ImageContainer"), + InputField = import("./InputField"), ---@source Label.lua ---@type Label - Label = require(PATH .. ".Label"), + Label = import("./Label"), Layouts = { - AdaptiveFlexLayout = require(PATH .. ".Layouts.AdaptiveFlexLayout"), - HorizontalFlexLayout = require(PATH .. ".Layouts.HorizontalFlexLayout"), - HorizontalWrapLayout = require(PATH .. ".Layouts.HorizontalWrapLayout"), - VerticalFlexLayout = require(PATH .. ".Layouts.VerticalFlexLayout"), + AdaptiveFlexLayout = import("./Layouts.AdaptiveFlexLayout"), + HorizontalFlexLayout = import("./Layouts.HorizontalFlexLayout"), + HorizontalWrapLayout = import("./Layouts.HorizontalWrapLayout"), + VerticalFlexLayout = import("./Layouts.VerticalFlexLayout"), }, - Leaderboard = require(PATH .. ".Leaderboard"), + Leaderboard = import("./Leaderboard"), ---@source LevelSlider.lua ---@type LevelSlider - LevelSlider = require(PATH .. ".LevelSlider"), + LevelSlider = import("./LevelSlider"), ---@source MenuItem.lua ---@type MenuItem - MenuItem = require(PATH .. ".MenuItem"), - MultiPlayerSelectionWrapper = require(PATH .. ".MultiPlayerSelectionWrapper"), - PagedUniGrid = require(PATH .. ".PagedUniGrid"), - PanelCarousel = require(PATH .. ".PanelCarousel"), - ---@source PassThroughElement.lua - ---@type PassThroughElement - PassThroughElement = require(PATH .. ".PassThroughElement"), + MenuItem = import("./MenuItem"), + MultiPlayerSelectionWrapper = import("./MultiPlayerSelectionWrapper"), + PagedUniGrid = import("./PagedUniGrid"), + PanelCarousel = import("./PanelCarousel"), ---@source PixelFontLabel.lua ---@type PixelFontLabel - PixelFontLabel = require(PATH .. ".PixelFontLabel"), + PixelFontLabel = import("./PixelFontLabel"), ---@source ScrollContainer.lua ---@type ScrollContainer - ScrollContainer = require(PATH .. ".ScrollContainer"), - ScrollText = require(PATH .. ".ScrollText"), + ScrollContainer = import("./ScrollContainer"), + ScrollText = import("./ScrollText"), ---@source Slider.lua ---@type Slider - Slider = require(PATH .. ".Slider"), - StackPanel = require(PATH .. ".StackPanel"), - StageCarousel = require(PATH .. ".StageCarousel"), - Stepper = require(PATH .. ".Stepper"), + Slider = import("./Slider"), + StackPanel = import("./StackPanel"), + StageCarousel = import("./StageCarousel"), + Stepper = import("./Stepper"), ---@source TextButton.lua ---@type TextButton - TextButton = require(PATH .. ".TextButton"), + TextButton = import("./TextButton"), ---@source UiElement.lua ---@type UiElement ---@class UiElement - UiElement = require(PATH .. ".UIElement"), + UiElement = import("./UIElement"), ---@source UniSizedContainer.lua ---@type UniSizedContainer - UniSizedContainer = require(PATH .. ".UniSizedContainer"), - ValueLabel = require(PATH .. ".ValueLabel"), + UniSizedContainer = import("./UniSizedContainer"), + ValueLabel = import("./ValueLabel"), ---@source VerticalMenu.lua ---@type VerticalMenu - VerticalMenu = require(PATH .. ".VerticalMenu"), + VerticalMenu = import("./VerticalMenu"), } -- the default layout diff --git a/common/lib/import.lua b/common/lib/import.lua new file mode 100644 index 00000000..d9e86db0 --- /dev/null +++ b/common/lib/import.lua @@ -0,0 +1,64 @@ +--[[ +MIT License + +Copyright (c) 2023 Justin van der Leij + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +]] + +local function extractPathComponents(path) + local components = {} + for component in path:gmatch("[^/]+") do + table.insert(components, component) + end + + return components +end + +local import = function(path) + local callerPath = debug.getinfo(2, "S").source:sub(2) + + local pathStack = {} + + if (path:sub(1, 1) == ".") then + local components = extractPathComponents(callerPath) + + for i = 1, #components - 1 do + pathStack[i] = components[i] + end + end + + local components = extractPathComponents(path) + + for _, component in ipairs(components) do + if (component == ".") then + -- Skip + elseif (component == "..") then + table.remove(pathStack, #pathStack) + else + table.insert(pathStack, component) + end + end + + local out = table.concat(pathStack, ".") + + return require(out) +end + +return import \ No newline at end of file diff --git a/common/lib/luaLsPlugin.lua b/common/lib/luaLsPlugin.lua new file mode 100644 index 00000000..e02cfb07 --- /dev/null +++ b/common/lib/luaLsPlugin.lua @@ -0,0 +1,80 @@ +--[[ +MIT License + +Copyright (c) 2024 Elmārs Āboliņš, including code from Justin van der Leij + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +]] + +local workspace = require "workspace" + +local function extractPathComponents(path) + local components = {} + for component in path:gmatch("[^/]+") do + table.insert(components, component) + end + + return components +end + +local import = function(path, fileUri) + local callerPath = fileUri + + local pathStack = {} + + if (path:sub(1, 1) == ".") then + local components = extractPathComponents(callerPath) + + for i = 1, #components - 1 do + pathStack[i] = components[i] + end + end + + local components = extractPathComponents(path) + + for _, component in ipairs(components) do + if (component == ".") then + -- Skip + elseif (component == "..") then + table.remove(pathStack, #pathStack) + else + table.insert(pathStack, component) + end + end + + local out = table.concat(pathStack, ".") + + return "require(\""..out.."\")" +end + +function OnSetText(uri, text) + local diffs = {} + + local transformedUri = uri:sub(#workspace.rootUri+2) + + for startPos, path, finish in text:gmatch '()import%(([^%(%)]+)%)()' do + diffs[#diffs+1] = { + start = startPos, + finish = finish - 1, + text = import(path:gsub('\"',""), transformedUri), + } + end + + return diffs +end \ No newline at end of file From 8b27fc60a097f1d8a9e2330e36fe30568e8c69a1 Mon Sep 17 00:00:00 2001 From: Endaris Date: Wed, 28 May 2025 17:54:06 +0200 Subject: [PATCH 28/49] widely use import with the luaLS plugin so it properly resolves relative requires --- .luarc.json | 2 +- client/src/ui/BoolSelector.lua | 4 ++-- client/src/ui/Button.lua | 4 ++-- client/src/ui/ButtonGroup.lua | 6 +++--- client/src/ui/Carousel.lua | 6 +++--- client/src/ui/CharacterButton.lua | 4 ++-- client/src/ui/Grid.lua | 8 ++++---- client/src/ui/GridCursor.lua | 8 ++++---- client/src/ui/GridElement.lua | 6 +++--- client/src/ui/ImageContainer.lua | 4 ++-- client/src/ui/InputField.lua | 6 +++--- client/src/ui/Label.lua | 6 +++--- client/src/ui/Layouts/AdaptiveFlexLayout.lua | 9 +++++---- client/src/ui/Layouts/FlexLayout.lua | 2 +- client/src/ui/Layouts/HorizontalFlexLayout.lua | 2 +- client/src/ui/Layouts/HorizontalScrollLayout.lua | 2 +- client/src/ui/Layouts/HorizontalWrapLayout.lua | 2 +- client/src/ui/Layouts/StaticLayout.lua | 2 +- client/src/ui/Layouts/VerticalFlexLayout.lua | 2 +- client/src/ui/Layouts/VerticalScrollLayout.lua | 2 +- client/src/ui/Leaderboard.lua | 8 ++++---- client/src/ui/LevelSlider.lua | 4 ++-- client/src/ui/MenuItem.lua | 12 ++++++------ client/src/ui/MultiPlayerSelectionWrapper.lua | 8 ++++---- client/src/ui/PagedUniGrid.lua | 10 +++++----- client/src/ui/PanelCarousel.lua | 8 ++++---- client/src/ui/PixelFontLabel.lua | 4 ++-- client/src/ui/ScrollContainer.lua | 8 ++++---- client/src/ui/ScrollText.lua | 6 +++--- client/src/ui/Slider.lua | 4 ++-- client/src/ui/StackPanel.lua | 4 ++-- client/src/ui/StageCarousel.lua | 10 +++++----- client/src/ui/Stepper.lua | 10 +++++----- client/src/ui/TextButton.lua | 4 ++-- client/src/ui/UniSizedContainer.lua | 8 ++++---- client/src/ui/ValueLabel.lua | 4 ++-- client/src/ui/VerticalMenu.lua | 12 ++++++------ client/src/ui/init.lua | 12 +++++++++--- 38 files changed, 115 insertions(+), 108 deletions(-) diff --git a/.luarc.json b/.luarc.json index 8f435faa..8442c927 100644 --- a/.luarc.json +++ b/.luarc.json @@ -21,7 +21,7 @@ "hint.semicolon": "Disable", - "misc.parameters": ["--develop=true"], + //"misc.parameters": ["--develop=true"], "runtime.plugin": "common/lib/luaLsPlugin.lua", "workspace.checkThirdParty": false, diff --git a/client/src/ui/BoolSelector.lua b/client/src/ui/BoolSelector.lua index e0130c78..c8c5027c 100644 --- a/client/src/ui/BoolSelector.lua +++ b/client/src/ui/BoolSelector.lua @@ -1,6 +1,6 @@ -local PATH = (...):gsub('%.[^%.]+$', '') +local import = require("common.lib.import") -local UiElement = require(PATH .. ".UIElement") +local UiElement = import("./UIElement") local class = require("common.lib.class") local GraphicsUtil = require("client.src.graphics.graphics_util") diff --git a/client/src/ui/Button.lua b/client/src/ui/Button.lua index 2c9bf930..d1fb0cea 100644 --- a/client/src/ui/Button.lua +++ b/client/src/ui/Button.lua @@ -1,5 +1,5 @@ -local PATH = (...):gsub('%.[^%.]+$', '') -local UIElement = require(PATH .. ".UIElement") +local import = require("common.lib.import") +local UIElement = import("./UIElement") local class = require("common.lib.class") local GraphicsUtil = require("client.src.graphics.graphics_util") local input = require("client.src.inputManager") diff --git a/client/src/ui/ButtonGroup.lua b/client/src/ui/ButtonGroup.lua index 1a5dba96..1cf648dd 100644 --- a/client/src/ui/ButtonGroup.lua +++ b/client/src/ui/ButtonGroup.lua @@ -1,9 +1,9 @@ -local PATH = (...):gsub('%.[^%.]+$', '') -local UIElement = require(PATH .. ".UIElement") +local import = require("common.lib.import") +local UIElement = import("./UIElement") local class = require("common.lib.class") local util = require("common.lib.util") local tableUtils = require("common.lib.tableUtils") -local HorizontalFlexLayout = require(PATH .. ".Layouts.HorizontalFlexLayout") +local HorizontalFlexLayout = import("./Layouts.HorizontalFlexLayout") local BUTTON_PADDING = 5 diff --git a/client/src/ui/Carousel.lua b/client/src/ui/Carousel.lua index 914d1c55..34edea78 100644 --- a/client/src/ui/Carousel.lua +++ b/client/src/ui/Carousel.lua @@ -1,6 +1,6 @@ -local PATH = (...):gsub('%.[^%.]+$', '') -local UiElement = require(PATH .. ".UIElement") -local Focusable = require(PATH .. ".Focusable") +local import = require("common.lib.import") +local UiElement = import("./UIElement") +local Focusable = import("./Focusable") local class = require("common.lib.class") local GraphicsUtil = require("client.src.graphics.graphics_util") local tableUtils = require("common.lib.tableUtils") diff --git a/client/src/ui/CharacterButton.lua b/client/src/ui/CharacterButton.lua index 6b0756e8..3fd88e5c 100644 --- a/client/src/ui/CharacterButton.lua +++ b/client/src/ui/CharacterButton.lua @@ -1,6 +1,6 @@ -local PATH = (...):gsub('%.[^%.]+$', '') +local import = require("common.lib.import") local class = require("common.lib.class") -local Button = require(PATH .. ".Button") +local Button = import("./Button") local consts = require("common.engine.consts") local GraphicsUtil = require("client.src.graphics.graphics_util") diff --git a/client/src/ui/Grid.lua b/client/src/ui/Grid.lua index 1776e02e..9e811025 100644 --- a/client/src/ui/Grid.lua +++ b/client/src/ui/Grid.lua @@ -1,9 +1,9 @@ -local PATH = (...):gsub('%.[^%.]+$', '') -local UiElement = require(PATH .. ".UIElement") -local GridElement = require(PATH .. ".GridElement") +local import = require("common.lib.import") +local UiElement = import("./UIElement") +local GridElement = import("./GridElement") local class = require("common.lib.class") local GraphicsUtil = require("client.src.graphics.graphics_util") -local StaticLayout = require(PATH .. ".Layouts.StaticLayout") +local StaticLayout = import("./Layouts.StaticLayout") local Grid = class(function(self, options) self.unitSize = options.unitSize diff --git a/client/src/ui/GridCursor.lua b/client/src/ui/GridCursor.lua index 26bf9639..1de63198 100644 --- a/client/src/ui/GridCursor.lua +++ b/client/src/ui/GridCursor.lua @@ -1,10 +1,10 @@ -local PATH = (...):gsub('%.[^%.]+$', '') -local UiElement = require(PATH .. ".UIElement") +local import = require("common.lib.import") +local UiElement = import("./UIElement") local class = require("common.lib.class") -local directsFocus = require(PATH .. ".FocusDirector") +local directsFocus = import("./FocusDirector") local consts = require("common.engine.consts") local GraphicsUtil = require("client.src.graphics.graphics_util") -local StaticLayout = require(PATH .. ".Layouts.StaticLayout") +local StaticLayout = import("./Layouts.StaticLayout") -- create a new cursor that can navigate on the specified grid -- grid: the target grid that is navigated on diff --git a/client/src/ui/GridElement.lua b/client/src/ui/GridElement.lua index 607e4359..329d49eb 100644 --- a/client/src/ui/GridElement.lua +++ b/client/src/ui/GridElement.lua @@ -1,8 +1,8 @@ -local PATH = (...):gsub('%.[^%.]+$', '') -local UiElement = require(PATH .. ".UIElement") +local import = require("common.lib.import") +local UiElement = import("./UIElement") local class = require("common.lib.class") local GraphicsUtil = require("client.src.graphics.graphics_util") -local StaticLayout = require(PATH .. ".Layouts.StaticLayout") +local StaticLayout = import("./Layouts.StaticLayout") local GridElement = class(function(gridElement, options) if options.content then diff --git a/client/src/ui/ImageContainer.lua b/client/src/ui/ImageContainer.lua index 94c7bbad..62ae1da9 100644 --- a/client/src/ui/ImageContainer.lua +++ b/client/src/ui/ImageContainer.lua @@ -1,5 +1,5 @@ -local PATH = (...):gsub('%.[^%.]+$', '') -local UiElement = require(PATH .. ".UIElement") +local import = require("common.lib.import") +local UiElement = import("./UIElement") local class = require("common.lib.class") local GraphicsUtil = require("client.src.graphics.graphics_util") diff --git a/client/src/ui/InputField.lua b/client/src/ui/InputField.lua index 4050b391..1163975c 100644 --- a/client/src/ui/InputField.lua +++ b/client/src/ui/InputField.lua @@ -1,9 +1,9 @@ local utf8 = require("common.lib.utf8Additions") local util = require("common.lib.util") -local PATH = (...):gsub('%.[^%.]+$', '') -local UIElement = require(PATH .. ".UIElement") -local inputFieldManager = require(PATH .. ".inputFieldManager") +local import = require("common.lib.import") +local UIElement = import("./UIElement") +local inputFieldManager = import("./inputFieldManager") local class = require("common.lib.class") local GraphicsUtil = require("client.src.graphics.graphics_util") diff --git a/client/src/ui/Label.lua b/client/src/ui/Label.lua index 148060f8..11b3855b 100644 --- a/client/src/ui/Label.lua +++ b/client/src/ui/Label.lua @@ -1,8 +1,8 @@ -local PATH = (...):gsub('%.[^%.]+$', '') -local UIElement = require(PATH .. ".UIElement") +local import = require("common.lib.import") +local UIElement = import("./UIElement") local class = require("common.lib.class") local GraphicsUtil = require("client.src.graphics.graphics_util") -local HorizontalWrapLayout = require(PATH .. ".Layouts.HorizontalWrapLayout") +local HorizontalWrapLayout = import("./Layouts.HorizontalWrapLayout") ---@class LabelOptions : UiElementOptions ---@field id string? The localization key; nil if there should be no translation diff --git a/client/src/ui/Layouts/AdaptiveFlexLayout.lua b/client/src/ui/Layouts/AdaptiveFlexLayout.lua index 99b59efc..3a5ae329 100644 --- a/client/src/ui/Layouts/AdaptiveFlexLayout.lua +++ b/client/src/ui/Layouts/AdaptiveFlexLayout.lua @@ -1,9 +1,10 @@ -local PATH = (...):gsub('%.[^%.]+$', '') -local Layout = require(PATH .. ".Layout") -local VerticalFlexLayout = require(PATH ..".VerticalFlexLayout") -local HorizontalFlexLayout = require(PATH ..".HorizontalFlexLayout") +local import = require("common.lib.import") +local Layout = import("./Layout") +local VerticalFlexLayout = import("./VerticalFlexLayout") +local HorizontalFlexLayout = import("./HorizontalFlexLayout") ---@class AdaptiveFlexLayout : FlexLayout +---@diagnostic disable-next-line: assign-type-mismatch local AdaptiveFlexLayout = setmetatable({}, {__index = HorizontalFlexLayout}) function AdaptiveFlexLayout.resize(uiElement, width, height) diff --git a/client/src/ui/Layouts/FlexLayout.lua b/client/src/ui/Layouts/FlexLayout.lua index 60642e43..80c326e5 100644 --- a/client/src/ui/Layouts/FlexLayout.lua +++ b/client/src/ui/Layouts/FlexLayout.lua @@ -1,4 +1,4 @@ -local PATH = (...):gsub('%.[^%.]+$', '') +local import = require("common.lib.import") local Layout = require(PATH ..".Layout") local util = require("common.lib.util") diff --git a/client/src/ui/Layouts/HorizontalFlexLayout.lua b/client/src/ui/Layouts/HorizontalFlexLayout.lua index 5c7af85c..d5d03345 100644 --- a/client/src/ui/Layouts/HorizontalFlexLayout.lua +++ b/client/src/ui/Layouts/HorizontalFlexLayout.lua @@ -1,4 +1,4 @@ -local PATH = (...):gsub('%.[^%.]+$', '') +local import = require("common.lib.import") local FlexLayout = require(PATH ..".FlexLayout") ---@class HorizontalFlexLayout : FlexLayout diff --git a/client/src/ui/Layouts/HorizontalScrollLayout.lua b/client/src/ui/Layouts/HorizontalScrollLayout.lua index c8980b14..fa18786b 100644 --- a/client/src/ui/Layouts/HorizontalScrollLayout.lua +++ b/client/src/ui/Layouts/HorizontalScrollLayout.lua @@ -1,4 +1,4 @@ -local PATH = (...):gsub('%.[^%.]+$', '') +local import = require("common.lib.import") local HorizontalFlexLayout = require(PATH ..".HorizontalFlexLayout") local util = require("common.lib.util") diff --git a/client/src/ui/Layouts/HorizontalWrapLayout.lua b/client/src/ui/Layouts/HorizontalWrapLayout.lua index 039b45cb..73511994 100644 --- a/client/src/ui/Layouts/HorizontalWrapLayout.lua +++ b/client/src/ui/Layouts/HorizontalWrapLayout.lua @@ -1,4 +1,4 @@ -local PATH = (...):gsub('%.[^%.]+$', '') +local import = require("common.lib.import") local HorizontalFlexLayout = require(PATH ..".HorizontalFlexLayout") local util = require("common.lib.util") diff --git a/client/src/ui/Layouts/StaticLayout.lua b/client/src/ui/Layouts/StaticLayout.lua index 7647a161..de26a386 100644 --- a/client/src/ui/Layouts/StaticLayout.lua +++ b/client/src/ui/Layouts/StaticLayout.lua @@ -1,4 +1,4 @@ -local PATH = (...):gsub('%.[^%.]+$', '') +local import = require("common.lib.import") local Layout = require(PATH ..".Layout") ---@class StaticLayout : Layout diff --git a/client/src/ui/Layouts/VerticalFlexLayout.lua b/client/src/ui/Layouts/VerticalFlexLayout.lua index 33080f1a..6bf446f8 100644 --- a/client/src/ui/Layouts/VerticalFlexLayout.lua +++ b/client/src/ui/Layouts/VerticalFlexLayout.lua @@ -1,4 +1,4 @@ -local PATH = (...):gsub('%.[^%.]+$', '') +local import = require("common.lib.import") local FlexLayout = require(PATH ..".FlexLayout") ---@class VerticalFlexLayout : FlexLayout diff --git a/client/src/ui/Layouts/VerticalScrollLayout.lua b/client/src/ui/Layouts/VerticalScrollLayout.lua index 7b2436df..9cf97e52 100644 --- a/client/src/ui/Layouts/VerticalScrollLayout.lua +++ b/client/src/ui/Layouts/VerticalScrollLayout.lua @@ -1,4 +1,4 @@ -local PATH = (...):gsub('%.[^%.]+$', '') +local import = require("common.lib.import") local VerticalFlexLayout = require(PATH ..".VerticalFlexLayout") local util = require("common.lib.util") diff --git a/client/src/ui/Leaderboard.lua b/client/src/ui/Leaderboard.lua index fc2680ee..93f1d918 100644 --- a/client/src/ui/Leaderboard.lua +++ b/client/src/ui/Leaderboard.lua @@ -1,7 +1,7 @@ -local PATH = (...):gsub('%.[^%.]+$', '') -local UiElement = require(PATH .. ".UIElement") -local Label = require(PATH .. ".Label") -local Focusable = require(PATH .. ".Focusable") +local import = require("common.lib.import") +local UiElement = import("./UIElement") +local Label = import("./Label") +local Focusable = import("./Focusable") local class = require("common.lib.class") local util = require("common.lib.util") diff --git a/client/src/ui/LevelSlider.lua b/client/src/ui/LevelSlider.lua index 873708bf..99b3c374 100644 --- a/client/src/ui/LevelSlider.lua +++ b/client/src/ui/LevelSlider.lua @@ -1,5 +1,5 @@ -local PATH = (...):gsub('%.[^%.]+$', '') -local Slider = require(PATH .. ".Slider") +local import = require("common.lib.import") +local Slider = import("./Slider") local class = require("common.lib.class") local util = require("common.lib.util") local GraphicsUtil = require("client.src.graphics.graphics_util") diff --git a/client/src/ui/MenuItem.lua b/client/src/ui/MenuItem.lua index 527db2bc..4d946829 100644 --- a/client/src/ui/MenuItem.lua +++ b/client/src/ui/MenuItem.lua @@ -1,11 +1,11 @@ -local PATH = (...):gsub('%.[^%.]+$', '') -local UiElement = require(PATH .. ".UIElement") -local Label = require(PATH .. ".Label") -local Button = require(PATH .. ".Button") -local TextButton = require(PATH .. ".TextButton") +local import = require("common.lib.import") +local UiElement = import("./UIElement") +local Label = import("./Label") +local Button = import("./Button") +local TextButton = import("./TextButton") local class = require("common.lib.class") local GraphicsUtil = require("client.src.graphics.graphics_util") -local HorizontalFlexLayout = require(PATH .. ".Layouts.HorizontalFlexLayout") +local HorizontalFlexLayout = import("./Layouts.HorizontalFlexLayout") -- MenuItem is a specific UIElement that all children of Menu should be ---@class MenuItem diff --git a/client/src/ui/MultiPlayerSelectionWrapper.lua b/client/src/ui/MultiPlayerSelectionWrapper.lua index fa749358..4c1f8656 100644 --- a/client/src/ui/MultiPlayerSelectionWrapper.lua +++ b/client/src/ui/MultiPlayerSelectionWrapper.lua @@ -1,8 +1,8 @@ -local PATH = (...):gsub('%.[^%.]+$', '') -local Label = require(PATH .. ".Label") -local StackPanel = require(PATH .. ".StackPanel") +local import = require("common.lib.import") +local Label = import("./Label") +local StackPanel = import("./StackPanel") local class = require("common.lib.class") -local Focusable = require(PATH .. ".Focusable") +local Focusable = import("./Focusable") -- forms a layer of abstraction between a player specific selector (e.g. GridCursor) and UiElements that exist per player -- the MultiPlayerSelectionWrapper displays the UiElements of all players but upon selection only redirects inputs to the diff --git a/client/src/ui/PagedUniGrid.lua b/client/src/ui/PagedUniGrid.lua index 2dfe1bc4..9984a305 100644 --- a/client/src/ui/PagedUniGrid.lua +++ b/client/src/ui/PagedUniGrid.lua @@ -1,8 +1,8 @@ -local PATH = (...):gsub('%.[^%.]+$', '') -local UiElement = require(PATH .. ".UIElement") -local Label = require(PATH .. ".Label") -local TextButton = require(PATH .. ".TextButton") -local Grid = require(PATH .. ".Grid") +local import = require("common.lib.import") +local UiElement = import("./UIElement") +local Label = import("./Label") +local TextButton = import("./TextButton") +local Grid = import("./Grid") local class = require("common.lib.class") local Signal = require("common.lib.signal") local GraphicsUtil = require("client.src.graphics.graphics_util") diff --git a/client/src/ui/PanelCarousel.lua b/client/src/ui/PanelCarousel.lua index 3d544b1c..3eeb5a80 100644 --- a/client/src/ui/PanelCarousel.lua +++ b/client/src/ui/PanelCarousel.lua @@ -1,8 +1,8 @@ -local PATH = (...):gsub('%.[^%.]+$', '') -local Carousel = require(PATH .. ".Carousel") +local import = require("common.lib.import") +local Carousel = import("./Carousel") local class = require("common.lib.class") -local StackPanel = require(PATH .. ".StackPanel") -local ImageContainer = require(PATH .. ".ImageContainer") +local StackPanel = import("./StackPanel") +local ImageContainer = import("./ImageContainer") local PanelCarousel = class(function(carousel, options) carousel.colorCount = 5 diff --git a/client/src/ui/PixelFontLabel.lua b/client/src/ui/PixelFontLabel.lua index c439441f..2efb067f 100644 --- a/client/src/ui/PixelFontLabel.lua +++ b/client/src/ui/PixelFontLabel.lua @@ -1,5 +1,5 @@ -local PATH = (...):gsub('%.[^%.]+$', '') -local UiElement = require(PATH .. ".UIElement") +local import = require("common.lib.import") +local UiElement = import("./UIElement") local class = require("common.lib.class") local GraphicsUtil = require("client.src.graphics.graphics_util") diff --git a/client/src/ui/ScrollContainer.lua b/client/src/ui/ScrollContainer.lua index 6092c376..a07f6738 100644 --- a/client/src/ui/ScrollContainer.lua +++ b/client/src/ui/ScrollContainer.lua @@ -1,11 +1,11 @@ -local PATH = (...):gsub('%.[^%.]+$', '') -local UiElement = require(PATH .. ".UIElement") +local import = require("common.lib.import") +local UiElement = import("./UIElement") local class = require("common.lib.class") local util = require("common.lib.util") local GraphicsUtil = require("client.src.graphics.graphics_util") local logger = require("common.lib.logger") -local VerticalScrollLayout = require(PATH .. ".Layouts.VerticalScrollLayout") -local HorizontalScrollLayout = require(PATH .. ".Layouts.HorizontalScrollLayout") +local VerticalScrollLayout = import("./Layouts.VerticalScrollLayout") +local HorizontalScrollLayout = import("./Layouts.HorizontalScrollLayout") ---@class ScrollContainerOptions : UiElementOptions ---@field scrollOrientation ("vertical" | "horizontal" | nil) diff --git a/client/src/ui/ScrollText.lua b/client/src/ui/ScrollText.lua index edcc5fe6..f6e521a3 100644 --- a/client/src/ui/ScrollText.lua +++ b/client/src/ui/ScrollText.lua @@ -1,6 +1,6 @@ -local PATH = (...):gsub('%.[^%.]+$', '') -local UiElement = require(PATH .. ".UIElement") -local Focusable = require(PATH .. ".Focusable") +local import = require("common.lib.import") +local UiElement = import("./UIElement") +local Focusable = import("./Focusable") local class = require("common.lib.class") -- technically this value should be derived from the font size set for the label diff --git a/client/src/ui/Slider.lua b/client/src/ui/Slider.lua index 1999d4cd..91f1f202 100644 --- a/client/src/ui/Slider.lua +++ b/client/src/ui/Slider.lua @@ -1,5 +1,5 @@ -local PATH = (...):gsub('%.[^%.]+$', '') -local UIElement = require(PATH .. ".UIElement") +local import = require("common.lib.import") +local UIElement = import("./UIElement") local class = require("common.lib.class") local util = require("common.lib.util") local GraphicsUtil = require("client.src.graphics.graphics_util") diff --git a/client/src/ui/StackPanel.lua b/client/src/ui/StackPanel.lua index 5c7e723b..e89e2c89 100644 --- a/client/src/ui/StackPanel.lua +++ b/client/src/ui/StackPanel.lua @@ -1,5 +1,5 @@ -local PATH = (...):gsub('%.[^%.]+$', '') -local UiElement = require(PATH .. ".UIElement") +local import = require("common.lib.import") +local UiElement = import("./UIElement") local class = require("common.lib.class") local tableUtils = require("common.lib.tableUtils") local GraphicsUtil = require("client.src.graphics.graphics_util") diff --git a/client/src/ui/StageCarousel.lua b/client/src/ui/StageCarousel.lua index 299b174b..dcee7c52 100644 --- a/client/src/ui/StageCarousel.lua +++ b/client/src/ui/StageCarousel.lua @@ -1,8 +1,8 @@ -local PATH = (...):gsub('%.[^%.]+$', '') -local Carousel = require(PATH .. ".Carousel") -local StackPanel = require(PATH .. ".StackPanel") -local Label = require(PATH .. ".Label") -local ImageContainer = require(PATH .. ".ImageContainer") +local import = require("common.lib.import") +local Carousel = import("./Carousel") +local StackPanel = import("./StackPanel") +local Label = import("./Label") +local ImageContainer = import("./ImageContainer") local class = require("common.lib.class") local consts = require("common.engine.consts") local Stage = require("client.src.mods.Stage") diff --git a/client/src/ui/Stepper.lua b/client/src/ui/Stepper.lua index fbdaad37..8fb30518 100644 --- a/client/src/ui/Stepper.lua +++ b/client/src/ui/Stepper.lua @@ -1,11 +1,11 @@ -local PATH = (...):gsub('%.[^%.]+$', '') -local UIElement = require(PATH .. ".UIElement") -local TextButton = require(PATH .. ".TextButton") -local Label = require(PATH .. ".Label") +local import = require("common.lib.import") +local UIElement = import("./UIElement") +local TextButton = import("./TextButton") +local Label = import("./Label") local class = require("common.lib.class") local util = require("common.lib.util") local GraphicsUtil = require("client.src.graphics.graphics_util") -local HorizontalFlexLayout = require(PATH .. ".Layouts.HorizontalFlexLayout") +local HorizontalFlexLayout = import("./Layouts.HorizontalFlexLayout") local NAV_BUTTON_WIDTH = 25 diff --git a/client/src/ui/TextButton.lua b/client/src/ui/TextButton.lua index e8a5d2f0..8bb591e4 100644 --- a/client/src/ui/TextButton.lua +++ b/client/src/ui/TextButton.lua @@ -1,5 +1,5 @@ -local PATH = (...):gsub('%.[^%.]+$', '') -local Button = require(PATH .. ".Button") +local import = require("common.lib.import") +local Button = import("./Button") local class = require("common.lib.class") ---@class TextButtonOptions : ButtonOptions diff --git a/client/src/ui/UniSizedContainer.lua b/client/src/ui/UniSizedContainer.lua index 9b769f15..c5a435b9 100644 --- a/client/src/ui/UniSizedContainer.lua +++ b/client/src/ui/UniSizedContainer.lua @@ -1,8 +1,8 @@ -local PATH = (...):gsub('%.[^%.]+$', '') -local UiElement = require(PATH .. ".UIElement") +local import = require("common.lib.import") +local UiElement = import("./UIElement") local class = require("common.lib.class") -local HorizontalWrapLayout = require(PATH .. ".Layouts.HorizontalWrapLayout") -local Focusable = require(PATH .. ".Focusable") +local HorizontalWrapLayout = import("./Layouts.HorizontalWrapLayout") +local Focusable = import("./Focusable") local consts = require("common.engine.consts") local GraphicsUtil = require("client.src.graphics.graphics_util") local input = require("client.src.inputManager") diff --git a/client/src/ui/ValueLabel.lua b/client/src/ui/ValueLabel.lua index edb00dbc..099c2249 100644 --- a/client/src/ui/ValueLabel.lua +++ b/client/src/ui/ValueLabel.lua @@ -1,5 +1,5 @@ -local PATH = (...):gsub('%.[^%.]+$', '') -local UiElement = require(PATH .. ".UIElement") +local import = require("common.lib.import") +local UiElement = import("./UIElement") local class = require("common.lib.class") local GraphicsUtil = require("client.src.graphics.graphics_util") diff --git a/client/src/ui/VerticalMenu.lua b/client/src/ui/VerticalMenu.lua index f691e54b..4bd8439f 100644 --- a/client/src/ui/VerticalMenu.lua +++ b/client/src/ui/VerticalMenu.lua @@ -1,14 +1,14 @@ -local PATH = (...):gsub('%.[^%.]+$', '') +local import = require("common.lib.import") PATH = "client.src.ui" -local ScrollContainer = require(PATH .. ".ScrollContainer") +local ScrollContainer = import("./ScrollContainer") local class = require("common.lib.class") local util = require("common.lib.util") local GraphicsUtil = require("client.src.graphics.graphics_util") -local Focusable = require(PATH .. ".Focusable") -local FocusDirector = require(PATH .. ".FocusDirector") +local Focusable = import("./Focusable") +local FocusDirector = import("./FocusDirector") local input = require("client.src.inputManager") -local VerticalScrollLayout = require(PATH .. ".Layouts.VerticalScrollLayout") -local CursorNavigable = require(PATH .. ".CursorNavigable") +local VerticalScrollLayout = import("./Layouts.VerticalScrollLayout") +local CursorNavigable = import("./CursorNavigable") ---@class VerticalMenu : ScrollContainer, Focusable ---@operator call(ScrollContainerOptions): VerticalMenu diff --git a/client/src/ui/init.lua b/client/src/ui/init.lua index 0fe4352c..fafaddeb 100644 --- a/client/src/ui/init.lua +++ b/client/src/ui/init.lua @@ -1,5 +1,4 @@ local PATH = (...):gsub('%.[^%.]+$', '') ----@diagnostic disable-next-line: unused-local local import = require("common.lib.import") --[[ @@ -18,11 +17,12 @@ so that you get intellisense local ui = { ---@source BoolSelector.lua ---@type BoolSelector - BoolSelector = import("./BoolSelector"), + BoolSelector = require(PATH ..".BoolSelector"), ---@source Button.lua ---@type Button Button = import("./Button"), - ButtonGroup = import(PATH .. ".ButtonGroup"), + ---@source ButtonGroup.lua + ButtonGroup = import("./ButtonGroup"), Carousel = import("./Carousel"), ---@source CharacterButton.lua ---@type CharacterButton @@ -34,15 +34,21 @@ local ui = { FocusDirector = import("./FocusDirector"), Grid = import("./Grid"), GridCursor = import("./GridCursor"), + ---@source ImageContainer.lua ImageContainer = import("./ImageContainer"), + ---@source InputField.lua InputField = import("./InputField"), ---@source Label.lua ---@type Label Label = import("./Label"), Layouts = { + ---@source Layouts/AdaptiveFlexLayout.lua AdaptiveFlexLayout = import("./Layouts.AdaptiveFlexLayout"), + ---@source Layouts/HorizontalFlexLayout.lua HorizontalFlexLayout = import("./Layouts.HorizontalFlexLayout"), + ---@source Layouts/HorizontalWrapLayout.lua HorizontalWrapLayout = import("./Layouts.HorizontalWrapLayout"), + ---@source Layouts/VerticalFlexLayout.lua VerticalFlexLayout = import("./Layouts.VerticalFlexLayout"), }, Leaderboard = import("./Leaderboard"), From b9bbe0af469212c407a0e2d65d773daff4bb6a09 Mon Sep 17 00:00:00 2001 From: Endaris Date: Thu, 29 May 2025 01:38:26 +0200 Subject: [PATCH 29/49] change receiveInputs to be cursor driven to achieve a UI system in which relevant UI elements always know whether they are being hovered by a keyboard/controller cursor cursors can now also traverse focus within a scene in a non-linear (aka a button can pass it to an entirely different UiElement) and reversible way --- .luarc.json | 6 +- client/src/inputManager.lua | 2 +- client/src/scenes/DesignHelper.lua | 34 ++--- client/src/scenes/MainMenu.lua | 9 +- client/src/ui/BoolSelector.lua | 40 ++--- client/src/ui/Button.lua | 9 +- client/src/ui/ButtonGroup.lua | 7 +- client/src/ui/CharacterButton.lua | 5 +- client/src/ui/Cursor.lua | 143 ++++++------------ client/src/ui/CursorInteractable.lua | 51 +++++++ client/src/ui/CursorNavigable.lua | 103 ++++++------- client/src/ui/Layouts/FlexLayout.lua | 2 +- .../src/ui/Layouts/HorizontalFlexLayout.lua | 2 +- .../src/ui/Layouts/HorizontalScrollLayout.lua | 2 +- .../src/ui/Layouts/HorizontalWrapLayout.lua | 2 +- client/src/ui/Layouts/StaticLayout.lua | 2 +- client/src/ui/Layouts/VerticalFlexLayout.lua | 2 +- .../src/ui/Layouts/VerticalScrollLayout.lua | 2 +- client/src/ui/MenuItem.lua | 10 +- client/src/ui/ScrollContainer.lua | 31 ++-- client/src/ui/Slider.lua | 9 +- client/src/ui/Stepper.lua | 9 +- client/src/ui/UniSizedContainer.lua | 57 ++++--- client/src/ui/VerticalMenu.lua | 93 +++++------- client/src/ui/init.lua | 7 +- 25 files changed, 342 insertions(+), 297 deletions(-) create mode 100644 client/src/ui/CursorInteractable.lua diff --git a/.luarc.json b/.luarc.json index 8442c927..4e267e9d 100644 --- a/.luarc.json +++ b/.luarc.json @@ -21,9 +21,11 @@ "hint.semicolon": "Disable", - //"misc.parameters": ["--develop=true"], - "runtime.plugin": "common/lib/luaLsPlugin.lua", + + // to support mixin union types where a table is both types rather than just one + "type.weakUnionCheck": true, + "workspace.checkThirdParty": false, "workspace.ignoreDir": [ ".vscode", diff --git a/client/src/inputManager.lua b/client/src/inputManager.lua index 44211827..c1b9e27a 100644 --- a/client/src/inputManager.lua +++ b/client/src/inputManager.lua @@ -33,7 +33,7 @@ local inputKeys = { ---@field isDown table ---@field isPressed table ---@field isUp table ----@field isPressedWithRepeat fun(inputs: KeyConfiguration, key: InputKeys, delay: number, repeatPeriod: number): boolean +---@field isPressedWithRepeat fun(inputs: KeyConfiguration, key: InputKeys, delay: number?, repeatPeriod: number?): boolean -- table containing the set of keys in various states -- base structure: diff --git a/client/src/scenes/DesignHelper.lua b/client/src/scenes/DesignHelper.lua index e82b969a..734cf213 100644 --- a/client/src/scenes/DesignHelper.lua +++ b/client/src/scenes/DesignHelper.lua @@ -28,18 +28,24 @@ local function getSelectorTemplate(id) local label = ui.Label({id = id}) selector:addChild(button) selector:addChild(label) - selector.receiveInputs = function(selector, input, dt) - button:receiveInputs(input, dt) + selector.receiveInputs = function(selector, cursor, dt) + button:receiveInputs(cursor, dt) end return selector, button end -local function createCharacterSelect() +local function createCharacterSelect(scene) local characterSelect = ui.UniSizedContainer({ childrenWidth = 84, childrenHeight = 84, - childGap = 16 + childGap = 16, + onFocus = function (self) + scene.subSelection:addChild(self) + end, + onYield = function (self) + self:detach() + end }) for i, characterId in ipairs(visibleCharacters) do @@ -54,6 +60,7 @@ function DesignHelper:load() self.characterSelect = createCharacterSelect() self.uiRoot.layout = ui.Layouts.VerticalFlexLayout self.uiRoot.childGap = 8 + ui.CursorNavigable(self.uiRoot) local roomMode = ui.UiElement({ childGap = 8, @@ -114,7 +121,7 @@ function DesignHelper:load() local characterSelectionSelector, characterButton = getSelectorTemplate("character") characterButton:addChild(characterImage) characterButton.onClick = function() - self:focusCharacterSelect() + self.cursor:deepenFocus(self.characterSelect) end local stageImage = ui.ImageContainer({ @@ -211,10 +218,7 @@ function DesignHelper:load() self.uiRoot:addChild(self.subSelection) - self.cursor = ui.Cursor({ - target = self.uiRoot, - hoveredIndex = 3, - }) + self.cursor = ui.Cursor(self.uiRoot) end function DesignHelper:loadRankedSelection(width) @@ -237,17 +241,7 @@ function DesignHelper:update(dt) if inputs.isDown["MenuEsc"] and not self.cursor.focused then GAME.navigationStack:pop() end - self.cursor:receiveInputs(inputs, dt) -end - -function DesignHelper:focusCharacterSelect() - self.characterSelect.yieldFocus = function(characterSelect) - self.cursor:setTarget(self.uiRoot, 3) - characterSelect.yieldFocus = nil - end - - self.subSelection:addChild(self.characterSelect) - self.cursor:setTarget(self.characterSelect) + self.cursor:receiveInputs(dt) end function DesignHelper:draw() diff --git a/client/src/scenes/MainMenu.lua b/client/src/scenes/MainMenu.lua index 16895e0d..7ca71947 100644 --- a/client/src/scenes/MainMenu.lua +++ b/client/src/scenes/MainMenu.lua @@ -30,12 +30,8 @@ local PuzzleGame = require("client.src.scenes.PuzzleGame") local MainMenu = class(function(self, sceneParams) self.music = "main" self.menu = self:createMainMenu() - self.cursor = ui.Cursor({ - target = self.menu, - cursorImage = GAME.theme:getGridCursor(1) - }) + self.cursor = ui.Cursor(self.menu) self.uiRoot:addChild(self.menu) - self.uiRoot:addChild(self.cursor) end, Scene) MainMenu.name = "MainMenu" @@ -189,7 +185,7 @@ end function MainMenu:update(dt) GAME.theme.images.bg_main:update(dt) - self.cursor:receiveInputs(inputs) + self.cursor:receiveInputs(dt) self:checkForUpdates() end @@ -197,6 +193,7 @@ end function MainMenu:draw() GAME.theme.images.bg_main:draw() self.uiRoot:draw() + self.cursor:draw() local fontHeight = GraphicsUtil.getGlobalFont():getHeight() local infoYPosition = 705 - fontHeight / 2 local screenWidth = love.graphics.getWidth() diff --git a/client/src/ui/BoolSelector.lua b/client/src/ui/BoolSelector.lua index c8c5027c..287fb857 100644 --- a/client/src/ui/BoolSelector.lua +++ b/client/src/ui/BoolSelector.lua @@ -1,5 +1,5 @@ local import = require("common.lib.import") - +local addCursorInteractionInterface = import("./CursorInteractable") local UiElement = import("./UIElement") local class = require("common.lib.class") local GraphicsUtil = require("client.src.graphics.graphics_util") @@ -19,6 +19,7 @@ end, UiElement) BoolSelector.TYPE = "BoolSelector" +addCursorInteractionInterface(BoolSelector) function BoolSelector:onTouch(x, y) end @@ -32,27 +33,30 @@ function BoolSelector:onSelect(boolSelector, selector) self:setValue(not self.value) end -function BoolSelector:receiveInputs(input) - if self.isFocusable then - if (input:isPressedWithRepeat("Right") and self.vertical == false) or - (input:isPressedWithRepeat("Up") and self.vertical) then - self:setValue(true) - elseif (input:isPressedWithRepeat("Left") and self.vertical == false) or - (input:isPressedWithRepeat("Down") and self.vertical) then - self:setValue(false) - elseif input.isDown["Swap1"] then - GAME.theme:playValidationSfx() - self:yieldFocus() - elseif input.isDown["Swap2"] then - GAME.theme:playCancelSfx() - self:yieldFocus() - end - else +---@param cursor Cursor +---@param dt number? +function BoolSelector:receiveInputs(cursor, dt) + local input = cursor.keyInput + -- if self.isFocusable then + -- if (input:isPressedWithRepeat("Right") and self.vertical == false) or + -- (input:isPressedWithRepeat("Up") and self.vertical) then + -- self:setValue(true) + -- elseif (input:isPressedWithRepeat("Left") and self.vertical == false) or + -- (input:isPressedWithRepeat("Down") and self.vertical) then + -- self:setValue(false) + -- elseif input.isDown["Swap1"] then + -- GAME.theme:playValidationSfx() + -- self:yieldFocus() + -- elseif input.isDown["Swap2"] then + -- GAME.theme:playCancelSfx() + -- self:yieldFocus() + -- end + -- else if input.isDown["Swap1"] then GAME.theme:playValidationSfx() self:setValue(not self.value) end - end + --end end function BoolSelector:setValue(value) diff --git a/client/src/ui/Button.lua b/client/src/ui/Button.lua index d1fb0cea..1069d834 100644 --- a/client/src/ui/Button.lua +++ b/client/src/ui/Button.lua @@ -3,13 +3,14 @@ local UIElement = import("./UIElement") local class = require("common.lib.class") local GraphicsUtil = require("client.src.graphics.graphics_util") local input = require("client.src.inputManager") +local addCursorInteractionInterface = import("./CursorInteractable") ---@class ButtonOptions : UiElementOptions ---@field backgroundColor number[]? ---@field outlineColor number[]? ---@field onClick fun(button: Button?, input: table?, timeHeld: number?)? ----@class Button : UiElement +---@class Button : UiElement, CursorInteractable ---@operator call(ButtonOptions): Button ---@field backgroundColor number[] ---@field outlineColor number [] @@ -28,6 +29,7 @@ local Button = class( ) Button.TYPE = "Button" +addCursorInteractionInterface(Button) function Button:onTouch(x, y) self.backgroundColor[4] = 1 @@ -41,7 +43,10 @@ function Button:onRelease(x, y, timeHeld) end end -function Button:receiveInputs(input) +---@param cursor Cursor +---@param dt number +function Button:receiveInputs(cursor, dt) + local input = cursor.keyInput if input.isDown["MenuSelect"] then self:onClick(input) -- this is a really stupid way to make sure you can activate back buttons with escape diff --git a/client/src/ui/ButtonGroup.lua b/client/src/ui/ButtonGroup.lua index 1cf648dd..76c70e0f 100644 --- a/client/src/ui/ButtonGroup.lua +++ b/client/src/ui/ButtonGroup.lua @@ -1,4 +1,5 @@ local import = require("common.lib.import") +local addCursorInteractionInterface = import("./CursorInteractable") local UIElement = import("./UIElement") local class = require("common.lib.class") local util = require("common.lib.util") @@ -82,6 +83,7 @@ local ButtonGroup = class( ) ButtonGroup.TYPE = "ButtonGroup" ButtonGroup.layout = HorizontalFlexLayout +addCursorInteractionInterface(ButtonGroup) -- changes state for the button group -- updates the color of the selected button @@ -94,7 +96,10 @@ function ButtonGroup:buttonClicked(button) self.selectedIndex = i end -function ButtonGroup:receiveInputs(input) +---@param cursor Cursor +---@param dt number? +function ButtonGroup:receiveInputs(cursor, dt) + local input = cursor.keyInput if input:isPressedWithRepeat("Left") then self:setActiveButton(self.selectedIndex - 1) elseif input:isPressedWithRepeat("Right") then diff --git a/client/src/ui/CharacterButton.lua b/client/src/ui/CharacterButton.lua index 3fd88e5c..c61ed048 100644 --- a/client/src/ui/CharacterButton.lua +++ b/client/src/ui/CharacterButton.lua @@ -123,7 +123,10 @@ function CharacterButton:onRelease(x, y, timeHeld) self.holdTime = 0 end -function CharacterButton:receiveInputs(inputs, dt) +---@param cursor Cursor +---@param dt number? +function CharacterButton:receiveInputs(cursor, dt) + local inputs = cursor.keyInput if not self.holding then if inputs.isDown.Swap1 then self.holding = true diff --git a/client/src/ui/Cursor.lua b/client/src/ui/Cursor.lua index 39b4de9c..51b44465 100644 --- a/client/src/ui/Cursor.lua +++ b/client/src/ui/Cursor.lua @@ -1,140 +1,97 @@ local class = require("common.lib.class") local consts = require("common.engine.consts") local GraphicsUtil = require("client.src.graphics.graphics_util") +local input = require("client.src.inputManager") ---@class CursorOptions ---@field target UiElement ----@field keyInput KeyConfiguration ----@field hoveredIndex integer? +---@field keyInput KeyConfiguration? ----@class Cursor : FocusDirector, UiElement +---@class Cursor ---@operator call(CursorOptions): Cursor +---@overload fun(target: CursorNavigable | UiElement, keyInput: KeyConfiguration?): Cursor ---@field keyInput KeyConfiguration ----@field focusStack (UiElement | CursorNavigable)[] ----@field hovered UiElement ----@field focused UiElement | CursorNavigable ----@field hoveredIndex integer ----@field escapeCallback fun(self: Cursor) +---@field focusStack CursorNavigable[] A stack of focused UiElement, new elements go on top, as focus is released it goes back down +---@field focusToHover table Persists the last hovered element for each navigable UiElement so that the last position can be reconstructed when focus is released +---@field focused CursorNavigable The navigable UiElement that is currently consuming the cursor's inputs local Cursor = class( -function(self, options) - self.keyInput = options.keyInput - self.focusStack = {} - - if options.escapeCallback then - self.escapeCallback = options.escapeCallback - end - self:setFocus(options.target) +function(self, target, keyInput) + self.keyInput = keyInput or input + self:setFocus(target) end) - +---@param currentFocus UiElement | CursorNavigable +---@param newFocus UiElement | CursorNavigable function Cursor:moveFocus(currentFocus, newFocus) self:releaseFocus(currentFocus) self:deepenFocus(newFocus) end ---@param uiElement UiElement | CursorNavigable -function Cursor:setFocus(uiElement, callback) - if self.focused then - self.focused.cursor = nil - end - uiElement:receiveFocus(self) - self.focused = uiElement +function Cursor:setFocus(uiElement) self.focusStack = {} + self.focusToHover = {} + self.focused = uiElement + self.focusStack = { uiElement } + uiElement:receiveFocus(self) end ---@param uiElement UiElement | CursorNavigable function Cursor:deepenFocus(uiElement) - self.focusStack[#self.focusStack+1] = self.focused - self.focused.cursor = nil + self.focusToHover[self.focused]:setHover(self, false) + + self.focusStack[#self.focusStack+1] = uiElement self.focused = uiElement - self.focused.cursor = self + uiElement:receiveFocus(self) end ----@param uiElement UiElement +---@param uiElement UiElement | CursorNavigable function Cursor:releaseFocus(uiElement) - for i = #self.focusStack, 2, -1 do - local focused = self.focusStack[i] - self.focusStack[i] = nil - if focused.cursor then - focused.cursor = nil - focused.hoveredElement = nil + if #self.focusStack >= 1 then + for i = #self.focusStack, 2, -1 do + local focused = self.focusStack[i] + local hovered = self.focusToHover[focused] + self.focusStack[i] = nil + self.focusToHover[focused] = nil + hovered:setHover(self, false) + if focused == uiElement then + break + end end - if focused == uiElement then - break + if uiElement.onYield then + uiElement:onYield() end - end - - self.focused = table.remove(self.focusStack, #self.focusStack) -end - -function Cursor:getLastIndex() - for i = #self.focused.children, 1, -1 do - local child = self.focused.children[i] - if child.receiveInputs and child.isEnabled and child.isVisible then - return i - end + self.focused = self.focusStack[#self.focusStack] + self.focusToHover[self.focused]:setHover(self, true) end end -function Cursor:moveToLast() - self.hoveredIndex = self:getLastIndex() - self.hovered = self.focused.children[self.hoveredIndex] -end - - -function Cursor:receiveInputs(dt) - local focused = self.focused - local hoveredElement = self.focused.hoveredElement - self.focused:processCursorInput(dt) - if self.focused.hoveredElement ~= hoveredElement then - hoveredElement.cursorFocus = false - if focused == self.focused then - --self.focused.hoveredElement. +function Cursor:updateHover(focused, newHovered) + local currentHovered = self.focusToHover[focused] + self.focusToHover[focused] = newHovered + if focused == self.focused then + if currentHovered then + currentHovered:setHover(self, false) end + newHovered:setHover(self, true) end end ----@param uiElement UiElement ----@param hoveredIndex integer? -function Cursor:setTarget(uiElement, hoveredIndex) - if self.focused ~= uiElement then - self.hovered = nil - end - self.focused = uiElement - if uiElement.isFocusable then - self:setFocus(self.focused) - if hoveredIndex then - self.hoveredIndex = hoveredIndex - self.hovered = self.focused.children[self.hoveredIndex] - else - self.hoveredIndex = 0 - self:moveToNext() - end - end +function Cursor:receiveInputs(dt) + self.focused:receiveInputs(self, dt) end -function Cursor:drawSelf() +function Cursor:draw() -- GraphicsUtil.setColor(0, 0, 0, 0.2) -- love.graphics.rectangle("fill", self.target.x, self.target.y, self.target.width, self.target.height) - if self.hovered then + if self.focused then + local uiElement = self.focusToHover[self.focused] GraphicsUtil.setColor(1, 1, 1, 0.2) - local x, y = self.hovered:getScreenPos() - love.graphics.rectangle("fill", x, y, self.hovered.width, self.hovered.height) + local x, y = uiElement:getScreenPos() + love.graphics.rectangle("fill", x, y, uiElement.width, uiElement.height) GraphicsUtil.setColor(1, 1, 1, 1) end end -function Cursor:escapeCallback() - if self.hoveredIndex == self:getLastIndex() then - if self.focused then - self.focused:yieldFocus() - else - GAME.navigationStack:pop() - end - else - self:moveToLast() - end -end - return Cursor \ No newline at end of file diff --git a/client/src/ui/CursorInteractable.lua b/client/src/ui/CursorInteractable.lua new file mode 100644 index 00000000..bc202722 --- /dev/null +++ b/client/src/ui/CursorInteractable.lua @@ -0,0 +1,51 @@ + + +---@class CursorInteractable +---@field receiveInputs fun(cursorInteractable: CursorInteractable | UiElement, cursor: Cursor, dt: number?) +---@field setHover fun(cursorInteractable: CursorInteractable | UiElement, cursor: Cursor, hovering: boolean) +---@field isHovered fun(cursorInteractable: CursorInteractable | UiElement): boolean +---@field hoveringCursors table + +---@param cursorInteractable CursorInteractable | UiElement +---@param cursor Cursor +---@param dt number +local function defaultReceiveInputs(cursorInteractable, cursor, dt) + error("UiElement of type " .. (cursorInteractable.TYPE or "unknown") .. " does not implement receiveInputs") +end + +---@param cursorInteractable CursorInteractable | UiElement +---@param cursor Cursor +---@param hovering boolean +local function setHover(cursorInteractable, cursor, hovering) + if not hovering then + cursorInteractable.hoveringCursors[cursor] = nil + else + cursorInteractable.hoveringCursors[cursor] = true + end +end + +---@param cursorInteractable CursorInteractable | UiElement +local function isHovered(cursorInteractable) + if next(cursorInteractable.hoveringCursors) then + return true + else + return false + end +end + + +---@param uiElement UiElement +---@param receiveInputs fun(cursorInteractable: CursorInteractable | UiElement, cursor: Cursor, dt: number?)? +---@return UiElement | CursorInteractable +local function addCursorInteractionInterface(uiElement, receiveInputs) + uiElement.hoveringCursors = {} + uiElement.setHover = setHover + uiElement.isHovered = isHovered + uiElement.receiveInputs = receiveInputs or defaultReceiveInputs + uiElement.isNavigable = false + + ---@cast uiElement +CursorInteractable + return uiElement +end + +return addCursorInteractionInterface \ No newline at end of file diff --git a/client/src/ui/CursorNavigable.lua b/client/src/ui/CursorNavigable.lua index 3c0afd90..9f207e93 100644 --- a/client/src/ui/CursorNavigable.lua +++ b/client/src/ui/CursorNavigable.lua @@ -1,117 +1,114 @@ local consts = require("common.engine.consts") local tableUtils = require("common.lib.tableUtils") +local import = require("common.lib.import") +local addCursorInteractionInterface = import("./CursorInteractable") ----@class CursorNavigable ----@field receiveFocus fun(cursorNavigable: CursorNavigable | UiElement, cursor: Cursor) ----@field processCursorInput fun(cursorNavigable: CursorNavigable | UiElement, dt) ----@field receiveInputs fun(cursorNavigable: CursorNavigable | UiElement, inputs: KeyConfiguration, dt: number?) ----@field moveToNext fun(cursorNavigable: CursorNavigable | UiElement) ----@field moveToPrevious fun(cursorNavigable: CursorNavigable | UiElement) ----@field interactsWithCursor boolean ----@field isFocusable boolean ----@field cursor Cursor? ----@field hoveredElement UiElement? +---@class CursorNavigableOptions +---@field onFocus fun(cursorNavigable: CursorNavigable | UiElement)? +---@field onYield fun(cursorNavigable: CursorNavigable | UiElement)? +---@class CursorNavigable : CursorInteractable +---@field isNavigable boolean +---@field receiveFocus fun(cursorNavigable: CursorNavigable | UiElement, cursor: Cursor) +---@field receiveInputs fun(cursorNavigable: CursorNavigable | UiElement, cursor: Cursor, dt: number?) +---@field moveToNext fun(cursorNavigable: CursorNavigable | UiElement, cursor: Cursor) +---@field moveToPrevious fun(cursorNavigable: CursorNavigable | UiElement, cursor: Cursor) +---@field onFocus fun(cursorNavigable: CursorNavigable | UiElement)? +---@field onYield fun(cursorNavigable: CursorNavigable | UiElement)? +---@param cursorNavigable CursorNavigable | UiElement +---@param cursor Cursor local function receiveFocus(cursorNavigable, cursor) cursorNavigable.cursor = cursor - if not cursorNavigable.hoveredElement then - for i, child in ipairs(cursorNavigable.children) do - if child.isVisible and child.isEnabled then - cursorNavigable.hoveredElement = child - break - end + for i, child in ipairs(cursorNavigable.children) do + if child.receiveInputs and child.isVisible and child.isEnabled then + cursor:updateHover(cursorNavigable, child) + break end end + if cursorNavigable.onFocus then + cursorNavigable:onFocus() + end end ---@param cursorNavigable CursorNavigable | UiElement -local function moveToNext(cursorNavigable) - local hoveredIndex = tableUtils.indexOf(cursorNavigable.children, cursorNavigable.hoveredElement) +---@param cursor Cursor +local function moveToNext(cursorNavigable, cursor) + local hoveredIndex = tableUtils.indexOf(cursorNavigable.children, cursor.focusToHover[cursorNavigable]) for i = hoveredIndex + 1, hoveredIndex + #cursorNavigable.children do local index = wrap(1, i, #cursorNavigable.children) local child = cursorNavigable.children[index] if child.receiveInputs and child.isEnabled and child.isVisible then - hoveredIndex = index - cursorNavigable.hoveredElement = cursorNavigable.children[hoveredIndex] + cursor:updateHover(cursorNavigable, child) break end end end ---@param cursorNavigable CursorNavigable | UiElement -local function moveToPrevious(cursorNavigable) - local hoveredIndex = tableUtils.indexOf(cursorNavigable.children, cursorNavigable.hoveredElement) +---@param cursor Cursor +local function moveToPrevious(cursorNavigable, cursor) + local hoveredIndex = tableUtils.indexOf(cursorNavigable.children, cursor.focusToHover[cursorNavigable]) for i = hoveredIndex - 1, hoveredIndex - #cursorNavigable.children, -1 do local index = wrap(1, i, #cursorNavigable.children) local child = cursorNavigable.children[index] if child.receiveInputs and child.isEnabled and child.isVisible then - hoveredIndex = index - cursorNavigable.hoveredElement = cursorNavigable.children[hoveredIndex] + cursor:updateHover(cursorNavigable, child) break end end end ---@param cursorNavigable CursorNavigable | UiElement ----@param inputs KeyConfiguration +---@param cursor Cursor ---@param dt number -local function defaultReceiveInputs(cursorNavigable, inputs, dt) +local function defaultReceiveInputs(cursorNavigable, cursor, dt) + local inputs = cursor.keyInput if inputs.isDown.Swap2 then GAME.theme:playCancelSfx() - cursorNavigable:escapeCallback() + cursor:releaseFocus(cursorNavigable) + elseif cursor.focusToHover[cursorNavigable].isNavigable and (inputs.isDown.Swap1 or inputs.isDown.Start) then + GAME.theme:playValidationSfx() + cursor:deepenFocus(cursor.focusToHover[cursorNavigable]) elseif cursorNavigable.layout.characteristic == "horizontal" then if inputs:isPressedWithRepeat("Left", consts.KEY_DELAY, consts.KEY_REPEAT_PERIOD) then GAME.theme:playMoveSfx() - cursorNavigable:moveToPrevious() + cursorNavigable:moveToPrevious(cursor) elseif inputs:isPressedWithRepeat("Right", consts.KEY_DELAY, consts.KEY_REPEAT_PERIOD) then GAME.theme:playMoveSfx() - cursorNavigable:moveToNext() + cursorNavigable:moveToNext(cursor) + elseif cursor.focusToHover[cursorNavigable].receiveInputs then + cursor.focusToHover[cursorNavigable]:receiveInputs(inputs, dt) end elseif cursorNavigable.layout.characteristic == "vertical" then if inputs:isPressedWithRepeat("Up", consts.KEY_DELAY, consts.KEY_REPEAT_PERIOD) then GAME.theme:playMoveSfx() - cursorNavigable:moveToPrevious() + cursorNavigable:moveToPrevious(cursor) elseif inputs:isPressedWithRepeat("Down", consts.KEY_DELAY, consts.KEY_REPEAT_PERIOD) then GAME.theme:playMoveSfx() - cursorNavigable:moveToNext() - end - elseif cursorNavigable.hoveredElement.isFocusable then - if inputs.isDown.Swap1 or inputs.isDown.Start then - GAME.theme:playValidationSfx() - cursorNavigable:setFocus(cursorNavigable.hovered) + cursorNavigable:moveToNext(cursor) + elseif cursor.focusToHover[cursorNavigable].receiveInputs then + cursor.focusToHover[cursorNavigable]:receiveInputs(inputs, dt) end - elseif cursorNavigable.hovered.receiveInputs then - cursorNavigable.hovered:receiveInputs(inputs, dt) else GAME.theme:playCancelSfx() end end ----@param cursorNavigable CursorNavigable | UiElement ----@param dt number -local function processCursorInput(cursorNavigable, dt) - if cursorNavigable.cursor then - cursorNavigable:receiveInputs(cursorNavigable.cursor.keyInput, dt) - else - error("Tried to process cursor inputs without a cursor") - end -end - --[[ when adding it to a class instead of a single element:
-- make sure to annotate the class table as its own type using ---@type to get rid of the warning
- have the class definition inherit CursorNavigable
-- you can discard the return value; it is only returned so that when doing it for an instance LuaLS can easily infer the new union type +- discard the return value; it is only returned so that when doing it for an instance, LuaLS can easily infer the new union type ]] ---@param uiElement UiElement +---@param receiveInputs fun(cursorNavigable: CursorNavigable | UiElement, cursor: Cursor, dt: number?)? ---@return UiElement | CursorNavigable -local function addCursorNavigationInterface(uiElement) - uiElement.processCursorInput = processCursorInput +local function addCursorNavigationInterface(uiElement, receiveInputs) + addCursorInteractionInterface(uiElement, receiveInputs or defaultReceiveInputs) uiElement.receiveFocus = receiveFocus uiElement.moveToNext = moveToNext uiElement.moveToPrevious = moveToPrevious - uiElement.receiveInputs = defaultReceiveInputs + uiElement.isNavigable = true ---@cast uiElement +CursorNavigable return uiElement diff --git a/client/src/ui/Layouts/FlexLayout.lua b/client/src/ui/Layouts/FlexLayout.lua index 80c326e5..a682224f 100644 --- a/client/src/ui/Layouts/FlexLayout.lua +++ b/client/src/ui/Layouts/FlexLayout.lua @@ -1,5 +1,5 @@ local import = require("common.lib.import") -local Layout = require(PATH ..".Layout") +local Layout = import("./Layout") local util = require("common.lib.util") ---@class FlexLayout : Layout diff --git a/client/src/ui/Layouts/HorizontalFlexLayout.lua b/client/src/ui/Layouts/HorizontalFlexLayout.lua index d5d03345..1dbf26ac 100644 --- a/client/src/ui/Layouts/HorizontalFlexLayout.lua +++ b/client/src/ui/Layouts/HorizontalFlexLayout.lua @@ -1,5 +1,5 @@ local import = require("common.lib.import") -local FlexLayout = require(PATH ..".FlexLayout") +local FlexLayout = import("./FlexLayout") ---@class HorizontalFlexLayout : FlexLayout local HorizontalFlexLayout = setmetatable({characteristic = "horizontal"}, {__index = FlexLayout}) diff --git a/client/src/ui/Layouts/HorizontalScrollLayout.lua b/client/src/ui/Layouts/HorizontalScrollLayout.lua index fa18786b..0427bf57 100644 --- a/client/src/ui/Layouts/HorizontalScrollLayout.lua +++ b/client/src/ui/Layouts/HorizontalScrollLayout.lua @@ -1,5 +1,5 @@ local import = require("common.lib.import") -local HorizontalFlexLayout = require(PATH ..".HorizontalFlexLayout") +local HorizontalFlexLayout = import("./HorizontalFlexLayout") local util = require("common.lib.util") ---@class HorizontalScrollLayout : HorizontalFlexLayout diff --git a/client/src/ui/Layouts/HorizontalWrapLayout.lua b/client/src/ui/Layouts/HorizontalWrapLayout.lua index 73511994..afae448b 100644 --- a/client/src/ui/Layouts/HorizontalWrapLayout.lua +++ b/client/src/ui/Layouts/HorizontalWrapLayout.lua @@ -1,5 +1,5 @@ local import = require("common.lib.import") -local HorizontalFlexLayout = require(PATH ..".HorizontalFlexLayout") +local HorizontalFlexLayout = import("./HorizontalFlexLayout") local util = require("common.lib.util") ---@class HorizontalWrapLayout : HorizontalFlexLayout diff --git a/client/src/ui/Layouts/StaticLayout.lua b/client/src/ui/Layouts/StaticLayout.lua index de26a386..f9f997ed 100644 --- a/client/src/ui/Layouts/StaticLayout.lua +++ b/client/src/ui/Layouts/StaticLayout.lua @@ -1,5 +1,5 @@ local import = require("common.lib.import") -local Layout = require(PATH ..".Layout") +local Layout = import("./Layout") ---@class StaticLayout : Layout local StaticLayout = setmetatable({}, {__index = Layout}) diff --git a/client/src/ui/Layouts/VerticalFlexLayout.lua b/client/src/ui/Layouts/VerticalFlexLayout.lua index 6bf446f8..af8bfb7e 100644 --- a/client/src/ui/Layouts/VerticalFlexLayout.lua +++ b/client/src/ui/Layouts/VerticalFlexLayout.lua @@ -1,5 +1,5 @@ local import = require("common.lib.import") -local FlexLayout = require(PATH ..".FlexLayout") +local FlexLayout = import("./FlexLayout") ---@class VerticalFlexLayout : FlexLayout local VerticalFlexLayout = setmetatable({characteristic = "vertical"}, {__index = FlexLayout}) diff --git a/client/src/ui/Layouts/VerticalScrollLayout.lua b/client/src/ui/Layouts/VerticalScrollLayout.lua index 9cf97e52..d3490276 100644 --- a/client/src/ui/Layouts/VerticalScrollLayout.lua +++ b/client/src/ui/Layouts/VerticalScrollLayout.lua @@ -1,5 +1,5 @@ local import = require("common.lib.import") -local VerticalFlexLayout = require(PATH ..".VerticalFlexLayout") +local VerticalFlexLayout = import("./VerticalFlexLayout") local util = require("common.lib.util") ---@class VerticalScrollLayout : VerticalFlexLayout diff --git a/client/src/ui/MenuItem.lua b/client/src/ui/MenuItem.lua index 4d946829..6d89660a 100644 --- a/client/src/ui/MenuItem.lua +++ b/client/src/ui/MenuItem.lua @@ -6,6 +6,7 @@ local TextButton = import("./TextButton") local class = require("common.lib.class") local GraphicsUtil = require("client.src.graphics.graphics_util") local HorizontalFlexLayout = import("./Layouts.HorizontalFlexLayout") +local addCursorInteractionInterface = import("./CursorInteractable") -- MenuItem is a specific UIElement that all children of Menu should be ---@class MenuItem @@ -19,6 +20,9 @@ MenuItem.PADDING = 2 -- Takes a label and an optional extra element and makes and combines them into a menu item -- which is suitable for inserting into a menu +---@param label Label +---@param item UiElement +---@return UiElement | CursorInteractable function MenuItem.createMenuItem(label, item) assert(label ~= nil) @@ -42,9 +46,9 @@ function MenuItem.createMenuItem(label, item) item.hAlign = "left" item.hFill = true menuItem:addChild(item) - menuItem.receiveInputs = function(i, inputs) - item:receiveInputs(inputs) - end + addCursorInteractionInterface(menuItem, function(self, cursor, dt) + item:receiveInputs(cursor, dt) + end) end return menuItem diff --git a/client/src/ui/ScrollContainer.lua b/client/src/ui/ScrollContainer.lua index a07f6738..c39439b5 100644 --- a/client/src/ui/ScrollContainer.lua +++ b/client/src/ui/ScrollContainer.lua @@ -42,21 +42,28 @@ end -- update the scroll offset so the element with passed scroll offset + size remains visible -- usually we want a cursor type of object to call this on move to automatically advance the scrollContainer ----@param offset number the offset of the element to be kept visible ----@param size number the size of the element to be kept visible -function ScrollContainer:keepVisible(offset, size) - logger.debug("Firing ScrollContainer.keepVisible") +---@param cursor Cursor +function ScrollContainer:keepVisible(cursor) + local hoveredElement = cursor.focusToHover[self] + local offset -- the offset of the element to be kept visible + local size -- the size of the element to be kept visible + local refSize + if self.scrollOrientation == "horizontal" then + offset = hoveredElement.x + size = hoveredElement.width + refSize = self.width + else + offset = hoveredElement.y + size = hoveredElement.height + refSize = self.height + end + logger.debug("Firing ScrollContiner.keepVisible") -- weird implementation detail that with scrolling scrolloffset goes negative -- childGap is added to guarantee some whitespace if the container is built around it offset = -offset + self.childGap / 2 size = size + self.childGap -- with increasing negative value we scroll further down/right - local refSize - if self.scrollOrientation == "vertical" then - refSize = self.height - else - refSize = self.width - end + if self.scrollOffset - refSize > offset - size then self:setScrollOffset(offset - size + refSize) elseif offset > self.scrollOffset then @@ -220,4 +227,8 @@ function ScrollContainer:getPreferredWidth() return self.minWidth end +function ScrollContainer:getScreenPos() + +end + return ScrollContainer \ No newline at end of file diff --git a/client/src/ui/Slider.lua b/client/src/ui/Slider.lua index 91f1f202..827f4f4a 100644 --- a/client/src/ui/Slider.lua +++ b/client/src/ui/Slider.lua @@ -3,6 +3,7 @@ local UIElement = import("./UIElement") local class = require("common.lib.class") local util = require("common.lib.util") local GraphicsUtil = require("client.src.graphics.graphics_util") +local addCursorInteractionInterface = import("./CursorInteractable") local handleRadius = 7.5 local xPadding = 8 @@ -23,7 +24,7 @@ local sliderBarThickness = 6 ---@field height nil height is calculated internally based on min, max, tickLength and tickAmount -- A horizontal Slider element ----@class Slider: UiElement +---@class Slider: UiElement, CursorInteractable ---@operator call(SliderOptions): Slider ---@field min number minimum value ---@field max number maximum value @@ -70,6 +71,7 @@ local Slider = class( UIElement ) Slider.TYPE = "Slider" +addCursorInteractionInterface(Slider) function Slider:onTouch(x, y) self:setValueFromPos(x, false) @@ -83,7 +85,10 @@ function Slider:onRelease(x, y) self:setValueFromPos(x, true) end -function Slider:receiveInputs(input) +---@param cursor Cursor +---@param dt number? +function Slider:receiveInputs(cursor, dt) + local input = cursor.keyInput if input:isPressedWithRepeat("Left") then self:setValue(self.value - self.tickAmount, true) elseif input:isPressedWithRepeat("Right") then diff --git a/client/src/ui/Stepper.lua b/client/src/ui/Stepper.lua index 8fb30518..9acde8df 100644 --- a/client/src/ui/Stepper.lua +++ b/client/src/ui/Stepper.lua @@ -6,10 +6,12 @@ local class = require("common.lib.class") local util = require("common.lib.util") local GraphicsUtil = require("client.src.graphics.graphics_util") local HorizontalFlexLayout = import("./Layouts.HorizontalFlexLayout") +local addCursorInteractionInterface = import("./CursorInteractable") local NAV_BUTTON_WIDTH = 25 -- UIElement representing a scrolling list of options +---@class Stepper : UiElement, CursorInteractable local Stepper = class( function(self, options) self.onChange = options.onChange or function() end @@ -52,6 +54,8 @@ local Stepper = class( Stepper.TYPE = "Stepper" Stepper.layout = HorizontalFlexLayout +addCursorInteractionInterface(Stepper) + function Stepper:setLabels(labels, values, selectedIndex) self.selectedIndex = selectedIndex self.values = values @@ -91,7 +95,10 @@ function Stepper.setState(self, i) self.onChange(self.value) end -function Stepper:receiveInputs(input) +---@param cursor Cursor +---@param dt number? +function Stepper:receiveInputs(cursor, dt) + local input = cursor.keyInput if input:isPressedWithRepeat("Left") then self:setState(self.selectedIndex - 1) elseif input:isPressedWithRepeat("Right") then diff --git a/client/src/ui/UniSizedContainer.lua b/client/src/ui/UniSizedContainer.lua index c5a435b9..ab6d9df1 100644 --- a/client/src/ui/UniSizedContainer.lua +++ b/client/src/ui/UniSizedContainer.lua @@ -8,7 +8,7 @@ local GraphicsUtil = require("client.src.graphics.graphics_util") local input = require("client.src.inputManager") local CursorNavigable = require("client.src.ui.CursorNavigable") ----@class UniSizedContainerOptions : UiElementOptions +---@class UniSizedContainerOptions : UiElementOptions, CursorNavigableOptions ---@field childrenWidth integer ---@field childrenHeight integer @@ -20,7 +20,6 @@ local CursorNavigable = require("client.src.ui.CursorNavigable") ---@field selectedColumn integer ---@field rows UiElement[][] ---@overload fun(options: UniSizedContainerOptions): UniSizedContainer ----@type UniSizedContainer local UniSizedContainer = class( function(self, options) assert(options.childrenHeight and options.childrenWidth) @@ -49,8 +48,9 @@ function UniSizedContainer:addChild(uiElement, index) UiElement.addChild(self, uiElement, index) end +---@param cursor Cursor ---@return boolean? # if the movement was successful -function UniSizedContainer:moveToPreviousRow() +function UniSizedContainer:moveToPreviousRow(cursor) if not self.rows or #self.rows == 1 then return false end @@ -58,14 +58,16 @@ function UniSizedContainer:moveToPreviousRow() local nextRow = wrap(1, self.selectedRow - 1, #self.rows) if self.rows[nextRow][self.selectedColumn] then self.selectedRow = nextRow + cursor:updateHover(self, self.rows[self.selectedRow][self.selectedColumn]) return true else return false end end +---@param cursor Cursor ---@return boolean? # if the movement was successful -function UniSizedContainer:moveToNextRow() +function UniSizedContainer:moveToNextRow(cursor) if not self.rows or #self.rows == 1 then return false end @@ -73,69 +75,78 @@ function UniSizedContainer:moveToNextRow() local nextRow = wrap(1, self.selectedRow + 1, #self.rows) if self.rows[nextRow][self.selectedColumn] then self.selectedRow = nextRow + cursor:updateHover(self, self.rows[self.selectedRow][self.selectedColumn]) return true else return false end end +---@param cursor Cursor ---@return boolean? # if the movement was successful -function UniSizedContainer:moveToPrevious() +function UniSizedContainer:moveToPrevious(cursor) if not self.rows or not self.rows[self.selectedRow] or #self.rows[self.selectedRow] == 1 then return false end self.selectedColumn = wrap(1, self.selectedColumn - 1, #self.rows[self.selectedRow]) + cursor:updateHover(self, self.rows[self.selectedRow][self.selectedColumn]) return true end +---@param cursor Cursor ---@return boolean? # if the movement was successful -function UniSizedContainer:moveToNext() +function UniSizedContainer:moveToNext(cursor) if not self.rows or not self.rows[self.selectedRow] or #self.rows[self.selectedRow] == 1 then return false end self.selectedColumn = wrap(1, self.selectedColumn + 1, #self.rows[self.selectedRow]) + cursor:updateHover(self, self.rows[self.selectedRow][self.selectedColumn]) return true end -function UniSizedContainer:receiveInputs(inputs, dt) +---@param cursor Cursor +---@param dt number? +function UniSizedContainer:receiveInputs(cursor, dt) + local inputs = cursor.keyInput if inputs.isDown.Swap2 then GAME.theme:playCancelSfx() - self.cursor:releaseFocus(self) + cursor:releaseFocus(self) elseif inputs:isPressedWithRepeat("Left", consts.KEY_DELAY, consts.KEY_REPEAT_PERIOD) then - if self:moveToPrevious() then + if self:moveToPrevious(cursor) then GAME.theme:playMoveSfx() + else + GAME.theme:playCancelSfx() end elseif inputs:isPressedWithRepeat("Right", consts.KEY_DELAY, consts.KEY_REPEAT_PERIOD) then - if self:moveToNext() then + if self:moveToNext(cursor) then GAME.theme:playMoveSfx() + else + GAME.theme:playCancelSfx() end elseif inputs:isPressedWithRepeat("Up", consts.KEY_DELAY, consts.KEY_REPEAT_PERIOD) then - if self:moveToPreviousRow() then + if self:moveToPreviousRow(cursor) then GAME.theme:playMoveSfx() + else + GAME.theme:playCancelSfx() end elseif inputs:isPressedWithRepeat("Down", consts.KEY_DELAY, consts.KEY_REPEAT_PERIOD) then - if self:moveToNextRow() then + if self:moveToNextRow(cursor) then GAME.theme:playMoveSfx() + else + GAME.theme:playCancelSfx() end - elseif self.rows[self.selectedRow][self.selectedColumn].isFocusable then - if inputs.isDown.Swap1 or inputs.isDown.Start then - GAME.theme:playValidationSfx() - self.cursor:deepenFocus(self.rows[self.selectedRow][self.selectedColumn]) - end + elseif self.rows[self.selectedRow][self.selectedColumn].isNavigable and (inputs.isDown.Swap1 or inputs.isDown.Start) then + GAME.theme:playValidationSfx() + cursor:deepenFocus(self.rows[self.selectedRow][self.selectedColumn]) elseif self.rows[self.selectedRow][self.selectedColumn].receiveInputs then - self.rows[self.selectedRow][self.selectedColumn]:receiveInputs(inputs, dt) + self.rows[self.selectedRow][self.selectedColumn]:receiveInputs(cursor, dt) else GAME.theme:playCancelSfx() end end -function UniSizedContainer:processCursorInput(dt) - local inputs = self.cursor.keyInput - self:receiveInputs(inputs, dt) -end - function UniSizedContainer:onResized() if #self.children == 0 then return diff --git a/client/src/ui/VerticalMenu.lua b/client/src/ui/VerticalMenu.lua index 4bd8439f..bcd90b2e 100644 --- a/client/src/ui/VerticalMenu.lua +++ b/client/src/ui/VerticalMenu.lua @@ -1,14 +1,11 @@ local import = require("common.lib.import") -PATH = "client.src.ui" local ScrollContainer = import("./ScrollContainer") local class = require("common.lib.class") local util = require("common.lib.util") local GraphicsUtil = require("client.src.graphics.graphics_util") -local Focusable = import("./Focusable") -local FocusDirector = import("./FocusDirector") -local input = require("client.src.inputManager") local VerticalScrollLayout = import("./Layouts.VerticalScrollLayout") -local CursorNavigable = import("./CursorNavigable") +local tableUtils = require("common.lib.tableUtils") +local addCursorNavigationInterface = import("./CursorNavigable") ---@class VerticalMenu : ScrollContainer, Focusable ---@operator call(ScrollContainerOptions): VerticalMenu @@ -22,91 +19,83 @@ function(self, options) end, ScrollContainer) -CursorNavigable(VerticalMenu) +VerticalMenu.TYPE = "VerticalMenu" +addCursorNavigationInterface(VerticalMenu) -Focusable(VerticalMenu) -FocusDirector(VerticalMenu) - -function VerticalMenu:setInitialFocus() - for i, child in ipairs(self.children) do - if child.receiveInputs and child.isEnabled and child.isVisible then - self.selectedIndex = i - break - end - end -end - -function VerticalMenu:selectPrevious() - for i = self.selectedIndex - 1, self.selectedIndex - #self.children, -1 do +---@param cursor Cursor +function VerticalMenu:selectPrevious(cursor) + local hoveredIndex = tableUtils.indexOf(self.children, cursor.focusToHover[self]) + for i = hoveredIndex - 1, hoveredIndex - #self.children, -1 do local index = wrap(1, i, #self.children) local child = self.children[index] if child.receiveInputs and child.isEnabled and child.isVisible then - self.selectedIndex = index + cursor:updateHover(self, self.children[index]) break end end - self:keepVisible(self.children[self.selectedIndex].y, self.children[self.selectedIndex].height) + self:keepVisible(cursor) GAME.theme:playMoveSfx() end -function VerticalMenu:selectNext() - for i = self.selectedIndex + 1, self.selectedIndex + #self.children do +---@param cursor Cursor +function VerticalMenu:selectNext(cursor) + local hoveredIndex = tableUtils.indexOf(self.children, cursor.focusToHover[self]) + for i = hoveredIndex + 1, hoveredIndex + #self.children do local index = wrap(1, i, #self.children) local child = self.children[index] if child.receiveInputs and child.isEnabled and child.isVisible then - self.selectedIndex = index + cursor:updateHover(self, self.children[index]) break end end - self:keepVisible(self.children[self.selectedIndex].y, self.children[self.selectedIndex].height) + self:keepVisible(cursor) GAME.theme:playMoveSfx() end -function VerticalMenu:selectLast() +---@param cursor Cursor +function VerticalMenu:selectLast(cursor) + cursor:updateHover(self, self:getLast()) + self:keepVisible(cursor) +end + +---@return CursorInteractable | UiElement +function VerticalMenu:getLast() for i = #self.children, 1, -1 do local child = self.children[i] if child.receiveInputs and child.isEnabled and child.isVisible then - self.selectedIndex = i - break + return child end end - self:keepVisible(self.children[self.selectedIndex].y, self.children[self.selectedIndex].height) -end -function VerticalMenu:receiveInputs(inputs, dt) - if not self.selectedIndex then - self:setInitialFocus() - end + return self.children[1] +end +---@param cursor Cursor +---@param dt number? +function VerticalMenu:receiveInputs(cursor, dt) if not self.isEnabled then return end - if not inputs then - -- if we don't get inputs passed, use the global input table - inputs = input - end - - local selectedElement = self.children[self.selectedIndex] + local inputs = cursor.keyInput + local selectedElement = cursor.focusToHover[self] - if self.focused then - self.focused:receiveInputs(inputs, dt) - elseif inputs.isDown["MenuEsc"] then - if self.selectedIndex ~= #self.children then - self:selectLast() + if inputs.isDown["MenuEsc"] then + if self:getLast() ~= selectedElement then + self:selectLast(cursor) GAME.theme:playCancelSfx() else - selectedElement:receiveInputs(inputs, dt) + selectedElement:receiveInputs(cursor, dt) end elseif inputs:isPressedWithRepeat("MenuUp") then - self:selectPrevious() + self:selectPrevious(cursor) elseif inputs:isPressedWithRepeat("MenuDown") then - self:selectNext() + self:selectNext(cursor) else - if inputs.isDown["MenuSelect"] and selectedElement.isFocusable then - self:setFocus(selectedElement) + if inputs.isDown["MenuSelect"] and selectedElement.isNavigable then + self:deepenFocus(selectedElement) else - selectedElement:receiveInputs(inputs, dt) + selectedElement:receiveInputs(cursor, dt) end end end diff --git a/client/src/ui/init.lua b/client/src/ui/init.lua index fafaddeb..834bd8cb 100644 --- a/client/src/ui/init.lua +++ b/client/src/ui/init.lua @@ -1,4 +1,3 @@ -local PATH = (...):gsub('%.[^%.]+$', '') local import = require("common.lib.import") --[[ @@ -17,7 +16,7 @@ so that you get intellisense local ui = { ---@source BoolSelector.lua ---@type BoolSelector - BoolSelector = require(PATH ..".BoolSelector"), + BoolSelector = import("./BoolSelector"), ---@source Button.lua ---@type Button Button = import("./Button"), @@ -30,6 +29,10 @@ local ui = { ---@source Cursor.lua ---@type Cursor Cursor = import("./Cursor"), + ---@source CursorInteractable + CursorInteractable = import("./CursorInteractable"), + ---@source CursorNavigable.lua + CursorNavigable = import("./CursorNavigable"), Focusable = import("./Focusable"), FocusDirector = import("./FocusDirector"), Grid = import("./Grid"), From 8fadb861de71de9e89e84a71398881ae8c0edef1 Mon Sep 17 00:00:00 2001 From: Endaris Date: Thu, 29 May 2025 02:00:28 +0200 Subject: [PATCH 30/49] fix getScreenPos() for children living in ScrollContainers not yielding the actual on-screen position --- client/src/ui/ScrollContainer.lua | 22 +++++++++++++++++++--- client/src/ui/UIElement.lua | 5 +++-- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/client/src/ui/ScrollContainer.lua b/client/src/ui/ScrollContainer.lua index c39439b5..a96335eb 100644 --- a/client/src/ui/ScrollContainer.lua +++ b/client/src/ui/ScrollContainer.lua @@ -75,9 +75,9 @@ end local function getTranslatedOffset(scrollContainer, x, y) local translatedX, translatedY = x, y if scrollContainer.scrollOrientation == "vertical" then - translatedY = translatedY - scrollContainer.scrollOffset + translatedY = translatedY + scrollContainer.scrollOffset else - translatedX = translatedX - scrollContainer.scrollOffset + translatedX = translatedX + scrollContainer.scrollOffset end return translatedX, translatedY end @@ -227,8 +227,24 @@ function ScrollContainer:getPreferredWidth() return self.minWidth end -function ScrollContainer:getScreenPos() +---@param whoIsAsking table? +function ScrollContainer:getScreenPos(whoIsAsking) + local x, y = 0, 0 + if self.parent then + x, y = self.parent:getScreenPos(self) + end + + x = x + self.x + y = y + self.y + + if whoIsAsking and whoIsAsking.parent and whoIsAsking.parent == self then + --local xOffset, yOffset = getTranslatedOffset(self, 0, 0) + --x = x - xOffset + --y = y - yOffset + x, y = getTranslatedOffset(self, x, y) + end + return x, y end return ScrollContainer \ No newline at end of file diff --git a/client/src/ui/UIElement.lua b/client/src/ui/UIElement.lua index d96f8adc..525468ac 100644 --- a/client/src/ui/UIElement.lua +++ b/client/src/ui/UIElement.lua @@ -175,10 +175,11 @@ end function UIElement:onDetach() end -function UIElement:getScreenPos() +---@param whoIsAsking table? +function UIElement:getScreenPos(whoIsAsking) local x, y = 0, 0 if self.parent then - x, y = self.parent:getScreenPos() + x, y = self.parent:getScreenPos(self) end return x + self.x, y + self.y From 24015c58817029b171f31df51e9bbd7f1e5655b2 Mon Sep 17 00:00:00 2001 From: Endaris Date: Thu, 29 May 2025 02:46:44 +0200 Subject: [PATCH 31/49] fix touch detection in ScrollContainers and mostly unbrick DesignHelper --- client/src/scenes/DesignHelper.lua | 12 ++++++------ client/src/ui/CursorNavigable.lua | 14 ++++++++------ client/src/ui/ScrollContainer.lua | 19 +++++++++---------- client/src/ui/UniSizedContainer.lua | 3 +++ 4 files changed, 26 insertions(+), 22 deletions(-) diff --git a/client/src/scenes/DesignHelper.lua b/client/src/scenes/DesignHelper.lua index 734cf213..1d6aa95f 100644 --- a/client/src/scenes/DesignHelper.lua +++ b/client/src/scenes/DesignHelper.lua @@ -28,9 +28,9 @@ local function getSelectorTemplate(id) local label = ui.Label({id = id}) selector:addChild(button) selector:addChild(label) - selector.receiveInputs = function(selector, cursor, dt) + ui.CursorInteractable(selector, function(selector, cursor, dt) button:receiveInputs(cursor, dt) - end + end) return selector, button end @@ -57,7 +57,7 @@ local function createCharacterSelect(scene) end function DesignHelper:load() - self.characterSelect = createCharacterSelect() + self.characterSelect = createCharacterSelect(self) self.uiRoot.layout = ui.Layouts.VerticalFlexLayout self.uiRoot.childGap = 8 ui.CursorNavigable(self.uiRoot) @@ -74,7 +74,7 @@ function DesignHelper:load() roomMode:addChild(ui.Label({text = "Battle", hAlign = "center", vAlign = "center"})) roomMode:addChild(ui.Label({text = "Arcade", hAlign = "center", vAlign = "center"})) - roomMode.receiveInputs = function() end + ui.CursorInteractable(roomMode, function() end) self.uiRoot:addChild(roomMode) @@ -96,7 +96,7 @@ function DesignHelper:load() gameMode:addChild(ui.Label({text = "Training"})) gameMode:addChild(ui.Label({text = "Line Clear"})) - gameMode.receiveInputs = function() end + ui.CursorInteractable(gameMode, function() end) self.uiRoot:addChild(gameMode) @@ -214,7 +214,7 @@ function DesignHelper:load() backgroundColor = {0, 1, 0, 0.5}--{0.7, 0, 0.5, 1}, }) - self.subSelection.receiveInputs = function() end + ui.CursorInteractable(self.subSelection, function() end) self.uiRoot:addChild(self.subSelection) diff --git a/client/src/ui/CursorNavigable.lua b/client/src/ui/CursorNavigable.lua index 9f207e93..e14fea37 100644 --- a/client/src/ui/CursorNavigable.lua +++ b/client/src/ui/CursorNavigable.lua @@ -19,16 +19,16 @@ local addCursorInteractionInterface = import("./CursorInteractable") ---@param cursorNavigable CursorNavigable | UiElement ---@param cursor Cursor local function receiveFocus(cursorNavigable, cursor) - cursorNavigable.cursor = cursor + if cursorNavigable.onFocus then + cursorNavigable:onFocus() + end + for i, child in ipairs(cursorNavigable.children) do if child.receiveInputs and child.isVisible and child.isEnabled then cursor:updateHover(cursorNavigable, child) break end end - if cursorNavigable.onFocus then - cursorNavigable:onFocus() - end end ---@param cursorNavigable CursorNavigable | UiElement @@ -78,7 +78,8 @@ local function defaultReceiveInputs(cursorNavigable, cursor, dt) GAME.theme:playMoveSfx() cursorNavigable:moveToNext(cursor) elseif cursor.focusToHover[cursorNavigable].receiveInputs then - cursor.focusToHover[cursorNavigable]:receiveInputs(inputs, dt) + local hovered = cursor.focusToHover[cursorNavigable] + hovered:receiveInputs(cursor, dt) end elseif cursorNavigable.layout.characteristic == "vertical" then if inputs:isPressedWithRepeat("Up", consts.KEY_DELAY, consts.KEY_REPEAT_PERIOD) then @@ -88,7 +89,8 @@ local function defaultReceiveInputs(cursorNavigable, cursor, dt) GAME.theme:playMoveSfx() cursorNavigable:moveToNext(cursor) elseif cursor.focusToHover[cursorNavigable].receiveInputs then - cursor.focusToHover[cursorNavigable]:receiveInputs(inputs, dt) + local hovered = cursor.focusToHover[cursorNavigable] + hovered:receiveInputs(cursor, dt) end else GAME.theme:playCancelSfx() diff --git a/client/src/ui/ScrollContainer.lua b/client/src/ui/ScrollContainer.lua index a96335eb..71944d59 100644 --- a/client/src/ui/ScrollContainer.lua +++ b/client/src/ui/ScrollContainer.lua @@ -75,9 +75,9 @@ end local function getTranslatedOffset(scrollContainer, x, y) local translatedX, translatedY = x, y if scrollContainer.scrollOrientation == "vertical" then - translatedY = translatedY + scrollContainer.scrollOffset + translatedY = translatedY - scrollContainer.scrollOffset else - translatedX = translatedX + scrollContainer.scrollOffset + translatedX = translatedX - scrollContainer.scrollOffset end return translatedX, translatedY end @@ -92,9 +92,12 @@ function ScrollContainer:onTouch(x, y) local realTouchedElement = self:getTouchedChildElement(x, y) if realTouchedElement then + local sX, sY = self:getScreenPos() + logger.debug("ScrollContainer screenPos: " .. sX .. "|" .. sY) + logger.debug("touchChild coordinates: " .. realTouchedElement.x .. "|" .. realTouchedElement.y) self.touchedChild = realTouchedElement if self.touchedChild.onTouch then - x, y = getTranslatedOffset(self, x, y) + logger.debug("scroll translated touch coordinates: " .. x .. "|" .. y) self.touchedChild:onTouch(x, y) end end @@ -110,7 +113,6 @@ function ScrollContainer:onDrag(x, y) self:setScrollOffset(self.originalOffset + (x - self.initialTouchX)) end else - x, y = getTranslatedOffset(self, x, y) self.touchedChild:onDrag(x, y) end end @@ -121,7 +123,6 @@ function ScrollContainer:onRelease(x, y, duration) if self.touchedChild then if self.touchedChild.onRelease then - x, y = getTranslatedOffset(self, x, y) self.touchedChild:onRelease(x, y) end self.touchedChild = nil @@ -196,7 +197,6 @@ end -- in order for the "offscreen" children to pass the inBounds check, the touch coordinates get corrected by the scroll offset before recursing down the ui function ScrollContainer:getTouchedChildElement(x, y) - x, y = getTranslatedOffset(self, x, y) local touchedElement for i = 1, #self.children do touchedElement = self.children[i]:getTouchedElement(x, y) @@ -238,10 +238,9 @@ function ScrollContainer:getScreenPos(whoIsAsking) y = y + self.y if whoIsAsking and whoIsAsking.parent and whoIsAsking.parent == self then - --local xOffset, yOffset = getTranslatedOffset(self, 0, 0) - --x = x - xOffset - --y = y - yOffset - x, y = getTranslatedOffset(self, x, y) + local xOffset, yOffset = getTranslatedOffset(self, 0, 0) + x = x - xOffset + y = y - yOffset end return x, y diff --git a/client/src/ui/UniSizedContainer.lua b/client/src/ui/UniSizedContainer.lua index ab6d9df1..1c824609 100644 --- a/client/src/ui/UniSizedContainer.lua +++ b/client/src/ui/UniSizedContainer.lua @@ -30,6 +30,9 @@ function(self, options) self.selectedRow = 1 self.selectedColumn = 1 + + self.onFocus = options.onFocus + self.onYield = options.onYield end, UiElement) From d8a47faf53027e9fa478e60a32943b58b18822c4 Mon Sep 17 00:00:00 2001 From: Endaris Date: Sat, 31 May 2025 22:04:27 +0200 Subject: [PATCH 32/49] some cleanup --- client/src/ui/BoolSelector.lua | 2 +- client/src/ui/Button.lua | 4 +++- client/src/ui/ButtonGroup.lua | 2 +- client/src/ui/CursorInteractable.lua | 13 ++++--------- client/src/ui/Focusable.lua | 16 ---------------- client/src/ui/Slider.lua | 3 ++- client/src/ui/Stepper.lua | 3 ++- client/src/ui/UIElement.lua | 4 ---- client/src/ui/UniSizedContainer.lua | 4 ++-- client/src/ui/VerticalMenu.lua | 2 +- 10 files changed, 16 insertions(+), 37 deletions(-) diff --git a/client/src/ui/BoolSelector.lua b/client/src/ui/BoolSelector.lua index 287fb857..d098b626 100644 --- a/client/src/ui/BoolSelector.lua +++ b/client/src/ui/BoolSelector.lua @@ -15,11 +15,11 @@ local GraphicsUtil = require("client.src.graphics.graphics_util") local BoolSelector = class(function(boolSelector, options) boolSelector.value = options.startValue or false boolSelector.vertical = false + addCursorInteractionInterface(boolSelector, boolSelector.receiveInputs) end, UiElement) BoolSelector.TYPE = "BoolSelector" -addCursorInteractionInterface(BoolSelector) function BoolSelector:onTouch(x, y) end diff --git a/client/src/ui/Button.lua b/client/src/ui/Button.lua index 1069d834..7c022b05 100644 --- a/client/src/ui/Button.lua +++ b/client/src/ui/Button.lua @@ -9,6 +9,7 @@ local addCursorInteractionInterface = import("./CursorInteractable") ---@field backgroundColor number[]? ---@field outlineColor number[]? ---@field onClick fun(button: Button?, input: table?, timeHeld: number?)? +---@field receiveInputs fun(button: Button, cursor: Cursor, dt: number?)? ---@class Button : UiElement, CursorInteractable ---@operator call(ButtonOptions): Button @@ -24,12 +25,13 @@ local Button = class( self.onClick = options.onClick or function() GAME.theme:playValidationSfx() end + + addCursorInteractionInterface(self, options.receiveInputs or self.receiveInputs) end, UIElement ) Button.TYPE = "Button" -addCursorInteractionInterface(Button) function Button:onTouch(x, y) self.backgroundColor[4] = 1 diff --git a/client/src/ui/ButtonGroup.lua b/client/src/ui/ButtonGroup.lua index 76c70e0f..6d41da7e 100644 --- a/client/src/ui/ButtonGroup.lua +++ b/client/src/ui/ButtonGroup.lua @@ -78,12 +78,12 @@ local ButtonGroup = class( self.onChange = options.onChange or function() end setButtons(self, options.buttons, options.values, self.selectedIndex) + addCursorInteractionInterface(self, self.receiveInputs) end, UIElement ) ButtonGroup.TYPE = "ButtonGroup" ButtonGroup.layout = HorizontalFlexLayout -addCursorInteractionInterface(ButtonGroup) -- changes state for the button group -- updates the color of the selected button diff --git a/client/src/ui/CursorInteractable.lua b/client/src/ui/CursorInteractable.lua index bc202722..cdd455cd 100644 --- a/client/src/ui/CursorInteractable.lua +++ b/client/src/ui/CursorInteractable.lua @@ -6,13 +6,6 @@ ---@field isHovered fun(cursorInteractable: CursorInteractable | UiElement): boolean ---@field hoveringCursors table ----@param cursorInteractable CursorInteractable | UiElement ----@param cursor Cursor ----@param dt number -local function defaultReceiveInputs(cursorInteractable, cursor, dt) - error("UiElement of type " .. (cursorInteractable.TYPE or "unknown") .. " does not implement receiveInputs") -end - ---@param cursorInteractable CursorInteractable | UiElement ---@param cursor Cursor ---@param hovering boolean @@ -35,13 +28,15 @@ end ---@param uiElement UiElement ----@param receiveInputs fun(cursorInteractable: CursorInteractable | UiElement, cursor: Cursor, dt: number?)? +---@param receiveInputs fun(cursorInteractable: CursorInteractable | UiElement, cursor: Cursor, dt: number?) ---@return UiElement | CursorInteractable local function addCursorInteractionInterface(uiElement, receiveInputs) uiElement.hoveringCursors = {} uiElement.setHover = setHover uiElement.isHovered = isHovered - uiElement.receiveInputs = receiveInputs or defaultReceiveInputs + if not uiElement.receiveInputs or uiElement.receiveInputs ~= receiveInputs then + uiElement.receiveInputs = receiveInputs + end uiElement.isNavigable = false ---@cast uiElement +CursorInteractable diff --git a/client/src/ui/Focusable.lua b/client/src/ui/Focusable.lua index 6eb8b146..6734fbc4 100644 --- a/client/src/ui/Focusable.lua +++ b/client/src/ui/Focusable.lua @@ -6,10 +6,6 @@ ---@field hasFocus boolean? ---@field yieldFocus fun()? ----@class CursorInteractable ----@field interactsWithCursor boolean ----@field receiveInputs fun(any, Cursor, number?) - local function focusable(uiElement) uiElement.isFocusable = true uiElement.hasFocus = false @@ -27,16 +23,4 @@ local function focusable(uiElement) -- end end -local function receiveFocus(cursorNavigable, cursor) - cursorNavigable.cursor = cursor - if not cursorNavigable.hoveredElement then - for i, child in ipairs(cursorNavigable.children) do - if child.isVisible and child.isEnabled then - cursorNavigable.hoveredElement = child - break - end - end - end -end - return focusable \ No newline at end of file diff --git a/client/src/ui/Slider.lua b/client/src/ui/Slider.lua index 827f4f4a..6b1eb913 100644 --- a/client/src/ui/Slider.lua +++ b/client/src/ui/Slider.lua @@ -67,11 +67,12 @@ local Slider = class( self.width = self.tickLength * self:tickCount() + xPadding + math.max(xPadding, textWidth / 2) self.height = handleRadius * 2 + valueTextHeight + textHeight self.minHeight = self.height + + addCursorInteractionInterface(self, self.receiveInputs) end, UIElement ) Slider.TYPE = "Slider" -addCursorInteractionInterface(Slider) function Slider:onTouch(x, y) self:setValueFromPos(x, false) diff --git a/client/src/ui/Stepper.lua b/client/src/ui/Stepper.lua index 9acde8df..6ec600b1 100644 --- a/client/src/ui/Stepper.lua +++ b/client/src/ui/Stepper.lua @@ -48,13 +48,14 @@ local Stepper = class( self:setLabels(options.labels, options.values, self.selectedIndex) self.color = {.5, .5, 1, .7} self.borderColor = {.7, .7, 1, .7} + + addCursorInteractionInterface(self, self.receiveInputs) end, UIElement ) Stepper.TYPE = "Stepper" Stepper.layout = HorizontalFlexLayout -addCursorInteractionInterface(Stepper) function Stepper:setLabels(labels, values, selectedIndex) self.selectedIndex = selectedIndex diff --git a/client/src/ui/UIElement.lua b/client/src/ui/UIElement.lua index 525468ac..a205b15f 100644 --- a/client/src/ui/UIElement.lua +++ b/client/src/ui/UIElement.lua @@ -260,10 +260,6 @@ function UIElement:isTouchable() or self.onRelease end -function UIElement:isFocused() - return self.cursorFocus or self.mouseFocus -end - function UIElement:getTouchedElement(x, y) if self.isVisible and self.isEnabled and self:inBounds(x, y) then local touchedElement diff --git a/client/src/ui/UniSizedContainer.lua b/client/src/ui/UniSizedContainer.lua index 1c824609..6ab00398 100644 --- a/client/src/ui/UniSizedContainer.lua +++ b/client/src/ui/UniSizedContainer.lua @@ -6,7 +6,7 @@ local Focusable = import("./Focusable") local consts = require("common.engine.consts") local GraphicsUtil = require("client.src.graphics.graphics_util") local input = require("client.src.inputManager") -local CursorNavigable = require("client.src.ui.CursorNavigable") +local addCursorNavigationInterface = import("./CursorNavigable") ---@class UniSizedContainerOptions : UiElementOptions, CursorNavigableOptions ---@field childrenWidth integer @@ -31,12 +31,12 @@ function(self, options) self.selectedRow = 1 self.selectedColumn = 1 + addCursorNavigationInterface(self, self.receiveInputs) self.onFocus = options.onFocus self.onYield = options.onYield end, UiElement) -CursorNavigable(UniSizedContainer) UniSizedContainer.TYPE = "UniSizedContainer" UniSizedContainer.layout = HorizontalWrapLayout diff --git a/client/src/ui/VerticalMenu.lua b/client/src/ui/VerticalMenu.lua index bcd90b2e..1af22f2b 100644 --- a/client/src/ui/VerticalMenu.lua +++ b/client/src/ui/VerticalMenu.lua @@ -16,11 +16,11 @@ function(self, options) self.selectedIndex = nil self.scrollOrientation = "vertical" self.layout = VerticalScrollLayout + addCursorNavigationInterface(self, self.receiveInputs) end, ScrollContainer) VerticalMenu.TYPE = "VerticalMenu" -addCursorNavigationInterface(VerticalMenu) ---@param cursor Cursor function VerticalMenu:selectPrevious(cursor) From 4d4b0ffa5f97b953c644a9b71a8fae2445243f5b Mon Sep 17 00:00:00 2001 From: Endaris Date: Sat, 31 May 2025 22:17:21 +0200 Subject: [PATCH 33/49] more cleanup --- client/src/inputManager.lua | 24 ------------------------ client/src/ui/CursorInteractable.lua | 3 ++- client/src/ui/CursorNavigable.lua | 7 ++----- client/src/ui/ScrollContainer.lua | 6 ++---- client/src/ui/UIElement.lua | 2 -- common/lib/import.lua | 4 ++++ common/lib/luaLsPlugin.lua | 3 +++ 7 files changed, 13 insertions(+), 36 deletions(-) diff --git a/client/src/inputManager.lua b/client/src/inputManager.lua index c1b9e27a..7f444b91 100644 --- a/client/src/inputManager.lua +++ b/client/src/inputManager.lua @@ -5,30 +5,6 @@ local logger = require("common.lib.logger") ---@alias InputKeys ("Up" | "Down" | "Left" | "Right" | "Swap1" | "Swap2" | "TauntUp" | "TauntDown" | "Raise1" | "Raise2" | "Start" | "MenuUp" | "MenuDown" | "MenuLeft" | "MenuRight" | "MenuEsc" | "MenuSelect") ------@enum InputKeys -local inputKeys = { - Up = "Up", - Down = "Down", - Left = "Left", - Right = "Right", - Swap1 = "Swap1", - Swap2 = "Swap2", - TauntUp = "TauntUp", - TauntDown = "TauntDown", - Raise1 = "Raise1", - Raise2 = "Raise2", - Start = "Start", - MenuUp = "MenuUp", - MenuDown = "MenuDown", - MenuLeft = "MenuLeft", - MenuRight = "MenuRight", - MenuEsc = "MenuEsc", - MenuNextPage = "MenuNextPage", - MenuPrevPage = "MenuPrevPage", - MenuBack = "MenuBack", - MenuSelect = "MenuSelect", -} - ---@class KeyConfiguration ---@field isDown table ---@field isPressed table diff --git a/client/src/ui/CursorInteractable.lua b/client/src/ui/CursorInteractable.lua index cdd455cd..0929a5a8 100644 --- a/client/src/ui/CursorInteractable.lua +++ b/client/src/ui/CursorInteractable.lua @@ -26,7 +26,8 @@ local function isHovered(cursorInteractable) end end - +-- always call this on an instance, not the class +-- that is to make sure that the per-instance fields are set on the instance ---@param uiElement UiElement ---@param receiveInputs fun(cursorInteractable: CursorInteractable | UiElement, cursor: Cursor, dt: number?) ---@return UiElement | CursorInteractable diff --git a/client/src/ui/CursorNavigable.lua b/client/src/ui/CursorNavigable.lua index e14fea37..1a04d8a2 100644 --- a/client/src/ui/CursorNavigable.lua +++ b/client/src/ui/CursorNavigable.lua @@ -97,11 +97,8 @@ local function defaultReceiveInputs(cursorNavigable, cursor, dt) end end ---[[ -when adding it to a class instead of a single element:
-- have the class definition inherit CursorNavigable
-- discard the return value; it is only returned so that when doing it for an instance, LuaLS can easily infer the new union type -]] +-- always call this on an instance, not the class +-- that is to make sure that the per-instance fields are set on the instance ---@param uiElement UiElement ---@param receiveInputs fun(cursorNavigable: CursorNavigable | UiElement, cursor: Cursor, dt: number?)? ---@return UiElement | CursorNavigable diff --git a/client/src/ui/ScrollContainer.lua b/client/src/ui/ScrollContainer.lua index 71944d59..10d60aa6 100644 --- a/client/src/ui/ScrollContainer.lua +++ b/client/src/ui/ScrollContainer.lua @@ -92,12 +92,8 @@ function ScrollContainer:onTouch(x, y) local realTouchedElement = self:getTouchedChildElement(x, y) if realTouchedElement then - local sX, sY = self:getScreenPos() - logger.debug("ScrollContainer screenPos: " .. sX .. "|" .. sY) - logger.debug("touchChild coordinates: " .. realTouchedElement.x .. "|" .. realTouchedElement.y) self.touchedChild = realTouchedElement if self.touchedChild.onTouch then - logger.debug("scroll translated touch coordinates: " .. x .. "|" .. y) self.touchedChild:onTouch(x, y) end end @@ -237,6 +233,8 @@ function ScrollContainer:getScreenPos(whoIsAsking) x = x + self.x y = y + self.y + -- for children the scrolloffset needs to be applied, otherwise not; + -- as whoIsAsking is recursively calling upwards, whoIsAsking will be a direct child even if the call originates from further down in the tree if whoIsAsking and whoIsAsking.parent and whoIsAsking.parent == self then local xOffset, yOffset = getTranslatedOffset(self, 0, 0) x = x - xOffset diff --git a/client/src/ui/UIElement.lua b/client/src/ui/UIElement.lua index a205b15f..4b5aba1e 100644 --- a/client/src/ui/UIElement.lua +++ b/client/src/ui/UIElement.lua @@ -30,8 +30,6 @@ local GraphicsUtil = require("client.src.graphics.graphics_util") ---@field onRelease function? touch callback for when the mouse touching the element is released ---@field onHold function? touch callback for when a touch is held on the element for a longer duration ---@field onResized function? layout callback for when a UIElement and all of its children have been resized and positioned ----@field cursorFocus boolean? ----@field mouseFocus boolean? ---@field [any] any ---@class UiElementOptions diff --git a/common/lib/import.lua b/common/lib/import.lua index d9e86db0..4f2e0de2 100644 --- a/common/lib/import.lua +++ b/common/lib/import.lua @@ -1,4 +1,8 @@ --[[ +A library to provide a relative require. +Makes sense to use wherever we have grouped files that assuredly only ever move together. +Otherwise require is probably still better. + MIT License Copyright (c) 2023 Justin van der Leij diff --git a/common/lib/luaLsPlugin.lua b/common/lib/luaLsPlugin.lua index e02cfb07..f98d7143 100644 --- a/common/lib/luaLsPlugin.lua +++ b/common/lib/luaLsPlugin.lua @@ -1,4 +1,7 @@ --[[ +A plugin for the language server to resolve the import libraries requires +Intellisense becomes available for the returned types despite them technically being just a lua function call + MIT License Copyright (c) 2024 Elmārs Āboliņš, including code from Justin van der Leij From a345524820a6feb63a30be26c0ee12ac21186812 Mon Sep 17 00:00:00 2001 From: Endaris Date: Sat, 31 May 2025 23:04:14 +0200 Subject: [PATCH 34/49] port client parts of consts.lua to its own file --- client/src/BattleRoom.lua | 2 +- client/src/ClientMatch.lua | 2 +- client/src/ClientStack.lua | 2 +- client/src/Game.lua | 2 +- client/src/MatchParticipant.lua | 2 +- client/src/Player.lua | 2 +- client/src/PlayerStack.lua | 2 +- client/src/PuzzleLibrary.lua | 2 +- client/src/RunTimeGraph.lua | 2 +- client/src/config.lua | 2 +- client/src/consts.lua | 33 ++++++++++ client/src/globals.lua | 2 +- client/src/graphics/graphics_util.lua | 2 +- client/src/inputManager.lua | 2 +- client/src/localization.lua | 2 +- client/src/mods/Character.lua | 2 +- client/src/mods/CharacterLoader.lua | 2 +- client/src/mods/Stage.lua | 2 +- client/src/mods/StageLoader.lua | 2 +- client/src/mods/Theme.lua | 2 +- client/src/scenes/ChallengeModeRecapScene.lua | 2 +- client/src/scenes/CharacterSelect.lua | 26 +------- client/src/scenes/Game1pChallenge.lua | 2 +- client/src/scenes/GameBase.lua | 2 +- client/src/scenes/GameCatchUp.lua | 2 +- client/src/scenes/InputConfigMenu.lua | 2 +- client/src/scenes/MainMenu.lua | 2 +- client/src/scenes/ModManagement.lua | 2 +- client/src/scenes/OptionsMenu.lua | 2 +- client/src/scenes/PortraitGame.lua | 2 +- client/src/scenes/PuzzleGame.lua | 2 +- client/src/scenes/PuzzleMenu.lua | 2 +- client/src/scenes/ReplayGame.lua | 2 +- client/src/scenes/Scene.lua | 2 +- client/src/scenes/StartUp.lua | 2 +- client/src/scenes/TitleScreen.lua | 2 +- .../Transitions/BlackFadeTransition.lua | 2 +- client/src/scenes/Transitions/Transition.lua | 2 +- client/src/ui/CharacterButton.lua | 11 ++-- client/src/ui/Cursor.lua | 2 +- client/src/ui/CursorNavigable.lua | 2 +- client/src/ui/GridCursor.lua | 2 +- client/src/ui/ScrollContainer.lua | 2 + client/src/ui/StageCarousel.lua | 2 +- client/src/ui/UIElement.lua | 2 + client/src/ui/UniSizedContainer.lua | 2 +- client/tests/ModControllerTests.lua | 2 +- client/tests/SoundGroupTests.lua | 2 +- client/tests/StackGraphicsTests.lua | 2 +- client/tests/TcpClientTests.lua | 2 +- client/tests/ThemeTests.lua | 2 +- common/engine/consts.lua | 65 +++++-------------- main.lua | 2 +- 53 files changed, 109 insertions(+), 124 deletions(-) create mode 100644 client/src/consts.lua diff --git a/client/src/BattleRoom.lua b/client/src/BattleRoom.lua index ee3dd1ae..602321ad 100644 --- a/client/src/BattleRoom.lua +++ b/client/src/BattleRoom.lua @@ -11,7 +11,7 @@ local ClientMatch = require("client.src.ClientMatch") local GameBase = require("client.src.scenes.GameBase") local BlackFadeTransition = require("client.src.scenes.Transitions.BlackFadeTransition") local Easings = require("client.src.Easings") -local consts = require("common.engine.consts") +local consts = require("client.src.consts") local system = require("client.src.system") local GeneratorSource = require("common.engine.GeneratorSource") diff --git a/client/src/ClientMatch.lua b/client/src/ClientMatch.lua index 11f876c2..1755ffac 100644 --- a/client/src/ClientMatch.lua +++ b/client/src/ClientMatch.lua @@ -3,7 +3,7 @@ local class = require("common.lib.class") local logger = require("common.lib.logger") local StageLoader = require("client.src.mods.StageLoader") local ModController = require("client.src.mods.ModController") -local consts = require("common.engine.consts") +local consts = require("client.src.consts") local tableUtils = require("common.lib.tableUtils") local GameModes = require("common.data.GameModes") local ChallengeModePlayer = require("client.src.ChallengeModePlayer") diff --git a/client/src/ClientStack.lua b/client/src/ClientStack.lua index a6c21b6c..e57e79d7 100644 --- a/client/src/ClientStack.lua +++ b/client/src/ClientStack.lua @@ -1,5 +1,5 @@ local class = require("common.lib.class") -local consts = require("common.engine.consts") +local consts = require("client.src.consts") local Signal = require("common.lib.signal") local GraphicsUtil = require("client.src.graphics.graphics_util") diff --git a/client/src/Game.lua b/client/src/Game.lua index cce08590..af17c155 100644 --- a/client/src/Game.lua +++ b/client/src/Game.lua @@ -9,7 +9,7 @@ require("client.src.mods.Theme") -- The main game object for tracking everything in Panel Attack. -- Not to be confused with "Match" which is the current battle / instance of the game. -local consts = require("common.engine.consts") +local consts = require("client.src.consts") local GraphicsUtil = require("client.src.graphics.graphics_util") local class = require("common.lib.class") local logger = require("common.lib.logger") diff --git a/client/src/MatchParticipant.lua b/client/src/MatchParticipant.lua index 80819269..08b7f105 100644 --- a/client/src/MatchParticipant.lua +++ b/client/src/MatchParticipant.lua @@ -1,5 +1,5 @@ local class = require("common.lib.class") -local consts = require("common.engine.consts") +local consts = require("client.src.consts") local Signal = require("common.lib.signal") local logger = require("common.lib.logger") local CharacterLoader = require("client.src.mods.CharacterLoader") diff --git a/client/src/Player.lua b/client/src/Player.lua index 40d8ad91..a1aad96f 100644 --- a/client/src/Player.lua +++ b/client/src/Player.lua @@ -3,7 +3,7 @@ local GameModes = require("common.data.GameModes") local LevelPresets = require("common.data.LevelPresets") local input = require("client.src.inputManager") local MatchParticipant = require("client.src.MatchParticipant") -local consts = require("common.engine.consts") +local consts = require("client.src.consts") local CharacterLoader = require("client.src.mods.CharacterLoader") local PlayerStack = require("client.src.PlayerStack") require("client.src.network.PlayerStack") diff --git a/client/src/PlayerStack.lua b/client/src/PlayerStack.lua index e0a85a08..a6f56515 100644 --- a/client/src/PlayerStack.lua +++ b/client/src/PlayerStack.lua @@ -3,7 +3,7 @@ local class = require("common.lib.class") require("common.lib.util") local GraphicsUtil = require("client.src.graphics.graphics_util") local TouchDataEncoding = require("common.data.TouchDataEncoding") -local consts = require("common.engine.consts") +local consts = require("client.src.consts") local prof = require("common.lib.zoneProfiler") local EngineStack = require("common.engine.Stack") require("common.engine.checkMatches") diff --git a/client/src/PuzzleLibrary.lua b/client/src/PuzzleLibrary.lua index 7f5238a7..291e27b2 100644 --- a/client/src/PuzzleLibrary.lua +++ b/client/src/PuzzleLibrary.lua @@ -1,5 +1,5 @@ local class = require("common.lib.class") -local consts = require("common.engine.consts") +local consts = require("client.src.consts") local FileUtils = require("client.src.FileUtils") local logger = require("common.lib.logger") local Puzzle = require("common.engine.Puzzle") diff --git a/client/src/RunTimeGraph.lua b/client/src/RunTimeGraph.lua index bcb6c1fa..285a54c0 100644 --- a/client/src/RunTimeGraph.lua +++ b/client/src/RunTimeGraph.lua @@ -1,5 +1,5 @@ local BarGraph = require("client.lib.BarGraph") -local consts = require("common.engine.consts") +local consts = require("client.src.consts") local class = require("common.lib.class") local RunTimeGraph = class(function(self) diff --git a/client/src/config.lua b/client/src/config.lua index 4c81d6a6..81261d5e 100644 --- a/client/src/config.lua +++ b/client/src/config.lua @@ -1,7 +1,7 @@ json = require("common.lib.dkjson") local util = require("common.lib.util") local fileUtils = require("client.src.FileUtils") -local consts = require("common.engine.consts") +local consts = require("client.src.consts") require("client.src.globals") -- Default configuration values diff --git a/client/src/consts.lua b/client/src/consts.lua new file mode 100644 index 00000000..e3697ee9 --- /dev/null +++ b/client/src/consts.lua @@ -0,0 +1,33 @@ +local clientConstants = { + CANVAS_WIDTH = 1280, + CANVAS_HEIGHT = 720, + DEFAULT_THEME_DIR = "Panel Attack", + RANDOM_CHARACTER_SPECIAL_VALUE = "__RandomCharacter", + RANDOM_STAGE_SPECIAL_VALUE = "__RandomStage", + MOUSE_POINTER_TIMEOUT = 1.5, --seconds + KEY_NAMES = {"Up", "Down", "Left", "Right", "Swap1", "Swap2", "TauntUp", "TauntDown", "Raise1", "Raise2", "Start"}, + FRAME_RATE = 1 / 60, + KEY_DELAY = .25, + KEY_REPEAT_PERIOD = .05, + MENU_PADDING = 10 +} + +clientConstants.PUZZLES_SAVE_DIRECTORY = "puzzles" + +clientConstants.SERVER_SAVE_DIRECTORY = "servers/" +clientConstants.LEGACY_SERVER_LOCATION = "18.188.43.50" +clientConstants.SERVER_LOCATION = "panelattack.com" +clientConstants.DEFAULT_THEME_DIRECTORY = "Panel Attack Modern" + +clientConstants.ATTACK_TYPE = { combo=0, chain=1, shock=2 } + +local engineConstants = require("common.engine.consts") + +for key, value in pairs(engineConstants) do + if clientConstants[key] then + error("key collision between client and engine constants for key " .. key) + end + clientConstants[key] = value +end + +return clientConstants \ No newline at end of file diff --git a/client/src/globals.lua b/client/src/globals.lua index 45cf48e1..171ef44d 100644 --- a/client/src/globals.lua +++ b/client/src/globals.lua @@ -1,4 +1,4 @@ -local consts = require("common.engine.consts") +local consts = require("client.src.consts") score_mode = consts.SCOREMODE_TA GARBAGE_TRANSIT_TIME = 45 -- the amount of time the garbage attack animation plays before getting to the telegraph diff --git a/client/src/graphics/graphics_util.lua b/client/src/graphics/graphics_util.lua index 5c625786..ed7b75d2 100644 --- a/client/src/graphics/graphics_util.lua +++ b/client/src/graphics/graphics_util.lua @@ -1,4 +1,4 @@ -local consts = require("common.engine.consts") +local consts = require("client.src.consts") local logger = require("common.lib.logger") local FileUtils = require("client.src.FileUtils") local util = require("common.lib.util") diff --git a/client/src/inputManager.lua b/client/src/inputManager.lua index 7f444b91..4a2fb053 100644 --- a/client/src/inputManager.lua +++ b/client/src/inputManager.lua @@ -1,6 +1,6 @@ local tableUtils = require("common.lib.tableUtils") local joystickManager = require("common.lib.joystickManager") -local consts = require("common.engine.consts") +local consts = require("client.src.consts") local logger = require("common.lib.logger") ---@alias InputKeys ("Up" | "Down" | "Left" | "Right" | "Swap1" | "Swap2" | "TauntUp" | "TauntDown" | "Raise1" | "Raise2" | "Start" | "MenuUp" | "MenuDown" | "MenuLeft" | "MenuRight" | "MenuEsc" | "MenuSelect") diff --git a/client/src/localization.lua b/client/src/localization.lua index 3fecfe5f..fb11b5ba 100644 --- a/client/src/localization.lua +++ b/client/src/localization.lua @@ -1,6 +1,6 @@ -- TODO rename local FILENAME = "client/assets/localization.csv" -local consts = require("common.engine.consts") +local consts = require("client.src.consts") local GraphicsUtil = require("client.src.graphics.graphics_util") local class = require("common.lib.class") diff --git a/client/src/mods/Character.lua b/client/src/mods/Character.lua index 9d603416..8c37f7dc 100644 --- a/client/src/mods/Character.lua +++ b/client/src/mods/Character.lua @@ -7,7 +7,7 @@ local class = require("common.lib.class") local logger = require("common.lib.logger") local tableUtils = require("common.lib.tableUtils") local fileUtils = require("client.src.FileUtils") -local consts = require("common.engine.consts") +local consts = require("client.src.consts") local GraphicsUtil = require("client.src.graphics.graphics_util") local StageTrack = require("client.src.music.StageTrack") local DynamicStageTrack = require("client.src.music.DynamicStageTrack") diff --git a/client/src/mods/CharacterLoader.lua b/client/src/mods/CharacterLoader.lua index 3bfab808..a2635884 100644 --- a/client/src/mods/CharacterLoader.lua +++ b/client/src/mods/CharacterLoader.lua @@ -1,7 +1,7 @@ local Character = require("client.src.mods.Character") local logger = require("common.lib.logger") local tableUtils = require("common.lib.tableUtils") -local consts = require("common.engine.consts") +local consts = require("client.src.consts") local ModLoader = require("client.src.mods.ModLoader") local CharacterLoader = {} diff --git a/client/src/mods/Stage.lua b/client/src/mods/Stage.lua index 9de4a318..eab08869 100644 --- a/client/src/mods/Stage.lua +++ b/client/src/mods/Stage.lua @@ -1,7 +1,7 @@ local logger = require("common.lib.logger") local tableUtils = require("common.lib.tableUtils") local fileUtils = require("client.src.FileUtils") -local consts = require("common.engine.consts") +local consts = require("client.src.consts") local GraphicsUtil = require("client.src.graphics.graphics_util") local StageTrack = require("client.src.music.StageTrack") local DynamicStageTrack = require("client.src.music.DynamicStageTrack") diff --git a/client/src/mods/StageLoader.lua b/client/src/mods/StageLoader.lua index b04b65e1..26035160 100644 --- a/client/src/mods/StageLoader.lua +++ b/client/src/mods/StageLoader.lua @@ -1,5 +1,5 @@ local Stage = require("client.src.mods.Stage") -local consts = require("common.engine.consts") +local consts = require("client.src.consts") local tableUtils = require("common.lib.tableUtils") local logger = require("common.lib.logger") local ModLoader = require("client.src.mods.ModLoader") diff --git a/client/src/mods/Theme.lua b/client/src/mods/Theme.lua index dfc5fce1..ecfcf85e 100644 --- a/client/src/mods/Theme.lua +++ b/client/src/mods/Theme.lua @@ -1,4 +1,4 @@ -local consts = require("common.engine.consts") +local consts = require("client.src.consts") local class = require("common.lib.class") local logger = require("common.lib.logger") local fileUtils = require("client.src.FileUtils") diff --git a/client/src/scenes/ChallengeModeRecapScene.lua b/client/src/scenes/ChallengeModeRecapScene.lua index 9a60212a..b5e72259 100644 --- a/client/src/scenes/ChallengeModeRecapScene.lua +++ b/client/src/scenes/ChallengeModeRecapScene.lua @@ -1,4 +1,4 @@ -local consts = require("common.engine.consts") +local consts = require("client.src.consts") local GraphicsUtil = require("client.src.graphics.graphics_util") local Scene = require("client.src.scenes.Scene") local class = require("common.lib.class") diff --git a/client/src/scenes/CharacterSelect.lua b/client/src/scenes/CharacterSelect.lua index 643ff01b..e3c0cb14 100644 --- a/client/src/scenes/CharacterSelect.lua +++ b/client/src/scenes/CharacterSelect.lua @@ -1,4 +1,4 @@ -local consts = require("common.engine.consts") +local consts = require("client.src.consts") local input = require("client.src.inputManager") local class = require("common.lib.class") local tableUtils = require("common.lib.tableUtils") @@ -322,15 +322,6 @@ function CharacterSelect:getCharacterButtons() end GAME.theme:playValidationSfx() if character then - if character:canSuperSelect() and holdTime > consts.SUPER_SELECTION_START + consts.SUPER_SELECTION_DURATION then - -- super select - if character.panels and panels[character.panels] then - player:setPanels(character.panels) - end - if character.stage and stages[character.stage] then - player:setStage(character.stage) - end - end character:playSelectionSfx() end player:setCharacter(selfElement.characterId) @@ -348,20 +339,7 @@ function CharacterSelect:getCharacterButtons() end local function updateSuperSelectShader(image, timer) - if timer > consts.SUPER_SELECTION_START then - if image.isVisible == false then - image:setVisibility(true) - end - local progress = (timer - consts.SUPER_SELECTION_START) / consts.SUPER_SELECTION_DURATION - if progress <= 1 then - image.shader:send("percent", progress) - end - else - if image.isVisible then - image:setVisibility(false) - end - image.shader:send("percent", 0) - end + end ---@param characterButton Button diff --git a/client/src/scenes/Game1pChallenge.lua b/client/src/scenes/Game1pChallenge.lua index 4cdaa480..4b83ce26 100644 --- a/client/src/scenes/Game1pChallenge.lua +++ b/client/src/scenes/Game1pChallenge.lua @@ -1,6 +1,6 @@ local GameBase = require("client.src.scenes.GameBase") local class = require("common.lib.class") -local consts = require("common.engine.consts") +local consts = require("client.src.consts") local GraphicsUtil = require("client.src.graphics.graphics_util") local ChallengeModeTimeSplitsUIElement = require("client.src.graphics.ChallengeModeTimeSplitsUIElement") local ChallengeModeRecapScene = require("client.src.scenes.ChallengeModeRecapScene") diff --git a/client/src/scenes/GameBase.lua b/client/src/scenes/GameBase.lua index 25b89a73..aa34865b 100644 --- a/client/src/scenes/GameBase.lua +++ b/client/src/scenes/GameBase.lua @@ -5,7 +5,7 @@ local logger = require("common.lib.logger") local analytics = require("client.src.analytics") local input = require("client.src.inputManager") local tableUtils = require("common.lib.tableUtils") -local consts = require("common.engine.consts") +local consts = require("client.src.consts") local StageLoader = require("client.src.mods.StageLoader") local ModController = require("client.src.mods.ModController") local SoundController = require("client.src.music.SoundController") diff --git a/client/src/scenes/GameCatchUp.lua b/client/src/scenes/GameCatchUp.lua index 0c679ecc..9d3aab46 100644 --- a/client/src/scenes/GameCatchUp.lua +++ b/client/src/scenes/GameCatchUp.lua @@ -1,7 +1,7 @@ local class = require("common.lib.class") local Scene = require("client.src.scenes.Scene") local GraphicsUtil = require("client.src.graphics.graphics_util") -local consts = require("common.engine.consts") +local consts = require("client.src.consts") local ModLoader = require("client.src.mods.ModLoader") local SoundController = require("client.src.music.SoundController") local logger = require("common.lib.logger") diff --git a/client/src/scenes/InputConfigMenu.lua b/client/src/scenes/InputConfigMenu.lua index f58d60cb..effd0e94 100644 --- a/client/src/scenes/InputConfigMenu.lua +++ b/client/src/scenes/InputConfigMenu.lua @@ -1,7 +1,7 @@ local Scene = require("client.src.scenes.Scene") local tableUtils = require("common.lib.tableUtils") local ui = require("client.src.ui") -local consts = require("common.engine.consts") +local consts = require("client.src.consts") local input = require("client.src.inputManager") local joystickManager = require("common.lib.joystickManager") local util = require("common.lib.util") diff --git a/client/src/scenes/MainMenu.lua b/client/src/scenes/MainMenu.lua index 7ca71947..d4a5834d 100644 --- a/client/src/scenes/MainMenu.lua +++ b/client/src/scenes/MainMenu.lua @@ -1,5 +1,5 @@ local Scene = require("client.src.scenes.Scene") -local consts = require("common.engine.consts") +local consts = require("client.src.consts") local ui = require("client.src.ui") local GraphicsUtil = require("client.src.graphics.graphics_util") local class = require("common.lib.class") diff --git a/client/src/scenes/ModManagement.lua b/client/src/scenes/ModManagement.lua index 9481501a..13460df2 100644 --- a/client/src/scenes/ModManagement.lua +++ b/client/src/scenes/ModManagement.lua @@ -3,7 +3,7 @@ local class = require("common.lib.class") local ui = require("client.src.ui") local inputs = require("client.src.inputManager") local tableUtils = require("common.lib.tableUtils") -local consts = require("common.engine.consts") +local consts = require("client.src.consts") local CharacterLoader = require("client.src.mods.CharacterLoader") local SoundController = require("client.src.music.SoundController") local system = require("client.src.system") diff --git a/client/src/scenes/OptionsMenu.lua b/client/src/scenes/OptionsMenu.lua index bb8aceb0..87cbae11 100644 --- a/client/src/scenes/OptionsMenu.lua +++ b/client/src/scenes/OptionsMenu.lua @@ -2,7 +2,7 @@ local Scene = require("client.src.scenes.Scene") local ui = require("client.src.ui") local inputManager = require("client.src.inputManager") local save = require("client.src.save") -local consts = require("common.engine.consts") +local consts = require("client.src.consts") local fileUtils = require("client.src.FileUtils") local analytics = require("client.src.analytics") local class = require("common.lib.class") diff --git a/client/src/scenes/PortraitGame.lua b/client/src/scenes/PortraitGame.lua index 97d7d8b3..9d03e0b7 100644 --- a/client/src/scenes/PortraitGame.lua +++ b/client/src/scenes/PortraitGame.lua @@ -1,6 +1,6 @@ local GameBase = require("client.src.scenes.GameBase") local class = require("common.lib.class") -local consts = require("common.engine.consts") +local consts = require("client.src.consts") local Telegraph = require("client.src.graphics.Telegraph") local GraphicsUtil = require("client.src.graphics.graphics_util") local ui = require("client.src.ui") diff --git a/client/src/scenes/PuzzleGame.lua b/client/src/scenes/PuzzleGame.lua index 405e64cf..6ec6490e 100644 --- a/client/src/scenes/PuzzleGame.lua +++ b/client/src/scenes/PuzzleGame.lua @@ -4,7 +4,7 @@ local tableUtils = require("common.lib.tableUtils") local MessageTransition = require("client.src.scenes.Transitions.MessageTransition") local GraphicsUtil = require("client.src.graphics.graphics_util") local InputCompression = require("common.data.InputCompression") -local consts = require("common.engine.consts") +local consts = require("client.src.consts") local FileUtils = require("client.src.FileUtils") -- Scene for a puzzle mode instance of the game diff --git a/client/src/scenes/PuzzleMenu.lua b/client/src/scenes/PuzzleMenu.lua index 640ce791..3e109de2 100644 --- a/client/src/scenes/PuzzleMenu.lua +++ b/client/src/scenes/PuzzleMenu.lua @@ -1,5 +1,5 @@ local Scene = require("client.src.scenes.Scene") -local consts = require("common.engine.consts") +local consts = require("client.src.consts") local logger = require("common.lib.logger") local ui = require("client.src.ui") local PuzzleLibrary = require("client.src.PuzzleLibrary") diff --git a/client/src/scenes/ReplayGame.lua b/client/src/scenes/ReplayGame.lua index 861a6c8d..c9769857 100644 --- a/client/src/scenes/ReplayGame.lua +++ b/client/src/scenes/ReplayGame.lua @@ -1,6 +1,6 @@ local GameBase = require("client.src.scenes.GameBase") local input = require("client.src.inputManager") -local consts = require("common.engine.consts") +local consts = require("client.src.consts") local util = require("common.lib.util") local class = require("common.lib.class") local GraphicsUtil = require("client.src.graphics.graphics_util") diff --git a/client/src/scenes/Scene.lua b/client/src/scenes/Scene.lua index 303b187b..a5000d59 100644 --- a/client/src/scenes/Scene.lua +++ b/client/src/scenes/Scene.lua @@ -1,6 +1,6 @@ local class = require("common.lib.class") local ui = require("client.src.ui") -local consts = require("common.engine.consts") +local consts = require("client.src.consts") local GraphicsUtil = require("client.src.graphics.graphics_util") local tableUtils = require("common.lib.tableUtils") local SoundController = require("client.src.music.SoundController") diff --git a/client/src/scenes/StartUp.lua b/client/src/scenes/StartUp.lua index c133d56b..351784cd 100644 --- a/client/src/scenes/StartUp.lua +++ b/client/src/scenes/StartUp.lua @@ -1,6 +1,6 @@ local class = require("common.lib.class") local Scene = require("client.src.scenes.Scene") -local consts = require("common.engine.consts") +local consts = require("client.src.consts") local GraphicsUtil = require("client.src.graphics.graphics_util") local logger = require("common.lib.logger") local fileUtils = require("client.src.FileUtils") diff --git a/client/src/scenes/TitleScreen.lua b/client/src/scenes/TitleScreen.lua index 0c8521b0..91b24821 100644 --- a/client/src/scenes/TitleScreen.lua +++ b/client/src/scenes/TitleScreen.lua @@ -1,5 +1,5 @@ local Scene = require("client.src.scenes.Scene") -local consts = require("common.engine.consts") +local consts = require("client.src.consts") local input = require("client.src.inputManager") local tableUtils = require("common.lib.tableUtils") local class = require("common.lib.class") diff --git a/client/src/scenes/Transitions/BlackFadeTransition.lua b/client/src/scenes/Transitions/BlackFadeTransition.lua index 9250101e..4332fc75 100644 --- a/client/src/scenes/Transitions/BlackFadeTransition.lua +++ b/client/src/scenes/Transitions/BlackFadeTransition.lua @@ -1,6 +1,6 @@ local class = require("common.lib.class") local Transition = require("client.src.scenes.Transitions.Transition") -local consts = require("common.engine.consts") +local consts = require("client.src.consts") local GraphicsUtil = require("client.src.graphics.graphics_util") local BlackFadeTransition = class(function(transition, startTime, duration, easing) diff --git a/client/src/scenes/Transitions/Transition.lua b/client/src/scenes/Transitions/Transition.lua index 6c2b282d..c787c154 100644 --- a/client/src/scenes/Transitions/Transition.lua +++ b/client/src/scenes/Transitions/Transition.lua @@ -1,6 +1,6 @@ local class = require("common.lib.class") local UiElement = require("client.src.ui.UIElement") -local consts = require("common.engine.consts") +local consts = require("client.src.consts") -- A transition, or more specifically a scene transition represents an object that handles going from one scene to the next -- For the duration of handover, the transition is responsible for handling both update and draw calls on the scenes diff --git a/client/src/ui/CharacterButton.lua b/client/src/ui/CharacterButton.lua index c61ed048..9bb6e905 100644 --- a/client/src/ui/CharacterButton.lua +++ b/client/src/ui/CharacterButton.lua @@ -1,9 +1,12 @@ local import = require("common.lib.import") local class = require("common.lib.class") local Button = import("./Button") -local consts = require("common.engine.consts") +local consts = require("client.src.consts") local GraphicsUtil = require("client.src.graphics.graphics_util") +local SUPER_SELECTION_DURATION = 0.5 -- time held in seconds at which super select actually happens +local SUPER_SELECTION_START = 0.1 -- time held in seconds at which super select is considered started + ---@class CharacterButtonOptions : ButtonOptions ---@field character Character @@ -66,11 +69,11 @@ CharacterButton.padding = 2 CharacterButton.superSelectShader = love.graphics.newShader(super_select_pixelcode) function CharacterButton:updateSuperSelectShader(timer) - if timer > consts.SUPER_SELECTION_START then + if timer > SUPER_SELECTION_START then if self.superSelectVisible == false then self.superSelectVisible = true end - local progress = (timer - consts.SUPER_SELECTION_START) / consts.SUPER_SELECTION_DURATION + local progress = (timer - SUPER_SELECTION_START) / SUPER_SELECTION_DURATION if progress <= 1 then CharacterButton.superSelectShader:send("percent", progress) end @@ -95,7 +98,7 @@ function CharacterButton:action(inputSource, holdTime) else local player = inputSource.player GAME.theme:playValidationSfx() - if character:canSuperSelect() and holdTime > consts.SUPER_SELECTION_START + consts.SUPER_SELECTION_DURATION then + if character:canSuperSelect() and holdTime > SUPER_SELECTION_START + SUPER_SELECTION_DURATION then -- super select if character.panels and panels[character.panels] then player:setPanels(character.panels) diff --git a/client/src/ui/Cursor.lua b/client/src/ui/Cursor.lua index 51b44465..642b15e4 100644 --- a/client/src/ui/Cursor.lua +++ b/client/src/ui/Cursor.lua @@ -1,5 +1,5 @@ local class = require("common.lib.class") -local consts = require("common.engine.consts") +local consts = require("client.src.consts") local GraphicsUtil = require("client.src.graphics.graphics_util") local input = require("client.src.inputManager") diff --git a/client/src/ui/CursorNavigable.lua b/client/src/ui/CursorNavigable.lua index 1a04d8a2..4a541c0c 100644 --- a/client/src/ui/CursorNavigable.lua +++ b/client/src/ui/CursorNavigable.lua @@ -1,4 +1,4 @@ -local consts = require("common.engine.consts") +local consts = require("client.src.consts") local tableUtils = require("common.lib.tableUtils") local import = require("common.lib.import") local addCursorInteractionInterface = import("./CursorInteractable") diff --git a/client/src/ui/GridCursor.lua b/client/src/ui/GridCursor.lua index 1de63198..b194414d 100644 --- a/client/src/ui/GridCursor.lua +++ b/client/src/ui/GridCursor.lua @@ -2,7 +2,7 @@ local import = require("common.lib.import") local UiElement = import("./UIElement") local class = require("common.lib.class") local directsFocus = import("./FocusDirector") -local consts = require("common.engine.consts") +local consts = require("client.src.consts") local GraphicsUtil = require("client.src.graphics.graphics_util") local StaticLayout = import("./Layouts.StaticLayout") diff --git a/client/src/ui/ScrollContainer.lua b/client/src/ui/ScrollContainer.lua index 10d60aa6..b891d4ce 100644 --- a/client/src/ui/ScrollContainer.lua +++ b/client/src/ui/ScrollContainer.lua @@ -224,6 +224,8 @@ function ScrollContainer:getPreferredWidth() end ---@param whoIsAsking table? +---@return integer x +---@return integer y function ScrollContainer:getScreenPos(whoIsAsking) local x, y = 0, 0 if self.parent then diff --git a/client/src/ui/StageCarousel.lua b/client/src/ui/StageCarousel.lua index dcee7c52..0468e548 100644 --- a/client/src/ui/StageCarousel.lua +++ b/client/src/ui/StageCarousel.lua @@ -4,7 +4,7 @@ local StackPanel = import("./StackPanel") local Label = import("./Label") local ImageContainer = import("./ImageContainer") local class = require("common.lib.class") -local consts = require("common.engine.consts") +local consts = require("client.src.consts") local Stage = require("client.src.mods.Stage") local StageCarousel = class(function(carousel, options) diff --git a/client/src/ui/UIElement.lua b/client/src/ui/UIElement.lua index 4b5aba1e..3168d11d 100644 --- a/client/src/ui/UIElement.lua +++ b/client/src/ui/UIElement.lua @@ -174,6 +174,8 @@ function UIElement:onDetach() end ---@param whoIsAsking table? +---@return integer x +---@return integer y function UIElement:getScreenPos(whoIsAsking) local x, y = 0, 0 if self.parent then diff --git a/client/src/ui/UniSizedContainer.lua b/client/src/ui/UniSizedContainer.lua index 6ab00398..13250c1d 100644 --- a/client/src/ui/UniSizedContainer.lua +++ b/client/src/ui/UniSizedContainer.lua @@ -3,7 +3,7 @@ local UiElement = import("./UIElement") local class = require("common.lib.class") local HorizontalWrapLayout = import("./Layouts.HorizontalWrapLayout") local Focusable = import("./Focusable") -local consts = require("common.engine.consts") +local consts = require("client.src.consts") local GraphicsUtil = require("client.src.graphics.graphics_util") local input = require("client.src.inputManager") local addCursorNavigationInterface = import("./CursorNavigable") diff --git a/client/tests/ModControllerTests.lua b/client/tests/ModControllerTests.lua index 7a4c8470..8e2814c1 100644 --- a/client/tests/ModControllerTests.lua +++ b/client/tests/ModControllerTests.lua @@ -3,7 +3,7 @@ local ModController = require("client.src.mods.ModController") local Player = require("client.src.Player") local StageLoader = require("client.src.mods.StageLoader") local CharacterLoader = require("client.src.mods.CharacterLoader") -local consts = require("common.engine.consts") +local consts = require("client.src.consts") local Stage = require("client.src.mods.Stage") -- caveat: this test is not as effective if you have no character bundles installed diff --git a/client/tests/SoundGroupTests.lua b/client/tests/SoundGroupTests.lua index a0a52a27..7a45df90 100644 --- a/client/tests/SoundGroupTests.lua +++ b/client/tests/SoundGroupTests.lua @@ -1,4 +1,4 @@ -local consts = require("common.engine.consts") +local consts = require("client.src.consts") local logger = require("common.lib.logger") local FileGroup = require("client.src.FileGroup") local fileUtils = require("client.src.FileUtils") diff --git a/client/tests/StackGraphicsTests.lua b/client/tests/StackGraphicsTests.lua index c4f91a93..66930475 100644 --- a/client/tests/StackGraphicsTests.lua +++ b/client/tests/StackGraphicsTests.lua @@ -1,7 +1,7 @@ local ClientMatch = require("client.src.ClientMatch") local GameModes = require("common.data.GameModes") local Player = require("client.src.Player") -local consts = require("common.engine.consts") +local consts = require("client.src.consts") local GeneratorSource = require("common.engine.GeneratorSource") local logger = require("common.lib.logger") local LevelPresets = require("common.data.LevelPresets") diff --git a/client/tests/TcpClientTests.lua b/client/tests/TcpClientTests.lua index e4634926..9c7ce463 100644 --- a/client/tests/TcpClientTests.lua +++ b/client/tests/TcpClientTests.lua @@ -1,5 +1,5 @@ local TcpClient = require("client.src.network.TcpClient") -local consts = require("common.engine.consts") +local consts = require("client.src.consts") local ClientMessages = require("common.network.ClientProtocol") local NetworkProtocol = require("common.network.NetworkProtocol") diff --git a/client/tests/ThemeTests.lua b/client/tests/ThemeTests.lua index 05aaeb18..8ab0362a 100644 --- a/client/tests/ThemeTests.lua +++ b/client/tests/ThemeTests.lua @@ -1,4 +1,4 @@ -local consts = require("common.engine.consts") +local consts = require("client.src.consts") local fileUtils = require("client.src.FileUtils") local Theme = require("client.src.mods.Theme") diff --git a/common/engine/consts.lua b/common/engine/consts.lua index 7f371a0c..ca157a8b 100644 --- a/common/engine/consts.lua +++ b/common/engine/consts.lua @@ -1,59 +1,28 @@ ---[[ - TODO: - consts is currently kind of the "all over the place" collection - it should get split into - 1. constants decisively important for the engine - 2. constants decisively important for the client -]] -require("common.lib.util") local tableUtils = require("common.lib.tableUtils") -local consts = { - CANVAS_WIDTH = 1280, - CANVAS_HEIGHT = 720, - DEFAULT_THEME_DIR = "Panel Attack", - RANDOM_CHARACTER_SPECIAL_VALUE = "__RandomCharacter", - RANDOM_STAGE_SPECIAL_VALUE = "__RandomStage", - MOUSE_POINTER_TIMEOUT = 1.5, --seconds - KEY_NAMES = {"Up", "Down", "Left", "Right", "Swap1", "Swap2", "TauntUp", "TauntDown", "Raise1", "Raise2", "Start"}, - FRAME_RATE = 1 / 60, - KEY_DELAY = .25, - KEY_REPEAT_PERIOD = .05, - MENU_PADDING = 10 -} +local engineConstants = {} -- The values in this file are constants (except in this file perhaps) and are expected never to change during the game, not to be confused with globals! -consts.ENGINE_VERSIONS = {} -consts.ENGINE_VERSIONS.PRE_TELEGRAPH = "045" -consts.ENGINE_VERSIONS.TELEGRAPH_COMPATIBLE = "046" -consts.ENGINE_VERSIONS.TOUCH_COMPATIBLE = "047" -consts.ENGINE_VERSIONS.LEVELDATA = "048" -consts.ENGINE_VERSIONS.WIGGLE_PUNISH = "049" +engineConstants.ENGINE_VERSIONS = {} +engineConstants.ENGINE_VERSIONS.PRE_TELEGRAPH = "045" +engineConstants.ENGINE_VERSIONS.TELEGRAPH_COMPATIBLE = "046" +engineConstants.ENGINE_VERSIONS.TOUCH_COMPATIBLE = "047" +engineConstants.ENGINE_VERSIONS.LEVELDATA = "048" +engineConstants.ENGINE_VERSIONS.WIGGLE_PUNISH = "049" -consts.ENGINE_VERSION = consts.ENGINE_VERSIONS.WIGGLE_PUNISH -- The current engine version -consts.VERSION_MIN_VIEW = consts.ENGINE_VERSIONS.LEVELDATA -- The lowest version number that can be watched +engineConstants.ENGINE_VERSION = engineConstants.ENGINE_VERSIONS.WIGGLE_PUNISH -- The current engine version +engineConstants.VERSION_MIN_VIEW = engineConstants.ENGINE_VERSIONS.LEVELDATA -- The lowest version number that can be watched -consts.COUNTDOWN_CURSOR_SPEED = 4 --one move every this many frames -consts.COUNTDOWN_START = 8 -consts.COUNTDOWN_LENGTH = 180 --3 seconds at 60 fps +engineConstants.COUNTDOWN_CURSOR_SPEED = 4 --one move every this many frames +engineConstants.COUNTDOWN_START = 8 +engineConstants.COUNTDOWN_LENGTH = 180 --3 seconds at 60 fps -consts.PUZZLES_SAVE_DIRECTORY = "puzzles" - -consts.SERVER_SAVE_DIRECTORY = "servers/" -consts.LEGACY_SERVER_LOCATION = "18.188.43.50" -consts.SERVER_LOCATION = "panelattack.com" - -consts.SUPER_SELECTION_DURATION = 0.5 -- seconds -consts.SUPER_SELECTION_START = 0.1 -- time held at which super enable is considered started - -consts.DEFAULT_THEME_DIRECTORY = "Panel Attack Modern" - -consts.SCOREMODE_TA = 1 -consts.SCOREMODE_PDP64 = 2 -- currently not used +engineConstants.SCOREMODE_TA = 1 +engineConstants.SCOREMODE_PDP64 = 2 -- currently not used -- Yes, 2 is slower than 1 and 50..99 are the same. -consts.SPEED_TO_RISE_TIME = tableUtils.map( +engineConstants.SPEED_TO_RISE_TIME = tableUtils.map( {942, 983, 838, 790, 755, 695, 649, 604, 570, 515, 474, 444, 394, 370, 347, 325, 306, 289, 271, 256, 240, 227, 213, 201, 189, 178, 169, 158, 148, 138, @@ -71,6 +40,4 @@ consts.SPEED_TO_RISE_TIME = tableUtils.map( -- on stage 1, the increases occur at increments of: -- 20, 15, 15, 15, 10, 10, 10 -consts.ATTACK_TYPE = { combo=0, chain=1, shock=2 } - -return consts \ No newline at end of file +return engineConstants \ No newline at end of file diff --git a/main.lua b/main.lua index eaa187ff..aed371e7 100644 --- a/main.lua +++ b/main.lua @@ -11,7 +11,7 @@ local GraphicsUtil = require("client.src.graphics.graphics_util") local prof = require("common.lib.zoneProfiler") local ReplayV3 = require("common.data.ReplayV3") require("common.lib.util") -local consts = require("common.engine.consts") +local consts = require("client.src.consts") local system = require("client.src.system") local Game = require("client.src.Game") From 80c2730de3e0aef037564bb0ff630386594b3cbe Mon Sep 17 00:00:00 2001 From: Endaris Date: Sun, 1 Jun 2025 00:38:24 +0200 Subject: [PATCH 35/49] wrap characterSelect buttons in scrollcontainer and fix a bug that caused children to get repositioned twice, causing failure for wrappables inside of scrollContainers --- client/src/scenes/DesignHelper.lua | 24 ++++++++++++------- .../src/ui/Layouts/HorizontalScrollLayout.lua | 6 ----- .../src/ui/Layouts/VerticalScrollLayout.lua | 6 ----- 3 files changed, 16 insertions(+), 20 deletions(-) diff --git a/client/src/scenes/DesignHelper.lua b/client/src/scenes/DesignHelper.lua index 1d6aa95f..c81fff48 100644 --- a/client/src/scenes/DesignHelper.lua +++ b/client/src/scenes/DesignHelper.lua @@ -36,28 +36,35 @@ local function getSelectorTemplate(id) end local function createCharacterSelect(scene) - local characterSelect = ui.UniSizedContainer({ + local scrollContainer = ui.ScrollContainer({ + scrollOrientation = "vertical", + hFill = true, + vFill = true, + maxHeight = 800, + --maxWidth = 1000, + }) + + scene.characterSelect = ui.UniSizedContainer({ childrenWidth = 84, childrenHeight = 84, childGap = 16, - onFocus = function (self) - scene.subSelection:addChild(self) - end, onYield = function (self) - self:detach() + scene.characterSelectContainer:detach() end }) for i, characterId in ipairs(visibleCharacters) do local button = ui.CharacterButton({character = characters[characterId]}) - characterSelect:addChild(button) + scene.characterSelect:addChild(button) end - return characterSelect + scrollContainer:addChild(scene.characterSelect) + + return scrollContainer end function DesignHelper:load() - self.characterSelect = createCharacterSelect(self) + self.characterSelectContainer = createCharacterSelect(self) self.uiRoot.layout = ui.Layouts.VerticalFlexLayout self.uiRoot.childGap = 8 ui.CursorNavigable(self.uiRoot) @@ -121,6 +128,7 @@ function DesignHelper:load() local characterSelectionSelector, characterButton = getSelectorTemplate("character") characterButton:addChild(characterImage) characterButton.onClick = function() + self.subSelection:addChild(self.characterSelectContainer) self.cursor:deepenFocus(self.characterSelect) end diff --git a/client/src/ui/Layouts/HorizontalScrollLayout.lua b/client/src/ui/Layouts/HorizontalScrollLayout.lua index 0427bf57..b74b89b7 100644 --- a/client/src/ui/Layouts/HorizontalScrollLayout.lua +++ b/client/src/ui/Layouts/HorizontalScrollLayout.lua @@ -36,12 +36,6 @@ function HorizontalScrollLayout.positionChildren(uiElement) x = x + uiElement.childGap + child.width end end - - for _, child in ipairs(uiElement.children) do - if child.isVisible then - child.layout.positionChildren(child) - end - end end return HorizontalScrollLayout \ No newline at end of file diff --git a/client/src/ui/Layouts/VerticalScrollLayout.lua b/client/src/ui/Layouts/VerticalScrollLayout.lua index d3490276..5bd958d9 100644 --- a/client/src/ui/Layouts/VerticalScrollLayout.lua +++ b/client/src/ui/Layouts/VerticalScrollLayout.lua @@ -32,12 +32,6 @@ function VerticalScrollLayout.positionChildren(uiElement) y = y + uiElement.childGap + child.height end end - - for _, child in ipairs(uiElement.children) do - if child.isVisible then - child.layout.positionChildren(child) - end - end end return VerticalScrollLayout \ No newline at end of file From e60743e22672cfcf9bd0e151c84b6ed69bedc08a Mon Sep 17 00:00:00 2001 From: Endaris Date: Sun, 1 Jun 2025 01:41:53 +0200 Subject: [PATCH 36/49] fix characterbutton offsets --- client/src/ui/CharacterButton.lua | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/client/src/ui/CharacterButton.lua b/client/src/ui/CharacterButton.lua index 9bb6e905..1303d662 100644 --- a/client/src/ui/CharacterButton.lua +++ b/client/src/ui/CharacterButton.lua @@ -65,6 +65,7 @@ local super_select_pixelcode = [[ } ]] +CharacterButton.stageIconScale = 0.4 CharacterButton.padding = 2 CharacterButton.superSelectShader = love.graphics.newShader(super_select_pixelcode) @@ -167,8 +168,10 @@ function CharacterButton:drawSelf() local imageWidth, imageHeight = self.characterIcon:getDimensions() local scale = math.min(self.width / imageWidth, self.height / imageHeight) - GraphicsUtil.draw(self.characterIcon, self.x, self.y, 0, scale, scale) - GraphicsUtil.printf(self.displayName, self.x, self.y, self.width, "center", nil, nil, getFontSizeByScale(scale)) + love.graphics.push("transform") + love.graphics.translate(self.x, self.y) + GraphicsUtil.draw(self.characterIcon, 0, 0, 0, scale, scale) + GraphicsUtil.printf(self.displayName, 0, 0, self.width, "center", nil, nil, getFontSizeByScale(scale)) local bottomOffset = self.height - CharacterButton.padding * scale @@ -179,28 +182,26 @@ function CharacterButton:drawSelf() if self.panelIcon then imageWidth, imageHeight = self.panelIcon:getDimensions() - GraphicsUtil.draw(self.panelIcon, self.x + CharacterButton.padding * scale, bottomOffset, 0, scale, scale, imageWidth, imageHeight) + GraphicsUtil.draw(self.panelIcon, CharacterButton.padding * scale, bottomOffset, 0, scale, scale, 0, imageHeight) end if self.stageIcon then imageWidth, imageHeight = self.stageIcon:getDimensions() - local leftOffset = self.width / 2 - imageWidth / 2 - love.graphics.push("transform") - love.graphics.translate(leftOffset, bottomOffset) - GraphicsUtil.draw(self.stageIcon, 0, 0, 0, scale, scale, imageWidth / 2, imageHeight) - love.graphics.rectangle("line", - imageWidth / 2 * scale, -imageHeight * scale, imageWidth * scale, imageHeight * scale) - love.graphics.pop() + local stageScale = scale * CharacterButton.stageIconScale + GraphicsUtil.draw(self.stageIcon, self.width / 2, bottomOffset, 0, stageScale, stageScale, imageWidth / 2, imageHeight) + love.graphics.rectangle("line", (self.width / 2 - imageWidth * stageScale / 2), bottomOffset - imageHeight * stageScale, imageWidth * stageScale, imageHeight * stageScale) end if self.superSelectVisible then imageWidth, imageHeight = GAME.theme.images.IMG_super:getDimensions() scale = math.min(self.width / imageWidth, self.height / imageHeight) GraphicsUtil.setShader(CharacterButton.superSelectShader) - GraphicsUtil.draw(GAME.theme.images.IMG_super, self.x + self.width / 2, self.y + self.height / 2, 0, scale, scale, imageWidth / 2, imageHeight / 2) + GraphicsUtil.draw(GAME.theme.images.IMG_super, self.width / 2, self.height / 2, 0, scale, scale, imageWidth / 2, imageHeight / 2) GraphicsUtil.setShader() end - GraphicsUtil.drawRectangle("line", self.x, self.y, self.width, self.height) + GraphicsUtil.drawRectangle("line", 0, 0, self.width, self.height) + love.graphics.pop() end return CharacterButton \ No newline at end of file From cb56357395feb0688713486c0e73a5cd84210e48 Mon Sep 17 00:00:00 2001 From: Endaris Date: Sun, 1 Jun 2025 12:39:59 +0200 Subject: [PATCH 37/49] fix CharacterButton display --- client/src/scenes/DesignHelper.lua | 2 ++ client/src/ui/CharacterButton.lua | 21 ++++++++++++++------- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/client/src/scenes/DesignHelper.lua b/client/src/scenes/DesignHelper.lua index c81fff48..692e2e6c 100644 --- a/client/src/scenes/DesignHelper.lua +++ b/client/src/scenes/DesignHelper.lua @@ -41,6 +41,7 @@ local function createCharacterSelect(scene) hFill = true, vFill = true, maxHeight = 800, + hAlign = "center", --maxWidth = 1000, }) @@ -215,6 +216,7 @@ function DesignHelper:load() self.uiRoot:addChild(subSelectionSelector) self.subSelection = ui.UiElement({ + hAlign = "center", hFill = true, minHeight = 200, vFill = true, diff --git a/client/src/ui/CharacterButton.lua b/client/src/ui/CharacterButton.lua index 1303d662..7ea5e8bc 100644 --- a/client/src/ui/CharacterButton.lua +++ b/client/src/ui/CharacterButton.lua @@ -65,9 +65,10 @@ local super_select_pixelcode = [[ } ]] -CharacterButton.stageIconScale = 0.4 +CharacterButton.superSelectScale = 0.1904 CharacterButton.padding = 2 CharacterButton.superSelectShader = love.graphics.newShader(super_select_pixelcode) +CharacterButton.standardSize = 84 function CharacterButton:updateSuperSelectShader(timer) if timer > SUPER_SELECTION_START then @@ -171,25 +172,31 @@ function CharacterButton:drawSelf() love.graphics.push("transform") love.graphics.translate(self.x, self.y) GraphicsUtil.draw(self.characterIcon, 0, 0, 0, scale, scale) - GraphicsUtil.printf(self.displayName, 0, 0, self.width, "center", nil, nil, getFontSizeByScale(scale)) + GraphicsUtil.printf(self.displayName, 0, 0, self.width, "center", nil, nil, getFontSizeByScale(self.width / CharacterButton.standardSize)) + local leftOffset = CharacterButton.padding * scale + local rightOffset = self.width - CharacterButton.padding * scale local bottomOffset = self.height - CharacterButton.padding * scale + if self.flagIcon then imageWidth, imageHeight = self.flagIcon:getDimensions() - GraphicsUtil.draw(self.flagIcon, self.width - CharacterButton.padding * scale, bottomOffset, 0, scale, scale, imageWidth, imageHeight) + scale = math.round(self.width * CharacterButton.superSelectScale) / imageWidth + GraphicsUtil.draw(self.flagIcon, rightOffset, bottomOffset, 0, scale, scale, imageWidth, imageHeight) end if self.panelIcon then imageWidth, imageHeight = self.panelIcon:getDimensions() - GraphicsUtil.draw(self.panelIcon, CharacterButton.padding * scale, bottomOffset, 0, scale, scale, 0, imageHeight) + scale = math.round(self.width * CharacterButton.superSelectScale) / imageWidth + GraphicsUtil.draw(self.panelIcon, leftOffset, bottomOffset, 0, scale, scale, 0, imageHeight) end if self.stageIcon then imageWidth, imageHeight = self.stageIcon:getDimensions() - local stageScale = scale * CharacterButton.stageIconScale - GraphicsUtil.draw(self.stageIcon, self.width / 2, bottomOffset, 0, stageScale, stageScale, imageWidth / 2, imageHeight) - love.graphics.rectangle("line", (self.width / 2 - imageWidth * stageScale / 2), bottomOffset - imageHeight * stageScale, imageWidth * stageScale, imageHeight * stageScale) + local xScale = math.round(self.width * CharacterButton.superSelectScale * 2) / imageWidth + local yScale = math.round(self.width * CharacterButton.superSelectScale) / imageHeight + GraphicsUtil.draw(self.stageIcon, self.width / 2, bottomOffset, 0, xScale, yScale, imageWidth / 2, imageHeight) + love.graphics.rectangle("line", (self.width / 2 - imageWidth * xScale / 2), bottomOffset - imageHeight * yScale, imageWidth * xScale, imageHeight * yScale) end if self.superSelectVisible then From a0220d6c089aeb976a13a68142dd143942cb89e2 Mon Sep 17 00:00:00 2001 From: Endaris Date: Sun, 1 Jun 2025 13:35:10 +0200 Subject: [PATCH 38/49] fix cursor navigation on UniSizedContainer --- client/src/scenes/DesignHelper.lua | 4 +- client/src/ui/CursorNavigable.lua | 23 ++++--- client/src/ui/ImageCursor.lua | 29 +++++++++ client/src/ui/UniSizedContainer.lua | 98 ++++++++++++++--------------- client/src/ui/init.lua | 2 + 5 files changed, 95 insertions(+), 61 deletions(-) create mode 100644 client/src/ui/ImageCursor.lua diff --git a/client/src/scenes/DesignHelper.lua b/client/src/scenes/DesignHelper.lua index 692e2e6c..472c1cec 100644 --- a/client/src/scenes/DesignHelper.lua +++ b/client/src/scenes/DesignHelper.lua @@ -228,7 +228,7 @@ function DesignHelper:load() self.uiRoot:addChild(self.subSelection) - self.cursor = ui.Cursor(self.uiRoot) + self.cursor = ui.ImageCursor(self.uiRoot, nil, GAME.theme:getGridCursor(1)[1]) end function DesignHelper:loadRankedSelection(width) @@ -255,8 +255,8 @@ function DesignHelper:update(dt) end function DesignHelper:draw() - self.cursor:draw() self.uiRoot:draw() + self.cursor:draw() end return DesignHelper diff --git a/client/src/ui/CursorNavigable.lua b/client/src/ui/CursorNavigable.lua index 4a541c0c..3ca815af 100644 --- a/client/src/ui/CursorNavigable.lua +++ b/client/src/ui/CursorNavigable.lua @@ -63,13 +63,14 @@ end ---@param cursor Cursor ---@param dt number local function defaultReceiveInputs(cursorNavigable, cursor, dt) + local selected = cursor.focusToHover[cursorNavigable] local inputs = cursor.keyInput if inputs.isDown.Swap2 then GAME.theme:playCancelSfx() cursor:releaseFocus(cursorNavigable) - elseif cursor.focusToHover[cursorNavigable].isNavigable and (inputs.isDown.Swap1 or inputs.isDown.Start) then + elseif selected.isNavigable and (inputs.isDown.Swap1 or inputs.isDown.Start) then GAME.theme:playValidationSfx() - cursor:deepenFocus(cursor.focusToHover[cursorNavigable]) + cursor:deepenFocus(selected) elseif cursorNavigable.layout.characteristic == "horizontal" then if inputs:isPressedWithRepeat("Left", consts.KEY_DELAY, consts.KEY_REPEAT_PERIOD) then GAME.theme:playMoveSfx() @@ -77,9 +78,8 @@ local function defaultReceiveInputs(cursorNavigable, cursor, dt) elseif inputs:isPressedWithRepeat("Right", consts.KEY_DELAY, consts.KEY_REPEAT_PERIOD) then GAME.theme:playMoveSfx() cursorNavigable:moveToNext(cursor) - elseif cursor.focusToHover[cursorNavigable].receiveInputs then - local hovered = cursor.focusToHover[cursorNavigable] - hovered:receiveInputs(cursor, dt) + elseif selected.receiveInputs and not selected.isNavigable then + selected:receiveInputs(cursor, dt) end elseif cursorNavigable.layout.characteristic == "vertical" then if inputs:isPressedWithRepeat("Up", consts.KEY_DELAY, consts.KEY_REPEAT_PERIOD) then @@ -88,9 +88,8 @@ local function defaultReceiveInputs(cursorNavigable, cursor, dt) elseif inputs:isPressedWithRepeat("Down", consts.KEY_DELAY, consts.KEY_REPEAT_PERIOD) then GAME.theme:playMoveSfx() cursorNavigable:moveToNext(cursor) - elseif cursor.focusToHover[cursorNavigable].receiveInputs then - local hovered = cursor.focusToHover[cursorNavigable] - hovered:receiveInputs(cursor, dt) + elseif selected.receiveInputs and not selected.isNavigable then + selected:receiveInputs(cursor, dt) end else GAME.theme:playCancelSfx() @@ -105,8 +104,12 @@ end local function addCursorNavigationInterface(uiElement, receiveInputs) addCursorInteractionInterface(uiElement, receiveInputs or defaultReceiveInputs) uiElement.receiveFocus = receiveFocus - uiElement.moveToNext = moveToNext - uiElement.moveToPrevious = moveToPrevious + if not uiElement.moveToNext then + uiElement.moveToNext = moveToNext + end + if not uiElement.moveToPrevious then + uiElement.moveToPrevious = moveToPrevious + end uiElement.isNavigable = true ---@cast uiElement +CursorNavigable diff --git a/client/src/ui/ImageCursor.lua b/client/src/ui/ImageCursor.lua new file mode 100644 index 00000000..9ed180dd --- /dev/null +++ b/client/src/ui/ImageCursor.lua @@ -0,0 +1,29 @@ +local import = require("common.lib.import") +local Cursor = import("./Cursor") +local class = require("common.lib.class") +local consts = require("client.src.consts") +local GraphicsUtil = require("client.src.graphics.graphics_util") +local input = require("client.src.inputManager") + + +---@class ImageCursor : Cursor +---@field image love.Texture +local ImageCursor = class( +function (self, target, keyInput, image) + self.image = image +end, +Cursor) + +function ImageCursor:draw() + -- GraphicsUtil.setColor(0, 0, 0, 0.2) + -- love.graphics.rectangle("fill", self.target.x, self.target.y, self.target.width, self.target.height) + if self.focused then + local uiElement = self.focusToHover[self.focused] + GraphicsUtil.setColor(1, 1, 1, 1) + local x, y = uiElement:getScreenPos() + local imageWidth, imageHeight = self.image:getDimensions() + GraphicsUtil.draw(self.image, x, y, 0, uiElement.width / imageWidth, uiElement.height / imageHeight) + end +end + +return ImageCursor \ No newline at end of file diff --git a/client/src/ui/UniSizedContainer.lua b/client/src/ui/UniSizedContainer.lua index 13250c1d..f8469358 100644 --- a/client/src/ui/UniSizedContainer.lua +++ b/client/src/ui/UniSizedContainer.lua @@ -7,6 +7,7 @@ local consts = require("client.src.consts") local GraphicsUtil = require("client.src.graphics.graphics_util") local input = require("client.src.inputManager") local addCursorNavigationInterface = import("./CursorNavigable") +local tableUtils = require("common.lib.tableUtils") ---@class UniSizedContainerOptions : UiElementOptions, CursorNavigableOptions ---@field childrenWidth integer @@ -16,9 +17,8 @@ local addCursorNavigationInterface = import("./CursorNavigable") ---@operator call(UniSizedContainerOptions): UniSizedContainer ---@field childrenWidth integer ---@field childrenHeight integer ----@field selectedRow integer ----@field selectedColumn integer ---@field rows UiElement[][] +---@field childToRow table ---@overload fun(options: UniSizedContainerOptions): UniSizedContainer local UniSizedContainer = class( function(self, options) @@ -28,9 +28,6 @@ function(self, options) self.childrenHeight = options.childrenHeight self.minHeight = self.padding * 2 + self.childrenHeight - self.selectedRow = 1 - self.selectedColumn = 1 - addCursorNavigationInterface(self, self.receiveInputs) self.onFocus = options.onFocus self.onYield = options.onYield @@ -58,10 +55,16 @@ function UniSizedContainer:moveToPreviousRow(cursor) return false end - local nextRow = wrap(1, self.selectedRow - 1, #self.rows) - if self.rows[nextRow][self.selectedColumn] then - self.selectedRow = nextRow - cursor:updateHover(self, self.rows[self.selectedRow][self.selectedColumn]) + local selectedChild = cursor.focusToHover[self] + local currentRow = self.childToRow[selectedChild] + local currentCol = tableUtils.indexOf(self.rows[currentRow], selectedChild) + local nextRow = wrap(1, currentRow - 1, #self.rows) + if not self.rows[nextRow][currentCol] then + -- if the wrapped elements don't extend into the selected columns, redo the wrap without considering that row + nextRow = wrap(1, currentRow - 1, #self.rows - 1) + end + if self.rows[nextRow][currentCol] then + cursor:updateHover(self, self.rows[nextRow][currentCol]) return true else return false @@ -75,10 +78,16 @@ function UniSizedContainer:moveToNextRow(cursor) return false end - local nextRow = wrap(1, self.selectedRow + 1, #self.rows) - if self.rows[nextRow][self.selectedColumn] then - self.selectedRow = nextRow - cursor:updateHover(self, self.rows[self.selectedRow][self.selectedColumn]) + local selectedChild = cursor.focusToHover[self] + local currentRow = self.childToRow[selectedChild] + local currentCol = tableUtils.indexOf(self.rows[currentRow], selectedChild) + local nextRow = wrap(1, currentRow + 1, #self.rows) + if not self.rows[nextRow][currentCol] then + -- if the wrapped elements don't extend into the selected columns, redo the wrap without considering that row + nextRow = wrap(1, currentRow + 1, #self.rows - 1) + end + if self.rows[nextRow][currentCol] then + cursor:updateHover(self, self.rows[nextRow][currentCol]) return true else return false @@ -88,24 +97,32 @@ end ---@param cursor Cursor ---@return boolean? # if the movement was successful function UniSizedContainer:moveToPrevious(cursor) - if not self.rows or not self.rows[self.selectedRow] or #self.rows[self.selectedRow] == 1 then + local selectedChild = cursor.focusToHover[self] + local currentRow = self.childToRow[selectedChild] + + if not self.rows or not self.rows[currentRow] or #self.rows[currentRow] == 1 then return false end - self.selectedColumn = wrap(1, self.selectedColumn - 1, #self.rows[self.selectedRow]) - cursor:updateHover(self, self.rows[self.selectedRow][self.selectedColumn]) + local currentCol = tableUtils.indexOf(self.rows[currentRow], selectedChild) + local nextCol = wrap(1, currentCol - 1, #self.rows[currentRow]) + cursor:updateHover(self, self.rows[currentRow][nextCol]) return true end ---@param cursor Cursor ---@return boolean? # if the movement was successful function UniSizedContainer:moveToNext(cursor) - if not self.rows or not self.rows[self.selectedRow] or #self.rows[self.selectedRow] == 1 then + local selectedChild = cursor.focusToHover[self] + local currentRow = self.childToRow[selectedChild] + + if not self.rows or not self.rows[currentRow] or #self.rows[currentRow] == 1 then return false end - self.selectedColumn = wrap(1, self.selectedColumn + 1, #self.rows[self.selectedRow]) - cursor:updateHover(self, self.rows[self.selectedRow][self.selectedColumn]) + local currentCol = tableUtils.indexOf(self.rows[currentRow], selectedChild) + local nextCol = wrap(1, currentCol + 1, #self.rows[currentRow]) + cursor:updateHover(self, self.rows[currentRow][nextCol]) return true end @@ -140,13 +157,16 @@ function UniSizedContainer:receiveInputs(cursor, dt) else GAME.theme:playCancelSfx() end - elseif self.rows[self.selectedRow][self.selectedColumn].isNavigable and (inputs.isDown.Swap1 or inputs.isDown.Start) then - GAME.theme:playValidationSfx() - cursor:deepenFocus(self.rows[self.selectedRow][self.selectedColumn]) - elseif self.rows[self.selectedRow][self.selectedColumn].receiveInputs then - self.rows[self.selectedRow][self.selectedColumn]:receiveInputs(cursor, dt) else - GAME.theme:playCancelSfx() + local selectedChild = cursor.focusToHover[self] + if selectedChild.isNavigable and (inputs.isDown.Swap1 or inputs.isDown.Start) then + GAME.theme:playValidationSfx() + cursor:deepenFocus(selectedChild) + elseif selectedChild.receiveInputs then + selectedChild:receiveInputs(cursor, dt) + else + GAME.theme:playCancelSfx() + end end end @@ -155,13 +175,9 @@ function UniSizedContainer:onResized() return end - local selectedChild - if self.rows then - selectedChild = self.rows[self.selectedRow][self.selectedColumn] - end - local rowIndex = 1 local rows = {{}} + local childToRow = {} local yOffset = self.children[1].y for i, child in ipairs(self.children) do if child.y > yOffset then @@ -170,32 +186,16 @@ function UniSizedContainer:onResized() rows[rowIndex] = {} end table.insert(rows[rowIndex], child) + childToRow[child] = rowIndex end self.rows = rows - - if selectedChild then - for row = 1, #self.rows do - for col = 1, #self.rows[row] do - if self.rows[row][col] == selectedChild then - self.selectedRow = row - self.selectedColumn = col - break - end - end - end - end + self.childToRow = childToRow end function UniSizedContainer:drawSelf() UiElement.drawSelf(self) - if self.hasFocus then - local selectedChild = self.rows[self.selectedRow][self.selectedColumn] - local x, y = selectedChild:getScreenPos() - GraphicsUtil.setColor(1, 1, 1, 0.2) - love.graphics.rectangle("fill", x, y, selectedChild.width, selectedChild.height) - GraphicsUtil.setColor(1, 1, 1, 1) - + if self:isHovered() then if input.isDown["Left"] or input.isPressed["Left"] then GraphicsUtil.printf("Left", 0, 0) elseif input.isDown["Right"] or input.isPressed["Right"] then diff --git a/client/src/ui/init.lua b/client/src/ui/init.lua index 834bd8cb..5d768b5f 100644 --- a/client/src/ui/init.lua +++ b/client/src/ui/init.lua @@ -39,6 +39,8 @@ local ui = { GridCursor = import("./GridCursor"), ---@source ImageContainer.lua ImageContainer = import("./ImageContainer"), + ---@source ImageCursor.lua + ImageCursor = import("./ImageCursor"), ---@source InputField.lua InputField = import("./InputField"), ---@source Label.lua From e5b1e11364b5f1f3107c61eb3a61b8c38f51d7e4 Mon Sep 17 00:00:00 2001 From: Endaris Date: Sun, 1 Jun 2025 14:03:10 +0200 Subject: [PATCH 39/49] only show super select stage while a character is hovered (cause it can look bad as default) --- client/src/ui/CharacterButton.lua | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/client/src/ui/CharacterButton.lua b/client/src/ui/CharacterButton.lua index 7ea5e8bc..4e752498 100644 --- a/client/src/ui/CharacterButton.lua +++ b/client/src/ui/CharacterButton.lua @@ -65,7 +65,7 @@ local super_select_pixelcode = [[ } ]] -CharacterButton.superSelectScale = 0.1904 +CharacterButton.extraIconScale = 0.1904 CharacterButton.padding = 2 CharacterButton.superSelectShader = love.graphics.newShader(super_select_pixelcode) CharacterButton.standardSize = 84 @@ -181,22 +181,24 @@ function CharacterButton:drawSelf() if self.flagIcon then imageWidth, imageHeight = self.flagIcon:getDimensions() - scale = math.round(self.width * CharacterButton.superSelectScale) / imageWidth + scale = math.round(self.width * CharacterButton.extraIconScale) / imageWidth GraphicsUtil.draw(self.flagIcon, rightOffset, bottomOffset, 0, scale, scale, imageWidth, imageHeight) end if self.panelIcon then imageWidth, imageHeight = self.panelIcon:getDimensions() - scale = math.round(self.width * CharacterButton.superSelectScale) / imageWidth + scale = math.round(self.width * CharacterButton.extraIconScale) / imageWidth GraphicsUtil.draw(self.panelIcon, leftOffset, bottomOffset, 0, scale, scale, 0, imageHeight) end - if self.stageIcon then + if self.stageIcon and self:isHovered() then imageWidth, imageHeight = self.stageIcon:getDimensions() - local xScale = math.round(self.width * CharacterButton.superSelectScale * 2) / imageWidth - local yScale = math.round(self.width * CharacterButton.superSelectScale) / imageHeight + local xScale = math.round(self.width * CharacterButton.extraIconScale * 2) / imageWidth + local yScale = math.round(self.width * CharacterButton.extraIconScale) / imageHeight GraphicsUtil.draw(self.stageIcon, self.width / 2, bottomOffset, 0, xScale, yScale, imageWidth / 2, imageHeight) + love.graphics.setLineWidth(0.5) love.graphics.rectangle("line", (self.width / 2 - imageWidth * xScale / 2), bottomOffset - imageHeight * yScale, imageWidth * xScale, imageHeight * yScale) + love.graphics.setLineWidth(1) end if self.superSelectVisible then From af1fbce571657e1d7ebd192d172893262d29b11f Mon Sep 17 00:00:00 2001 From: Endaris Date: Sun, 1 Jun 2025 18:16:40 +0200 Subject: [PATCH 40/49] add PanelSetButton --- client/src/mods/Panels.lua | 33 ++++--- client/src/scenes/DesignHelper.lua | 32 +++++++ client/src/ui/Button.lua | 11 +-- client/src/ui/CharacterButton.lua | 1 + client/src/ui/Cursor.lua | 10 ++- .../src/ui/Layouts/HorizontalFlexLayout.lua | 8 +- client/src/ui/Layouts/VerticalFlexLayout.lua | 6 +- client/src/ui/PanelSetButton.lua | 87 +++++++++++++++++++ client/src/ui/VerticalMenu.lua | 3 + client/src/ui/init.lua | 2 + client/src/ui/touchHandler.lua | 6 ++ 11 files changed, 178 insertions(+), 21 deletions(-) create mode 100644 client/src/ui/PanelSetButton.lua diff --git a/client/src/mods/Panels.lua b/client/src/mods/Panels.lua index ddd907d8..968c4e0f 100644 --- a/client/src/mods/Panels.lua +++ b/client/src/mods/Panels.lua @@ -52,8 +52,19 @@ local DEFAULT_PANEL_ANIM = -- The class representing the panel image data -- Not to be confused with "Panel" which is one individual panel in the game stack model -Panels = - class( +---@class PanelSet +---@field path string +---@field id string +---@field name string +---@field size integer +---@field images {metals: {left: love.Texture, mid: love.Texture, right: love.Texture, flash: love.Texture}} +---@field sheets love.Texture[] +---@field sheetConfig table +---@field batches love.SpriteBatch[] +local Panels = class( +---@param self PanelSet +---@param full_path string +---@param folder_name string function(self, full_path, folder_name) self.path = full_path -- string | path to the panels folder content self.id = folder_name -- string | id of the panel set, is also the name of its folder by default, may change in json_init @@ -595,19 +606,21 @@ function Panels:getDrawProps(panel, x, y, dangerCol, dangerTimer, stopTime) return conf, frame, x, y end --- adds the panel to a batch for later drawing --- x, y: relative coordinates on the stack canvas --- clock: Stack.clock to calculate animation frames --- danger: nil - no danger, false - regular danger, true - panic --- dangerTimer: remaining time for which the danger animation continues --- stopTime: the remaining stop time -function Panels:addToDraw(panel, x, y, stackScale, danger, dangerTimer, stopTime) +-- adds the panel to a batch for later drawing or, if color 9, draws it directly +---@param panel Panel +---@param x integer relative x coordinate to the parent / stack +---@param y integer relative y coordinate to the parent / stack +---@param stackScale number +---@param dangerCol boolean[] danger state per column, true if the column touches top +---@param dangerTimer integer +---@param stopTime integer +function Panels:addToDraw(panel, x, y, stackScale, dangerCol, dangerTimer, stopTime) if panel.color == 9 then love.graphics.draw(self.greyPanel, x * stackScale, y * stackScale, 0, self.scale * stackScale) else local batch = self.batches[panel.color] local conf, frame - conf, frame, x, y = self:getDrawProps(panel, x, y, danger, dangerTimer, stopTime) + conf, frame, x, y = self:getDrawProps(panel, x, y, dangerCol, dangerTimer, stopTime) if conf then self.quad:setViewport((frame - 1) * self.size, (conf.row - 1) * self.size, self.size, self.size) diff --git a/client/src/scenes/DesignHelper.lua b/client/src/scenes/DesignHelper.lua index 472c1cec..d542a5b5 100644 --- a/client/src/scenes/DesignHelper.lua +++ b/client/src/scenes/DesignHelper.lua @@ -64,8 +64,36 @@ local function createCharacterSelect(scene) return scrollContainer end +local function createPanelSetSelect(scene) + scene.panelSetSelect = ui.VerticalMenu({ + childGap = 8, + hFill = true, + vFill = true, + maxHeight = 800, + hAlign = "center", + onYield = function (self) + self:detach() + end + }) + + for panelSetId, panelSet in pairs(panels) do + local button = ui.PanelSetButton({panelSet = panelSet, hFill = true, maxWidth = 48 * 9}) + scene.panelSetSelect:addChild(button) + end + + -- alphabetical order I guess? + table.sort(scene.panelSetSelect.children, function(a, b) + return a.panelSet.id < b.panelSet.id + end) + + scene.panelSetSelect:addChild(ui.TextButton({label = ui.Label({id = "back"}), onClick = function() scene.cursor:releaseFocus(scene.panelSetSelect) end})) + + return scene.panelSetSelect +end + function DesignHelper:load() self.characterSelectContainer = createCharacterSelect(self) + self.panelSetSelect = createPanelSetSelect(self) self.uiRoot.layout = ui.Layouts.VerticalFlexLayout self.uiRoot.childGap = 8 ui.CursorNavigable(self.uiRoot) @@ -170,6 +198,10 @@ function DesignHelper:load() panelButton.padding = 4 panelSelectionSelector.childGap = 0 panelButton:addChild(panelContainer) + panelButton.onClick = function() + self.subSelection:addChild(self.panelSetSelect) + self.cursor:deepenFocus(self.panelSetSelect) + end local levelImage = ui.ImageContainer({ image = GAME.theme.images.IMG_levels[GAME.localPlayer.settings.level or 1], diff --git a/client/src/ui/Button.lua b/client/src/ui/Button.lua index 7c022b05..66b465b2 100644 --- a/client/src/ui/Button.lua +++ b/client/src/ui/Button.lua @@ -18,6 +18,7 @@ local addCursorInteractionInterface = import("./CursorInteractable") ---@field onClick fun(button: Button?, input: table?, timeHeld: number?) local Button = class( function(self, options) + self.hoveredBackgroundColor = {.5, .5, .5, .7} self.backgroundColor = options.backgroundColor or {.3, .3, .3, .7} self.outlineColor = options.outlineColor or {.5, .5, .5, .7} @@ -34,11 +35,9 @@ local Button = class( Button.TYPE = "Button" function Button:onTouch(x, y) - self.backgroundColor[4] = 1 end function Button:onRelease(x, y, timeHeld) - self.backgroundColor[4] = 0.7 if self:inBounds(x, y) then -- first argument non-self of onClick is the input source to accomodate inputs via controllers from different players self:onClick(input.mouse, timeHeld) @@ -58,11 +57,13 @@ function Button:receiveInputs(cursor, dt) end function Button:drawBackground() - if self.backgroundColor[4] > 0 then + if self:isHovered() then + GraphicsUtil.setColor(self.hoveredBackgroundColor) + else GraphicsUtil.setColor(self.backgroundColor) - GraphicsUtil.drawRectangle("fill", self.x, self.y, self.width, self.height) - GraphicsUtil.setColor(1, 1, 1, 1) end + GraphicsUtil.drawRectangle("fill", self.x, self.y, self.width, self.height) + GraphicsUtil.setColor(1, 1, 1, 1) end function Button:drawOutline() diff --git a/client/src/ui/CharacterButton.lua b/client/src/ui/CharacterButton.lua index 4e752498..76f4a498 100644 --- a/client/src/ui/CharacterButton.lua +++ b/client/src/ui/CharacterButton.lua @@ -3,6 +3,7 @@ local class = require("common.lib.class") local Button = import("./Button") local consts = require("client.src.consts") local GraphicsUtil = require("client.src.graphics.graphics_util") +local input = require("client.src.inputManager") local SUPER_SELECTION_DURATION = 0.5 -- time held in seconds at which super select actually happens local SUPER_SELECTION_START = 0.1 -- time held in seconds at which super select is considered started diff --git a/client/src/ui/Cursor.lua b/client/src/ui/Cursor.lua index 642b15e4..eafea0ec 100644 --- a/client/src/ui/Cursor.lua +++ b/client/src/ui/Cursor.lua @@ -87,10 +87,12 @@ function Cursor:draw() -- love.graphics.rectangle("fill", self.target.x, self.target.y, self.target.width, self.target.height) if self.focused then local uiElement = self.focusToHover[self.focused] - GraphicsUtil.setColor(1, 1, 1, 0.2) - local x, y = uiElement:getScreenPos() - love.graphics.rectangle("fill", x, y, uiElement.width, uiElement.height) - GraphicsUtil.setColor(1, 1, 1, 1) + if not uiElement.receiveInputs then + GraphicsUtil.setColor(1, 1, 1, 0.2) + local x, y = uiElement:getScreenPos() + love.graphics.rectangle("fill", x, y, uiElement.width, uiElement.height) + GraphicsUtil.setColor(1, 1, 1, 1) + end end end diff --git a/client/src/ui/Layouts/HorizontalFlexLayout.lua b/client/src/ui/Layouts/HorizontalFlexLayout.lua index 1dbf26ac..fb1359a5 100644 --- a/client/src/ui/Layouts/HorizontalFlexLayout.lua +++ b/client/src/ui/Layouts/HorizontalFlexLayout.lua @@ -41,7 +41,13 @@ function HorizontalFlexLayout.getMinHeight(uiElement) end end - return h + maxHeight + h = h + maxHeight + + if uiElement.getMinHeight then + return math.max(h, uiElement:getMinHeight()) + else + return h + end end local growables = {} diff --git a/client/src/ui/Layouts/VerticalFlexLayout.lua b/client/src/ui/Layouts/VerticalFlexLayout.lua index af8bfb7e..8d29e37e 100644 --- a/client/src/ui/Layouts/VerticalFlexLayout.lua +++ b/client/src/ui/Layouts/VerticalFlexLayout.lua @@ -42,7 +42,11 @@ function VerticalFlexLayout.getMinHeight(uiElement) end end - return h + if uiElement.getMinHeight then + return math.max(h, uiElement:getMinHeight()) + else + return h + end end ---@param uiElement UiElement diff --git a/client/src/ui/PanelSetButton.lua b/client/src/ui/PanelSetButton.lua new file mode 100644 index 00000000..0f840968 --- /dev/null +++ b/client/src/ui/PanelSetButton.lua @@ -0,0 +1,87 @@ +local import = require("common.lib.import") +local class = require("common.lib.class") +local Button = import("./Button") +local consts = require("client.src.consts") +local GraphicsUtil = require("client.src.graphics.graphics_util") +local input = require("client.src.inputManager") +local Panel = require("common.engine.Panel") +local LevelPresets = require("common.data.LevelPresets") + + +---@class PanelButtonOptions : ButtonOptions +---@field panelSet PanelSet + +---@class PanelButton : Button +---@field panelSet PanelSet +---@field dangerTimer integer +---@field panels Panel[] +local PanelButton = class( +function (self, options) + self.panelSet = options.panelSet + self.panels = {} + + local frameTimes = LevelPresets.getModern(5).frameConstants + if GAME.localPlayer and GAME.localPlayer.settings.levelData then + frameTimes = GAME.localPlayer.settings.levelData.frameConstants + end + + for i = 1, 9 do + local panel = Panel(1, 1, i, frameTimes) + panel.color = i + self.panels[i] = panel + end + + self.minWidth = 9 * 16 + + self.dangerTimer = 0 +end, +Button) + +---@param inputSource table +function PanelButton:action(inputSource) + ---@type MatchParticipant + local player + if inputSource and inputSource.player then + player = inputSource.player + else + player = GAME.localPlayer + end + player:setPanels(self.panelSet.id) +end + +function PanelButton:getMinHeight() + return math.ceil(self.width / 9) + 4 + GraphicsUtil.getTextHeightForWidth("normal", self.panelSet.name or self.panelSet.id, self.width, "center") +end + +function PanelButton:drawSelf() + self.panelSet:prepareDraw() + love.graphics.push("transform") + love.graphics.translate(self.x, self.y) + local width = math.floor(self.width / 9) + + if self:isHovered() then + self.dangerTimer = self.dangerTimer + 1 + else + self.dangerTimer = 0 + end + + local x = 0 + local scale = width / 16 + + for i = 1, 9 do + -- 16 because panelSet draw multiplies the location by the scale argument as well and 16 is the base value + x = (i - 1) * 16 + local panel = self.panels[i] + self.panelSet:addToDraw(panel, x, 0, scale, { self:isHovered() }, self.dangerTimer, 0) + end + + self.panelSet:drawBatch() + + GraphicsUtil.printf(self.panelSet.name or self.panelSet.id, 0, width + 4, self.width, "center", nil, nil, "normal") + + love.graphics.pop() +end + + + +return PanelButton \ No newline at end of file diff --git a/client/src/ui/VerticalMenu.lua b/client/src/ui/VerticalMenu.lua index 1af22f2b..38dcd9b5 100644 --- a/client/src/ui/VerticalMenu.lua +++ b/client/src/ui/VerticalMenu.lua @@ -17,6 +17,9 @@ function(self, options) self.scrollOrientation = "vertical" self.layout = VerticalScrollLayout addCursorNavigationInterface(self, self.receiveInputs) + + self.onYield = options.onYield + self.onFocus = options.onFocus end, ScrollContainer) diff --git a/client/src/ui/init.lua b/client/src/ui/init.lua index 5d768b5f..6b71f88a 100644 --- a/client/src/ui/init.lua +++ b/client/src/ui/init.lua @@ -66,6 +66,8 @@ local ui = { MultiPlayerSelectionWrapper = import("./MultiPlayerSelectionWrapper"), PagedUniGrid = import("./PagedUniGrid"), PanelCarousel = import("./PanelCarousel"), + ---@source PanelSetButton.lua + PanelSetButton = import("./PanelSetButton"), ---@source PixelFontLabel.lua ---@type PixelFontLabel PixelFontLabel = import("./PixelFontLabel"), diff --git a/client/src/ui/touchHandler.lua b/client/src/ui/touchHandler.lua index b5977e8b..f9e8a794 100644 --- a/client/src/ui/touchHandler.lua +++ b/client/src/ui/touchHandler.lua @@ -16,6 +16,9 @@ function touchHandler:touch(x, y) if not self.touchedElement then self.touchedElement = activeScene.uiRoot:getTouchedElement(x, y) if self.touchedElement and self.touchedElement.onTouch then + if self.touchedElement.hoveringCursors then + self.touchedElement.hoveringCursors["mouse"] = true + end self.touchedElement:onTouch(x, y) end end @@ -34,6 +37,9 @@ end function touchHandler:release(x, y) if self.touchedElement then if self.touchedElement.onRelease then + if self.touchedElement.hoveringCursors then + self.touchedElement.hoveringCursors["mouse"] = nil + end self.touchedElement:onRelease(x, y, self.holdTimer) end self.touchedElement = nil From ee12a09318bb6a689e2d8358575ea1944831e257 Mon Sep 17 00:00:00 2001 From: Endaris Date: Sun, 1 Jun 2025 20:24:57 +0200 Subject: [PATCH 41/49] fix some things regarding resizing with overflowing height --- client/src/Game.lua | 4 + client/src/scenes/DesignHelper.lua | 5 +- client/src/scenes/Scene.lua | 1 + client/src/ui/Layouts/FlexLayout.lua | 4 +- client/src/ui/Layouts/Layout.lua | 3 + client/src/ui/Layouts/VerticalFlexLayout.lua | 172 ++++++++++++++----- 6 files changed, 139 insertions(+), 50 deletions(-) diff --git a/client/src/Game.lua b/client/src/Game.lua index af17c155..68d1cf4f 100644 --- a/client/src/Game.lua +++ b/client/src/Game.lua @@ -352,6 +352,10 @@ end function Game:handleResize(newWidth, newHeight) local activeScene = GAME.navigationStack:getActiveScene() if activeScene then + local _, _, flags = love.window.getMode() + local screenWidth, screenHeight = love.window.getDesktopDimensions(flags.display) + newWidth = math.min(newWidth, screenWidth) + newHeight = math.min(newHeight, screenHeight) activeScene.uiRoot.minWidth = newWidth activeScene.uiRoot.minHeight = newHeight activeScene.uiRoot.layout.resize(activeScene.uiRoot, newWidth, newHeight) diff --git a/client/src/scenes/DesignHelper.lua b/client/src/scenes/DesignHelper.lua index d542a5b5..7062b3e8 100644 --- a/client/src/scenes/DesignHelper.lua +++ b/client/src/scenes/DesignHelper.lua @@ -38,6 +38,7 @@ end local function createCharacterSelect(scene) local scrollContainer = ui.ScrollContainer({ scrollOrientation = "vertical", + minHeight = 400, hFill = true, vFill = true, maxHeight = 800, @@ -67,6 +68,7 @@ end local function createPanelSetSelect(scene) scene.panelSetSelect = ui.VerticalMenu({ childGap = 8, + minHeight = 400, hFill = true, vFill = true, maxHeight = 800, @@ -253,8 +255,9 @@ function DesignHelper:load() minHeight = 200, vFill = true, padding = 8, - backgroundColor = {0, 1, 0, 0.5}--{0.7, 0, 0.5, 1}, + backgroundColor = {0, 1, 0, 0.5},--{0.7, 0, 0.5, 1}, }) + self.subSelection.debug = true ui.CursorInteractable(self.subSelection, function() end) diff --git a/client/src/scenes/Scene.lua b/client/src/scenes/Scene.lua index a5000d59..bcebd2e2 100644 --- a/client/src/scenes/Scene.lua +++ b/client/src/scenes/Scene.lua @@ -34,6 +34,7 @@ local Scene = class( vAlign = "center", }) self.uiRoot.controlsWindow = true + self.uiRoot.debug = true -- scenes may specify theme music to use that is played once they are switched to -- eligible labels: -- main diff --git a/client/src/ui/Layouts/FlexLayout.lua b/client/src/ui/Layouts/FlexLayout.lua index a682224f..2605f91a 100644 --- a/client/src/ui/Layouts/FlexLayout.lua +++ b/client/src/ui/Layouts/FlexLayout.lua @@ -54,9 +54,9 @@ function FlexLayout.setHeight(uiElement, height) uiElement.layout.fitSizeHeight(uiElement) end if height then - uiElement.height = math.max(height, uiElement.newHeight) + uiElement.height = math.min(math.max(height, uiElement.newHeight), uiElement.maxHeight) else - uiElement.height = uiElement.newHeight + uiElement.height = math.min(uiElement.newHeight, uiElement.maxHeight) end uiElement.newHeight = nil end diff --git a/client/src/ui/Layouts/Layout.lua b/client/src/ui/Layouts/Layout.lua index 6aad694e..bb58a410 100644 --- a/client/src/ui/Layouts/Layout.lua +++ b/client/src/ui/Layouts/Layout.lua @@ -52,6 +52,9 @@ end ---@param uiElement UiElement function Layout.updateHeights(uiElement, height) + if uiElement.debug then + local phi = 5 + end uiElement.layout.setHeight(uiElement, height) uiElement.layout.finalizeChildrenHeights(uiElement) diff --git a/client/src/ui/Layouts/VerticalFlexLayout.lua b/client/src/ui/Layouts/VerticalFlexLayout.lua index 8d29e37e..769cc324 100644 --- a/client/src/ui/Layouts/VerticalFlexLayout.lua +++ b/client/src/ui/Layouts/VerticalFlexLayout.lua @@ -34,14 +34,16 @@ end ---@param uiElement UiElement function VerticalFlexLayout.getMinHeight(uiElement) - local h = uiElement.padding * 2 + uiElement.childGap * (#uiElement.children - 1) + local h = uiElement.padding * 2 for _, child in ipairs(uiElement.children) do if child.isVisible then - h = h + child.newHeight + h = h + child.newHeight + uiElement.childGap end end + h = h - uiElement.childGap + if uiElement.getMinHeight then return math.max(h, uiElement:getMinHeight()) else @@ -49,71 +51,147 @@ function VerticalFlexLayout.getMinHeight(uiElement) end end +local growables = {} +local shrinkables = {} + ---@param uiElement UiElement function VerticalFlexLayout.finalizeChildrenHeights(uiElement) if #uiElement.children == 0 then return end - local growables = {} + local remainingHeight = uiElement.height - uiElement.padding * 2 - for i, child in ipairs(uiElement.children) do - if child.isVisible and child.vFill and child.newHeight < child.maxHeight then - growables[#growables+1] = child + for _, child in ipairs(uiElement.children) do + if uiElement.isVisible then + remainingHeight = remainingHeight - child.newHeight + remainingHeight = remainingHeight - uiElement.childGap end end - if #growables > 0 then - local remainingHeight = uiElement.height - uiElement.layout.getMinHeight(uiElement) - - while #growables > 0 and remainingHeight > 0 do - local smallest = growables[1].newHeight - local secondSmallest = math.huge - -- if growables[1] is already the smallest, it will increment the counter at some point within the loop so we can't count it yet - local smallestCount = 1 - - for i = 2, #growables do - local growable = growables[i] - if growable.newHeight < smallest then - secondSmallest = smallest - smallest = growable.newHeight - smallestCount = 1 - elseif growable.newHeight > smallest then - secondSmallest = math.min(secondSmallest, growable.newHeight) + remainingHeight = remainingHeight + uiElement.childGap + + if remainingHeight >= 1 then + table.clear(growables) + + for i, child in ipairs(uiElement.children) do + if child.isVisible and child.vFill and child.newHeight < child.maxHeight then + growables[#growables+1] = child + end + end + + if #growables > 0 then + while #growables > 0 and remainingHeight > 0 do + local smallest = growables[1].newHeight + local secondSmallest = math.huge + -- if growables[1] is already the smallest, it will increment the counter at some point within the loop so we can't count it yet + local smallestCount = 1 + + for i = 2, #growables do + local growable = growables[i] + if growable.newHeight < smallest then + secondSmallest = smallest + smallest = growable.newHeight + smallestCount = 1 + elseif growable.newHeight > smallest then + secondSmallest = math.min(secondSmallest, growable.newHeight) + else + smallestCount = smallestCount + 1 + end + end + + local delta = secondSmallest - smallest + local heightToAdd = math.min(delta, remainingHeight / smallestCount) + + if delta * smallestCount >= remainingHeight then + for _, growable in ipairs(growables) do + if growable.newHeight == smallest then + local toAdd = math.min(heightToAdd, growable.maxHeight - growable.newHeight) + growable.newHeight = growable.newHeight + toAdd + remainingHeight = remainingHeight - toAdd + end + end + if remainingHeight < 1 then + -- we could run into floating point shenanigans here + remainingHeight = 0 + end else - smallestCount = smallestCount + 1 + for _, growable in ipairs(growables) do + if growable.newHeight == smallest then + local toAdd = math.min(heightToAdd, growable.maxHeight - growable.newHeight) + growable.newHeight = growable.newHeight + toAdd + remainingHeight = remainingHeight - toAdd + end + end + end + + for i = #growables, 1, -1 do + local growable = growables[i] + if growable.newHeight >= growable.maxHeight then + table.remove(growables, i) + end end end + end + elseif remainingHeight <= -1 then + table.clear(shrinkables) - local delta = secondSmallest - smallest - local heightToAdd = math.min(delta, remainingHeight / smallestCount) + for i, child in ipairs(uiElement.children) do + if child.isVisible and child.newHeight > child.minHeight then + shrinkables[#shrinkables+1] = child + end + end - if delta * smallestCount >= remainingHeight then - for _, growable in ipairs(growables) do - if growable.newHeight == smallest then - local toAdd = math.min(heightToAdd, growable.maxHeight - growable.newHeight) - growable.newHeight = growable.newHeight + toAdd - remainingHeight = remainingHeight - toAdd + if #shrinkables > 0 then + while #shrinkables > 0 and remainingHeight < 0 do + local biggest = shrinkables[1].newHeight + local secondBiggest = 0 + -- if growables[1] is already the smallest, it will increment the counter at some point within the loop so we can't count it yet + local biggestCount = 1 + + for i = 2, #shrinkables do + local shrinkable = shrinkables[i] + if shrinkable.newHeight > biggest then + secondBiggest = biggest + biggest = shrinkable.newHeight + biggestCount = 1 + elseif shrinkable.newHeight < biggest then + secondBiggest = math.max(secondBiggest, shrinkable.newHeight) + else + biggestCount = biggestCount + 1 end end - if remainingHeight < 1 then - -- we could run into floating point shenanigans here - remainingHeight = 0 - end - else - for _, growable in ipairs(growables) do - if growable.newHeight == smallest then - local toAdd = math.min(heightToAdd, growable.maxHeight - growable.newHeight) - growable.newHeight = growable.newHeight + toAdd - remainingHeight = remainingHeight - toAdd + + local delta = math.abs(secondBiggest - biggest) + local heightToSubtract = math.min(delta, math.abs(remainingHeight / biggestCount)) + + if delta * biggestCount >= -remainingHeight then + for _, shrinkable in ipairs(shrinkables) do + if shrinkable.newHeight == biggest then + local toSubtract = math.min(heightToSubtract, shrinkable.newHeight - shrinkable.minHeight) + shrinkable.newHeight = shrinkable.newHeight - toSubtract + remainingHeight = remainingHeight + toSubtract + end + end + if remainingHeight < 1 then + -- we could run into floating point shenanigans here + remainingHeight = 0 + end + else + for _, shrinkable in ipairs(shrinkables) do + if shrinkable.newHeight == biggest then + local toSubtract = math.min(heightToSubtract, shrinkable.newHeight - shrinkable.minHeight) + shrinkable.newHeight = shrinkable.newHeight - toSubtract + remainingHeight = remainingHeight + toSubtract + end end end - end - for i = #growables, 1, -1 do - local growable = growables[i] - if growable.newHeight >= growable.maxHeight then - table.remove(growables, i) + for i = #shrinkables, 1, -1 do + local shrinkable = shrinkables[i] + if shrinkable.newHeight <= shrinkable.minHeight then + table.remove(shrinkables, i) + end end end end From e8bb9df30a3331a5251c3d915b44743d03a7c894 Mon Sep 17 00:00:00 2001 From: Endaris Date: Tue, 10 Jun 2025 17:17:50 +0200 Subject: [PATCH 42/49] 4-slice the cursor image and draw the cursor properly --- client/src/mods/Theme.lua | 4 +-- client/src/scenes/DesignHelper.lua | 4 +-- client/src/ui/CharacterButton.lua | 2 ++ client/src/ui/ImageCursor.lua | 51 ++++++++++++++++++++++++---- client/src/ui/Layouts/FlexLayout.lua | 1 + client/src/ui/Layouts/Layout.lua | 7 ++-- client/src/ui/init.lua | 6 ---- 7 files changed, 55 insertions(+), 20 deletions(-) diff --git a/client/src/mods/Theme.lua b/client/src/mods/Theme.lua index ecfcf85e..c33cf06a 100644 --- a/client/src/mods/Theme.lua +++ b/client/src/mods/Theme.lua @@ -898,7 +898,7 @@ end ---@param index integer? ---@return love.Texture[] -function Theme:getGridCursor(index) +function Theme:getCursorImages(index) index = index or 1 if not (self.images.IMG_char_sel_cursors and self.images.IMG_char_sel_cursors[index]) then loadGridCursors(self) @@ -1098,7 +1098,7 @@ end function Theme:getSelectionAssetPack(index) local pack = { playerNumberIcon = self:getPlayerNumberIcon(index), - gridCursor = self:getGridCursor(index), + gridCursor = self:getCursorImages(index), } return pack diff --git a/client/src/scenes/DesignHelper.lua b/client/src/scenes/DesignHelper.lua index 7062b3e8..7db350e1 100644 --- a/client/src/scenes/DesignHelper.lua +++ b/client/src/scenes/DesignHelper.lua @@ -70,7 +70,7 @@ local function createPanelSetSelect(scene) childGap = 8, minHeight = 400, hFill = true, - vFill = true, + --vFill = true, maxHeight = 800, hAlign = "center", onYield = function (self) @@ -263,7 +263,7 @@ function DesignHelper:load() self.uiRoot:addChild(self.subSelection) - self.cursor = ui.ImageCursor(self.uiRoot, nil, GAME.theme:getGridCursor(1)[1]) + self.cursor = ui.ImageCursor(self.uiRoot, nil, GAME.theme:getCursorImages(1)) end function DesignHelper:loadRankedSelection(width) diff --git a/client/src/ui/CharacterButton.lua b/client/src/ui/CharacterButton.lua index 76f4a498..4485ff8b 100644 --- a/client/src/ui/CharacterButton.lua +++ b/client/src/ui/CharacterButton.lua @@ -210,7 +210,9 @@ function CharacterButton:drawSelf() GraphicsUtil.setShader() end + GraphicsUtil.setColor(1, 1, 1, 0.4) GraphicsUtil.drawRectangle("line", 0, 0, self.width, self.height) + GraphicsUtil.setColor(1, 1, 1, 1) love.graphics.pop() end diff --git a/client/src/ui/ImageCursor.lua b/client/src/ui/ImageCursor.lua index 9ed180dd..fe60a551 100644 --- a/client/src/ui/ImageCursor.lua +++ b/client/src/ui/ImageCursor.lua @@ -7,22 +7,59 @@ local input = require("client.src.inputManager") ---@class ImageCursor : Cursor ----@field image love.Texture +---@field images love.Texture[] +---@field framesPerImage number +---@field private imageWidth integer +---@field private imageHeight integer +---@field quads table +---@field thickness integer +---@field blinking boolean local ImageCursor = class( -function (self, target, keyInput, image) - self.image = image +function (self, target, keyInput, cursorImages) + self.images = cursorImages + self.imageWidth, self.imageHeight = self.images[1]:getDimensions() + local quadWidth = self.imageWidth / 2 + local quadHeight = self.imageHeight / 2 + self.quads = { + topLeft = GraphicsUtil:newRecycledQuad(0, 0, quadWidth, quadHeight, self.imageWidth, self.imageHeight), + topRight = GraphicsUtil:newRecycledQuad(quadWidth, 0, quadWidth, quadHeight, self.imageWidth, self.imageHeight), + bottomLeft = GraphicsUtil:newRecycledQuad(0, quadHeight, quadWidth, quadHeight, self.imageWidth, self.imageHeight), + bottomRight = GraphicsUtil:newRecycledQuad(quadWidth, quadHeight, quadWidth, quadHeight, self.imageWidth, self.imageHeight) + } + self.thickness = 3 + + self.framesPerImage = 8 + self.blinking = false end, Cursor) function ImageCursor:draw() - -- GraphicsUtil.setColor(0, 0, 0, 0.2) - -- love.graphics.rectangle("fill", self.target.x, self.target.y, self.target.width, self.target.height) if self.focused then local uiElement = self.focusToHover[self.focused] GraphicsUtil.setColor(1, 1, 1, 1) local x, y = uiElement:getScreenPos() - local imageWidth, imageHeight = self.image:getDimensions() - GraphicsUtil.draw(self.image, x, y, 0, uiElement.width / imageWidth, uiElement.height / imageHeight) + + local image + if not self.blinking then + local n = consts.FRAME_RATE * self.framesPerImage + local imageIndex = math.ceil((love.timer.getTime() % (n * #self.images)) / n) + image = self.images[imageIndex] + else + -- TODO: Player offset blinking so that cursors blink in turns + local playerNumber = 1 + if (math.floor(math.round(love.timer.getTime() / consts.FRAME_RATE) / self.framesPerImage) + playerNumber) % 2 + 1 == playerNumber then + return + else + image = self.images[1] + end + end + + local scale = math.min(uiElement.width / self.imageWidth, uiElement.height / self.imageHeight) + local thickness = math.round(self.thickness * scale) + GraphicsUtil.drawQuad(image, self.quads.topLeft, x - thickness, y - thickness, 0, scale) + GraphicsUtil.drawQuad(image, self.quads.topRight, x + uiElement.width + thickness, y - thickness, 0, scale, scale, self.imageWidth / 2) + GraphicsUtil.drawQuad(image, self.quads.bottomLeft, x - thickness, y + uiElement.height / 2 + thickness, 0, scale) + GraphicsUtil.drawQuad(image, self.quads.bottomRight, x + uiElement.width + thickness, y + uiElement.height / 2 + thickness, 0, scale, scale, self.imageWidth / 2) end end diff --git a/client/src/ui/Layouts/FlexLayout.lua b/client/src/ui/Layouts/FlexLayout.lua index 2605f91a..49687ecb 100644 --- a/client/src/ui/Layouts/FlexLayout.lua +++ b/client/src/ui/Layouts/FlexLayout.lua @@ -53,6 +53,7 @@ function FlexLayout.setHeight(uiElement, height) if not uiElement.newHeight then uiElement.layout.fitSizeHeight(uiElement) end + local minHeight = uiElement.layout.getMinHeight(uiElement) if height then uiElement.height = math.min(math.max(height, uiElement.newHeight), uiElement.maxHeight) else diff --git a/client/src/ui/Layouts/Layout.lua b/client/src/ui/Layouts/Layout.lua index bb58a410..2e0268cd 100644 --- a/client/src/ui/Layouts/Layout.lua +++ b/client/src/ui/Layouts/Layout.lua @@ -52,9 +52,10 @@ end ---@param uiElement UiElement function Layout.updateHeights(uiElement, height) - if uiElement.debug then - local phi = 5 - end + -- about the most reasonable way to troubleshoot why a certain element is laid out in an expected way + -- if uiElement.debug then + -- local phi = 5 + -- end uiElement.layout.setHeight(uiElement, height) uiElement.layout.finalizeChildrenHeights(uiElement) diff --git a/client/src/ui/init.lua b/client/src/ui/init.lua index 6b71f88a..8eb0c7ad 100644 --- a/client/src/ui/init.lua +++ b/client/src/ui/init.lua @@ -7,18 +7,12 @@ that way "Go to source" on an import of ui elsewhere will lead to the respective the "./" is assumed given for relative paths but it's still a path so adding the extension is necessary when addressing files in subdirectories (layouts) use forward slashes as the path separator https://luals.github.io/wiki/annotations/#source - -also tag with ----@type -so that you get intellisense ]] local ui = { ---@source BoolSelector.lua - ---@type BoolSelector BoolSelector = import("./BoolSelector"), ---@source Button.lua - ---@type Button Button = import("./Button"), ---@source ButtonGroup.lua ButtonGroup = import("./ButtonGroup"), From 2b800aff9f31ca5cbd598ae251d5c60664fe96af Mon Sep 17 00:00:00 2001 From: Endaris Date: Tue, 10 Jun 2025 19:37:14 +0200 Subject: [PATCH 43/49] fix vertical resizing for preferred vs min height apply some of Jam's feedback in comments / var renames 4-slice the image cursor and draw it with maintained aspect ratio --- client/src/scenes/DesignHelper.lua | 4 --- client/src/scenes/Scene.lua | 2 +- client/src/ui/ImageContainer.lua | 2 -- client/src/ui/Layouts/FlexLayout.lua | 13 +++++++--- .../src/ui/Layouts/HorizontalFlexLayout.lua | 19 +++++++++++--- client/src/ui/Layouts/Layout.lua | 13 +++++++--- client/src/ui/Layouts/VerticalFlexLayout.lua | 26 ++++++++++++++----- .../src/ui/Layouts/VerticalScrollLayout.lua | 2 +- client/src/ui/UIElement.lua | 10 ++++++- client/src/ui/UniSizedContainer.lua | 4 +++ client/src/ui/VerticalMenu.lua | 10 +++++++ 11 files changed, 79 insertions(+), 26 deletions(-) diff --git a/client/src/scenes/DesignHelper.lua b/client/src/scenes/DesignHelper.lua index 7db350e1..11dcd700 100644 --- a/client/src/scenes/DesignHelper.lua +++ b/client/src/scenes/DesignHelper.lua @@ -245,8 +245,6 @@ function DesignHelper:load() subSelectionSelector:addChild(readyButton) subSelectionSelector:addChild(leaveButton) - --passThroughSelector:addChild(subSelectionSelector) - --self.uiRoot:addChild(passThroughSelector) self.uiRoot:addChild(subSelectionSelector) self.subSelection = ui.UiElement({ @@ -259,8 +257,6 @@ function DesignHelper:load() }) self.subSelection.debug = true - ui.CursorInteractable(self.subSelection, function() end) - self.uiRoot:addChild(self.subSelection) self.cursor = ui.ImageCursor(self.uiRoot, nil, GAME.theme:getCursorImages(1)) diff --git a/client/src/scenes/Scene.lua b/client/src/scenes/Scene.lua index bcebd2e2..368a4fc2 100644 --- a/client/src/scenes/Scene.lua +++ b/client/src/scenes/Scene.lua @@ -34,7 +34,7 @@ local Scene = class( vAlign = "center", }) self.uiRoot.controlsWindow = true - self.uiRoot.debug = true + -- self.uiRoot.debug = true -- scenes may specify theme music to use that is played once they are switched to -- eligible labels: -- main diff --git a/client/src/ui/ImageContainer.lua b/client/src/ui/ImageContainer.lua index 62ae1da9..a319e445 100644 --- a/client/src/ui/ImageContainer.lua +++ b/client/src/ui/ImageContainer.lua @@ -40,8 +40,6 @@ end function ImageContainer:onResized() self.scale = math.min(self.width / self.imageWidth, self.height / self.imageHeight) - -- self.width = self.imageWidth * self.scale - -- self.height = self.imageHeight * self.scale end function ImageContainer:drawSelf() diff --git a/client/src/ui/Layouts/FlexLayout.lua b/client/src/ui/Layouts/FlexLayout.lua index 49687ecb..a0bbab76 100644 --- a/client/src/ui/Layouts/FlexLayout.lua +++ b/client/src/ui/Layouts/FlexLayout.lua @@ -23,8 +23,8 @@ function FlexLayout.fitSizeHeight(uiElement) child.layout.fitSizeHeight(child) end end - local h = uiElement.layout.getMinHeight(uiElement) - uiElement.newHeight = math.max(h, uiElement.minHeight) + local h = uiElement.layout.getPreferredHeight(uiElement) + uiElement.newHeight = math.max(h, uiElement:getPreferredHeight(), uiElement.minHeight) end function FlexLayout.setWidth(uiElement, width) @@ -53,11 +53,16 @@ function FlexLayout.setHeight(uiElement, height) if not uiElement.newHeight then uiElement.layout.fitSizeHeight(uiElement) end + local minHeight = uiElement.layout.getMinHeight(uiElement) if height then - uiElement.height = math.min(math.max(height, uiElement.newHeight), uiElement.maxHeight) + if height > minHeight then + uiElement.height = util.bound(minHeight, uiElement.newHeight, height) + else + uiElement.height = minHeight + end else - uiElement.height = math.min(uiElement.newHeight, uiElement.maxHeight) + uiElement.height = util.bound(minHeight, uiElement.newHeight, uiElement.maxHeight) end uiElement.newHeight = nil end diff --git a/client/src/ui/Layouts/HorizontalFlexLayout.lua b/client/src/ui/Layouts/HorizontalFlexLayout.lua index fb1359a5..bdad792d 100644 --- a/client/src/ui/Layouts/HorizontalFlexLayout.lua +++ b/client/src/ui/Layouts/HorizontalFlexLayout.lua @@ -33,15 +33,15 @@ end ---@param uiElement UiElement function HorizontalFlexLayout.getMinHeight(uiElement) local h = uiElement.padding * 2 - local maxHeight = 0 + local maxChildHeight = 0 for _, child in ipairs(uiElement.children) do if child.isVisible then - maxHeight = math.max(maxHeight, child.newHeight) + maxChildHeight = math.max(maxChildHeight, child.newHeight) end end - h = h + maxHeight + h = h + maxChildHeight if uiElement.getMinHeight then return math.max(h, uiElement:getMinHeight()) @@ -50,6 +50,19 @@ function HorizontalFlexLayout.getMinHeight(uiElement) end end +function HorizontalFlexLayout.getPreferredHeight(uiElement) + local h = uiElement.padding * 2 + local maxChildHeight = 0 + + for _, child in ipairs(uiElement.children) do + if child.isVisible then + maxChildHeight = math.max(maxChildHeight, child.layout.getMinHeight(child), child:getPreferredHeight()) + end + end + + return h + maxChildHeight +end + local growables = {} local shrinkables = {} diff --git a/client/src/ui/Layouts/Layout.lua b/client/src/ui/Layouts/Layout.lua index 2e0268cd..3954da15 100644 --- a/client/src/ui/Layouts/Layout.lua +++ b/client/src/ui/Layouts/Layout.lua @@ -53,9 +53,9 @@ end ---@param uiElement UiElement function Layout.updateHeights(uiElement, height) -- about the most reasonable way to troubleshoot why a certain element is laid out in an expected way - -- if uiElement.debug then - -- local phi = 5 - -- end + if uiElement.debug then + local phi = 5 + end uiElement.layout.setHeight(uiElement, height) uiElement.layout.finalizeChildrenHeights(uiElement) @@ -106,5 +106,12 @@ function Layout.getPreferredWidth(uiElement) error("Layout does not implement getPreferredWidth") end +---@param uiElement UiElement +---@return number # the preferred height of the element based on the preferred heights of its children +function Layout.getPreferredHeight(uiElement) + error("Layout does not implement getPreferredHeight") +end + + return Layout \ No newline at end of file diff --git a/client/src/ui/Layouts/VerticalFlexLayout.lua b/client/src/ui/Layouts/VerticalFlexLayout.lua index 769cc324..f1b13ebb 100644 --- a/client/src/ui/Layouts/VerticalFlexLayout.lua +++ b/client/src/ui/Layouts/VerticalFlexLayout.lua @@ -8,28 +8,28 @@ local VerticalFlexLayout = setmetatable({characteristic = "vertical"}, {__index ---@return number # the minimum width of the element as dictated by its children function VerticalFlexLayout.getMinWidth(uiElement) local w = uiElement.padding * 2 - local maxWidth = 0 + local maxChildWidth = 0 for _, child in ipairs(uiElement.children) do if child.isVisible then - maxWidth = math.max(maxWidth, child.layout.getMinWidth(child)) + maxChildWidth = math.max(maxChildWidth, child.layout.getMinWidth(child)) end end - return w + maxWidth + return w + maxChildWidth end function VerticalFlexLayout.getPreferredWidth(uiElement) local w = uiElement.padding * 2 - local maxWidth = 0 + local maxChildWidth = 0 for _, child in ipairs(uiElement.children) do if child.isVisible then - maxWidth = math.max(maxWidth, child.layout.getMinWidth(child), child:getPreferredWidth()) + maxChildWidth = math.max(maxChildWidth, child.layout.getMinWidth(child), child:getPreferredWidth()) end end - return w + maxWidth + return w + maxChildWidth end ---@param uiElement UiElement @@ -38,7 +38,7 @@ function VerticalFlexLayout.getMinHeight(uiElement) for _, child in ipairs(uiElement.children) do if child.isVisible then - h = h + child.newHeight + uiElement.childGap + h = h + math.max(child.layout.getMinHeight(child), child.minHeight) + uiElement.childGap end end @@ -51,6 +51,18 @@ function VerticalFlexLayout.getMinHeight(uiElement) end end +function VerticalFlexLayout.getPreferredHeight(uiElement) + local h = uiElement.padding * 2 + uiElement.childGap * (#uiElement.children - 1) + + for _, child in ipairs(uiElement.children) do + if child.isVisible then + h = h + math.max(child.layout.getMinHeight(child), child:getPreferredHeight()) + end + end + + return h +end + local growables = {} local shrinkables = {} diff --git a/client/src/ui/Layouts/VerticalScrollLayout.lua b/client/src/ui/Layouts/VerticalScrollLayout.lua index 5bd958d9..0f142ed6 100644 --- a/client/src/ui/Layouts/VerticalScrollLayout.lua +++ b/client/src/ui/Layouts/VerticalScrollLayout.lua @@ -6,7 +6,7 @@ local util = require("common.lib.util") local VerticalScrollLayout = setmetatable({}, {__index = VerticalFlexLayout}) function VerticalScrollLayout.getMinHeight(uiElement) - return util.bound(uiElement.minHeight, VerticalFlexLayout.getMinHeight(uiElement), uiElement.maxHeight) + return uiElement.minHeight end function VerticalScrollLayout.finalizeChildrenHeights(uiElement) diff --git a/client/src/ui/UIElement.lua b/client/src/ui/UIElement.lua index 3168d11d..e92db792 100644 --- a/client/src/ui/UIElement.lua +++ b/client/src/ui/UIElement.lua @@ -128,8 +128,10 @@ function UIElement:onChildrenChanged() if self.controlsWindow then --if not DEBUG_ENABLED then - self.layout.resize(self) + -- we want to keep window size if possible + self.layout.resize(self, self.width, self.height) --end + -- but if it changed (due to the min size increasing with the added element), resize the window so everything fits if oWidth ~= self.width or oHeight ~= self.height then GraphicsUtil.updateMode(self.width, self.height, {}) end @@ -282,4 +284,10 @@ function UIElement:getPreferredWidth() return self.minWidth end +--- defaults to minHeight; elements that can wrap have to override this getter with a function returning their preferred height +---@return integer # the height the UIElement would prefer to take up if any space is available +function UIElement:getPreferredHeight() + return self.minHeight +end + return UIElement \ No newline at end of file diff --git a/client/src/ui/UniSizedContainer.lua b/client/src/ui/UniSizedContainer.lua index f8469358..2d98d3ef 100644 --- a/client/src/ui/UniSizedContainer.lua +++ b/client/src/ui/UniSizedContainer.lua @@ -13,6 +13,10 @@ local tableUtils = require("common.lib.tableUtils") ---@field childrenWidth integer ---@field childrenHeight integer +-- a layout and navigation focused container that arranges children horizontally
+-- when not enough width available it will automatically wrap around further elements to the next row
+-- due to all children being forced to the same size, it can automatically adjust navigation to offer grid-like navigation +-- but it's not a real grid! ---@class UniSizedContainer : UiElement, CursorNavigable ---@operator call(UniSizedContainerOptions): UniSizedContainer ---@field childrenWidth integer diff --git a/client/src/ui/VerticalMenu.lua b/client/src/ui/VerticalMenu.lua index 38dcd9b5..15c6071e 100644 --- a/client/src/ui/VerticalMenu.lua +++ b/client/src/ui/VerticalMenu.lua @@ -3,6 +3,7 @@ local ScrollContainer = import("./ScrollContainer") local class = require("common.lib.class") local util = require("common.lib.util") local GraphicsUtil = require("client.src.graphics.graphics_util") +local VerticalFlexLayout = import("./Layouts.VerticalFlexLayout") local VerticalScrollLayout = import("./Layouts.VerticalScrollLayout") local tableUtils = require("common.lib.tableUtils") local addCursorNavigationInterface = import("./CursorNavigable") @@ -120,4 +121,13 @@ function VerticalMenu:drawChildren() end end +function VerticalMenu:onResized() + ScrollContainer.onResized(self) + for _, child in ipairs(self.children) do + for cursor, _ in pairs(child.hoveringCursors) do + self:keepVisible(cursor) + end + end +end + return VerticalMenu \ No newline at end of file From ad10ad2961cc4621d4e1dd82e0bec2a865feb9f2 Mon Sep 17 00:00:00 2001 From: Endaris Date: Tue, 10 Jun 2025 22:28:59 +0200 Subject: [PATCH 44/49] make onChildrenChanged local --- client/src/ui/UIElement.lua | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/client/src/ui/UIElement.lua b/client/src/ui/UIElement.lua index e92db792..fcf76119 100644 --- a/client/src/ui/UIElement.lua +++ b/client/src/ui/UIElement.lua @@ -30,6 +30,7 @@ local GraphicsUtil = require("client.src.graphics.graphics_util") ---@field onRelease function? touch callback for when the mouse touching the element is released ---@field onHold function? touch callback for when a touch is held on the element for a longer duration ---@field onResized function? layout callback for when a UIElement and all of its children have been resized and positioned +---@field onDetach function? callback for when a UIElement detaches from its parent (aka the parent loses its child) ---@field [any] any ---@class UiElementOptions @@ -112,11 +113,12 @@ local UIElement = class( UIElement.TYPE = "UIElement" -function UIElement:onChildrenChanged() +---@param self UiElement +local function onChildrenChanged(self) if self.parent then -- resizing segments does not make much sense if the segments have to be resized later anyway -- so bubble just straight up - self.parent:onChildrenChanged() + onChildrenChanged(self.parent) else -- and then only resize from the root element local oWidth = self.width @@ -153,7 +155,7 @@ function UIElement:addChild(uiElement, index) self.children[#self.children + 1] = uiElement end uiElement.parent = self - self:onChildrenChanged() + onChildrenChanged(self) end end @@ -162,8 +164,10 @@ function UIElement:detach() for i, child in ipairs(self.parent.children) do if child.id == self.id then table.remove(self.parent.children, i) - self:onDetach() - self.parent:onChildrenChanged() + if self.onDetach then + self:onDetach() + end + onChildrenChanged(self.parent) self.parent = nil break end @@ -172,9 +176,6 @@ function UIElement:detach() end end -function UIElement:onDetach() -end - ---@param whoIsAsking table? ---@return integer x ---@return integer y @@ -242,7 +243,7 @@ end function UIElement:onVisibilityChanged() if self.parent then - self.parent:onChildrenChanged() + onChildrenChanged(self.parent) end end @@ -290,4 +291,14 @@ function UIElement:getPreferredHeight() return self.minHeight end +--- Use this function to change the layout of an UIElement after its creation
+--- will trigger a layout update of the UI tree +---@param layout Layout +function UIElement:setLayout(layout) + if self.layout ~= layout then + self.layout = layout + onChildrenChanged(self.parent) + end +end + return UIElement \ No newline at end of file From b514bd19b13ee0484e73aa227bf8c0e14058a3ca Mon Sep 17 00:00:00 2001 From: Endaris Date: Tue, 10 Jun 2025 22:42:11 +0200 Subject: [PATCH 45/49] change UIElement:drawSelf and all its override to assume 0, 0 offset and move the translate in UIElement:draw --- client/src/ui/BoolSelector.lua | 2 +- client/src/ui/Button.lua | 4 ++-- client/src/ui/Carousel.lua | 2 +- client/src/ui/CharacterButton.lua | 3 --- client/src/ui/Grid.lua | 14 +++++++------- client/src/ui/GridElement.lua | 2 +- client/src/ui/ImageContainer.lua | 4 ++-- client/src/ui/InputField.lua | 12 ++++++------ client/src/ui/Label.lua | 2 +- client/src/ui/LevelSlider.lua | 4 ++-- client/src/ui/MenuItem.lua | 8 ++++---- client/src/ui/PagedUniGrid.lua | 2 +- client/src/ui/PanelSetButton.lua | 4 ---- client/src/ui/PixelFontLabel.lua | 4 ++-- client/src/ui/ScrollContainer.lua | 2 +- client/src/ui/Slider.lua | 12 ++++++------ client/src/ui/StackPanel.lua | 2 +- client/src/ui/Stepper.lua | 4 ++-- client/src/ui/UIElement.lua | 16 ++++++++-------- client/src/ui/ValueLabel.lua | 2 +- 20 files changed, 49 insertions(+), 56 deletions(-) diff --git a/client/src/ui/BoolSelector.lua b/client/src/ui/BoolSelector.lua index d098b626..a4ae64ca 100644 --- a/client/src/ui/BoolSelector.lua +++ b/client/src/ui/BoolSelector.lua @@ -81,7 +81,7 @@ local fakeCenteredChild = {hAlign = "center", vAlign = "center", width = totalWi function BoolSelector:drawSelf() if DEBUG_ENABLED then GraphicsUtil.setColor(0, 0, 1, 1) - GraphicsUtil.drawRectangle("line", self.x + 1, self.y + 1, self.width - 2, self.height - 2) + GraphicsUtil.drawRectangle("line", 1, 1, self.width - 2, self.height - 2) GraphicsUtil.setColor(1, 1, 1, 1) end diff --git a/client/src/ui/Button.lua b/client/src/ui/Button.lua index 66b465b2..61d17200 100644 --- a/client/src/ui/Button.lua +++ b/client/src/ui/Button.lua @@ -62,13 +62,13 @@ function Button:drawBackground() else GraphicsUtil.setColor(self.backgroundColor) end - GraphicsUtil.drawRectangle("fill", self.x, self.y, self.width, self.height) + GraphicsUtil.drawRectangle("fill", 0, 0, self.width, self.height) GraphicsUtil.setColor(1, 1, 1, 1) end function Button:drawOutline() GraphicsUtil.setColor(self.outlineColor) - GraphicsUtil.drawRectangle("line", self.x, self.y, self.width, self.height) + GraphicsUtil.drawRectangle("line", 0, 0, self.width, self.height) GraphicsUtil.setColor(1, 1, 1, 1) end diff --git a/client/src/ui/Carousel.lua b/client/src/ui/Carousel.lua index 34edea78..409bc9e4 100644 --- a/client/src/ui/Carousel.lua +++ b/client/src/ui/Carousel.lua @@ -88,7 +88,7 @@ end function Carousel:drawSelf() if DEBUG_ENABLED then - GraphicsUtil.drawRectangle("line", self.x, self.y, self.width, self.height) + GraphicsUtil.drawRectangle("line", 0, 0, self.width, self.height) end end diff --git a/client/src/ui/CharacterButton.lua b/client/src/ui/CharacterButton.lua index 4485ff8b..52230d1a 100644 --- a/client/src/ui/CharacterButton.lua +++ b/client/src/ui/CharacterButton.lua @@ -170,8 +170,6 @@ function CharacterButton:drawSelf() local imageWidth, imageHeight = self.characterIcon:getDimensions() local scale = math.min(self.width / imageWidth, self.height / imageHeight) - love.graphics.push("transform") - love.graphics.translate(self.x, self.y) GraphicsUtil.draw(self.characterIcon, 0, 0, 0, scale, scale) GraphicsUtil.printf(self.displayName, 0, 0, self.width, "center", nil, nil, getFontSizeByScale(self.width / CharacterButton.standardSize)) @@ -213,7 +211,6 @@ function CharacterButton:drawSelf() GraphicsUtil.setColor(1, 1, 1, 0.4) GraphicsUtil.drawRectangle("line", 0, 0, self.width, self.height) GraphicsUtil.setColor(1, 1, 1, 1) - love.graphics.pop() end return CharacterButton \ No newline at end of file diff --git a/client/src/ui/Grid.lua b/client/src/ui/Grid.lua index 9e811025..971a611e 100644 --- a/client/src/ui/Grid.lua +++ b/client/src/ui/Grid.lua @@ -77,18 +77,18 @@ end function Grid:drawSelf() if DEBUG_ENABLED then GraphicsUtil.setColor(1, 1, 1, 0.5) - GraphicsUtil.drawRectangle("line", self.x, self.y, self.width, self.height) + GraphicsUtil.drawRectangle("line", 0, 0, self.width, self.height) GraphicsUtil.setColor(1, 1, 1, 1) -- draw all units - local right = self.x + self.width - local bottom = self.y + self.height + local right = self.width + local bottom = self.height for i = 1, self.gridHeight - 1 do - local y = self.y + self.unitSize * i - GraphicsUtil.drawStraightLine(self.x, y, right, y, 1, 1, 1, 0.5) + local y = self.unitSize * i + GraphicsUtil.drawStraightLine(0, y, right, y, 1, 1, 1, 0.5) end for i = 1, self.gridWidth - 1 do - local x = self.x + self.unitSize * i - GraphicsUtil.drawStraightLine(x, self.y, x, bottom, 1, 1, 1, 0.5) + local x = self.unitSize * i + GraphicsUtil.drawStraightLine(x, 0, x, bottom, 1, 1, 1, 0.5) end end end diff --git a/client/src/ui/GridElement.lua b/client/src/ui/GridElement.lua index 329d49eb..da4539a5 100644 --- a/client/src/ui/GridElement.lua +++ b/client/src/ui/GridElement.lua @@ -29,7 +29,7 @@ GridElement.layout = StaticLayout function GridElement:drawSelf() if self.drawBorders then - GraphicsUtil.drawRectangle("line", self.x, self.y, self.width, self.height) + GraphicsUtil.drawRectangle("line", 0, 0, self.width, self.height) end end diff --git a/client/src/ui/ImageContainer.lua b/client/src/ui/ImageContainer.lua index a319e445..25fde6b0 100644 --- a/client/src/ui/ImageContainer.lua +++ b/client/src/ui/ImageContainer.lua @@ -43,12 +43,12 @@ function ImageContainer:onResized() end function ImageContainer:drawSelf() - GraphicsUtil.draw(self.image, self.x, self.y, 0, self.scale, self.scale) + GraphicsUtil.draw(self.image, 0, 0, 0, self.scale, self.scale) if self.drawBorders then -- border is just drawn on top, not around GraphicsUtil.setColor(self.outlineColor) - GraphicsUtil.drawRectangle("line", self.x, self.y, self.width, self.height) + GraphicsUtil.drawRectangle("line", 0, 0, self.width, self.height) GraphicsUtil.setColor(1, 1, 1, 1) end end diff --git a/client/src/ui/InputField.lua b/client/src/ui/InputField.lua index 1163975c..839e43af 100644 --- a/client/src/ui/InputField.lua +++ b/client/src/ui/InputField.lua @@ -69,12 +69,12 @@ end function InputField:getCursorPos() if self.offset == 0 then - return self.x + textOffset + return textOffset end local byteoffset = utf8.offset(self.value, self.offset) local text = string.sub(self.value, 1, byteoffset) - return self.x + textOffset + GraphicsUtil.newText(love.graphics.getFont(), text):getWidth() + return textOffset + GraphicsUtil.newText(love.graphics.getFont(), text):getWidth() end function InputField:unfocus() @@ -138,22 +138,22 @@ local valueColor = {1, 1, 1, 1} local placeholderColor = {.5, .5, .5, 1} function InputField:drawSelf() GraphicsUtil.setColor(self.outlineColor) - GraphicsUtil.drawRectangle("line", self.x, self.y, self.width, self.height) + GraphicsUtil.drawRectangle("line", 0, 0, self.width, self.height) GraphicsUtil.setColor(self.backgroundColor) - GraphicsUtil.drawRectangle("fill", self.x, self.y, self.width, self.height) + GraphicsUtil.drawRectangle("fill", 0, 0, self.width, self.height) local text = self.value ~= "" and self.text or self.placeholderText local textColor = self.value ~= "" and valueColor or placeholderColor local textHeight = text:getHeight() GraphicsUtil.setColor(textColor) - GraphicsUtil.draw(text, self.x + textOffset, self.y + (self.height - textHeight) / 2, 0, 1, 1) + GraphicsUtil.draw(text, textOffset, (self.height - textHeight) / 2, 0, 1, 1) if self.hasFocus then local cursorFlashPeriod = .5 if (math.floor(love.timer.getTime() / cursorFlashPeriod)) % 2 == 0 then GraphicsUtil.setColor(1, 1, 1, 1) - GraphicsUtil.draw(textCursor, self:getCursorPos(), self.y + (self.height - textHeight) / 2, 0, 1, 1) + GraphicsUtil.draw(textCursor, self:getCursorPos(), (self.height - textHeight) / 2, 0, 1, 1) end end GraphicsUtil.setColor(1, 1, 1, 1) diff --git a/client/src/ui/Label.lua b/client/src/ui/Label.lua index 11b3855b..fc1b3caf 100644 --- a/client/src/ui/Label.lua +++ b/client/src/ui/Label.lua @@ -106,7 +106,7 @@ function Label:refreshLocalization() end function Label:drawSelf() - GraphicsUtil.printf(self.text, self.x, self.y, self.width, self.hAlign, nil, nil, self.fontSize) + GraphicsUtil.printf(self.text, 0, 0, self.width, self.hAlign, nil, nil, self.fontSize) end function Label:getPreferredWidth() diff --git a/client/src/ui/LevelSlider.lua b/client/src/ui/LevelSlider.lua index 99b3c374..1b5533b6 100644 --- a/client/src/ui/LevelSlider.lua +++ b/client/src/ui/LevelSlider.lua @@ -35,10 +35,10 @@ function LevelSlider:drawSelf() for i, level_img in ipairs(themes[config.theme].images.IMG_levels) do local img = i <= self.value and level_img or themes[config.theme].images.IMG_levels_unfocus[i] - GraphicsUtil.draw(img, self.x + (i - 1) * self.tickLength, self.y, 0, self.tickLength / img:getWidth(), self.tickLength / img:getHeight(), 0, 0) + GraphicsUtil.draw(img, (i - 1) * self.tickLength, 0, 0, self.tickLength / img:getWidth(), self.tickLength / img:getHeight(), 0, 0) end local cursor_image = themes[config.theme].images.IMG_level_cursor - GraphicsUtil.draw(cursor_image, self.x + (self.value - 1 + .5) * self.tickLength, self.y + self.tickLength, 0, 1, 1, cursor_image:getWidth() / 2, 0) + GraphicsUtil.draw(cursor_image, (self.value - 1 + .5) * self.tickLength, self.tickLength, 0, 1, 1, cursor_image:getWidth() / 2, 0) end return LevelSlider \ No newline at end of file diff --git a/client/src/ui/MenuItem.lua b/client/src/ui/MenuItem.lua index 6d89660a..24b6e025 100644 --- a/client/src/ui/MenuItem.lua +++ b/client/src/ui/MenuItem.lua @@ -183,11 +183,11 @@ function MenuItem:drawSelf() local selectedAdditionalOpacity = 0.5 local fillOpacity = (math.cos(6 * love.timer.getTime()) + 1) / 16 + baseOpacity + selectedAdditionalOpacity local borderOpacity = (math.cos(6 * love.timer.getTime()) + 1) / 4 + baseOpacity + selectedAdditionalOpacity - GraphicsUtil.drawRectangle("fill", self.x, self.y, self.width, self.height, SELECTED_BACKGROUND_COLOR[1], SELECTED_BACKGROUND_COLOR[2], SELECTED_BACKGROUND_COLOR[3], fillOpacity) - GraphicsUtil.drawRectangle("line", self.x, self.y, self.width, self.height, SELECTED_BORDER_COLOR[1], SELECTED_BORDER_COLOR[2], SELECTED_BORDER_COLOR[3], borderOpacity) + GraphicsUtil.drawRectangle("fill", 0, 0, self.width, self.height, SELECTED_BACKGROUND_COLOR[1], SELECTED_BACKGROUND_COLOR[2], SELECTED_BACKGROUND_COLOR[3], fillOpacity) + GraphicsUtil.drawRectangle("line", 0, 0, self.width, self.height, SELECTED_BORDER_COLOR[1], SELECTED_BORDER_COLOR[2], SELECTED_BORDER_COLOR[3], borderOpacity) else - GraphicsUtil.drawRectangle("fill", self.x, self.y, self.width, self.height, DEFAULT_BACKGROUND_COLOR[1], DEFAULT_BACKGROUND_COLOR[2], DEFAULT_BACKGROUND_COLOR[3], baseOpacity) - GraphicsUtil.drawRectangle("line", self.x, self.y, self.width, self.height, DEFAULT_BORDER_COLOR[1], DEFAULT_BORDER_COLOR[2], DEFAULT_BORDER_COLOR[3], baseOpacity) + GraphicsUtil.drawRectangle("fill", 0, 0, self.width, self.height, DEFAULT_BACKGROUND_COLOR[1], DEFAULT_BACKGROUND_COLOR[2], DEFAULT_BACKGROUND_COLOR[3], baseOpacity) + GraphicsUtil.drawRectangle("line", 0, 0, self.width, self.height, DEFAULT_BORDER_COLOR[1], DEFAULT_BORDER_COLOR[2], DEFAULT_BORDER_COLOR[3], baseOpacity) end end diff --git a/client/src/ui/PagedUniGrid.lua b/client/src/ui/PagedUniGrid.lua index 9984a305..8ce7ee2d 100644 --- a/client/src/ui/PagedUniGrid.lua +++ b/client/src/ui/PagedUniGrid.lua @@ -103,7 +103,7 @@ end function PagedUniGrid:drawSelf() if DEBUG_ENABLED then GraphicsUtil.setColor(1, 0, 0, 1) - GraphicsUtil.drawRectangle("line", self.x, self.y, self.width, self.height) + GraphicsUtil.drawRectangle("line", 0, 0, self.width, self.height) GraphicsUtil.setColor(1, 1, 1, 1) end end diff --git a/client/src/ui/PanelSetButton.lua b/client/src/ui/PanelSetButton.lua index 0f840968..f4c4b6ad 100644 --- a/client/src/ui/PanelSetButton.lua +++ b/client/src/ui/PanelSetButton.lua @@ -55,8 +55,6 @@ end function PanelButton:drawSelf() self.panelSet:prepareDraw() - love.graphics.push("transform") - love.graphics.translate(self.x, self.y) local width = math.floor(self.width / 9) if self:isHovered() then @@ -78,8 +76,6 @@ function PanelButton:drawSelf() self.panelSet:drawBatch() GraphicsUtil.printf(self.panelSet.name or self.panelSet.id, 0, width + 4, self.width, "center", nil, nil, "normal") - - love.graphics.pop() end diff --git a/client/src/ui/PixelFontLabel.lua b/client/src/ui/PixelFontLabel.lua index 2efb067f..3fecf847 100644 --- a/client/src/ui/PixelFontLabel.lua +++ b/client/src/ui/PixelFontLabel.lua @@ -59,10 +59,10 @@ function PixelFontLabel:drawSelf() for i = 1, self.text:len(), 1 do local char = self.text:sub(i, i) if char ~= " " then - local characterX = self.x + ((i - 1) * self.charDistanceScaled) + local characterX = ((i - 1) * self.charDistanceScaled) -- Render it at the proper digit location - GraphicsUtil.drawQuad(self.fontMap.atlas, self.fontMap.charToQuad[char], characterX, self.y, 0, self.xScale, self.yScale) + GraphicsUtil.drawQuad(self.fontMap.atlas, self.fontMap.charToQuad[char], characterX, 0, 0, self.xScale, self.yScale) end end end diff --git a/client/src/ui/ScrollContainer.lua b/client/src/ui/ScrollContainer.lua index b891d4ce..4e53ba74 100644 --- a/client/src/ui/ScrollContainer.lua +++ b/client/src/ui/ScrollContainer.lua @@ -129,7 +129,6 @@ local loveMajor = love.getVersion() function ScrollContainer:draw() if self.isVisible then - UiElement.drawSelf(self) -- make a stencil according to width/height if loveMajor >= 12 then love.graphics.setStencilMode("draw", 1) @@ -151,6 +150,7 @@ function ScrollContainer:draw() else love.graphics.translate(self.x + self.scrollOffset, self.y) end + UiElement.drawSelf(self) -- and then just render everything -- by combining stencil + translate only the elements positioned within the stencil after the translate get drawn self:drawChildren() diff --git a/client/src/ui/Slider.lua b/client/src/ui/Slider.lua index 6b1eb913..04be3d46 100644 --- a/client/src/ui/Slider.lua +++ b/client/src/ui/Slider.lua @@ -161,27 +161,27 @@ function Slider:drawSelf() -- Slider bar GraphicsUtil.setColor(gray, gray, gray, alpha) - GraphicsUtil.drawRectangle("fill", self.x + xPadding, self.y + yPadding + valueTextHeight, barWidth, sliderBarThickness) + GraphicsUtil.drawRectangle("fill", xPadding, yPadding + valueTextHeight, barWidth, sliderBarThickness) -- Slider circle GraphicsUtil.setColor(unpack(SLIDER_CIRCLE_COLOR)) - love.graphics.circle("fill", currentX, self.y + yPadding + valueTextHeight + sliderBarThickness / 2, handleRadius, 32) + love.graphics.circle("fill", currentX, yPadding + valueTextHeight + sliderBarThickness / 2, handleRadius, 32) -- Value background GraphicsUtil.setColor(gray, gray, gray, alpha) - GraphicsUtil.drawRectangle("fill", currentX - valueTextWidth / 2 - valueBackgroundPaddingX, self.y + yPadding - valueBackgroundPaddingY, valueTextWidth + valueBackgroundPaddingX*2, valueTextHeight + valueBackgroundPaddingY*2) + GraphicsUtil.drawRectangle("fill", currentX - valueTextWidth / 2 - valueBackgroundPaddingX, yPadding - valueBackgroundPaddingY, valueTextWidth + valueBackgroundPaddingX*2, valueTextHeight + valueBackgroundPaddingY*2) -- Value centered at top GraphicsUtil.setColor(1, 1, 1, 1) - GraphicsUtil.draw(self.valueText, currentX - valueTextWidth / 2, self.y + yPadding, 0, 1, 1, 0, 0) + GraphicsUtil.draw(self.valueText, currentX - valueTextWidth / 2, yPadding, 0, 1, 1, 0, 0) GraphicsUtil.setColor(lightGray, lightGray, lightGray, 1) local textWidth, textHeight = self.minText:getDimensions() - GraphicsUtil.draw(self.minText, self.x + xPadding - textWidth / 2, self.y + yPadding + sliderBarThickness + textHeight, 0, 1, 1, 0, 0) + GraphicsUtil.draw(self.minText, xPadding - textWidth / 2, yPadding + sliderBarThickness + textHeight, 0, 1, 1, 0, 0) textWidth, textHeight = self.maxText:getDimensions() - GraphicsUtil.draw(self.maxText, self.x + xPadding + barWidth - textWidth / 2, self.y + yPadding + sliderBarThickness + textHeight, 0, 1, 1, 0, 0) + GraphicsUtil.draw(self.maxText, xPadding + barWidth - textWidth / 2, yPadding + sliderBarThickness + textHeight, 0, 1, 1, 0, 0) GraphicsUtil.setColor(1, 1, 1, 1) diff --git a/client/src/ui/StackPanel.lua b/client/src/ui/StackPanel.lua index e89e2c89..5f9af82a 100644 --- a/client/src/ui/StackPanel.lua +++ b/client/src/ui/StackPanel.lua @@ -116,7 +116,7 @@ end function StackPanel:drawSelf() if DEBUG_ENABLED then GraphicsUtil.setColor(1, 0, 0, 0.7) - GraphicsUtil.drawRectangle("line", self.x, self.y, self.width, self.height) + GraphicsUtil.drawRectangle("line", 0, 0, self.width, self.height) GraphicsUtil.setColor(1, 1, 1, 1) end end diff --git a/client/src/ui/Stepper.lua b/client/src/ui/Stepper.lua index 6ec600b1..c8d2336e 100644 --- a/client/src/ui/Stepper.lua +++ b/client/src/ui/Stepper.lua @@ -119,9 +119,9 @@ end function Stepper:drawSelf() if config.debug_mode then GraphicsUtil.setColor(self.color) - GraphicsUtil.drawRectangle("fill", self.x, self.y, self.width, self.height) + GraphicsUtil.drawRectangle("fill", 0, 0, self.width, self.height) GraphicsUtil.setColor(self.borderColor) - GraphicsUtil.drawRectangle("line", self.x, self.y, self.width, self.height) + GraphicsUtil.drawRectangle("line", 0, 0, self.width, self.height) GraphicsUtil.setColor(1, 1, 1, 1) end end diff --git a/client/src/ui/UIElement.lua b/client/src/ui/UIElement.lua index fcf76119..69194308 100644 --- a/client/src/ui/UIElement.lua +++ b/client/src/ui/UIElement.lua @@ -197,12 +197,12 @@ end function UIElement:draw() if self.isVisible then + love.graphics.push("transform") + love.graphics.translate(self.x, self.y) self:drawSelf() -- if DEBUG_ENABLED then -- GraphicsUtil.drawRectangle("line", self.x, self.y, self.width, self.height, 1, 1, 1, 0.5) -- end - love.graphics.push("transform") - love.graphics.translate(self.x, self.y) self:drawChildren() love.graphics.pop() end @@ -212,15 +212,15 @@ end -- implementation is optional so layout elements don't have to function UIElement:drawSelf() love.graphics.setColor(self.backgroundColor) - love.graphics.rectangle("fill", self.x, self.y, self.width, self.height) + love.graphics.rectangle("fill", 0, 0, self.width, self.height) love.graphics.setColor(1, 1, 1, 1) --if self.padding > 0 then - -- love.graphics.rectangle("line", self.x + self.padding, self.y + self.padding, 1, self.height - self.padding * 2) - -- love.graphics.rectangle("line", self.x + self.padding, self.y + self.padding, self.width - self.padding * 2, 1) - -- love.graphics.rectangle("line", self.x + self.width - self.padding, self.y + self.padding, 1, self.height - self.padding * 2) - -- love.graphics.rectangle("line", self.x + self.padding, self.y + self.height - self.padding, self.width - self.padding *2, 1) + -- love.graphics.rectangle("line", self.padding, self.padding, 1, self.height - self.padding * 2) + -- love.graphics.rectangle("line", self.padding, elf.padding, self.width - self.padding * 2, 1) + -- love.graphics.rectangle("line", self.width - self.padding, self.padding, 1, self.height - self.padding * 2) + -- love.graphics.rectangle("line", self.padding, self.height - self.padding, self.width - self.padding *2, 1) --end - --love.graphics.print(self.width .. ", " .. self.height, self.x + 5, self.y + 5) + --love.graphics.print(self.width .. ", " .. self.height, 5, 5) end function UIElement:drawChildren() diff --git a/client/src/ui/ValueLabel.lua b/client/src/ui/ValueLabel.lua index 099c2249..e9501b08 100644 --- a/client/src/ui/ValueLabel.lua +++ b/client/src/ui/ValueLabel.lua @@ -11,7 +11,7 @@ end, UiElement) function ValueLabel:drawSelf() - love.graphics.print(self.valueFunction(), self.x, self.y) + love.graphics.print(self.valueFunction(), 0, 0) end return ValueLabel \ No newline at end of file From dbff55d7b63e65fa9b9bfcd043924a6e067c43e7 Mon Sep 17 00:00:00 2001 From: Endaris Date: Wed, 11 Jun 2025 00:59:26 +0200 Subject: [PATCH 46/49] readjust MenuItem explicitly use cursors for Options/SoundTest --- client/src/scenes/InputConfigMenu.lua | 2 +- client/src/scenes/OptionsMenu.lua | 33 +++++++++++++------- client/src/scenes/SoundTest.lua | 4 ++- client/src/ui/MenuItem.lua | 45 +++++++++------------------ common/lib/tableUtils.lua | 19 ++++++++--- 5 files changed, 55 insertions(+), 48 deletions(-) diff --git a/client/src/scenes/InputConfigMenu.lua b/client/src/scenes/InputConfigMenu.lua index effd0e94..7333e857 100644 --- a/client/src/scenes/InputConfigMenu.lua +++ b/client/src/scenes/InputConfigMenu.lua @@ -100,7 +100,7 @@ function InputConfigMenu:setAllKeys() end function InputConfigMenu:currentKeyLabelForIndex(index) - return self.menu.menuItems[index].textButton.children[1] + return self.menu.menuItems[index].item.children[1] end function InputConfigMenu:setKeyStart(key) diff --git a/client/src/scenes/OptionsMenu.lua b/client/src/scenes/OptionsMenu.lua index 87cbae11..b8ce950d 100644 --- a/client/src/scenes/OptionsMenu.lua +++ b/client/src/scenes/OptionsMenu.lua @@ -25,11 +25,13 @@ end, Scene) OptionsMenu.name = "OptionsMenu" -local SCROLL_STEP = 14 - function OptionsMenu:loadScreens() local menus = {} + local function onMenuDetach(menu) + self.cursor:releaseFocus(menu) + end + menus.baseMenu = self:loadBaseMenu() menus.generalMenu = self:loadGeneralMenu() menus.graphicsMenu = self:loadGraphicsMenu() @@ -38,12 +40,12 @@ function OptionsMenu:loadScreens() menus.aboutMenu = self:loadAboutMenu() menus.modifyUserIdMenu = self:loadModifyUserIdMenu() menus.systemInfo = self:loadInfoScreen(self:getSystemInfo()) - menus.aboutThemes = self:loadInfoScreen(save.read_txt_file("docs/themes.md")) - menus.aboutCharacters = self:loadInfoScreen(save.read_txt_file("docs/characters.md")) - menus.aboutStages = self:loadInfoScreen(save.read_txt_file("docs/stages.md")) - menus.aboutPanels = self:loadInfoScreen(save.read_txt_file("docs/panels.md")) - menus.aboutAttackFiles = self:loadInfoScreen(save.read_txt_file("docs/training.txt")) - menus.installingMods = self:loadInfoScreen(save.read_txt_file("docs/installMods.md")) + + for menuName, menu in pairs(menus) do + if menuName ~= "baseMenu" then + menu.onDetach = onMenuDetach + end + end if #menus.modifyUserIdMenu.children == 1 then menus.modifyUserIdMenu:detach() @@ -76,8 +78,15 @@ function OptionsMenu:updateMenuLanguage() end function OptionsMenu:switchToScreen(screenName) - self.menus[self.activeMenuName]:detach() - self.uiRoot:addChild(self.menus[screenName]) + local oldMenu = self.menus[self.activeMenuName] + local newMenu = self.menus[screenName] + oldMenu:detach() + if not self.cursor.focusToHover[newMenu] then + self.cursor:deepenFocus(newMenu) + else + self.cursor:releaseFocus(oldMenu) + end + self.uiRoot:addChild(newMenu) self.activeMenuName = screenName end @@ -680,15 +689,17 @@ function OptionsMenu:load() self.backgroundImage = themes[config.theme].images.bg_main self.uiRoot:addChild(self.menus.baseMenu) + self.cursor = ui.Cursor(self.menus.baseMenu) end function OptionsMenu:update(dt) self.backgroundImage:update(dt) - self.menus[self.activeMenuName]:receiveInputs(inputManager) + self.cursor:receiveInputs(dt) end function OptionsMenu:draw() self.backgroundImage:draw() + self.cursor:draw() self.uiRoot:draw() end diff --git a/client/src/scenes/SoundTest.lua b/client/src/scenes/SoundTest.lua index f061a556..5a65d0de 100644 --- a/client/src/scenes/SoundTest.lua +++ b/client/src/scenes/SoundTest.lua @@ -213,6 +213,7 @@ function SoundTest:load() self.soundTestMenu:addChild(back) self.uiRoot:addChild(self.soundTestMenu) + self.cursor = ui.Cursor(self.soundTestMenu) self.backgroundImg = themes[config.theme].images.bg_main @@ -227,12 +228,13 @@ function SoundTest:load() end function SoundTest:update(dt) - self.soundTestMenu:receiveInputs() + self.cursor:receiveInputs(dt) self.backgroundImg:update(dt) end function SoundTest:draw() self.backgroundImg:draw() + self.cursor:draw() self.uiRoot:draw() end diff --git a/client/src/ui/MenuItem.lua b/client/src/ui/MenuItem.lua index 24b6e025..1abc83f5 100644 --- a/client/src/ui/MenuItem.lua +++ b/client/src/ui/MenuItem.lua @@ -8,15 +8,16 @@ local GraphicsUtil = require("client.src.graphics.graphics_util") local HorizontalFlexLayout = import("./Layouts.HorizontalFlexLayout") local addCursorInteractionInterface = import("./CursorInteractable") --- MenuItem is a specific UIElement that all children of Menu should be ----@class MenuItem +-- MenuItem is a wrapper UIElement that bundles a label element with another interactable UIElement and passes through inputs to that second element +---@class MenuItem : UiElement, CursorInteractable local MenuItem = class(function(self, options) - self.selected = false - self.TYPE = "MenuItem" + addCursorInteractionInterface(self, self.receiveInputs) end, UiElement) +MenuItem.TYPE = "MenuItem" MenuItem.PADDING = 2 +MenuItem.layout = HorizontalFlexLayout -- Takes a label and an optional extra element and makes and combines them into a menu item -- which is suitable for inserting into a menu @@ -26,7 +27,7 @@ MenuItem.PADDING = 2 function MenuItem.createMenuItem(label, item) assert(label ~= nil) - local menuItem = UiElement({ + local menuItem = MenuItem({ hAlign = "center", vAlign = "center", layout = HorizontalFlexLayout, @@ -42,19 +43,16 @@ function MenuItem.createMenuItem(label, item) label.hFill = true menuItem:addChild(label) if item ~= nil then - item.vAlign = "center" - item.hAlign = "left" item.hFill = true + item.vAlign = "center" menuItem:addChild(item) - addCursorInteractionInterface(menuItem, function(self, cursor, dt) - item:receiveInputs(cursor, dt) - end) + menuItem.item = item end return menuItem end --- Creates a menu item with just a button +-- Creates just a button as buttons already have their own hover draw function MenuItem.createButtonMenuItem(text, replacements, translate, onClick) assert(text ~= nil) local id @@ -109,10 +107,7 @@ function MenuItem.createLabeledButtonMenuItem(labelText, labelTextReplacements, }) local textButton = TextButton({label = Label({text = buttonText, replacements = buttonTextReplacements, translate = buttonTextTranslate, hAlign = "center", vAlign = "center"}), onClick = buttonOnClick, width = BUTTON_WIDTH}) - local menuItem = MenuItem.createMenuItem(label, textButton) - menuItem.textButton = textButton - - return menuItem + return MenuItem.createMenuItem(label, textButton) end function MenuItem.createStepperMenuItem(text, replacements, translate, stepper) @@ -164,14 +159,6 @@ function MenuItem.createSliderMenuItem(text, replacements, translate, slider) return MenuItem.createMenuItem(label, slider) end -function MenuItem:setSelected(selected) - self.selected = selected - if selected and self.onSelectedFunction then - self.onSelectedFunction() - end -end - - local DEFAULT_BACKGROUND_COLOR = {1, 1, 1} local SELECTED_BACKGROUND_COLOR = {0.6, 0.6, 1} local DEFAULT_BORDER_COLOR = {1, 1, 1} @@ -179,7 +166,7 @@ local SELECTED_BORDER_COLOR = {0.6, 0.6, 1} function MenuItem:drawSelf() local baseOpacity = 0.15 - if self.selected then + if next(self.hoveringCursors) then local selectedAdditionalOpacity = 0.5 local fillOpacity = (math.cos(6 * love.timer.getTime()) + 1) / 16 + baseOpacity + selectedAdditionalOpacity local borderOpacity = (math.cos(6 * love.timer.getTime()) + 1) / 4 + baseOpacity + selectedAdditionalOpacity @@ -191,13 +178,9 @@ function MenuItem:drawSelf() end end --- inputs as a passthrough in case we ever implement player specific menus -function MenuItem:receiveInputs(inputs) - for _, child in ipairs(self.children) do - if child.receiveInputs then - child:receiveInputs(inputs) - return - end +function MenuItem:receiveInputs(cursor, dt) + if self.item then + self.item:receiveInputs(cursor, dt) end end diff --git a/common/lib/tableUtils.lua b/common/lib/tableUtils.lua index df177898..fe62aa5a 100644 --- a/common/lib/tableUtils.lua +++ b/common/lib/tableUtils.lua @@ -104,10 +104,21 @@ end -- returns true if the table contains the given element or an identical copy of it, otherwise false -- may result in a deathloop if there are recursive references -function tableUtils.contains(tab, element) - for _, value in pairs(tab) do - if deep_content_equal(value, element) then - return true +---@param tab table +---@param element any +---@param referenceEquality boolean? if true, don't check equality by content for tables but instead check equality by reference only ("===" in other languages) +function tableUtils.contains(tab, element, referenceEquality) + if referenceEquality then + for _, value in pairs(tab) do + if value == element then + return true + end + end + else + for _, value in pairs(tab) do + if deep_content_equal(value, element) then + return true + end end end From 0eef8fb74b7884a43af7c0cd4ea784cbb084fc20 Mon Sep 17 00:00:00 2001 From: Endaris Date: Wed, 11 Jun 2025 01:03:58 +0200 Subject: [PATCH 47/49] update doc --- docs/Understanding the Codebase.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/Understanding the Codebase.md b/docs/Understanding the Codebase.md index 82e673fa..93e80a5d 100644 --- a/docs/Understanding the Codebase.md +++ b/docs/Understanding the Codebase.md @@ -362,7 +362,7 @@ The first core problem has led to the introduction of a bunch more fields and fu - minWidth, minHeight, defaults to 0 - maxWidth, maxHeight, defaults to math.inf -- getPreferredWidth() +- getPreferredWidth(), getPreferredHeight() - hFill, vFill There are some more subtle problems hidden in this but the baseline is that the width of an element does not depend on its height but as per core problem #2 the height may depend on its width. @@ -385,11 +385,10 @@ If the delta is negative, it is distributed between all children that have a low By traversing downwards, the extra width compared to the initial estimate is thus spent where possible and `newWidth` is finalized as `width`. -For height the same steps are essentially repeated with the exception that there is no corresponding `getPreferredHeight()` function and instead all elements start at their minimum height. For wrappable elements their width-based minimum height is accessed through a `getMinHeight()` function that is only required by the HorizontalWrapLayout. -Correspondingly, the delta for `newHeight` to `height` will always be positive. +For height the same steps are essentially repeated. For wrappable elements their width-based minimum height is accessed through a `getMinHeight()` function that is only required by the HorizontalWrapLayout but the exact implementation of this width-to-height mechanism might change later. To sum up, a valid and desirable estimate is first made for widths, than corrected with the true width using only operations that are known to be valid so the final outcome is guaranteed to be valid and related to the window size. -The same happens for height with consideration to the width values. +The same happens for height with special consideration to the width values. In the following positioning step, each parent assigns `x` and `y` to its children according to its layout. The values are relative to the parent. From 389e9163fa6f83b447240acd9a7f0b9636c14b94 Mon Sep 17 00:00:00 2001 From: Endaris Date: Wed, 11 Jun 2025 03:34:35 +0200 Subject: [PATCH 48/49] divide Button clicks into action and onAction start on stage selection --- .../default/stage/thumbnail.png | Bin 3883 -> 4714 bytes client/src/graphics/graphics_util.lua | 8 +-- client/src/scenes/CharacterSelect.lua | 16 ++--- client/src/scenes/DesignHelper.lua | 53 +++++++++++++- client/src/scenes/OptionsMenu.lua | 2 +- client/src/scenes/SetNameMenu.lua | 2 +- client/src/scenes/SetUserIdMenu.lua | 2 +- client/src/scenes/WindowSizeTester.lua | 4 +- client/src/ui/Button.lua | 33 +++++---- client/src/ui/ButtonGroup.lua | 10 +-- client/src/ui/CharacterButton.lua | 2 + client/src/ui/ImageContainer.lua | 25 +++++-- client/src/ui/Label.lua | 11 ++- client/src/ui/MenuItem.lua | 16 ++++- client/src/ui/PagedUniGrid.lua | 4 +- client/src/ui/PanelSetButton.lua | 2 + client/src/ui/StageButton.lua | 65 ++++++++++++++++++ client/src/ui/Stepper.lua | 4 +- client/src/ui/init.lua | 4 ++ 19 files changed, 212 insertions(+), 51 deletions(-) create mode 100644 client/src/ui/StageButton.lua diff --git a/client/assets/themes/Panel Attack Modern/default/stage/thumbnail.png b/client/assets/themes/Panel Attack Modern/default/stage/thumbnail.png index 76c468a4298a53a6b454d464b890bb18a8cac140..aba6f28a891fa1ed49433ba04f1b1891134f8eac 100644 GIT binary patch literal 4714 zcmeHKdvp}#6`x=X5MCxwh=6oDNCCs_Ja%SxXULL-Y#!TW!;&l{XxrmFzD=g=&TM99 zvzu@%7|3ZPEhtjO077d|YQ?6l6+A$TfR%_R8ZAAo6dEZ9(6(7o;=yuIBRs5UBu zI2zHlpb`S8(GUSBNGL%BN&Gpy@`0(Zpp*aIy|_NLrD5LpI(EM}=juoHM`O>lEhx*Z zQZp|k++WqN_-9O{o(xp#o@amb9m#*zGw15JvU2Y7iAVlW7@xj4KakGuSbs3<^wzhQR;2y0C17tVKejV}r*tq-`td!&(zBcCt;X_sJwH9k zPJ8Oinfa}L_qo5mmbYWd-aU(ICug4Cc;&(GuRJLqY5u|S=Kq{|{0brc<#p^__LA7f z-=ui`;_rnMi)J85$|j|t!0#<67)TDLwCkbA+$FF0XEd*<-2daRjBjmm$TMN$KmWzL-k%(5 zy}sm=Bf0xlyz#)~2`#^?3+`}S=da&r4@Y_yZu#Z5=QHe2R_o3^fh=Y!G<6|e12DcaHy*qdK}^PJYV?!an6KVF)? zaKoFkI{%q<=kmjO&%Eb)w{y!WS4va&(I57_U)6iGYvJYnPt0_W``1ekw_bUo{c6gS zOOL+w_}6y6kbS2zv3XqIcZ<5W-;t(1{PZeKedD2B%aFUO)TG%GYnh0!(a0GTCg5=~TB7(FQ7^nzp z6xEGgJNz<+DzY1^Vtu49QUGd|l13d=HkJpZ#!H`uv0DCSx^9IAS@0wHX0y zD8bJGHZ`{Z!{LLtBf=P<%8CM7`Fy-blVWBzUXL3y{qwRWDKc+8ifmA11(wEHJICOR zLy~cUmQ0#cM@C+=iL8bSaP7afv-Q!cy6 zut5-X3Sbz-3SEIsEQE$fWkSgiiX&MEOG+Hh1WhP`1GvjBv$)`J0!pGqm)#Mxpk#?J z()6$ZtFMHG8bCzU8cShTf%hv35UTblFzG04I5$RpO1I_~omnV8-|ux}v@MyiCGm#@ zBM2S2u`)%CB`g?A5rE*8hG15Kax$EQWJt!va-`kq8Zuf9^rU~(-o22=EkCweA zE5U~UWo<4FXb!Z8JzAnfq5lR;lw2_?L0$4K`5IEJr2<8*NzV&XlB1|l4`eG&h?P`H zwSrm$;Of{vzXs&WXY?*$1cqasEG~;Ig)=V6g$pELa6ob_D|3v)B`2#kh#u8~MqJQA zehn-Xa8Ji2&fH1S#hOG%Ryhf@?z+5M3lu4%`3A7kYw(FNj&zb3-->F^4dh{DFez5Vw>( zX)tV$fYc;qaPNUz88Nt*^>YT}y`4vYtKCi)C_3h3RQ!(7HAdH{7#NlDSa*%lH7W*1 zWjxkh|2MkQhrWh^3jgHA;kRzy`KoR3J2_1(E%qR+`MdC#_BNDEjFha2BFLoa=9z-D z?aG3}@rKt|IR0|lq?GYT9QxGIImAv=R$Cu^418nuepRH(r fI%DRSJC;?~y@d3(HoS;INW@!M?&(J(P%Or zje3)YmQnJMAT~XA&%Ptu2X=J-?#Lu~;^SmAdhEWrcvMw}R>$Zyf#Z|kod5LPvU|km zhi}TQx?{sH?!Wiu|8BVBz^-C$^~|A_hhF>H>`%8JKXd2erLP_DeERHHdrtA^7O08E zwW+sG|9Qo>U%r0*OCa>ZT@&-!f4;Z8w_{>vY)AB=*j3$&=d*hk-uv+EuO2=1?>&pm z!XNJ(T$X>hye-8&SAM1}5ANi(HJ45Py8G|`?1`@f^XvDYIe8$(4(#3jQEdG9+9Q|m z864m2*)1;aCt>_I!w2W|r}*rjrVoew>s~$d)(zjkxcTLePX1$BIQF3L{`DJ=?(1K- zy!*vtTgI?{^)C=SqL=ocLHpNqPs4#WRTnaT8jvBvyFV5}i1KIE|hS#BzI05NXek zYwJnRCPxP)*~1Vx)$7pD@UZr}8?7IFKp~bn*k{odDXxzbMCUivYf>r0yb3JMz-8fO z{fwU@2$p7efnfwX;AI(sWd$}s5FN7}H7?I=XMK%n|Dz6`N0&?Vt>Y#^TzUomd58ng zUP%zEfRY-s#u7t>_4#s^r3(~QDwX`DfL}AR6vOj8MY9yk zl1PIzrwSG*lLfP_4K~pjm-BPrI7xRL&RYfzzSGtaEGunb_)c3G@*8EK8?naSvBHt2erlxdAjX+Z?8WJ4b4GUIJt02X7O9~=S2FrCqloQF0lWED6OMzl( zI!w|m$?`4T((^^r?N!l)SY%V>Jy~J(CF)ER;A6_ z$vm(!nxTq2m4d95Oh>&YZq`pJDjcn^D7tg0c!QOk^C0Th^=})l}VprX2$0lWoE+Yt|pHyCwFbs^w{lq0z^I zx^Fi_$9@>Z7XAKthnSZ z9O{BZLfER9s9C1$L0+4$_Cz0rcQ;CLgW-{!SM15G1H*)Pp!E50bnrxR_hOyQ9!!Fi z1BEPyloq zxMbgN4_6AN1qu?3$3zr7zoN(j41yrf^>U<~<`^;2dRP_6QyS&Gzx*%&3+JL=GbD1;;IF{rBe2@%=B$)&}pCQvs zus6&zX(;iPiH?HdqH&pU9eKi#CfQ?ZG^dpcP2tBm830+zC62hy%eLT1D+~5vhPtNi zNVN+u$i`B5gOw4OMeT1)#V^`GOJoCgzhRX$W7n`ToSuY|b+J2W50Bu-2O+BIQ>bWj z)^Mo+jTY>v0~&Mi!>|f#Tm@M;TGaKtauKBMP%cjAq2z-7v?-T@DvU#lgbow!!gt*Q z5RoO}R`fIgEK$Lmru0HKLe+&%!BwV->!IA;M1& diff --git a/client/src/graphics/graphics_util.lua b/client/src/graphics/graphics_util.lua index ed7b75d2..590f2577 100644 --- a/client/src/graphics/graphics_util.lua +++ b/client/src/graphics/graphics_util.lua @@ -391,18 +391,18 @@ end function GraphicsUtil.getAlignmentOffset(parentElement, childElement) local xOffset, yOffset - if childElement.hAlign == "center" then + if parentElement.hAlign == "center" then xOffset = parentElement.width / 2 - childElement.width / 2 - elseif childElement.hAlign == "right" then + elseif parentElement.hAlign == "right" then xOffset = parentElement.width - childElement.width else -- if hAlign == "left" then -- default xOffset = 0 end - if childElement.vAlign == "center" then + if parentElement.vAlign == "center" then yOffset = parentElement.height / 2 - childElement.height / 2 - elseif childElement.vAlign == "bottom" then + elseif parentElement.vAlign == "bottom" then yOffset = parentElement.height - childElement.height else --if uiElement.vAlign == "top" then -- default diff --git a/client/src/scenes/CharacterSelect.lua b/client/src/scenes/CharacterSelect.lua index e3c0cb14..380dcb58 100644 --- a/client/src/scenes/CharacterSelect.lua +++ b/client/src/scenes/CharacterSelect.lua @@ -165,7 +165,7 @@ function CharacterSelect:createReadyButton() }) -- assign player generic callback - readyButton.onClick = function(self, inputSource, holdTime) + readyButton.action = function(self, inputSource, holdTime) local player if inputSource and inputSource.player then player = inputSource.player @@ -174,7 +174,7 @@ function CharacterSelect:createReadyButton() end player:setWantsReady(not player.settings.wantsReady) end - readyButton.onSelect = readyButton.onClick + readyButton.onSelect = readyButton.action return readyButton end @@ -187,12 +187,12 @@ function CharacterSelect:createLeaveButton() label = ui.Label({id = "leave"}), backgroundColor = {1, 1, 1, 0}, outlineColor = {1, 1, 1, 1}, - onClick = function() + action = function() GAME.theme:playCancelSfx() self:leave() end }) - leaveButton.onSelect = leaveButton.onClick + leaveButton.onSelect = leaveButton.action return leaveButton end @@ -310,7 +310,7 @@ function CharacterSelect:getCharacterButtons() -- assign player generic callbacks for i = 1, #characterButtons do local characterButton = characterButtons[i] - characterButton.onClick = function(selfElement, inputSource, holdTime) + characterButton.action = function(selfElement, inputSource, holdTime) local character = characters[selfElement.characterId] local player if inputSource and inputSource.player then @@ -331,7 +331,7 @@ function CharacterSelect:getCharacterButtons() if characters[characterButton.characterId] and characters[characterButton.characterId]:canSuperSelect() then self.applySuperSelectInteraction(characterButton) else - characterButton.onSelect = characterButton.onClick + characterButton.onSelect = characterButton.action end end @@ -371,7 +371,7 @@ function CharacterSelect.applySuperSelectInteraction(characterButton) characterButton.onRelease = function(self, x, y, timeHeld) self.updateSuperSelectShader(self.superSelectImage, 0) if self:inBounds(x, y) then - self:onClick(input.mouse, timeHeld) + self:action(input.mouse, timeHeld) end end @@ -386,7 +386,7 @@ function CharacterSelect.applySuperSelectInteraction(characterButton) else self:yieldFocus() -- apply the actual click on release with the held time and reset it afterwards - self:onClick(inputs, self.holdTime) + self:action(inputs, self.holdTime) self.holdTime = 0 end self.updateSuperSelectShader(self.superSelectImage, self.holdTime) diff --git a/client/src/scenes/DesignHelper.lua b/client/src/scenes/DesignHelper.lua index 11dcd700..591ba36f 100644 --- a/client/src/scenes/DesignHelper.lua +++ b/client/src/scenes/DesignHelper.lua @@ -55,8 +55,13 @@ local function createCharacterSelect(scene) end }) + local releaseFocus = function() + scene.cursor:releaseFocus(scene.characterSelect) + end + for i, characterId in ipairs(visibleCharacters) do local button = ui.CharacterButton({character = characters[characterId]}) + button.onAction = releaseFocus scene.characterSelect:addChild(button) end @@ -65,6 +70,40 @@ local function createCharacterSelect(scene) return scrollContainer end +local function createStageSelect(scene) + local scrollContainer = ui.ScrollContainer({ + scrollOrientation = "vertical", + minHeight = 400, + hFill = true, + vFill = true, + maxHeight = 800, + hAlign = "center", + }) + + scene.stageSelect = ui.UniSizedContainer({ + childrenWidth = 112, + childrenHeight = 84, + childGap = 16, + onYield = function (self) + scene.stageSelectContainer:detach() + end + }) + + local releaseFocus = function() + scene.cursor:releaseFocus(scene.stageSelect) + end + + for i, stageId in ipairs(visibleStages) do + local button = ui.StageButton({stage = stages[stageId]}) + button.onAction = releaseFocus + scene.stageSelect:addChild(button) + end + + scrollContainer:addChild(scene.stageSelect) + + return scrollContainer +end + local function createPanelSetSelect(scene) scene.panelSetSelect = ui.VerticalMenu({ childGap = 8, @@ -88,13 +127,17 @@ local function createPanelSetSelect(scene) return a.panelSet.id < b.panelSet.id end) - scene.panelSetSelect:addChild(ui.TextButton({label = ui.Label({id = "back"}), onClick = function() scene.cursor:releaseFocus(scene.panelSetSelect) end})) + scene.panelSetSelect:addChild(ui.TextButton({ + label = ui.Label({id = "back"}), + action = function() scene.cursor:releaseFocus(scene.panelSetSelect) end + })) return scene.panelSetSelect end function DesignHelper:load() self.characterSelectContainer = createCharacterSelect(self) + self.stageSelectContainer = createStageSelect(self) self.panelSetSelect = createPanelSetSelect(self) self.uiRoot.layout = ui.Layouts.VerticalFlexLayout self.uiRoot.childGap = 8 @@ -158,7 +201,7 @@ function DesignHelper:load() }) local characterSelectionSelector, characterButton = getSelectorTemplate("character") characterButton:addChild(characterImage) - characterButton.onClick = function() + characterButton.action = function() self.subSelection:addChild(self.characterSelectContainer) self.cursor:deepenFocus(self.characterSelect) end @@ -172,6 +215,10 @@ function DesignHelper:load() }) local stageSelectionSelector, stageButton = getSelectorTemplate("stage") stageButton:addChild(stageImage) + stageButton.action = function() + self.subSelection:addChild(self.stageSelectContainer) + self.cursor:deepenFocus(self.stageSelect) + end local panelSelectionSelector, panelButton = getSelectorTemplate("panels") @@ -200,7 +247,7 @@ function DesignHelper:load() panelButton.padding = 4 panelSelectionSelector.childGap = 0 panelButton:addChild(panelContainer) - panelButton.onClick = function() + panelButton.action = function() self.subSelection:addChild(self.panelSetSelect) self.cursor:deepenFocus(self.panelSetSelect) end diff --git a/client/src/scenes/OptionsMenu.lua b/client/src/scenes/OptionsMenu.lua index b8ce950d..d6dfef65 100644 --- a/client/src/scenes/OptionsMenu.lua +++ b/client/src/scenes/OptionsMenu.lua @@ -309,7 +309,7 @@ function OptionsMenu:loadGeneralMenu() ---@cast index integer index = util.bound(1, index, #group.buttons) -- simulate changing to the button that replaces the one that got removed due to no attached versions - group.buttons[index]:onClick(nil, 0) + group.buttons[index]:action(nil, 0) end end }) diff --git a/client/src/scenes/SetNameMenu.lua b/client/src/scenes/SetNameMenu.lua index c01eec03..277c720e 100644 --- a/client/src/scenes/SetNameMenu.lua +++ b/client/src/scenes/SetNameMenu.lua @@ -61,7 +61,7 @@ function SetNameMenu:load() y = y + 100, vAlign = "top", hAlign = "center", - onClick = function(selfElement, inputSource, holdTime) + action = function(selfElement, inputSource, holdTime) self:confirmName() end }) diff --git a/client/src/scenes/SetUserIdMenu.lua b/client/src/scenes/SetUserIdMenu.lua index e9174121..f3643dfd 100644 --- a/client/src/scenes/SetUserIdMenu.lua +++ b/client/src/scenes/SetUserIdMenu.lua @@ -34,7 +34,7 @@ function SetUserIdMenu:load(sceneParams) x = menuX, y = menuY + 60, vAlign = "top", - onClick = function() self:confirmId() end + action = function() self:confirmId() end }) self.warningLabel = ui.Label({ diff --git a/client/src/scenes/WindowSizeTester.lua b/client/src/scenes/WindowSizeTester.lua index 5fed33a6..54624d1e 100644 --- a/client/src/scenes/WindowSizeTester.lua +++ b/client/src/scenes/WindowSizeTester.lua @@ -126,7 +126,7 @@ function WindowSizeTester:load() grid:createElementAt(1, 1, 20, 1, "desktopDimensions", desktopSizeLabel) grid:createElementAt(1, 2, 20, 1, "maximized", maximizedLabel) grid:createElementAt(1, 3, 20, 1, nil, ui.TextButton({label = ui.Label({text = "maximize", translate = false}), - onClick = function() love.window.maximize() end})) + action = function() love.window.maximize() end})) grid:createElementAt(1, 4, 2, 1, nil, ui.Label({text = "fullscreen", translate = false})) grid:createElementAt(3, 4, 20, 1, "fullscreen", self.uiRoot.fullscreenSelection) grid:createElementAt(1, 5, 2, 1, nil, ui.Label({text = "width", translate = false})) @@ -139,7 +139,7 @@ function WindowSizeTester:load() grid:createElementAt(3, 8, 20, 1, "y", self.uiRoot.ySlider) grid:createElementAt(1, 9, 20, 1, "back", ui.TextButton({ label = ui.Label({text = "back"}), - onClick = + action = function() love.resize = self.originalResize GAME.navigationStack:pop() diff --git a/client/src/ui/Button.lua b/client/src/ui/Button.lua index 61d17200..023192a3 100644 --- a/client/src/ui/Button.lua +++ b/client/src/ui/Button.lua @@ -8,14 +8,16 @@ local addCursorInteractionInterface = import("./CursorInteractable") ---@class ButtonOptions : UiElementOptions ---@field backgroundColor number[]? ---@field outlineColor number[]? ----@field onClick fun(button: Button?, input: table?, timeHeld: number?)? ---@field receiveInputs fun(button: Button, cursor: Cursor, dt: number?)? +---@field action fun(button: Button?, input: table?, timeHeld: number?)? Callback for the main (often UI agnostic) action that should happen, e.g. setting character for a player and playing its selection SFX +---@field onAction fun(button: Button?, input: table?, timeHeld: number?)? Callback for UI specific actions that are unrelated to the action, e.g. redirecting cursor focus or playing validation SFX ---@class Button : UiElement, CursorInteractable ---@operator call(ButtonOptions): Button ---@field backgroundColor number[] ---@field outlineColor number [] ----@field onClick fun(button: Button?, input: table?, timeHeld: number?) +---@field action fun(button: Button?, input: table?, timeHeld: number?)? Callback for the main (often UI agnostic) action that should happen, e.g. setting character for a player and playing its selection SFX +---@field onAction fun(button: Button?, input: table?, timeHeld: number?) Callback for UI specific actions that are unrelated to the action, e.g. redirecting cursor focus or playing validation SFX local Button = class( function(self, options) self.hoveredBackgroundColor = {.5, .5, .5, .7} @@ -23,8 +25,9 @@ local Button = class( self.outlineColor = options.outlineColor or {.5, .5, .5, .7} -- callbacks - self.onClick = options.onClick or function() - GAME.theme:playValidationSfx() + self.action = options.action or self.action + if options.onAction then + self.onAction = options.onAction end addCursorInteractionInterface(self, options.receiveInputs or self.receiveInputs) @@ -34,30 +37,34 @@ local Button = class( Button.TYPE = "Button" -function Button:onTouch(x, y) -end - function Button:onRelease(x, y, timeHeld) - if self:inBounds(x, y) then - -- first argument non-self of onClick is the input source to accomodate inputs via controllers from different players - self:onClick(input.mouse, timeHeld) + if self.action and self:inBounds(x, y) then + -- first argument non-self of action is the input source to accomodate inputs via controllers from different players + self:action(input.mouse, timeHeld) + self:onAction() end end +function Button:onAction() + GAME.theme:playValidationSfx() +end + ---@param cursor Cursor ---@param dt number function Button:receiveInputs(cursor, dt) local input = cursor.keyInput if input.isDown["MenuSelect"] then - self:onClick(input) + -- these are always called together but the separation is (an optional) semantic one for cases where the action can be fully independent of the UI + self:action(input) + self:onAction() -- this is a really stupid way to make sure you can activate back buttons with escape elseif input.isDown["MenuEsc"] then - self:onClick(input) + self:action(input) end end function Button:drawBackground() - if self:isHovered() then + if self.action and self:isHovered() then GraphicsUtil.setColor(self.hoveredBackgroundColor) else GraphicsUtil.setColor(self.backgroundColor) diff --git a/client/src/ui/ButtonGroup.lua b/client/src/ui/ButtonGroup.lua index 6d41da7e..18e31a09 100644 --- a/client/src/ui/ButtonGroup.lua +++ b/client/src/ui/ButtonGroup.lua @@ -10,13 +10,13 @@ local BUTTON_PADDING = 5 -- UIElement representing a set of buttons which share state (think radio buttons) --- forced override for each of the button's onClick function +-- forced override for each of the button's action function -- this allows buttons to have individual custom behaviour while also triggering the global state change local function genButtonGroupFn(self, button) - local onClick = button.onClick + local action = button.action return function(b, inputSource, holdTime) self:buttonClicked(b) - onClick(b, inputSource, holdTime) + action(b, inputSource, holdTime) self:onChange(self.value) end end @@ -41,7 +41,7 @@ local function setButtons(self, buttons, values, selectedIndex) button.x = self.buttons[i - 1].x + self.buttons[i - 1].width + BUTTON_PADDING overallWidth = overallWidth + BUTTON_PADDING end - button.onClick = genButtonGroupFn(self, button) + button.action = genButtonGroupFn(self, button) button.vAlign = "center" self:addChild(button) overallHeight = math.max(overallHeight, button.height) @@ -55,7 +55,7 @@ end local function setActiveButton(self, selectedIndex) local newIndex = util.bound(1, selectedIndex, #self.buttons) if self.selectedIndex ~= newIndex then - self.buttons[newIndex]:onClick(nil, 0) + self.buttons[newIndex]:action(nil, 0) end end diff --git a/client/src/ui/CharacterButton.lua b/client/src/ui/CharacterButton.lua index 52230d1a..d21163d6 100644 --- a/client/src/ui/CharacterButton.lua +++ b/client/src/ui/CharacterButton.lua @@ -125,6 +125,7 @@ function CharacterButton:onRelease(x, y, timeHeld) self:updateSuperSelectShader(0) if self:inBounds(x, y) then self:action(input.mouse, timeHeld) + self:onAction() end self.holdTime = 0 end @@ -144,6 +145,7 @@ function CharacterButton:receiveInputs(cursor, dt) else -- apply the actual click on release with the held time and reset it afterwards self:action(inputs, self.holdTime) + self:onAction() self.holdTime = 0 self.holding = false end diff --git a/client/src/ui/ImageContainer.lua b/client/src/ui/ImageContainer.lua index 25fde6b0..cb885bc5 100644 --- a/client/src/ui/ImageContainer.lua +++ b/client/src/ui/ImageContainer.lua @@ -3,12 +3,28 @@ local UiElement = import("./UIElement") local class = require("common.lib.class") local GraphicsUtil = require("client.src.graphics.graphics_util") -local ImageContainer = class(function(self, options) +---@class ImageOptions : UiElementOptions +---@field image love.Texture +---@field scalePower integer the power of scale to which the element snaps, e.g. if it's 2, only allow scale 0.25, 0.5, 1, 2, 4, 8 + +---@class Image : UiElement +---@operator call(ImageOptions): Image +---@overload fun(options: ImageOptions): Image +---@field image love.Texture +---@field drawBorders boolean +---@field outlineColor color +local ImageContainer = class( +function(self, options) self.drawBorders = options.drawBorders or false self.outlineColor = options.outlineColor or {1, 1, 1, 1} + self.scalePower = options.scalePower or 2 + self:setImage(options.image, options.width, options.height, options.scale) -end, UiElement) +end, +UiElement) + +ImageContainer.TYPE = "Image" function ImageContainer:setImage(image, width, height, scale) self.image = image @@ -39,11 +55,12 @@ function ImageContainer:setImage(image, width, height, scale) end function ImageContainer:onResized() - self.scale = math.min(self.width / self.imageWidth, self.height / self.imageHeight) + self.scale = math.floor(math.min(self.width / self.imageWidth, self.height / self.imageHeight)) end function ImageContainer:drawSelf() - GraphicsUtil.draw(self.image, 0, 0, 0, self.scale, self.scale) + local x, y = GraphicsUtil.getAlignmentOffset(self, {width = self.imageWidth * self.scale, height = self.imageHeight * self.scale}) + GraphicsUtil.draw(self.image, x, y, 0, self.scale, self.scale) if self.drawBorders then -- border is just drawn on top, not around diff --git a/client/src/ui/Label.lua b/client/src/ui/Label.lua index fc1b3caf..99a522ad 100644 --- a/client/src/ui/Label.lua +++ b/client/src/ui/Label.lua @@ -8,7 +8,7 @@ local HorizontalWrapLayout = import("./Layouts.HorizontalWrapLayout") ---@field id string? The localization key; nil if there should be no translation ---@field text string? The raw text; ignored if there is a localization key ---@field replacements string[]? Additional strings to perform string format on a localized key with parts marked for replacement ----@field fontSize integer? The size of the font +---@field fontSize FontSize? The size of the font ---@field wrap boolean? If the font should wrap around ---@class Label : UiElement @@ -106,7 +106,14 @@ function Label:refreshLocalization() end function Label:drawSelf() - GraphicsUtil.printf(self.text, 0, 0, self.width, self.hAlign, nil, nil, self.fontSize) + UIElement.drawSelf(self) + local y = 0 + if self.vAlign == "center" then + y = self.height / 2 - GraphicsUtil.getTextHeightForWidth(self.fontSize, self.text, self.width, self.hAlign) / 2 + elseif self.vAlign == "bottom" then + y = self.height - GraphicsUtil.getTextHeightForWidth(self.fontSize, self.text, self.width, self.hAlign) + end + GraphicsUtil.printf(self.text, 0, y, self.width, self.hAlign, nil, nil, self.fontSize) end function Label:getPreferredWidth() diff --git a/client/src/ui/MenuItem.lua b/client/src/ui/MenuItem.lua index 1abc83f5..b71dc541 100644 --- a/client/src/ui/MenuItem.lua +++ b/client/src/ui/MenuItem.lua @@ -53,7 +53,7 @@ function MenuItem.createMenuItem(label, item) end -- Creates just a button as buttons already have their own hover draw -function MenuItem.createButtonMenuItem(text, replacements, translate, onClick) +function MenuItem.createButtonMenuItem(text, replacements, translate, action) assert(text ~= nil) local id if translate == nil or translate then @@ -73,7 +73,7 @@ function MenuItem.createButtonMenuItem(text, replacements, translate, onClick) width = 140, maxWidth = 300, padding = 8, - onClick = onClick, + action = action, hAlign = "center", vAlign = "center", }) @@ -105,7 +105,17 @@ function MenuItem.createLabeledButtonMenuItem(labelText, labelTextReplacements, replacements = labelTextReplacements, vAlign = "center" }) - local textButton = TextButton({label = Label({text = buttonText, replacements = buttonTextReplacements, translate = buttonTextTranslate, hAlign = "center", vAlign = "center"}), onClick = buttonOnClick, width = BUTTON_WIDTH}) + local textButton = TextButton({ + label = Label({ + text = buttonText, + replacements = buttonTextReplacements, + translate = buttonTextTranslate, + hAlign = "center", + vAlign = "center" + }), + action = buttonOnClick, + width = BUTTON_WIDTH + }) return MenuItem.createMenuItem(label, textButton) end diff --git a/client/src/ui/PagedUniGrid.lua b/client/src/ui/PagedUniGrid.lua index 8ce7ee2d..6b6c0c1e 100644 --- a/client/src/ui/PagedUniGrid.lua +++ b/client/src/ui/PagedUniGrid.lua @@ -50,7 +50,7 @@ local PagedUniGrid = class(function(self, options) vAlign = "top", width = self.unitSize / 2, height = self.unitSize / 2, - onClick = function(selfElement, inputSource, holdTime) self:turnPage(-1) end, + action = function(selfElement, inputSource, holdTime) self:turnPage(-1) end, }) self.pageTurnButtons.right = TextButton({ label = Label({text = ">", translate = false}), @@ -58,7 +58,7 @@ local PagedUniGrid = class(function(self, options) vAlign = "top", width = self.unitSize / 2, height = self.unitSize / 2, - onClick = function(selfElement, inputSource, holdTime) self:turnPage(1) end, + action = function(selfElement, inputSource, holdTime) self:turnPage(1) end, }) addNewPage(self) goToPage(self, 1) diff --git a/client/src/ui/PanelSetButton.lua b/client/src/ui/PanelSetButton.lua index f4c4b6ad..5a4e9665 100644 --- a/client/src/ui/PanelSetButton.lua +++ b/client/src/ui/PanelSetButton.lua @@ -37,6 +37,8 @@ function (self, options) end, Button) +PanelButton.TYPE = "PanelButton" + ---@param inputSource table function PanelButton:action(inputSource) ---@type MatchParticipant diff --git a/client/src/ui/StageButton.lua b/client/src/ui/StageButton.lua new file mode 100644 index 00000000..42d1d983 --- /dev/null +++ b/client/src/ui/StageButton.lua @@ -0,0 +1,65 @@ +local import = require("common.lib.import") +local class = require("common.lib.class") +local Button = import("./Button") +local Label = import("./Label") +local Image = import("./ImageContainer") +local VerticalFlexLayout = import("./Layouts/VerticalFlexLayout") + +---@class StageButtonOptions : ButtonOptions +---@field stage Stage + +---@class StageButton : Button +---@operator call(StageButtonOptions): StageButton +---@overload fun(options: StageButtonOptions): StageButton +---@field stage Stage +local StageButton = class( +---@param self StageButton +---@param options StageButtonOptions +function(self, options) + self.stage = options.stage + + self.childGap = 4 + + self.hAlign = "center" + self.vAlign = "center" + + self.image = Image({ + image = self.stage.images.thumbnail, + hFill = true, + vFill = true, + hAlign = "center", + vAlign = "center", + backgroundColor = {0.2, 0.7, 0.3, 0.6} + }) + + self.label = Label({ + text = self.stage.display_name, + hAlign = "center", + vAlign = "center", + vFill = true, + backgroundColor = {0.2, 0.1, 0.7, 0.6} + }) + + self:addChild(self.image) + self:addChild(self.label) +end, +Button) + +StageButton.TYPE = "StageButton" +StageButton.layout = VerticalFlexLayout + +function StageButton:action(inputSource) + if not inputSource or not inputSource.player then + return + else + local player = inputSource.player + player:setStage(self.stage.id) + GAME.theme:playValidationSfx() + end +end + +function StageButton:drawSelf() + +end + +return StageButton \ No newline at end of file diff --git a/client/src/ui/Stepper.lua b/client/src/ui/Stepper.lua index c8d2336e..4f279d0b 100644 --- a/client/src/ui/Stepper.lua +++ b/client/src/ui/Stepper.lua @@ -24,7 +24,7 @@ local Stepper = class( vAlign = "center", hAlign = "center", label = Label({text = "<"}), - onClick = function(selfElement, inputSource, holdTime) + action = function(selfElement, inputSource, holdTime) self:setState(self.selectedIndex - 1) end }) @@ -37,7 +37,7 @@ local Stepper = class( vAlign = "center", hAlign = "center", label = Label({text = ">"}), - onClick = function(selfElement, inputSource, holdTime) + action = function(selfElement, inputSource, holdTime) self:setState(self.selectedIndex + 1) end }) diff --git a/client/src/ui/init.lua b/client/src/ui/init.lua index 8eb0c7ad..fa8eb4ae 100644 --- a/client/src/ui/init.lua +++ b/client/src/ui/init.lua @@ -32,6 +32,7 @@ local ui = { Grid = import("./Grid"), GridCursor = import("./GridCursor"), ---@source ImageContainer.lua + ---@type Image ImageContainer = import("./ImageContainer"), ---@source ImageCursor.lua ImageCursor = import("./ImageCursor"), @@ -73,6 +74,9 @@ local ui = { ---@type Slider Slider = import("./Slider"), StackPanel = import("./StackPanel"), + ---@source StageButton.lua + ---@type StageButton + StageButton = import("./StageButton"), StageCarousel = import("./StageCarousel"), Stepper = import("./Stepper"), ---@source TextButton.lua From 9d58ae1bd927c1cd203ad63c57011bad1361c14a Mon Sep 17 00:00:00 2001 From: Endaris Date: Wed, 11 Jun 2025 15:50:05 +0200 Subject: [PATCH 49/49] fix some stuff about images and stagebuttons --- client/src/ui/ImageContainer.lua | 45 +++++++++++++++++++++++++++----- client/src/ui/Label.lua | 1 - client/src/ui/StageButton.lua | 15 +++++++---- 3 files changed, 48 insertions(+), 13 deletions(-) diff --git a/client/src/ui/ImageContainer.lua b/client/src/ui/ImageContainer.lua index cb885bc5..6a4c0b9c 100644 --- a/client/src/ui/ImageContainer.lua +++ b/client/src/ui/ImageContainer.lua @@ -5,7 +5,8 @@ local GraphicsUtil = require("client.src.graphics.graphics_util") ---@class ImageOptions : UiElementOptions ---@field image love.Texture ----@field scalePower integer the power of scale to which the element snaps, e.g. if it's 2, only allow scale 0.25, 0.5, 1, 2, 4, 8 +---@field forceIntegerScaling boolean? force the image scale to snap to integer scales even if it doesn't fill up the available space, e.g. 1/4, 1/3, 1/2, 1/1, 2/1, 3/1 etc. +---@field minScale number? ---@class Image : UiElement ---@operator call(ImageOptions): Image @@ -13,12 +14,20 @@ local GraphicsUtil = require("client.src.graphics.graphics_util") ---@field image love.Texture ---@field drawBorders boolean ---@field outlineColor color +---@field forceIntegerScaling boolean if the image scale is forced to snap to a fraction number with 1 in numerator or denominator +---@field minScale number the minimum the image can be scaled down to local ImageContainer = class( function(self, options) self.drawBorders = options.drawBorders or false self.outlineColor = options.outlineColor or {1, 1, 1, 1} - self.scalePower = options.scalePower or 2 + if options.forceIntegerScaling ~= nil then + self.forceIntegerScaling = options.forceIntegerScaling + else + self.forceIntegerScaling = false + end + + self.minScale = 0.125 self:setImage(options.image, options.width, options.height, options.scale) end, @@ -29,17 +38,17 @@ ImageContainer.TYPE = "Image" function ImageContainer:setImage(image, width, height, scale) self.image = image self.imageWidth, self.imageHeight = self.image:getDimensions() - self.minWidth = math.max(self.imageWidth, self.minWidth) - self.minHeight = math.max(self.imageHeight, self.minHeight) + self.minWidth = math.max(self.imageWidth * self.minScale, self.minWidth) + self.minHeight = math.max(self.imageHeight * self.minScale, self.minHeight) if self.hFill and self.vFill then self.scale = math.min(self.width / self.imageWidth, self.height / self.imageHeight) else scale = scale or 1 - + local scaledImageWidth = self.imageWidth * scale local scaledImageHeight = self.imageHeight * scale - + if width and height then -- scale is getting capped to what width and height actually give us self.scale = math.min(width / scaledImageWidth, height / scaledImageHeight) @@ -55,10 +64,24 @@ function ImageContainer:setImage(image, width, height, scale) end function ImageContainer:onResized() - self.scale = math.floor(math.min(self.width / self.imageWidth, self.height / self.imageHeight)) + self.scale = math.min(self.width / self.imageWidth, self.height / self.imageHeight) + if self.forceIntegerScaling then + if self.scale >= 1 then + self.scale = math.floor(self.scale) + else + for i = 1, math.floor(1 / self.minScale) do + if 1 / i < self.scale then + self.scale = 1 / i + break + end + end + end + end end function ImageContainer:drawSelf() + UiElement.drawSelf(self) + local x, y = GraphicsUtil.getAlignmentOffset(self, {width = self.imageWidth * self.scale, height = self.imageHeight * self.scale}) GraphicsUtil.draw(self.image, x, y, 0, self.scale, self.scale) @@ -70,4 +93,12 @@ function ImageContainer:drawSelf() end end +function ImageContainer:getPreferredWidth() + return self.imageWidth +end + +function ImageContainer:getPreferredHeight() + return self.imageHeight * (self.width / self.imageWidth) +end + return ImageContainer diff --git a/client/src/ui/Label.lua b/client/src/ui/Label.lua index 99a522ad..090bd018 100644 --- a/client/src/ui/Label.lua +++ b/client/src/ui/Label.lua @@ -52,7 +52,6 @@ local Label = class( self.maxHeight = options.maxHeight or math.huge self.hFill = true - self.vFill = false end, UIElement ) diff --git a/client/src/ui/StageButton.lua b/client/src/ui/StageButton.lua index 42d1d983..fdf76cb0 100644 --- a/client/src/ui/StageButton.lua +++ b/client/src/ui/StageButton.lua @@ -4,6 +4,7 @@ local Button = import("./Button") local Label = import("./Label") local Image = import("./ImageContainer") local VerticalFlexLayout = import("./Layouts/VerticalFlexLayout") +local UiElement = import("./UIElement") ---@class StageButtonOptions : ButtonOptions ---@field stage Stage @@ -19,25 +20,29 @@ function(self, options) self.stage = options.stage self.childGap = 4 + self.debug = true self.hAlign = "center" self.vAlign = "center" + --self.backgroundColor = {0.7, 0.3, 0.2, 0.6} + self.image = Image({ image = self.stage.images.thumbnail, hFill = true, vFill = true, hAlign = "center", - vAlign = "center", - backgroundColor = {0.2, 0.7, 0.3, 0.6} + vAlign = "bottom", + --backgroundColor = {0.2, 0.7, 0.3, 0.6}, + forceIntegerScaling = true, }) self.label = Label({ text = self.stage.display_name, hAlign = "center", - vAlign = "center", + vAlign = "top", vFill = true, - backgroundColor = {0.2, 0.1, 0.7, 0.6} + --backgroundColor = {0.2, 0.1, 0.7, 0.6} }) self:addChild(self.image) @@ -59,7 +64,7 @@ function StageButton:action(inputSource) end function StageButton:drawSelf() - + UiElement.drawSelf(self) end return StageButton \ No newline at end of file