cleaned up rendering mode swapping

This commit is contained in:
Michael Freno
2025-12-12 00:08:25 -05:00
parent 9d8f6aa60d
commit ec73d8c7c8
11 changed files with 1270 additions and 46 deletions

View File

@@ -38,7 +38,18 @@ function Context.registerElement(element)
end
function Context.clearFrameElements()
Context._zIndexOrderedElements = {}
-- Preserve retained-mode elements
if Context._immediateMode then
local retainedElements = {}
for _, element in ipairs(Context._zIndexOrderedElements) do
if element._elementMode == "retained" then
table.insert(retainedElements, element)
end
end
Context._zIndexOrderedElements = retainedElements
else
Context._zIndexOrderedElements = {}
end
end
--- Sort elements by z-index (called after all elements are registered)

View File

@@ -188,6 +188,24 @@ end
---@param props ElementProps
---@return Element
function Element.new(props)
-- Early check: If this is a retained-mode element in an immediate-mode context,
-- check if it already exists (was restored to parent) to prevent duplicates
local elementMode = props.mode
if elementMode == nil then
elementMode = Element._Context._immediateMode and "immediate" or "retained"
end
-- If retained mode and has an ID, check if element already exists in parent's children
if elementMode == "retained" and props.id and props.id ~= "" and props.parent then
-- Check if this element already exists in parent's restored children
for _, child in ipairs(props.parent.children) do
if child.id == props.id and child._elementMode == "retained" then
-- Element already exists (was restored), return existing instance
return child
end
end
end
local self = setmetatable({}, Element)
-- Create dependency subsets for sub-modules (defined once, used throughout)
@@ -272,11 +290,56 @@ function Element.new(props)
self.children = {}
self.onEvent = props.onEvent
-- Auto-generate ID in immediate mode if not provided
if self._elementMode == "immediate" and (not props.id or props.id == "") then
-- Track whether ID was auto-generated (before ID assignment)
local idWasAutoGenerated = not props.id or props.id == ""
-- Auto-generate ID if not provided (for all elements)
if idWasAutoGenerated then
self.id = Element._StateManager.generateID(props, props.parent)
else
self.id = props.id or ""
self.id = props.id
end
-- AFTER ID is determined, check for duplicate top-level OR child retained elements
-- ONLY for auto-generated IDs (same call site recreating same element)
-- If user provides explicit ID, they control uniqueness
if self._elementMode == "retained" and idWasAutoGenerated and self.id and self.id ~= "" then
if not props.parent then
-- Top-level element: check in topElements
for _, existingElement in ipairs(Element._Context.topElements) do
if existingElement.id == self.id and existingElement._elementMode == "retained" then
-- Element already exists (was preserved from previous frame), return existing instance
-- CRITICAL: Clear children array to prevent accumulation
-- Children will be re-declared this frame (retained children will be found via duplicate check)
existingElement.children = {}
return existingElement
end
end
else
-- Child element: check in parent's children
for _, existingChild in ipairs(props.parent.children) do
if existingChild.id == self.id and existingChild._elementMode == "retained" then
-- Element already exists (was restored to parent), return existing instance
-- CRITICAL: Clear children array to prevent accumulation
-- Children will be re-declared this frame (retained children will be found via duplicate check)
existingChild.children = {}
return existingChild
end
end
end
end
-- In immediate mode, restore retained children from StateManager
-- This allows retained-mode children to persist when immediate-mode parents recreate
if self._elementMode == "immediate" and self.id and self.id ~= "" then
local retainedChildren = Element._StateManager.getRetainedChildren(self.id)
if retainedChildren and #retainedChildren > 0 then
-- Restore retained children and update their parent references
for _, child in ipairs(retainedChildren) do
child.parent = self
table.insert(self.children, child)
end
end
end
self.userdata = props.userdata
@@ -2237,6 +2300,11 @@ function Element:destroy()
-- Clear children table
self.children = {}
-- Clear retained children from StateManager (if this is an immediate-mode element)
if self._elementMode == "immediate" and self.id and self.id ~= "" then
Element._StateManager.clearRetainedChildren(self.id)
end
-- Clear parent reference
if self.parent then
self.parent = nil
@@ -3503,6 +3571,12 @@ function Element:saveState()
state._textDragOccurred = self._textDragOccurred
end
-- Save retained children references (for mixed-mode trees)
-- Only save if this is an immediate-mode element with retained children
if self._elementMode == "immediate" and #self.children > 0 then
Element._StateManager.saveRetainedChildren(self.id, self.children)
end
return state
end

