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

@@ -399,6 +399,7 @@ function flexlove.beginFrame()
-- Cleanup elements from PREVIOUS frame (after they've been drawn)
-- This breaks circular references and allows GC to collect memory
-- Note: Cleanup is minimal to preserve functionality
-- IMPORTANT: Only cleanup immediate-mode elements, preserve retained-mode elements
if flexlove._currentFrameElements then
local function cleanupChildren(elem)
for _, child in ipairs(elem.children) do
@@ -408,17 +409,31 @@ function flexlove.beginFrame()
end
for _, element in ipairs(flexlove._currentFrameElements) do
if not element.parent then
-- Only cleanup immediate-mode top-level elements
-- Retained-mode elements persist across frames
if not element.parent and element._elementMode == "immediate" then
cleanupChildren(element)
end
end
end
-- Preserve top-level retained elements before resetting
local retainedTopElements = {}
if flexlove.topElements then
for _, element in ipairs(flexlove.topElements) do
if element._elementMode == "retained" then
table.insert(retainedTopElements, element)
end
end
end
flexlove._frameNumber = flexlove._frameNumber + 1
StateManager.incrementFrame()
flexlove._currentFrameElements = {}
flexlove._frameStarted = true
flexlove.topElements = {}
-- Restore retained top-level elements
flexlove.topElements = retainedTopElements
Context.clearFrameElements()
end

View File

@@ -221,7 +221,7 @@ function love.draw()
width = "30vw",
height = "40vh",
backgroundColor = Color.new(0.2, 0.2, 0.2, 1),
-- No ID auto-generation since it's in retained mode
-- ID is auto-generated if not provided (works in both modes)
})
FlexLove.endFrame()
@@ -229,15 +229,51 @@ end
```
**Key behaviors:**
- `mode = "immediate"` - Element uses immediate-mode lifecycle (recreated each frame, auto-generates ID, uses StateManager)
- `mode = "retained"` - Element uses retained-mode lifecycle (persists across frames, no auto-ID, no StateManager)
- `mode = "immediate"` - Element uses immediate-mode lifecycle (recreated each frame, uses StateManager)
- `mode = "retained"` - Element uses retained-mode lifecycle (persists across frames, uses StateManager for mixed-mode children)
- `mode = nil` - Element inherits from global mode setting (default behavior)
- **ID auto-generation** - All elements receive an auto-generated ID if not explicitly provided, enabling state management features
- Mode does NOT inherit from parent to child - each element independently controls its own lifecycle
- Common use cases:
- Performance-critical static UI in retained mode while using immediate mode globally
- Reactive debug overlays in immediate mode within a retained-mode application
- Mixed UI where some components are static (menus) and others are dynamic (HUD)
#### Automatic ID Generation
All elements automatically receive a unique ID if not explicitly provided. This enables powerful state management features:
```lua
-- ID is automatically generated based on call site
local button = FlexLove.new({
text = "Click Me",
-- id is auto-generated (e.g., "main_L42_child0_123456")
})
-- Or provide a custom ID for explicit control
local namedButton = FlexLove.new({
id = "submit_button",
text = "Submit",
})
```
**ID Generation Strategy:**
- **Top-level elements**: ID based on source file and line number (e.g., `main_L42`)
- **Child elements**: ID based on parent ID + sibling index (e.g., `parent_child0`, `parent_child1`)
- **Multiple elements at same location**: Automatically numbered (e.g., `main_L42_1`, `main_L42_2`)
- **Property differentiation**: Similar elements get unique hash suffixes based on their properties
**Benefits of Auto-Generated IDs:**
- Enables state persistence in immediate mode (scroll position, input text, animations)
- Allows retained children in immediate parents (mixed-mode trees)
- Supports state management features without manual ID tracking
- Stable across frames when element structure is consistent
**When to Use Custom IDs:**
- When you need to reference elements programmatically
- For debugging and logging (readable names)
- When element creation order might change but identity should remain stable
### Element Properties
Common properties for all elements:
@@ -303,6 +339,9 @@ Common properties for all elements:
-- Rendering Mode
mode = nil, -- "immediate", "retained", or nil (uses global setting)
-- Element Identity
id = nil, -- Element ID (auto-generated if not provided)
-- Hierarchy
parent = nil, -- Parent element
}

View File

@@ -0,0 +1,219 @@
-- Demo: Retained children with immediate parents
-- Shows how retained-mode children persist when immediate-mode parents recreate each frame
local FlexLove = require("FlexLove")
-- Track frame count for demo
local frameCount = 0
-- Retained button state (will persist across frames)
local buttonClicks = 0
local topLevelButtonClicks = 0
function love.load()
love.window.setTitle("Mixed-Mode Demo: Retained Children + Immediate Parents")
love.window.setMode(800, 600)
FlexLove.init({
immediateMode = true,
performanceMonitoring = true,
})
end
function love.update(dt)
FlexLove.update(dt)
end
function love.draw()
FlexLove.beginFrame()
-- Frame counter (immediate mode - recreates each frame)
local header = FlexLove.new({
width = 800,
height = 60,
backgroundColor = { 0.1, 0.1, 0.15, 1 },
padding = 20,
flexDirection = "horizontal",
justifyContent = "space-between",
alignItems = "center",
})
FlexLove.new({
parent = header,
text = "Frame: " .. frameCount,
textColor = { 1, 1, 1, 1 },
textSize = 20,
})
FlexLove.new({
parent = header,
text = "Mixed-Mode Element Tree Demo",
textColor = { 0.8, 0.9, 1, 1 },
textSize = 24,
})
-- Main content area (immediate parent)
local container = FlexLove.new({
id = "main_container",
y = 60,
width = 800,
height = 540,
padding = 30,
gap = 20,
backgroundColor = { 0.05, 0.05, 0.08, 1 },
flexDirection = "vertical",
})
-- Section 1: Retained button in immediate parent
FlexLove.new({
parent = container,
text = "1. Retained Button (persists across frames)",
textColor = { 0.9, 0.9, 0.9, 1 },
textSize = 18,
})
local retainedButton = FlexLove.new({
id = "retained_button",
mode = "retained", -- This button will persist!
parent = container,
width = 300,
height = 50,
backgroundColor = { 0.2, 0.6, 0.9, 1 },
cornerRadius = 8,
text = "Clicks: " .. buttonClicks,
textColor = { 1, 1, 1, 1 },
textSize = 16,
textAlign = "center",
onEvent = function(element, event)
if event.type == "click" then
buttonClicks = buttonClicks + 1
-- Update button text
element.text = "Clicks: " .. buttonClicks
end
end,
})
FlexLove.new({
parent = container,
text = "Note: Button state persists even though parent recreates every frame",
textColor = { 0.6, 0.6, 0.6, 1 },
textSize = 12,
})
-- Section 2: Top-level retained element
FlexLove.new({
parent = container,
text = "2. Top-Level Retained Element (also persists)",
textColor = { 0.9, 0.9, 0.9, 1 },
textSize = 18,
margin = { top = 20 },
})
FlexLove.new({
parent = container,
text = "Look at the bottom-left corner for a persistent panel",
textColor = { 0.6, 0.6, 0.6, 1 },
textSize = 12,
})
-- Section 3: Comparison with immediate button
FlexLove.new({
parent = container,
text = "3. Immediate Button (recreates every frame)",
textColor = { 0.9, 0.9, 0.9, 1 },
textSize = 18,
margin = { top = 20 },
})
FlexLove.new({
parent = container,
width = 300,
height = 50,
backgroundColor = { 0.9, 0.3, 0.3, 1 },
cornerRadius = 8,
text = "Can't track clicks (recreated)",
textColor = { 1, 1, 1, 1 },
textSize = 16,
textAlign = "center",
onEvent = function(element, event)
if event.type == "click" then
print("Immediate button clicked (but counter can't persist)")
end
end,
})
FlexLove.new({
parent = container,
text = "Note: This button is recreated every frame, so it can't maintain state",
textColor = { 0.6, 0.6, 0.6, 1 },
textSize = 12,
})
-- Top-level retained element (persists in immediate mode!)
local topLevelPanel = FlexLove.new({
id = "top_level_panel",
mode = "retained",
x = 10,
y = 500,
width = 250,
height = 90,
backgroundColor = { 0.15, 0.5, 0.3, 1 },
cornerRadius = 10,
padding = 15,
gap = 10,
flexDirection = "vertical",
})
FlexLove.new({
id = "panel_title",
mode = "retained",
parent = topLevelPanel,
text = "Persistent Panel",
textColor = { 1, 1, 1, 1 },
textSize = 16,
})
FlexLove.new({
id = "panel_button",
mode = "retained",
parent = topLevelPanel,
width = 220,
height = 35,
backgroundColor = { 1, 1, 1, 0.9 },
cornerRadius = 5,
text = "Panel Clicks: " .. topLevelButtonClicks,
textColor = { 0.15, 0.5, 0.3, 1 },
textSize = 14,
textAlign = "center",
onEvent = function(element, event)
if event.type == "click" then
topLevelButtonClicks = topLevelButtonClicks + 1
element.text = "Panel Clicks: " .. topLevelButtonClicks
end
end,
})
FlexLove.endFrame()
-- Increment frame counter AFTER drawing
frameCount = frameCount + 1
-- Draw all UI elements
FlexLove.draw()
end
function love.mousepressed(x, y, button)
FlexLove.mousepressed(x, y, button)
end
function love.mousereleased(x, y, button)
FlexLove.mousereleased(x, y, button)
end
function love.mousemoved(x, y, dx, dy)
FlexLove.mousemoved(x, y, dx, dy)
end
function love.wheelmoved(x, y)
FlexLove.wheelmoved(x, y)
end

View File

@@ -38,8 +38,19 @@ function Context.registerElement(element)
end
function Context.clearFrameElements()
-- 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)
function Context.sortElementsByZIndex()

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
-- 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
-- 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]
-- Add property hash if provided (for additional differentiation at same position)
if props then
if instanceNum > 1 then
baseID = baseID .. "_" .. instanceNum
end
-- 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

View File

@@ -0,0 +1,400 @@
-- Test retained children persisting when immediate parents recreate
package.path = package.path .. ";./?.lua;./modules/?.lua"
-- Load love stub before anything else
require("testing.loveStub")
local luaunit = require("testing.luaunit")
local FlexLove = require("FlexLove")
TestMixedModeChildren = {}
function TestMixedModeChildren:setUp()
FlexLove.init({ immediateMode = true })
FlexLove.setMode("immediate")
end
function TestMixedModeChildren:tearDown()
FlexLove._defaultDependencies.StateManager.reset()
FlexLove.topElements = {}
FlexLove._currentFrameElements = {}
FlexLove._frameStarted = false
end
-- Test 1: Retained child persists when immediate parent recreates
function TestMixedModeChildren:testRetainedChildPersistsWithImmediateParent()
FlexLove.beginFrame()
-- Frame 1: Create immediate parent with retained child
local parent1 = FlexLove.new({
id = "parent",
mode = "immediate",
width = 200,
height = 200,
})
local retainedChild = FlexLove.new({
id = "retained_child",
mode = "retained",
width = 50,
height = 50,
})
parent1:addChild(retainedChild)
luaunit.assertEquals(#parent1.children, 1, "Parent should have 1 child")
luaunit.assertEquals(parent1.children[1], retainedChild, "Child should be the retained element")
luaunit.assertEquals(retainedChild.parent, parent1, "Child's parent should be set")
FlexLove.endFrame()
-- Frame 2: Recreate immediate parent, retained child should persist
FlexLove.beginFrame()
local parent2 = FlexLove.new({
id = "parent",
mode = "immediate",
width = 200,
height = 200,
})
-- The retained child should be automatically restored
luaunit.assertEquals(#parent2.children, 1, "Parent should still have 1 child after recreation")
luaunit.assertEquals(parent2.children[1], retainedChild, "Child should be the same retained element")
luaunit.assertEquals(retainedChild.parent, parent2, "Child's parent reference should be updated")
FlexLove.endFrame()
end
-- Test 2: Multiple retained children persist
function TestMixedModeChildren:testMultipleRetainedChildrenPersist()
FlexLove.beginFrame()
local parent1 = FlexLove.new({
id = "parent",
mode = "immediate",
width = 200,
height = 200,
})
local child1 = FlexLove.new({
id = "child1",
mode = "retained",
width = 50,
height = 50,
})
local child2 = FlexLove.new({
id = "child2",
mode = "retained",
width = 50,
height = 50,
})
local child3 = FlexLove.new({
id = "child3",
mode = "retained",
width = 50,
height = 50,
})
parent1:addChild(child1)
parent1:addChild(child2)
parent1:addChild(child3)
luaunit.assertEquals(#parent1.children, 3, "Parent should have 3 children")
FlexLove.endFrame()
-- Frame 2
FlexLove.beginFrame()
local parent2 = FlexLove.new({
id = "parent",
mode = "immediate",
width = 200,
height = 200,
})
luaunit.assertEquals(#parent2.children, 3, "Parent should still have 3 children")
luaunit.assertEquals(parent2.children[1], child1, "First child should persist")
luaunit.assertEquals(parent2.children[2], child2, "Second child should persist")
luaunit.assertEquals(parent2.children[3], child3, "Third child should persist")
FlexLove.endFrame()
end
-- Test 3: Immediate children do NOT persist (only retained children)
function TestMixedModeChildren:testImmediateChildrenDoNotPersist()
FlexLove.beginFrame()
local parent1 = FlexLove.new({
id = "parent",
mode = "immediate",
width = 200,
height = 200,
})
local immediateChild = FlexLove.new({
id = "immediate_child",
mode = "immediate",
width = 50,
height = 50,
})
local retainedChild = FlexLove.new({
id = "retained_child",
mode = "retained",
width = 50,
height = 50,
})
parent1:addChild(immediateChild)
parent1:addChild(retainedChild)
luaunit.assertEquals(#parent1.children, 2, "Parent should have 2 children")
FlexLove.endFrame()
-- Frame 2
FlexLove.beginFrame()
local parent2 = FlexLove.new({
id = "parent",
mode = "immediate",
width = 200,
height = 200,
})
-- Only retained child should persist
luaunit.assertEquals(#parent2.children, 1, "Parent should only have 1 child (retained)")
luaunit.assertEquals(parent2.children[1], retainedChild, "Only retained child should persist")
FlexLove.endFrame()
end
-- Test 4: Top-level retained element persists in immediate mode
function TestMixedModeChildren:testTopLevelRetainedElementPersists()
FlexLove.beginFrame()
local retainedElement = FlexLove.new({
id = "top_retained",
mode = "retained",
width = 100,
height = 100,
})
luaunit.assertEquals(#FlexLove.topElements, 1, "Should have 1 top-level element")
luaunit.assertEquals(FlexLove.topElements[1], retainedElement, "Top element should be retained element")
FlexLove.endFrame()
-- Frame 2
FlexLove.beginFrame()
-- Retained element should still be in topElements
luaunit.assertEquals(#FlexLove.topElements, 1, "Retained element should persist in topElements")
luaunit.assertEquals(FlexLove.topElements[1], retainedElement, "Should be same retained element")
FlexLove.endFrame()
end
-- Test 5: Mixed top-level elements (immediate and retained)
function TestMixedModeChildren:testMixedTopLevelElements()
FlexLove.beginFrame()
local immediateElement1 = FlexLove.new({
id = "immediate1",
mode = "immediate",
width = 100,
height = 100,
})
local retainedElement = FlexLove.new({
id = "retained",
mode = "retained",
width = 100,
height = 100,
})
luaunit.assertEquals(#FlexLove.topElements, 2, "Should have 2 top-level elements")
FlexLove.endFrame()
-- Frame 2
FlexLove.beginFrame()
local immediateElement2 = FlexLove.new({
id = "immediate2",
mode = "immediate",
width = 100,
height = 100,
})
-- Should have retained element + new immediate element
luaunit.assertEquals(#FlexLove.topElements, 2, "Should have 2 top-level elements")
-- Find retained element in topElements
local foundRetained = false
for _, elem in ipairs(FlexLove.topElements) do
if elem == retainedElement then
foundRetained = true
break
end
end
luaunit.assertTrue(foundRetained, "Retained element should still be in topElements")
FlexLove.endFrame()
end
-- Test 6: Retained child cleanup on parent destroy
function TestMixedModeChildren:testRetainedChildCleanupOnDestroy()
FlexLove.beginFrame()
local parent = FlexLove.new({
id = "parent",
mode = "immediate",
width = 200,
height = 200,
})
local retainedChild = FlexLove.new({
id = "retained_child",
mode = "retained",
width = 50,
height = 50,
})
parent:addChild(retainedChild)
FlexLove.endFrame()
-- Destroy parent (simulating explicit cleanup)
parent:destroy()
-- Frame 2: Create new parent with same ID
FlexLove.beginFrame()
local parent2 = FlexLove.new({
id = "parent",
mode = "immediate",
width = 200,
height = 200,
})
-- Since parent was destroyed, retained children should be cleared
luaunit.assertEquals(#parent2.children, 0, "Parent should have no children after destroy")
FlexLove.endFrame()
end
-- Test 7: Nested mixed-mode tree (immediate -> retained -> immediate)
function TestMixedModeChildren:testNestedMixedModeTree()
FlexLove.beginFrame()
local immediateParent = FlexLove.new({
id = "immediate_parent",
mode = "immediate",
width = 300,
height = 300,
})
local retainedMiddle = FlexLove.new({
id = "retained_middle",
mode = "retained",
width = 200,
height = 200,
})
local immediateGrandchild = FlexLove.new({
id = "immediate_grandchild",
mode = "immediate",
width = 100,
height = 100,
})
immediateParent:addChild(retainedMiddle)
retainedMiddle:addChild(immediateGrandchild)
luaunit.assertEquals(#immediateParent.children, 1, "Immediate parent should have 1 child")
luaunit.assertEquals(#retainedMiddle.children, 1, "Retained middle should have 1 child")
FlexLove.endFrame()
-- Frame 2: Recreate immediate parent
FlexLove.beginFrame()
local immediateParent2 = FlexLove.new({
id = "immediate_parent",
mode = "immediate",
width = 300,
height = 300,
})
-- Retained middle should persist
luaunit.assertEquals(#immediateParent2.children, 1, "Immediate parent should still have retained child")
luaunit.assertEquals(immediateParent2.children[1], retainedMiddle, "Retained middle should persist")
-- Immediate grandchild should also persist (as child of retained middle)
luaunit.assertEquals(#retainedMiddle.children, 1, "Retained middle should still have its child")
FlexLove.endFrame()
end
-- Test 8: Prevent duplicate creation of retained children
function TestMixedModeChildren:testPreventDuplicateRetainedChildren()
FlexLove.beginFrame()
-- Frame 1: Create immediate parent with retained child
local parent1 = FlexLove.new({
id = "parent",
mode = "immediate",
width = 200,
height = 200,
})
local retainedChild = FlexLove.new({
id = "unique_child",
mode = "retained",
parent = parent1,
width = 50,
height = 50,
})
luaunit.assertEquals(#parent1.children, 1, "Parent should have 1 child")
local originalChild = parent1.children[1]
FlexLove.endFrame()
-- Frame 2: Recreate parent and try to create child again
FlexLove.beginFrame()
local parent2 = FlexLove.new({
id = "parent",
mode = "immediate",
width = 200,
height = 200,
})
-- Try to create the same retained child again
local duplicateAttempt = FlexLove.new({
id = "unique_child",
mode = "retained",
parent = parent2,
width = 50,
height = 50,
})
-- Should return the existing child, not create a new one
luaunit.assertEquals(duplicateAttempt, originalChild, "Should return existing child instead of creating duplicate")
luaunit.assertEquals(duplicateAttempt, retainedChild, "Should be the same retained child instance")
luaunit.assertEquals(#parent2.children, 1, "Parent should still have only 1 child")
FlexLove.endFrame()
end
-- Run tests
if not _G.RUNNING_ALL_TESTS then
os.exit(luaunit.LuaUnit.run())
end

View File

@@ -16,7 +16,9 @@ local originalSearchers = package.searchers or package.loaders
table.insert(originalSearchers, 2, function(modname)
if modname:match("^FlexLove%.modules%.") then
local moduleName = modname:gsub("^FlexLove%.modules%.", "")
return function() return require("modules." .. moduleName) end
return function()
return require("modules." .. moduleName)
end
end
end)
@@ -189,4 +191,7 @@ function TestMixedModeEvents:test_multipleImmediateChildrenHandleEventsIndepende
luaunit.assertTrue(button2Clicked)
end
-- Run tests
if not _G.RUNNING_ALL_TESTS then
os.exit(luaunit.LuaUnit.run())
end

View File

@@ -0,0 +1,193 @@
--[[
Test: Retained Elements in Immediate Mode (No Duplication)
This test verifies that retained-mode elements don't get recreated
when the overall application is in immediate mode.
]]
package.path = package.path .. ";./?.lua;./modules/?.lua"
require("testing.loveStub")
local luaunit = require("testing.luaunit")
local FlexLove = require("FlexLove")
local Color = require("modules.Color")
TestRetainedInImmediateMode = {}
function TestRetainedInImmediateMode:setUp()
-- Initialize in IMMEDIATE mode
FlexLove.init({ immediateMode = true })
end
function TestRetainedInImmediateMode:tearDown()
if FlexLove.getMode() == "immediate" then
FlexLove.endFrame()
end
FlexLove.init({ immediateMode = false })
end
-- Test that top-level retained elements persist across frames
function TestRetainedInImmediateMode:test_topLevelRetainedElementPersists()
-- Helper function that creates elements (simulates user code called each frame)
local function createUI()
local backdrop = FlexLove.new({
mode = "retained",
width = "100%",
height = "100%",
backgroundColor = Color.new(0.1, 0.2, 0.3, 0.5),
})
return backdrop
end
FlexLove.beginFrame()
-- Frame 1: Create a retained element (no explicit ID)
local backdrop = createUI()
local backdropId = backdrop.id
luaunit.assertNotNil(backdropId, "Backdrop should have auto-generated ID")
luaunit.assertEquals(backdrop._elementMode, "retained")
FlexLove.endFrame()
-- Frame 2: Call createUI() again (same function, same line numbers)
FlexLove.beginFrame()
local backdrop2 = createUI()
-- Should return the SAME element, not create a new one
luaunit.assertEquals(backdrop2.id, backdropId, "Should return existing element with same ID")
luaunit.assertEquals(backdrop2, backdrop, "Should return exact same element instance")
FlexLove.endFrame()
end
-- Test that retained elements with explicit IDs can be recreated
function TestRetainedInImmediateMode:test_explicitIdAllowsNewElements()
FlexLove.beginFrame()
-- Create element with explicit ID
local element1 = FlexLove.new({
id = "my_custom_id",
mode = "retained",
width = 100,
height = 100,
backgroundColor = Color.new(1, 0, 0, 1),
})
FlexLove.endFrame()
FlexLove.beginFrame()
-- Create another element with SAME explicit ID but different properties
-- This should create a NEW element (user controls uniqueness)
local element2 = FlexLove.new({
id = "my_custom_id",
mode = "retained",
width = 200, -- Different properties
height = 200,
backgroundColor = Color.new(0, 1, 0, 1),
})
-- With explicit IDs, we allow duplicates (user responsibility)
luaunit.assertEquals(element2.id, "my_custom_id")
-- Properties should match NEW element, not old
luaunit.assertEquals(element2.width, 200)
FlexLove.endFrame()
end
-- Test that multiple retained elements persist independently
function TestRetainedInImmediateMode:test_multipleRetainedElementsPersist()
local function createUI()
local backdrop = FlexLove.new({
mode = "retained",
width = "100%",
height = "100%",
})
local window = FlexLove.new({
mode = "retained",
width = "90%",
height = "90%",
})
return backdrop, window
end
FlexLove.beginFrame()
local backdrop, window = createUI()
local backdropId = backdrop.id
local windowId = window.id
luaunit.assertNotEquals(backdropId, windowId, "Different elements should have different IDs")
FlexLove.endFrame()
-- Frame 2
FlexLove.beginFrame()
local backdrop2, window2 = createUI()
-- Both should return existing elements
luaunit.assertEquals(backdrop2.id, backdropId)
luaunit.assertEquals(window2.id, windowId)
luaunit.assertEquals(backdrop2, backdrop)
luaunit.assertEquals(window2, window)
FlexLove.endFrame()
end
-- Test that retained children of retained parents persist
function TestRetainedInImmediateMode:test_retainedChildOfRetainedParentPersists()
local function createUI()
local parent = FlexLove.new({
mode = "retained",
width = 400,
height = 400,
})
local child = FlexLove.new({
mode = "retained",
parent = parent,
width = 100,
height = 100,
})
return parent, child
end
FlexLove.beginFrame()
local parent, child = createUI()
local parentId = parent.id
local childId = child.id
FlexLove.endFrame()
-- Frame 2
FlexLove.beginFrame()
local parent2, child2 = createUI()
-- Parent should be the same
luaunit.assertEquals(parent2.id, parentId)
luaunit.assertEquals(parent2, parent)
-- Child should also be the same instance
luaunit.assertEquals(child2.id, childId, "Child ID should match")
luaunit.assertEquals(child2, child, "Child should be same instance")
-- Child should still exist in parent's children
luaunit.assertEquals(#parent2.children, 1, "Parent should have exactly 1 child")
luaunit.assertEquals(parent2.children[1].id, childId)
FlexLove.endFrame()
end
-- Run tests
os.exit(luaunit.LuaUnit.run())

View File

@@ -0,0 +1,184 @@
--[[
Test: Retained Elements with Varying Props (ID Stability)
This test verifies that retained-mode elements return the same instance
across frames even when props vary slightly (e.g., different Color instances).
]]
package.path = package.path .. ";./?.lua;./modules/?.lua"
require("testing.loveStub")
local luaunit = require("testing.luaunit")
local FlexLove = require("FlexLove")
local Color = require("modules.Color")
TestRetainedPropStability = {}
function TestRetainedPropStability:setUp()
-- Initialize in IMMEDIATE mode
FlexLove.init({ immediateMode = true })
end
function TestRetainedPropStability:tearDown()
if FlexLove.getMode() == "immediate" then
FlexLove.endFrame()
end
FlexLove.init({ immediateMode = false })
end
-- Test that retained elements persist despite creating new Color instances
function TestRetainedPropStability:test_retainedElementIgnoresColorInstanceChanges()
FlexLove.beginFrame()
-- Frame 1: Create retained element with Color instance
local backdrop1 = FlexLove.new({
mode = "retained",
width = "100%",
height = "100%",
backgroundColor = Color.new(1, 1, 1, 0.1), -- NEW Color instance
})
local id1 = backdrop1.id
FlexLove.endFrame()
-- Frame 2: Same props but NEW Color instance (common pattern in user code)
FlexLove.beginFrame()
local backdrop2 = FlexLove.new({
mode = "retained",
width = "100%",
height = "100%",
backgroundColor = Color.new(1, 1, 1, 0.1), -- NEW Color instance (different table)
})
-- Should return SAME element despite different Color instance
luaunit.assertEquals(backdrop2.id, id1, "ID should be stable across frames")
luaunit.assertEquals(backdrop2, backdrop1, "Should return same element instance")
FlexLove.endFrame()
end
-- Test that retained elements with complex props persist
function TestRetainedPropStability:test_retainedElementWithComplexProps()
local function createWindow()
return FlexLove.new({
mode = "retained",
z = 100,
x = "5%",
y = "5%",
width = "90%",
height = "90%",
themeComponent = "framev3",
positioning = "flex",
flexDirection = "vertical",
justifySelf = "center",
justifyContent = "flex-start",
alignItems = "center",
scaleCorners = 3,
padding = { horizontal = "5%", vertical = "3%" },
gap = 10,
})
end
FlexLove.beginFrame()
local window1 = createWindow()
local id1 = window1.id
FlexLove.endFrame()
-- Frame 2: Same function, same props
FlexLove.beginFrame()
local window2 = createWindow()
-- Should return same element
luaunit.assertEquals(window2.id, id1)
luaunit.assertEquals(window2, window1)
FlexLove.endFrame()
end
-- Test that retained elements with backdrop blur persist
function TestRetainedPropStability:test_retainedElementWithBackdropBlur()
local function createBackdrop()
return FlexLove.new({
mode = "retained",
width = "100%",
height = "100%",
backdropBlur = { radius = 10 }, -- Table prop
backgroundColor = Color.new(1, 1, 1, 0.1),
})
end
FlexLove.beginFrame()
local backdrop1 = createBackdrop()
local id1 = backdrop1.id
FlexLove.endFrame()
-- Frame 2
FlexLove.beginFrame()
local backdrop2 = createBackdrop()
-- Should return same element
luaunit.assertEquals(backdrop2.id, id1)
luaunit.assertEquals(backdrop2, backdrop1)
FlexLove.endFrame()
end
-- Test that multiple retained elements persist independently
function TestRetainedPropStability:test_multipleRetainedElementsWithVaryingProps()
local function createUI()
local backdrop = FlexLove.new({
mode = "retained",
z = 50,
width = "100%",
height = "100%",
backdropBlur = { radius = 10 },
backgroundColor = Color.new(1, 1, 1, 0.1),
})
local window = FlexLove.new({
mode = "retained",
z = 100,
x = "5%",
y = "5%",
width = "90%",
height = "90%",
themeComponent = "framev3",
padding = { horizontal = "5%", vertical = "3%" },
})
return backdrop, window
end
FlexLove.beginFrame()
local backdrop1, window1 = createUI()
local backdropId = backdrop1.id
local windowId = window1.id
FlexLove.endFrame()
-- Frame 2: New Color instances, new table instances for props
FlexLove.beginFrame()
local backdrop2, window2 = createUI()
-- Both should return existing elements
luaunit.assertEquals(backdrop2.id, backdropId)
luaunit.assertEquals(window2.id, windowId)
luaunit.assertEquals(backdrop2, backdrop1)
luaunit.assertEquals(window2, window1)
FlexLove.endFrame()
end
-- Run tests
os.exit(luaunit.LuaUnit.run())

View File

@@ -46,6 +46,9 @@ local testFiles = {
"testing/__tests__/image_scaler_test.lua",
"testing/__tests__/input_event_test.lua",
"testing/__tests__/layout_engine_test.lua",
"testing/__tests__/mixed_mode_events_test.lua",
"testing/__tests__/mixed_mode_children_test.lua",
"testing/__tests__/retained_in_immediate_test.lua",
"testing/__tests__/module_loader_test.lua",
"testing/__tests__/ninepatch_test.lua",
"testing/__tests__/performance_test.lua",
@@ -58,7 +61,7 @@ local testFiles = {
"testing/__tests__/utils_test.lua",
"testing/__tests__/calc_test.lua",
-- Feature/Integration tests
--"testing/__tests__/critical_failures_test.lua",
"testing/__tests__/critical_failures_test.lua",
"testing/__tests__/flexlove_test.lua",
"testing/__tests__/touch_events_test.lua",
}