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
|
||||
-- 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
|
||||
if not element.parent then
|
||||
if not element.parent and element._elementMode == "immediate" then
|
||||
element:layoutChildren() -- Layout with all children present
|
||||
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
|
||||
-- This happens AFTER layout so positions are correct
|
||||
-- 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
|
||||
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)
|
||||
end
|
||||
end
|
||||
@@ -452,8 +480,9 @@ function flexlove.endFrame()
|
||||
-- Save state for all elements created this frame
|
||||
-- State is collected from element and all sub-modules via element:saveState()
|
||||
-- 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
|
||||
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
|
||||
local stateUpdate = element:saveState()
|
||||
|
||||
@@ -1010,11 +1039,15 @@ end
|
||||
function flexlove.new(props)
|
||||
props = props or {}
|
||||
|
||||
-- If not in immediate mode, use standard Element.new
|
||||
if not flexlove._immediateMode then
|
||||
-- Determine effective mode: props.mode takes precedence over global mode
|
||||
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)
|
||||
end
|
||||
|
||||
-- Element is in immediate mode - proceed with immediate-mode logic
|
||||
-- Auto-begin frame if not manually started (convenience feature)
|
||||
if not flexlove._frameStarted then
|
||||
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,
|
||||
though this hasn't been tested.
|
||||
#### Per-Element Mode Override
|
||||
|
||||
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
|
||||
|
||||
@@ -262,6 +300,9 @@ Common properties for all elements:
|
||||
disabled = false,
|
||||
disableHighlight = false, -- Disable pressed overlay (auto-true for themed elements)
|
||||
|
||||
-- Rendering Mode
|
||||
mode = nil, -- "immediate", "retained", or nil (uses global setting)
|
||||
|
||||
-- Hierarchy
|
||||
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 _themeManager ThemeManager -- Internal: theme manager instance
|
||||
---@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 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)
|
||||
@@ -257,11 +258,22 @@ function Element.new(props)
|
||||
}
|
||||
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.onEvent = props.onEvent
|
||||
|
||||
-- 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)
|
||||
else
|
||||
self.id = props.id or ""
|
||||
@@ -288,7 +300,7 @@ function Element.new(props)
|
||||
onEvent = self.onEvent,
|
||||
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)
|
||||
if state then
|
||||
-- Restore EventHandler state from StateManager (sparse storage - provide defaults)
|
||||
@@ -419,7 +431,7 @@ function Element.new(props)
|
||||
}, textEditorDeps)
|
||||
|
||||
-- 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)
|
||||
if state and state.textEditor then
|
||||
-- Restore from nested textEditor state (saved via saveState())
|
||||
@@ -1659,7 +1671,7 @@ function Element.new(props)
|
||||
self._scrollbarDragOffset = 0
|
||||
|
||||
-- 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)
|
||||
if state and state.scrollManager then
|
||||
-- Restore from nested scrollManager state (saved via saveState())
|
||||
@@ -1688,7 +1700,7 @@ function Element.new(props)
|
||||
end
|
||||
|
||||
-- Register element in z-index tracking for immediate mode
|
||||
if Element._Context._immediateMode then
|
||||
if self._elementMode == "immediate" then
|
||||
Element._Context.registerElement(self)
|
||||
end
|
||||
|
||||
@@ -2413,7 +2425,7 @@ function Element:update(dt)
|
||||
end
|
||||
|
||||
-- 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)
|
||||
if state and state.scrollManager then
|
||||
-- Restore from nested scrollManager state (saved via saveState())
|
||||
@@ -2560,7 +2572,7 @@ function Element:update(dt)
|
||||
self:_syncScrollManagerState()
|
||||
end
|
||||
|
||||
if self._stateId and Element._Context._immediateMode then
|
||||
if self._stateId and self._elementMode == "immediate" then
|
||||
Element._StateManager.updateState(self._stateId, {
|
||||
scrollbarDragging = false,
|
||||
})
|
||||
@@ -2642,7 +2654,7 @@ function Element:update(dt)
|
||||
self._eventHandler:processMouseEvents(self, mx, my, isHovering, isActiveElement)
|
||||
|
||||
-- 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()
|
||||
Element._StateManager.updateState(self._stateId, {
|
||||
_pressed = eventHandlerState._pressed,
|
||||
@@ -2665,7 +2677,7 @@ function Element:update(dt)
|
||||
-- Update theme state via ThemeManager
|
||||
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 pressed = newThemeState == "pressed"
|
||||
local focused = newThemeState == "active" or self._focused
|
||||
|
||||
@@ -33,6 +33,7 @@ local AnimationProps = {}
|
||||
--=====================================--
|
||||
---@class ElementProps
|
||||
---@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 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)
|
||||
|
||||
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