immediate mode state management cleanup

This commit is contained in:
Michael Freno
2025-11-06 00:11:43 -05:00
parent 0177195061
commit 3a82bcee8e
6 changed files with 629 additions and 638 deletions

View File

@@ -20,7 +20,6 @@ local NinePatchParser = req("NinePatchParser")
local utils = req("utils")
local Units = req("Units")
local GuiState = req("GuiState")
local ImmediateModeState = req("ImmediateModeState")
local StateManager = req("StateManager")
-- externals
@@ -95,7 +94,7 @@ function Gui.init(config)
-- Configure state management
if config.stateRetentionFrames or config.maxStateEntries then
ImmediateModeState.configure({
StateManager.configure({
stateRetentionFrames = config.stateRetentionFrames,
maxStateEntries = config.maxStateEntries,
})
@@ -126,7 +125,7 @@ end
function Gui.setMode(mode)
if mode == "immediate" then
Gui._immediateMode = true
Gui._immediateModeState = ImmediateModeState
Gui._immediateModeState = StateManager
-- Reset frame state
Gui._frameStarted = false
Gui._autoBeganFrame = false
@@ -157,7 +156,6 @@ function Gui.beginFrame()
-- Increment frame counter
Gui._frameNumber = Gui._frameNumber + 1
ImmediateModeState.incrementFrame()
StateManager.incrementFrame()
-- Clear current frame elements
@@ -166,6 +164,9 @@ 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
--- End the current immediate mode frame
@@ -174,6 +175,9 @@ function Gui.endFrame()
return
end
-- Sort elements by z-index for occlusion detection
GuiState.sortElementsByZIndex()
-- Auto-update all top-level elements (triggers layout calculation and overflow detection)
-- This must happen BEFORE saving state so that scroll positions and overflow are calculated
for _, element in ipairs(Gui._currentFrameElements) do
@@ -187,7 +191,7 @@ function Gui.endFrame()
-- Save state back for all elements created this frame
for _, element in ipairs(Gui._currentFrameElements) do
if element.id and element.id ~= "" then
local state = ImmediateModeState.getState(element.id, {})
local state = StateManager.getState(element.id, {})
-- Save stateful properties back to persistent state
state._pressed = element._pressed
@@ -210,16 +214,14 @@ function Gui.endFrame()
state._hoveredScrollbar = element._hoveredScrollbar
state._scrollbarDragOffset = element._scrollbarDragOffset
ImmediateModeState.setState(element.id, state)
StateManager.setState(element.id, state)
end
end
-- Cleanup stale states
ImmediateModeState.cleanup()
StateManager.cleanup()
-- Force cleanup if we have too many states
ImmediateModeState.forceCleanupIfNeeded()
StateManager.forceCleanupIfNeeded()
-- Clear frame started flag
@@ -476,7 +478,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]
@@ -505,11 +507,32 @@ function Gui.wheelmoved(x, y)
return nil
end
-- In immediate mode, use current frame elements; in retained mode, use topElements
local elements = Gui._immediateMode and Gui._currentFrameElements or Gui.topElements
local scrollableElement = findScrollableAtPosition(elements, mx, my)
if scrollableElement then
scrollableElement:_handleWheelScroll(x, y)
-- In immediate mode, use z-index ordered elements and respect occlusion
if Gui._immediateMode then
-- 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
if (overflowX == "scroll" or overflowX == "auto" or overflowY == "scroll" or overflowY == "auto") and (element._overflowX or element._overflowY) then
element:_handleWheelScroll(x, y)
return
end
end
end
else
-- In retained mode, use the old tree traversal method
local scrollableElement = findScrollableAtPosition(Gui.topElements, mx, my)
if scrollableElement then
scrollableElement:_handleWheelScroll(x, y)
end
end
end
@@ -551,14 +574,14 @@ function Gui.new(props)
-- Immediate mode: generate ID if not provided
if not props.id then
props.id = ImmediateModeState.generateID(props)
props.id = StateManager.generateID(props)
end
-- Get or create state for this element
local state = ImmediateModeState.getState(props.id, {})
local state = StateManager.getState(props.id, {})
-- Mark state as used this frame
ImmediateModeState.markStateUsed(props.id)
StateManager.markStateUsed(props.id)
-- Create the element
local element = Element.new(props)
@@ -589,24 +612,23 @@ function Gui.new(props)
-- Use the same ID for StateManager so state persists across frames
element._stateId = props.id
-- Load interactive state from StateManager
local interactiveState = StateManager.getState(props.id)
element._scrollbarHoveredVertical = interactiveState.scrollbarHoveredVertical
element._scrollbarHoveredHorizontal = interactiveState.scrollbarHoveredHorizontal
element._scrollbarDragging = interactiveState.scrollbarDragging
element._hoveredScrollbar = interactiveState.hoveredScrollbar
element._scrollbarDragOffset = interactiveState.scrollbarDragOffset or 0
-- Load interactive state from StateManager (already loaded in 'state' variable above)
element._scrollbarHoveredVertical = state.scrollbarHoveredVertical
element._scrollbarHoveredHorizontal = state.scrollbarHoveredHorizontal
element._scrollbarDragging = state.scrollbarDragging
element._hoveredScrollbar = state.hoveredScrollbar
element._scrollbarDragOffset = state.scrollbarDragOffset or 0
-- Set initial theme state based on StateManager state
-- This will be updated in Element:update() but we need an initial value
if element.themeComponent then
if element.disabled or interactiveState.disabled then
if element.disabled or state.disabled then
element._themeState = "disabled"
elseif element.active or interactiveState.active then
elseif element.active or state.active then
element._themeState = "active"
elseif interactiveState.pressed then
elseif state.pressed then
element._themeState = "pressed"
elseif interactiveState.hover then
elseif state.hover then
element._themeState = "hover"
else
element._themeState = "normal"
@@ -630,7 +652,7 @@ function Gui.getStateCount()
if not Gui._immediateMode then
return 0
end
return ImmediateModeState.getStateCount()
return StateManager.getStateCount()
end
--- Clear state for a specific element ID
@@ -639,7 +661,7 @@ function Gui.clearState(id)
if not Gui._immediateMode then
return
end
ImmediateModeState.clearState(id)
StateManager.clearState(id)
end
--- Clear all immediate mode states
@@ -647,7 +669,7 @@ function Gui.clearAllStates()
if not Gui._immediateMode then
return
end
ImmediateModeState.clearAllStates()
StateManager.clearAllStates()
end
--- Get state statistics (for debugging)
@@ -656,7 +678,7 @@ function Gui.getStateStats()
if not Gui._immediateMode then
return { stateCount = 0, frameNumber = 0 }
end
return ImmediateModeState.getStats()
return StateManager.getStats()
end
--- Helper function: Create a button with default styling
@@ -702,7 +724,7 @@ Gui.ImageDataReader = ImageDataReader
Gui.ImageRenderer = ImageRenderer
Gui.ImageScaler = ImageScaler
Gui.NinePatchParser = NinePatchParser
Gui.ImmediateModeState = ImmediateModeState
Gui.StateManager = StateManager
return {
Gui = Gui,