From 6a0fcfdfbd0ba0b1aebc5ddf7d6dcb199b2c1e3e Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Thu, 6 Nov 2025 09:39:56 -0500 Subject: [PATCH] scrolling fixed in immediate mode --- FlexLove.lua | 33 ++++++++++++++++++++++---------- modules/Element.lua | 46 ++++++++++++++++++++++++--------------------- 2 files changed, 48 insertions(+), 31 deletions(-) diff --git a/FlexLove.lua b/FlexLove.lua index 700eb96..55b8e7b 100644 --- a/FlexLove.lua +++ b/FlexLove.lua @@ -164,7 +164,7 @@ function Gui.beginFrame() -- Clear top elements (they will be recreated this frame) Gui.topElements = {} - + -- Clear z-index ordered elements from previous frame GuiState.clearFrameElements() end @@ -178,7 +178,15 @@ function Gui.endFrame() -- Sort elements by z-index for occlusion detection GuiState.sortElementsByZIndex() - -- Auto-update all top-level elements (triggers layout calculation and overflow detection) + -- Layout all top-level elements now that all children have been added + -- This ensures overflow detection happens with complete child lists + for _, element in ipairs(Gui._currentFrameElements) do + if not element.parent then + element:layoutChildren() -- Layout with all children present + end + end + + -- Auto-update all top-level elements (triggers additional state updates) -- This must happen BEFORE saving state so that scroll positions and overflow are calculated for _, element in ipairs(Gui._currentFrameElements) do -- Only update top-level elements (those without parents in the current frame) @@ -305,7 +313,7 @@ function Gui.draw(gameDrawFunc, postDrawFunc) for _, win in ipairs(Gui.topElements) do -- Only draw with backdrop canvas if this element tree has backdrop blur local needsBackdrop = hasBackdropBlur(win) - + if needsBackdrop then -- Draw element with backdrop blur applied win:draw(backdropCanvas) @@ -478,7 +486,7 @@ end --- Handle mouse wheel scrolling function Gui.wheelmoved(x, y) local mx, my = love.mouse.getPosition() - + local function findScrollableAtPosition(elements, mx, my) for i = #elements, 1, -1 do local element = elements[i] @@ -512,12 +520,12 @@ function Gui.wheelmoved(x, y) -- Find topmost scrollable element at mouse position using z-index ordering for i = #GuiState._zIndexOrderedElements, 1, -1 do local element = GuiState._zIndexOrderedElements[i] - + local bx = element.x local by = element.y local bw = element._borderBoxWidth or (element.width + element.padding.left + element.padding.right) local bh = element._borderBoxHeight or (element.height + element.padding.top + element.padding.bottom) - + if mx >= bx and mx <= bx + bw and my >= by and my <= by + bh then local overflowX = element.overflowX or element.overflow local overflowY = element.overflowY or element.overflow @@ -583,6 +591,11 @@ function Gui.new(props) -- Mark state as used this frame StateManager.markStateUsed(props.id) + -- Inject scroll state into props BEFORE creating element + -- This ensures scroll position is set before layoutChildren/detectOverflow is called + props._scrollX = state._scrollX or 0 + props._scrollY = state._scrollY or 0 + -- Create the element local element = Element.new(props) @@ -602,11 +615,11 @@ function Gui.new(props) element._selectionStart = state._selectionStart element._selectionEnd = state._selectionEnd element._textBuffer = state._textBuffer or element.text or "" - element._scrollX = state._scrollX or element._scrollX or 0 - element._scrollY = state._scrollY or element._scrollY or 0 - element._scrollbarDragging = state._scrollbarDragging or false + -- Note: scroll position already set from props during Element.new() + -- element._scrollX and element._scrollY already restored + element._scrollbarDragging = state._scrollbarDragging ~= nil and state._scrollbarDragging or false element._hoveredScrollbar = state._hoveredScrollbar - element._scrollbarDragOffset = state._scrollbarDragOffset or 0 + element._scrollbarDragOffset = state._scrollbarDragOffset ~= nil and state._scrollbarDragOffset or 0 -- Bind element to StateManager for interactive states -- Use the same ID for StateManager so state persists across frames diff --git a/modules/Element.lua b/modules/Element.lua index a678f2b..2a3c967 100644 --- a/modules/Element.lua +++ b/modules/Element.lua @@ -185,14 +185,14 @@ function Element.new(props) local self = setmetatable({}, Element) self.children = {} self.callback = props.callback - + -- Auto-generate ID in immediate mode if not provided if Gui._immediateMode and (not props.id or props.id == "") then self.id = StateManager.generateID(props) else self.id = props.id or "" end - + self.userdata = props.userdata -- Input event callbacks @@ -217,7 +217,7 @@ function Element.new(props) -- Initialize theme state (will be managed by StateManager in immediate mode) self._themeState = "normal" - + -- Initialize state manager ID for immediate mode (use self.id which may be auto-generated) self._stateId = self.id @@ -1165,9 +1165,9 @@ function Element.new(props) self._contentWidth = 0 self._contentHeight = 0 - -- Scroll state - self._scrollX = 0 - self._scrollY = 0 + -- Scroll state (can be restored from props in immediate mode) + self._scrollX = props._scrollX or 0 + self._scrollY = props._scrollY or 0 self._maxScrollX = 0 self._maxScrollY = 0 @@ -1286,7 +1286,7 @@ function Element:setScrollPosition(x, y) if y ~= nil then self._scrollY = math.max(0, math.min(y, self._maxScrollY)) end - + -- Note: Scroll position is saved to ImmediateModeState in Gui.endFrame() -- No need to save here end @@ -1536,7 +1536,7 @@ function Element:_handleScrollbarPress(mouseX, mouseY, button) local thumbX = trackX + dims.horizontal.thumbX self._scrollbarDragOffset = mouseX - thumbX end - + -- Update StateManager if in immediate mode if self._stateId and Gui._immediateMode then StateManager.updateState(self._stateId, { @@ -1614,14 +1614,14 @@ function Element:_handleScrollbarRelease(button) if self._scrollbarDragging then self._scrollbarDragging = false - + -- Update StateManager if in immediate mode if self._stateId and Gui._immediateMode then StateManager.updateState(self._stateId, { scrollbarDragging = false, }) end - + return true end @@ -1928,7 +1928,11 @@ function Element:addChild(child) end end - self:layoutChildren() + -- In immediate mode, defer layout until endFrame() when all elements are created + -- This prevents premature overflow detection with incomplete children + if not Gui._immediateMode then + self:layoutChildren() + end end --- Apply positioning offsets (top, right, bottom, left) to an element @@ -2867,7 +2871,7 @@ function Element:update(dt) if not scrollbar and not self._scrollbarDragging then self._hoveredScrollbar = nil end - + -- Update scrollbar state in StateManager if in immediate mode if self._stateId and Gui._immediateMode then StateManager.updateState(self._stateId, { @@ -2884,7 +2888,7 @@ function Element:update(dt) elseif self._scrollbarDragging then -- Mouse button released self._scrollbarDragging = false - + -- Update StateManager if in immediate mode if self._stateId and Gui._immediateMode then StateManager.updateState(self._stateId, { @@ -2963,7 +2967,7 @@ function Element:update(dt) -- Update theme state based on interaction if self.themeComponent then local newThemeState = "normal" - + -- Disabled state takes priority if self.disabled then newThemeState = "disabled" @@ -2987,14 +2991,14 @@ function Element:update(dt) newThemeState = "hover" end end - + -- Update state (in StateManager if in immediate mode, otherwise locally) if self._stateId and Gui._immediateMode then -- Update in StateManager for immediate mode local hover = newThemeState == "hover" local pressed = newThemeState == "pressed" local focused = newThemeState == "active" or self._focused - + StateManager.updateState(self._stateId, { hover = hover, pressed = pressed, @@ -3003,7 +3007,7 @@ function Element:update(dt) active = self.active, }) end - + -- Always update local state for backward compatibility self._themeState = newThemeState end @@ -3012,15 +3016,15 @@ function Element:update(dt) -- and this is the topmost element at the mouse position (z-index ordering) -- Exception: Allow drag continuation even if occluded (once drag starts, it continues) local isDragging = false - for _, button in ipairs({1, 2, 3}) do + for _, button in ipairs({ 1, 2, 3 }) do if self._pressed[button] and love.mouse.isDown(button) then isDragging = true break end end - + local canProcessEvents = self.callback and not self.disabled and (isActiveElement or isDragging) - + if canProcessEvents then -- Check all three mouse buttons local buttons = { 1, 2, 3 } -- left, right, middle @@ -4004,7 +4008,7 @@ function Element:insertText(text, position) local currentLength = utf8.len(buffer) or 0 local textLength = utf8.len(text) or 0 local newLength = currentLength + textLength - + if newLength > self.maxLength then -- Don't insert if it would exceed maxLength return