auto handle late init call(and error report) with element creation queue

This commit is contained in:
Michael Freno
2025-12-13 01:54:30 -05:00
parent c069b2be22
commit 6fe452ef97
5 changed files with 232 additions and 7 deletions

View File

@@ -112,10 +112,19 @@ flexlove._deferredCallbacks = {}
-- Track accumulated delta time for immediate mode updates
flexlove._accumulatedDt = 0
--- Check if FlexLove initialization is complete and ready to create elements
--- Use this before creating elements to avoid automatic queueing
---@return boolean ready True if FlexLove is initialized and ready to use
function flexlove.isReady()
return flexlove._initState == "ready"
end
--- Set up FlexLove for your application's specific needs - configure responsive scaling, theming, rendering mode, and debugging tools
--- Use this to establish a consistent UI foundation that adapts to different screen sizes and provides performance insights
--- After initialization, any queued element creation calls will be automatically processed
---@param config FlexLoveConfig?
function flexlove.init(config)
flexlove._initState = "initializing"
config = config or {}
flexlove._ErrorHandler = ErrorHandler.init({
@@ -282,6 +291,22 @@ function flexlove.init(config)
maxStateEntries = config.maxStateEntries,
})
end
flexlove.initialized = true
flexlove._initState = "ready"
-- Process all queued element creations
local queue = flexlove._initQueue
flexlove._initQueue = {} -- Clear queue before processing to prevent re-entry issues
for _, item in ipairs(queue) do
local element = Element.new(item.props)
if item.callback and type(item.callback) == "function" then
local success, err = pcall(item.callback, element)
if not success then
flexlove._ErrorHandler:warn("FlexLove", string.format("Failed to execute queued element callback: %s", tostring(err)))
end
end
end
end
--- Safely schedule operations that modify LÖVE's rendering state (like window mode changes) to execute after all canvas operations complete
@@ -431,7 +456,7 @@ function flexlove.beginFrame()
StateManager.incrementFrame()
flexlove._currentFrameElements = {}
flexlove._frameStarted = true
-- Restore retained top-level elements
flexlove.topElements = retainedTopElements
@@ -1051,11 +1076,32 @@ end
--- Create a new UI element with flexbox layout, styling, and interaction capabilities
--- This is your primary API for building interfaces - buttons, panels, text, images, and containers
--- If called before FlexLove.init(), the element creation will be automatically queued and executed after initialization
---@param props ElementProps
---@return Element
function flexlove.new(props)
---@param callback? function Optional callback function(element) that will be called with the created element (useful when queued)
---@return Element|nil element Returns element if initialized, nil if queued for later creation
function flexlove.new(props, callback)
props = props or {}
if not flexlove.initialized then
-- Queue element creation for after initialization
table.insert(flexlove._initQueue, {
props = props,
callback = callback,
})
if flexlove._initState == "uninitialized" then
if flexlove._ErrorHandler then
flexlove._ErrorHandler:warn(
"FlexLove",
"[FlexLove] Element creation queued - FlexLove.init() has not been called yet. Element will be created automatically after init() is called."
)
end
end
return nil -- Element will be created later
end
-- Determine effective mode: props.mode takes precedence over global mode
local effectiveMode = props.mode or (flexlove._immediateMode and "immediate" or "retained")

View File

@@ -20,6 +20,14 @@ local Context = {
_zIndexOrderedElements = {}, -- Array of elements sorted by z-index (lowest to highest)
-- Focus management guard
_settingFocus = false,
initialized = false,
-- Initialization state tracking
---@type "uninitialized"|"initializing"|"ready"
_initState = "uninitialized",
---@type table[] Queue of {props: ElementProps, callback: function(element)|nil}
_initQueue = {},
}
---@return number, number -- scaleX, scaleY

View File

