scrolling fixed in immediate mode

This commit is contained in:
Michael Freno
2025-11-06 09:39:56 -05:00
parent f65d4b312b
commit 6a0fcfdfbd
2 changed files with 48 additions and 31 deletions

View File

@@ -164,7 +164,7 @@ function Gui.beginFrame()
-- Clear top elements (they will be recreated this frame) -- Clear top elements (they will be recreated this frame)
Gui.topElements = {} Gui.topElements = {}
-- Clear z-index ordered elements from previous frame -- Clear z-index ordered elements from previous frame
GuiState.clearFrameElements() GuiState.clearFrameElements()
end end
@@ -178,7 +178,15 @@ function Gui.endFrame()
-- Sort elements by z-index for occlusion detection -- Sort elements by z-index for occlusion detection
GuiState.sortElementsByZIndex() 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 -- This must happen BEFORE saving state so that scroll positions and overflow are calculated
for _, element in ipairs(Gui._currentFrameElements) do for _, element in ipairs(Gui._currentFrameElements) do
-- Only update top-level elements (those without parents in the current frame) -- 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 for _, win in ipairs(Gui.topElements) do
-- Only draw with backdrop canvas if this element tree has backdrop blur -- Only draw with backdrop canvas if this element tree has backdrop blur
local needsBackdrop = hasBackdropBlur(win) local needsBackdrop = hasBackdropBlur(win)
if needsBackdrop then if needsBackdrop then
-- Draw element with backdrop blur applied -- Draw element with backdrop blur applied
win:draw(backdropCanvas) win:draw(backdropCanvas)
@@ -478,7 +486,7 @@ end
--- Handle mouse wheel scrolling --- Handle mouse wheel scrolling
function Gui.wheelmoved(x, y) function Gui.wheelmoved(x, y)
local mx, my = love.mouse.getPosition() local mx, my = love.mouse.getPosition()
local function findScrollableAtPosition(elements, mx, my) local function findScrollableAtPosition(elements, mx, my)
for i = #elements, 1, -1 do for i = #elements, 1, -1 do
local element = elements[i] local element = elements[i]
@@ -512,12 +520,12 @@ function Gui.wheelmoved(x, y)
-- Find topmost scrollable element at mouse position using z-index ordering -- Find topmost scrollable element at mouse position using z-index ordering
for i = #GuiState._zIndexOrderedElements, 1, -1 do for i = #GuiState._zIndexOrderedElements, 1, -1 do
local element = GuiState._zIndexOrderedElements[i] local element = GuiState._zIndexOrderedElements[i]
local bx = element.x local bx = element.x
local by = element.y local by = element.y
local bw = element._borderBoxWidth or (element.width + element.padding.left + element.padding.right) 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) 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 if mx >= bx and mx <= bx + bw and my >= by and my <= by + bh then
local overflowX = element.overflowX or element.overflow local overflowX = element.overflowX or element.overflow
local overflowY = element.overflowY or element.overflow local overflowY = element.overflowY or element.overflow
@@ -583,6 +591,11 @@ function Gui.new(props)
-- Mark state as used this frame -- Mark state as used this frame
StateManager.markStateUsed(props.id) 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 -- Create the element
local element = Element.new(props) local element = Element.new(props)
@@ -602,11 +615,11 @@ function Gui.new(props)
element._selectionStart = state._selectionStart element._selectionStart = state._selectionStart
element._selectionEnd = state._selectionEnd element._selectionEnd = state._selectionEnd
element._textBuffer = state._textBuffer or element.text or "" element._textBuffer = state._textBuffer or element.text or ""
element._scrollX = state._scrollX or element._scrollX or 0 -- Note: scroll position already set from props during Element.new()
element._scrollY = state._scrollY or element._scrollY or 0 -- element._scrollX and element._scrollY already restored
element._scrollbarDragging = state._scrollbarDragging or false element._scrollbarDragging = state._scrollbarDragging ~= nil and state._scrollbarDragging or false
element._hoveredScrollbar = state._hoveredScrollbar 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 -- Bind element to StateManager for interactive states
-- Use the same ID for StateManager so state persists across frames -- Use the same ID for StateManager so state persists across frames

View File

