hover&unhover events

This commit is contained in:
Michael Freno
2025-12-11 13:06:50 -05:00
parent 0bceade7d5
commit 56c8e744d5
7 changed files with 742 additions and 230 deletions

186
examples/hover_demo.lua Normal file
View File

@@ -0,0 +1,186 @@
-- Example demonstrating hover and unhover events in FlexLöve
-- This shows how to use the new hover/unhover events for interactive UI elements
local FlexLove = require("FlexLove")
function love.load()
FlexLove.init({
baseScale = { width = 1920, height = 1080 },
immediateMode = true,
autoFrameManagement = false,
})
end
-- State to track hover status
local hoverStatus = "Not hovering"
local hoverCount = 0
local unhoverCount = 0
local lastEventTime = 0
function love.update(dt)
FlexLove.beginFrame()
-- Create a container
FlexLove.new({
width = "100vw",
height = "100vh",
backgroundColor = FlexLove.Color.fromHex("#1a1a2e"),
positioning = "flex",
flexDirection = "vertical",
justifyContent = "center",
alignItems = "center",
gap = 30,
})
-- Title
FlexLove.new({
text = "Hover Event Demo",
textSize = "4xl",
textColor = FlexLove.Color.fromHex("#ffffff"),
})
-- Instructions
FlexLove.new({
text = "Move your mouse over the boxes below to see hover events",
textSize = "lg",
textColor = FlexLove.Color.fromHex("#a0a0a0"),
})
-- Status display
FlexLove.new({
text = hoverStatus,
textSize = "xl",
textColor = FlexLove.Color.fromHex("#4ecca3"),
padding = 20,
})
-- Event counters
FlexLove.new({
text = string.format("Hover events: %d | Unhover events: %d", hoverCount, unhoverCount),
textSize = "md",
textColor = FlexLove.Color.fromHex("#ffffff"),
})
-- Container for hover boxes
local boxContainer = FlexLove.new({
positioning = "flex",
flexDirection = "horizontal",
gap = 30,
})
-- Hover Box 1
FlexLove.new({
parent = boxContainer,
width = 200,
height = 200,
backgroundColor = FlexLove.Color.fromHex("#e94560"),
cornerRadius = 15,
positioning = "flex",
justifyContent = "center",
alignItems = "center",
text = "Hover Me!",
textSize = "xl",
textColor = FlexLove.Color.fromHex("#ffffff"),
onEvent = function(element, event)
if event.type == "hover" then
hoverStatus = "Hovering over RED box!"
hoverCount = hoverCount + 1
lastEventTime = love.timer.getTime()
elseif event.type == "unhover" then
hoverStatus = "Left RED box"
unhoverCount = unhoverCount + 1
lastEventTime = love.timer.getTime()
elseif event.type == "click" then
print("Clicked RED box!")
end
end,
})
-- Hover Box 2
FlexLove.new({
parent = boxContainer,
width = 200,
height = 200,
backgroundColor = FlexLove.Color.fromHex("#4ecca3"),
cornerRadius = 15,
positioning = "flex",
justifyContent = "center",
alignItems = "center",
text = "Hover Me!",
textSize = "xl",
textColor = FlexLove.Color.fromHex("#1a1a2e"),
onEvent = function(element, event)
if event.type == "hover" then
hoverStatus = "Hovering over GREEN box!"
hoverCount = hoverCount + 1
lastEventTime = love.timer.getTime()
elseif event.type == "unhover" then
hoverStatus = "Left GREEN box"
unhoverCount = unhoverCount + 1
lastEventTime = love.timer.getTime()
elseif event.type == "click" then
print("Clicked GREEN box!")
end
end,
})
-- Hover Box 3
FlexLove.new({
parent = boxContainer,
width = 200,
height = 200,
backgroundColor = FlexLove.Color.fromHex("#0f3460"),
cornerRadius = 15,
positioning = "flex",
justifyContent = "center",
alignItems = "center",
text = "Hover Me!",
textSize = "xl",
textColor = FlexLove.Color.fromHex("#ffffff"),
onEvent = function(element, event)
if event.type == "hover" then
hoverStatus = "Hovering over BLUE box!"
hoverCount = hoverCount + 1
lastEventTime = love.timer.getTime()
elseif event.type == "unhover" then
hoverStatus = "Left BLUE box"
unhoverCount = unhoverCount + 1
lastEventTime = love.timer.getTime()
elseif event.type == "click" then
print("Clicked BLUE box!")
end
end,
})
-- Reset button
FlexLove.new({
width = 200,
height = 50,
backgroundColor = FlexLove.Color.fromHex("#e94560"),
cornerRadius = 25,
positioning = "flex",
justifyContent = "center",
alignItems = "center",
text = "Reset Counters",
textSize = "md",
textColor = FlexLove.Color.fromHex("#ffffff"),
margin = { top = 30 },
onEvent = function(element, event)
if event.type == "click" then
hoverCount = 0
unhoverCount = 0
hoverStatus = "Counters reset!"
end
end,
})
FlexLove.endFrame()
end
function love.draw()
FlexLove.draw()
end
function love.resize(w, h)
FlexLove.resize(w, h)
end

View File