View File

@@ -64,6 +64,9 @@ local stateDefaults = {
_cursorVisible = true,
_cursorBlinkPaused = false,
_cursorBlinkPauseTimer = 0,
-- Retained children references (for mixed-mode trees)
retainedChildren = nil,
}
--- Check if a value equals the default for a key
@@ -208,16 +211,24 @@ function StateManager.generateID(props, parent)
-- If we have a parent, use tree-based ID generation for stability
if parent and parent.id and parent.id ~= "" then
-- Count how many children the parent currently has
-- This gives us a stable sibling index
local siblingIndex = #(parent.children or {})
-- For child elements, use call-site (file + line) like top-level elements
-- This ensures the same call site always generates the same ID, even when
-- retained children persist in parent.children array
local baseID = parent.id .. "_" .. locationKey
-- Count how many children have been created at THIS call site
local callSiteKey = parent.id .. "_" .. locationKey
callSiteCounters[callSiteKey] = (callSiteCounters[callSiteKey] or 0) + 1
local instanceNum = callSiteCounters[callSiteKey]
if instanceNum > 1 then
baseID = baseID .. "_" .. instanceNum
end
-- Generate ID based on parent ID + sibling position (NO line number for stability)
-- This ensures the same position in the tree always gets the same ID
local baseID = parent.id .. "_child" .. siblingIndex
-- Add property hash if provided (for additional differentiation at same position)
if props then
-- Add property hash if provided (for additional differentiation)
-- IMPORTANT: Skip property hash for retained-mode elements to ensure ID stability
-- Retained elements should persist across frames even if props change slightly
if props and props.mode ~= "retained" then
local propHash = hashProps(props)
if propHash ~= "" then
-- Use first 8 chars of a simple hash
@@ -245,7 +256,9 @@ function StateManager.generateID(props, parent)
end
-- Add property hash if provided (for additional differentiation)
if props then
-- IMPORTANT: Skip property hash for retained-mode elements to ensure ID stability
-- Retained elements should persist across frames even if props change slightly
if props and props.mode ~= "retained" then
local propHash = hashProps(props)
if propHash ~= "" then
-- Use first 8 chars of a simple hash
@@ -655,4 +668,72 @@ function StateManager.isActive(id)
return state.active or false
end
-- ====================
-- Retained Children Management (for mixed-mode trees)
-- ====================
--- Save retained children for an element
--- Only stores children that are in retained mode
---@param id string Parent element ID
---@param children table Array of child elements
function StateManager.saveRetainedChildren(id, children)
if not id or not children then
return
end
-- Filter to only retained-mode children
local retainedChildren = {}
for _, child in ipairs(children) do
if child._elementMode == "retained" then
table.insert(retainedChildren, child)
end
end
-- Only save if we have retained children
if #retainedChildren > 0 then
local state = StateManager.getState(id)
state.retainedChildren = retainedChildren
end
end
--- Get retained children for an element
--- Returns an array of retained-mode child elements
---@param id string Parent element ID
---@return table children Array of retained child elements (empty if none)
function StateManager.getRetainedChildren(id)
if not id then
return {}
end
local state = StateManager.getCurrentState(id)
if state.retainedChildren then
-- Verify children still exist (weren't destroyed)
local validChildren = {}
for _, child in ipairs(state.retainedChildren) do
-- Children are element objects, check if they're still valid
-- A destroyed element would have nil references or be garbage collected
if child and type(child) == "table" and child.id then
table.insert(validChildren, child)
end
end
return validChildren
end
return {}
end
--- Clear retained children for an element
--- Used when parent is destroyed or children are manually removed
---@param id string Parent element ID
function StateManager.clearRetainedChildren(id)
if not id then
return
end
local state = StateManager.getCurrentState(id)
if state.retainedChildren then
state.retainedChildren = nil
end
end
return StateManager