diff --git a/FlexLove.lua b/FlexLove.lua index b722caf..57cfa83 100644 --- a/FlexLove.lua +++ b/FlexLove.lua @@ -440,7 +440,7 @@ function flexlove.endFrame() 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 @@ -451,7 +451,7 @@ function flexlove.endFrame() retainedParentsToLayout[element.parent] = true end end - + -- Layout all retained parents that had immediate children added for parent, _ in pairs(retainedParentsToLayout) do parent:layoutChildren() @@ -466,7 +466,7 @@ function flexlove.endFrame() 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) diff --git a/examples/hover_demo.lua b/examples/hover_demo.lua new file mode 100644 index 0000000..72602c7 --- /dev/null +++ b/examples/hover_demo.lua @@ -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 diff --git a/modules/EventHandler.lua b/modules/EventHandler.lua index f4260a8..242b3cb 100644 --- a/modules/EventHandler.lua +++ b/modules/EventHandler.lua @@ -152,12 +152,65 @@ function EventHandler:processMouseEvents(element, mx, my, isHovering, isActiveEl 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 EventHandler._Performance:stopTimer("event_mouse") end return 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 local buttons = { 1, 2, 3 } -- left, right, middle diff --git a/modules/InputEvent.lua b/modules/InputEvent.lua index 08fd0cd..8f1be53 100644 --- a/modules/InputEvent.lua +++ b/modules/InputEvent.lua @@ -1,5 +1,5 @@ ---@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 x number -- Mouse/Touch X position ---@field y number -- Mouse/Touch Y position @@ -15,7 +15,7 @@ local InputEvent = {} InputEvent.__index = InputEvent ---@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 x number ---@field y number @@ -42,12 +42,12 @@ function InputEvent.new(props) self.modifiers = props.modifiers self.clickCount = props.clickCount or 1 self.timestamp = props.timestamp or love.timer.getTime() - + -- Touch-specific properties self.touchId = props.touchId self.pressure = props.pressure or 1.0 self.phase = props.phase - + return self end @@ -68,7 +68,7 @@ function InputEvent.fromTouch(id, x, y, phase, pressure) elseif phase == "cancelled" then eventType = "touchcancel" end - + return InputEvent.new({ type = eventType, button = 1, -- Treat touch as left button @@ -76,7 +76,7 @@ function InputEvent.fromTouch(id, x, y, phase, pressure) y = y, dx = 0, dy = 0, - modifiers = {shift = false, ctrl = false, alt = false, super = false}, + modifiers = { shift = false, ctrl = false, alt = false, super = false }, clickCount = 1, timestamp = love.timer.getTime(), touchId = touchIdStr, diff --git a/profiling/__profiles__/settings_menu_mode_profile.lua b/profiling/__profiles__/settings_menu_mode_profile.lua index 8dd4514..1561c58 100644 --- a/profiling/__profiles__/settings_menu_mode_profile.lua +++ b/profiling/__profiles__/settings_menu_mode_profile.lua @@ -1,24 +1,19 @@ --- 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" +-- Settings Menu Mode Comparison Profile +-- Compares performance between explicit mode="retained" flags vs implicit retained mode 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 }, +local profile = { + name = "Settings Menu Mode Comparison", + description = "Tests whether explicit mode='retained' has performance overhead", + testPhase = "warmup", -- warmup, implicit, explicit, complete + frameCount = 0, + framesPerPhase = 300, -- 5 seconds at 60 FPS + results = { + implicit = { startMem = 0, endMem = 0, avgFrameTime = 0, frameTimes = {} }, + explicit = { startMem = 0, endMem = 0, avgFrameTime = 0, frameTimes = {} }, }, + currentFrameStart = nil, } -- Mock Settings object @@ -28,48 +23,32 @@ local Settings = { 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 function createSettingsMenu(useExplicitMode) local GuiZIndexing = { MainMenuOverlay = 100 } -- Backdrop - local backdrop_props = { + local backdropProps = { z = GuiZIndexing.MainMenuOverlay - 1, width = "100%", height = "100%", 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 - backdrop_props.mode = "retained" + if useExplicitMode then + backdropProps.mode = "retained" end - local backdrop = FlexLove.new(backdrop_props) + local backdrop = FlexLove.new(backdropProps) -- Main window - local window_props = { + local windowProps = { z = GuiZIndexing.MainMenuOverlay, x = "5%", y = "5%", @@ -85,10 +64,10 @@ local function create_settings_menu_with_mode_flag(use_mode_flag) padding = { horizontal = "5%", vertical = "3%" }, gap = 10, } - if use_mode_flag then - window_props.mode = "retained" + if useExplicitMode then + windowProps.mode = "retained" end - local window = FlexLove.new(window_props) + local window = FlexLove.new(windowProps) -- Close button FlexLove.new({ @@ -131,7 +110,7 @@ local function create_settings_menu_with_mode_flag(use_mode_flag) textAlign = "start", textSize = "xl", width = "100%", - textColor = Color.new(0.8, 0.9, 1, 1), + textColor = FlexLove.Color.new(0.8, 0.9, 1, 1), }) -- Resolution control @@ -246,7 +225,7 @@ local function create_settings_menu_with_mode_flag(use_mode_flag) width = "30%", }) - local button_container = FlexLove.new({ + local buttonContainer = FlexLove.new({ parent = row4, width = "60%", height = "100%", @@ -255,19 +234,19 @@ local function create_settings_menu_with_mode_flag(use_mode_flag) 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 + local msaaValues = { 0, 1, 2, 4, 8, 16 } + for _, msaaVal in ipairs(msaaValues) do + local isSelected = Settings:get("msaa") == msaaVal FlexLove.new({ - parent = button_container, - themeComponent = is_selected and "buttonv1" or "buttonv2", - text = tostring(msaa_val), + parent = buttonContainer, + themeComponent = isSelected and "buttonv1" or "buttonv2", + text = tostring(msaaVal), textAlign = "center", width = "8vw", height = "100%", textSize = "sm", - disabled = is_selected, - opacity = is_selected and 0.7 or 1.0, + disabled = isSelected, + opacity = isSelected and 0.7 or 1.0, }) end @@ -278,7 +257,7 @@ local function create_settings_menu_with_mode_flag(use_mode_flag) textAlign = "start", textSize = "xl", width = "100%", - textColor = Color.new(0.8, 0.9, 1, 1), + textColor = FlexLove.Color.new(0.8, 0.9, 1, 1), }) -- Master volume slider @@ -301,7 +280,7 @@ local function create_settings_menu_with_mode_flag(use_mode_flag) width = "30%", }) - local slider_container = FlexLove.new({ + local sliderContainer = FlexLove.new({ parent = row5, width = "50%", height = "100%", @@ -314,8 +293,8 @@ local function create_settings_menu_with_mode_flag(use_mode_flag) local value = Settings:get("masterVolume") local normalized = value - local slider_track = FlexLove.new({ - parent = slider_container, + local sliderTrack = FlexLove.new({ + parent = sliderContainer, width = "80%", height = "75%", positioning = "flex", @@ -324,7 +303,7 @@ local function create_settings_menu_with_mode_flag(use_mode_flag) }) FlexLove.new({ - parent = slider_track, + parent = sliderTrack, width = (normalized * 100) .. "%", height = "100%", themeComponent = "buttonv1", @@ -332,7 +311,7 @@ local function create_settings_menu_with_mode_flag(use_mode_flag) }) FlexLove.new({ - parent = slider_container, + parent = sliderContainer, text = string.format("%d", value * 100), textAlign = "center", textSize = "md", @@ -340,7 +319,7 @@ local function create_settings_menu_with_mode_flag(use_mode_flag) }) -- Meta controls (bottom buttons) - local meta_container = FlexLove.new({ + local metaContainer = FlexLove.new({ parent = window, positioning = "absolute", width = "100%", @@ -349,8 +328,8 @@ local function create_settings_menu_with_mode_flag(use_mode_flag) x = "0%", }) - local button_bar = FlexLove.new({ - parent = meta_container, + local buttonBar = FlexLove.new({ + parent = metaContainer, width = "100%", positioning = "flex", flexDirection = "horizontal", @@ -360,7 +339,7 @@ local function create_settings_menu_with_mode_flag(use_mode_flag) }) FlexLove.new({ - parent = button_bar, + parent = buttonBar, themeComponent = "buttonv2", text = "Reset", textAlign = "center", @@ -372,136 +351,365 @@ local function create_settings_menu_with_mode_flag(use_mode_flag) 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) +function profile.init() + print("\n=== Settings Menu Mode Comparison Profile ===\n") + print("Testing whether explicit mode='retained' has performance overhead") + print("compared to implicit retained mode (global setting).\n") + + FlexLove.init({ + width = love.graphics.getWidth(), + height = love.graphics.getHeight(), + immediateMode = false, -- Global retained mode + theme = "space", + }) + + profile.testPhase = "warmup" + profile.frameCount = 0 + + print("Phase 1: Warmup (30 frames)...") 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) +function profile.update(dt) + if profile.testPhase == "complete" then + return + end + + -- Track frame time + local frameStart = love.timer.getTime() + + if profile.testPhase == "warmup" then + -- Warmup phase - create menu a few times + createSettingsMenu(false) + profile.frameCount = profile.frameCount + 1 + + if profile.frameCount >= 30 then + print(" Warmup complete.\n") + print("Phase 2: Testing WITHOUT explicit mode flags (" .. profile.framesPerPhase .. " frames)...") + profile.testPhase = "implicit" + profile.frameCount = 0 + collectgarbage("collect") + collectgarbage("collect") + profile.results.implicit.startMem = collectgarbage("count") + end + + elseif profile.testPhase == "implicit" then + -- 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 -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.") +function profile.draw() + -- Draw the current menu + if profile.testPhase ~= "complete" then + FlexLove.draw() + end + + -- Draw status overlay + love.graphics.setColor(0, 0, 0, 0.85) + love.graphics.rectangle("fill", 10, 10, 400, 120) + + love.graphics.setColor(1, 1, 1, 1) + love.graphics.print("Settings Menu Mode Comparison", 20, 20) + + if profile.testPhase == "warmup" then + love.graphics.print("Phase: Warmup (" .. profile.frameCount .. "/30)", 20, 45) + elseif profile.testPhase == "implicit" then + love.graphics.print("Phase: Without mode flags", 20, 45) + 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 + + love.graphics.print("Memory: " .. string.format("%.2f", collectgarbage("count") / 1024) .. " MB", 20, 85) + love.graphics.print("Press ESC to return to menu", 20, 105) 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)) +function profile.generateReport() + print("\n" .. string.rep("=", 80)) + print("RESULTS COMPARISON") + print(string.rep("=", 80) .. "\n") + + 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) + + print("Time Comparison:") + print(string.format(" Without mode flag: %.4f ms", profile.results.implicit.avgFrameTime)) + print(string.format(" With mode flag: %.4f ms", profile.results.explicit.avgFrameTime)) + 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(" The explicit mode flag has negligible impact on performance.") + elseif timePercent > 0 then + print(string.format(" ⚠ Explicit mode flag is %.2f%% SLOWER", timePercent)) + print(" This indicates overhead from mode checking/resolution.") + else + print(string.format(" ✓ Explicit mode flag is %.2f%% FASTER", -timePercent)) + print(" This indicates potential optimization benefits.") + end + print() + + if math.abs(memDiff) < 50 then + print(" ✓ Memory usage is essentially identical (< 50 KB difference)") + elseif memDiff > 0 then + print(string.format(" ⚠ Explicit mode flag uses %.2f KB MORE memory", memDiff)) + else + print(string.format(" ✓ Explicit mode flag uses %.2f KB LESS memory", -memDiff)) + end + print() + + print("RECOMMENDATION:") + print() + if math.abs(timePercent) < 5 and math.abs(memDiff) < 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("=", 80)) + print("Profile complete! Press S to save report.") + print(string.rep("=", 80) .. "\n") + + -- Save report automatically + profile.saveReportToFile() 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.") +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 -print() -print("=" .. string.rep("=", 78)) -print("Profile complete!") -print("=" .. string.rep("=", 78)) +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 diff --git a/profiling/main.lua b/profiling/main.lua index 57550d3..97d97fe 100644 --- a/profiling/main.lua +++ b/profiling/main.lua @@ -1,8 +1,26 @@ -- FlexLöve Profiler - Main Entry Point -- 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 lv = love @@ -118,17 +136,9 @@ local function buildMenu() padding = { horizontal = 40, vertical = 40 }, }) - local container = FlexLove.new({ - parent = root, - positioning = "flex", - flexDirection = "vertical", - alignItems = "center", - gap = 30, - }) - -- Title FlexLove.new({ - parent = container, + parent = root, width = 600, height = 80, 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), }) + local container = FlexLove.new({ + parent = root, + positioning = "flex", + flexDirection = "vertical", + alignItems = "center", + height = "100%", + width = "100%", + gap = 30, + }) + -- Subtitle FlexLove.new({ parent = container, @@ -152,32 +172,43 @@ local function buildMenu() -- Profile list local profileList = FlexLove.new({ parent = container, - width = 600, + width = "80%", + height = "80%", positioning = "flex", flexDirection = "vertical", gap = 10, + padding = { vertical = 20, horizontal = 20 }, + overflowY = "scroll", + --mode = "retained", }) for i, profile in ipairs(state.profiles) do local isSelected = i == state.selectedIndex + local isHovered = i == state.hoveredIndex local button = FlexLove.new({ parent = profileList, - width = "100%", + width = "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, positioning = "flex", - justifyContent = "flex-start", + justifyContent = "center", alignItems = "center", + alignSelf = "center", + --mode = "retained", padding = { horizontal = 15, vertical = 15 }, - onEvent = function(element, event) + onEvent = function(_, event) if event.type == "release" then state.selectedIndex = i loadProfile(profile) 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 - 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, }) @@ -223,8 +254,6 @@ end function lv.load(args) FlexLove.init({ - width = lv.graphics.getWidth(), - height = lv.graphics.getHeight(), immediateMode = true, }) @@ -266,6 +295,10 @@ function lv.update(dt) end end +function lv.wheelmoved(x, y) + FlexLove.wheelmoved(x, y) +end + function lv.draw() if state.mode == "menu" then buildMenu() diff --git a/testing/__tests__/touch_events_test.lua b/testing/__tests__/touch_events_test.lua index 412b07a..766d162 100644 --- a/testing/__tests__/touch_events_test.lua +++ b/testing/__tests__/touch_events_test.lua @@ -97,11 +97,19 @@ function TestTouchEvents:testEventHandler_TouchBegan() element._eventHandler:processTouchEvents(element) 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 -- 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.assertEquals(touchEvents[1].type, "touchpress") - lu.assertEquals(touchEvents[1].touchId, "touch1") + lu.assertTrue(#filteredEvents >= 1, "Should receive at least 1 touch event, got " .. #filteredEvents) + lu.assertEquals(filteredEvents[1].type, "touchpress") + lu.assertEquals(filteredEvents[1].touchId, "touch1") end -- Test: EventHandler tracks touch moved @@ -147,12 +155,20 @@ function TestTouchEvents:testEventHandler_TouchMoved() element._eventHandler:processTouchEvents(element) 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 - lu.assertEquals(#touchEvents, 2) - lu.assertEquals(touchEvents[1].type, "touchpress") - lu.assertEquals(touchEvents[2].type, "touchmove") - lu.assertEquals(touchEvents[2].dx, 50) - lu.assertEquals(touchEvents[2].dy, 50) + lu.assertEquals(#filteredEvents, 2) + lu.assertEquals(filteredEvents[1].type, "touchpress") + lu.assertEquals(filteredEvents[2].type, "touchmove") + lu.assertEquals(filteredEvents[2].dx, 50) + lu.assertEquals(filteredEvents[2].dy, 50) end -- Test: EventHandler tracks touch ended @@ -195,10 +211,18 @@ function TestTouchEvents:testEventHandler_TouchEnded() element._eventHandler:processTouchEvents(element) 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 - lu.assertEquals(#touchEvents, 2) - lu.assertEquals(touchEvents[1].type, "touchpress") - lu.assertEquals(touchEvents[2].type, "touchrelease") + lu.assertEquals(#filteredEvents, 2) + lu.assertEquals(filteredEvents[1].type, "touchpress") + lu.assertEquals(filteredEvents[2].type, "touchrelease") end -- Test: EventHandler tracks multiple simultaneous touches @@ -234,10 +258,18 @@ function TestTouchEvents:testEventHandler_MultiTouch() element._eventHandler:processTouchEvents(element) FlexLove.endFrame() - -- Should have received two touchpress events - lu.assertEquals(#touchEvents, 2) - lu.assertEquals(touchEvents[1].type, "touchpress") - lu.assertEquals(touchEvents[2].type, "touchpress") + -- 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 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 lu.assertNotEquals(touchEvents[1].touchId, touchEvents[2].touchId)