starting mode escape hatch
This commit is contained in:
43
FlexLove.lua
43
FlexLove.lua
@@ -434,17 +434,45 @@ function flexlove.endFrame()
|
|||||||
|
|
||||||
-- Layout all top-level elements now that all children have been added
|
-- Layout all top-level elements now that all children have been added
|
||||||
-- This ensures overflow detection happens with complete child lists
|
-- This ensures overflow detection happens with complete child lists
|
||||||
|
-- Only process immediate-mode elements (retained elements handle their own layout)
|
||||||
for _, element in ipairs(flexlove._currentFrameElements) do
|
for _, element in ipairs(flexlove._currentFrameElements) do
|
||||||
if not element.parent then
|
if not element.parent and element._elementMode == "immediate" then
|
||||||
element:layoutChildren() -- Layout with all children present
|
element:layoutChildren() -- Layout with all children present
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Handle mixed-mode trees: if immediate-mode children were added to retained-mode parents,
|
||||||
|
-- trigger layout on those parents so the children are properly positioned
|
||||||
|
-- We check for parents with _childrenDirty flag OR parents with immediate-mode children
|
||||||
|
local retainedParentsToLayout = {}
|
||||||
|
for _, element in ipairs(flexlove._currentFrameElements) do
|
||||||
|
if element._elementMode == "immediate" and element.parent and element.parent._elementMode == "retained" then
|
||||||
|
-- Found immediate child with retained parent - mark parent for layout
|
||||||
|
retainedParentsToLayout[element.parent] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Layout all retained parents that had immediate children added
|
||||||
|
for parent, _ in pairs(retainedParentsToLayout) do
|
||||||
|
parent:layoutChildren()
|
||||||
|
end
|
||||||
|
|
||||||
-- Auto-update all top-level elements created this frame
|
-- Auto-update all top-level elements created this frame
|
||||||
-- This happens AFTER layout so positions are correct
|
-- This happens AFTER layout so positions are correct
|
||||||
-- Use accumulated dt from FlexLove.update() calls to properly update animations and cursor blink
|
-- Use accumulated dt from FlexLove.update() calls to properly update animations and cursor blink
|
||||||
|
-- Process immediate-mode top-level elements (they recursively update their children)
|
||||||
for _, element in ipairs(flexlove._currentFrameElements) do
|
for _, element in ipairs(flexlove._currentFrameElements) do
|
||||||
if not element.parent then
|
if not element.parent and element._elementMode == "immediate" then
|
||||||
|
element:update(flexlove._accumulatedDt)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Also update immediate-mode children that have retained-mode parents
|
||||||
|
-- These won't be updated by the loop above (since they have parents)
|
||||||
|
-- And their retained parents won't auto-update (retained = manual lifecycle)
|
||||||
|
-- So we need to explicitly update them here
|
||||||
|
for _, element in ipairs(flexlove._currentFrameElements) do
|
||||||
|
if element.parent and element.parent._elementMode == "retained" and element._elementMode == "immediate" then
|
||||||
element:update(flexlove._accumulatedDt)
|
element:update(flexlove._accumulatedDt)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -452,8 +480,9 @@ function flexlove.endFrame()
|
|||||||
-- Save state for all elements created this frame
|
-- Save state for all elements created this frame
|
||||||
-- State is collected from element and all sub-modules via element:saveState()
|
-- State is collected from element and all sub-modules via element:saveState()
|
||||||
-- This is the ONLY place state is saved in immediate mode
|
-- This is the ONLY place state is saved in immediate mode
|
||||||
|
-- Only process immediate-mode elements (retained elements don't use StateManager)
|
||||||
for _, element in ipairs(flexlove._currentFrameElements) do
|
for _, element in ipairs(flexlove._currentFrameElements) do
|
||||||
if element.id and element.id ~= "" then
|
if element._elementMode == "immediate" and element.id and element.id ~= "" then
|
||||||
-- Collect state from element and all sub-modules
|
-- Collect state from element and all sub-modules
|
||||||
local stateUpdate = element:saveState()
|
local stateUpdate = element:saveState()
|
||||||
|
|
||||||
@@ -1010,11 +1039,15 @@ end
|
|||||||
function flexlove.new(props)
|
function flexlove.new(props)
|
||||||
props = props or {}
|
props = props or {}
|
||||||
|
|
||||||
-- If not in immediate mode, use standard Element.new
|
-- Determine effective mode: props.mode takes precedence over global mode
|
||||||
if not flexlove._immediateMode then
|
local effectiveMode = props.mode or (flexlove._immediateMode and "immediate" or "retained")
|
||||||
|
|
||||||
|
-- If element is in retained mode, use standard Element.new
|
||||||
|
if effectiveMode == "retained" then
|
||||||
return Element.new(props)
|
return Element.new(props)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Element is in immediate mode - proceed with immediate-mode logic
|
||||||
-- Auto-begin frame if not manually started (convenience feature)
|
-- Auto-begin frame if not manually started (convenience feature)
|
||||||
if not flexlove._frameStarted then
|
if not flexlove._frameStarted then
|
||||||
flexlove.beginFrame()
|
flexlove.beginFrame()
|
||||||
|
|||||||
45
README.md
45
README.md
@@ -197,8 +197,46 @@ local button2 = FlexLove.new({
|
|||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
You should be able to mix both modes in the same application - use retained mode for your main UI and immediate mode for debug overlays or dynamic elements,
|
#### Per-Element Mode Override
|
||||||
though this hasn't been tested.
|
|
||||||
|
You can override the rendering mode on a per-element basis using the `mode` property. This allows you to mix immediate and retained mode elements in the same application:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Initialize in immediate mode globally
|
||||||
|
FlexLove.init({ immediateMode = true })
|
||||||
|
|
||||||
|
function love.draw()
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
|
||||||
|
-- This button uses immediate mode (follows global setting)
|
||||||
|
local dynamicButton = FlexLove.new({
|
||||||
|
text = "Frame: " .. love.timer.getTime(),
|
||||||
|
onEvent = function() print("Dynamic!") end
|
||||||
|
})
|
||||||
|
|
||||||
|
-- This panel uses retained mode (override with mode prop)
|
||||||
|
-- It will persist and won't be recreated each frame
|
||||||
|
local staticPanel = FlexLove.new({
|
||||||
|
mode = "retained", -- Explicit override
|
||||||
|
width = "30vw",
|
||||||
|
height = "40vh",
|
||||||
|
backgroundColor = Color.new(0.2, 0.2, 0.2, 1),
|
||||||
|
-- No ID auto-generation since it's in retained mode
|
||||||
|
})
|
||||||
|
|
||||||
|
FlexLove.endFrame()
|
||||||
|
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 = nil` - Element inherits from global mode setting (default behavior)
|
||||||
|
- 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)
|
||||||
|
|
||||||
### Element Properties
|
### Element Properties
|
||||||
|
|
||||||
@@ -262,6 +300,9 @@ Common properties for all elements:
|
|||||||
disabled = false,
|
disabled = false,
|
||||||
disableHighlight = false, -- Disable pressed overlay (auto-true for themed elements)
|
disableHighlight = false, -- Disable pressed overlay (auto-true for themed elements)
|
||||||
|
|
||||||
|
-- Rendering Mode
|
||||||
|
mode = nil, -- "immediate", "retained", or nil (uses global setting)
|
||||||
|
|
||||||
-- Hierarchy
|
-- Hierarchy
|
||||||
parent = nil, -- Parent element
|
parent = nil, -- Parent element
|
||||||
}
|
}
|
||||||
|
|||||||
275
examples/mode_override_demo.lua
Normal file
275
examples/mode_override_demo.lua
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
-- Mode Override Demo
|
||||||
|
-- Demonstrates per-element mode override in FlexLöve
|
||||||
|
-- Shows how to mix immediate and retained mode elements in the same application
|
||||||
|
|
||||||
|
package.path = package.path .. ";../?.lua;../modules/?.lua"
|
||||||
|
local FlexLove = require("FlexLove")
|
||||||
|
local Color = require("modules.Color")
|
||||||
|
|
||||||
|
-- Global state
|
||||||
|
local frameCount = 0
|
||||||
|
local clickCount = 0
|
||||||
|
local retainedPanelCreated = false
|
||||||
|
local retainedPanel = nil
|
||||||
|
|
||||||
|
function love.load()
|
||||||
|
-- Initialize FlexLove in immediate mode globally
|
||||||
|
FlexLove.init({
|
||||||
|
immediateMode = true,
|
||||||
|
theme = "space",
|
||||||
|
})
|
||||||
|
|
||||||
|
love.window.setTitle("Mode Override Demo - FlexLöve")
|
||||||
|
love.window.setMode(1200, 800)
|
||||||
|
end
|
||||||
|
|
||||||
|
function love.update(dt)
|
||||||
|
FlexLove.update(dt)
|
||||||
|
frameCount = frameCount + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
function love.draw()
|
||||||
|
love.graphics.clear(0.1, 0.1, 0.15, 1)
|
||||||
|
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
|
||||||
|
-- Title - Immediate mode (default, recreated every frame)
|
||||||
|
FlexLove.new({
|
||||||
|
text = "Mode Override Demo",
|
||||||
|
textSize = 32,
|
||||||
|
textColor = Color.new(1, 1, 1, 1),
|
||||||
|
width = "100vw",
|
||||||
|
height = 60,
|
||||||
|
textAlign = "center",
|
||||||
|
backgroundColor = Color.new(0.2, 0.2, 0.3, 1),
|
||||||
|
padding = { top = 15, bottom = 15 },
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Container for demo panels
|
||||||
|
local container = FlexLove.new({
|
||||||
|
positioning = "flex",
|
||||||
|
flexDirection = "horizontal",
|
||||||
|
justifyContent = "center",
|
||||||
|
alignItems = "flex-start",
|
||||||
|
gap = 20,
|
||||||
|
width = "100vw",
|
||||||
|
height = "calc(100vh - 60px)",
|
||||||
|
padding = { top = 20, left = 20, right = 20, bottom = 20 },
|
||||||
|
})
|
||||||
|
|
||||||
|
-- LEFT PANEL: Immediate Mode (Dynamic, recreated every frame)
|
||||||
|
local leftPanel = FlexLove.new({
|
||||||
|
mode = "immediate", -- Explicit immediate mode (would be default anyway)
|
||||||
|
parent = container,
|
||||||
|
width = "45vw",
|
||||||
|
height = "calc(100vh - 100px)",
|
||||||
|
backgroundColor = Color.new(0.15, 0.15, 0.2, 0.95),
|
||||||
|
cornerRadius = 8,
|
||||||
|
padding = { top = 20, left = 20, right = 20, bottom = 20 },
|
||||||
|
positioning = "flex",
|
||||||
|
flexDirection = "vertical",
|
||||||
|
gap = 15,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Panel title
|
||||||
|
FlexLove.new({
|
||||||
|
parent = leftPanel,
|
||||||
|
text = "Immediate Mode Panel",
|
||||||
|
textSize = 24,
|
||||||
|
textColor = Color.new(0.4, 0.8, 1, 1),
|
||||||
|
width = "100%",
|
||||||
|
height = 40,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Description
|
||||||
|
FlexLove.new({
|
||||||
|
parent = leftPanel,
|
||||||
|
text = "Elements in this panel are recreated every frame.\nState is preserved by StateManager.",
|
||||||
|
textSize = 14,
|
||||||
|
textColor = Color.new(0.8, 0.8, 0.8, 1),
|
||||||
|
width = "100%",
|
||||||
|
height = 60,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Live frame counter (updates automatically)
|
||||||
|
FlexLove.new({
|
||||||
|
parent = leftPanel,
|
||||||
|
text = string.format("Frame: %d", frameCount),
|
||||||
|
textSize = 18,
|
||||||
|
textColor = Color.new(1, 1, 0.5, 1),
|
||||||
|
width = "100%",
|
||||||
|
height = 30,
|
||||||
|
backgroundColor = Color.new(0.2, 0.2, 0.3, 1),
|
||||||
|
padding = { top = 5, left = 10, right = 10, bottom = 5 },
|
||||||
|
cornerRadius = 4,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Click counter (updates automatically)
|
||||||
|
FlexLove.new({
|
||||||
|
parent = leftPanel,
|
||||||
|
text = string.format("Clicks: %d", clickCount),
|
||||||
|
textSize = 18,
|
||||||
|
textColor = Color.new(0.5, 1, 0.5, 1),
|
||||||
|
width = "100%",
|
||||||
|
height = 30,
|
||||||
|
backgroundColor = Color.new(0.2, 0.2, 0.3, 1),
|
||||||
|
padding = { top = 5, left = 10, right = 10, bottom = 5 },
|
||||||
|
cornerRadius = 4,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Interactive button (state preserved across frames)
|
||||||
|
FlexLove.new({
|
||||||
|
parent = leftPanel,
|
||||||
|
text = "Click Me! (Immediate Mode)",
|
||||||
|
textSize = 16,
|
||||||
|
textColor = Color.new(1, 1, 1, 1),
|
||||||
|
width = "100%",
|
||||||
|
height = 50,
|
||||||
|
themeComponent = "button",
|
||||||
|
onEvent = function(element, event)
|
||||||
|
if event.type == "release" then
|
||||||
|
clickCount = clickCount + 1
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Info box
|
||||||
|
FlexLove.new({
|
||||||
|
parent = leftPanel,
|
||||||
|
text = "Notice how the frame counter updates\nautomatically without any manual updates.\n\nThis is the power of immediate mode:\nUI reflects application state automatically.",
|
||||||
|
textSize = 13,
|
||||||
|
textColor = Color.new(0.7, 0.7, 0.7, 1),
|
||||||
|
width = "100%",
|
||||||
|
height = "auto",
|
||||||
|
backgroundColor = Color.new(0.1, 0.1, 0.15, 1),
|
||||||
|
padding = { top = 10, left = 10, right = 10, bottom = 10 },
|
||||||
|
cornerRadius = 4,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- RIGHT PANEL: Retained Mode (Static, created once)
|
||||||
|
-- Only create on first frame
|
||||||
|
if not retainedPanelCreated then
|
||||||
|
retainedPanel = FlexLove.new({
|
||||||
|
mode = "retained", -- Explicit retained mode override
|
||||||
|
parent = container,
|
||||||
|
width = "45vw",
|
||||||
|
height = "calc(100vh - 100px)",
|
||||||
|
backgroundColor = Color.new(0.2, 0.15, 0.15, 0.95),
|
||||||
|
cornerRadius = 8,
|
||||||
|
padding = { top = 20, left = 20, right = 20, bottom = 20 },
|
||||||
|
positioning = "flex",
|
||||||
|
flexDirection = "vertical",
|
||||||
|
gap = 15,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Panel title (retained)
|
||||||
|
FlexLove.new({
|
||||||
|
mode = "retained",
|
||||||
|
parent = retainedPanel,
|
||||||
|
text = "Retained Mode Panel",
|
||||||
|
textSize = 24,
|
||||||
|
textColor = Color.new(1, 0.6, 0.4, 1),
|
||||||
|
width = "100%",
|
||||||
|
height = 40,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Description (retained)
|
||||||
|
FlexLove.new({
|
||||||
|
mode = "retained",
|
||||||
|
parent = retainedPanel,
|
||||||
|
text = "Elements in this panel are created once\nand persist across frames.",
|
||||||
|
textSize = 14,
|
||||||
|
textColor = Color.new(0.8, 0.8, 0.8, 1),
|
||||||
|
width = "100%",
|
||||||
|
height = 60,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Static frame counter (won't update)
|
||||||
|
local staticCounter = FlexLove.new({
|
||||||
|
mode = "retained",
|
||||||
|
parent = retainedPanel,
|
||||||
|
text = string.format("Created at frame: %d", frameCount),
|
||||||
|
textSize = 18,
|
||||||
|
textColor = Color.new(1, 1, 0.5, 1),
|
||||||
|
width = "100%",
|
||||||
|
height = 30,
|
||||||
|
backgroundColor = Color.new(0.3, 0.2, 0.2, 1),
|
||||||
|
padding = { top = 5, left = 10, right = 10, bottom = 5 },
|
||||||
|
cornerRadius = 4,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Click counter placeholder (must be manually updated)
|
||||||
|
local retainedClickCounter = FlexLove.new({
|
||||||
|
mode = "retained",
|
||||||
|
parent = retainedPanel,
|
||||||
|
text = string.format("Clicks: %d (manual update needed)", clickCount),
|
||||||
|
textSize = 18,
|
||||||
|
textColor = Color.new(0.5, 1, 0.5, 1),
|
||||||
|
width = "100%",
|
||||||
|
height = 30,
|
||||||
|
backgroundColor = Color.new(0.3, 0.2, 0.2, 1),
|
||||||
|
padding = { top = 5, left = 10, right = 10, bottom = 5 },
|
||||||
|
cornerRadius = 4,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Interactive button with manual update
|
||||||
|
FlexLove.new({
|
||||||
|
mode = "retained",
|
||||||
|
parent = retainedPanel,
|
||||||
|
text = "Click Me! (Retained Mode)",
|
||||||
|
textSize = 16,
|
||||||
|
textColor = Color.new(1, 1, 1, 1),
|
||||||
|
width = "100%",
|
||||||
|
height = 50,
|
||||||
|
themeComponent = "button",
|
||||||
|
onEvent = function(element, event)
|
||||||
|
if event.type == "release" then
|
||||||
|
clickCount = clickCount + 1
|
||||||
|
-- In retained mode, we must manually update the UI
|
||||||
|
retainedClickCounter.text = string.format("Clicks: %d (manual update needed)", clickCount)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Info box (retained)
|
||||||
|
FlexLove.new({
|
||||||
|
mode = "retained",
|
||||||
|
parent = retainedPanel,
|
||||||
|
text = "Notice how this panel's elements\ndon't update automatically.\n\nIn retained mode, you must manually\nupdate element properties when state changes.\n\nThis gives better performance for\nstatic UI elements.",
|
||||||
|
textSize = 13,
|
||||||
|
textColor = Color.new(0.7, 0.7, 0.7, 1),
|
||||||
|
width = "100%",
|
||||||
|
height = "auto",
|
||||||
|
backgroundColor = Color.new(0.15, 0.1, 0.1, 1),
|
||||||
|
padding = { top = 10, left = 10, right = 10, bottom = 10 },
|
||||||
|
cornerRadius = 4,
|
||||||
|
})
|
||||||
|
|
||||||
|
retainedPanelCreated = true
|
||||||
|
end
|
||||||
|
|
||||||
|
FlexLove.endFrame()
|
||||||
|
|
||||||
|
-- Bottom instructions
|
||||||
|
love.graphics.setColor(0.5, 0.5, 0.5, 1)
|
||||||
|
love.graphics.print("Global mode: Immediate | Left panel: Immediate (explicit) | Right panel: Retained (override)", 10, love.graphics.getHeight() - 30)
|
||||||
|
love.graphics.print("Press ESC to quit", 10, love.graphics.getHeight() - 15)
|
||||||
|
end
|
||||||
|
|
||||||
|
function love.keypressed(key)
|
||||||
|
if key == "escape" then
|
||||||
|
love.event.quit()
|
||||||
|
end
|
||||||
|
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
|
||||||
@@ -64,6 +64,7 @@
|
|||||||
---@field _themeState string? -- Current theme state (normal, hover, pressed, active, disabled)
|
---@field _themeState string? -- Current theme state (normal, hover, pressed, active, disabled)
|
||||||
---@field _themeManager ThemeManager -- Internal: theme manager instance
|
---@field _themeManager ThemeManager -- Internal: theme manager instance
|
||||||
---@field _stateId string? -- State manager ID for this element
|
---@field _stateId string? -- State manager ID for this element
|
||||||
|
---@field _elementMode "immediate"|"retained" -- Lifecycle mode for this element (resolved from props.mode or global mode)
|
||||||
---@field disabled boolean? -- Whether the element is disabled (default: false)
|
---@field disabled boolean? -- Whether the element is disabled (default: false)
|
||||||
---@field active boolean? -- Whether the element is active/focused (for inputs, default: false)
|
---@field active boolean? -- Whether the element is active/focused (for inputs, default: false)
|
||||||
---@field disableHighlight boolean? -- Whether to disable the pressed state highlight overlay (default: false)
|
---@field disableHighlight boolean? -- Whether to disable the pressed state highlight overlay (default: false)
|
||||||
@@ -257,11 +258,22 @@ function Element.new(props)
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Resolve element mode: props.mode takes precedence over global mode
|
||||||
|
-- This must happen BEFORE ID generation and state management
|
||||||
|
if props.mode == "immediate" then
|
||||||
|
self._elementMode = "immediate"
|
||||||
|
elseif props.mode == "retained" then
|
||||||
|
self._elementMode = "retained"
|
||||||
|
else
|
||||||
|
-- nil or invalid: use global mode
|
||||||
|
self._elementMode = Element._Context._immediateMode and "immediate" or "retained"
|
||||||
|
end
|
||||||
|
|
||||||
self.children = {}
|
self.children = {}
|
||||||
self.onEvent = props.onEvent
|
self.onEvent = props.onEvent
|
||||||
|
|
||||||
-- Auto-generate ID in immediate mode if not provided
|
-- Auto-generate ID in immediate mode if not provided
|
||||||
if Element._Context._immediateMode and (not props.id or props.id == "") then
|
if self._elementMode == "immediate" and (not props.id or props.id == "") then
|
||||||
self.id = Element._StateManager.generateID(props, props.parent)
|
self.id = Element._StateManager.generateID(props, props.parent)
|
||||||
else
|
else
|
||||||
self.id = props.id or ""
|
self.id = props.id or ""
|
||||||
@@ -288,7 +300,7 @@ function Element.new(props)
|
|||||||
onEvent = self.onEvent,
|
onEvent = self.onEvent,
|
||||||
onEventDeferred = props.onEventDeferred,
|
onEventDeferred = props.onEventDeferred,
|
||||||
}
|
}
|
||||||
if Element._Context._immediateMode and self._stateId and self._stateId ~= "" then
|
if self._elementMode == "immediate" and self._stateId and self._stateId ~= "" then
|
||||||
local state = Element._StateManager.getState(self._stateId)
|
local state = Element._StateManager.getState(self._stateId)
|
||||||
if state then
|
if state then
|
||||||
-- Restore EventHandler state from StateManager (sparse storage - provide defaults)
|
-- Restore EventHandler state from StateManager (sparse storage - provide defaults)
|
||||||
@@ -419,7 +431,7 @@ function Element.new(props)
|
|||||||
}, textEditorDeps)
|
}, textEditorDeps)
|
||||||
|
|
||||||
-- Restore TextEditor state from StateManager in immediate mode
|
-- Restore TextEditor state from StateManager in immediate mode
|
||||||
if Element._Context._immediateMode and self._stateId and self._stateId ~= "" then
|
if self._elementMode == "immediate" and self._stateId and self._stateId ~= "" then
|
||||||
local state = Element._StateManager.getState(self._stateId)
|
local state = Element._StateManager.getState(self._stateId)
|
||||||
if state and state.textEditor then
|
if state and state.textEditor then
|
||||||
-- Restore from nested textEditor state (saved via saveState())
|
-- Restore from nested textEditor state (saved via saveState())
|
||||||
@@ -1659,7 +1671,7 @@ function Element.new(props)
|
|||||||
self._scrollbarDragOffset = 0
|
self._scrollbarDragOffset = 0
|
||||||
|
|
||||||
-- Restore scrollbar state from StateManager in immediate mode (must happen before layout)
|
-- Restore scrollbar state from StateManager in immediate mode (must happen before layout)
|
||||||
if Element._Context._immediateMode and self._stateId and self._stateId ~= "" then
|
if self._elementMode == "immediate" and self._stateId and self._stateId ~= "" then
|
||||||
local state = Element._StateManager.getState(self._stateId)
|
local state = Element._StateManager.getState(self._stateId)
|
||||||
if state and state.scrollManager then
|
if state and state.scrollManager then
|
||||||
-- Restore from nested scrollManager state (saved via saveState())
|
-- Restore from nested scrollManager state (saved via saveState())
|
||||||
@@ -1688,7 +1700,7 @@ function Element.new(props)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Register element in z-index tracking for immediate mode
|
-- Register element in z-index tracking for immediate mode
|
||||||
if Element._Context._immediateMode then
|
if self._elementMode == "immediate" then
|
||||||
Element._Context.registerElement(self)
|
Element._Context.registerElement(self)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -2413,7 +2425,7 @@ function Element:update(dt)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Restore scrollbar state from StateManager in immediate mode
|
-- Restore scrollbar state from StateManager in immediate mode
|
||||||
if self._stateId and Element._Context._immediateMode then
|
if self._stateId and self._elementMode == "immediate" then
|
||||||
local state = Element._StateManager.getState(self._stateId)
|
local state = Element._StateManager.getState(self._stateId)
|
||||||
if state and state.scrollManager then
|
if state and state.scrollManager then
|
||||||
-- Restore from nested scrollManager state (saved via saveState())
|
-- Restore from nested scrollManager state (saved via saveState())
|
||||||
@@ -2560,7 +2572,7 @@ function Element:update(dt)
|
|||||||
self:_syncScrollManagerState()
|
self:_syncScrollManagerState()
|
||||||
end
|
end
|
||||||
|
|
||||||
if self._stateId and Element._Context._immediateMode then
|
if self._stateId and self._elementMode == "immediate" then
|
||||||
Element._StateManager.updateState(self._stateId, {
|
Element._StateManager.updateState(self._stateId, {
|
||||||
scrollbarDragging = false,
|
scrollbarDragging = false,
|
||||||
})
|
})
|
||||||
@@ -2642,7 +2654,7 @@ function Element:update(dt)
|
|||||||
self._eventHandler:processMouseEvents(self, mx, my, isHovering, isActiveElement)
|
self._eventHandler:processMouseEvents(self, mx, my, isHovering, isActiveElement)
|
||||||
|
|
||||||
-- In immediate mode, save EventHandler state to StateManager after processing events
|
-- In immediate mode, save EventHandler state to StateManager after processing events
|
||||||
if self._stateId and Element._Context._immediateMode and self._stateId ~= "" then
|
if self._stateId and self._elementMode == "immediate" and self._stateId ~= "" then
|
||||||
local eventHandlerState = self._eventHandler:getState()
|
local eventHandlerState = self._eventHandler:getState()
|
||||||
Element._StateManager.updateState(self._stateId, {
|
Element._StateManager.updateState(self._stateId, {
|
||||||
_pressed = eventHandlerState._pressed,
|
_pressed = eventHandlerState._pressed,
|
||||||
@@ -2665,7 +2677,7 @@ function Element:update(dt)
|
|||||||
-- Update theme state via ThemeManager
|
-- Update theme state via ThemeManager
|
||||||
local newThemeState = self._themeManager:updateState(isHovering and isActiveElement, anyPressed, self._focused, self.disabled)
|
local newThemeState = self._themeManager:updateState(isHovering and isActiveElement, anyPressed, self._focused, self.disabled)
|
||||||
|
|
||||||
if self._stateId and Element._Context._immediateMode then
|
if self._stateId and self._elementMode == "immediate" then
|
||||||
local hover = newThemeState == "hover"
|
local hover = newThemeState == "hover"
|
||||||
local pressed = newThemeState == "pressed"
|
local pressed = newThemeState == "pressed"
|
||||||
local focused = newThemeState == "active" or self._focused
|
local focused = newThemeState == "active" or self._focused
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ local AnimationProps = {}
|
|||||||
--=====================================--
|
--=====================================--
|
||||||
---@class ElementProps
|
---@class ElementProps
|
||||||
---@field id string? -- Unique identifier for the element (auto-generated in immediate mode if not provided)
|
---@field id string? -- Unique identifier for the element (auto-generated in immediate mode if not provided)
|
||||||
|
---@field mode "immediate"|"retained"|nil -- Lifecycle mode override: "immediate" (auto-managed state), "retained" (manual state), nil (use global mode from FlexLove.getMode(), default)
|
||||||
---@field parent Element? -- Parent element for hierarchical structure
|
---@field parent Element? -- Parent element for hierarchical structure
|
||||||
---@field x number|string|CalcObject? -- X coordinate: number (px), string ("50%", "10vw"), or CalcObject from FlexLove.calc() (default: 0)
|
---@field x number|string|CalcObject? -- X coordinate: number (px), string ("50%", "10vw"), or CalcObject from FlexLove.calc() (default: 0)
|
||||||
---@field y number|string|CalcObject? -- Y coordinate: number (px), string ("50%", "10vh"), or CalcObject from FlexLove.calc() (default: 0)
|
---@field y number|string|CalcObject? -- Y coordinate: number (px), string ("50%", "10vh"), or CalcObject from FlexLove.calc() (default: 0)
|
||||||
|
|||||||
507
profiling/__profiles__/settings_menu_mode_profile.lua
Normal file
507
profiling/__profiles__/settings_menu_mode_profile.lua
Normal file
@@ -0,0 +1,507 @@
|
|||||||
|
-- Profiling test comparing retained mode flag vs. default behavior in complex UI
|
||||||
|
-- This simulates creating a settings menu multiple times per frame to stress test
|
||||||
|
-- the performance difference between explicit mode="retained" and implicit retained mode
|
||||||
|
|
||||||
|
package.path = package.path .. ";../../?.lua;../../modules/?.lua"
|
||||||
|
|
||||||
|
local FlexLove = require("FlexLove")
|
||||||
|
local Color = require("modules.Color")
|
||||||
|
|
||||||
|
-- Mock resolution sets (simplified)
|
||||||
|
local resolution_sets = {
|
||||||
|
["16:9"] = {
|
||||||
|
{ 1920, 1080 },
|
||||||
|
{ 1600, 900 },
|
||||||
|
{ 1280, 720 },
|
||||||
|
},
|
||||||
|
["16:10"] = {
|
||||||
|
{ 1920, 1200 },
|
||||||
|
{ 1680, 1050 },
|
||||||
|
{ 1280, 800 },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Mock Settings object
|
||||||
|
local Settings = {
|
||||||
|
values = {
|
||||||
|
resolution = { width = 1920, height = 1080 },
|
||||||
|
fullscreen = false,
|
||||||
|
vsync = true,
|
||||||
|
msaa = 4,
|
||||||
|
resizable = true,
|
||||||
|
borderless = false,
|
||||||
|
masterVolume = 0.8,
|
||||||
|
musicVolume = 0.7,
|
||||||
|
sfxVolume = 0.9,
|
||||||
|
crtEffectStrength = 0.3,
|
||||||
|
},
|
||||||
|
get = function(self, key)
|
||||||
|
return self.values[key]
|
||||||
|
end,
|
||||||
|
set = function(self, key, value)
|
||||||
|
self.values[key] = value
|
||||||
|
end,
|
||||||
|
reset_to_defaults = function(self) end,
|
||||||
|
apply = function(self) end,
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Helper function to round numbers
|
||||||
|
local function round(num, decimals)
|
||||||
|
local mult = 10 ^ (decimals or 0)
|
||||||
|
return math.floor(num * mult + 0.5) / mult
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Simplified SettingsMenu implementation
|
||||||
|
local function create_settings_menu_with_mode_flag(use_mode_flag)
|
||||||
|
local GuiZIndexing = { MainMenuOverlay = 100 }
|
||||||
|
|
||||||
|
-- Backdrop
|
||||||
|
local backdrop_props = {
|
||||||
|
z = GuiZIndexing.MainMenuOverlay - 1,
|
||||||
|
width = "100%",
|
||||||
|
height = "100%",
|
||||||
|
backdropBlur = { radius = 10 },
|
||||||
|
backgroundColor = Color.new(1, 1, 1, 0.1),
|
||||||
|
}
|
||||||
|
if use_mode_flag then
|
||||||
|
backdrop_props.mode = "retained"
|
||||||
|
end
|
||||||
|
local backdrop = FlexLove.new(backdrop_props)
|
||||||
|
|
||||||
|
-- Main window
|
||||||
|
local window_props = {
|
||||||
|
z = GuiZIndexing.MainMenuOverlay,
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
if use_mode_flag then
|
||||||
|
window_props.mode = "retained"
|
||||||
|
end
|
||||||
|
local window = FlexLove.new(window_props)
|
||||||
|
|
||||||
|
-- Close button
|
||||||
|
FlexLove.new({
|
||||||
|
parent = window,
|
||||||
|
x = "2%",
|
||||||
|
y = "2%",
|
||||||
|
alignSelf = "flex-start",
|
||||||
|
themeComponent = "buttonv2",
|
||||||
|
width = "4vw",
|
||||||
|
height = "4vw",
|
||||||
|
text = "X",
|
||||||
|
textSize = "2xl",
|
||||||
|
textAlign = "center",
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Title
|
||||||
|
FlexLove.new({
|
||||||
|
parent = window,
|
||||||
|
text = "Settings",
|
||||||
|
textAlign = "center",
|
||||||
|
textSize = "3xl",
|
||||||
|
width = "100%",
|
||||||
|
margin = { top = "-4%", bottom = "4%" },
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Content container
|
||||||
|
local content = FlexLove.new({
|
||||||
|
parent = window,
|
||||||
|
width = "100%",
|
||||||
|
height = "100%",
|
||||||
|
positioning = "flex",
|
||||||
|
flexDirection = "vertical",
|
||||||
|
padding = { top = "4%" },
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Display Settings Section
|
||||||
|
FlexLove.new({
|
||||||
|
parent = content,
|
||||||
|
text = "Display Settings",
|
||||||
|
textAlign = "start",
|
||||||
|
textSize = "xl",
|
||||||
|
width = "100%",
|
||||||
|
textColor = Color.new(0.8, 0.9, 1, 1),
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Resolution control
|
||||||
|
local row1 = FlexLove.new({
|
||||||
|
parent = content,
|
||||||
|
width = "100%",
|
||||||
|
height = "5vh",
|
||||||
|
positioning = "flex",
|
||||||
|
flexDirection = "horizontal",
|
||||||
|
justifyContent = "space-between",
|
||||||
|
alignItems = "center",
|
||||||
|
gap = 10,
|
||||||
|
})
|
||||||
|
|
||||||
|
FlexLove.new({
|
||||||
|
parent = row1,
|
||||||
|
text = "Resolution",
|
||||||
|
textAlign = "start",
|
||||||
|
textSize = "md",
|
||||||
|
width = "30%",
|
||||||
|
})
|
||||||
|
|
||||||
|
local resolution = Settings:get("resolution")
|
||||||
|
FlexLove.new({
|
||||||
|
parent = row1,
|
||||||
|
text = resolution.width .. " x " .. resolution.height,
|
||||||
|
themeComponent = "buttonv2",
|
||||||
|
width = "30%",
|
||||||
|
textAlign = "center",
|
||||||
|
textSize = "lg",
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Fullscreen toggle
|
||||||
|
local row2 = FlexLove.new({
|
||||||
|
parent = content,
|
||||||
|
width = "100%",
|
||||||
|
height = "5vh",
|
||||||
|
positioning = "flex",
|
||||||
|
flexDirection = "horizontal",
|
||||||
|
justifyContent = "space-between",
|
||||||
|
alignItems = "center",
|
||||||
|
gap = 10,
|
||||||
|
})
|
||||||
|
|
||||||
|
FlexLove.new({
|
||||||
|
parent = row2,
|
||||||
|
text = "Fullscreen",
|
||||||
|
textAlign = "start",
|
||||||
|
textSize = "md",
|
||||||
|
width = "60%",
|
||||||
|
})
|
||||||
|
|
||||||
|
local fullscreen = Settings:get("fullscreen")
|
||||||
|
FlexLove.new({
|
||||||
|
parent = row2,
|
||||||
|
text = fullscreen and "ON" or "OFF",
|
||||||
|
themeComponent = fullscreen and "buttonv1" or "buttonv2",
|
||||||
|
textAlign = "center",
|
||||||
|
width = "15vw",
|
||||||
|
height = "4vh",
|
||||||
|
textSize = "md",
|
||||||
|
})
|
||||||
|
|
||||||
|
-- VSync toggle
|
||||||
|
local row3 = FlexLove.new({
|
||||||
|
parent = content,
|
||||||
|
width = "100%",
|
||||||
|
height = "5vh",
|
||||||
|
positioning = "flex",
|
||||||
|
flexDirection = "horizontal",
|
||||||
|
justifyContent = "space-between",
|
||||||
|
alignItems = "center",
|
||||||
|
gap = 10,
|
||||||
|
})
|
||||||
|
|
||||||
|
FlexLove.new({
|
||||||
|
parent = row3,
|
||||||
|
text = "VSync",
|
||||||
|
textAlign = "start",
|
||||||
|
textSize = "md",
|
||||||
|
width = "60%",
|
||||||
|
})
|
||||||
|
|
||||||
|
local vsync = Settings:get("vsync")
|
||||||
|
FlexLove.new({
|
||||||
|
parent = row3,
|
||||||
|
text = vsync and "ON" or "OFF",
|
||||||
|
themeComponent = vsync and "buttonv1" or "buttonv2",
|
||||||
|
textAlign = "center",
|
||||||
|
width = "15vw",
|
||||||
|
height = "4vh",
|
||||||
|
textSize = "md",
|
||||||
|
})
|
||||||
|
|
||||||
|
-- MSAA control
|
||||||
|
local row4 = FlexLove.new({
|
||||||
|
parent = content,
|
||||||
|
width = "100%",
|
||||||
|
height = "5vh",
|
||||||
|
positioning = "flex",
|
||||||
|
flexDirection = "horizontal",
|
||||||
|
justifyContent = "space-between",
|
||||||
|
alignItems = "center",
|
||||||
|
gap = 10,
|
||||||
|
})
|
||||||
|
|
||||||
|
FlexLove.new({
|
||||||
|
parent = row4,
|
||||||
|
text = "MSAA",
|
||||||
|
textAlign = "start",
|
||||||
|
textSize = "md",
|
||||||
|
width = "30%",
|
||||||
|
})
|
||||||
|
|
||||||
|
local button_container = FlexLove.new({
|
||||||
|
parent = row4,
|
||||||
|
width = "60%",
|
||||||
|
height = "100%",
|
||||||
|
positioning = "flex",
|
||||||
|
flexDirection = "horizontal",
|
||||||
|
gap = 5,
|
||||||
|
})
|
||||||
|
|
||||||
|
local msaa_values = { 0, 1, 2, 4, 8, 16 }
|
||||||
|
for _, msaa_val in ipairs(msaa_values) do
|
||||||
|
local is_selected = Settings:get("msaa") == msaa_val
|
||||||
|
FlexLove.new({
|
||||||
|
parent = button_container,
|
||||||
|
themeComponent = is_selected and "buttonv1" or "buttonv2",
|
||||||
|
text = tostring(msaa_val),
|
||||||
|
textAlign = "center",
|
||||||
|
width = "8vw",
|
||||||
|
height = "100%",
|
||||||
|
textSize = "sm",
|
||||||
|
disabled = is_selected,
|
||||||
|
opacity = is_selected and 0.7 or 1.0,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Audio Settings Section
|
||||||
|
FlexLove.new({
|
||||||
|
parent = content,
|
||||||
|
text = "Audio Settings",
|
||||||
|
textAlign = "start",
|
||||||
|
textSize = "xl",
|
||||||
|
width = "100%",
|
||||||
|
textColor = Color.new(0.8, 0.9, 1, 1),
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Master volume slider
|
||||||
|
local row5 = FlexLove.new({
|
||||||
|
parent = content,
|
||||||
|
width = "100%",
|
||||||
|
height = "5vh",
|
||||||
|
positioning = "flex",
|
||||||
|
flexDirection = "horizontal",
|
||||||
|
justifyContent = "space-between",
|
||||||
|
alignItems = "center",
|
||||||
|
gap = 10,
|
||||||
|
})
|
||||||
|
|
||||||
|
FlexLove.new({
|
||||||
|
parent = row5,
|
||||||
|
text = "Master Volume",
|
||||||
|
textAlign = "start",
|
||||||
|
textSize = "md",
|
||||||
|
width = "30%",
|
||||||
|
})
|
||||||
|
|
||||||
|
local slider_container = FlexLove.new({
|
||||||
|
parent = row5,
|
||||||
|
width = "50%",
|
||||||
|
height = "100%",
|
||||||
|
positioning = "flex",
|
||||||
|
flexDirection = "horizontal",
|
||||||
|
alignItems = "center",
|
||||||
|
gap = 5,
|
||||||
|
})
|
||||||
|
|
||||||
|
local value = Settings:get("masterVolume")
|
||||||
|
local normalized = value
|
||||||
|
|
||||||
|
local slider_track = FlexLove.new({
|
||||||
|
parent = slider_container,
|
||||||
|
width = "80%",
|
||||||
|
height = "75%",
|
||||||
|
positioning = "flex",
|
||||||
|
flexDirection = "horizontal",
|
||||||
|
themeComponent = "framev3",
|
||||||
|
})
|
||||||
|
|
||||||
|
FlexLove.new({
|
||||||
|
parent = slider_track,
|
||||||
|
width = (normalized * 100) .. "%",
|
||||||
|
height = "100%",
|
||||||
|
themeComponent = "buttonv1",
|
||||||
|
themeStateLock = true,
|
||||||
|
})
|
||||||
|
|
||||||
|
FlexLove.new({
|
||||||
|
parent = slider_container,
|
||||||
|
text = string.format("%d", value * 100),
|
||||||
|
textAlign = "center",
|
||||||
|
textSize = "md",
|
||||||
|
width = "15%",
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Meta controls (bottom buttons)
|
||||||
|
local meta_container = FlexLove.new({
|
||||||
|
parent = window,
|
||||||
|
positioning = "absolute",
|
||||||
|
width = "100%",
|
||||||
|
height = "10%",
|
||||||
|
y = "90%",
|
||||||
|
x = "0%",
|
||||||
|
})
|
||||||
|
|
||||||
|
local button_bar = FlexLove.new({
|
||||||
|
parent = meta_container,
|
||||||
|
width = "100%",
|
||||||
|
positioning = "flex",
|
||||||
|
flexDirection = "horizontal",
|
||||||
|
justifyContent = "center",
|
||||||
|
alignItems = "center",
|
||||||
|
gap = 10,
|
||||||
|
})
|
||||||
|
|
||||||
|
FlexLove.new({
|
||||||
|
parent = button_bar,
|
||||||
|
themeComponent = "buttonv2",
|
||||||
|
text = "Reset",
|
||||||
|
textAlign = "center",
|
||||||
|
width = "15vw",
|
||||||
|
height = "6vh",
|
||||||
|
textSize = "lg",
|
||||||
|
})
|
||||||
|
|
||||||
|
return { backdrop = backdrop, window = window }
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Profile configuration
|
||||||
|
local PROFILE_NAME = "Settings Menu Mode Comparison"
|
||||||
|
local ITERATIONS_PER_TEST = 100 -- Create the menu 100 times to measure difference
|
||||||
|
|
||||||
|
print("=" .. string.rep("=", 78))
|
||||||
|
print(string.format(" %s", PROFILE_NAME))
|
||||||
|
print("=" .. string.rep("=", 78))
|
||||||
|
print()
|
||||||
|
print("This profile compares performance when creating a complex settings menu")
|
||||||
|
print("with explicit mode='retained' flags vs. implicit retained mode (global).")
|
||||||
|
print()
|
||||||
|
print(string.format("Test configuration:"))
|
||||||
|
print(string.format(" - Iterations: %d menu creations per test", ITERATIONS_PER_TEST))
|
||||||
|
print(string.format(" - Elements per menu: ~45 (backdrop, window, buttons, sliders, etc.)"))
|
||||||
|
print(string.format(" - Total elements created: ~%d per test", ITERATIONS_PER_TEST * 45))
|
||||||
|
print()
|
||||||
|
|
||||||
|
-- Warm up
|
||||||
|
print("Warming up...")
|
||||||
|
FlexLove.init({ immediateMode = false, theme = "space" })
|
||||||
|
for i = 1, 10 do
|
||||||
|
create_settings_menu_with_mode_flag(false)
|
||||||
|
end
|
||||||
|
collectgarbage("collect")
|
||||||
|
|
||||||
|
-- Test 1: Without explicit mode flags (implicit retained via global setting)
|
||||||
|
print("Running Test 1: Without explicit mode='retained' flags...")
|
||||||
|
FlexLove.init({ immediateMode = false, theme = "space" })
|
||||||
|
collectgarbage("collect")
|
||||||
|
local mem_before_implicit = collectgarbage("count")
|
||||||
|
local time_before_implicit = os.clock()
|
||||||
|
|
||||||
|
for i = 1, ITERATIONS_PER_TEST do
|
||||||
|
local menu = create_settings_menu_with_mode_flag(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
local time_after_implicit = os.clock()
|
||||||
|
collectgarbage("collect")
|
||||||
|
local mem_after_implicit = collectgarbage("count")
|
||||||
|
|
||||||
|
local time_implicit = time_after_implicit - time_before_implicit
|
||||||
|
local mem_implicit = mem_after_implicit - mem_before_implicit
|
||||||
|
|
||||||
|
print(string.format(" Time: %.4f seconds", time_implicit))
|
||||||
|
print(string.format(" Memory: %.2f KB", mem_implicit))
|
||||||
|
print(string.format(" Avg time per menu: %.4f ms", (time_implicit / ITERATIONS_PER_TEST) * 1000))
|
||||||
|
print()
|
||||||
|
|
||||||
|
-- Test 2: With explicit mode="retained" flags
|
||||||
|
print("Running Test 2: With explicit mode='retained' flags...")
|
||||||
|
FlexLove.init({ immediateMode = false, theme = "space" })
|
||||||
|
collectgarbage("collect")
|
||||||
|
local mem_before_explicit = collectgarbage("count")
|
||||||
|
local time_before_explicit = os.clock()
|
||||||
|
|
||||||
|
for i = 1, ITERATIONS_PER_TEST do
|
||||||
|
local menu = create_settings_menu_with_mode_flag(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
local time_after_explicit = os.clock()
|
||||||
|
collectgarbage("collect")
|
||||||
|
local mem_after_explicit = collectgarbage("count")
|
||||||
|
|
||||||
|
local time_explicit = time_after_explicit - time_before_explicit
|
||||||
|
local mem_explicit = mem_after_explicit - mem_before_explicit
|
||||||
|
|
||||||
|
print(string.format(" Time: %.4f seconds", time_explicit))
|
||||||
|
print(string.format(" Memory: %.2f KB", mem_explicit))
|
||||||
|
print(string.format(" Avg time per menu: %.4f ms", (time_explicit / ITERATIONS_PER_TEST) * 1000))
|
||||||
|
print()
|
||||||
|
|
||||||
|
-- Calculate differences
|
||||||
|
print("=" .. string.rep("=", 78))
|
||||||
|
print("RESULTS COMPARISON")
|
||||||
|
print("=" .. string.rep("=", 78))
|
||||||
|
print()
|
||||||
|
|
||||||
|
local time_diff = time_explicit - time_implicit
|
||||||
|
local time_percent = (time_diff / time_implicit) * 100
|
||||||
|
local mem_diff = mem_explicit - mem_implicit
|
||||||
|
|
||||||
|
print(string.format("Time Difference:"))
|
||||||
|
print(string.format(" Without mode flag: %.4f seconds", time_implicit))
|
||||||
|
print(string.format(" With mode flag: %.4f seconds", time_explicit))
|
||||||
|
print(string.format(" Difference: %.4f seconds (%+.2f%%)", time_diff, time_percent))
|
||||||
|
print()
|
||||||
|
|
||||||
|
print(string.format("Memory Difference:"))
|
||||||
|
print(string.format(" Without mode flag: %.2f KB", mem_implicit))
|
||||||
|
print(string.format(" With mode flag: %.2f KB", mem_explicit))
|
||||||
|
print(string.format(" Difference: %+.2f KB", mem_diff))
|
||||||
|
print()
|
||||||
|
|
||||||
|
-- Interpretation
|
||||||
|
print("INTERPRETATION:")
|
||||||
|
print()
|
||||||
|
if math.abs(time_percent) < 5 then
|
||||||
|
print(" ✓ Performance is essentially identical (< 5% difference)")
|
||||||
|
print(" The explicit mode flag has negligible impact on performance.")
|
||||||
|
elseif time_percent > 0 then
|
||||||
|
print(string.format(" ⚠ Explicit mode flag is %.2f%% SLOWER", time_percent))
|
||||||
|
print(" This indicates overhead from mode checking/resolution.")
|
||||||
|
else
|
||||||
|
print(string.format(" ✓ Explicit mode flag is %.2f%% FASTER", -time_percent))
|
||||||
|
print(" This indicates potential optimization benefits.")
|
||||||
|
end
|
||||||
|
print()
|
||||||
|
|
||||||
|
if math.abs(mem_diff) < 50 then
|
||||||
|
print(" ✓ Memory usage is essentially identical (< 50 KB difference)")
|
||||||
|
elseif mem_diff > 0 then
|
||||||
|
print(string.format(" ⚠ Explicit mode flag uses %.2f KB MORE memory", mem_diff))
|
||||||
|
else
|
||||||
|
print(string.format(" ✓ Explicit mode flag uses %.2f KB LESS memory", -mem_diff))
|
||||||
|
end
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("RECOMMENDATION:")
|
||||||
|
print()
|
||||||
|
if math.abs(time_percent) < 5 and math.abs(mem_diff) < 50 then
|
||||||
|
print(" The explicit mode='retained' flag provides clarity and explicitness")
|
||||||
|
print(" without any meaningful performance cost. It's recommended for:")
|
||||||
|
print(" - Code readability (makes intent explicit)")
|
||||||
|
print(" - Future-proofing (if global mode changes)")
|
||||||
|
print(" - Mixed-mode UIs (where some elements are immediate)")
|
||||||
|
else
|
||||||
|
print(" Consider the trade-offs based on your specific use case.")
|
||||||
|
end
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("=" .. string.rep("=", 78))
|
||||||
|
print("Profile complete!")
|
||||||
|
print("=" .. string.rep("=", 78))
|
||||||
343
testing/__tests__/element_mode_override_test.lua
Normal file
343
testing/__tests__/element_mode_override_test.lua
Normal file
@@ -0,0 +1,343 @@
|
|||||||
|
-- Test suite for Element mode override functionality
|
||||||
|
|
||||||
|
package.path = package.path .. ";./?.lua;./modules/?.lua"
|
||||||
|
|
||||||
|
-- Load love stub before anything else
|
||||||
|
require("testing.loveStub")
|
||||||
|
|
||||||
|
local luaunit = require("testing.luaunit")
|
||||||
|
local ErrorHandler = require("modules.ErrorHandler")
|
||||||
|
|
||||||
|
-- Initialize ErrorHandler
|
||||||
|
ErrorHandler.init({})
|
||||||
|
|
||||||
|
-- Setup package loader to map FlexLove.modules.X to modules/X
|
||||||
|
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)
|
||||||
|
|
||||||
|
-- Load FlexLove which properly initializes all dependencies
|
||||||
|
local FlexLove = require("FlexLove")
|
||||||
|
local StateManager = require("modules.StateManager")
|
||||||
|
|
||||||
|
TestElementModeOverride = {}
|
||||||
|
|
||||||
|
function TestElementModeOverride:setUp()
|
||||||
|
-- Initialize FlexLove in immediate mode by default
|
||||||
|
FlexLove.init({ immediateMode = true })
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestElementModeOverride:tearDown()
|
||||||
|
if FlexLove.getMode() == "immediate" then
|
||||||
|
FlexLove.endFrame()
|
||||||
|
end
|
||||||
|
-- Reset to default state
|
||||||
|
FlexLove.init({ immediateMode = false })
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test 01: Mode resolution - explicit immediate
|
||||||
|
function TestElementModeOverride:test_modeResolution_explicitImmediate()
|
||||||
|
local element = FlexLove.new({
|
||||||
|
mode = "immediate",
|
||||||
|
text = "Test",
|
||||||
|
})
|
||||||
|
|
||||||
|
luaunit.assertEquals(element._elementMode, "immediate")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test 02: Mode resolution - explicit retained
|
||||||
|
function TestElementModeOverride:test_modeResolution_explicitRetained()
|
||||||
|
local element = FlexLove.new({
|
||||||
|
mode = "retained",
|
||||||
|
text = "Test",
|
||||||
|
})
|
||||||
|
|
||||||
|
luaunit.assertEquals(element._elementMode, "retained")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test 03: Mode resolution - nil uses global (immediate)
|
||||||
|
function TestElementModeOverride:test_modeResolution_nilUsesGlobalImmediate()
|
||||||
|
FlexLove.setMode("immediate")
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
|
||||||
|
local element = FlexLove.new({
|
||||||
|
text = "Test",
|
||||||
|
})
|
||||||
|
|
||||||
|
luaunit.assertEquals(element._elementMode, "immediate")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test 04: Mode resolution - nil uses global (retained)
|
||||||
|
function TestElementModeOverride:test_modeResolution_nilUsesGlobalRetained()
|
||||||
|
FlexLove.setMode("retained")
|
||||||
|
|
||||||
|
local element = FlexLove.new({
|
||||||
|
text = "Test",
|
||||||
|
})
|
||||||
|
|
||||||
|
luaunit.assertEquals(element._elementMode, "retained")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test 05: ID auto-generation only for immediate mode
|
||||||
|
function TestElementModeOverride:test_idGeneration_onlyForImmediate()
|
||||||
|
-- Immediate element without ID should get auto-generated ID
|
||||||
|
local immediateEl = FlexLove.new({
|
||||||
|
mode = "immediate",
|
||||||
|
text = "Immediate",
|
||||||
|
})
|
||||||
|
luaunit.assertNotNil(immediateEl.id)
|
||||||
|
luaunit.assertNotEquals(immediateEl.id, "")
|
||||||
|
|
||||||
|
-- Retained element without ID should have empty ID
|
||||||
|
local retainedEl = FlexLove.new({
|
||||||
|
mode = "retained",
|
||||||
|
text = "Retained",
|
||||||
|
})
|
||||||
|
luaunit.assertEquals(retainedEl.id, "")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test 06: Immediate override in retained context
|
||||||
|
function TestElementModeOverride:test_immediateOverrideInRetainedContext()
|
||||||
|
FlexLove.setMode("retained")
|
||||||
|
|
||||||
|
local element = FlexLove.new({
|
||||||
|
mode = "immediate",
|
||||||
|
id = "test-immediate",
|
||||||
|
text = "Immediate in retained context",
|
||||||
|
})
|
||||||
|
|
||||||
|
luaunit.assertEquals(element._elementMode, "immediate")
|
||||||
|
luaunit.assertEquals(element.id, "test-immediate")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test 07: Retained override in immediate context
|
||||||
|
function TestElementModeOverride:test_retainedOverrideInImmediateContext()
|
||||||
|
FlexLove.setMode("immediate")
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
|
||||||
|
local element = FlexLove.new({
|
||||||
|
mode = "retained",
|
||||||
|
text = "Retained in immediate context",
|
||||||
|
})
|
||||||
|
|
||||||
|
luaunit.assertEquals(element._elementMode, "retained")
|
||||||
|
luaunit.assertEquals(element.id, "") -- Should not auto-generate ID
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test 08: Mixed-mode parent-child (immediate parent, retained child)
|
||||||
|
function TestElementModeOverride:test_mixedMode_immediateParent_retainedChild()
|
||||||
|
FlexLove.setMode("immediate")
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
|
||||||
|
local parent = FlexLove.new({
|
||||||
|
mode = "immediate",
|
||||||
|
id = "parent",
|
||||||
|
text = "Parent",
|
||||||
|
})
|
||||||
|
|
||||||
|
local child = FlexLove.new({
|
||||||
|
mode = "retained",
|
||||||
|
parent = parent,
|
||||||
|
text = "Child",
|
||||||
|
})
|
||||||
|
|
||||||
|
luaunit.assertEquals(parent._elementMode, "immediate")
|
||||||
|
luaunit.assertEquals(child._elementMode, "retained")
|
||||||
|
-- Child should not inherit parent mode
|
||||||
|
luaunit.assertNotEquals(child._elementMode, parent._elementMode)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test 09: Mixed-mode parent-child (retained parent, immediate child)
|
||||||
|
function TestElementModeOverride:test_mixedMode_retainedParent_immediateChild()
|
||||||
|
FlexLove.setMode("retained")
|
||||||
|
|
||||||
|
local parent = FlexLove.new({
|
||||||
|
mode = "retained",
|
||||||
|
text = "Parent",
|
||||||
|
})
|
||||||
|
|
||||||
|
local child = FlexLove.new({
|
||||||
|
mode = "immediate",
|
||||||
|
id = "child",
|
||||||
|
parent = parent,
|
||||||
|
text = "Child",
|
||||||
|
})
|
||||||
|
|
||||||
|
luaunit.assertEquals(parent._elementMode, "retained")
|
||||||
|
luaunit.assertEquals(child._elementMode, "immediate")
|
||||||
|
luaunit.assertEquals(child.id, "child")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test 10: Frame registration only for immediate elements
|
||||||
|
function TestElementModeOverride:test_frameRegistration_onlyImmediate()
|
||||||
|
FlexLove.setMode("immediate")
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
|
||||||
|
local immediate1 = FlexLove.new({
|
||||||
|
mode = "immediate",
|
||||||
|
id = "imm1",
|
||||||
|
text = "Immediate 1",
|
||||||
|
})
|
||||||
|
|
||||||
|
local retained1 = FlexLove.new({
|
||||||
|
mode = "retained",
|
||||||
|
text = "Retained 1",
|
||||||
|
})
|
||||||
|
|
||||||
|
local immediate2 = FlexLove.new({
|
||||||
|
mode = "immediate",
|
||||||
|
id = "imm2",
|
||||||
|
text = "Immediate 2",
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Count immediate elements in _currentFrameElements
|
||||||
|
local immediateCount = 0
|
||||||
|
for _, element in ipairs(FlexLove._currentFrameElements) do
|
||||||
|
if element._elementMode == "immediate" then
|
||||||
|
immediateCount = immediateCount + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
luaunit.assertEquals(immediateCount, 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test 11: Layout calculation for retained parent with immediate children
|
||||||
|
function TestElementModeOverride:test_layoutRetainedParentWithImmediateChildren()
|
||||||
|
FlexLove.setMode("retained")
|
||||||
|
|
||||||
|
-- Create retained parent with flex layout
|
||||||
|
local parent = FlexLove.new({
|
||||||
|
mode = "retained",
|
||||||
|
width = 800,
|
||||||
|
height = 600,
|
||||||
|
positioning = "flex",
|
||||||
|
flexDirection = "horizontal",
|
||||||
|
gap = 10,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Switch to immediate mode and add children
|
||||||
|
FlexLove.setMode("immediate")
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
|
||||||
|
local child1 = FlexLove.new({
|
||||||
|
mode = "immediate",
|
||||||
|
id = "child1",
|
||||||
|
parent = parent,
|
||||||
|
width = 100,
|
||||||
|
height = 50,
|
||||||
|
})
|
||||||
|
|
||||||
|
local child2 = FlexLove.new({
|
||||||
|
mode = "immediate",
|
||||||
|
id = "child2",
|
||||||
|
parent = parent,
|
||||||
|
width = 100,
|
||||||
|
height = 50,
|
||||||
|
})
|
||||||
|
|
||||||
|
FlexLove.endFrame()
|
||||||
|
|
||||||
|
-- Verify children are positioned correctly by flex layout
|
||||||
|
luaunit.assertEquals(child1.x, 0)
|
||||||
|
luaunit.assertEquals(child1.y, 0)
|
||||||
|
luaunit.assertEquals(child2.x, 110) -- 100 + 10 gap
|
||||||
|
luaunit.assertEquals(child2.y, 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test 12: Deeply nested mixed modes (retained -> immediate -> retained)
|
||||||
|
function TestElementModeOverride:test_deeplyNestedMixedModes()
|
||||||
|
FlexLove.setMode("immediate")
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
|
||||||
|
-- Level 1: Retained root
|
||||||
|
local root = FlexLove.new({
|
||||||
|
mode = "retained",
|
||||||
|
width = 800,
|
||||||
|
height = 600,
|
||||||
|
positioning = "flex",
|
||||||
|
flexDirection = "vertical",
|
||||||
|
gap = 5,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Level 2: Immediate child of retained parent
|
||||||
|
local middle = FlexLove.new({
|
||||||
|
mode = "immediate",
|
||||||
|
id = "middle",
|
||||||
|
parent = root,
|
||||||
|
width = 400,
|
||||||
|
height = 300,
|
||||||
|
positioning = "flex",
|
||||||
|
flexDirection = "horizontal",
|
||||||
|
gap = 10,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Level 3: Retained grandchildren
|
||||||
|
local leaf1 = FlexLove.new({
|
||||||
|
mode = "retained",
|
||||||
|
parent = middle,
|
||||||
|
width = 100,
|
||||||
|
height = 50,
|
||||||
|
})
|
||||||
|
|
||||||
|
local leaf2 = FlexLove.new({
|
||||||
|
mode = "retained",
|
||||||
|
parent = middle,
|
||||||
|
width = 100,
|
||||||
|
height = 50,
|
||||||
|
})
|
||||||
|
|
||||||
|
FlexLove.endFrame()
|
||||||
|
|
||||||
|
-- Verify all levels are positioned correctly
|
||||||
|
luaunit.assertEquals(root.x, 0)
|
||||||
|
luaunit.assertEquals(root.y, 0)
|
||||||
|
luaunit.assertEquals(middle.x, 0)
|
||||||
|
luaunit.assertEquals(middle.y, 0)
|
||||||
|
luaunit.assertEquals(leaf1.x, 0)
|
||||||
|
luaunit.assertEquals(leaf1.y, 0)
|
||||||
|
luaunit.assertEquals(leaf2.x, 110) -- 100 + 10 gap
|
||||||
|
luaunit.assertEquals(leaf2.y, 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test 13: Immediate children of retained parents receive updates
|
||||||
|
function TestElementModeOverride:test_immediateChildrenOfRetainedParentsGetUpdated()
|
||||||
|
FlexLove.setMode("retained")
|
||||||
|
|
||||||
|
local updateCount = 0
|
||||||
|
|
||||||
|
-- Create retained parent
|
||||||
|
local parent = FlexLove.new({
|
||||||
|
mode = "retained",
|
||||||
|
width = 800,
|
||||||
|
height = 600,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Switch to immediate mode for child
|
||||||
|
FlexLove.setMode("immediate")
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
|
||||||
|
-- Create immediate child that tracks updates
|
||||||
|
local child = FlexLove.new({
|
||||||
|
mode = "immediate",
|
||||||
|
id = "updateTest",
|
||||||
|
parent = parent,
|
||||||
|
width = 100,
|
||||||
|
height = 50,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Manually call update on the child to simulate what endFrame should do
|
||||||
|
-- In the real implementation, endFrame calls update on retained parents,
|
||||||
|
-- which cascades to immediate children
|
||||||
|
FlexLove.endFrame()
|
||||||
|
|
||||||
|
-- The child should be in the state manager
|
||||||
|
local state = StateManager.getState("updateTest")
|
||||||
|
luaunit.assertNotNil(state)
|
||||||
|
end
|
||||||
|
|
||||||
|
os.exit(luaunit.LuaUnit.run())
|
||||||
192
testing/__tests__/mixed_mode_events_test.lua
Normal file
192
testing/__tests__/mixed_mode_events_test.lua
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
-- Test event handling for immediate children of retained parents
|
||||||
|
|
||||||
|
package.path = package.path .. ";./?.lua;./modules/?.lua"
|
||||||
|
|
||||||
|
-- Load love stub before anything else
|
||||||
|
require("testing.loveStub")
|
||||||
|
|
||||||
|
local luaunit = require("testing.luaunit")
|
||||||
|
local ErrorHandler = require("modules.ErrorHandler")
|
||||||
|
|
||||||
|
-- Initialize ErrorHandler
|
||||||
|
ErrorHandler.init({})
|
||||||
|
|
||||||
|
-- Setup package loader to map FlexLove.modules.X to modules/X
|
||||||
|
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)
|
||||||
|
|
||||||
|
local FlexLove = require("FlexLove")
|
||||||
|
local Color = require("modules.Color")
|
||||||
|
local Element = require("modules.Element")
|
||||||
|
|
||||||
|
TestMixedModeEvents = {}
|
||||||
|
|
||||||
|
function TestMixedModeEvents:setUp()
|
||||||
|
FlexLove.init({ immediateMode = false })
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestMixedModeEvents:tearDown()
|
||||||
|
if FlexLove.getMode() == "immediate" then
|
||||||
|
FlexLove.endFrame()
|
||||||
|
end
|
||||||
|
FlexLove.init({ immediateMode = false })
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test that immediate children of retained parents can handle events
|
||||||
|
function TestMixedModeEvents:test_immediateChildOfRetainedParentHandlesEvents()
|
||||||
|
local eventFired = false
|
||||||
|
local eventType = nil
|
||||||
|
|
||||||
|
-- Create retained parent
|
||||||
|
local parent = FlexLove.new({
|
||||||
|
mode = "retained",
|
||||||
|
width = 800,
|
||||||
|
height = 600,
|
||||||
|
})
|
||||||
|
|
||||||
|
FlexLove.setMode("immediate")
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
|
||||||
|
-- Create immediate child with event handler
|
||||||
|
local child = FlexLove.new({
|
||||||
|
mode = "immediate",
|
||||||
|
id = "eventTestChild",
|
||||||
|
parent = parent,
|
||||||
|
x = 0,
|
||||||
|
y = 0,
|
||||||
|
width = 100,
|
||||||
|
height = 50,
|
||||||
|
onEvent = function(element, event)
|
||||||
|
eventFired = true
|
||||||
|
eventType = event.type
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
|
FlexLove.endFrame()
|
||||||
|
|
||||||
|
-- Verify child is positioned correctly
|
||||||
|
luaunit.assertEquals(child.x, 0)
|
||||||
|
luaunit.assertEquals(child.y, 0)
|
||||||
|
|
||||||
|
-- Manually call the event handler (simulating event processing)
|
||||||
|
-- In the real app, this would be triggered by mousepressed/released
|
||||||
|
if child.onEvent then
|
||||||
|
child.onEvent(child, { type = "release", x = 50, y = 25, button = 1 })
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Verify event was handled
|
||||||
|
luaunit.assertTrue(eventFired)
|
||||||
|
luaunit.assertEquals(eventType, "release")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test that hover state is tracked for immediate children
|
||||||
|
function TestMixedModeEvents:test_immediateChildOfRetainedParentTracksHover()
|
||||||
|
FlexLove.setMode("retained")
|
||||||
|
|
||||||
|
-- Create retained parent
|
||||||
|
local parent = FlexLove.new({
|
||||||
|
mode = "retained",
|
||||||
|
width = 800,
|
||||||
|
height = 600,
|
||||||
|
})
|
||||||
|
|
||||||
|
FlexLove.setMode("immediate")
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
|
||||||
|
-- Create immediate child
|
||||||
|
local child = FlexLove.new({
|
||||||
|
mode = "immediate",
|
||||||
|
id = "hoverTestChild",
|
||||||
|
parent = parent,
|
||||||
|
x = 100,
|
||||||
|
y = 100,
|
||||||
|
width = 100,
|
||||||
|
height = 50,
|
||||||
|
})
|
||||||
|
|
||||||
|
FlexLove.endFrame()
|
||||||
|
|
||||||
|
-- Child should have event handler module
|
||||||
|
luaunit.assertNotNil(child._eventHandler)
|
||||||
|
|
||||||
|
-- Verify child can track hover state (stored in StateManager for immediate mode)
|
||||||
|
-- The actual hover detection happens in Element's event processing
|
||||||
|
luaunit.assertEquals(child._elementMode, "immediate")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test multiple immediate children with different event handlers
|
||||||
|
function TestMixedModeEvents:test_multipleImmediateChildrenHandleEventsIndependently()
|
||||||
|
local button1Clicked = false
|
||||||
|
local button2Clicked = false
|
||||||
|
|
||||||
|
FlexLove.setMode("retained")
|
||||||
|
|
||||||
|
-- Create retained parent
|
||||||
|
local parent = FlexLove.new({
|
||||||
|
mode = "retained",
|
||||||
|
width = 800,
|
||||||
|
height = 600,
|
||||||
|
positioning = "flex",
|
||||||
|
flexDirection = "horizontal",
|
||||||
|
gap = 10,
|
||||||
|
})
|
||||||
|
|
||||||
|
FlexLove.setMode("immediate")
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
|
||||||
|
-- Create two immediate button children
|
||||||
|
local button1 = FlexLove.new({
|
||||||
|
mode = "immediate",
|
||||||
|
id = "button1",
|
||||||
|
parent = parent,
|
||||||
|
width = 100,
|
||||||
|
height = 50,
|
||||||
|
onEvent = function(element, event)
|
||||||
|
if event.type == "release" then
|
||||||
|
button1Clicked = true
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
|
local button2 = FlexLove.new({
|
||||||
|
mode = "immediate",
|
||||||
|
id = "button2",
|
||||||
|
parent = parent,
|
||||||
|
width = 100,
|
||||||
|
height = 50,
|
||||||
|
onEvent = function(element, event)
|
||||||
|
if event.type == "release" then
|
||||||
|
button2Clicked = true
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
|
FlexLove.endFrame()
|
||||||
|
|
||||||
|
-- Verify buttons are positioned correctly
|
||||||
|
luaunit.assertEquals(button1.x, 0)
|
||||||
|
luaunit.assertEquals(button2.x, 110) -- 100 + 10 gap
|
||||||
|
|
||||||
|
-- Simulate clicking button1
|
||||||
|
if button1.onEvent then
|
||||||
|
button1.onEvent(button1, { type = "release", x = 50, y = 25, button = 1 })
|
||||||
|
end
|
||||||
|
|
||||||
|
luaunit.assertTrue(button1Clicked)
|
||||||
|
luaunit.assertFalse(button2Clicked)
|
||||||
|
|
||||||
|
-- Simulate clicking button2
|
||||||
|
if button2.onEvent then
|
||||||
|
button2.onEvent(button2, { type = "release", x = 150, y = 25, button = 1 })
|
||||||
|
end
|
||||||
|
|
||||||
|
luaunit.assertTrue(button1Clicked)
|
||||||
|
luaunit.assertTrue(button2Clicked)
|
||||||
|
end
|
||||||
|
|
||||||
|
os.exit(luaunit.LuaUnit.run())
|
||||||
Reference in New Issue
Block a user