@@ -185,14 +185,14 @@ function Element.new(props)
local self = setmetatable({}, Element) local self = setmetatable({}, Element)
self.children = {} self.children = {}
self.callback = props.callback self.callback = props.callback
-- Auto-generate ID in immediate mode if not provided -- Auto-generate ID in immediate mode if not provided
if Gui._immediateMode and (not props.id or props.id == "") then if Gui._immediateMode and (not props.id or props.id == "") then
self.id = StateManager.generateID(props) self.id = StateManager.generateID(props)
else else
self.id = props.id or "" self.id = props.id or ""
end end
self.userdata = props.userdata self.userdata = props.userdata
-- Input event callbacks -- Input event callbacks
@@ -217,7 +217,7 @@ function Element.new(props)
-- Initialize theme state (will be managed by StateManager in immediate mode) -- Initialize theme state (will be managed by StateManager in immediate mode)
self._themeState = "normal" self._themeState = "normal"
-- Initialize state manager ID for immediate mode (use self.id which may be auto-generated) -- Initialize state manager ID for immediate mode (use self.id which may be auto-generated)
self._stateId = self.id self._stateId = self.id
@@ -1165,9 +1165,9 @@ function Element.new(props)
self._contentWidth = 0 self._contentWidth = 0
self._contentHeight = 0 self._contentHeight = 0
-- Scroll state -- Scroll state (can be restored from props in immediate mode)
self._scrollX = 0 self._scrollX = props._scrollX or 0
self._scrollY = 0 self._scrollY = props._scrollY or 0
self._maxScrollX = 0 self._maxScrollX = 0
self._maxScrollY = 0 self._maxScrollY = 0
@@ -1286,7 +1286,7 @@ function Element:setScrollPosition(x, y)
if y ~= nil then if y ~= nil then
self._scrollY = math.max(0, math.min(y, self._maxScrollY)) self._scrollY = math.max(0, math.min(y, self._maxScrollY))
end end
-- Note: Scroll position is saved to ImmediateModeState in Gui.endFrame() -- Note: Scroll position is saved to ImmediateModeState in Gui.endFrame()
-- No need to save here -- No need to save here
end end
@@ -1536,7 +1536,7 @@ function Element:_handleScrollbarPress(mouseX, mouseY, button)
local thumbX = trackX + dims.horizontal.thumbX local thumbX = trackX + dims.horizontal.thumbX
self._scrollbarDragOffset = mouseX - thumbX self._scrollbarDragOffset = mouseX - thumbX
end end
-- Update StateManager if in immediate mode -- Update StateManager if in immediate mode
if self._stateId and Gui._immediateMode then if self._stateId and Gui._immediateMode then
StateManager.updateState(self._stateId, { StateManager.updateState(self._stateId, {
@@ -1614,14 +1614,14 @@ function Element:_handleScrollbarRelease(button)
if self._scrollbarDragging then if self._scrollbarDragging then
self._scrollbarDragging = false self._scrollbarDragging = false
-- Update StateManager if in immediate mode -- Update StateManager if in immediate mode
if self._stateId and Gui._immediateMode then if self._stateId and Gui._immediateMode then
StateManager.updateState(self._stateId, { StateManager.updateState(self._stateId, {
scrollbarDragging = false, scrollbarDragging = false,
}) })
end end
return true return true
end end
@@ -1928,7 +1928,11 @@ function Element:addChild(child)
end end
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 end
--- Apply positioning offsets (top, right, bottom, left) to an element --- 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 if not scrollbar and not self._scrollbarDragging then
self._hoveredScrollbar = nil self._hoveredScrollbar = nil
end end
-- Update scrollbar state in StateManager if in immediate mode -- Update scrollbar state in StateManager if in immediate mode
if self._stateId and Gui._immediateMode then if self._stateId and Gui._immediateMode then
StateManager.updateState(self._stateId, { StateManager.updateState(self._stateId, {
@@ -2884,7 +2888,7 @@ function Element:update(dt)
elseif self._scrollbarDragging then elseif self._scrollbarDragging then
-- Mouse button released -- Mouse button released
self._scrollbarDragging = false self._scrollbarDragging = false
-- Update StateManager if in immediate mode -- Update StateManager if in immediate mode
if self._stateId and Gui._immediateMode then if self._stateId and Gui._immediateMode then
StateManager.updateState(self._stateId, { StateManager.updateState(self._stateId, {
@@ -2963,7 +2967,7 @@ function Element:update(dt)
-- Update theme state based on interaction -- Update theme state based on interaction
if self.themeComponent then if self.themeComponent then
local newThemeState = "normal" local newThemeState = "normal"
-- Disabled state takes priority -- Disabled state takes priority
if self.disabled then if self.disabled then
newThemeState = "disabled" newThemeState = "disabled"
@@ -2987,14 +2991,14 @@ function Element:update(dt)
newThemeState = "hover" newThemeState = "hover"
end end
end end
-- Update state (in StateManager if in immediate mode, otherwise locally) -- Update state (in StateManager if in immediate mode, otherwise locally)
if self._stateId and Gui._immediateMode then if self._stateId and Gui._immediateMode then
-- Update in StateManager for immediate mode -- Update in StateManager for immediate mode
local hover = newThemeState == "hover" local hover = newThemeState == "hover"
local pressed = newThemeState == "pressed" local pressed = newThemeState == "pressed"
local focused = newThemeState == "active" or self._focused local focused = newThemeState == "active" or self._focused
StateManager.updateState(self._stateId, { StateManager.updateState(self._stateId, {
hover = hover, hover = hover,
pressed = pressed, pressed = pressed,
@@ -3003,7 +3007,7 @@ function Element:update(dt)
active = self.active, active = self.active,
}) })
end end
-- Always update local state for backward compatibility -- Always update local state for backward compatibility
self._themeState = newThemeState self._themeState = newThemeState
end end
@@ -3012,15 +3016,15 @@ function Element:update(dt)
-- and this is the topmost element at the mouse position (z-index ordering) -- 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) -- Exception: Allow drag continuation even if occluded (once drag starts, it continues)
local isDragging = false 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 if self._pressed[button] and love.mouse.isDown(button) then
isDragging = true isDragging = true
break break
end end
end end
local canProcessEvents = self.callback and not self.disabled and (isActiveElement or isDragging) local canProcessEvents = self.callback and not self.disabled and (isActiveElement or isDragging)
if canProcessEvents then if canProcessEvents then
-- Check all three mouse buttons -- Check all three mouse buttons
local buttons = { 1, 2, 3 } -- left, right, middle 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 currentLength = utf8.len(buffer) or 0
local textLength = utf8.len(text) or 0 local textLength = utf8.len(text) or 0
local newLength = currentLength + textLength local newLength = currentLength + textLength
if newLength > self.maxLength then if newLength > self.maxLength then
-- Don't insert if it would exceed maxLength -- Don't insert if it would exceed maxLength
return return