starting mode escape hatch

This commit is contained in:
Michael Freno
2025-12-11 11:37:20 -05:00
parent eaf7383b8f
commit 0bceade7d5
8 changed files with 1420 additions and 16 deletions

View File

@@ -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()

View File

@@ -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
} }

View 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

View File

@@ -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

View File

@@ -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)

View 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))

View 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())

View 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())