From 8e16ffcc9385587c365deb9ab3e870373d836ef9 Mon Sep 17 00:00:00 2001 From: makatymba2001 Date: Thu, 26 Feb 2026 16:11:41 +0200 Subject: [PATCH 01/10] `no_overflow` property implementation & SMODS.UIScrollBox implementation --- lovely/overflow.toml | 64 ++++++++++++++ src/ui.lua | 199 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 263 insertions(+) create mode 100644 lovely/overflow.toml diff --git a/lovely/overflow.toml b/lovely/overflow.toml new file mode 100644 index 000000000..804cd26c0 --- /dev/null +++ b/lovely/overflow.toml @@ -0,0 +1,64 @@ +[manifest] +version = "1.0.0" +dump_lua = true +priority = -10 + +# Allow use stencil in global canvas +# Game:draw() +[[patches]] +[patches.pattern] +target = "game.lua" +pattern = '''love.graphics.setCanvas{self.CANVAS}''' +position = "at" +payload = ''' +love.graphics.setCanvas{self.CANVAS, stencil = true} +''' +match_indent = true + +# Limit overflow container size +# UIBox:calculate_xywh() +[[patches]] +[patches.pattern] +target = "engine/ui.lua" +pattern = '''_nt.h = math.max(_ct.h + padding, _nt.h)-- ''' +position = "after" +payload = ''' +if node.config and node.config.no_overflow then + if node.config.w then + _nt.w = node.config.w + elseif node.config.maxw then + _nt.w = math.min(_nt.w, node.config.maxw) + end + if node.config.h then + _nt.h = node.config.h + elseif node.config.maxh then + _nt.h = math.min(_nt.h, node.config.maxh) + end +end +''' +match_indent = true + +# Prevent text be rescaled by overflow container +# UIBox:calculate_xywh() +[[patches]] +[patches.pattern] +target = "engine/ui.lua" +pattern = '''fac = fac*restriction/(node.config.maxw and _ct.w or _ct.h)''' +position = "after" +payload = ''' +if node.config.no_overflow then fac = _scale or 1 end +''' +match_indent = true + +# Exclude overflowed elements from colliding +# Controller:get_cursor_collision() +[[patches]] +[patches.pattern] +target = "engine/controller.lua" +pattern = '''if v:collides_with_point(cursor_trans) and not v.REMOVED then''' +position = "at" +payload = ''' +if v:collides_with_point(cursor_trans) and not v.REMOVED and v:inside_overflow_boundaries(cursor_trans) then +''' +match_indent = true +overwrite = false diff --git a/src/ui.lua b/src/ui.lua index bcda315e4..95f53e4f3 100644 --- a/src/ui.lua +++ b/src/ui.lua @@ -1,6 +1,205 @@ SMODS.GUI = {} SMODS.GUI.DynamicUIManager = {} +-- used to properly truncate overflow content inside another overflow content +SMODS.stencil_stack = {} + +function SMODS.push_to_stencil_stack(stencil_fn) + assert(type(stencil_fn) == "function", "No stencil function passed to SMODS.push_to_stencil_stack") + local old_level = #SMODS.stencil_stack + local new_level = old_level + 1 + + love.graphics.setStencilTest("equal", old_level) + love.graphics.stencil(function() + stencil_fn(false) + end, "increment", 1, true) + love.graphics.setStencilTest("equal", new_level) + + SMODS.stencil_stack[new_level] = stencil_fn +end +function SMODS.pop_from_stencil_stack() + local old_level = #SMODS.stencil_stack + local new_level = old_level - 1 + + local stencil_fn = SMODS.stencil_stack[old_level] + if not stencil_fn then + return + end + + love.graphics.setStencilTest("equal", old_level) + love.graphics.stencil(function() + stencil_fn(true) + end, "decrement", 1, true) + love.graphics.setStencilTest("equal", new_level) + + SMODS.stencil_stack[old_level] = nil +end +function SMODS.reset_stencil_stack() + EMPTY(SMODS.stencil_stack) + love.graphics.setStencilTest() +end + +local gameDrawRef = Game.draw +function Game:draw(...) + SMODS.reset_stencil_stack() + gameDrawRef(self, ...) +end + +-- + +local uieDrawChildrenRef = UIElement.draw_children +function UIElement:draw_children(...) + local stenciled = false + if self.states.visible and self.config and self.config.no_overflow then + -- draw stencil for overflow container + stenciled = true + SMODS.push_to_stencil_stack(function(exit) + prep_draw(self, 1) + love.graphics.scale(1 / G.TILESIZE) + love.graphics.setColor(0, 0, 0, 1) + + if self.config.r and self.VT.w > 0.01 then + self:draw_pixellated_rect("fill", 0) + else + love.graphics.rectangle("fill", 0, 0, self.VT.w * G.TILESIZE, self.VT.h * G.TILESIZE) + end + + love.graphics.pop() + end) + end + uieDrawChildrenRef(self, ...) + -- cancel stencil for overflow container + if stenciled then SMODS.pop_from_stencil_stack() end +end + + +-- collision check +function Node:inside_overflow_boundaries(point) + -- Use cached value if present + if self.overflow_check_timer == G.TIMERS.REAL then + return self.overflow_check_result or false + end + self.overflow_check_timer = G.TIMERS.REAL + local r = true + + -- No parent = no overflow can be done so collide as usual + if not self.parent then r = true + -- If parent has overflow then we should check do we collide with it and if not, all children in it cannot be collided too + elseif self.parent.config and self.parent.config.no_overflow and not Node.collides_with_point(self.parent, point) then r = false + -- Otherwise process all parents looking for first non-collideable overflow + else r = Node.inside_overflow_boundaries(self.parent, point) end + + self.overflow_check_result = r + return r +end + +-- + + +SMODS.UIScrollBox = UIBox:extend() +function SMODS.UIScrollBox:init(args) + args.content = args.content or {} + args.container = args.container or {} + args.overflow = args.overflow or {} + + self.scroll_args = args + self.scroll_progress = args.progress or { x = 0, y = 0 } + self.scroll_offset = args.offset or { x = 0, y = 0 } + self.scroll_sync_mode = args.sync_mode or "progress" + + if args.content and args.content.is and args.content:is(Object) then + self.content = args.content + else + self.content = UIBox(args.content) + end + + args.container.config = args.container.config or {} + args.container.config.align = args.container.config.align or "cm" + args.container.config.offset = args.container.config.offset or { x = 0, y = 0 } + args.container.node_config = args.container.node_config or {} + args.container.node_config.colour = args.container.node_config.colour or G.C.CLEAR + args.container.definition = { + n = G.UIT.ROOT, + config = args.container.node_config, + nodes = { + { + n = G.UIT.O, + config = { + object = self.content, + }, + }, + }, + } + self.content_container = UIBox(args.container) + + args.overflow.config = args.overflow.config or {} + args.overflow.node_config = args.overflow.node_config or {} + args.overflow.node_config.colour = args.overflow.node_config.colour or G.C.CLEAR + args.overflow.node_config.no_overflow = true + args.overflow.definition = { + n = G.UIT.ROOT, + config = args.overflow.node_config, + nodes = { + { + n = G.UIT.O, + config = { + object = self.content_container, + }, + }, + }, + } + + UIBox.init(self, args.overflow) + + self:sync_scroll(0, true) +end +-- Returns distance content overflows in both directions +function SMODS.UIScrollBox:get_scroll_distance() + return math.max(0, self.content_container.T.w - self.T.w), math.max(0, self.content_container.T.h - self.T.h) +end +-- Update offset to match progress +function SMODS.UIScrollBox:sync_scroll_offset() + local dx, dy = self:get_scroll_distance() + self.scroll_offset.x = dx * (self.scroll_progress.x or 0) + self.scroll_offset.y = dy * (self.scroll_progress.y or 0) +end +-- Update progress to match offset +function SMODS.UIScrollBox:sync_scroll_progress() + local dx, dy = self:get_scroll_distance() + self.scroll_progress.x = (dx == 0 and 0) or ((self.offset.x or 0) / dx) + self.scroll_progress.y = (dy == 0 and 0) or ((self.offset.y or 0) / dy) +end +-- Set new value for offset table +function SMODS.UIScrollBox:set_scroll_offset(t) + self.scroll_offset = t or {} + self:sync_scroll_progress() +end +-- Set new value for progress table +function SMODS.UIScrollBox:set_scroll_progress(t) + self.scroll_progress = t or {} + self:sync_scroll_offset() +end +-- Sync things +function SMODS.UIScrollBox:sync_scroll(dt, init) + if self.scroll_sync_mode == "none" then + elseif self.scroll_sync_mode == "offset" then + self:sync_scroll_progress() + else + self:sync_scroll_offset() + end + self.content_container.config.offset.x = -(self.scroll_offset.x or 0) + self.content_container.config.offset.y = -(self.scroll_offset.y or 0) +end +function SMODS.UIScrollBox:update(dt) + if self.scroll_args.scroll_move then + self.scroll_args.scroll_move(self, dt) + end + self:sync_scroll(dt) + UIBox.update(self, dt) +end + +-- + function STR_UNPACK(str) local chunk, err = loadstring(str) if chunk then From fcd7711ba26b94146ffa3923d490221200c1d9f3 Mon Sep 17 00:00:00 2001 From: makatymba2001 Date: Thu, 26 Feb 2026 20:22:20 +0200 Subject: [PATCH 02/10] lsp is done I guess? --- lovely/{overflow.toml => ui_overflow.toml} | 0 lsp_def/classes/ui_scroll_box.lua | 45 ++++++++++++++++++++++ lsp_def/ui.lua | 15 ++++++++ src/ui.lua | 6 --- 4 files changed, 60 insertions(+), 6 deletions(-) rename lovely/{overflow.toml => ui_overflow.toml} (100%) create mode 100644 lsp_def/classes/ui_scroll_box.lua diff --git a/lovely/overflow.toml b/lovely/ui_overflow.toml similarity index 100% rename from lovely/overflow.toml rename to lovely/ui_overflow.toml diff --git a/lsp_def/classes/ui_scroll_box.lua b/lsp_def/classes/ui_scroll_box.lua new file mode 100644 index 000000000..4ba015888 --- /dev/null +++ b/lsp_def/classes/ui_scroll_box.lua @@ -0,0 +1,45 @@ +---@meta + +---@class SMODS.UIScrollBox.Input +---@field content Moveable | { definition: UINode, config: table, T?: table } Moveable or UIBox definition which will be rendered inside scrollable content (passed to G.UIT.O). +---@field container? { node_config?: UINode.config, config?: table, T?: table } UIBox args for scroll container which will be moved to create scroll effect. +---@field overflow? { node_config?: UINode.config, config?: table, T?: table } UIBox args for main element. +---@field progress? { x: number, y: number } Value of scroll content relative offset in directions (0-1). Keeps reference for original table. +---@field offset? { x: number, y: number } Value of scroll content absolute offset in directions (in game units). Keeps reference for original table. +---@field sync_mode? "offset" | "progress" | "none" Sync mode. `offset` sync progress to match offset, `progress` sync offset to match progress, `none` disables syncing. Default is `progress`. +---@field scroll_move? fun(self: SMODS.UIScrollBox, dt: number) Function which calld every frame before scroll syncing and can be used to perform automatic scrolling. + +---@class SMODS.UIScrollBox: UIBox +---@field content Moveable Displayed content. +---@field content_container UIBox Container which positions `content` according to scroll offset. +---@field scroll_args SMODS.UIScrollBox.Input Input args +---@field scroll_progress { x: number, y: number } Value of scroll content relative offset in directions (0-1). Keeps reference for original table. +---@field scroll_offset { x: number, y: number } Value of scroll content absolute offset in directions (in game units). Keeps reference for original table. +---@field scroll_sync_mode "offset" | "progress" | "none" Sync mode. `offset` sync progress to match offset, `progress` sync offset to match progress, `none` disables syncing. Default is `progress`. +---@overload fun(args: SMODS.UIScrollBox.Input): SMODS.UIScrollBox +SMODS.UIScrollBox = {} +SMODS.UIScrollBox.__index = SMODS.UIScrollBox +SMODS.UIScrollBox.super = UIBox + +---@return number, number +--- Distance of content overflow in both directions +function SMODS.UIScrollBox:get_scroll_distance() end + +--- Update offset to match progress. Called every frame if `scroll_sync_mode = "progress"` +function SMODS.UIScrollBox:sync_scroll_offset() end + +--- Update progress to match offset. Called every frame if `scroll_sync_mode = "offset"` +function SMODS.UIScrollBox:sync_scroll_progress() end + +---@param t { x: number, y: number } +--- Set new table for offset (keeps reference), and sync progress to match new offset +function SMODS.UIScrollBox:set_scroll_offset(t) end + +---@param t { x: number, y: number } +--- Set new table for progress (keeps reference), and sync offset to match new progress +function SMODS.UIScrollBox:set_scroll_progress(t) end + +---@param dt number +---@param init? boolean Is sync called during initialization +--- Perform syncing according to `scroll_sync_mode`, and position elements to match result offset +function SMODS.UIScrollBox:sync_scroll(dt, init) end diff --git a/lsp_def/ui.lua b/lsp_def/ui.lua index d17b03a57..7d0ee58c4 100644 --- a/lsp_def/ui.lua +++ b/lsp_def/ui.lua @@ -3,6 +3,9 @@ SMODS.GUI = {} SMODS.GUI.DynamicUIManager = {} +--- @type table +SMODS.stencil_stack = {} + ---@type string|"achievements"|"config"|"credits"|"mod_desc"|"additions" SMODS.LAST_SELECTED_MOD_TAB = "" @@ -53,6 +56,7 @@ G.UIT = { ---@field vert? boolean Sets if the text is drawn vertically. ---@field object? Node Object to render. ---@field role? "Major"|"Minor"|"Glued" Sets object's role type. +---@field no_overflow? boolean Renders node as overflow container: constrain it's size, truncate drawing and prevent colliding child nodes which go outside of parent's boundaries --- Internal class for annotating UIBox/UIElement tables before being turned into objects. ---@class UINode: table @@ -61,6 +65,17 @@ G.UIT = { ---@field nodes? UINode[] Child UINodes -- UI Functions + +---@param stencil_fn fun(exit?: boolean) +--- Add new stencil to stencil stack; result stencil is sum of all stencils in stack +function SMODS.push_to_stencil_stack(stencil_fn) end + +--- Discard last applied stencil in stack +function SMODS.pop_from_stencil_stack() end + +--- Cleanup stencil stack +function SMODS.reset_stencil_stack() end + ---@param str string ---@return any --- Unpacks provided string. diff --git a/src/ui.lua b/src/ui.lua index 95f53e4f3..cd022457c 100644 --- a/src/ui.lua +++ b/src/ui.lua @@ -153,33 +153,27 @@ function SMODS.UIScrollBox:init(args) self:sync_scroll(0, true) end --- Returns distance content overflows in both directions function SMODS.UIScrollBox:get_scroll_distance() return math.max(0, self.content_container.T.w - self.T.w), math.max(0, self.content_container.T.h - self.T.h) end --- Update offset to match progress function SMODS.UIScrollBox:sync_scroll_offset() local dx, dy = self:get_scroll_distance() self.scroll_offset.x = dx * (self.scroll_progress.x or 0) self.scroll_offset.y = dy * (self.scroll_progress.y or 0) end --- Update progress to match offset function SMODS.UIScrollBox:sync_scroll_progress() local dx, dy = self:get_scroll_distance() self.scroll_progress.x = (dx == 0 and 0) or ((self.offset.x or 0) / dx) self.scroll_progress.y = (dy == 0 and 0) or ((self.offset.y or 0) / dy) end --- Set new value for offset table function SMODS.UIScrollBox:set_scroll_offset(t) self.scroll_offset = t or {} self:sync_scroll_progress() end --- Set new value for progress table function SMODS.UIScrollBox:set_scroll_progress(t) self.scroll_progress = t or {} self:sync_scroll_offset() end --- Sync things function SMODS.UIScrollBox:sync_scroll(dt, init) if self.scroll_sync_mode == "none" then elseif self.scroll_sync_mode == "offset" then From 6f2d5639c61b86e0100113368f3e96f912b4b387 Mon Sep 17 00:00:00 2001 From: makatymba2001 Date: Fri, 27 Feb 2026 04:17:15 +0200 Subject: [PATCH 03/10] fix no_overflow size constrain bypass during filling --- lovely/ui_overflow.toml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/lovely/ui_overflow.toml b/lovely/ui_overflow.toml index 804cd26c0..c1363f647 100644 --- a/lovely/ui_overflow.toml +++ b/lovely/ui_overflow.toml @@ -50,6 +50,29 @@ if node.config.no_overflow then fac = _scale or 1 end ''' match_indent = true +# Limit overflow container size during filling +# UIBox:set_wh() +[[patches]] +[patches.pattern] +target = "engine/ui.lua" +pattern = '''if w.UIT == G.UIT.C then w.T.h = _max_h end''' +position = "after" +payload = ''' +if w.config and w.config.no_overflow then + if w.config.w then + w.T.w = w.config.w + elseif w.config.maxw then + w.T.w = math.min(w.T.w, w.config.maxw) + end + if w.config.h then + w.T.h = w.config.h + elseif w.config.maxh then + w.T.h = math.min(w.T.h, w.config.maxh) + end +end +''' +match_indent = true + # Exclude overflowed elements from colliding # Controller:get_cursor_collision() [[patches]] From 636aa6493e27aad5c25a2ed3b562754907ef265c Mon Sep 17 00:00:00 2001 From: makatymba2001 Date: Fri, 27 Feb 2026 04:27:44 +0200 Subject: [PATCH 04/10] cache point in overflow boundaries check --- src/ui.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ui.lua b/src/ui.lua index cd022457c..1533ff37c 100644 --- a/src/ui.lua +++ b/src/ui.lua @@ -75,11 +75,12 @@ end -- collision check function Node:inside_overflow_boundaries(point) - -- Use cached value if present - if self.overflow_check_timer == G.TIMERS.REAL then + -- Use cached value if present for current point + if self.overflow_check_timer == G.TIMERS.REAL and self.overflow_check_point == point then return self.overflow_check_result or false end self.overflow_check_timer = G.TIMERS.REAL + self.overflow_check_point = point local r = true -- No parent = no overflow can be done so collide as usual From ae640121718db92aecb2ea9567aab0532bdf45b3 Mon Sep 17 00:00:00 2001 From: makatymba2001 Date: Fri, 27 Feb 2026 04:55:39 +0200 Subject: [PATCH 05/10] Correct target function in patch comment --- lovely/ui_overflow.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lovely/ui_overflow.toml b/lovely/ui_overflow.toml index c1363f647..884771ad5 100644 --- a/lovely/ui_overflow.toml +++ b/lovely/ui_overflow.toml @@ -51,7 +51,7 @@ if node.config.no_overflow then fac = _scale or 1 end match_indent = true # Limit overflow container size during filling -# UIBox:set_wh() +# UIElement:set_wh() [[patches]] [patches.pattern] target = "engine/ui.lua" From c1357fb7bca32345689fae8bfc60c4f5503558bf Mon Sep 17 00:00:00 2001 From: makatymba2001 Date: Fri, 27 Feb 2026 05:01:45 +0200 Subject: [PATCH 06/10] Add function to reload stencil stack --- lsp_def/ui.lua | 3 +++ src/ui.lua | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/lsp_def/ui.lua b/lsp_def/ui.lua index 7d0ee58c4..cc35036a3 100644 --- a/lsp_def/ui.lua +++ b/lsp_def/ui.lua @@ -76,6 +76,9 @@ function SMODS.pop_from_stencil_stack() end --- Cleanup stencil stack function SMODS.reset_stencil_stack() end +--- Reload stencil stack by cleaning up current stencil and redrawing all stencils from stack +function SMODS.reload_stencil_stack() end + ---@param str string ---@return any --- Unpacks provided string. diff --git a/src/ui.lua b/src/ui.lua index 1533ff37c..35fdb0da1 100644 --- a/src/ui.lua +++ b/src/ui.lua @@ -38,6 +38,13 @@ function SMODS.reset_stencil_stack() EMPTY(SMODS.stencil_stack) love.graphics.setStencilTest() end +function SMODS.reload_stencil_stack() + local stack_snapshot = SMODS.shallow_copy(SMODS.stencil_stack) + SMODS.reset_stencil_stack() + for _, stencil_fn in ipairs(stack_snapshot) do + SMODS.push_to_stencil_stack(stencil_fn) + end +end local gameDrawRef = Game.draw function Game:draw(...) From f24c42234aa1ff5894d6687ba8beb4f9b20206e3 Mon Sep 17 00:00:00 2001 From: makatymba2001 Date: Fri, 27 Feb 2026 05:15:23 +0200 Subject: [PATCH 07/10] Cleanup values in stencil on reset --- src/ui.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ui.lua b/src/ui.lua index 35fdb0da1..e75b976e0 100644 --- a/src/ui.lua +++ b/src/ui.lua @@ -37,6 +37,7 @@ end function SMODS.reset_stencil_stack() EMPTY(SMODS.stencil_stack) love.graphics.setStencilTest() + love.graphics.stencil(function() end) end function SMODS.reload_stencil_stack() local stack_snapshot = SMODS.shallow_copy(SMODS.stencil_stack) From 90a2dd4d80984cea9255c27f5a6fa310bfc23eeb Mon Sep 17 00:00:00 2001 From: makatymba2001 Date: Sat, 28 Feb 2026 14:57:39 +0200 Subject: [PATCH 08/10] Use SMODS.merge_defaults; Move LSP; Ready for merge? --- lsp_def/classes/ui_scroll_box.lua | 45 ------------------------------ lsp_def/ui.lua | 46 ++++++++++++++++++++++++++++++- src/ui.lua | 35 ++++++++++------------- 3 files changed, 59 insertions(+), 67 deletions(-) delete mode 100644 lsp_def/classes/ui_scroll_box.lua diff --git a/lsp_def/classes/ui_scroll_box.lua b/lsp_def/classes/ui_scroll_box.lua deleted file mode 100644 index 4ba015888..000000000 --- a/lsp_def/classes/ui_scroll_box.lua +++ /dev/null @@ -1,45 +0,0 @@ ----@meta - ----@class SMODS.UIScrollBox.Input ----@field content Moveable | { definition: UINode, config: table, T?: table } Moveable or UIBox definition which will be rendered inside scrollable content (passed to G.UIT.O). ----@field container? { node_config?: UINode.config, config?: table, T?: table } UIBox args for scroll container which will be moved to create scroll effect. ----@field overflow? { node_config?: UINode.config, config?: table, T?: table } UIBox args for main element. ----@field progress? { x: number, y: number } Value of scroll content relative offset in directions (0-1). Keeps reference for original table. ----@field offset? { x: number, y: number } Value of scroll content absolute offset in directions (in game units). Keeps reference for original table. ----@field sync_mode? "offset" | "progress" | "none" Sync mode. `offset` sync progress to match offset, `progress` sync offset to match progress, `none` disables syncing. Default is `progress`. ----@field scroll_move? fun(self: SMODS.UIScrollBox, dt: number) Function which calld every frame before scroll syncing and can be used to perform automatic scrolling. - ----@class SMODS.UIScrollBox: UIBox ----@field content Moveable Displayed content. ----@field content_container UIBox Container which positions `content` according to scroll offset. ----@field scroll_args SMODS.UIScrollBox.Input Input args ----@field scroll_progress { x: number, y: number } Value of scroll content relative offset in directions (0-1). Keeps reference for original table. ----@field scroll_offset { x: number, y: number } Value of scroll content absolute offset in directions (in game units). Keeps reference for original table. ----@field scroll_sync_mode "offset" | "progress" | "none" Sync mode. `offset` sync progress to match offset, `progress` sync offset to match progress, `none` disables syncing. Default is `progress`. ----@overload fun(args: SMODS.UIScrollBox.Input): SMODS.UIScrollBox -SMODS.UIScrollBox = {} -SMODS.UIScrollBox.__index = SMODS.UIScrollBox -SMODS.UIScrollBox.super = UIBox - ----@return number, number ---- Distance of content overflow in both directions -function SMODS.UIScrollBox:get_scroll_distance() end - ---- Update offset to match progress. Called every frame if `scroll_sync_mode = "progress"` -function SMODS.UIScrollBox:sync_scroll_offset() end - ---- Update progress to match offset. Called every frame if `scroll_sync_mode = "offset"` -function SMODS.UIScrollBox:sync_scroll_progress() end - ----@param t { x: number, y: number } ---- Set new table for offset (keeps reference), and sync progress to match new offset -function SMODS.UIScrollBox:set_scroll_offset(t) end - ----@param t { x: number, y: number } ---- Set new table for progress (keeps reference), and sync offset to match new progress -function SMODS.UIScrollBox:set_scroll_progress(t) end - ----@param dt number ----@param init? boolean Is sync called during initialization ---- Perform syncing according to `scroll_sync_mode`, and position elements to match result offset -function SMODS.UIScrollBox:sync_scroll(dt, init) end diff --git a/lsp_def/ui.lua b/lsp_def/ui.lua index cc35036a3..319177815 100644 --- a/lsp_def/ui.lua +++ b/lsp_def/ui.lua @@ -281,4 +281,48 @@ function create_UIBox_your_collection_stickers() end ---@return UINode ---@param args ScoreContainerArgs -function SMODS.GUI.score_container(args) end \ No newline at end of file +function SMODS.GUI.score_container(args) end + +---@class SMODS.UIScrollBox.input +---@field content Moveable | { definition: UINode, config: table, T?: table } Moveable or UIBox definition to render inside scrollable content (passed to G.UIT.O). +---@field container? { node_config?: UINode.config, config?: table, T?: table } UIBox args for scroll container which will be moved to create scroll effect. +---@field overflow? { node_config?: UINode.config, config?: table, T?: table } UIBox args for main element. +---@field progress? { x?: number, y?: number } Value of scroll content relative offset in directions (0-1). Keeps reference for original table. +---@field offset? { x?: number, y?: number } Value of scroll content absolute offset in directions (in game units). Keeps reference for original table. +---@field sync_mode? "offset" | "progress" | "none" Sync mode. `offset` sync progress to match offset, `progress` sync offset to match progress, `none` disables syncing. Default is `progress`. +---@field scroll_move? fun(self: SMODS.UIScrollBox, dt: number) Function which called every frame before scroll syncing and can be used to perform automatic scrolling. + +---@class SMODS.UIScrollBox: UIBox +---@field content Moveable Displayed content. +---@field content_container UIBox Container which positions `content` according to scroll offset. +---@field scroll_args SMODS.UIScrollBox.input Input args +---@field scroll_progress { x: number, y: number } Relative offset of scroll content in directions (0-1). Keeps reference for original table. +---@field scroll_offset { x: number, y: number } Absolute offset of scroll content in directions (in game units). Keeps reference for original table. +---@field scroll_sync_mode "offset" | "progress" | "none" Sync mode. `offset` sync progress to match offset, `progress` sync offset to match progress, `none` disables syncing. Default is `progress`. +---@overload fun(args: SMODS.UIScrollBox.input): SMODS.UIScrollBox +SMODS.UIScrollBox = {} +SMODS.UIScrollBox.__index = SMODS.UIScrollBox +SMODS.UIScrollBox.super = UIBox + +---@return number, number +--- Distance of content overflow in both directions +function SMODS.UIScrollBox:get_scroll_distance() end + +--- Update offset to match progress. Called every frame if `scroll_sync_mode = "progress"` +function SMODS.UIScrollBox:sync_scroll_offset() end + +--- Update progress to match offset. Called every frame if `scroll_sync_mode = "offset"` +function SMODS.UIScrollBox:sync_scroll_progress() end + +---@param t? { x?: number, y?: number } +--- Set new table for offset (keeps reference), and sync progress to match new offset +function SMODS.UIScrollBox:set_scroll_offset(t) end + +---@param t? { x?: number, y?: number } +--- Set new table for progress (keeps reference), and sync offset to match new progress +function SMODS.UIScrollBox:set_scroll_progress(t) end + +---@param dt number +---@param init? boolean Is sync called during initialization +--- Perform syncing according to `scroll_sync_mode`, and position elements to match result offset +function SMODS.UIScrollBox:sync_scroll(dt, init) end diff --git a/src/ui.lua b/src/ui.lua index e75b976e0..adab902bb 100644 --- a/src/ui.lua +++ b/src/ui.lua @@ -104,17 +104,16 @@ end -- - SMODS.UIScrollBox = UIBox:extend() function SMODS.UIScrollBox:init(args) - args.content = args.content or {} - args.container = args.container or {} - args.overflow = args.overflow or {} + args = SMODS.merge_defaults(args, { content = {}, container = {}, overflow = {}, sync_mode = "progress" }) + args.progress = SMODS.merge_defaults(args.progress, { x = 0, y = 0 }) + args.offset = SMODS.merge_defaults(args.offset, { x = 0, y = 0 }) self.scroll_args = args - self.scroll_progress = args.progress or { x = 0, y = 0 } - self.scroll_offset = args.offset or { x = 0, y = 0 } - self.scroll_sync_mode = args.sync_mode or "progress" + self.scroll_progress = args.progress + self.scroll_offset = args.offset + self.scroll_sync_mode = args.sync_mode if args.content and args.content.is and args.content:is(Object) then self.content = args.content @@ -122,11 +121,8 @@ function SMODS.UIScrollBox:init(args) self.content = UIBox(args.content) end - args.container.config = args.container.config or {} - args.container.config.align = args.container.config.align or "cm" - args.container.config.offset = args.container.config.offset or { x = 0, y = 0 } - args.container.node_config = args.container.node_config or {} - args.container.node_config.colour = args.container.node_config.colour or G.C.CLEAR + args.container.config = SMODS.merge_defaults(args.container.config, { align = "cm", offset = { x = 0, y = 0 } }) + args.container.node_config = SMODS.merge_defaults(args.container.node_config, { colour = G.C.CLEAR }) args.container.definition = { n = G.UIT.ROOT, config = args.container.node_config, @@ -141,10 +137,8 @@ function SMODS.UIScrollBox:init(args) } self.content_container = UIBox(args.container) - args.overflow.config = args.overflow.config or {} - args.overflow.node_config = args.overflow.node_config or {} - args.overflow.node_config.colour = args.overflow.node_config.colour or G.C.CLEAR - args.overflow.node_config.no_overflow = true + args.overflow.config = SMODS.merge_defaults(args.overflow.config, {}) + args.overflow.node_config = SMODS.merge_defaults(args.overflow.node_config, { colour = G.C.CLEAR, no_overflow = true }) args.overflow.definition = { n = G.UIT.ROOT, config = args.overflow.node_config, @@ -176,19 +170,18 @@ function SMODS.UIScrollBox:sync_scroll_progress() self.scroll_progress.y = (dy == 0 and 0) or ((self.offset.y or 0) / dy) end function SMODS.UIScrollBox:set_scroll_offset(t) - self.scroll_offset = t or {} + self.scroll_offset = SMODS.merge_defaults(t, { x = 0, y = 0 }) self:sync_scroll_progress() end function SMODS.UIScrollBox:set_scroll_progress(t) - self.scroll_progress = t or {} + self.scroll_progress = SMODS.merge_defaults(t, { x = 0, y = 0 }) self:sync_scroll_offset() end function SMODS.UIScrollBox:sync_scroll(dt, init) - if self.scroll_sync_mode == "none" then + if self.scroll_sync_mode == "progress" then + self:sync_scroll_offset() elseif self.scroll_sync_mode == "offset" then self:sync_scroll_progress() - else - self:sync_scroll_offset() end self.content_container.config.offset.x = -(self.scroll_offset.x or 0) self.content_container.config.offset.y = -(self.scroll_offset.y or 0) From bd9e3b451aa4a527874960e7f328cafb0f8cf920 Mon Sep 17 00:00:00 2001 From: makatymba2001 Date: Sat, 28 Feb 2026 15:06:04 +0200 Subject: [PATCH 09/10] Bring back line break here --- src/ui.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ui.lua b/src/ui.lua index adab902bb..cbc170cec 100644 --- a/src/ui.lua +++ b/src/ui.lua @@ -104,6 +104,7 @@ end -- + SMODS.UIScrollBox = UIBox:extend() function SMODS.UIScrollBox:init(args) args = SMODS.merge_defaults(args, { content = {}, container = {}, overflow = {}, sync_mode = "progress" }) From f430802a550688103e8d5d9431fe689060f92052 Mon Sep 17 00:00:00 2001 From: makatymba2001 Date: Sat, 28 Feb 2026 15:12:32 +0200 Subject: [PATCH 10/10] Nope, move it here --- lsp_def/ui.lua | 93 ++++++++++++++++++++++++++------------------------ src/ui.lua | 1 - 2 files changed, 48 insertions(+), 46 deletions(-) diff --git a/lsp_def/ui.lua b/lsp_def/ui.lua index 319177815..9d5963c1f 100644 --- a/lsp_def/ui.lua +++ b/lsp_def/ui.lua @@ -64,6 +64,53 @@ G.UIT = { ---@field config UINode.config Config of the UINode. ---@field nodes? UINode[] Child UINodes +-- + +---@class SMODS.UIScrollBox.input +---@field content Moveable | { definition: UINode, config: table, T?: table } Moveable or UIBox definition to render inside scrollable content (passed to G.UIT.O). +---@field container? { node_config?: UINode.config, config?: table, T?: table } UIBox args for scroll container which will be moved to create scroll effect. +---@field overflow? { node_config?: UINode.config, config?: table, T?: table } UIBox args for main element. +---@field progress? { x?: number, y?: number } Value of scroll content relative offset in directions (0-1). Keeps reference for original table. +---@field offset? { x?: number, y?: number } Value of scroll content absolute offset in directions (in game units). Keeps reference for original table. +---@field sync_mode? "offset" | "progress" | "none" Sync mode. `offset` sync progress to match offset, `progress` sync offset to match progress, `none` disables syncing. Default is `progress`. +---@field scroll_move? fun(self: SMODS.UIScrollBox, dt: number) Function which called every frame before scroll syncing and can be used to perform automatic scrolling. + +--- Element for displaying scrollable content +---@class SMODS.UIScrollBox: UIBox +---@field content Moveable Displayed content. +---@field content_container UIBox Container which positions `content` according to scroll offset. +---@field scroll_args SMODS.UIScrollBox.input Input args +---@field scroll_progress { x: number, y: number } Relative offset of scroll content in directions (0-1). Keeps reference for original table. +---@field scroll_offset { x: number, y: number } Absolute offset of scroll content in directions (in game units). Keeps reference for original table. +---@field scroll_sync_mode "offset" | "progress" | "none" Sync mode. `offset` sync progress to match offset, `progress` sync offset to match progress, `none` disables syncing. Default is `progress`. +---@overload fun(args: SMODS.UIScrollBox.input): SMODS.UIScrollBox +SMODS.UIScrollBox = {} +SMODS.UIScrollBox.__index = SMODS.UIScrollBox +SMODS.UIScrollBox.super = UIBox + +---@return number, number +--- Distance of content overflow in both directions +function SMODS.UIScrollBox:get_scroll_distance() end + +--- Update offset to match progress. Called every frame if `scroll_sync_mode = "progress"` +function SMODS.UIScrollBox:sync_scroll_offset() end + +--- Update progress to match offset. Called every frame if `scroll_sync_mode = "offset"` +function SMODS.UIScrollBox:sync_scroll_progress() end + +---@param t? { x?: number, y?: number } +--- Set new table for offset (keeps reference), and sync progress to match new offset +function SMODS.UIScrollBox:set_scroll_offset(t) end + +---@param t? { x?: number, y?: number } +--- Set new table for progress (keeps reference), and sync offset to match new progress +function SMODS.UIScrollBox:set_scroll_progress(t) end + +---@param dt number +---@param init? boolean Is sync called during initialization +--- Perform syncing according to `scroll_sync_mode`, and position elements to match result offset +function SMODS.UIScrollBox:sync_scroll(dt, init) end + -- UI Functions ---@param stencil_fn fun(exit?: boolean) @@ -281,48 +328,4 @@ function create_UIBox_your_collection_stickers() end ---@return UINode ---@param args ScoreContainerArgs -function SMODS.GUI.score_container(args) end - ----@class SMODS.UIScrollBox.input ----@field content Moveable | { definition: UINode, config: table, T?: table } Moveable or UIBox definition to render inside scrollable content (passed to G.UIT.O). ----@field container? { node_config?: UINode.config, config?: table, T?: table } UIBox args for scroll container which will be moved to create scroll effect. ----@field overflow? { node_config?: UINode.config, config?: table, T?: table } UIBox args for main element. ----@field progress? { x?: number, y?: number } Value of scroll content relative offset in directions (0-1). Keeps reference for original table. ----@field offset? { x?: number, y?: number } Value of scroll content absolute offset in directions (in game units). Keeps reference for original table. ----@field sync_mode? "offset" | "progress" | "none" Sync mode. `offset` sync progress to match offset, `progress` sync offset to match progress, `none` disables syncing. Default is `progress`. ----@field scroll_move? fun(self: SMODS.UIScrollBox, dt: number) Function which called every frame before scroll syncing and can be used to perform automatic scrolling. - ----@class SMODS.UIScrollBox: UIBox ----@field content Moveable Displayed content. ----@field content_container UIBox Container which positions `content` according to scroll offset. ----@field scroll_args SMODS.UIScrollBox.input Input args ----@field scroll_progress { x: number, y: number } Relative offset of scroll content in directions (0-1). Keeps reference for original table. ----@field scroll_offset { x: number, y: number } Absolute offset of scroll content in directions (in game units). Keeps reference for original table. ----@field scroll_sync_mode "offset" | "progress" | "none" Sync mode. `offset` sync progress to match offset, `progress` sync offset to match progress, `none` disables syncing. Default is `progress`. ----@overload fun(args: SMODS.UIScrollBox.input): SMODS.UIScrollBox -SMODS.UIScrollBox = {} -SMODS.UIScrollBox.__index = SMODS.UIScrollBox -SMODS.UIScrollBox.super = UIBox - ----@return number, number ---- Distance of content overflow in both directions -function SMODS.UIScrollBox:get_scroll_distance() end - ---- Update offset to match progress. Called every frame if `scroll_sync_mode = "progress"` -function SMODS.UIScrollBox:sync_scroll_offset() end - ---- Update progress to match offset. Called every frame if `scroll_sync_mode = "offset"` -function SMODS.UIScrollBox:sync_scroll_progress() end - ----@param t? { x?: number, y?: number } ---- Set new table for offset (keeps reference), and sync progress to match new offset -function SMODS.UIScrollBox:set_scroll_offset(t) end - ----@param t? { x?: number, y?: number } ---- Set new table for progress (keeps reference), and sync offset to match new progress -function SMODS.UIScrollBox:set_scroll_progress(t) end - ----@param dt number ----@param init? boolean Is sync called during initialization ---- Perform syncing according to `scroll_sync_mode`, and position elements to match result offset -function SMODS.UIScrollBox:sync_scroll(dt, init) end +function SMODS.GUI.score_container(args) end \ No newline at end of file diff --git a/src/ui.lua b/src/ui.lua index cbc170cec..adab902bb 100644 --- a/src/ui.lua +++ b/src/ui.lua @@ -104,7 +104,6 @@ end -- - SMODS.UIScrollBox = UIBox:extend() function SMODS.UIScrollBox:init(args) args = SMODS.merge_defaults(args, { content = {}, container = {}, overflow = {}, sync_mode = "progress" })