auto handle late init call(and error report) with element creation queue
This commit is contained in:
52
FlexLove.lua
52
FlexLove.lua
@@ -112,10 +112,19 @@ flexlove._deferredCallbacks = {}
|
|||||||
-- Track accumulated delta time for immediate mode updates
|
-- Track accumulated delta time for immediate mode updates
|
||||||
flexlove._accumulatedDt = 0
|
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
|
--- 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
|
--- 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?
|
---@param config FlexLoveConfig?
|
||||||
function flexlove.init(config)
|
function flexlove.init(config)
|
||||||
|
flexlove._initState = "initializing"
|
||||||
config = config or {}
|
config = config or {}
|
||||||
|
|
||||||
flexlove._ErrorHandler = ErrorHandler.init({
|
flexlove._ErrorHandler = ErrorHandler.init({
|
||||||
@@ -282,6 +291,22 @@ function flexlove.init(config)
|
|||||||
maxStateEntries = config.maxStateEntries,
|
maxStateEntries = config.maxStateEntries,
|
||||||
})
|
})
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
--- Safely schedule operations that modify LÖVE's rendering state (like window mode changes) to execute after all canvas operations complete
|
--- 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()
|
StateManager.incrementFrame()
|
||||||
flexlove._currentFrameElements = {}
|
flexlove._currentFrameElements = {}
|
||||||
flexlove._frameStarted = true
|
flexlove._frameStarted = true
|
||||||
|
|
||||||
-- Restore retained top-level elements
|
-- Restore retained top-level elements
|
||||||
flexlove.topElements = retainedTopElements
|
flexlove.topElements = retainedTopElements
|
||||||
|
|
||||||
@@ -1051,11 +1076,32 @@ end
|
|||||||
|
|
||||||
--- Create a new UI element with flexbox layout, styling, and interaction capabilities
|
--- 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
|
--- 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
|
---@param props ElementProps
|
||||||
---@return Element
|
---@param callback? function Optional callback function(element) that will be called with the created element (useful when queued)
|
||||||
function flexlove.new(props)
|
---@return Element|nil element Returns element if initialized, nil if queued for later creation
|
||||||
|
function flexlove.new(props, callback)
|
||||||
props = props or {}
|
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
|
-- Determine effective mode: props.mode takes precedence over global mode
|
||||||
local effectiveMode = props.mode or (flexlove._immediateMode and "immediate" or "retained")
|
local effectiveMode = props.mode or (flexlove._immediateMode and "immediate" or "retained")
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,14 @@ local Context = {
|
|||||||
_zIndexOrderedElements = {}, -- Array of elements sorted by z-index (lowest to highest)
|
_zIndexOrderedElements = {}, -- Array of elements sorted by z-index (lowest to highest)
|
||||||
-- Focus management guard
|
-- Focus management guard
|
||||||
_settingFocus = false,
|
_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
|
---@return number, number -- scaleX, scaleY
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ function Element.new(props)
|
|||||||
if elementMode == nil then
|
if elementMode == nil then
|
||||||
elementMode = Element._Context._immediateMode and "immediate" or "retained"
|
elementMode = Element._Context._immediateMode and "immediate" or "retained"
|
||||||
end
|
end
|
||||||
|
|
||||||
-- If retained mode and has an ID, check if element already exists in parent's children
|
-- 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
|
if elementMode == "retained" and props.id and props.id ~= "" and props.parent then
|
||||||
-- Check if this element already exists in parent's restored children
|
-- 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)
|
-- Track whether ID was auto-generated (before ID assignment)
|
||||||
local idWasAutoGenerated = not props.id or props.id == ""
|
local idWasAutoGenerated = not props.id or props.id == ""
|
||||||
|
|
||||||
-- Auto-generate ID if not provided (for all elements)
|
-- Auto-generate ID if not provided (for all elements)
|
||||||
if idWasAutoGenerated then
|
if idWasAutoGenerated then
|
||||||
self.id = Element._StateManager.generateID(props, props.parent)
|
self.id = Element._StateManager.generateID(props, props.parent)
|
||||||
@@ -524,7 +524,7 @@ function Element.new(props)
|
|||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
self.border = {
|
self.border = {
|
||||||
top = normalizeBorderValue(props.border.top),
|
top = normalizeBorderValue(props.border.top),
|
||||||
right = normalizeBorderValue(props.border.right),
|
right = normalizeBorderValue(props.border.right),
|
||||||
@@ -1065,7 +1065,7 @@ function Element.new(props)
|
|||||||
end
|
end
|
||||||
tempHeight = 0
|
tempHeight = 0
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Get scaled 9-patch content padding from ThemeManager
|
-- Get scaled 9-patch content padding from ThemeManager
|
||||||
local scaledPadding = self._themeManager:getScaledContentPadding(tempWidth, tempHeight)
|
local scaledPadding = self._themeManager:getScaledContentPadding(tempWidth, tempHeight)
|
||||||
if scaledPadding then
|
if scaledPadding then
|
||||||
|
|||||||
170
testing/__tests__/init_queue_test.lua
Normal file
170
testing/__tests__/init_queue_test.lua
Normal 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
|
||||||
@@ -48,6 +48,7 @@ local testFiles = {
|
|||||||
"testing/__tests__/image_cache_test.lua",
|
"testing/__tests__/image_cache_test.lua",
|
||||||
"testing/__tests__/image_renderer_test.lua",
|
"testing/__tests__/image_renderer_test.lua",
|
||||||
"testing/__tests__/image_scaler_test.lua",
|
"testing/__tests__/image_scaler_test.lua",
|
||||||
|
"testing/__tests__/init_queue_test.lua",
|
||||||
"testing/__tests__/input_event_test.lua",
|
"testing/__tests__/input_event_test.lua",
|
||||||
"testing/__tests__/layout_engine_test.lua",
|
"testing/__tests__/layout_engine_test.lua",
|
||||||
"testing/__tests__/mixed_mode_children_test.lua",
|
"testing/__tests__/mixed_mode_children_test.lua",
|
||||||
|
|||||||
Reference in New Issue
Block a user