@@ -194,7 +194,7 @@ function Element.new(props)
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
@@ -292,7 +292,7 @@ function Element.new(props)
-- 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)
@@ -524,7 +524,7 @@ function Element.new(props)
return false
end
end
self.border = {
top = normalizeBorderValue(props.border.top),
right = normalizeBorderValue(props.border.right),
@@ -1065,7 +1065,7 @@ function Element.new(props)
end
tempHeight = 0
end
-- Get scaled 9-patch content padding from ThemeManager
local scaledPadding = self._themeManager:getScaledContentPadding(tempWidth, tempHeight)
if scaledPadding then

View File

@@ -0,0 +1,170 @@
package.path = package.path .. ";./?.lua;./modules/?.lua"
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
end
end)
-- Test automatic initialization queue functionality
require("testing.loveStub")
local luaunit = require("testing.luaunit")
local FlexLove = require("FlexLove")
TestInitQueue = {}
function TestInitQueue:setUp()
-- Reset FlexLove state before each test
FlexLove.destroy()
-- Reset initialization state
FlexLove._initState = "uninitialized"
FlexLove.initialized = false
FlexLove._initQueue = {}
end
function TestInitQueue:tearDown()
FlexLove.destroy()
end
function TestInitQueue:test_elementCreationIsQueuedBeforeInit()
-- Element creation before init should be queued
local element = FlexLove.new({ text = "Test" })
-- Should return nil when queued
luaunit.assertNil(element)
-- Queue should have one item
luaunit.assertEquals(#FlexLove._initQueue, 1)
luaunit.assertEquals(FlexLove._initQueue[1].props.text, "Test")
end
function TestInitQueue:test_queuedElementsCreatedAfterInit()
local createdElement = nil
-- Create element before init with callback
FlexLove.new({
text = "Queued Element",
width = 100,
height = 50,
}, function(element)
createdElement = element
end)
-- Should be queued
luaunit.assertNil(createdElement)
luaunit.assertEquals(#FlexLove._initQueue, 1)
-- Initialize FlexLove
FlexLove.init()
-- Callback should have been called with created element
luaunit.assertNotNil(createdElement)
luaunit.assertEquals(createdElement.text, "Queued Element")
luaunit.assertEquals(createdElement.width, 100)
luaunit.assertEquals(createdElement.height, 50)
-- Queue should be empty after init
luaunit.assertEquals(#FlexLove._initQueue, 0)
end
function TestInitQueue:test_multipleElementsQueuedAndCreated()
local elements = {}
-- Queue multiple elements
for i = 1, 5 do
FlexLove.new({
text = "Element " .. i,
width = i * 10,
}, function(element)
table.insert(elements, element)
end)
end
-- All should be queued
luaunit.assertEquals(#FlexLove._initQueue, 5)
luaunit.assertEquals(#elements, 0)
-- Initialize
FlexLove.init()
-- All should be created
luaunit.assertEquals(#elements, 5)
luaunit.assertEquals(#FlexLove._initQueue, 0)
-- Verify properties
for i = 1, 5 do
luaunit.assertEquals(elements[i].text, "Element " .. i)
luaunit.assertEquals(elements[i].width, i * 10)
end
end
function TestInitQueue:test_elementCreatedImmediatelyAfterInit()
-- Initialize first
FlexLove.init()
-- Element creation after init should work immediately
local element = FlexLove.new({ text = "Immediate" })
-- Should return element, not nil
luaunit.assertNotNil(element)
luaunit.assertEquals(element.text, "Immediate")
-- Queue should remain empty
luaunit.assertEquals(#FlexLove._initQueue, 0)
end
function TestInitQueue:test_isReadyReturnsFalseBeforeInit()
luaunit.assertFalse(FlexLove.isReady())
luaunit.assertEquals(FlexLove._initState, "uninitialized")
end
function TestInitQueue:test_isReadyReturnsTrueAfterInit()
FlexLove.init()
luaunit.assertTrue(FlexLove.isReady())
luaunit.assertEquals(FlexLove._initState, "ready")
end
function TestInitQueue:test_callbackErrorDoesNotStopQueue()
local elements = {}
-- First element with failing callback
FlexLove.new({ text = "Element 1" }, function(element)
table.insert(elements, element)
error("Intentional error")
end)
-- Second element with working callback
FlexLove.new({ text = "Element 2" }, function(element)
table.insert(elements, element)
end)
-- Initialize (errors should be caught and logged)
FlexLove.init()
-- Both elements should have been created despite error
luaunit.assertEquals(#elements, 2)
luaunit.assertEquals(elements[1].text, "Element 1")
luaunit.assertEquals(elements[2].text, "Element 2")
end
function TestInitQueue:test_queueWithoutCallback()
-- Element without callback
FlexLove.new({ text = "No Callback" })
luaunit.assertEquals(#FlexLove._initQueue, 1)
luaunit.assertNil(FlexLove._initQueue[1].callback)
-- Should still be created after init
FlexLove.init()
luaunit.assertEquals(#FlexLove._initQueue, 0)
-- Element was created, just no way to reference it without callback
end
if not _G.RUNNING_ALL_TESTS then
os.exit(luaunit.LuaUnit.run())
end

View File

@@ -48,6 +48,7 @@ local testFiles = {
"testing/__tests__/image_cache_test.lua",
"testing/__tests__/image_renderer_test.lua",
"testing/__tests__/image_scaler_test.lua",
"testing/__tests__/init_queue_test.lua",
"testing/__tests__/input_event_test.lua",
"testing/__tests__/layout_engine_test.lua",
"testing/__tests__/mixed_mode_children_test.lua",