From 6fe452ef9757ae3cfc92b62c2f8f1df81227ca5a Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Sat, 13 Dec 2025 01:54:30 -0500 Subject: [PATCH] auto handle late init call(and error report) with element creation queue --- FlexLove.lua | 52 +++++++- modules/Context.lua | 8 ++ modules/Element.lua | 8 +- testing/__tests__/init_queue_test.lua | 170 ++++++++++++++++++++++++++ testing/runAll.lua | 1 + 5 files changed, 232 insertions(+), 7 deletions(-) create mode 100644 testing/__tests__/init_queue_test.lua diff --git a/FlexLove.lua b/FlexLove.lua index c89e32d..108d4b9 100644 --- a/FlexLove.lua +++ b/FlexLove.lua @@ -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") diff --git a/modules/Context.lua b/modules/Context.lua index 2c00b9b..7d69e2a 100644 --- a/modules/Context.lua +++ b/modules/Context.lua @@ -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 diff --git a/modules/Element.lua b/modules/Element.lua index 5350e04..e4bd459 100644 --- a/modules/Element.lua +++ b/modules/Element.lua @@ -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 diff --git a/testing/__tests__/init_queue_test.lua b/testing/__tests__/init_queue_test.lua new file mode 100644 index 0000000..6b679db --- /dev/null +++ b/testing/__tests__/init_queue_test.lua @@ -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 diff --git a/testing/runAll.lua b/testing/runAll.lua index af96b44..ce57c80 100644 --- a/testing/runAll.lua +++ b/testing/runAll.lua @@ -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",