@@ -152,12 +152,65 @@ function EventHandler:processMouseEvents(element, mx, my, isHovering, isActiveEl
end end
end end
end end
-- Track hover state changes even when events can't be processed
-- Fire unhover event if we were hovering and now we're not
if self._hovered and not isHovering then
self._hovered = false
-- Fire unhover event if handler exists
if self.onEvent then
local modifiers = EventHandler._utils.getModifiers()
local unhoverEvent = EventHandler._InputEvent.new({
type = "unhover",
button = 0,
x = mx,
y = my,
modifiers = modifiers,
clickCount = 0,
})
self:_invokeCallback(element, unhoverEvent)
end
end
if EventHandler._Performance and EventHandler._Performance.enabled then if EventHandler._Performance and EventHandler._Performance.enabled then
EventHandler._Performance:stopTimer("event_mouse") EventHandler._Performance:stopTimer("event_mouse")
end end
return return
end end
-- Track hover state changes and fire hover/unhover events BEFORE button processing
-- This ensures hover fires before press when mouse first enters element
local wasHovered = self._hovered
local isHoveringAndActive = isHovering and isActiveElement
if isHoveringAndActive and not wasHovered then
-- Just started hovering - fire hover event
self._hovered = true
local modifiers = EventHandler._utils.getModifiers()
local hoverEvent = EventHandler._InputEvent.new({
type = "hover",
button = 0,
x = mx,
y = my,
modifiers = modifiers,
clickCount = 0,
})
self:_invokeCallback(element, hoverEvent)
elseif not isHoveringAndActive and wasHovered then
-- Just stopped hovering - fire unhover event
self._hovered = false
local modifiers = EventHandler._utils.getModifiers()
local unhoverEvent = EventHandler._InputEvent.new({
type = "unhover",
button = 0,
x = mx,
y = my,
modifiers = modifiers,
clickCount = 0,
})
self:_invokeCallback(element, unhoverEvent)
end
-- Process all three mouse buttons -- Process all three mouse buttons
local buttons = { 1, 2, 3 } -- left, right, middle local buttons = { 1, 2, 3 } -- left, right, middle

View File

@@ -1,5 +1,5 @@
---@class InputEvent ---@class InputEvent
---@field type "click"|"press"|"release"|"rightclick"|"middleclick"|"drag"|"touchpress"|"touchmove"|"touchrelease"|"touchcancel" ---@field type "click"|"press"|"release"|"rightclick"|"middleclick"|"drag"|"hover"|"unhover"|"touchpress"|"touchmove"|"touchrelease"|"touchcancel"
---@field button number -- Mouse button: 1 (left), 2 (right), 3 (middle) ---@field button number -- Mouse button: 1 (left), 2 (right), 3 (middle)
---@field x number -- Mouse/Touch X position ---@field x number -- Mouse/Touch X position
---@field y number -- Mouse/Touch Y position ---@field y number -- Mouse/Touch Y position
@@ -15,7 +15,7 @@ local InputEvent = {}
InputEvent.__index = InputEvent InputEvent.__index = InputEvent
---@class InputEventProps ---@class InputEventProps
---@field type "click"|"press"|"release"|"rightclick"|"middleclick"|"drag"|"touchpress"|"touchmove"|"touchrelease"|"touchcancel" ---@field type "click"|"press"|"release"|"rightclick"|"middleclick"|"drag"|"hover"|"unhover"|"touchpress"|"touchmove"|"touchrelease"|"touchcancel"
---@field button number ---@field button number
---@field x number ---@field x number
---@field y number ---@field y number
@@ -76,7 +76,7 @@ function InputEvent.fromTouch(id, x, y, phase, pressure)
y = y, y = y,
dx = 0, dx = 0,
dy = 0, dy = 0,
modifiers = {shift = false, ctrl = false, alt = false, super = false}, modifiers = { shift = false, ctrl = false, alt = false, super = false },
clickCount = 1, clickCount = 1,
timestamp = love.timer.getTime(), timestamp = love.timer.getTime(),
touchId = touchIdStr, touchId = touchIdStr,

View File

@@ -1,24 +1,19 @@
-- Profiling test comparing retained mode flag vs. default behavior in complex UI -- Settings Menu Mode Comparison Profile
-- This simulates creating a settings menu multiple times per frame to stress test -- Compares performance between explicit mode="retained" flags vs implicit retained mode
-- the performance difference between explicit mode="retained" and implicit retained mode
package.path = package.path .. ";../../?.lua;../../modules/?.lua"
local FlexLove = require("FlexLove") local FlexLove = require("FlexLove")
local Color = require("modules.Color")
-- Mock resolution sets (simplified) local profile = {
local resolution_sets = { name = "Settings Menu Mode Comparison",
["16:9"] = { description = "Tests whether explicit mode='retained' has performance overhead",
{ 1920, 1080 }, testPhase = "warmup", -- warmup, implicit, explicit, complete
{ 1600, 900 }, frameCount = 0,
{ 1280, 720 }, framesPerPhase = 300, -- 5 seconds at 60 FPS
}, results = {
["16:10"] = { implicit = { startMem = 0, endMem = 0, avgFrameTime = 0, frameTimes = {} },
{ 1920, 1200 }, explicit = { startMem = 0, endMem = 0, avgFrameTime = 0, frameTimes = {} },
{ 1680, 1050 },
{ 1280, 800 },
}, },
currentFrameStart = nil,
} }
-- Mock Settings object -- Mock Settings object
@@ -28,48 +23,32 @@ local Settings = {
fullscreen = false, fullscreen = false,
vsync = true, vsync = true,
msaa = 4, msaa = 4,
resizable = true,
borderless = false,
masterVolume = 0.8, masterVolume = 0.8,
musicVolume = 0.7,
sfxVolume = 0.9,
crtEffectStrength = 0.3,
}, },
get = function(self, key) get = function(self, key)
return self.values[key] return self.values[key]
end, 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 -- Simplified SettingsMenu implementation
local function create_settings_menu_with_mode_flag(use_mode_flag) local function createSettingsMenu(useExplicitMode)
local GuiZIndexing = { MainMenuOverlay = 100 } local GuiZIndexing = { MainMenuOverlay = 100 }
-- Backdrop -- Backdrop
local backdrop_props = { local backdropProps = {
z = GuiZIndexing.MainMenuOverlay - 1, z = GuiZIndexing.MainMenuOverlay - 1,
width = "100%", width = "100%",
height = "100%", height = "100%",
backdropBlur = { radius = 10 }, backdropBlur = { radius = 10 },
backgroundColor = Color.new(1, 1, 1, 0.1), backgroundColor = FlexLove.Color.new(1, 1, 1, 0.1),
} }
if use_mode_flag then if useExplicitMode then
backdrop_props.mode = "retained" backdropProps.mode = "retained"
end end
local backdrop = FlexLove.new(backdrop_props) local backdrop = FlexLove.new(backdropProps)
-- Main window -- Main window
local window_props = { local windowProps = {
z = GuiZIndexing.MainMenuOverlay, z = GuiZIndexing.MainMenuOverlay,
x = "5%", x = "5%",
y = "5%", y = "5%",
@@ -85,10 +64,10 @@ local function create_settings_menu_with_mode_flag(use_mode_flag)
padding = { horizontal = "5%", vertical = "3%" }, padding = { horizontal = "5%", vertical = "3%" },
gap = 10, gap = 10,
} }
if use_mode_flag then if useExplicitMode then
window_props.mode = "retained" windowProps.mode = "retained"
end end
local window = FlexLove.new(window_props) local window = FlexLove.new(windowProps)
-- Close button -- Close button
FlexLove.new({ FlexLove.new({
@@ -131,7 +110,7 @@ local function create_settings_menu_with_mode_flag(use_mode_flag)
textAlign = "start", textAlign = "start",
textSize = "xl", textSize = "xl",
width = "100%", width = "100%",
textColor = Color.new(0.8, 0.9, 1, 1), textColor = FlexLove.Color.new(0.8, 0.9, 1, 1),
}) })
-- Resolution control -- Resolution control
@@ -246,7 +225,7 @@ local function create_settings_menu_with_mode_flag(use_mode_flag)
width = "30%", width = "30%",
}) })
local button_container = FlexLove.new({ local buttonContainer = FlexLove.new({
parent = row4, parent = row4,
width = "60%", width = "60%",
height = "100%", height = "100%",
@@ -255,19 +234,19 @@ local function create_settings_menu_with_mode_flag(use_mode_flag)
gap = 5, gap = 5,
}) })
local msaa_values = { 0, 1, 2, 4, 8, 16 } local msaaValues = { 0, 1, 2, 4, 8, 16 }
for _, msaa_val in ipairs(msaa_values) do for _, msaaVal in ipairs(msaaValues) do
local is_selected = Settings:get("msaa") == msaa_val local isSelected = Settings:get("msaa") == msaaVal
FlexLove.new({ FlexLove.new({
parent = button_container, parent = buttonContainer,
themeComponent = is_selected and "buttonv1" or "buttonv2", themeComponent = isSelected and "buttonv1" or "buttonv2",
text = tostring(msaa_val), text = tostring(msaaVal),
textAlign = "center", textAlign = "center",
width = "8vw", width = "8vw",
height = "100%", height = "100%",
textSize = "sm", textSize = "sm",
disabled = is_selected, disabled = isSelected,
opacity = is_selected and 0.7 or 1.0, opacity = isSelected and 0.7 or 1.0,
}) })
end end
@@ -278,7 +257,7 @@ local function create_settings_menu_with_mode_flag(use_mode_flag)
textAlign = "start", textAlign = "start",
textSize = "xl", textSize = "xl",
width = "100%", width = "100%",
textColor = Color.new(0.8, 0.9, 1, 1), textColor = FlexLove.Color.new(0.8, 0.9, 1, 1),
}) })
-- Master volume slider -- Master volume slider
@@ -301,7 +280,7 @@ local function create_settings_menu_with_mode_flag(use_mode_flag)
width = "30%", width = "30%",
}) })
local slider_container = FlexLove.new({ local sliderContainer = FlexLove.new({
parent = row5, parent = row5,
width = "50%", width = "50%",
height = "100%", height = "100%",
@@ -314,8 +293,8 @@ local function create_settings_menu_with_mode_flag(use_mode_flag)
local value = Settings:get("masterVolume") local value = Settings:get("masterVolume")
local normalized = value local normalized = value
local slider_track = FlexLove.new({ local sliderTrack = FlexLove.new({
parent = slider_container, parent = sliderContainer,
width = "80%", width = "80%",
height = "75%", height = "75%",
positioning = "flex", positioning = "flex",
@@ -324,7 +303,7 @@ local function create_settings_menu_with_mode_flag(use_mode_flag)
}) })
FlexLove.new({ FlexLove.new({
parent = slider_track, parent = sliderTrack,
width = (normalized * 100) .. "%", width = (normalized * 100) .. "%",
height = "100%", height = "100%",
themeComponent = "buttonv1", themeComponent = "buttonv1",
@@ -332,7 +311,7 @@ local function create_settings_menu_with_mode_flag(use_mode_flag)
}) })
FlexLove.new({ FlexLove.new({
parent = slider_container, parent = sliderContainer,
text = string.format("%d", value * 100), text = string.format("%d", value * 100),
textAlign = "center", textAlign = "center",
textSize = "md", textSize = "md",
@@ -340,7 +319,7 @@ local function create_settings_menu_with_mode_flag(use_mode_flag)
}) })
-- Meta controls (bottom buttons) -- Meta controls (bottom buttons)
local meta_container = FlexLove.new({ local metaContainer = FlexLove.new({
parent = window, parent = window,
positioning = "absolute", positioning = "absolute",
width = "100%", width = "100%",
@@ -349,8 +328,8 @@ local function create_settings_menu_with_mode_flag(use_mode_flag)
x = "0%", x = "0%",
}) })
local button_bar = FlexLove.new({ local buttonBar = FlexLove.new({
parent = meta_container, parent = metaContainer,
width = "100%", width = "100%",
positioning = "flex", positioning = "flex",
flexDirection = "horizontal", flexDirection = "horizontal",
@@ -360,7 +339,7 @@ local function create_settings_menu_with_mode_flag(use_mode_flag)
}) })
FlexLove.new({ FlexLove.new({
parent = button_bar, parent = buttonBar,
themeComponent = "buttonv2", themeComponent = "buttonv2",
text = "Reset", text = "Reset",
textAlign = "center", textAlign = "center",
@@ -372,136 +351,365 @@ local function create_settings_menu_with_mode_flag(use_mode_flag)
return { backdrop = backdrop, window = window } return { backdrop = backdrop, window = window }
end end
-- Profile configuration function profile.init()
local PROFILE_NAME = "Settings Menu Mode Comparison" print("\n=== Settings Menu Mode Comparison Profile ===\n")
local ITERATIONS_PER_TEST = 100 -- Create the menu 100 times to measure difference print("Testing whether explicit mode='retained' has performance overhead")
print("compared to implicit retained mode (global setting).\n")
print("=" .. string.rep("=", 78)) FlexLove.init({
print(string.format(" %s", PROFILE_NAME)) width = love.graphics.getWidth(),
print("=" .. string.rep("=", 78)) height = love.graphics.getHeight(),
print() immediateMode = false, -- Global retained mode
print("This profile compares performance when creating a complex settings menu") theme = "space",
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 profile.testPhase = "warmup"
print("Warming up...") profile.frameCount = 0
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("Phase 1: Warmup (30 frames)...")
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 end
local time_after_implicit = os.clock() function profile.update(dt)
collectgarbage("collect") if profile.testPhase == "complete" then
local mem_after_implicit = collectgarbage("count") return
end
local time_implicit = time_after_implicit - time_before_implicit -- Track frame time
local mem_implicit = mem_after_implicit - mem_before_implicit local frameStart = love.timer.getTime()
print(string.format(" Time: %.4f seconds", time_implicit)) if profile.testPhase == "warmup" then
print(string.format(" Memory: %.2f KB", mem_implicit)) -- Warmup phase - create menu a few times
print(string.format(" Avg time per menu: %.4f ms", (time_implicit / ITERATIONS_PER_TEST) * 1000)) createSettingsMenu(false)
print() profile.frameCount = profile.frameCount + 1
-- Test 2: With explicit mode="retained" flags if profile.frameCount >= 30 then
print("Running Test 2: With explicit mode='retained' flags...") print(" Warmup complete.\n")
FlexLove.init({ immediateMode = false, theme = "space" }) print("Phase 2: Testing WITHOUT explicit mode flags (" .. profile.framesPerPhase .. " frames)...")
collectgarbage("collect") profile.testPhase = "implicit"
local mem_before_explicit = collectgarbage("count") profile.frameCount = 0
local time_before_explicit = os.clock() collectgarbage("collect")
collectgarbage("collect")
profile.results.implicit.startMem = collectgarbage("count")
end
for i = 1, ITERATIONS_PER_TEST do elseif profile.testPhase == "implicit" then
local menu = create_settings_menu_with_mode_flag(true) -- Test implicit mode (no explicit mode flags)
createSettingsMenu(false)
local frameTime = (love.timer.getTime() - frameStart) * 1000
table.insert(profile.results.implicit.frameTimes, frameTime)
profile.frameCount = profile.frameCount + 1
if profile.frameCount >= profile.framesPerPhase then
collectgarbage("collect")
collectgarbage("collect")
profile.results.implicit.endMem = collectgarbage("count")
-- Calculate average
local sum = 0
for _, ft in ipairs(profile.results.implicit.frameTimes) do
sum = sum + ft
end
profile.results.implicit.avgFrameTime = sum / #profile.results.implicit.frameTimes
print(" Complete. Avg frame time: " .. string.format("%.4f", profile.results.implicit.avgFrameTime) .. "ms\n")
print("Phase 3: Testing WITH explicit mode='retained' flags (" .. profile.framesPerPhase .. " frames)...")
profile.testPhase = "explicit"
profile.frameCount = 0
collectgarbage("collect")
collectgarbage("collect")
profile.results.explicit.startMem = collectgarbage("count")
end
elseif profile.testPhase == "explicit" then
-- Test explicit mode (with mode="retained" flags)
createSettingsMenu(true)
local frameTime = (love.timer.getTime() - frameStart) * 1000
table.insert(profile.results.explicit.frameTimes, frameTime)
profile.frameCount = profile.frameCount + 1
if profile.frameCount >= profile.framesPerPhase then
collectgarbage("collect")
collectgarbage("collect")
profile.results.explicit.endMem = collectgarbage("count")
-- Calculate average
local sum = 0
for _, ft in ipairs(profile.results.explicit.frameTimes) do
sum = sum + ft
end
profile.results.explicit.avgFrameTime = sum / #profile.results.explicit.frameTimes
print(" Complete. Avg frame time: " .. string.format("%.4f", profile.results.explicit.avgFrameTime) .. "ms\n")
profile.testPhase = "complete"
profile.generateReport()
end
end
end end
local time_after_explicit = os.clock() function profile.draw()
collectgarbage("collect") -- Draw the current menu
local mem_after_explicit = collectgarbage("count") if profile.testPhase ~= "complete" then
FlexLove.draw()
end
local time_explicit = time_after_explicit - time_before_explicit -- Draw status overlay
local mem_explicit = mem_after_explicit - mem_before_explicit love.graphics.setColor(0, 0, 0, 0.85)
love.graphics.rectangle("fill", 10, 10, 400, 120)
print(string.format(" Time: %.4f seconds", time_explicit)) love.graphics.setColor(1, 1, 1, 1)
print(string.format(" Memory: %.2f KB", mem_explicit)) love.graphics.print("Settings Menu Mode Comparison", 20, 20)
print(string.format(" Avg time per menu: %.4f ms", (time_explicit / ITERATIONS_PER_TEST) * 1000))
print()
-- Calculate differences if profile.testPhase == "warmup" then
print("=" .. string.rep("=", 78)) love.graphics.print("Phase: Warmup (" .. profile.frameCount .. "/30)", 20, 45)
print("RESULTS COMPARISON") elseif profile.testPhase == "implicit" then
print("=" .. string.rep("=", 78)) love.graphics.print("Phase: Without mode flags", 20, 45)
print() love.graphics.print("Progress: " .. profile.frameCount .. "/" .. profile.framesPerPhase, 20, 65)
elseif profile.testPhase == "explicit" then
love.graphics.print("Phase: With mode='retained' flags", 20, 45)
love.graphics.print("Progress: " .. profile.frameCount .. "/" .. profile.framesPerPhase, 20, 65)
elseif profile.testPhase == "complete" then
love.graphics.print("Phase: COMPLETE", 20, 45)
love.graphics.print("Report saved! Press S to save again, ESC to exit.", 20, 65)
end
local time_diff = time_explicit - time_implicit love.graphics.print("Memory: " .. string.format("%.2f", collectgarbage("count") / 1024) .. " MB", 20, 85)
local time_percent = (time_diff / time_implicit) * 100 love.graphics.print("Press ESC to return to menu", 20, 105)
local mem_diff = mem_explicit - mem_implicit end
print(string.format("Time Difference:")) function profile.generateReport()
print(string.format(" Without mode flag: %.4f seconds", time_implicit)) print("\n" .. string.rep("=", 80))
print(string.format(" With mode flag: %.4f seconds", time_explicit)) print("RESULTS COMPARISON")
print(string.format(" Difference: %.4f seconds (%+.2f%%)", time_diff, time_percent)) print(string.rep("=", 80) .. "\n")
print()
print(string.format("Memory Difference:")) local timeDiff = profile.results.explicit.avgFrameTime - profile.results.implicit.avgFrameTime
print(string.format(" Without mode flag: %.2f KB", mem_implicit)) local timePercent = (timeDiff / profile.results.implicit.avgFrameTime) * 100
print(string.format(" With mode flag: %.2f KB", mem_explicit)) local memDiff = (profile.results.explicit.endMem - profile.results.explicit.startMem) -
print(string.format(" Difference: %+.2f KB", mem_diff)) (profile.results.implicit.endMem - profile.results.implicit.startMem)
print()
-- Interpretation print("Time Comparison:")
print("INTERPRETATION:") print(string.format(" Without mode flag: %.4f ms", profile.results.implicit.avgFrameTime))
print() print(string.format(" With mode flag: %.4f ms", profile.results.explicit.avgFrameTime))
if math.abs(time_percent) < 5 then print(string.format(" Difference: %.4f ms (%+.2f%%)", timeDiff, timePercent))
print()
print("Memory Comparison:")
print(string.format(" Without mode flag: %.2f KB", profile.results.implicit.endMem - profile.results.implicit.startMem))
print(string.format(" With mode flag: %.2f KB", profile.results.explicit.endMem - profile.results.explicit.startMem))
print(string.format(" Difference: %+.2f KB", memDiff))
print()
print("INTERPRETATION:")
print()
if math.abs(timePercent) < 5 then
print(" ✓ Performance is essentially identical (< 5% difference)") print(" ✓ Performance is essentially identical (< 5% difference)")
print(" The explicit mode flag has negligible impact on performance.") print(" The explicit mode flag has negligible impact on performance.")
elseif time_percent > 0 then elseif timePercent > 0 then
print(string.format(" ⚠ Explicit mode flag is %.2f%% SLOWER", time_percent)) print(string.format(" ⚠ Explicit mode flag is %.2f%% SLOWER", timePercent))
print(" This indicates overhead from mode checking/resolution.") print(" This indicates overhead from mode checking/resolution.")
else else
print(string.format(" ✓ Explicit mode flag is %.2f%% FASTER", -time_percent)) print(string.format(" ✓ Explicit mode flag is %.2f%% FASTER", -timePercent))
print(" This indicates potential optimization benefits.") print(" This indicates potential optimization benefits.")
end end
print() print()
if math.abs(mem_diff) < 50 then if math.abs(memDiff) < 50 then
print(" ✓ Memory usage is essentially identical (< 50 KB difference)") print(" ✓ Memory usage is essentially identical (< 50 KB difference)")
elseif mem_diff > 0 then elseif memDiff > 0 then
print(string.format(" ⚠ Explicit mode flag uses %.2f KB MORE memory", mem_diff)) print(string.format(" ⚠ Explicit mode flag uses %.2f KB MORE memory", memDiff))
else else
print(string.format(" ✓ Explicit mode flag uses %.2f KB LESS memory", -mem_diff)) print(string.format(" ✓ Explicit mode flag uses %.2f KB LESS memory", -memDiff))
end end
print() print()
print("RECOMMENDATION:") print("RECOMMENDATION:")
print() print()
if math.abs(time_percent) < 5 and math.abs(mem_diff) < 50 then if math.abs(timePercent) < 5 and math.abs(memDiff) < 50 then
print(" The explicit mode='retained' flag provides clarity and explicitness") print(" The explicit mode='retained' flag provides clarity and explicitness")
print(" without any meaningful performance cost. It's recommended for:") print(" without any meaningful performance cost. It's recommended for:")
print(" - Code readability (makes intent explicit)") print(" - Code readability (makes intent explicit)")
print(" - Future-proofing (if global mode changes)") print(" - Future-proofing (if global mode changes)")
print(" - Mixed-mode UIs (where some elements are immediate)") print(" - Mixed-mode UIs (where some elements are immediate)")
else else
print(" Consider the trade-offs based on your specific use case.") print(" Consider the trade-offs based on your specific use case.")
end end
print() print()
print(string.rep("=", 80))
print("Profile complete! Press S to save report.")
print(string.rep("=", 80) .. "\n")
print("=" .. string.rep("=", 78)) -- Save report automatically
print("Profile complete!") profile.saveReportToFile()
print("=" .. string.rep("=", 78)) end
function profile.saveReportToFile()
local timestamp = os.date("%Y-%m-%d_%H-%M-%S")
local filename = string.format("reports/settings_menu_mode_profile/%s.md", timestamp)
-- Get the actual project directory
local sourceDir = love.filesystem.getSource()
local filepath
if sourceDir:match("%.love$") then
-- Running from .love file, use save directory
love.filesystem.createDirectory("reports/settings_menu_mode_profile")
-- Use love.filesystem for sandboxed writes
local content = profile.formatReportMarkdown()
local success = love.filesystem.write(filename, content)
if success then
print("\n✓ Report saved to: " .. love.filesystem.getSaveDirectory() .. "/" .. filename)
else
print("\n✗ Failed to save report")
end
else
-- Running from source, use io module
filepath = sourceDir .. "/" .. filename
os.execute('mkdir -p "' .. sourceDir .. '/reports/settings_menu_mode_profile"')
local file, err = io.open(filepath, "w")
if file then
file:write(profile.formatReportMarkdown())
file:close()
print("\n✓ Report saved to: " .. filepath)
-- Also save as latest.md
local latestPath = sourceDir .. "/reports/settings_menu_mode_profile/latest.md"
local latestFile = io.open(latestPath, "w")
if latestFile then
latestFile:write(profile.formatReportMarkdown())
latestFile:close()
end
else
print("\n✗ Failed to save report: " .. tostring(err))
end
end
end
function profile.formatReportMarkdown()
local lines = {}
table.insert(lines, "# Settings Menu Mode Comparison Report")
table.insert(lines, "")
table.insert(lines, "**Generated:** " .. os.date("%Y-%m-%d %H:%M:%S"))
table.insert(lines, "")
table.insert(lines, "This profile compares performance when creating a complex settings menu")
table.insert(lines, "with explicit `mode='retained'` flags vs. implicit retained mode (global setting).")
table.insert(lines, "")
table.insert(lines, "---")
table.insert(lines, "")
table.insert(lines, "## Test Configuration")
table.insert(lines, "")
table.insert(lines, "- **Frames per test:** " .. profile.framesPerPhase)
table.insert(lines, "- **Elements per menu:** ~45 (backdrop, window, buttons, sliders, etc.)")
table.insert(lines, "- **Global mode:** `immediateMode = false` (retained)")
table.insert(lines, "")
table.insert(lines, "## Results")
table.insert(lines, "")
local timeDiff = profile.results.explicit.avgFrameTime - profile.results.implicit.avgFrameTime
local timePercent = (timeDiff / profile.results.implicit.avgFrameTime) * 100
local memDiff = (profile.results.explicit.endMem - profile.results.explicit.startMem) -
(profile.results.implicit.endMem - profile.results.implicit.startMem)
table.insert(lines, "### Frame Time Comparison")
table.insert(lines, "")
table.insert(lines, "| Metric | Without `mode` flag | With `mode='retained'` flag | Difference |")
table.insert(lines, "|--------|--------------------:|----------------------------:|-----------:|")
table.insert(lines, string.format("| Average Frame Time | %.4f ms | %.4f ms | %+.4f ms (%+.2f%%) |",
profile.results.implicit.avgFrameTime,
profile.results.explicit.avgFrameTime,
timeDiff,
timePercent))
table.insert(lines, "")
table.insert(lines, "### Memory Comparison")
table.insert(lines, "")
table.insert(lines, "| Metric | Without `mode` flag | With `mode='retained'` flag | Difference |")
table.insert(lines, "|--------|--------------------:|----------------------------:|-----------:|")
table.insert(lines, string.format("| Memory Used | %.2f KB | %.2f KB | %+.2f KB |",
profile.results.implicit.endMem - profile.results.implicit.startMem,
profile.results.explicit.endMem - profile.results.explicit.startMem,
memDiff))
table.insert(lines, "")
table.insert(lines, "## Interpretation")
table.insert(lines, "")
if math.abs(timePercent) < 5 then
table.insert(lines, "✓ **Performance is essentially identical** (< 5% difference)")
table.insert(lines, "")
table.insert(lines, "The explicit `mode='retained'` flag has negligible impact on performance.")
elseif timePercent > 0 then
table.insert(lines, string.format("⚠ **Explicit mode flag is %.2f%% SLOWER**", timePercent))
table.insert(lines, "")
table.insert(lines, "This indicates overhead from mode checking/resolution.")
else
table.insert(lines, string.format("✓ **Explicit mode flag is %.2f%% FASTER**", -timePercent))
table.insert(lines, "")
table.insert(lines, "This indicates potential optimization benefits.")
end
table.insert(lines, "")
if math.abs(memDiff) < 50 then
table.insert(lines, "✓ **Memory usage is essentially identical** (< 50 KB difference)")
elseif memDiff > 0 then
table.insert(lines, string.format("⚠ **Explicit mode flag uses %.2f KB MORE memory**", memDiff))
else
table.insert(lines, string.format("✓ **Explicit mode flag uses %.2f KB LESS memory**", -memDiff))
end
table.insert(lines, "")
table.insert(lines, "## Recommendation")
table.insert(lines, "")
if math.abs(timePercent) < 5 and math.abs(memDiff) < 50 then
table.insert(lines, "The explicit `mode='retained'` flag provides clarity and explicitness")
table.insert(lines, "without any meaningful performance cost. It's recommended for:")
table.insert(lines, "")
table.insert(lines, "- **Code readability** - Makes intent explicit")
table.insert(lines, "- **Future-proofing** - If global mode changes")
table.insert(lines, "- **Mixed-mode UIs** - Where some elements are immediate")
else
table.insert(lines, "Consider the trade-offs based on your specific use case.")
end
table.insert(lines, "")
table.insert(lines, "---")
table.insert(lines, "")
table.insert(lines, "*Report generated by FlexLöve Performance Profiler*")
return table.concat(lines, "\n")
end
function profile.keypressed(key, profiler)
if key == "s" and profile.testPhase == "complete" then
profile.saveReportToFile()
end
end
function profile.resize(w, h)
FlexLove.resize(w, h)
end
function profile.reset()
profile.testPhase = "warmup"
profile.frameCount = 0
profile.results = {
implicit = { startMem = 0, endMem = 0, avgFrameTime = 0, frameTimes = {} },
explicit = { startMem = 0, endMem = 0, avgFrameTime = 0, frameTimes = {} },
}
print("\nProfile reset. Starting over...\n")
end
function profile.cleanup()
print("\nCleaning up settings menu mode profile...\n")
end
return profile

View File

@@ -1,8 +1,26 @@
-- FlexLöve Profiler - Main Entry Point -- FlexLöve Profiler - Main Entry Point
-- Load FlexLöve from parent directory -- Load FlexLöve from parent directory
package.path = package.path .. ";../?.lua;../?/init.lua"
local FlexLove = require("libs.FlexLove") -- Override require to handle FlexLove.modules.X properly
-- When FlexLove.lua requires "FlexLove.modules.ErrorHandler",
-- we redirect it to "../modules/ErrorHandler"
local originalRequire = require
local function customRequire(modname)
-- Check if this is a FlexLove.modules.X require
local moduleName = modname:match("^FlexLove%.modules%.(.+)$")
if moduleName then
-- Redirect to ../modules/X
return originalRequire("modules." .. moduleName)
end
-- Otherwise use original require
return originalRequire(modname)
end
_G.require = customRequire
-- Set up package.path for normal requires
package.path = package.path .. ";../?.lua;../?/init.lua;../modules/?.lua"
local FlexLove = originalRequire("FlexLove")
local PerformanceProfiler = require("profiling.utils.PerformanceProfiler") local PerformanceProfiler = require("profiling.utils.PerformanceProfiler")
local lv = love local lv = love
@@ -118,17 +136,9 @@ local function buildMenu()
padding = { horizontal = 40, vertical = 40 }, padding = { horizontal = 40, vertical = 40 },
}) })
local container = FlexLove.new({
parent = root,
positioning = "flex",
flexDirection = "vertical",
alignItems = "center",
gap = 30,
})
-- Title -- Title
FlexLove.new({ FlexLove.new({
parent = container, parent = root,
width = 600, width = 600,
height = 80, height = 80,
backgroundColor = FlexLove.Color.new(0.15, 0.15, 0.25, 1), backgroundColor = FlexLove.Color.new(0.15, 0.15, 0.25, 1),
@@ -141,6 +151,16 @@ local function buildMenu()
textColor = FlexLove.Color.new(0.3, 0.8, 1, 1), textColor = FlexLove.Color.new(0.3, 0.8, 1, 1),
}) })
local container = FlexLove.new({
parent = root,
positioning = "flex",
flexDirection = "vertical",
alignItems = "center",
height = "100%",
width = "100%",
gap = 30,
})
-- Subtitle -- Subtitle
FlexLove.new({ FlexLove.new({
parent = container, parent = container,
@@ -152,32 +172,43 @@ local function buildMenu()
-- Profile list -- Profile list
local profileList = FlexLove.new({ local profileList = FlexLove.new({
parent = container, parent = container,
width = 600, width = "80%",
height = "80%",
positioning = "flex", positioning = "flex",
flexDirection = "vertical", flexDirection = "vertical",
gap = 10, gap = 10,
padding = { vertical = 20, horizontal = 20 },
overflowY = "scroll",
--mode = "retained",
}) })
for i, profile in ipairs(state.profiles) do for i, profile in ipairs(state.profiles) do
local isSelected = i == state.selectedIndex local isSelected = i == state.selectedIndex
local isHovered = i == state.hoveredIndex
local button = FlexLove.new({ local button = FlexLove.new({
parent = profileList, parent = profileList,
width = "100%", width = "50%",
height = 50, height = 50,
backgroundColor = isSelected and FlexLove.Color.new(0.2, 0.4, 0.8, 1) or FlexLove.Color.new(0.15, 0.15, 0.25, 1), backgroundColor = isSelected and FlexLove.Color.new(0.2, 0.4, 0.8, 1)
or isHovered and FlexLove.Color.new(0.2, 0.2, 0.35, 1)
or FlexLove.Color.new(0.15, 0.15, 0.25, 1),
borderRadius = 8, borderRadius = 8,
positioning = "flex", positioning = "flex",
justifyContent = "flex-start", justifyContent = "center",
alignItems = "center", alignItems = "center",
alignSelf = "center",
--mode = "retained",
padding = { horizontal = 15, vertical = 15 }, padding = { horizontal = 15, vertical = 15 },
onEvent = function(element, event) onEvent = function(_, event)
if event.type == "release" then if event.type == "release" then
state.selectedIndex = i state.selectedIndex = i
loadProfile(profile) loadProfile(profile)
elseif event.type == "hover" and not isSelected then elseif event.type == "hover" and not isSelected then
element.backgroundColor = FlexLove.Color.new(0.2, 0.2, 0.35, 1) state.hoveredIndex = i
--element.backgroundColor = FlexLove.Color.new(0.2, 0.2, 0.35, 1)
elseif event.type == "unhover" and not isSelected then elseif event.type == "unhover" and not isSelected then
element.backgroundColor = FlexLove.Color.new(0.15, 0.15, 0.25, 1) state.hoveredIndex = 0
--element.backgroundColor = FlexLove.Color.new(0.15, 0.15, 0.25, 1)
end end
end, end,
}) })
@@ -223,8 +254,6 @@ end
function lv.load(args) function lv.load(args)
FlexLove.init({ FlexLove.init({
width = lv.graphics.getWidth(),
height = lv.graphics.getHeight(),
immediateMode = true, immediateMode = true,
}) })
@@ -266,6 +295,10 @@ function lv.update(dt)
end end
end end
function lv.wheelmoved(x, y)
FlexLove.wheelmoved(x, y)
end
function lv.draw() function lv.draw()
if state.mode == "menu" then if state.mode == "menu" then
buildMenu() buildMenu()

View File

@@ -97,11 +97,19 @@ function TestTouchEvents:testEventHandler_TouchBegan()
element._eventHandler:processTouchEvents(element) element._eventHandler:processTouchEvents(element)
FlexLove.endFrame() FlexLove.endFrame()
-- Filter out hover/unhover events (from mouse processing)
local filteredEvents = {}
for _, event in ipairs(touchEvents) do
if event.type ~= "hover" and event.type ~= "unhover" then
table.insert(filteredEvents, event)
end
end
-- Should have received at least one touchpress event -- Should have received at least one touchpress event
-- Note: May receive multiple events due to test state/frame processing -- Note: May receive multiple events due to test state/frame processing
lu.assertTrue(#touchEvents >= 1, "Should receive at least 1 touch event, got " .. #touchEvents) lu.assertTrue(#filteredEvents >= 1, "Should receive at least 1 touch event, got " .. #filteredEvents)
lu.assertEquals(touchEvents[1].type, "touchpress") lu.assertEquals(filteredEvents[1].type, "touchpress")
lu.assertEquals(touchEvents[1].touchId, "touch1") lu.assertEquals(filteredEvents[1].touchId, "touch1")
end end
-- Test: EventHandler tracks touch moved -- Test: EventHandler tracks touch moved
@@ -147,12 +155,20 @@ function TestTouchEvents:testEventHandler_TouchMoved()
element._eventHandler:processTouchEvents(element) element._eventHandler:processTouchEvents(element)
FlexLove.endFrame() FlexLove.endFrame()
-- Filter out hover/unhover events (from mouse processing)
local filteredEvents = {}
for _, event in ipairs(touchEvents) do
if event.type ~= "hover" and event.type ~= "unhover" then
table.insert(filteredEvents, event)
end
end
-- Should have received touchpress and touchmove events -- Should have received touchpress and touchmove events
lu.assertEquals(#touchEvents, 2) lu.assertEquals(#filteredEvents, 2)
lu.assertEquals(touchEvents[1].type, "touchpress") lu.assertEquals(filteredEvents[1].type, "touchpress")
lu.assertEquals(touchEvents[2].type, "touchmove") lu.assertEquals(filteredEvents[2].type, "touchmove")
lu.assertEquals(touchEvents[2].dx, 50) lu.assertEquals(filteredEvents[2].dx, 50)
lu.assertEquals(touchEvents[2].dy, 50) lu.assertEquals(filteredEvents[2].dy, 50)
end end
-- Test: EventHandler tracks touch ended -- Test: EventHandler tracks touch ended
@@ -195,10 +211,18 @@ function TestTouchEvents:testEventHandler_TouchEnded()
element._eventHandler:processTouchEvents(element) element._eventHandler:processTouchEvents(element)
FlexLove.endFrame() FlexLove.endFrame()
-- Filter out hover/unhover events (from mouse processing)
local filteredEvents = {}
for _, event in ipairs(touchEvents) do
if event.type ~= "hover" and event.type ~= "unhover" then
table.insert(filteredEvents, event)
end
end
-- Should have received touchpress and touchrelease events -- Should have received touchpress and touchrelease events
lu.assertEquals(#touchEvents, 2) lu.assertEquals(#filteredEvents, 2)
lu.assertEquals(touchEvents[1].type, "touchpress") lu.assertEquals(filteredEvents[1].type, "touchpress")
lu.assertEquals(touchEvents[2].type, "touchrelease") lu.assertEquals(filteredEvents[2].type, "touchrelease")
end end
-- Test: EventHandler tracks multiple simultaneous touches -- Test: EventHandler tracks multiple simultaneous touches
@@ -234,10 +258,18 @@ function TestTouchEvents:testEventHandler_MultiTouch()
element._eventHandler:processTouchEvents(element) element._eventHandler:processTouchEvents(element)
FlexLove.endFrame() FlexLove.endFrame()
-- Should have received two touchpress events -- Filter out hover/unhover events (from mouse processing)
lu.assertEquals(#touchEvents, 2) local filteredEvents = {}
lu.assertEquals(touchEvents[1].type, "touchpress") for _, event in ipairs(touchEvents) do
lu.assertEquals(touchEvents[2].type, "touchpress") if event.type ~= "hover" and event.type ~= "unhover" then
table.insert(filteredEvents, event)
end
end
-- Should have received two touchpress events (one for each touch)
lu.assertEquals(#filteredEvents, 2)
lu.assertEquals(filteredEvents[1].type, "touchpress")
lu.assertEquals(filteredEvents[2].type, "touchpress")
-- Different touch IDs -- Different touch IDs
lu.assertNotEquals(touchEvents[1].touchId, touchEvents[2].touchId) lu.assertNotEquals(touchEvents[1].touchId, touchEvents[2].touchId)