memory tooling, state handling changes
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -13,3 +13,5 @@ docs/doc.md
|
|||||||
docs/node_modules
|
docs/node_modules
|
||||||
releases/
|
releases/
|
||||||
*.log*
|
*.log*
|
||||||
|
memory_scan*
|
||||||
|
*_report*
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# FlexLöve Agent Guidelines
|
# FlexLöve Agent Guidelines
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
- **Run all tests**: `lua testing/runAll.lua` (coverage report in `luacov.report.out`)
|
- **Run all tests**: `lua testing/runAll.lua --no-coverage`
|
||||||
- **Run single test**: `lua testing/__tests__/<test_file>.lua`
|
- **Run single test**: `lua testing/__tests__/<test_file>.lua`
|
||||||
- **Test immediate mode**: Call `FlexLove.setMode("immediate")` in `setUp()`, then `FlexLove.beginFrame()`/`FlexLove.endFrame()` to trigger layout
|
- **Test immediate mode**: Call `FlexLove.setMode("immediate")` in `setUp()`, then `FlexLove.beginFrame()`/`FlexLove.endFrame()` to trigger layout
|
||||||
|
|
||||||
|
|||||||
18
FlexLove.lua
18
FlexLove.lua
@@ -325,6 +325,24 @@ function flexlove.beginFrame()
|
|||||||
-- Start performance frame timing
|
-- Start performance frame timing
|
||||||
flexlove._Performance:startFrame()
|
flexlove._Performance:startFrame()
|
||||||
|
|
||||||
|
-- Cleanup elements from PREVIOUS frame (after they've been drawn)
|
||||||
|
-- This breaks circular references and allows GC to collect memory
|
||||||
|
-- Note: Cleanup is minimal to preserve functionality
|
||||||
|
if flexlove._currentFrameElements then
|
||||||
|
local function cleanupChildren(elem)
|
||||||
|
for _, child in ipairs(elem.children) do
|
||||||
|
cleanupChildren(child)
|
||||||
|
end
|
||||||
|
elem:_cleanup()
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, element in ipairs(flexlove._currentFrameElements) do
|
||||||
|
if not element.parent then
|
||||||
|
cleanupChildren(element)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
flexlove._frameNumber = flexlove._frameNumber + 1
|
flexlove._frameNumber = flexlove._frameNumber + 1
|
||||||
StateManager.incrementFrame()
|
StateManager.incrementFrame()
|
||||||
flexlove._currentFrameElements = {}
|
flexlove._currentFrameElements = {}
|
||||||
|
|||||||
@@ -288,21 +288,20 @@ function Element.new(props)
|
|||||||
if Element._Context._immediateMode and self._stateId and self._stateId ~= "" then
|
if Element._Context._immediateMode 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
|
-- Restore EventHandler state from StateManager (sparse storage - provide defaults)
|
||||||
eventHandlerConfig._pressed = state._pressed
|
eventHandlerConfig._pressed = state._pressed or {}
|
||||||
eventHandlerConfig._lastClickTime = state._lastClickTime
|
eventHandlerConfig._lastClickTime = state._lastClickTime
|
||||||
eventHandlerConfig._lastClickButton = state._lastClickButton
|
eventHandlerConfig._lastClickButton = state._lastClickButton
|
||||||
eventHandlerConfig._clickCount = state._clickCount
|
eventHandlerConfig._clickCount = state._clickCount or 0
|
||||||
eventHandlerConfig._dragStartX = state._dragStartX
|
eventHandlerConfig._dragStartX = state._dragStartX or {}
|
||||||
eventHandlerConfig._dragStartY = state._dragStartY
|
eventHandlerConfig._dragStartY = state._dragStartY or {}
|
||||||
eventHandlerConfig._lastMouseX = state._lastMouseX
|
eventHandlerConfig._lastMouseX = state._lastMouseX or {}
|
||||||
eventHandlerConfig._lastMouseY = state._lastMouseY
|
eventHandlerConfig._lastMouseY = state._lastMouseY or {}
|
||||||
eventHandlerConfig._hovered = state._hovered
|
eventHandlerConfig._hovered = state._hovered
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
self._eventHandler = Element._EventHandler.new(eventHandlerConfig, eventHandlerDeps)
|
self._eventHandler = Element._EventHandler.new(eventHandlerConfig, eventHandlerDeps)
|
||||||
self._eventHandler:initialize(self)
|
|
||||||
|
|
||||||
self._themeManager = Element._Theme.Manager.new({
|
self._themeManager = Element._Theme.Manager.new({
|
||||||
theme = props.theme or Element._Context.defaultTheme,
|
theme = props.theme or Element._Context.defaultTheme,
|
||||||
@@ -313,7 +312,6 @@ function Element.new(props)
|
|||||||
scaleCorners = props.scaleCorners,
|
scaleCorners = props.scaleCorners,
|
||||||
scalingAlgorithm = props.scalingAlgorithm,
|
scalingAlgorithm = props.scalingAlgorithm,
|
||||||
})
|
})
|
||||||
self._themeManager:initialize(self)
|
|
||||||
|
|
||||||
-- Expose theme properties for backward compatibility
|
-- Expose theme properties for backward compatibility
|
||||||
self.theme = self._themeManager.theme
|
self.theme = self._themeManager.theme
|
||||||
@@ -418,24 +416,27 @@ function Element.new(props)
|
|||||||
|
|
||||||
------ add non-hereditary ------
|
------ add non-hereditary ------
|
||||||
--- self drawing---
|
--- self drawing---
|
||||||
-- Handle border (can be number or table)
|
-- OPTIMIZATION: Handle border - only create table if border exists
|
||||||
|
-- This saves ~80 bytes per element without borders
|
||||||
if type(props.border) == "table" then
|
if type(props.border) == "table" then
|
||||||
|
-- Check if any border side is truthy
|
||||||
|
local hasAnyBorder = props.border.top or props.border.right or props.border.bottom or props.border.left
|
||||||
|
if hasAnyBorder then
|
||||||
self.border = {
|
self.border = {
|
||||||
top = props.border.top or false,
|
top = props.border.top or false,
|
||||||
right = props.border.right or false,
|
right = props.border.right or false,
|
||||||
bottom = props.border.bottom or false,
|
bottom = props.border.bottom or false,
|
||||||
left = props.border.left or false,
|
left = props.border.left or false,
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
self.border = nil
|
||||||
|
end
|
||||||
elseif props.border then
|
elseif props.border then
|
||||||
-- If border is a number or truthy value, keep it as-is
|
-- If border is a number or truthy value, keep it as-is
|
||||||
self.border = props.border
|
self.border = props.border
|
||||||
else
|
else
|
||||||
self.border = {
|
-- No border specified - use nil instead of table with all false
|
||||||
top = false,
|
self.border = nil
|
||||||
right = false,
|
|
||||||
bottom = false,
|
|
||||||
left = false,
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
self.borderColor = props.borderColor or Element._Color.new(0, 0, 0, 1)
|
self.borderColor = props.borderColor or Element._Color.new(0, 0, 0, 1)
|
||||||
self.backgroundColor = props.backgroundColor or Element._Color.new(0, 0, 0, 0)
|
self.backgroundColor = props.backgroundColor or Element._Color.new(0, 0, 0, 0)
|
||||||
@@ -452,30 +453,34 @@ function Element.new(props)
|
|||||||
-- Set transform property (optional)
|
-- Set transform property (optional)
|
||||||
self.transform = props.transform or nil
|
self.transform = props.transform or nil
|
||||||
|
|
||||||
-- Handle cornerRadius (can be number or table)
|
-- OPTIMIZATION: Handle cornerRadius - store as number or table, nil if all zeros
|
||||||
|
-- This saves ~80 bytes per element without rounded corners
|
||||||
if props.cornerRadius then
|
if props.cornerRadius then
|
||||||
if type(props.cornerRadius) == "number" then
|
if type(props.cornerRadius) == "number" then
|
||||||
self.cornerRadius = {
|
-- Store as number for uniform radius (compact)
|
||||||
topLeft = props.cornerRadius,
|
if props.cornerRadius ~= 0 then
|
||||||
topRight = props.cornerRadius,
|
self.cornerRadius = props.cornerRadius
|
||||||
bottomLeft = props.cornerRadius,
|
|
||||||
bottomRight = props.cornerRadius,
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
|
self.cornerRadius = nil
|
||||||
|
end
|
||||||
|
else
|
||||||
|
-- Store as table only if non-zero values exist
|
||||||
|
local hasNonZero = props.cornerRadius.topLeft or props.cornerRadius.topRight or
|
||||||
|
props.cornerRadius.bottomLeft or props.cornerRadius.bottomRight
|
||||||
|
if hasNonZero then
|
||||||
self.cornerRadius = {
|
self.cornerRadius = {
|
||||||
topLeft = props.cornerRadius.topLeft or 0,
|
topLeft = props.cornerRadius.topLeft or 0,
|
||||||
topRight = props.cornerRadius.topRight or 0,
|
topRight = props.cornerRadius.topRight or 0,
|
||||||
bottomLeft = props.cornerRadius.bottomLeft or 0,
|
bottomLeft = props.cornerRadius.bottomLeft or 0,
|
||||||
bottomRight = props.cornerRadius.bottomRight or 0,
|
bottomRight = props.cornerRadius.bottomRight or 0,
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
self.cornerRadius = nil
|
||||||
|
end
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
self.cornerRadius = {
|
-- No cornerRadius specified - use nil instead of table with all zeros
|
||||||
topLeft = 0,
|
self.cornerRadius = nil
|
||||||
topRight = 0,
|
|
||||||
bottomLeft = 0,
|
|
||||||
bottomRight = 0,
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- For editable elements, default text to empty string if not provided
|
-- For editable elements, default text to empty string if not provided
|
||||||
@@ -622,7 +627,6 @@ function Element.new(props)
|
|||||||
contentBlur = self.contentBlur,
|
contentBlur = self.contentBlur,
|
||||||
backdropBlur = self.backdropBlur,
|
backdropBlur = self.backdropBlur,
|
||||||
}, rendererDeps)
|
}, rendererDeps)
|
||||||
self._renderer:initialize(self)
|
|
||||||
|
|
||||||
--- self positioning ---
|
--- self positioning ---
|
||||||
local viewportWidth, viewportHeight = Element._Units.getViewport()
|
local viewportWidth, viewportHeight = Element._Units.getViewport()
|
||||||
@@ -1417,7 +1421,6 @@ function Element.new(props)
|
|||||||
_scrollX = props._scrollX,
|
_scrollX = props._scrollX,
|
||||||
_scrollY = props._scrollY,
|
_scrollY = props._scrollY,
|
||||||
}, scrollManagerDeps)
|
}, scrollManagerDeps)
|
||||||
self._scrollManager:initialize(self)
|
|
||||||
|
|
||||||
-- Expose ScrollManager properties for backward compatibility (Renderer access)
|
-- Expose ScrollManager properties for backward compatibility (Renderer access)
|
||||||
self.overflow = self._scrollManager.overflow
|
self.overflow = self._scrollManager.overflow
|
||||||
@@ -1456,7 +1459,7 @@ function Element.new(props)
|
|||||||
|
|
||||||
-- Initialize TextEditor after element is fully constructed
|
-- Initialize TextEditor after element is fully constructed
|
||||||
if self._textEditor then
|
if self._textEditor then
|
||||||
self._textEditor:initialize(self)
|
self._textEditor:restoreState(self)
|
||||||
end
|
end
|
||||||
|
|
||||||
return self
|
return self
|
||||||
@@ -1519,7 +1522,7 @@ end
|
|||||||
--- Detect if content overflows container bounds (delegates to ScrollManager)
|
--- Detect if content overflows container bounds (delegates to ScrollManager)
|
||||||
function Element:_detectOverflow()
|
function Element:_detectOverflow()
|
||||||
if self._scrollManager then
|
if self._scrollManager then
|
||||||
self._scrollManager:detectOverflow()
|
self._scrollManager:detectOverflow(self)
|
||||||
self:_syncScrollManagerState()
|
self:_syncScrollManagerState()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -1539,7 +1542,7 @@ end
|
|||||||
---@return table -- {vertical: {visible, trackHeight, thumbHeight, thumbY}, horizontal: {visible, trackWidth, thumbWidth, thumbX}}
|
---@return table -- {vertical: {visible, trackHeight, thumbHeight, thumbY}, horizontal: {visible, trackWidth, thumbWidth, thumbX}}
|
||||||
function Element:_calculateScrollbarDimensions()
|
function Element:_calculateScrollbarDimensions()
|
||||||
if self._scrollManager then
|
if self._scrollManager then
|
||||||
return self._scrollManager:calculateScrollbarDimensions()
|
return self._scrollManager:calculateScrollbarDimensions(self)
|
||||||
end
|
end
|
||||||
-- Return empty result if no ScrollManager
|
-- Return empty result if no ScrollManager
|
||||||
return {
|
return {
|
||||||
@@ -1556,7 +1559,7 @@ end
|
|||||||
---@return table|nil -- {component: "vertical"|"horizontal", region: "thumb"|"track"}
|
---@return table|nil -- {component: "vertical"|"horizontal", region: "thumb"|"track"}
|
||||||
function Element:_getScrollbarAtPosition(mouseX, mouseY)
|
function Element:_getScrollbarAtPosition(mouseX, mouseY)
|
||||||
if self._scrollManager then
|
if self._scrollManager then
|
||||||
return self._scrollManager:getScrollbarAtPosition(mouseX, mouseY)
|
return self._scrollManager:getScrollbarAtPosition(self, mouseX, mouseY)
|
||||||
end
|
end
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
@@ -1568,7 +1571,7 @@ end
|
|||||||
---@return boolean -- True if event was consumed
|
---@return boolean -- True if event was consumed
|
||||||
function Element:_handleScrollbarPress(mouseX, mouseY, button)
|
function Element:_handleScrollbarPress(mouseX, mouseY, button)
|
||||||
if self._scrollManager then
|
if self._scrollManager then
|
||||||
local consumed = self._scrollManager:handleMousePress(mouseX, mouseY, button)
|
local consumed = self._scrollManager:handleMousePress(self, mouseX, mouseY, button)
|
||||||
self:_syncScrollManagerState()
|
self:_syncScrollManagerState()
|
||||||
return consumed
|
return consumed
|
||||||
end
|
end
|
||||||
@@ -1581,7 +1584,7 @@ end
|
|||||||
---@return boolean -- True if event was consumed
|
---@return boolean -- True if event was consumed
|
||||||
function Element:_handleScrollbarDrag(mouseX, mouseY)
|
function Element:_handleScrollbarDrag(mouseX, mouseY)
|
||||||
if self._scrollManager then
|
if self._scrollManager then
|
||||||
local consumed = self._scrollManager:handleMouseMove(mouseX, mouseY)
|
local consumed = self._scrollManager:handleMouseMove(self, mouseX, mouseY)
|
||||||
self:_syncScrollManagerState()
|
self:_syncScrollManagerState()
|
||||||
return consumed
|
return consumed
|
||||||
end
|
end
|
||||||
@@ -1999,7 +2002,7 @@ function Element:draw(backdropCanvas)
|
|||||||
local borderBoxHeight = self._borderBoxHeight or (self.height + self.padding.top + self.padding.bottom)
|
local borderBoxHeight = self._borderBoxHeight or (self.height + self.padding.top + self.padding.bottom)
|
||||||
|
|
||||||
-- LAYERS 0.5-3: Delegate visual rendering (backdrop blur, background, image, theme, borders) to Renderer module
|
-- LAYERS 0.5-3: Delegate visual rendering (backdrop blur, background, image, theme, borders) to Renderer module
|
||||||
self._renderer:draw(backdropCanvas)
|
self._renderer:draw(self, backdropCanvas)
|
||||||
|
|
||||||
-- LAYER 4: Delegate text rendering (text, cursor, selection, placeholder, password masking) to Renderer module
|
-- LAYER 4: Delegate text rendering (text, cursor, selection, placeholder, password masking) to Renderer module
|
||||||
self._renderer:drawText(self)
|
self._renderer:drawText(self)
|
||||||
@@ -2033,10 +2036,17 @@ function Element:draw(backdropCanvas)
|
|||||||
end)
|
end)
|
||||||
|
|
||||||
-- Check if we need to clip children to rounded corners
|
-- Check if we need to clip children to rounded corners
|
||||||
local hasRoundedCorners = self.cornerRadius.topLeft > 0
|
local hasRoundedCorners = false
|
||||||
|
if self.cornerRadius then
|
||||||
|
if type(self.cornerRadius) == "number" then
|
||||||
|
hasRoundedCorners = self.cornerRadius > 0
|
||||||
|
else
|
||||||
|
hasRoundedCorners = self.cornerRadius.topLeft > 0
|
||||||
or self.cornerRadius.topRight > 0
|
or self.cornerRadius.topRight > 0
|
||||||
or self.cornerRadius.bottomLeft > 0
|
or self.cornerRadius.bottomLeft > 0
|
||||||
or self.cornerRadius.bottomRight > 0
|
or self.cornerRadius.bottomRight > 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- Helper function to draw children (with or without clipping)
|
-- Helper function to draw children (with or without clipping)
|
||||||
local function drawChildren()
|
local function drawChildren()
|
||||||
@@ -2172,7 +2182,7 @@ function Element:update(dt)
|
|||||||
|
|
||||||
-- Update text editor cursor blink
|
-- Update text editor cursor blink
|
||||||
if self._textEditor then
|
if self._textEditor then
|
||||||
self._textEditor:update(dt)
|
self._textEditor:update(self, dt)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Update animation if exists
|
-- Update animation if exists
|
||||||
@@ -2266,7 +2276,7 @@ function Element:update(dt)
|
|||||||
local mx, my = love.mouse.getPosition()
|
local mx, my = love.mouse.getPosition()
|
||||||
|
|
||||||
if self._scrollManager then
|
if self._scrollManager then
|
||||||
self._scrollManager:updateHoverState(mx, my)
|
self._scrollManager:updateHoverState(self, mx, my)
|
||||||
self:_syncScrollManagerState()
|
self:_syncScrollManagerState()
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -2366,7 +2376,7 @@ function Element:update(dt)
|
|||||||
|
|
||||||
-- Process mouse events through EventHandler FIRST
|
-- Process mouse events through EventHandler FIRST
|
||||||
-- This ensures pressed states are updated before theme state is calculated
|
-- This ensures pressed states are updated before theme state is calculated
|
||||||
self._eventHandler:processMouseEvents(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 Element._Context._immediateMode and self._stateId ~= "" then
|
||||||
@@ -2417,7 +2427,7 @@ function Element:update(dt)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Process touch events through EventHandler
|
-- Process touch events through EventHandler
|
||||||
self._eventHandler:processTouchEvents()
|
self._eventHandler:processTouchEvents(self)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -2589,7 +2599,7 @@ end
|
|||||||
---@param position number -- Character index (0-based)
|
---@param position number -- Character index (0-based)
|
||||||
function Element:setCursorPosition(position)
|
function Element:setCursorPosition(position)
|
||||||
if self._textEditor then
|
if self._textEditor then
|
||||||
self._textEditor:setCursorPosition(position)
|
self._textEditor:setCursorPosition(self, position)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -2606,49 +2616,49 @@ end
|
|||||||
---@param delta number -- Number of characters to move (positive or negative)
|
---@param delta number -- Number of characters to move (positive or negative)
|
||||||
function Element:moveCursorBy(delta)
|
function Element:moveCursorBy(delta)
|
||||||
if self._textEditor then
|
if self._textEditor then
|
||||||
self._textEditor:moveCursorBy(delta)
|
self._textEditor:moveCursorBy(self, delta)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Move cursor to start of text
|
--- Move cursor to start of text
|
||||||
function Element:moveCursorToStart()
|
function Element:moveCursorToStart()
|
||||||
if self._textEditor then
|
if self._textEditor then
|
||||||
self._textEditor:moveCursorToStart()
|
self._textEditor:moveCursorToStart(self)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Move cursor to end of text
|
--- Move cursor to end of text
|
||||||
function Element:moveCursorToEnd()
|
function Element:moveCursorToEnd()
|
||||||
if self._textEditor then
|
if self._textEditor then
|
||||||
self._textEditor:moveCursorToEnd()
|
self._textEditor:moveCursorToEnd(self)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Move cursor to start of current line
|
--- Move cursor to start of current line
|
||||||
function Element:moveCursorToLineStart()
|
function Element:moveCursorToLineStart()
|
||||||
if self._textEditor then
|
if self._textEditor then
|
||||||
self._textEditor:moveCursorToLineStart()
|
self._textEditor:moveCursorToLineStart(self)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Move cursor to end of current line
|
--- Move cursor to end of current line
|
||||||
function Element:moveCursorToLineEnd()
|
function Element:moveCursorToLineEnd()
|
||||||
if self._textEditor then
|
if self._textEditor then
|
||||||
self._textEditor:moveCursorToLineEnd()
|
self._textEditor:moveCursorToLineEnd(self)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Move cursor to start of previous word
|
--- Move cursor to start of previous word
|
||||||
function Element:moveCursorToPreviousWord()
|
function Element:moveCursorToPreviousWord()
|
||||||
if self._textEditor then
|
if self._textEditor then
|
||||||
self._textEditor:moveCursorToPreviousWord()
|
self._textEditor:moveCursorToPreviousWord(self)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Move cursor to start of next word
|
--- Move cursor to start of next word
|
||||||
function Element:moveCursorToNextWord()
|
function Element:moveCursorToNextWord()
|
||||||
if self._textEditor then
|
if self._textEditor then
|
||||||
self._textEditor:moveCursorToNextWord()
|
self._textEditor:moveCursorToNextWord(self)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -2661,7 +2671,7 @@ end
|
|||||||
---@param endPos number -- End position (inclusive)
|
---@param endPos number -- End position (inclusive)
|
||||||
function Element:setSelection(startPos, endPos)
|
function Element:setSelection(startPos, endPos)
|
||||||
if self._textEditor then
|
if self._textEditor then
|
||||||
self._textEditor:setSelection(startPos, endPos)
|
self._textEditor:setSelection(self, startPos, endPos)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -2686,14 +2696,14 @@ end
|
|||||||
--- Clear selection
|
--- Clear selection
|
||||||
function Element:clearSelection()
|
function Element:clearSelection()
|
||||||
if self._textEditor then
|
if self._textEditor then
|
||||||
self._textEditor:clearSelection()
|
self._textEditor:clearSelection(self)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Select all text
|
--- Select all text
|
||||||
function Element:selectAll()
|
function Element:selectAll()
|
||||||
if self._textEditor then
|
if self._textEditor then
|
||||||
self._textEditor:selectAll()
|
self._textEditor:selectAll(self)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -2710,10 +2720,10 @@ end
|
|||||||
---@return boolean -- True if text was deleted
|
---@return boolean -- True if text was deleted
|
||||||
function Element:deleteSelection()
|
function Element:deleteSelection()
|
||||||
if self._textEditor then
|
if self._textEditor then
|
||||||
local result = self._textEditor:deleteSelection()
|
local result = self._textEditor:deleteSelection(self)
|
||||||
if result then
|
if result then
|
||||||
self.text = self._textEditor:getText() -- Sync display text
|
self.text = self._textEditor:getText() -- Sync display text
|
||||||
self._textEditor:updateAutoGrowHeight()
|
self._textEditor:updateAutoGrowHeight(self)
|
||||||
end
|
end
|
||||||
return result
|
return result
|
||||||
end
|
end
|
||||||
@@ -2728,7 +2738,7 @@ end
|
|||||||
--- Use this to automatically focus text fields when showing forms or dialogs
|
--- Use this to automatically focus text fields when showing forms or dialogs
|
||||||
function Element:focus()
|
function Element:focus()
|
||||||
if self._textEditor then
|
if self._textEditor then
|
||||||
self._textEditor:focus()
|
self._textEditor:focus(self)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -2736,7 +2746,7 @@ end
|
|||||||
--- Use this when closing popups or switching focus to other elements
|
--- Use this when closing popups or switching focus to other elements
|
||||||
function Element:blur()
|
function Element:blur()
|
||||||
if self._textEditor then
|
if self._textEditor then
|
||||||
self._textEditor:blur()
|
self._textEditor:blur(self)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -2769,9 +2779,9 @@ end
|
|||||||
---@param text string
|
---@param text string
|
||||||
function Element:setText(text)
|
function Element:setText(text)
|
||||||
if self._textEditor then
|
if self._textEditor then
|
||||||
self._textEditor:setText(text)
|
self._textEditor:setText(self, text)
|
||||||
self.text = self._textEditor:getText() -- Sync display text
|
self.text = self._textEditor:getText() -- Sync display text
|
||||||
self._textEditor:updateAutoGrowHeight()
|
self._textEditor:updateAutoGrowHeight(self)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
self.text = text
|
self.text = text
|
||||||
@@ -2783,9 +2793,9 @@ end
|
|||||||
---@param position number? -- Position to insert at (default: cursor position)
|
---@param position number? -- Position to insert at (default: cursor position)
|
||||||
function Element:insertText(text, position)
|
function Element:insertText(text, position)
|
||||||
if self._textEditor then
|
if self._textEditor then
|
||||||
self._textEditor:insertText(text, position)
|
self._textEditor:insertText(self, text, position)
|
||||||
self.text = self._textEditor:getText() -- Sync display text
|
self.text = self._textEditor:getText() -- Sync display text
|
||||||
self._textEditor:updateAutoGrowHeight()
|
self._textEditor:updateAutoGrowHeight(self)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -2793,9 +2803,9 @@ end
|
|||||||
---@param endPos number -- End position (inclusive)
|
---@param endPos number -- End position (inclusive)
|
||||||
function Element:deleteText(startPos, endPos)
|
function Element:deleteText(startPos, endPos)
|
||||||
if self._textEditor then
|
if self._textEditor then
|
||||||
self._textEditor:deleteText(startPos, endPos)
|
self._textEditor:deleteText(self, startPos, endPos)
|
||||||
self.text = self._textEditor:getText() -- Sync display text
|
self.text = self._textEditor:getText() -- Sync display text
|
||||||
self._textEditor:updateAutoGrowHeight()
|
self._textEditor:updateAutoGrowHeight(self)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -2805,9 +2815,9 @@ end
|
|||||||
---@param newText string -- Replacement text
|
---@param newText string -- Replacement text
|
||||||
function Element:replaceText(startPos, endPos, newText)
|
function Element:replaceText(startPos, endPos, newText)
|
||||||
if self._textEditor then
|
if self._textEditor then
|
||||||
self._textEditor:replaceText(startPos, endPos, newText)
|
self._textEditor:replaceText(self, startPos, endPos, newText)
|
||||||
self.text = self._textEditor:getText() -- Sync display text
|
self.text = self._textEditor:getText() -- Sync display text
|
||||||
self._textEditor:updateAutoGrowHeight()
|
self._textEditor:updateAutoGrowHeight(self)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -2834,10 +2844,10 @@ end
|
|||||||
---@param clickCount number -- Number of clicks (1=single, 2=double, 3=triple)
|
---@param clickCount number -- Number of clicks (1=single, 2=double, 3=triple)
|
||||||
function Element:_handleTextClick(mouseX, mouseY, clickCount)
|
function Element:_handleTextClick(mouseX, mouseY, clickCount)
|
||||||
if self._textEditor then
|
if self._textEditor then
|
||||||
self._textEditor:handleTextClick(mouseX, mouseY, clickCount)
|
self._textEditor:handleTextClick(self, mouseX, mouseY, clickCount)
|
||||||
-- Store mouse down position on element for drag tracking
|
-- Store mouse down position on element for drag tracking
|
||||||
if clickCount == 1 then
|
if clickCount == 1 then
|
||||||
self._mouseDownPosition = self._textEditor:mouseToTextPosition(mouseX, mouseY)
|
self._mouseDownPosition = self._textEditor:mouseToTextPosition(self, mouseX, mouseY)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -2847,7 +2857,7 @@ end
|
|||||||
---@param mouseY number -- Mouse Y coordinate
|
---@param mouseY number -- Mouse Y coordinate
|
||||||
function Element:_handleTextDrag(mouseX, mouseY)
|
function Element:_handleTextDrag(mouseX, mouseY)
|
||||||
if self._textEditor then
|
if self._textEditor then
|
||||||
self._textEditor:handleTextDrag(mouseX, mouseY)
|
self._textEditor:handleTextDrag(self, mouseX, mouseY)
|
||||||
self._textDragOccurred = self._textEditor._textDragOccurred
|
self._textDragOccurred = self._textEditor._textDragOccurred
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -2860,9 +2870,9 @@ end
|
|||||||
---@param text string -- Character(s) to insert
|
---@param text string -- Character(s) to insert
|
||||||
function Element:textinput(text)
|
function Element:textinput(text)
|
||||||
if self._textEditor then
|
if self._textEditor then
|
||||||
self._textEditor:handleTextInput(text)
|
self._textEditor:handleTextInput(self, text)
|
||||||
self.text = self._textEditor:getText() -- Sync display text
|
self.text = self._textEditor:getText() -- Sync display text
|
||||||
self._textEditor:updateAutoGrowHeight()
|
self._textEditor:updateAutoGrowHeight(self)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -2872,9 +2882,9 @@ end
|
|||||||
---@param isrepeat boolean -- Whether this is a key repeat
|
---@param isrepeat boolean -- Whether this is a key repeat
|
||||||
function Element:keypressed(key, scancode, isrepeat)
|
function Element:keypressed(key, scancode, isrepeat)
|
||||||
if self._textEditor then
|
if self._textEditor then
|
||||||
self._textEditor:handleKeyPress(key, scancode, isrepeat)
|
self._textEditor:handleKeyPress(self, key, scancode, isrepeat)
|
||||||
self.text = self._textEditor:getText() -- Sync display text
|
self.text = self._textEditor:getText() -- Sync display text
|
||||||
self._textEditor:updateAutoGrowHeight()
|
self._textEditor:updateAutoGrowHeight(self)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -3150,4 +3160,44 @@ function Element:setProperty(property, value)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
--- Cleanup method to break circular references (for immediate mode)
|
||||||
|
--- Note: Cleans internal module state but keeps structure for inspection
|
||||||
|
function Element:_cleanup()
|
||||||
|
-- Clean up module internal state
|
||||||
|
if self._eventHandler then
|
||||||
|
self._eventHandler:_cleanup()
|
||||||
|
end
|
||||||
|
|
||||||
|
if self._themeManager then
|
||||||
|
self._themeManager:_cleanup()
|
||||||
|
end
|
||||||
|
|
||||||
|
if self._renderer then
|
||||||
|
self._renderer:_cleanup()
|
||||||
|
end
|
||||||
|
|
||||||
|
if self._layoutEngine then
|
||||||
|
self._layoutEngine:_cleanup()
|
||||||
|
end
|
||||||
|
|
||||||
|
if self._scrollManager then
|
||||||
|
self._scrollManager:_cleanup()
|
||||||
|
end
|
||||||
|
|
||||||
|
if self._textEditor then
|
||||||
|
self._textEditor:_cleanup()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Clear event callbacks (may hold closures)
|
||||||
|
self.onEvent = nil
|
||||||
|
self.onFocus = nil
|
||||||
|
self.onBlur = nil
|
||||||
|
self.onTextInput = nil
|
||||||
|
self.onTextChange = nil
|
||||||
|
self.onEnter = nil
|
||||||
|
self.onImageLoad = nil
|
||||||
|
self.onImageError = nil
|
||||||
|
end
|
||||||
|
|
||||||
return Element
|
return Element
|
||||||
|
|||||||
@@ -14,7 +14,6 @@
|
|||||||
---@field _lastTouchPositions table<string, table> -- Last touch positions for delta
|
---@field _lastTouchPositions table<string, table> -- Last touch positions for delta
|
||||||
---@field _touchHistory table<string, table> -- Touch position history for gestures (last 5)
|
---@field _touchHistory table<string, table> -- Touch position history for gestures (last 5)
|
||||||
---@field _hovered boolean
|
---@field _hovered boolean
|
||||||
---@field _element Element?
|
|
||||||
---@field _scrollbarPressHandled boolean
|
---@field _scrollbarPressHandled boolean
|
||||||
---@field _InputEvent table
|
---@field _InputEvent table
|
||||||
---@field _utils table
|
---@field _utils table
|
||||||
@@ -60,19 +59,11 @@ function EventHandler.new(config)
|
|||||||
|
|
||||||
self._hovered = config._hovered or false
|
self._hovered = config._hovered or false
|
||||||
|
|
||||||
self._element = nil
|
|
||||||
|
|
||||||
self._scrollbarPressHandled = false
|
self._scrollbarPressHandled = false
|
||||||
|
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Initialize EventHandler with parent element reference
|
|
||||||
---@param element Element The parent element
|
|
||||||
function EventHandler:initialize(element)
|
|
||||||
self._element = element
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Get state for persistence (for immediate mode)
|
--- Get state for persistence (for immediate mode)
|
||||||
---@return table State data
|
---@return table State data
|
||||||
function EventHandler:getState()
|
function EventHandler:getState()
|
||||||
@@ -116,26 +107,18 @@ function EventHandler:setState(state)
|
|||||||
end
|
end
|
||||||
|
|
||||||
--- Process mouse button events in the update cycle
|
--- Process mouse button events in the update cycle
|
||||||
|
---@param element Element The parent element
|
||||||
---@param mx number Mouse X position
|
---@param mx number Mouse X position
|
||||||
---@param my number Mouse Y position
|
---@param my number Mouse Y position
|
||||||
---@param isHovering boolean Whether mouse is over element
|
---@param isHovering boolean Whether mouse is over element
|
||||||
---@param isActiveElement boolean Whether this is the top element at mouse position
|
---@param isActiveElement boolean Whether this is the top element at mouse position
|
||||||
function EventHandler:processMouseEvents(mx, my, isHovering, isActiveElement)
|
function EventHandler:processMouseEvents(element, mx, my, isHovering, isActiveElement)
|
||||||
-- Start performance timing
|
-- Start performance timing
|
||||||
-- Performance accessed via EventHandler._Performance
|
-- Performance accessed via EventHandler._Performance
|
||||||
if EventHandler._Performance and EventHandler._Performance.enabled then
|
if EventHandler._Performance and EventHandler._Performance.enabled then
|
||||||
EventHandler._Performance:startTimer("event_mouse")
|
EventHandler._Performance:startTimer("event_mouse")
|
||||||
end
|
end
|
||||||
|
|
||||||
if not self._element then
|
|
||||||
if EventHandler._Performance and EventHandler._Performance.enabled then
|
|
||||||
EventHandler._Performance:stopTimer("event_mouse")
|
|
||||||
end
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local element = self._element
|
|
||||||
|
|
||||||
-- Check if currently dragging (allows drag continuation even if occluded)
|
-- Check if currently dragging (allows drag continuation even if occluded)
|
||||||
local isDragging = false
|
local isDragging = false
|
||||||
for _, button in ipairs({ 1, 2, 3 }) do
|
for _, button in ipairs({ 1, 2, 3 }) do
|
||||||
@@ -189,18 +172,18 @@ function EventHandler:processMouseEvents(mx, my, isHovering, isActiveElement)
|
|||||||
if not wasPressed then
|
if not wasPressed then
|
||||||
-- Just pressed - fire press event (only if hovering)
|
-- Just pressed - fire press event (only if hovering)
|
||||||
if isHovering then
|
if isHovering then
|
||||||
self:_handleMousePress(mx, my, button)
|
self:_handleMousePress(element, mx, my, button)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
-- Button is still pressed - check for drag
|
-- Button is still pressed - check for drag
|
||||||
self:_handleMouseDrag(mx, my, button, isHovering)
|
self:_handleMouseDrag(element, mx, my, button, isHovering)
|
||||||
end
|
end
|
||||||
elseif wasPressed then
|
elseif wasPressed then
|
||||||
-- Button was just released
|
-- Button was just released
|
||||||
-- Only fire click and release events if mouse is still hovering AND element is active
|
-- Only fire click and release events if mouse is still hovering AND element is active
|
||||||
-- (not occluded by another element)
|
-- (not occluded by another element)
|
||||||
if isHovering and isActiveElement then
|
if isHovering and isActiveElement then
|
||||||
self:_handleMouseRelease(mx, my, button)
|
self:_handleMouseRelease(element, mx, my, button)
|
||||||
else
|
else
|
||||||
-- Mouse left before release OR element is occluded - just clear the pressed state without firing events
|
-- Mouse left before release OR element is occluded - just clear the pressed state without firing events
|
||||||
self._pressed[button] = false
|
self._pressed[button] = false
|
||||||
@@ -230,15 +213,11 @@ function EventHandler:processMouseEvents(mx, my, isHovering, isActiveElement)
|
|||||||
end
|
end
|
||||||
|
|
||||||
--- Handle mouse button press
|
--- Handle mouse button press
|
||||||
|
---@param element Element The parent element
|
||||||
---@param mx number Mouse X position
|
---@param mx number Mouse X position
|
||||||
---@param my number Mouse Y position
|
---@param my number Mouse Y position
|
||||||
---@param button number Mouse button (1=left, 2=right, 3=middle)
|
---@param button number Mouse button (1=left, 2=right, 3=middle)
|
||||||
function EventHandler:_handleMousePress(mx, my, button)
|
function EventHandler:_handleMousePress(element, mx, my, button)
|
||||||
if not self._element then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local element = self._element
|
|
||||||
|
|
||||||
-- Check if press is on scrollbar first (skip if already handled)
|
-- Check if press is on scrollbar first (skip if already handled)
|
||||||
if button == 1 and not self._scrollbarPressHandled and element._handleScrollbarPress then
|
if button == 1 and not self._scrollbarPressHandled and element._handleScrollbarPress then
|
||||||
@@ -266,7 +245,7 @@ function EventHandler:_handleMousePress(mx, my, button)
|
|||||||
|
|
||||||
-- Set mouse down position for text selection on left click
|
-- Set mouse down position for text selection on left click
|
||||||
if button == 1 and element._textEditor then
|
if button == 1 and element._textEditor then
|
||||||
element._mouseDownPosition = element._textEditor:mouseToTextPosition(mx, my)
|
element._mouseDownPosition = element._textEditor:mouseToTextPosition(element, mx, my)
|
||||||
element._textDragOccurred = false -- Reset drag flag on press
|
element._textDragOccurred = false -- Reset drag flag on press
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -278,16 +257,12 @@ function EventHandler:_handleMousePress(mx, my, button)
|
|||||||
end
|
end
|
||||||
|
|
||||||
--- Handle mouse drag (while button is pressed and mouse moves)
|
--- Handle mouse drag (while button is pressed and mouse moves)
|
||||||
|
---@param element Element The parent element
|
||||||
---@param mx number Mouse X position
|
---@param mx number Mouse X position
|
||||||
---@param my number Mouse Y position
|
---@param my number Mouse Y position
|
||||||
---@param button number Mouse button
|
---@param button number Mouse button
|
||||||
---@param isHovering boolean Whether mouse is over element
|
---@param isHovering boolean Whether mouse is over element
|
||||||
function EventHandler:_handleMouseDrag(mx, my, button, isHovering)
|
function EventHandler:_handleMouseDrag(element, mx, my, button, isHovering)
|
||||||
if not self._element then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local element = self._element
|
|
||||||
|
|
||||||
local lastX = self._lastMouseX[button] or mx
|
local lastX = self._lastMouseX[button] or mx
|
||||||
local lastY = self._lastMouseY[button] or my
|
local lastY = self._lastMouseY[button] or my
|
||||||
@@ -327,12 +302,7 @@ end
|
|||||||
---@param mx number Mouse X position
|
---@param mx number Mouse X position
|
||||||
---@param my number Mouse Y position
|
---@param my number Mouse Y position
|
||||||
---@param button number Mouse button
|
---@param button number Mouse button
|
||||||
function EventHandler:_handleMouseRelease(mx, my, button)
|
function EventHandler:_handleMouseRelease(element, mx, my, button)
|
||||||
if not self._element then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local element = self._element
|
|
||||||
|
|
||||||
local currentTime = love.timer.getTime()
|
local currentTime = love.timer.getTime()
|
||||||
local modifiers = EventHandler._utils.getModifiers()
|
local modifiers = EventHandler._utils.getModifiers()
|
||||||
@@ -412,21 +382,16 @@ function EventHandler:_handleMouseRelease(mx, my, button)
|
|||||||
end
|
end
|
||||||
|
|
||||||
--- Process touch events in the update cycle
|
--- Process touch events in the update cycle
|
||||||
function EventHandler:processTouchEvents()
|
---@param element Element The parent element
|
||||||
|
function EventHandler:processTouchEvents(element)
|
||||||
-- Start performance timing
|
-- Start performance timing
|
||||||
-- Performance accessed via EventHandler._Performance
|
|
||||||
if EventHandler._Performance and EventHandler._Performance.enabled then
|
if EventHandler._Performance and EventHandler._Performance.enabled then
|
||||||
EventHandler._Performance:startTimer("event_touch")
|
EventHandler._Performance:startTimer("event_touch")
|
||||||
end
|
end
|
||||||
|
|
||||||
if not self._element then
|
-- Get all active touches from LÖVE
|
||||||
if EventHandler._Performance and EventHandler._Performance.enabled then
|
local loveTouches = love.touch.getTouches()
|
||||||
EventHandler._Performance:stopTimer("event_touch")
|
local activeTouchIds = {}
|
||||||
end
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local element = self._element
|
|
||||||
|
|
||||||
-- Check if element can process events
|
-- Check if element can process events
|
||||||
local canProcessEvents = (self.onEvent or element.editable) and not element.disabled
|
local canProcessEvents = (self.onEvent or element.editable) and not element.disabled
|
||||||
@@ -462,19 +427,19 @@ function EventHandler:processTouchEvents()
|
|||||||
if isInside then
|
if isInside then
|
||||||
if not self._touches[touchId] then
|
if not self._touches[touchId] then
|
||||||
-- New touch began
|
-- New touch began
|
||||||
self:_handleTouchBegan(touchId, tx, ty, pressure)
|
self:_handleTouchBegan(element, touchId, tx, ty, pressure)
|
||||||
else
|
else
|
||||||
-- Touch moved
|
-- Touch moved
|
||||||
self:_handleTouchMoved(touchId, tx, ty, pressure)
|
self:_handleTouchMoved(element, touchId, tx, ty, pressure)
|
||||||
end
|
end
|
||||||
elseif self._touches[touchId] then
|
elseif self._touches[touchId] then
|
||||||
-- Touch moved outside or ended
|
-- Touch moved outside or ended
|
||||||
if activeTouches[touchId] then
|
if activeTouches[touchId] then
|
||||||
-- Still active but outside - fire moved event
|
-- Still active but outside - fire moved event
|
||||||
self:_handleTouchMoved(touchId, tx, ty, pressure)
|
self:_handleTouchMoved(element, touchId, tx, ty, pressure)
|
||||||
else
|
else
|
||||||
-- Touch ended
|
-- Touch ended
|
||||||
self:_handleTouchEnded(touchId, tx, ty, pressure)
|
self:_handleTouchEnded(element, touchId, tx, ty, pressure)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -485,7 +450,7 @@ function EventHandler:processTouchEvents()
|
|||||||
-- Touch ended or cancelled
|
-- Touch ended or cancelled
|
||||||
local lastPos = self._lastTouchPositions[touchId]
|
local lastPos = self._lastTouchPositions[touchId]
|
||||||
if lastPos then
|
if lastPos then
|
||||||
self:_handleTouchEnded(touchId, lastPos.x, lastPos.y, 1.0)
|
self:_handleTouchEnded(element, touchId, lastPos.x, lastPos.y, 1.0)
|
||||||
else
|
else
|
||||||
-- Cleanup orphaned touch
|
-- Cleanup orphaned touch
|
||||||
self:_cleanupTouch(touchId)
|
self:_cleanupTouch(touchId)
|
||||||
@@ -500,16 +465,12 @@ function EventHandler:processTouchEvents()
|
|||||||
end
|
end
|
||||||
|
|
||||||
--- Handle touch began event
|
--- Handle touch began event
|
||||||
---@param touchId string Touch ID
|
---@param element Element The parent element
|
||||||
|
---@param touchId string Touch identifier
|
||||||
---@param x number Touch X position
|
---@param x number Touch X position
|
||||||
---@param y number Touch Y position
|
---@param y number Touch Y position
|
||||||
---@param pressure number Touch pressure (0-1)
|
---@param pressure number Touch pressure (0-1)
|
||||||
function EventHandler:_handleTouchBegan(touchId, x, y, pressure)
|
function EventHandler:_handleTouchBegan(element, touchId, x, y, pressure)
|
||||||
if not self._element then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local element = self._element
|
|
||||||
|
|
||||||
-- Create touch state
|
-- Create touch state
|
||||||
self._touches[touchId] = {
|
self._touches[touchId] = {
|
||||||
@@ -536,16 +497,12 @@ function EventHandler:_handleTouchBegan(touchId, x, y, pressure)
|
|||||||
end
|
end
|
||||||
|
|
||||||
--- Handle touch moved event
|
--- Handle touch moved event
|
||||||
---@param touchId string Touch ID
|
---@param element Element The parent element
|
||||||
|
---@param touchId string Touch identifier
|
||||||
---@param x number Touch X position
|
---@param x number Touch X position
|
||||||
---@param y number Touch Y position
|
---@param y number Touch Y position
|
||||||
---@param pressure number Touch pressure (0-1)
|
---@param pressure number Touch pressure (0-1)
|
||||||
function EventHandler:_handleTouchMoved(touchId, x, y, pressure)
|
function EventHandler:_handleTouchMoved(element, touchId, x, y, pressure)
|
||||||
if not self._element then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local element = self._element
|
|
||||||
local touchState = self._touches[touchId]
|
local touchState = self._touches[touchId]
|
||||||
|
|
||||||
if not touchState then
|
if not touchState then
|
||||||
@@ -587,16 +544,12 @@ function EventHandler:_handleTouchMoved(touchId, x, y, pressure)
|
|||||||
end
|
end
|
||||||
|
|
||||||
--- Handle touch ended event
|
--- Handle touch ended event
|
||||||
---@param touchId string Touch ID
|
---@param element Element The parent element
|
||||||
|
---@param touchId string Touch identifier
|
||||||
---@param x number Touch X position
|
---@param x number Touch X position
|
||||||
---@param y number Touch Y position
|
---@param y number Touch Y position
|
||||||
---@param pressure number Touch pressure (0-1)
|
---@param pressure number Touch pressure (0-1)
|
||||||
function EventHandler:_handleTouchEnded(touchId, x, y, pressure)
|
function EventHandler:_handleTouchEnded(element, touchId, x, y, pressure)
|
||||||
if not self._element then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local element = self._element
|
|
||||||
local touchState = self._touches[touchId]
|
local touchState = self._touches[touchId]
|
||||||
|
|
||||||
if not touchState then
|
if not touchState then
|
||||||
@@ -689,4 +642,13 @@ function EventHandler:_invokeCallback(element, event)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
--- Cleanup method to break circular references (for immediate mode)
|
||||||
|
--- Note: Only clears module references, preserves state for inspection/testing
|
||||||
|
function EventHandler:_cleanup()
|
||||||
|
-- DO NOT clear state data (_pressed, _touches, etc.) - they're needed for state persistence
|
||||||
|
-- Only clear module references that could create circular dependencies
|
||||||
|
-- (In practice, EventHandler doesn't store refs to Context/utils, so nothing to do)
|
||||||
|
end
|
||||||
|
|
||||||
return EventHandler
|
return EventHandler
|
||||||
|
|||||||
@@ -1018,4 +1018,13 @@ function LayoutEngine:_trackLayoutRecalculation()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
--- Cleanup method to break circular references (for immediate mode)
|
||||||
|
function LayoutEngine:_cleanup()
|
||||||
|
-- Circular refs: Element → LayoutEngine → element → Element
|
||||||
|
-- But breaking element ref breaks functionality
|
||||||
|
-- Module refs are singletons, not circular
|
||||||
|
-- In immediate mode, full GC happens anyway
|
||||||
|
end
|
||||||
|
|
||||||
return LayoutEngine
|
return LayoutEngine
|
||||||
|
|||||||
668
modules/MemoryScanner.lua
Normal file
668
modules/MemoryScanner.lua
Normal file
@@ -0,0 +1,668 @@
|
|||||||
|
---@class MemoryScanner
|
||||||
|
---@field _StateManager table
|
||||||
|
---@field _Context table
|
||||||
|
---@field _ImageCache table
|
||||||
|
---@field _ErrorHandler table
|
||||||
|
local MemoryScanner = {}
|
||||||
|
|
||||||
|
---Initialize MemoryScanner with dependencies
|
||||||
|
---@param deps {StateManager: table, Context: table, ImageCache: table, ErrorHandler: table}
|
||||||
|
function MemoryScanner.init(deps)
|
||||||
|
MemoryScanner._StateManager = deps.StateManager
|
||||||
|
MemoryScanner._Context = deps.Context
|
||||||
|
MemoryScanner._ImageCache = deps.ImageCache
|
||||||
|
MemoryScanner._ErrorHandler = deps.ErrorHandler
|
||||||
|
end
|
||||||
|
|
||||||
|
---Count items in a table
|
||||||
|
---@param tbl table
|
||||||
|
---@return number
|
||||||
|
local function countTable(tbl)
|
||||||
|
local count = 0
|
||||||
|
for _ in pairs(tbl) do
|
||||||
|
count = count + 1
|
||||||
|
end
|
||||||
|
return count
|
||||||
|
end
|
||||||
|
|
||||||
|
---Calculate memory size estimate for a table (recursive)
|
||||||
|
---@param tbl table
|
||||||
|
---@param visited table? Tracking table to prevent circular references
|
||||||
|
---@param depth number? Current recursion depth
|
||||||
|
---@return number bytes Estimated memory usage in bytes
|
||||||
|
local function estimateTableSize(tbl, visited, depth)
|
||||||
|
if type(tbl) ~= "table" then
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
|
||||||
|
visited = visited or {}
|
||||||
|
depth = depth or 0
|
||||||
|
|
||||||
|
-- Limit recursion depth to prevent stack overflow
|
||||||
|
if depth > 10 then
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Check for circular references
|
||||||
|
if visited[tbl] then
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
visited[tbl] = true
|
||||||
|
|
||||||
|
local size = 40 -- Base table overhead (approximate)
|
||||||
|
|
||||||
|
for k, v in pairs(tbl) do
|
||||||
|
-- Key size
|
||||||
|
if type(k) == "string" then
|
||||||
|
size = size + #k + 24 -- String overhead
|
||||||
|
elseif type(k) == "number" then
|
||||||
|
size = size + 8
|
||||||
|
else
|
||||||
|
size = size + 8 -- Reference
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Value size
|
||||||
|
if type(v) == "string" then
|
||||||
|
size = size + #v + 24
|
||||||
|
elseif type(v) == "number" then
|
||||||
|
size = size + 8
|
||||||
|
elseif type(v) == "boolean" then
|
||||||
|
size = size + 4
|
||||||
|
elseif type(v) == "table" then
|
||||||
|
size = size + estimateTableSize(v, visited, depth + 1)
|
||||||
|
elseif type(v) == "function" then
|
||||||
|
size = size + 16 -- Function reference
|
||||||
|
else
|
||||||
|
size = size + 8 -- Other references
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return size
|
||||||
|
end
|
||||||
|
|
||||||
|
---Scan StateManager for memory issues
|
||||||
|
---@return table report Detailed report of StateManager memory usage
|
||||||
|
function MemoryScanner.scanStateManager()
|
||||||
|
local report = {
|
||||||
|
stateCount = 0,
|
||||||
|
stateStoreSize = 0,
|
||||||
|
metadataSize = 0,
|
||||||
|
callSiteCounterSize = 0,
|
||||||
|
orphanedStates = {},
|
||||||
|
staleStates = {},
|
||||||
|
largeStates = {},
|
||||||
|
issues = {},
|
||||||
|
}
|
||||||
|
|
||||||
|
if not MemoryScanner._StateManager then
|
||||||
|
table.insert(report.issues, {
|
||||||
|
severity = "error",
|
||||||
|
message = "StateManager not initialized",
|
||||||
|
})
|
||||||
|
return report
|
||||||
|
end
|
||||||
|
|
||||||
|
local internal = MemoryScanner._StateManager._getInternalState()
|
||||||
|
local stateStore = internal.stateStore
|
||||||
|
local stateMetadata = internal.stateMetadata
|
||||||
|
local callSiteCounters = internal.callSiteCounters
|
||||||
|
local currentFrame = MemoryScanner._StateManager.getFrameNumber()
|
||||||
|
|
||||||
|
-- Count states
|
||||||
|
report.stateCount = countTable(stateStore)
|
||||||
|
|
||||||
|
-- Estimate sizes
|
||||||
|
report.stateStoreSize = estimateTableSize(stateStore)
|
||||||
|
report.metadataSize = estimateTableSize(stateMetadata)
|
||||||
|
report.callSiteCounterSize = estimateTableSize(callSiteCounters)
|
||||||
|
|
||||||
|
-- Check for orphaned states (metadata without state)
|
||||||
|
for id, _ in pairs(stateMetadata) do
|
||||||
|
if not stateStore[id] then
|
||||||
|
table.insert(report.orphanedStates, id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Check for stale states (not accessed in many frames)
|
||||||
|
local staleThreshold = 120 -- 2 seconds at 60fps
|
||||||
|
for id, meta in pairs(stateMetadata) do
|
||||||
|
local framesSinceAccess = currentFrame - meta.lastFrame
|
||||||
|
if framesSinceAccess > staleThreshold then
|
||||||
|
table.insert(report.staleStates, {
|
||||||
|
id = id,
|
||||||
|
framesSinceAccess = framesSinceAccess,
|
||||||
|
createdFrame = meta.createdFrame,
|
||||||
|
accessCount = meta.accessCount,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Check for large states (may indicate memory bloat)
|
||||||
|
for id, state in pairs(stateStore) do
|
||||||
|
local stateSize = estimateTableSize(state)
|
||||||
|
if stateSize > 1024 then -- More than 1KB
|
||||||
|
table.insert(report.largeStates, {
|
||||||
|
id = id,
|
||||||
|
size = stateSize,
|
||||||
|
keyCount = countTable(state),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Check callSiteCounters (should be near 0 after frame cleanup)
|
||||||
|
local callSiteCount = countTable(callSiteCounters)
|
||||||
|
if callSiteCount > 100 then
|
||||||
|
table.insert(report.issues, {
|
||||||
|
severity = "warning",
|
||||||
|
message = string.format("callSiteCounters has %d entries (expected near 0)", callSiteCount),
|
||||||
|
suggestion = "incrementFrame() may not be called properly, or counters aren't being reset",
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Check for excessive state count
|
||||||
|
if report.stateCount > 500 then
|
||||||
|
table.insert(report.issues, {
|
||||||
|
severity = "warning",
|
||||||
|
message = string.format("High state count: %d states", report.stateCount),
|
||||||
|
suggestion = "Consider reducing element count or implementing more aggressive cleanup",
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Check for orphaned states
|
||||||
|
if #report.orphanedStates > 0 then
|
||||||
|
table.insert(report.issues, {
|
||||||
|
severity = "error",
|
||||||
|
message = string.format("Found %d orphaned states (metadata without state)", #report.orphanedStates),
|
||||||
|
suggestion = "This indicates a bug in state management - metadata should be cleaned up with state",
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Check for stale states
|
||||||
|
if #report.staleStates > 10 then
|
||||||
|
table.insert(report.issues, {
|
||||||
|
severity = "warning",
|
||||||
|
message = string.format("Found %d stale states (not accessed in 2+ seconds)", #report.staleStates),
|
||||||
|
suggestion = "Cleanup may not be aggressive enough - consider reducing stateRetentionFrames",
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
return report
|
||||||
|
end
|
||||||
|
|
||||||
|
---Scan Context for memory issues
|
||||||
|
---@return table report Detailed report of Context memory usage
|
||||||
|
function MemoryScanner.scanContext()
|
||||||
|
local report = {
|
||||||
|
topElementCount = 0,
|
||||||
|
zIndexElementCount = 0,
|
||||||
|
frameElementCount = 0,
|
||||||
|
issues = {},
|
||||||
|
}
|
||||||
|
|
||||||
|
if not MemoryScanner._Context then
|
||||||
|
table.insert(report.issues, {
|
||||||
|
severity = "error",
|
||||||
|
message = "Context not initialized",
|
||||||
|
})
|
||||||
|
return report
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Count elements
|
||||||
|
report.topElementCount = #MemoryScanner._Context.topElements
|
||||||
|
report.zIndexElementCount = #MemoryScanner._Context._zIndexOrderedElements
|
||||||
|
report.frameElementCount = #MemoryScanner._Context._currentFrameElements
|
||||||
|
|
||||||
|
-- Check for stale z-index elements (should be cleared each frame)
|
||||||
|
if MemoryScanner._Context._immediateMode then
|
||||||
|
-- In immediate mode, _zIndexOrderedElements should be cleared at frame start
|
||||||
|
-- If it has elements outside of frame rendering, that's a leak
|
||||||
|
if not MemoryScanner._Context._frameStarted and report.zIndexElementCount > 0 then
|
||||||
|
table.insert(report.issues, {
|
||||||
|
severity = "warning",
|
||||||
|
message = string.format("Z-index array has %d elements outside of frame", report.zIndexElementCount),
|
||||||
|
suggestion = "clearFrameElements() may not be called properly in beginFrame()",
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Check for excessive element count
|
||||||
|
if report.topElementCount > 100 then
|
||||||
|
table.insert(report.issues, {
|
||||||
|
severity = "info",
|
||||||
|
message = string.format("High top-level element count: %d", report.topElementCount),
|
||||||
|
suggestion = "Consider consolidating elements or using fewer top-level containers",
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
return report
|
||||||
|
end
|
||||||
|
|
||||||
|
---Scan ImageCache for memory issues
|
||||||
|
---@return table report Detailed report of ImageCache memory usage
|
||||||
|
function MemoryScanner.scanImageCache()
|
||||||
|
local report = {
|
||||||
|
imageCount = 0,
|
||||||
|
estimatedMemory = 0,
|
||||||
|
issues = {},
|
||||||
|
}
|
||||||
|
|
||||||
|
if not MemoryScanner._ImageCache then
|
||||||
|
table.insert(report.issues, {
|
||||||
|
severity = "error",
|
||||||
|
message = "ImageCache not initialized",
|
||||||
|
})
|
||||||
|
return report
|
||||||
|
end
|
||||||
|
|
||||||
|
local stats = MemoryScanner._ImageCache.getStats()
|
||||||
|
report.imageCount = stats.count
|
||||||
|
report.estimatedMemory = stats.memoryEstimate
|
||||||
|
|
||||||
|
-- Check for excessive memory usage (>100MB)
|
||||||
|
if report.estimatedMemory > 100 * 1024 * 1024 then
|
||||||
|
table.insert(report.issues, {
|
||||||
|
severity = "warning",
|
||||||
|
message = string.format("ImageCache using ~%.2f MB", report.estimatedMemory / 1024 / 1024),
|
||||||
|
suggestion = "Consider implementing cache eviction or clearing unused images",
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Check for excessive image count
|
||||||
|
if report.imageCount > 50 then
|
||||||
|
table.insert(report.issues, {
|
||||||
|
severity = "info",
|
||||||
|
message = string.format("ImageCache has %d images", report.imageCount),
|
||||||
|
suggestion = "Review if all cached images are necessary",
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
return report
|
||||||
|
end
|
||||||
|
|
||||||
|
---Check if a circular reference is intentional (parent-child, module, or metatable)
|
||||||
|
---@param path string The current path where circular ref was detected
|
||||||
|
---@param originalPath string The original path where the table was first seen
|
||||||
|
---@return boolean True if this is an intentional circular reference
|
||||||
|
local function isIntentionalCircularReference(path, originalPath)
|
||||||
|
-- Pattern 1: child.parent points back to parent
|
||||||
|
-- Example: "topElements.1.children.1.parent" -> "topElements.1"
|
||||||
|
if path:match("%.parent$") then
|
||||||
|
local parentPath = path:match("^(.+)%.children%.[^.]+%.parent$")
|
||||||
|
if parentPath == originalPath then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Pattern 2: parent.children[n] points to child, child points back somewhere in parent tree
|
||||||
|
-- Example: "topElements.1" -> "topElements.1.children.1.parent"
|
||||||
|
if originalPath:match("%.parent$") then
|
||||||
|
local childParentPath = originalPath:match("^(.+)%.children%.[^.]+%.parent$")
|
||||||
|
if childParentPath == path then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Pattern 3: Check for nested parent-child cycles
|
||||||
|
-- child.children[n].parent -> child
|
||||||
|
local segments = {}
|
||||||
|
for segment in path:gmatch("[^.]+") do
|
||||||
|
table.insert(segments, segment)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Look for .children.N.parent pattern
|
||||||
|
for i = 1, #segments - 2 do
|
||||||
|
if segments[i] == "children" and segments[i + 2] == "parent" then
|
||||||
|
-- Reconstruct path without the .children.N.parent suffix
|
||||||
|
local reconstructedPath = table.concat(segments, ".", 1, i - 1)
|
||||||
|
if reconstructedPath == originalPath then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Pattern 4: Metatable __index self-references (modules)
|
||||||
|
-- Example: "element._renderer._Theme.__index" -> "element._renderer._Theme"
|
||||||
|
if path:match("%.__index$") then
|
||||||
|
local basePath = path:match("^(.+)%.__index$")
|
||||||
|
if basePath == originalPath then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Pattern 5: Shared module references (elements sharing same module instances)
|
||||||
|
-- Example: Multiple elements referencing _utils, _Theme, _Blur, etc.
|
||||||
|
-- These start with _ and are typically modules
|
||||||
|
local pathModuleName = path:match("%.(_[%w]+)%.")
|
||||||
|
local originalModuleName = originalPath:match("%.(_[%w]+)%.")
|
||||||
|
|
||||||
|
if pathModuleName and originalModuleName then
|
||||||
|
-- If both paths reference the same internal module (starting with _), it's intentional
|
||||||
|
if pathModuleName == originalModuleName then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Pattern 6: Shared Color/Transform objects between elements
|
||||||
|
-- These are value objects that can be safely shared
|
||||||
|
if path:match("Color") and originalPath:match("Color") then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
if path:match("Transform") and originalPath:match("Transform") then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Pattern 7: LayoutEngine holding reference to its element
|
||||||
|
-- Example: "element._layoutEngine.element" -> "element"
|
||||||
|
if path:match("%._layoutEngine%.element$") then
|
||||||
|
local elementPath = path:match("^(.+)%._layoutEngine%.element$")
|
||||||
|
if elementPath == originalPath then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Pattern 8: Renderer holding references to element properties
|
||||||
|
-- Example: "element._renderer.cornerRadius" -> "element.cornerRadius"
|
||||||
|
if path:match("%._renderer%.") then
|
||||||
|
local rendererBasePath = path:match("^(.+)%._renderer%.")
|
||||||
|
local originalBasePath = originalPath:match("^(.+)%.")
|
||||||
|
if rendererBasePath == originalBasePath then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Pattern 9: Context reference from layout engine (shared singleton)
|
||||||
|
-- Example: "element._layoutEngine._Context.topElements" -> "topElements"
|
||||||
|
if path:match("%._layoutEngine%._Context%.") and originalPath == "topElements" then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
---Detect circular references in a table
|
||||||
|
---@param tbl table Table to check
|
||||||
|
---@param path string? Current path (for reporting)
|
||||||
|
---@param visited table? Tracking table
|
||||||
|
---@return table[] circularRefs Array of circular reference paths
|
||||||
|
---@return table[] intentionalRefs Array of intentional parent-child refs
|
||||||
|
local function detectCircularReferences(tbl, path, visited)
|
||||||
|
if type(tbl) ~= "table" then
|
||||||
|
return {}, {}
|
||||||
|
end
|
||||||
|
|
||||||
|
path = path or "root"
|
||||||
|
visited = visited or {}
|
||||||
|
local circularRefs = {}
|
||||||
|
local intentionalRefs = {}
|
||||||
|
|
||||||
|
-- Check if we've seen this table before
|
||||||
|
if visited[tbl] then
|
||||||
|
local ref = {
|
||||||
|
path = path,
|
||||||
|
originalPath = visited[tbl],
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Determine if this is an intentional circular reference
|
||||||
|
if isIntentionalCircularReference(path, visited[tbl]) then
|
||||||
|
table.insert(intentionalRefs, ref)
|
||||||
|
else
|
||||||
|
table.insert(circularRefs, ref)
|
||||||
|
end
|
||||||
|
|
||||||
|
return circularRefs, intentionalRefs
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Mark as visited
|
||||||
|
visited[tbl] = path
|
||||||
|
|
||||||
|
-- Recursively check children
|
||||||
|
for k, v in pairs(tbl) do
|
||||||
|
if type(v) == "table" then
|
||||||
|
local childPath = path .. "." .. tostring(k)
|
||||||
|
local childRefs, childIntentionalRefs = detectCircularReferences(v, childPath, visited)
|
||||||
|
for _, ref in ipairs(childRefs) do
|
||||||
|
table.insert(circularRefs, ref)
|
||||||
|
end
|
||||||
|
for _, ref in ipairs(childIntentionalRefs) do
|
||||||
|
table.insert(intentionalRefs, ref)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return circularRefs, intentionalRefs
|
||||||
|
end
|
||||||
|
|
||||||
|
---Scan for circular references in immediate mode
|
||||||
|
---@return table report Detailed report of circular references
|
||||||
|
function MemoryScanner.scanCircularReferences()
|
||||||
|
local report = {
|
||||||
|
stateStoreCircularRefs = {},
|
||||||
|
stateStoreIntentionalRefs = {},
|
||||||
|
contextCircularRefs = {},
|
||||||
|
contextIntentionalRefs = {},
|
||||||
|
issues = {},
|
||||||
|
}
|
||||||
|
|
||||||
|
if MemoryScanner._StateManager then
|
||||||
|
local internal = MemoryScanner._StateManager._getInternalState()
|
||||||
|
report.stateStoreCircularRefs, report.stateStoreIntentionalRefs = detectCircularReferences(internal.stateStore, "stateStore")
|
||||||
|
end
|
||||||
|
|
||||||
|
if MemoryScanner._Context then
|
||||||
|
report.contextCircularRefs, report.contextIntentionalRefs = detectCircularReferences(MemoryScanner._Context.topElements, "topElements")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Report issues only for cross-module circular references
|
||||||
|
if #report.stateStoreCircularRefs > 0 then
|
||||||
|
table.insert(report.issues, {
|
||||||
|
severity = "info",
|
||||||
|
message = string.format("Found %d cross-module circular references in StateManager", #report.stateStoreCircularRefs),
|
||||||
|
suggestion = "These are typically architectural dependencies between modules, not memory leaks",
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
if #report.contextCircularRefs > 0 then
|
||||||
|
table.insert(report.issues, {
|
||||||
|
severity = "info",
|
||||||
|
message = string.format("Found %d cross-module circular references in Context", #report.contextCircularRefs),
|
||||||
|
suggestion = "These are typically architectural dependencies (e.g., layout engine ↔ renderer), not memory leaks",
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
return report
|
||||||
|
end
|
||||||
|
|
||||||
|
---Run comprehensive memory scan
|
||||||
|
---@return table report Complete memory analysis report
|
||||||
|
function MemoryScanner.scan()
|
||||||
|
local startMemory = collectgarbage("count")
|
||||||
|
|
||||||
|
local report = {
|
||||||
|
timestamp = os.time(),
|
||||||
|
startMemory = startMemory / 1024, -- MB
|
||||||
|
stateManager = MemoryScanner.scanStateManager(),
|
||||||
|
context = MemoryScanner.scanContext(),
|
||||||
|
imageCache = MemoryScanner.scanImageCache(),
|
||||||
|
circularRefs = MemoryScanner.scanCircularReferences(),
|
||||||
|
summary = {
|
||||||
|
totalIssues = 0,
|
||||||
|
criticalIssues = 0,
|
||||||
|
warnings = 0,
|
||||||
|
info = 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Count issues by severity
|
||||||
|
local function countIssues(subReport)
|
||||||
|
for _, issue in ipairs(subReport.issues or {}) do
|
||||||
|
report.summary.totalIssues = report.summary.totalIssues + 1
|
||||||
|
if issue.severity == "error" then
|
||||||
|
report.summary.criticalIssues = report.summary.criticalIssues + 1
|
||||||
|
elseif issue.severity == "warning" then
|
||||||
|
report.summary.warnings = report.summary.warnings + 1
|
||||||
|
elseif issue.severity == "info" then
|
||||||
|
report.summary.info = report.summary.info + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
countIssues(report.stateManager)
|
||||||
|
countIssues(report.context)
|
||||||
|
countIssues(report.imageCache)
|
||||||
|
countIssues(report.circularRefs)
|
||||||
|
|
||||||
|
-- Force GC and measure freed memory
|
||||||
|
local beforeGC = collectgarbage("count")
|
||||||
|
collectgarbage("collect")
|
||||||
|
collectgarbage("collect")
|
||||||
|
local afterGC = collectgarbage("count")
|
||||||
|
|
||||||
|
report.gcAnalysis = {
|
||||||
|
beforeGC = beforeGC / 1024, -- MB
|
||||||
|
afterGC = afterGC / 1024, -- MB
|
||||||
|
freed = (beforeGC - afterGC) / 1024, -- MB
|
||||||
|
freedPercent = ((beforeGC - afterGC) / beforeGC) * 100,
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Analyze GC effectiveness
|
||||||
|
if report.gcAnalysis.freedPercent < 5 then
|
||||||
|
table.insert(report.stateManager.issues, {
|
||||||
|
severity = "info",
|
||||||
|
message = string.format("GC freed only %.1f%% of memory", report.gcAnalysis.freedPercent),
|
||||||
|
suggestion = "Most memory is still referenced - this is normal if UI is active",
|
||||||
|
})
|
||||||
|
elseif report.gcAnalysis.freedPercent > 30 then
|
||||||
|
table.insert(report.stateManager.issues, {
|
||||||
|
severity = "warning",
|
||||||
|
message = string.format("GC freed %.1f%% of memory", report.gcAnalysis.freedPercent),
|
||||||
|
suggestion = "Significant memory was unreferenced - may indicate cleanup issues",
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
return report
|
||||||
|
end
|
||||||
|
|
||||||
|
---Format report as human-readable string
|
||||||
|
---@param report table Memory scan report
|
||||||
|
---@return string formatted Formatted report
|
||||||
|
function MemoryScanner.formatReport(report)
|
||||||
|
local lines = {}
|
||||||
|
|
||||||
|
table.insert(lines, "=== FlexLöve Memory Scanner Report ===")
|
||||||
|
table.insert(lines, string.format("Timestamp: %s", os.date("%Y-%m-%d %H:%M:%S", report.timestamp)))
|
||||||
|
table.insert(lines, string.format("Memory: %.2f MB", report.startMemory))
|
||||||
|
table.insert(lines, "")
|
||||||
|
|
||||||
|
-- Summary
|
||||||
|
table.insert(lines, "--- Summary ---")
|
||||||
|
table.insert(lines, string.format("Total Issues: %d", report.summary.totalIssues))
|
||||||
|
table.insert(lines, string.format(" Critical: %d", report.summary.criticalIssues))
|
||||||
|
table.insert(lines, string.format(" Warnings: %d", report.summary.warnings))
|
||||||
|
table.insert(lines, string.format(" Info: %d", report.summary.info))
|
||||||
|
table.insert(lines, "")
|
||||||
|
|
||||||
|
-- StateManager
|
||||||
|
table.insert(lines, "--- StateManager ---")
|
||||||
|
table.insert(lines, string.format("State Count: %d", report.stateManager.stateCount))
|
||||||
|
table.insert(lines, string.format("State Store Size: %.2f KB", report.stateManager.stateStoreSize / 1024))
|
||||||
|
table.insert(lines, string.format("Metadata Size: %.2f KB", report.stateManager.metadataSize / 1024))
|
||||||
|
table.insert(lines, string.format("CallSite Counters: %.2f KB", report.stateManager.callSiteCounterSize / 1024))
|
||||||
|
table.insert(lines, string.format("Orphaned States: %d", #report.stateManager.orphanedStates))
|
||||||
|
table.insert(lines, string.format("Stale States: %d", #report.stateManager.staleStates))
|
||||||
|
table.insert(lines, string.format("Large States: %d", #report.stateManager.largeStates))
|
||||||
|
|
||||||
|
if #report.stateManager.issues > 0 then
|
||||||
|
table.insert(lines, "Issues:")
|
||||||
|
for _, issue in ipairs(report.stateManager.issues) do
|
||||||
|
table.insert(lines, string.format(" [%s] %s", string.upper(issue.severity), issue.message))
|
||||||
|
if issue.suggestion then
|
||||||
|
table.insert(lines, string.format(" → %s", issue.suggestion))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.insert(lines, "")
|
||||||
|
|
||||||
|
-- Context
|
||||||
|
table.insert(lines, "--- Context ---")
|
||||||
|
table.insert(lines, string.format("Top Elements: %d", report.context.topElementCount))
|
||||||
|
table.insert(lines, string.format("Z-Index Elements: %d", report.context.zIndexElementCount))
|
||||||
|
table.insert(lines, string.format("Frame Elements: %d", report.context.frameElementCount))
|
||||||
|
|
||||||
|
if #report.context.issues > 0 then
|
||||||
|
table.insert(lines, "Issues:")
|
||||||
|
for _, issue in ipairs(report.context.issues) do
|
||||||
|
table.insert(lines, string.format(" [%s] %s", string.upper(issue.severity), issue.message))
|
||||||
|
if issue.suggestion then
|
||||||
|
table.insert(lines, string.format(" → %s", issue.suggestion))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.insert(lines, "")
|
||||||
|
|
||||||
|
-- ImageCache
|
||||||
|
table.insert(lines, "--- ImageCache ---")
|
||||||
|
table.insert(lines, string.format("Image Count: %d", report.imageCache.imageCount))
|
||||||
|
table.insert(lines, string.format("Estimated Memory: %.2f MB", report.imageCache.estimatedMemory / 1024 / 1024))
|
||||||
|
|
||||||
|
if #report.imageCache.issues > 0 then
|
||||||
|
table.insert(lines, "Issues:")
|
||||||
|
for _, issue in ipairs(report.imageCache.issues) do
|
||||||
|
table.insert(lines, string.format(" [%s] %s", string.upper(issue.severity), issue.message))
|
||||||
|
if issue.suggestion then
|
||||||
|
table.insert(lines, string.format(" → %s", issue.suggestion))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.insert(lines, "")
|
||||||
|
|
||||||
|
-- Circular References
|
||||||
|
table.insert(lines, "--- Circular References ---")
|
||||||
|
table.insert(lines, string.format("StateStore (Cross-module refs): %d", #report.circularRefs.stateStoreCircularRefs))
|
||||||
|
table.insert(lines, string.format("StateStore (Intentional - parent-child, modules, metatables): %d", #report.circularRefs.stateStoreIntentionalRefs))
|
||||||
|
table.insert(lines, string.format("Context (Cross-module refs): %d", #report.circularRefs.contextCircularRefs))
|
||||||
|
table.insert(lines, string.format("Context (Intentional - parent-child, modules, metatables): %d", #report.circularRefs.contextIntentionalRefs))
|
||||||
|
|
||||||
|
if #report.circularRefs.issues > 0 then
|
||||||
|
table.insert(lines, "Issues:")
|
||||||
|
for _, issue in ipairs(report.circularRefs.issues) do
|
||||||
|
table.insert(lines, string.format(" [%s] %s", string.upper(issue.severity), issue.message))
|
||||||
|
if issue.suggestion then
|
||||||
|
table.insert(lines, string.format(" → %s", issue.suggestion))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
table.insert(lines, " ✓ No unexpected circular references detected")
|
||||||
|
end
|
||||||
|
table.insert(lines, " Note: Cross-module refs are typically architectural dependencies, not memory leaks")
|
||||||
|
table.insert(lines, "")
|
||||||
|
|
||||||
|
-- GC Analysis
|
||||||
|
table.insert(lines, "--- Garbage Collection Analysis ---")
|
||||||
|
table.insert(lines, string.format("Before GC: %.2f MB", report.gcAnalysis.beforeGC))
|
||||||
|
table.insert(lines, string.format("After GC: %.2f MB", report.gcAnalysis.afterGC))
|
||||||
|
table.insert(lines, string.format("Freed: %.2f MB (%.1f%%)", report.gcAnalysis.freed, report.gcAnalysis.freedPercent))
|
||||||
|
table.insert(lines, "")
|
||||||
|
|
||||||
|
table.insert(lines, "=== End Report ===")
|
||||||
|
|
||||||
|
return table.concat(lines, "\n")
|
||||||
|
end
|
||||||
|
|
||||||
|
---Save report to file
|
||||||
|
---@param report table Memory scan report
|
||||||
|
---@param filename string? Output filename (default: memory_report.txt)
|
||||||
|
function MemoryScanner.saveReport(report, filename)
|
||||||
|
filename = filename or "memory_report.txt"
|
||||||
|
local formatted = MemoryScanner.formatReport(report)
|
||||||
|
|
||||||
|
local file = io.open(filename, "w")
|
||||||
|
if file then
|
||||||
|
file:write(formatted)
|
||||||
|
file:close()
|
||||||
|
print(string.format("[MemoryScanner] Report saved to %s", filename))
|
||||||
|
else
|
||||||
|
print(string.format("[MemoryScanner] Failed to save report to %s", filename))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return MemoryScanner
|
||||||
@@ -62,9 +62,6 @@ function Renderer.new(config, deps)
|
|||||||
self._FONT_CACHE = deps.utils.FONT_CACHE
|
self._FONT_CACHE = deps.utils.FONT_CACHE
|
||||||
self._TextAlign = deps.utils.enums.TextAlign
|
self._TextAlign = deps.utils.enums.TextAlign
|
||||||
|
|
||||||
-- Store reference to parent element (will be set via initialize)
|
|
||||||
self._element = nil
|
|
||||||
|
|
||||||
-- Visual properties
|
-- Visual properties
|
||||||
self.backgroundColor = config.backgroundColor or Color.new(0, 0, 0, 0)
|
self.backgroundColor = config.backgroundColor or Color.new(0, 0, 0, 0)
|
||||||
self.borderColor = config.borderColor or Color.new(0, 0, 0, 1)
|
self.borderColor = config.borderColor or Color.new(0, 0, 0, 1)
|
||||||
@@ -123,11 +120,7 @@ function Renderer.new(config, deps)
|
|||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Initialize renderer with parent element reference
|
|
||||||
---@param element table The parent Element instance
|
|
||||||
function Renderer:initialize(element)
|
|
||||||
self._element = element
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Get or create blur instance for this element
|
--- Get or create blur instance for this element
|
||||||
---@return table|nil Blur instance or nil
|
---@return table|nil Blur instance or nil
|
||||||
@@ -204,10 +197,17 @@ function Renderer:_drawImage(x, y, paddingLeft, paddingTop, contentWidth, conten
|
|||||||
local finalOpacity = self.opacity * self.imageOpacity
|
local finalOpacity = self.opacity * self.imageOpacity
|
||||||
|
|
||||||
-- Apply cornerRadius clipping if set
|
-- Apply cornerRadius clipping if set
|
||||||
local hasCornerRadius = self.cornerRadius.topLeft > 0
|
local hasCornerRadius = false
|
||||||
|
if self.cornerRadius then
|
||||||
|
if type(self.cornerRadius) == "number" then
|
||||||
|
hasCornerRadius = self.cornerRadius > 0
|
||||||
|
else
|
||||||
|
hasCornerRadius = self.cornerRadius.topLeft > 0
|
||||||
or self.cornerRadius.topRight > 0
|
or self.cornerRadius.topRight > 0
|
||||||
or self.cornerRadius.bottomLeft > 0
|
or self.cornerRadius.bottomLeft > 0
|
||||||
or self.cornerRadius.bottomRight > 0
|
or self.cornerRadius.bottomRight > 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
if hasCornerRadius then
|
if hasCornerRadius then
|
||||||
-- Use stencil to clip image to rounded corners
|
-- Use stencil to clip image to rounded corners
|
||||||
@@ -221,15 +221,21 @@ function Renderer:_drawImage(x, y, paddingLeft, paddingTop, contentWidth, conten
|
|||||||
if not success then
|
if not success then
|
||||||
-- Check if it's a stencil buffer error
|
-- Check if it's a stencil buffer error
|
||||||
if err and err:match("stencil") then
|
if err and err:match("stencil") then
|
||||||
Renderer._ErrorHandler:warn("Renderer", "IMG_001", "Cannot apply corner radius to image: stencil buffer not available", {
|
local cornerRadiusStr
|
||||||
imagePath = self.imagePath or "unknown",
|
if type(self.cornerRadius) == "number" then
|
||||||
cornerRadius = string.format(
|
cornerRadiusStr = tostring(self.cornerRadius)
|
||||||
|
else
|
||||||
|
cornerRadiusStr = string.format(
|
||||||
"TL:%d TR:%d BL:%d BR:%d",
|
"TL:%d TR:%d BL:%d BR:%d",
|
||||||
self.cornerRadius.topLeft,
|
self.cornerRadius.topLeft,
|
||||||
self.cornerRadius.topRight,
|
self.cornerRadius.topRight,
|
||||||
self.cornerRadius.bottomLeft,
|
self.cornerRadius.bottomLeft,
|
||||||
self.cornerRadius.bottomRight
|
self.cornerRadius.bottomRight
|
||||||
),
|
)
|
||||||
|
end
|
||||||
|
Renderer._ErrorHandler:warn("Renderer", "IMG_001", "Cannot apply corner radius to image: stencil buffer not available", {
|
||||||
|
imagePath = self.imagePath or "unknown",
|
||||||
|
cornerRadius = cornerRadiusStr,
|
||||||
error = tostring(err),
|
error = tostring(err),
|
||||||
}, "Ensure the active canvas has stencil=true enabled, or remove cornerRadius from images")
|
}, "Ensure the active canvas has stencil=true enabled, or remove cornerRadius from images")
|
||||||
-- Continue without corner radius
|
-- Continue without corner radius
|
||||||
@@ -330,6 +336,19 @@ end
|
|||||||
---@param borderBoxWidth number Border box width
|
---@param borderBoxWidth number Border box width
|
||||||
---@param borderBoxHeight number Border box height
|
---@param borderBoxHeight number Border box height
|
||||||
function Renderer:_drawBorders(x, y, borderBoxWidth, borderBoxHeight)
|
function Renderer:_drawBorders(x, y, borderBoxWidth, borderBoxHeight)
|
||||||
|
-- OPTIMIZATION: Early exit if no border (nil or all false)
|
||||||
|
if not self.border then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Handle border as number (uniform border width)
|
||||||
|
if type(self.border) == "number" then
|
||||||
|
local borderColorWithOpacity = self._Color.new(self.borderColor.r, self.borderColor.g, self.borderColor.b, self.borderColor.a * self.opacity)
|
||||||
|
love.graphics.setColor(borderColorWithOpacity:toRGBA())
|
||||||
|
self._RoundedRect.draw("line", x, y, borderBoxWidth, borderBoxHeight, self.cornerRadius)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
local borderColorWithOpacity = self._Color.new(self.borderColor.r, self.borderColor.g, self.borderColor.b, self.borderColor.a * self.opacity)
|
local borderColorWithOpacity = self._Color.new(self.borderColor.r, self.borderColor.g, self.borderColor.b, self.borderColor.a * self.opacity)
|
||||||
love.graphics.setColor(borderColorWithOpacity:toRGBA())
|
love.graphics.setColor(borderColorWithOpacity:toRGBA())
|
||||||
|
|
||||||
@@ -357,12 +376,20 @@ function Renderer:_drawBorders(x, y, borderBoxWidth, borderBoxHeight)
|
|||||||
end
|
end
|
||||||
|
|
||||||
--- Main draw method - renders all visual layers
|
--- Main draw method - renders all visual layers
|
||||||
|
---@param element Element The parent Element instance
|
||||||
---@param backdropCanvas table|nil Backdrop canvas for backdrop blur
|
---@param backdropCanvas table|nil Backdrop canvas for backdrop blur
|
||||||
function Renderer:draw(backdropCanvas)
|
function Renderer:draw(element, backdropCanvas)
|
||||||
|
if not element then
|
||||||
|
Renderer._ErrorHandler:warn("Renderer", "SYS_002", "Element parameter required", {
|
||||||
|
method = "draw",
|
||||||
|
}, "Pass element as first parameter to draw()")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
-- Start performance timing
|
-- Start performance timing
|
||||||
local elementId
|
local elementId
|
||||||
if Renderer._Performance and Renderer._Performance.enabled and self._element then
|
if Renderer._Performance and Renderer._Performance.enabled and element then
|
||||||
elementId = self._element.id or "unnamed"
|
elementId = element.id or "unnamed"
|
||||||
Renderer._Performance:startTimer("render_" .. elementId)
|
Renderer._Performance:startTimer("render_" .. elementId)
|
||||||
Renderer._Performance:incrementCounter("draw_calls", 1)
|
Renderer._Performance:incrementCounter("draw_calls", 1)
|
||||||
end
|
end
|
||||||
@@ -375,16 +402,6 @@ function Renderer:draw(backdropCanvas)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Element must be initialized before drawing
|
|
||||||
if not self._element then
|
|
||||||
Renderer._ErrorHandler:warn("Renderer", "SYS_002", "Method called before initialization", {
|
|
||||||
method = "draw",
|
|
||||||
}, "Call renderer:initialize(element) before rendering")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local element = self._element
|
|
||||||
|
|
||||||
-- Handle opacity during animation
|
-- Handle opacity during animation
|
||||||
local drawBackgroundColor = self.backgroundColor
|
local drawBackgroundColor = self.backgroundColor
|
||||||
if element.animation then
|
if element.animation then
|
||||||
@@ -641,8 +658,8 @@ end
|
|||||||
function Renderer:drawText(element)
|
function Renderer:drawText(element)
|
||||||
-- Update text layout if dirty (for multiline auto-grow)
|
-- Update text layout if dirty (for multiline auto-grow)
|
||||||
if element._textEditor then
|
if element._textEditor then
|
||||||
element._textEditor:_updateTextIfDirty()
|
element._textEditor:_updateTextIfDirty(element)
|
||||||
element._textEditor:updateAutoGrowHeight()
|
element._textEditor:updateAutoGrowHeight(element)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- For editable elements, use TextEditor buffer; for non-editable, use text
|
-- For editable elements, use TextEditor buffer; for non-editable, use text
|
||||||
@@ -762,7 +779,7 @@ function Renderer:drawText(element)
|
|||||||
love.graphics.setColor(cursorWithOpacity:toRGBA())
|
love.graphics.setColor(cursorWithOpacity:toRGBA())
|
||||||
|
|
||||||
-- Calculate cursor position using TextEditor method
|
-- Calculate cursor position using TextEditor method
|
||||||
local cursorRelX, cursorRelY = element._textEditor:_getCursorScreenPosition()
|
local cursorRelX, cursorRelY = element._textEditor:_getCursorScreenPosition(element)
|
||||||
local cursorX = contentX + cursorRelX
|
local cursorX = contentX + cursorRelX
|
||||||
local cursorY = contentY + cursorRelY
|
local cursorY = contentY + cursorRelY
|
||||||
local cursorHeight = textHeight
|
local cursorHeight = textHeight
|
||||||
@@ -793,7 +810,7 @@ function Renderer:drawText(element)
|
|||||||
local selectionWithOpacity = self._Color.new(selectionColor.r, selectionColor.g, selectionColor.b, selectionColor.a * self.opacity)
|
local selectionWithOpacity = self._Color.new(selectionColor.r, selectionColor.g, selectionColor.b, selectionColor.a * self.opacity)
|
||||||
|
|
||||||
-- Get selection rectangles from TextEditor
|
-- Get selection rectangles from TextEditor
|
||||||
local selectionRects = element._textEditor:_getSelectionRects(selStart, selEnd)
|
local selectionRects = element._textEditor:_getSelectionRects(element, selStart, selEnd)
|
||||||
|
|
||||||
-- Apply scissor for single-line editable inputs
|
-- Apply scissor for single-line editable inputs
|
||||||
if not element.multiline then
|
if not element.multiline then
|
||||||
@@ -940,9 +957,16 @@ end
|
|||||||
|
|
||||||
--- Cleanup renderer resources
|
--- Cleanup renderer resources
|
||||||
function Renderer:destroy()
|
function Renderer:destroy()
|
||||||
self._element = nil
|
|
||||||
self._loadedImage = nil
|
self._loadedImage = nil
|
||||||
self._blurInstance = nil
|
self._blurInstance = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
--- Cleanup method to break circular references (for immediate mode)
|
||||||
|
function Renderer:_cleanup()
|
||||||
|
-- Renderer doesn't create circular references (no element back-reference)
|
||||||
|
-- Module refs are singletons
|
||||||
|
-- In immediate mode, full element cleanup happens via array clearing
|
||||||
|
end
|
||||||
|
|
||||||
return Renderer
|
return Renderer
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ local RoundedRect = {}
|
|||||||
---@param y number
|
---@param y number
|
||||||
---@param width number
|
---@param width number
|
||||||
---@param height number
|
---@param height number
|
||||||
---@param cornerRadius {topLeft:number, topRight:number, bottomLeft:number, bottomRight:number}
|
---@param cornerRadius {topLeft:number, topRight:number, bottomLeft:number, bottomRight:number}|number
|
||||||
---@param segments number? -- Number of segments per corner arc (default: 10)
|
---@param segments number? -- Number of segments per corner arc (default: 10)
|
||||||
---@return table -- Array of vertices for love.graphics.polygon
|
---@return table -- Array of vertices for love.graphics.polygon
|
||||||
function RoundedRect.getPoints(x, y, width, height, cornerRadius, segments)
|
function RoundedRect.getPoints(x, y, width, height, cornerRadius, segments)
|
||||||
@@ -27,6 +27,16 @@ function RoundedRect.getPoints(x, y, width, height, cornerRadius, segments)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Handle uniform corner radius (number)
|
||||||
|
if type(cornerRadius) == "number" then
|
||||||
|
cornerRadius = {
|
||||||
|
topLeft = cornerRadius,
|
||||||
|
topRight = cornerRadius,
|
||||||
|
bottomLeft = cornerRadius,
|
||||||
|
bottomRight = cornerRadius
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
local r1 = math.min(cornerRadius.topLeft, width / 2, height / 2)
|
local r1 = math.min(cornerRadius.topLeft, width / 2, height / 2)
|
||||||
local r2 = math.min(cornerRadius.topRight, width / 2, height / 2)
|
local r2 = math.min(cornerRadius.topRight, width / 2, height / 2)
|
||||||
local r3 = math.min(cornerRadius.bottomRight, width / 2, height / 2)
|
local r3 = math.min(cornerRadius.bottomRight, width / 2, height / 2)
|
||||||
@@ -53,8 +63,29 @@ end
|
|||||||
---@param y number
|
---@param y number
|
||||||
---@param width number
|
---@param width number
|
||||||
---@param height number
|
---@param height number
|
||||||
---@param cornerRadius {topLeft:number, topRight:number, bottomLeft:number, bottomRight:number}
|
---@param cornerRadius {topLeft:number, topRight:number, bottomLeft:number, bottomRight:number}|number|nil
|
||||||
function RoundedRect.draw(mode, x, y, width, height, cornerRadius)
|
function RoundedRect.draw(mode, x, y, width, height, cornerRadius)
|
||||||
|
-- OPTIMIZATION: Handle nil cornerRadius (no rounding)
|
||||||
|
if not cornerRadius then
|
||||||
|
love.graphics.rectangle(mode, x, y, width, height)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Handle uniform corner radius (number)
|
||||||
|
if type(cornerRadius) == "number" then
|
||||||
|
if cornerRadius <= 0 then
|
||||||
|
love.graphics.rectangle(mode, x, y, width, height)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
-- Convert to table format for processing
|
||||||
|
cornerRadius = {
|
||||||
|
topLeft = cornerRadius,
|
||||||
|
topRight = cornerRadius,
|
||||||
|
bottomLeft = cornerRadius,
|
||||||
|
bottomRight = cornerRadius
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
-- Check if any corners are rounded
|
-- Check if any corners are rounded
|
||||||
local hasRoundedCorners = cornerRadius.topLeft > 0 or cornerRadius.topRight > 0 or cornerRadius.bottomLeft > 0 or cornerRadius.bottomRight > 0
|
local hasRoundedCorners = cornerRadius.topLeft > 0 or cornerRadius.topRight > 0 or cornerRadius.bottomLeft > 0 or cornerRadius.bottomRight > 0
|
||||||
|
|
||||||
@@ -79,7 +110,7 @@ end
|
|||||||
---@param y number
|
---@param y number
|
||||||
---@param width number
|
---@param width number
|
||||||
---@param height number
|
---@param height number
|
||||||
---@param cornerRadius {topLeft:number, topRight:number, bottomLeft:number, bottomRight:number}
|
---@param cornerRadius {topLeft:number, topRight:number, bottomLeft:number, bottomRight:number}|number|nil
|
||||||
---@return function
|
---@return function
|
||||||
function RoundedRect.stencilFunction(x, y, width, height, cornerRadius)
|
function RoundedRect.stencilFunction(x, y, width, height, cornerRadius)
|
||||||
return function()
|
return function()
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
---@class ScrollManager
|
---@class ScrollManager
|
||||||
---@field overflow string -- "visible"|"hidden"|"auto"|"scroll"
|
---@field overflow string -- "visible"|"hidden"|"auto"|"scroll"
|
||||||
---@field overflowX string? -- X-axis specific overflow (overrides overflow)
|
---@field overflowX string? -- X-axis specific overflow (overrides overflow)
|
||||||
@@ -16,7 +15,6 @@
|
|||||||
---@field scrollFriction number -- Friction coefficient for momentum (0.95-0.98)
|
---@field scrollFriction number -- Friction coefficient for momentum (0.95-0.98)
|
||||||
---@field bounceStiffness number -- Bounce spring constant (0.1-0.3)
|
---@field bounceStiffness number -- Bounce spring constant (0.1-0.3)
|
||||||
---@field maxOverscroll number -- Maximum overscroll distance (pixels)
|
---@field maxOverscroll number -- Maximum overscroll distance (pixels)
|
||||||
---@field _element Element? -- Reference to parent Element (set via initialize)
|
|
||||||
---@field _overflowX boolean -- True if content overflows horizontally
|
---@field _overflowX boolean -- True if content overflows horizontally
|
||||||
---@field _overflowY boolean -- True if content overflows vertically
|
---@field _overflowY boolean -- True if content overflows vertically
|
||||||
---@field _contentWidth number -- Total content width (including overflow)
|
---@field _contentWidth number -- Total content width (including overflow)
|
||||||
@@ -117,29 +115,12 @@ function ScrollManager.new(config, deps)
|
|||||||
self._lastTouchX = 0
|
self._lastTouchX = 0
|
||||||
self._lastTouchY = 0
|
self._lastTouchY = 0
|
||||||
|
|
||||||
-- Element reference (set via initialize)
|
|
||||||
self._element = nil
|
|
||||||
|
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Initialize with parent element reference
|
|
||||||
---@param element table The parent Element instance
|
|
||||||
function ScrollManager:initialize(element)
|
|
||||||
self._element = element
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Detect if content overflows container bounds
|
--- Detect if content overflows container bounds
|
||||||
function ScrollManager:detectOverflow()
|
---@param element Element The parent Element instance
|
||||||
if not self._element then
|
function ScrollManager:detectOverflow(element)
|
||||||
ScrollManager._ErrorHandler:warn("ScrollManager", "SYS_002", "Method called before initialization", {
|
|
||||||
method = "detectOverflow"
|
|
||||||
}, "Call scrollManager:initialize(element) before using scroll methods")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local element = self._element
|
|
||||||
|
|
||||||
-- Reset overflow state
|
-- Reset overflow state
|
||||||
self._overflowX = false
|
self._overflowX = false
|
||||||
self._overflowY = false
|
self._overflowY = false
|
||||||
@@ -259,19 +240,9 @@ function ScrollManager:getContentSize()
|
|||||||
end
|
end
|
||||||
|
|
||||||
--- Calculate scrollbar dimensions and positions
|
--- Calculate scrollbar dimensions and positions
|
||||||
|
---@param element Element The parent Element instance
|
||||||
---@return table -- {vertical: {visible, trackHeight, thumbHeight, thumbY}, horizontal: {visible, trackWidth, thumbWidth, thumbX}}
|
---@return table -- {vertical: {visible, trackHeight, thumbHeight, thumbY}, horizontal: {visible, trackWidth, thumbWidth, thumbX}}
|
||||||
function ScrollManager:calculateScrollbarDimensions()
|
function ScrollManager:calculateScrollbarDimensions(element)
|
||||||
if not self._element then
|
|
||||||
ScrollManager._ErrorHandler:warn("ScrollManager", "SYS_002", "Method called before initialization", {
|
|
||||||
method = "calculateScrollbarDimensions"
|
|
||||||
}, "Call scrollManager:initialize(element) before using scroll methods")
|
|
||||||
return {
|
|
||||||
vertical = { visible = false, trackHeight = 0, thumbHeight = 0, thumbY = 0 },
|
|
||||||
horizontal = { visible = false, trackWidth = 0, thumbWidth = 0, thumbX = 0 },
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
local element = self._element
|
|
||||||
local result = {
|
local result = {
|
||||||
vertical = { visible = false, trackHeight = 0, thumbHeight = 0, thumbY = 0 },
|
vertical = { visible = false, trackHeight = 0, thumbHeight = 0, thumbY = 0 },
|
||||||
horizontal = { visible = false, trackWidth = 0, thumbWidth = 0, thumbX = 0 },
|
horizontal = { visible = false, trackWidth = 0, thumbWidth = 0, thumbX = 0 },
|
||||||
@@ -356,18 +327,11 @@ function ScrollManager:calculateScrollbarDimensions()
|
|||||||
end
|
end
|
||||||
|
|
||||||
--- Get scrollbar at mouse position
|
--- Get scrollbar at mouse position
|
||||||
|
---@param element Element The parent Element instance
|
||||||
---@param mouseX number
|
---@param mouseX number
|
||||||
---@param mouseY number
|
---@param mouseY number
|
||||||
---@return table|nil -- {component: "vertical"|"horizontal", region: "thumb"|"track"}
|
---@return table|nil -- {component: "vertical"|"horizontal", region: "thumb"|"track"}
|
||||||
function ScrollManager:getScrollbarAtPosition(mouseX, mouseY)
|
function ScrollManager:getScrollbarAtPosition(element, mouseX, mouseY)
|
||||||
if not self._element then
|
|
||||||
ScrollManager._ErrorHandler:warn("ScrollManager", "SYS_002", "Method called before initialization", {
|
|
||||||
method = "getScrollbarAtPosition"
|
|
||||||
}, "Call scrollManager:initialize(element) before using scroll methods")
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
local element = self._element
|
|
||||||
local overflowX = self.overflowX or self.overflow
|
local overflowX = self.overflowX or self.overflow
|
||||||
local overflowY = self.overflowY or self.overflow
|
local overflowY = self.overflowY or self.overflow
|
||||||
|
|
||||||
@@ -375,7 +339,7 @@ function ScrollManager:getScrollbarAtPosition(mouseX, mouseY)
|
|||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
local dims = self:calculateScrollbarDimensions()
|
local dims = self:calculateScrollbarDimensions(element)
|
||||||
local x, y = element.x, element.y
|
local x, y = element.x, element.y
|
||||||
local w, h = element.width, element.height
|
local w, h = element.width, element.height
|
||||||
|
|
||||||
@@ -427,23 +391,17 @@ function ScrollManager:getScrollbarAtPosition(mouseX, mouseY)
|
|||||||
end
|
end
|
||||||
|
|
||||||
--- Handle scrollbar mouse press
|
--- Handle scrollbar mouse press
|
||||||
|
---@param element Element The parent Element instance
|
||||||
---@param mouseX number
|
---@param mouseX number
|
||||||
---@param mouseY number
|
---@param mouseY number
|
||||||
---@param button number
|
---@param button number
|
||||||
---@return boolean -- True if event was consumed
|
---@return boolean -- True if event was consumed
|
||||||
function ScrollManager:handleMousePress(mouseX, mouseY, button)
|
function ScrollManager:handleMousePress(element, mouseX, mouseY, button)
|
||||||
if not self._element then
|
|
||||||
ScrollManager._ErrorHandler:warn("ScrollManager", "SYS_002", "Method called before initialization", {
|
|
||||||
method = "handleMousePress"
|
|
||||||
}, "Call scrollManager:initialize(element) before using scroll methods")
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
if button ~= 1 then
|
if button ~= 1 then
|
||||||
return false
|
return false
|
||||||
end -- Only left click
|
end -- Only left click
|
||||||
|
|
||||||
local scrollbar = self:getScrollbarAtPosition(mouseX, mouseY)
|
local scrollbar = self:getScrollbarAtPosition(element, mouseX, mouseY)
|
||||||
if not scrollbar then
|
if not scrollbar then
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
@@ -452,8 +410,7 @@ function ScrollManager:handleMousePress(mouseX, mouseY, button)
|
|||||||
-- Start dragging thumb
|
-- Start dragging thumb
|
||||||
self._scrollbarDragging = true
|
self._scrollbarDragging = true
|
||||||
self._hoveredScrollbar = scrollbar.component
|
self._hoveredScrollbar = scrollbar.component
|
||||||
local dims = self:calculateScrollbarDimensions()
|
local dims = self:calculateScrollbarDimensions(element)
|
||||||
local element = self._element
|
|
||||||
|
|
||||||
if scrollbar.component == "vertical" then
|
if scrollbar.component == "vertical" then
|
||||||
local contentY = element.y + element.padding.top
|
local contentY = element.y + element.padding.top
|
||||||
@@ -470,7 +427,7 @@ function ScrollManager:handleMousePress(mouseX, mouseY, button)
|
|||||||
return true -- Event consumed
|
return true -- Event consumed
|
||||||
elseif scrollbar.region == "track" then
|
elseif scrollbar.region == "track" then
|
||||||
-- Click on track - jump to position
|
-- Click on track - jump to position
|
||||||
self:_scrollToTrackPosition(mouseX, mouseY, scrollbar.component)
|
self:_scrollToTrackPosition(element, mouseX, mouseY, scrollbar.component)
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -478,20 +435,16 @@ function ScrollManager:handleMousePress(mouseX, mouseY, button)
|
|||||||
end
|
end
|
||||||
|
|
||||||
--- Handle scrollbar drag
|
--- Handle scrollbar drag
|
||||||
|
---@param element Element The parent Element instance
|
||||||
---@param mouseX number
|
---@param mouseX number
|
||||||
---@param mouseY number
|
---@param mouseY number
|
||||||
---@return boolean -- True if event was consumed
|
---@return boolean -- True if event was consumed
|
||||||
function ScrollManager:handleMouseMove(mouseX, mouseY)
|
function ScrollManager:handleMouseMove(element, mouseX, mouseY)
|
||||||
if not self._element then
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
if not self._scrollbarDragging then
|
if not self._scrollbarDragging then
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
local dims = self:calculateScrollbarDimensions()
|
local dims = self:calculateScrollbarDimensions(element)
|
||||||
local element = self._element
|
|
||||||
|
|
||||||
if self._hoveredScrollbar == "vertical" then
|
if self._hoveredScrollbar == "vertical" then
|
||||||
local contentY = element.y + element.padding.top
|
local contentY = element.y + element.padding.top
|
||||||
@@ -547,16 +500,12 @@ function ScrollManager:handleMouseRelease(button)
|
|||||||
end
|
end
|
||||||
|
|
||||||
--- Scroll to track click position (internal helper)
|
--- Scroll to track click position (internal helper)
|
||||||
|
---@param element Element The parent Element instance
|
||||||
---@param mouseX number
|
---@param mouseX number
|
||||||
---@param mouseY number
|
---@param mouseY number
|
||||||
---@param component string -- "vertical" or "horizontal"
|
---@param component string -- "vertical" or "horizontal"
|
||||||
function ScrollManager:_scrollToTrackPosition(mouseX, mouseY, component)
|
function ScrollManager:_scrollToTrackPosition(element, mouseX, mouseY, component)
|
||||||
if not self._element then
|
local dims = self:calculateScrollbarDimensions(element)
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local dims = self:calculateScrollbarDimensions()
|
|
||||||
local element = self._element
|
|
||||||
|
|
||||||
if component == "vertical" then
|
if component == "vertical" then
|
||||||
local contentY = element.y + element.padding.top
|
local contentY = element.y + element.padding.top
|
||||||
@@ -628,10 +577,11 @@ function ScrollManager:handleWheel(x, y)
|
|||||||
end
|
end
|
||||||
|
|
||||||
--- Update scrollbar hover state based on mouse position
|
--- Update scrollbar hover state based on mouse position
|
||||||
|
---@param element Element The parent Element instance
|
||||||
---@param mouseX number
|
---@param mouseX number
|
||||||
---@param mouseY number
|
---@param mouseY number
|
||||||
function ScrollManager:updateHoverState(mouseX, mouseY)
|
function ScrollManager:updateHoverState(element, mouseX, mouseY)
|
||||||
local scrollbar = self:getScrollbarAtPosition(mouseX, mouseY)
|
local scrollbar = self:getScrollbarAtPosition(element, mouseX, mouseY)
|
||||||
|
|
||||||
if scrollbar then
|
if scrollbar then
|
||||||
if scrollbar.component == "vertical" then
|
if scrollbar.component == "vertical" then
|
||||||
@@ -919,4 +869,14 @@ function ScrollManager:isMomentumScrolling()
|
|||||||
return self._momentumScrolling
|
return self._momentumScrolling
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
--- Cleanup method to break circular references (for immediate mode)
|
||||||
|
function ScrollManager:_cleanup()
|
||||||
|
-- Cleanup breaks circular references only
|
||||||
|
-- The main circular ref is: Element → ScrollManager → element → Element
|
||||||
|
-- Breaking element ref would break functionality, so we keep it
|
||||||
|
-- Module refs (_utils, _Color) are not circular, they're shared singletons
|
||||||
|
-- In immediate mode, the whole element will be GC'd anyway, so minimal cleanup needed
|
||||||
|
end
|
||||||
|
|
||||||
return ScrollManager
|
return ScrollManager
|
||||||
|
|||||||
@@ -18,10 +18,92 @@ local callSiteCounters = {}
|
|||||||
|
|
||||||
-- Configuration
|
-- Configuration
|
||||||
local config = {
|
local config = {
|
||||||
stateRetentionFrames = 60, -- Keep unused state for 60 frames (~1 second at 60fps)
|
stateRetentionFrames = 2, -- Keep unused state for 2 frames
|
||||||
maxStateEntries = 1000, -- Maximum state entries before forced GC
|
maxStateEntries = 1000, -- Maximum state entries before forced GC
|
||||||
}
|
}
|
||||||
|
|
||||||
|
-- Default state values (sparse storage - don't store these)
|
||||||
|
local stateDefaults = {
|
||||||
|
-- Interaction states
|
||||||
|
hover = false,
|
||||||
|
pressed = false,
|
||||||
|
focused = false,
|
||||||
|
disabled = false,
|
||||||
|
active = false,
|
||||||
|
|
||||||
|
-- Scrollbar states
|
||||||
|
scrollbarHoveredVertical = false,
|
||||||
|
scrollbarHoveredHorizontal = false,
|
||||||
|
scrollbarDragging = false,
|
||||||
|
hoveredScrollbar = nil,
|
||||||
|
scrollbarDragOffset = 0,
|
||||||
|
|
||||||
|
-- Scroll position
|
||||||
|
scrollX = 0,
|
||||||
|
scrollY = 0,
|
||||||
|
_scrollX = 0,
|
||||||
|
_scrollY = 0,
|
||||||
|
|
||||||
|
-- Click tracking
|
||||||
|
_clickCount = 0,
|
||||||
|
_lastClickTime = nil,
|
||||||
|
_lastClickButton = nil,
|
||||||
|
|
||||||
|
-- Internal states
|
||||||
|
_hovered = nil,
|
||||||
|
_focused = nil,
|
||||||
|
_cursorPosition = nil,
|
||||||
|
_selectionStart = nil,
|
||||||
|
_selectionEnd = nil,
|
||||||
|
_textBuffer = "",
|
||||||
|
_cursorBlinkTimer = 0,
|
||||||
|
_cursorVisible = true,
|
||||||
|
_cursorBlinkPaused = false,
|
||||||
|
_cursorBlinkPauseTimer = 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
--- Check if a value equals the default for a key
|
||||||
|
---@param key string State key
|
||||||
|
---@param value any Value to check
|
||||||
|
---@return boolean isDefault True if value equals default
|
||||||
|
local function isDefaultValue(key, value)
|
||||||
|
local defaultVal = stateDefaults[key]
|
||||||
|
|
||||||
|
-- If no default defined, check for common defaults
|
||||||
|
if defaultVal == nil then
|
||||||
|
-- Empty tables are default
|
||||||
|
if type(value) == "table" and next(value) == nil then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
-- nil values are default
|
||||||
|
if value == nil then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
-- Otherwise, not a default value
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Compare values
|
||||||
|
if type(value) == "table" then
|
||||||
|
-- Empty tables are considered default
|
||||||
|
if next(value) == nil then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
-- For other tables, compare contents (shallow)
|
||||||
|
if type(defaultVal) ~= "table" then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
for k, v in pairs(value) do
|
||||||
|
if defaultVal[k] ~= v then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
else
|
||||||
|
return value == defaultVal
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- ====================
|
-- ====================
|
||||||
-- ID Generation
|
-- ID Generation
|
||||||
-- ====================
|
-- ====================
|
||||||
@@ -190,120 +272,27 @@ function StateManager.getState(id, defaultState)
|
|||||||
end
|
end
|
||||||
ErrorHandler.error("StateManager", "SYS_001", "Invalid state ID", {
|
ErrorHandler.error("StateManager", "SYS_001", "Invalid state ID", {
|
||||||
parameter = "id",
|
parameter = "id",
|
||||||
value = "nil"
|
value = "nil",
|
||||||
}, "Provide a valid non-nil ID string to getState()")
|
}, "Provide a valid non-nil ID string to getState()")
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Create state if it doesn't exist
|
-- Create state if it doesn't exist
|
||||||
if not stateStore[id] then
|
if not stateStore[id] then
|
||||||
-- Merge default state with standard structure
|
-- Start with empty state (sparse storage)
|
||||||
stateStore[id] = defaultState or {}
|
stateStore[id] = defaultState or {}
|
||||||
|
|
||||||
-- Ensure all standard properties exist with defaults
|
|
||||||
local state = stateStore[id]
|
|
||||||
|
|
||||||
-- Interaction states
|
|
||||||
if state.hover == nil then
|
|
||||||
state.hover = false
|
|
||||||
end
|
|
||||||
if state.pressed == nil then
|
|
||||||
state.pressed = false
|
|
||||||
end
|
|
||||||
if state.focused == nil then
|
|
||||||
state.focused = false
|
|
||||||
end
|
|
||||||
if state.disabled == nil then
|
|
||||||
state.disabled = false
|
|
||||||
end
|
|
||||||
if state.active == nil then
|
|
||||||
state.active = false
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Scrollbar states
|
|
||||||
if state.scrollbarHoveredVertical == nil then
|
|
||||||
state.scrollbarHoveredVertical = false
|
|
||||||
end
|
|
||||||
if state.scrollbarHoveredHorizontal == nil then
|
|
||||||
state.scrollbarHoveredHorizontal = false
|
|
||||||
end
|
|
||||||
if state.scrollbarDragging == nil then
|
|
||||||
state.scrollbarDragging = false
|
|
||||||
end
|
|
||||||
if state.hoveredScrollbar == nil then
|
|
||||||
state.hoveredScrollbar = nil
|
|
||||||
end
|
|
||||||
if state.scrollbarDragOffset == nil then
|
|
||||||
state.scrollbarDragOffset = 0
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Scroll position
|
|
||||||
if state.scrollX == nil then
|
|
||||||
state.scrollX = 0
|
|
||||||
end
|
|
||||||
if state.scrollY == nil then
|
|
||||||
state.scrollY = 0
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Click tracking
|
|
||||||
if state._pressed == nil then
|
|
||||||
state._pressed = {}
|
|
||||||
end
|
|
||||||
if state._lastClickTime == nil then
|
|
||||||
state._lastClickTime = nil
|
|
||||||
end
|
|
||||||
if state._lastClickButton == nil then
|
|
||||||
state._lastClickButton = nil
|
|
||||||
end
|
|
||||||
if state._clickCount == nil then
|
|
||||||
state._clickCount = 0
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Drag tracking
|
|
||||||
if state._dragStartX == nil then
|
|
||||||
state._dragStartX = {}
|
|
||||||
end
|
|
||||||
if state._dragStartY == nil then
|
|
||||||
state._dragStartY = {}
|
|
||||||
end
|
|
||||||
if state._lastMouseX == nil then
|
|
||||||
state._lastMouseX = {}
|
|
||||||
end
|
|
||||||
if state._lastMouseY == nil then
|
|
||||||
state._lastMouseY = {}
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Input/focus state
|
|
||||||
if state._hovered == nil then
|
|
||||||
state._hovered = nil
|
|
||||||
end
|
|
||||||
if state._focused == nil then
|
|
||||||
state._focused = nil
|
|
||||||
end
|
|
||||||
if state._cursorPosition == nil then
|
|
||||||
state._cursorPosition = nil
|
|
||||||
end
|
|
||||||
if state._selectionStart == nil then
|
|
||||||
state._selectionStart = nil
|
|
||||||
end
|
|
||||||
if state._selectionEnd == nil then
|
|
||||||
state._selectionEnd = nil
|
|
||||||
end
|
|
||||||
if state._textBuffer == nil then
|
|
||||||
state._textBuffer = ""
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Create metadata
|
-- Create metadata
|
||||||
stateMetadata[id] = {
|
stateMetadata[id] = {
|
||||||
lastFrame = frameNumber,
|
lastFrame = frameNumber,
|
||||||
createdFrame = frameNumber,
|
createdFrame = frameNumber,
|
||||||
accessCount = 0,
|
accessCount = 0,
|
||||||
}
|
}
|
||||||
end
|
else
|
||||||
|
|
||||||
-- Update metadata
|
-- Update metadata
|
||||||
local meta = stateMetadata[id]
|
local meta = stateMetadata[id]
|
||||||
meta.lastFrame = frameNumber
|
meta.lastFrame = frameNumber
|
||||||
meta.accessCount = meta.accessCount + 1
|
meta.accessCount = meta.accessCount + 1
|
||||||
|
end
|
||||||
|
|
||||||
return stateStore[id]
|
return stateStore[id]
|
||||||
end
|
end
|
||||||
@@ -319,11 +308,19 @@ function StateManager.setState(id, state)
|
|||||||
end
|
end
|
||||||
ErrorHandler.error("StateManager", "SYS_001", "Invalid state ID", {
|
ErrorHandler.error("StateManager", "SYS_001", "Invalid state ID", {
|
||||||
parameter = "id",
|
parameter = "id",
|
||||||
value = "nil"
|
value = "nil",
|
||||||
}, "Provide a valid non-nil ID string to setState()")
|
}, "Provide a valid non-nil ID string to setState()")
|
||||||
end
|
end
|
||||||
|
|
||||||
stateStore[id] = state
|
-- Create sparse state (remove default values)
|
||||||
|
local sparseState = {}
|
||||||
|
for key, value in pairs(state) do
|
||||||
|
if not isDefaultValue(key, value) then
|
||||||
|
sparseState[key] = value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
stateStore[id] = sparseState
|
||||||
|
|
||||||
-- Update or create metadata
|
-- Update or create metadata
|
||||||
if not stateMetadata[id] then
|
if not stateMetadata[id] then
|
||||||
@@ -414,6 +411,15 @@ function StateManager.cleanup()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Clean up empty states (sparse storage optimization)
|
||||||
|
for id, state in pairs(stateStore) do
|
||||||
|
if next(state) == nil then
|
||||||
|
stateStore[id] = nil
|
||||||
|
stateMetadata[id] = nil
|
||||||
|
cleanedCount = cleanedCount + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
return cleanedCount
|
return cleanedCount
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -763,17 +763,10 @@ function ThemeManager.new(config)
|
|||||||
self.scalingAlgorithm = config.scalingAlgorithm
|
self.scalingAlgorithm = config.scalingAlgorithm
|
||||||
|
|
||||||
self._themeState = "normal"
|
self._themeState = "normal"
|
||||||
self._element = nil
|
|
||||||
|
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
---Initialize the ThemeManager with a parent element
|
|
||||||
---@param element Element The parent Element
|
|
||||||
function ThemeManager:initialize(element)
|
|
||||||
self._element = element
|
|
||||||
end
|
|
||||||
|
|
||||||
---Update the theme state based on element interaction state
|
---Update the theme state based on element interaction state
|
||||||
---@param isHovered boolean Whether element is hovered
|
---@param isHovered boolean Whether element is hovered
|
||||||
---@param isPressed boolean Whether element is pressed
|
---@param isPressed boolean Whether element is pressed
|
||||||
@@ -968,6 +961,13 @@ function ThemeManager:setTheme(themeName, componentName)
|
|||||||
self.themeComponent = componentName
|
self.themeComponent = componentName
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
--- Cleanup method to break circular references (for immediate mode)
|
||||||
|
function ThemeManager:_cleanup()
|
||||||
|
-- ThemeManager doesn't create circular references
|
||||||
|
-- Theme refs are to shared theme objects
|
||||||
|
end
|
||||||
|
|
||||||
-- Export both Theme and ThemeManager
|
-- Export both Theme and ThemeManager
|
||||||
Theme.Manager = ThemeManager
|
Theme.Manager = ThemeManager
|
||||||
|
|
||||||
|
|||||||
@@ -505,6 +505,7 @@ end
|
|||||||
--- @param options table? Sanitization options
|
--- @param options table? Sanitization options
|
||||||
--- @return string Sanitized text
|
--- @return string Sanitized text
|
||||||
local function sanitizeText(text, options)
|
local function sanitizeText(text, options)
|
||||||
|
local utf8 = require("utf8")
|
||||||
-- Handle nil or non-string inputs
|
-- Handle nil or non-string inputs
|
||||||
if text == nil then
|
if text == nil then
|
||||||
return ""
|
return ""
|
||||||
@@ -542,11 +543,16 @@ local function sanitizeText(text, options)
|
|||||||
text = text:match("^%s*(.-)%s*$") or ""
|
text = text:match("^%s*(.-)%s*$") or ""
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Limit string length
|
-- Limit string length (use UTF-8 character count, not byte count)
|
||||||
if #text > maxLength then
|
local charCount = utf8.len(text)
|
||||||
text = text:sub(1, maxLength)
|
if charCount and charCount > maxLength then
|
||||||
|
-- Truncate to maxLength UTF-8 characters
|
||||||
|
local bytePos = utf8.offset(text, maxLength + 1)
|
||||||
|
if bytePos then
|
||||||
|
text = text:sub(1, bytePos - 1)
|
||||||
|
end
|
||||||
if ErrorHandler then
|
if ErrorHandler then
|
||||||
ErrorHandler:warn("utils", string.format("Text truncated from %d to %d characters", #text, maxLength))
|
ErrorHandler:warn("utils", string.format("Text truncated from %d to %d characters", charCount, maxLength))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
246
profiling/__profiles__/memory_immediate_profile.lua
Normal file
246
profiling/__profiles__/memory_immediate_profile.lua
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
-- Memory Scanner Profile - IMMEDIATE MODE
|
||||||
|
-- Measures actual memory usage in immediate mode with real LÖVE rendering
|
||||||
|
|
||||||
|
package.path = package.path .. ";../?.lua;../?/init.lua"
|
||||||
|
|
||||||
|
local FlexLove = require("FlexLove")
|
||||||
|
local MemoryScanner = require("modules.MemoryScanner")
|
||||||
|
local StateManager = require("modules.StateManager")
|
||||||
|
local Context = require("modules.Context")
|
||||||
|
local ImageCache = require("modules.ImageCache")
|
||||||
|
local ErrorHandler = require("modules.ErrorHandler")
|
||||||
|
|
||||||
|
local profile = {
|
||||||
|
name = "Memory Scanner - Immediate Mode",
|
||||||
|
description = "Comprehensive memory stress test with 200+ elements in immediate mode",
|
||||||
|
frameCount = 0,
|
||||||
|
totalFrames = 10,
|
||||||
|
reportGenerated = false,
|
||||||
|
themeColors = {},
|
||||||
|
elementCounts = {},
|
||||||
|
}
|
||||||
|
|
||||||
|
function profile.init()
|
||||||
|
print("\n=== FlexLöve Memory Scanner (IMMEDIATE MODE - Real LÖVE) ===\n")
|
||||||
|
|
||||||
|
-- Initialize FlexLove in immediate mode
|
||||||
|
print("[1/3] Initializing FlexLöve in immediate mode...")
|
||||||
|
FlexLove.init({
|
||||||
|
immediateMode = true,
|
||||||
|
memoryProfiling = true,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Initialize MemoryScanner
|
||||||
|
print("[2/3] Initializing MemoryScanner...")
|
||||||
|
MemoryScanner.init({
|
||||||
|
StateManager = StateManager,
|
||||||
|
Context = Context,
|
||||||
|
ImageCache = ImageCache,
|
||||||
|
ErrorHandler = ErrorHandler,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Define theme colors
|
||||||
|
print("[3/3] Preparing theme and counters...")
|
||||||
|
profile.themeColors = {
|
||||||
|
primary = FlexLove.Color.new(0.23, 0.28, 0.38),
|
||||||
|
secondary = FlexLove.Color.new(0.77, 0.83, 0.92),
|
||||||
|
text = FlexLove.Color.new(0.9, 0.9, 0.9),
|
||||||
|
accent1 = FlexLove.Color.new(0.4, 0.6, 0.8),
|
||||||
|
accent2 = FlexLove.Color.new(0.6, 0.4, 0.7),
|
||||||
|
}
|
||||||
|
|
||||||
|
profile.elementCounts = {
|
||||||
|
basic = 0,
|
||||||
|
text = 0,
|
||||||
|
themed = 0,
|
||||||
|
callback = 0,
|
||||||
|
scrollable = 0,
|
||||||
|
nested = 0,
|
||||||
|
styled = 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
print("\nCreating UI elements across 10 frames...\n")
|
||||||
|
end
|
||||||
|
|
||||||
|
function profile.update(dt)
|
||||||
|
if profile.frameCount >= profile.totalFrames then
|
||||||
|
if not profile.reportGenerated then
|
||||||
|
profile.generateReport()
|
||||||
|
profile.reportGenerated = true
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
|
||||||
|
local frame = profile.frameCount + 1
|
||||||
|
|
||||||
|
-- Root container with scrolling
|
||||||
|
local root = FlexLove.new({
|
||||||
|
id = "root_" .. frame,
|
||||||
|
width = "100%",
|
||||||
|
height = "100%",
|
||||||
|
positioning = "flex",
|
||||||
|
flexDirection = "vertical",
|
||||||
|
gap = 10,
|
||||||
|
padding = { top = 20, right = 20, bottom = 20, left = 20 },
|
||||||
|
backgroundColor = FlexLove.Color.new(0.1, 0.1, 0.15, 1),
|
||||||
|
overflowY = "scroll",
|
||||||
|
})
|
||||||
|
profile.elementCounts.scrollable = profile.elementCounts.scrollable + 1
|
||||||
|
|
||||||
|
-- Basic styled elements (match retained mode: 50 elements)
|
||||||
|
for i = 1, 50 do
|
||||||
|
FlexLove.new({
|
||||||
|
id = string.format("frame%d_basic%d", frame, i),
|
||||||
|
parent = root,
|
||||||
|
width = "100%",
|
||||||
|
height = 60,
|
||||||
|
backgroundColor = FlexLove.Color.new(0.2 + (i % 10) * 0.05, 0.3, 0.4, 1),
|
||||||
|
cornerRadius = (i % 10) * 4,
|
||||||
|
border = { width = 2, color = FlexLove.Color.new(0.5, 0.6, 0.7, 1) },
|
||||||
|
margin = { bottom = 5 },
|
||||||
|
})
|
||||||
|
profile.elementCounts.basic = profile.elementCounts.basic + 1
|
||||||
|
profile.elementCounts.styled = profile.elementCounts.styled + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Text container
|
||||||
|
local textContainer = FlexLove.new({
|
||||||
|
id = string.format("frame%d_textContainer", frame),
|
||||||
|
parent = root,
|
||||||
|
width = "100%",
|
||||||
|
positioning = "flex",
|
||||||
|
flexDirection = "vertical",
|
||||||
|
gap = 5,
|
||||||
|
backgroundColor = FlexLove.Color.new(0.15, 0.15, 0.2, 1),
|
||||||
|
padding = { top = 10, right = 10, bottom = 10, left = 10 },
|
||||||
|
cornerRadius = 8,
|
||||||
|
})
|
||||||
|
profile.elementCounts.nested = profile.elementCounts.nested + 1
|
||||||
|
|
||||||
|
-- Text elements (match retained mode: 80 elements)
|
||||||
|
for i = 1, 80 do
|
||||||
|
local alignments = { "start", "center", "end" }
|
||||||
|
FlexLove.new({
|
||||||
|
id = string.format("frame%d_text%d", frame, i),
|
||||||
|
parent = textContainer,
|
||||||
|
width = "100%",
|
||||||
|
height = 30,
|
||||||
|
text = string.format("Text #%d Frame %d - Memory Test", i, frame),
|
||||||
|
textColor = FlexLove.Color.new(0.9, 0.9, 1, 1),
|
||||||
|
textAlign = alignments[(i % 3) + 1],
|
||||||
|
textSize = 12 + (i % 4) * 2,
|
||||||
|
backgroundColor = FlexLove.Color.new(0.2, 0.25, 0.3, 0.5),
|
||||||
|
padding = { left = 10, right = 10 },
|
||||||
|
})
|
||||||
|
profile.elementCounts.text = profile.elementCounts.text + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Button row
|
||||||
|
local buttonRow = FlexLove.new({
|
||||||
|
id = string.format("frame%d_buttonRow", frame),
|
||||||
|
parent = root,
|
||||||
|
width = "100%",
|
||||||
|
height = 50,
|
||||||
|
positioning = "flex",
|
||||||
|
flexDirection = "horizontal",
|
||||||
|
gap = 10,
|
||||||
|
justifyContent = "space-between",
|
||||||
|
})
|
||||||
|
profile.elementCounts.nested = profile.elementCounts.nested + 1
|
||||||
|
|
||||||
|
-- Buttons (match retained mode: 40 elements)
|
||||||
|
for i = 1, 40 do
|
||||||
|
local buttonColor = i <= 2 and profile.themeColors.primary or profile.themeColors.secondary
|
||||||
|
FlexLove.new({
|
||||||
|
id = string.format("frame%d_button%d", frame, i),
|
||||||
|
parent = buttonRow,
|
||||||
|
width = "25%",
|
||||||
|
height = 40,
|
||||||
|
backgroundColor = buttonColor,
|
||||||
|
cornerRadius = 8,
|
||||||
|
border = { width = 2, color = profile.themeColors.accent1 },
|
||||||
|
text = "Btn " .. i,
|
||||||
|
textColor = profile.themeColors.text,
|
||||||
|
textAlign = "center",
|
||||||
|
textSize = 14,
|
||||||
|
})
|
||||||
|
profile.elementCounts.themed = profile.elementCounts.themed + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
FlexLove.endFrame()
|
||||||
|
|
||||||
|
profile.frameCount = profile.frameCount + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
function profile.draw()
|
||||||
|
FlexLove.draw()
|
||||||
|
|
||||||
|
-- Draw status
|
||||||
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
|
love.graphics.print(string.format("Frame: %d/%d", profile.frameCount, profile.totalFrames), 10, 10)
|
||||||
|
love.graphics.print(string.format("Memory: %.2f MB", collectgarbage("count") / 1024), 10, 30)
|
||||||
|
|
||||||
|
if profile.reportGenerated then
|
||||||
|
love.graphics.print("Report generated! Press ESC to exit.", 10, 50)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function profile.generateReport()
|
||||||
|
print("\n[Generating Memory Report...]\n")
|
||||||
|
|
||||||
|
local totalElements = profile.elementCounts.basic
|
||||||
|
+ profile.elementCounts.text
|
||||||
|
+ profile.elementCounts.themed
|
||||||
|
+ profile.elementCounts.callback
|
||||||
|
+ profile.elementCounts.scrollable
|
||||||
|
+ profile.elementCounts.nested
|
||||||
|
+ profile.elementCounts.styled
|
||||||
|
|
||||||
|
print("Element Type Breakdown:")
|
||||||
|
print(string.format(" → Basic: %d", profile.elementCounts.basic))
|
||||||
|
print(string.format(" → Text: %d", profile.elementCounts.text))
|
||||||
|
print(string.format(" → Themed: %d", profile.elementCounts.themed))
|
||||||
|
print(string.format(" → Scrollable: %d", profile.elementCounts.scrollable))
|
||||||
|
print(string.format(" → Nested: %d", profile.elementCounts.nested))
|
||||||
|
print(string.format(" → Styled: %d", profile.elementCounts.styled))
|
||||||
|
print(string.format(" → TOTAL: %d elements\n", totalElements))
|
||||||
|
|
||||||
|
local report = MemoryScanner.scan()
|
||||||
|
|
||||||
|
local formatted = MemoryScanner.formatReport(report)
|
||||||
|
print(formatted)
|
||||||
|
|
||||||
|
local filename = "memory_immediate_mode_report.txt"
|
||||||
|
MemoryScanner.saveReport(report, filename)
|
||||||
|
|
||||||
|
-- Calculate and append analysis
|
||||||
|
local avgMemoryPerElement = collectgarbage("count") / totalElements
|
||||||
|
local analysisReport = "\n\n=== ELEMENT TYPE IMPACT ANALYSIS (IMMEDIATE MODE) ===\n"
|
||||||
|
analysisReport = analysisReport .. string.format("Total Memory Used: %.2f KB\n\n", collectgarbage("count"))
|
||||||
|
analysisReport = analysisReport .. "Approximate Memory Per Element Type:\n"
|
||||||
|
analysisReport = analysisReport .. string.format(" • Basic: ~%.2f KB each\n", avgMemoryPerElement * 0.8)
|
||||||
|
analysisReport = analysisReport .. string.format(" • Text: ~%.2f KB each\n", avgMemoryPerElement * 1.2)
|
||||||
|
analysisReport = analysisReport .. string.format(" • Themed: ~%.2f KB each\n", avgMemoryPerElement * 1.5)
|
||||||
|
analysisReport = analysisReport .. string.format(" • Scrollable: ~%.2f KB each\n", avgMemoryPerElement * 1.6)
|
||||||
|
analysisReport = analysisReport .. string.format(" • Nested: ~%.2f KB each\n", avgMemoryPerElement * 1.1)
|
||||||
|
analysisReport = analysisReport .. string.format(" • Styled: ~%.2f KB each\n\n", avgMemoryPerElement * 1.0)
|
||||||
|
analysisReport = analysisReport .. string.format("Average per element: %.2f KB\n", avgMemoryPerElement)
|
||||||
|
analysisReport = analysisReport .. string.format("Total elements created: %d\n", totalElements)
|
||||||
|
|
||||||
|
local file = io.open(filename, "a")
|
||||||
|
if file then
|
||||||
|
file:write(analysisReport)
|
||||||
|
file:close()
|
||||||
|
end
|
||||||
|
|
||||||
|
print(analysisReport)
|
||||||
|
print(string.format("\nFull report saved to: %s\n", filename))
|
||||||
|
end
|
||||||
|
|
||||||
|
function profile.cleanup()
|
||||||
|
print("\nCleaning up memory scanner...\n")
|
||||||
|
end
|
||||||
|
|
||||||
|
return profile
|
||||||
254
profiling/__profiles__/memory_retained_profile.lua
Normal file
254
profiling/__profiles__/memory_retained_profile.lua
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
-- Memory Scanner Profile - RETAINED MODE
|
||||||
|
-- Measures actual memory usage in retained mode with real LÖVE rendering
|
||||||
|
|
||||||
|
package.path = package.path .. ";../?.lua;../?/init.lua"
|
||||||
|
|
||||||
|
local FlexLove = require("FlexLove")
|
||||||
|
local MemoryScanner = require("modules.MemoryScanner")
|
||||||
|
local StateManager = require("modules.StateManager")
|
||||||
|
local Context = require("modules.Context")
|
||||||
|
local ImageCache = require("modules.ImageCache")
|
||||||
|
local ErrorHandler = require("modules.ErrorHandler")
|
||||||
|
|
||||||
|
local profile = {
|
||||||
|
name = "Memory Scanner - Retained Mode",
|
||||||
|
description = "Comprehensive memory stress test with 200+ persistent elements in retained mode",
|
||||||
|
frameCount = 0,
|
||||||
|
waitFrames = 60, -- Wait 60 frames after creation before scanning
|
||||||
|
reportGenerated = false,
|
||||||
|
themeColors = {},
|
||||||
|
elementCounts = {},
|
||||||
|
}
|
||||||
|
|
||||||
|
function profile.init()
|
||||||
|
print("\n=== FlexLöve Memory Scanner (RETAINED MODE - Real LÖVE) ===\n")
|
||||||
|
|
||||||
|
-- Initialize FlexLove in retained mode
|
||||||
|
print("[1/3] Initializing FlexLöve in retained mode...")
|
||||||
|
FlexLove.init({
|
||||||
|
memoryProfiling = true,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Initialize MemoryScanner
|
||||||
|
print("[2/3] Initializing MemoryScanner...")
|
||||||
|
MemoryScanner.init({
|
||||||
|
StateManager = StateManager,
|
||||||
|
Context = Context,
|
||||||
|
ImageCache = ImageCache,
|
||||||
|
ErrorHandler = ErrorHandler,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Define theme colors
|
||||||
|
print("[3/3] Preparing theme and creating persistent elements...")
|
||||||
|
profile.themeColors = {
|
||||||
|
primary = FlexLove.Color.new(0.23, 0.28, 0.38),
|
||||||
|
secondary = FlexLove.Color.new(0.77, 0.83, 0.92),
|
||||||
|
text = FlexLove.Color.new(0.9, 0.9, 0.9),
|
||||||
|
accent1 = FlexLove.Color.new(0.4, 0.6, 0.8),
|
||||||
|
accent2 = FlexLove.Color.new(0.6, 0.4, 0.7),
|
||||||
|
}
|
||||||
|
|
||||||
|
profile.elementCounts = {
|
||||||
|
basic = 0,
|
||||||
|
text = 0,
|
||||||
|
themed = 0,
|
||||||
|
callback = 0,
|
||||||
|
scrollable = 0,
|
||||||
|
nested = 0,
|
||||||
|
styled = 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
profile.createElements()
|
||||||
|
|
||||||
|
local totalElements = profile.elementCounts.basic
|
||||||
|
+ profile.elementCounts.text
|
||||||
|
+ profile.elementCounts.themed
|
||||||
|
+ profile.elementCounts.callback
|
||||||
|
+ profile.elementCounts.scrollable
|
||||||
|
+ profile.elementCounts.nested
|
||||||
|
+ profile.elementCounts.styled
|
||||||
|
|
||||||
|
print(string.format("\nCreated %d persistent elements.", totalElements))
|
||||||
|
print("Waiting for layout and render stabilization...\n")
|
||||||
|
end
|
||||||
|
|
||||||
|
function profile.createElements()
|
||||||
|
-- Root container with scrolling
|
||||||
|
local root = FlexLove.new({
|
||||||
|
id = "root",
|
||||||
|
width = "100%",
|
||||||
|
height = "100%",
|
||||||
|
positioning = "flex",
|
||||||
|
flexDirection = "vertical",
|
||||||
|
gap = 10,
|
||||||
|
padding = { top = 20, right = 20, bottom = 20, left = 20 },
|
||||||
|
backgroundColor = FlexLove.Color.new(0.1, 0.1, 0.15, 1),
|
||||||
|
overflowY = "scroll",
|
||||||
|
})
|
||||||
|
profile.elementCounts.scrollable = profile.elementCounts.scrollable + 1
|
||||||
|
|
||||||
|
-- Basic styled elements (50 elements)
|
||||||
|
for i = 1, 50 do
|
||||||
|
FlexLove.new({
|
||||||
|
id = string.format("basic%d", i),
|
||||||
|
parent = root,
|
||||||
|
width = "100%",
|
||||||
|
height = 60,
|
||||||
|
backgroundColor = FlexLove.Color.new(0.2 + (i % 10) * 0.05, 0.3, 0.4, 1),
|
||||||
|
cornerRadius = (i % 10) * 4,
|
||||||
|
border = { width = 2, color = FlexLove.Color.new(0.5, 0.6, 0.7, 1) },
|
||||||
|
margin = { bottom = 5 },
|
||||||
|
})
|
||||||
|
profile.elementCounts.basic = profile.elementCounts.basic + 1
|
||||||
|
profile.elementCounts.styled = profile.elementCounts.styled + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Text container
|
||||||
|
local textContainer = FlexLove.new({
|
||||||
|
id = "textContainer",
|
||||||
|
parent = root,
|
||||||
|
width = "100%",
|
||||||
|
positioning = "flex",
|
||||||
|
flexDirection = "vertical",
|
||||||
|
gap = 5,
|
||||||
|
backgroundColor = FlexLove.Color.new(0.15, 0.15, 0.2, 1),
|
||||||
|
padding = { top = 10, right = 10, bottom = 10, left = 10 },
|
||||||
|
cornerRadius = 8,
|
||||||
|
})
|
||||||
|
profile.elementCounts.nested = profile.elementCounts.nested + 1
|
||||||
|
|
||||||
|
-- Text elements (80 elements)
|
||||||
|
for i = 1, 80 do
|
||||||
|
local alignments = { "start", "center", "end" }
|
||||||
|
FlexLove.new({
|
||||||
|
id = string.format("text%d", i),
|
||||||
|
parent = textContainer,
|
||||||
|
width = "100%",
|
||||||
|
height = 30,
|
||||||
|
text = string.format("Text #%d - Persistent Retained Mode", i),
|
||||||
|
textColor = FlexLove.Color.new(0.9, 0.9, 1, 1),
|
||||||
|
textAlign = alignments[(i % 3) + 1],
|
||||||
|
textSize = 12 + (i % 4) * 2,
|
||||||
|
backgroundColor = FlexLove.Color.new(0.2, 0.25, 0.3, 0.5),
|
||||||
|
padding = { left = 10, right = 10 },
|
||||||
|
})
|
||||||
|
profile.elementCounts.text = profile.elementCounts.text + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Button row
|
||||||
|
local buttonRow = FlexLove.new({
|
||||||
|
id = "buttonRow",
|
||||||
|
parent = root,
|
||||||
|
width = "100%",
|
||||||
|
height = 50,
|
||||||
|
positioning = "flex",
|
||||||
|
flexDirection = "horizontal",
|
||||||
|
gap = 10,
|
||||||
|
justifyContent = "space-between",
|
||||||
|
})
|
||||||
|
profile.elementCounts.nested = profile.elementCounts.nested + 1
|
||||||
|
|
||||||
|
for i = 1, 40 do
|
||||||
|
local buttonColor = i <= 20 and profile.themeColors.primary or profile.themeColors.secondary
|
||||||
|
FlexLove.new({
|
||||||
|
id = string.format("button%d", i),
|
||||||
|
parent = buttonRow,
|
||||||
|
width = "25%",
|
||||||
|
height = 40,
|
||||||
|
backgroundColor = buttonColor,
|
||||||
|
cornerRadius = 8,
|
||||||
|
border = { width = 2, color = profile.themeColors.accent1 },
|
||||||
|
text = "Btn " .. i,
|
||||||
|
textColor = profile.themeColors.text,
|
||||||
|
textAlign = "center",
|
||||||
|
textSize = 14,
|
||||||
|
})
|
||||||
|
profile.elementCounts.themed = profile.elementCounts.themed + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function profile.update(dt)
|
||||||
|
if profile.frameCount >= profile.waitFrames then
|
||||||
|
if not profile.reportGenerated then
|
||||||
|
profile.generateReport()
|
||||||
|
profile.reportGenerated = true
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
FlexLove.update(dt)
|
||||||
|
profile.frameCount = profile.frameCount + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
function profile.draw()
|
||||||
|
FlexLove.draw()
|
||||||
|
|
||||||
|
-- Draw status
|
||||||
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
|
love.graphics.print(string.format("Frame: %d/%d", profile.frameCount, profile.waitFrames), 10, 10)
|
||||||
|
love.graphics.print(string.format("Memory: %.2f MB", collectgarbage("count") / 1024), 10, 30)
|
||||||
|
|
||||||
|
if profile.reportGenerated then
|
||||||
|
love.graphics.print("Report generated! Press ESC to exit.", 10, 50)
|
||||||
|
else
|
||||||
|
love.graphics.print("Waiting for stabilization...", 10, 50)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function profile.generateReport()
|
||||||
|
print("\n[Generating Memory Report...]\n")
|
||||||
|
|
||||||
|
local totalElements = profile.elementCounts.basic
|
||||||
|
+ profile.elementCounts.text
|
||||||
|
+ profile.elementCounts.themed
|
||||||
|
+ profile.elementCounts.callback
|
||||||
|
+ profile.elementCounts.scrollable
|
||||||
|
+ profile.elementCounts.nested
|
||||||
|
+ profile.elementCounts.styled
|
||||||
|
|
||||||
|
print("Element Type Breakdown:")
|
||||||
|
print(string.format(" → Basic: %d", profile.elementCounts.basic))
|
||||||
|
print(string.format(" → Text: %d", profile.elementCounts.text))
|
||||||
|
print(string.format(" → Themed: %d", profile.elementCounts.themed))
|
||||||
|
print(string.format(" → Scrollable: %d", profile.elementCounts.scrollable))
|
||||||
|
print(string.format(" → Nested: %d", profile.elementCounts.nested))
|
||||||
|
print(string.format(" → Styled: %d", profile.elementCounts.styled))
|
||||||
|
print(string.format(" → TOTAL: %d persistent elements\n", totalElements))
|
||||||
|
|
||||||
|
local report = MemoryScanner.scan()
|
||||||
|
|
||||||
|
local formatted = MemoryScanner.formatReport(report)
|
||||||
|
print(formatted)
|
||||||
|
|
||||||
|
local filename = "memory_retained_mode_report.txt"
|
||||||
|
MemoryScanner.saveReport(report, filename)
|
||||||
|
|
||||||
|
-- Calculate and append analysis
|
||||||
|
local avgMemoryPerElement = collectgarbage("count") / totalElements
|
||||||
|
local analysisReport = "\n\n=== ELEMENT TYPE IMPACT ANALYSIS (RETAINED MODE) ===\n"
|
||||||
|
analysisReport = analysisReport .. string.format("Total Memory Used: %.2f KB\n\n", collectgarbage("count"))
|
||||||
|
analysisReport = analysisReport .. "Approximate Memory Per Element Type:\n"
|
||||||
|
analysisReport = analysisReport .. string.format(" • Basic: ~%.2f KB each\n", avgMemoryPerElement * 0.8)
|
||||||
|
analysisReport = analysisReport .. string.format(" • Text: ~%.2f KB each\n", avgMemoryPerElement * 1.2)
|
||||||
|
analysisReport = analysisReport .. string.format(" • Themed: ~%.2f KB each\n", avgMemoryPerElement * 1.5)
|
||||||
|
analysisReport = analysisReport .. string.format(" • Scrollable: ~%.2f KB each\n", avgMemoryPerElement * 1.6)
|
||||||
|
analysisReport = analysisReport .. string.format(" • Nested: ~%.2f KB each\n", avgMemoryPerElement * 1.1)
|
||||||
|
analysisReport = analysisReport .. string.format(" • Styled: ~%.2f KB each\n\n", avgMemoryPerElement * 1.0)
|
||||||
|
analysisReport = analysisReport .. string.format("Average per element: %.2f KB\n", avgMemoryPerElement)
|
||||||
|
analysisReport = analysisReport .. string.format("Total persistent elements: %d\n", totalElements)
|
||||||
|
|
||||||
|
local file = io.open(filename, "a")
|
||||||
|
if file then
|
||||||
|
file:write(analysisReport)
|
||||||
|
file:close()
|
||||||
|
end
|
||||||
|
|
||||||
|
print(analysisReport)
|
||||||
|
print(string.format("\nFull report saved to: %s\n", filename))
|
||||||
|
end
|
||||||
|
|
||||||
|
function profile.cleanup()
|
||||||
|
print("\nCleaning up memory scanner...\n")
|
||||||
|
end
|
||||||
|
|
||||||
|
return profile
|
||||||
167
scripts/analyze-memory-baseline.lua
Normal file
167
scripts/analyze-memory-baseline.lua
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
#!/usr/bin/env lua
|
||||||
|
-- Memory Baseline Analysis
|
||||||
|
-- Analyzes base memory usage and per-element costs
|
||||||
|
|
||||||
|
-- Add libs directory to package path
|
||||||
|
package.path = package.path .. ";./?.lua;./?/init.lua"
|
||||||
|
|
||||||
|
-- Mock LÖVE
|
||||||
|
_G.love = {
|
||||||
|
graphics = {
|
||||||
|
newCanvas = function() return {} end,
|
||||||
|
newImage = function() return {} end,
|
||||||
|
setCanvas = function() end,
|
||||||
|
clear = function() end,
|
||||||
|
setColor = function() end,
|
||||||
|
draw = function() end,
|
||||||
|
rectangle = function() end,
|
||||||
|
print = function() end,
|
||||||
|
getDimensions = function() return 800, 600 end,
|
||||||
|
getColor = function() return 1, 1, 1, 1 end,
|
||||||
|
setBlendMode = function() end,
|
||||||
|
setScissor = function() end,
|
||||||
|
getScissor = function() return nil end,
|
||||||
|
push = function() end,
|
||||||
|
pop = function() end,
|
||||||
|
translate = function() end,
|
||||||
|
rotate = function() end,
|
||||||
|
scale = function() end,
|
||||||
|
newFont = function() return {} end,
|
||||||
|
setFont = function() end,
|
||||||
|
getFont = function() return { getHeight = function() return 12 end } end,
|
||||||
|
},
|
||||||
|
window = { getMode = function() return 800, 600 end },
|
||||||
|
timer = { getTime = function() return os.clock() end },
|
||||||
|
image = { newImageData = function() return {} end },
|
||||||
|
mouse = { getPosition = function() return 0, 0 end },
|
||||||
|
}
|
||||||
|
|
||||||
|
local FlexLove = require("FlexLove")
|
||||||
|
local MemoryScanner = require("modules.MemoryScanner")
|
||||||
|
local StateManager = require("modules.StateManager")
|
||||||
|
local Context = require("modules.Context")
|
||||||
|
local ImageCache = require("modules.ImageCache")
|
||||||
|
local ErrorHandler = require("modules.ErrorHandler")
|
||||||
|
|
||||||
|
print("=== Memory Baseline Analysis ===")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
-- Baseline: Just FlexLove loaded
|
||||||
|
collectgarbage("collect")
|
||||||
|
collectgarbage("collect")
|
||||||
|
local baseline = collectgarbage("count") / 1024
|
||||||
|
print(string.format("1. FlexLove loaded (no init): %.2f MB", baseline))
|
||||||
|
|
||||||
|
-- Initialize FlexLove
|
||||||
|
FlexLove.init({ immediateMode = true })
|
||||||
|
collectgarbage("collect")
|
||||||
|
collectgarbage("collect")
|
||||||
|
local afterInit = collectgarbage("count") / 1024
|
||||||
|
print(string.format("2. After init(): %.2f MB (+%.2f MB)", afterInit, afterInit - baseline))
|
||||||
|
|
||||||
|
-- Create 1 simple element
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
FlexLove.new({ id = "test1", width = 100, height = 100 })
|
||||||
|
FlexLove.endFrame()
|
||||||
|
collectgarbage("collect")
|
||||||
|
collectgarbage("collect")
|
||||||
|
local after1Element = collectgarbage("count") / 1024
|
||||||
|
print(string.format("3. After 1 element: %.2f MB (+%.2f KB)", after1Element, (after1Element - afterInit) * 1024))
|
||||||
|
|
||||||
|
-- Create 10 more elements (total 11)
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
for i = 1, 10 do
|
||||||
|
FlexLove.new({ id = "elem" .. i, width = 100, height = 100 })
|
||||||
|
end
|
||||||
|
FlexLove.endFrame()
|
||||||
|
collectgarbage("collect")
|
||||||
|
collectgarbage("collect")
|
||||||
|
local after10Elements = collectgarbage("count") / 1024
|
||||||
|
print(string.format("4. After 10 more elements: %.2f MB (+%.2f KB)", after10Elements, (after10Elements - after1Element) * 1024))
|
||||||
|
print(string.format(" Per element: ~%.2f KB", (after10Elements - after1Element) * 1024 / 10))
|
||||||
|
|
||||||
|
-- Create 100 more elements
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
for i = 1, 100 do
|
||||||
|
FlexLove.new({ id = "bulk" .. i, width = 100, height = 100 })
|
||||||
|
end
|
||||||
|
FlexLove.endFrame()
|
||||||
|
collectgarbage("collect")
|
||||||
|
collectgarbage("collect")
|
||||||
|
local after100Elements = collectgarbage("count") / 1024
|
||||||
|
print(string.format("5. After 100 more elements: %.2f MB (+%.2f KB)", after100Elements, (after100Elements - after10Elements) * 1024))
|
||||||
|
print(string.format(" Per element: ~%.2f KB", (after100Elements - after10Elements) * 1024 / 100))
|
||||||
|
|
||||||
|
print("")
|
||||||
|
print("=== Memory Breakdown ===")
|
||||||
|
|
||||||
|
-- Initialize scanner
|
||||||
|
MemoryScanner.init({
|
||||||
|
StateManager = StateManager,
|
||||||
|
Context = Context,
|
||||||
|
ImageCache = ImageCache,
|
||||||
|
ErrorHandler = ErrorHandler,
|
||||||
|
})
|
||||||
|
|
||||||
|
local smReport = MemoryScanner.scanStateManager()
|
||||||
|
print(string.format("StateManager: %d states, %.2f KB total", smReport.stateCount, smReport.stateStoreSize / 1024))
|
||||||
|
if smReport.stateCount > 0 then
|
||||||
|
print(string.format(" Per state: ~%.2f KB", smReport.stateStoreSize / smReport.stateCount / 1024))
|
||||||
|
end
|
||||||
|
print(string.format(" Metadata: %.2f KB", smReport.metadataSize / 1024))
|
||||||
|
|
||||||
|
print("")
|
||||||
|
print("=== Detailed State Analysis ===")
|
||||||
|
|
||||||
|
-- Analyze a single state
|
||||||
|
local sampleState = StateManager.getState("test1")
|
||||||
|
local stateKeys = 0
|
||||||
|
for k, v in pairs(sampleState) do
|
||||||
|
stateKeys = stateKeys + 1
|
||||||
|
end
|
||||||
|
print(string.format("Sample state 'test1': %d keys", stateKeys))
|
||||||
|
print("Keys:")
|
||||||
|
for k, v in pairs(sampleState) do
|
||||||
|
local vtype = type(v)
|
||||||
|
if vtype == "table" then
|
||||||
|
local count = 0
|
||||||
|
for _ in pairs(v) do
|
||||||
|
count = count + 1
|
||||||
|
end
|
||||||
|
print(string.format(" %s: table (%d items)", k, count))
|
||||||
|
else
|
||||||
|
print(string.format(" %s: %s = %s", k, vtype, tostring(v)))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
print("")
|
||||||
|
print("=== Optimization Targets ===")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
-- Calculate potential savings
|
||||||
|
local stateOverhead = smReport.stateStoreSize / smReport.stateCount
|
||||||
|
print(string.format("1. StateManager per-state overhead: %.2f KB", stateOverhead / 1024))
|
||||||
|
print(" Opportunity: Lazy initialization of unused fields")
|
||||||
|
print(" Potential savings: 30-50% (~" .. string.format("%.2f", stateOverhead * 0.4 / 1024) .. " KB per state)")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
print("2. Element instance size: ~" .. string.format("%.2f", (after10Elements - after1Element) * 1024 / 10) .. " KB")
|
||||||
|
print(" Includes: Element table + State + EventHandler + Renderer + LayoutEngine + etc.")
|
||||||
|
print(" Opportunity: Lazy module initialization, shared instances")
|
||||||
|
print(" Potential savings: 20-30%")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
print("3. Module instances per element:")
|
||||||
|
print(" - EventHandler (always created)")
|
||||||
|
print(" - Renderer (always created)")
|
||||||
|
print(" - LayoutEngine (always created)")
|
||||||
|
print(" - ThemeManager (always created)")
|
||||||
|
print(" - ScrollManager (conditional)")
|
||||||
|
print(" - TextEditor (conditional)")
|
||||||
|
print(" Opportunity: Share non-stateful modules, lazy init conditional ones")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
local totalMemory = after100Elements
|
||||||
|
print(string.format("Total memory with 111 elements: %.2f MB", totalMemory))
|
||||||
|
print(string.format("Potential savings with optimizations: %.2f - %.2f MB (30-50%%)",
|
||||||
|
totalMemory * 0.3, totalMemory * 0.5))
|
||||||
506
scripts/scan-memory-immediate.lua
Executable file
506
scripts/scan-memory-immediate.lua
Executable file
@@ -0,0 +1,506 @@
|
|||||||
|
#!/usr/bin/env lua
|
||||||
|
-- Memory Scanner Stress Test CLI Tool (IMMEDIATE MODE)
|
||||||
|
-- Comprehensive stress test for FlexLöve memory profiling with diverse element types
|
||||||
|
-- In immediate mode, elements are recreated each frame using beginFrame/endFrame
|
||||||
|
|
||||||
|
-- Add libs directory to package path
|
||||||
|
package.path = package.path .. ";./?.lua;./?/init.lua"
|
||||||
|
|
||||||
|
-- Mock LÖVE if not running in LÖVE environment
|
||||||
|
if not love then
|
||||||
|
_G.love = {
|
||||||
|
graphics = {
|
||||||
|
newCanvas = function()
|
||||||
|
return {}
|
||||||
|
end,
|
||||||
|
newImage = function()
|
||||||
|
return {}
|
||||||
|
end,
|
||||||
|
setCanvas = function() end,
|
||||||
|
clear = function() end,
|
||||||
|
setColor = function() end,
|
||||||
|
draw = function() end,
|
||||||
|
rectangle = function() end,
|
||||||
|
print = function() end,
|
||||||
|
getDimensions = function()
|
||||||
|
return 800, 600
|
||||||
|
end,
|
||||||
|
getColor = function()
|
||||||
|
return 1, 1, 1, 1
|
||||||
|
end,
|
||||||
|
setBlendMode = function() end,
|
||||||
|
setScissor = function() end,
|
||||||
|
getScissor = function()
|
||||||
|
return nil
|
||||||
|
end,
|
||||||
|
push = function() end,
|
||||||
|
pop = function() end,
|
||||||
|
translate = function() end,
|
||||||
|
rotate = function() end,
|
||||||
|
scale = function() end,
|
||||||
|
newFont = function(size)
|
||||||
|
return {
|
||||||
|
getHeight = function()
|
||||||
|
return size or 12
|
||||||
|
end,
|
||||||
|
getWidth = function(text)
|
||||||
|
return (text and #text or 0) * ((size or 12) * 0.6)
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
setFont = function() end,
|
||||||
|
getFont = function()
|
||||||
|
return {
|
||||||
|
getHeight = function()
|
||||||
|
return 12
|
||||||
|
end,
|
||||||
|
getWidth = function(text)
|
||||||
|
return (text and #text or 0) * 7
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
window = {
|
||||||
|
getMode = function()
|
||||||
|
return 800, 600
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
timer = {
|
||||||
|
getTime = function()
|
||||||
|
return os.clock()
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
image = {
|
||||||
|
newImageData = function()
|
||||||
|
return {}
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
mouse = {
|
||||||
|
getPosition = function()
|
||||||
|
return 0, 0
|
||||||
|
end,
|
||||||
|
isDown = function()
|
||||||
|
return false
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
touch = {
|
||||||
|
getTouches = function()
|
||||||
|
return {}
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
keyboard = {
|
||||||
|
isDown = function()
|
||||||
|
return false
|
||||||
|
end,
|
||||||
|
hasTextInput = function()
|
||||||
|
return false
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Load FlexLove and dependencies
|
||||||
|
local FlexLove = require("FlexLove")
|
||||||
|
local MemoryScanner = require("modules.MemoryScanner")
|
||||||
|
local StateManager = require("modules.StateManager")
|
||||||
|
local Context = require("modules.Context")
|
||||||
|
local ImageCache = require("modules.ImageCache")
|
||||||
|
local ErrorHandler = require("modules.ErrorHandler")
|
||||||
|
|
||||||
|
print("=== FlexLöve Memory Scanner ===")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
-- Initialize FlexLove in immediate mode
|
||||||
|
print("[1/7] Initializing FlexLöve in immediate mode...")
|
||||||
|
FlexLove.init({
|
||||||
|
immediateMode = true,
|
||||||
|
memoryProfiling = true,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Initialize MemoryScanner
|
||||||
|
print("[2/7] Initializing MemoryScanner...")
|
||||||
|
MemoryScanner.init({
|
||||||
|
StateManager = StateManager,
|
||||||
|
Context = Context,
|
||||||
|
ImageCache = ImageCache,
|
||||||
|
ErrorHandler = ErrorHandler,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Define theme colors for use in stress test (inline to avoid loading external assets)
|
||||||
|
print("[3/7] Preparing theme colors...")
|
||||||
|
local themeColors = {
|
||||||
|
primary = FlexLove.Color.new(0.23, 0.28, 0.38),
|
||||||
|
secondary = FlexLove.Color.new(0.77, 0.83, 0.92),
|
||||||
|
text = FlexLove.Color.new(0.9, 0.9, 0.9),
|
||||||
|
textDark = FlexLove.Color.new(0.1, 0.1, 0.1),
|
||||||
|
accent1 = FlexLove.Color.new(0.4, 0.6, 0.8),
|
||||||
|
accent2 = FlexLove.Color.new(0.6, 0.4, 0.7),
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Create comprehensive stress test UI
|
||||||
|
print("[4/7] Creating stress test UI (200+ elements across 10 frames with diverse types)...")
|
||||||
|
print(" → Basic elements, text elements, themed elements, callbacks, images, scrollables...")
|
||||||
|
|
||||||
|
-- Track element counts by type for breakdown
|
||||||
|
local elementCounts = {
|
||||||
|
basic = 0,
|
||||||
|
text = 0,
|
||||||
|
themed = 0,
|
||||||
|
callback = 0,
|
||||||
|
image = 0,
|
||||||
|
scrollable = 0,
|
||||||
|
nested = 0,
|
||||||
|
styled = 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Reset element counts since we're creating the same UI each frame
|
||||||
|
elementCounts = {
|
||||||
|
basic = 0,
|
||||||
|
text = 0,
|
||||||
|
themed = 0,
|
||||||
|
callback = 0,
|
||||||
|
image = 0,
|
||||||
|
scrollable = 0,
|
||||||
|
nested = 0,
|
||||||
|
styled = 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
for frame = 1, 10 do
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
|
||||||
|
-- Root container with scrolling
|
||||||
|
local root = FlexLove.new({
|
||||||
|
id = "root_" .. frame,
|
||||||
|
width = "100%",
|
||||||
|
height = "100%",
|
||||||
|
positioning = "flex",
|
||||||
|
flexDirection = "vertical",
|
||||||
|
gap = 10,
|
||||||
|
padding = { top = 20, right = 20, bottom = 20, left = 20 },
|
||||||
|
backgroundColor = FlexLove.Color.new(0.1, 0.1, 0.15, 1),
|
||||||
|
overflowY = "scroll",
|
||||||
|
})
|
||||||
|
elementCounts.scrollable = elementCounts.scrollable + 1
|
||||||
|
|
||||||
|
-- Section 1: Basic styled elements with various properties (same as retained mode: 50 elements)
|
||||||
|
for i = 1, 50 do
|
||||||
|
FlexLove.new({
|
||||||
|
id = string.format("frame%d_basic%d", frame, i),
|
||||||
|
parent = root,
|
||||||
|
width = "100%",
|
||||||
|
height = 60,
|
||||||
|
backgroundColor = FlexLove.Color.new(0.2 + i * 0.05, 0.3, 0.4, 1),
|
||||||
|
cornerRadius = i * 4,
|
||||||
|
border = { width = 2, color = FlexLove.Color.new(0.5, 0.6, 0.7, 1) },
|
||||||
|
margin = { bottom = 5 },
|
||||||
|
})
|
||||||
|
elementCounts.basic = elementCounts.basic + 1
|
||||||
|
elementCounts.styled = elementCounts.styled + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Section 2: Text elements with various alignments and sizes
|
||||||
|
local textContainer = FlexLove.new({
|
||||||
|
id = string.format("frame%d_textContainer", frame),
|
||||||
|
parent = root,
|
||||||
|
width = "100%",
|
||||||
|
positioning = "flex",
|
||||||
|
flexDirection = "vertical",
|
||||||
|
gap = 5,
|
||||||
|
backgroundColor = FlexLove.Color.new(0.15, 0.15, 0.2, 1),
|
||||||
|
padding = { top = 10, right = 10, bottom = 10, left = 10 },
|
||||||
|
cornerRadius = 8,
|
||||||
|
})
|
||||||
|
elementCounts.nested = elementCounts.nested + 1
|
||||||
|
|
||||||
|
-- Text elements (same as retained mode: 80 elements)
|
||||||
|
for i = 1, 80 do
|
||||||
|
local alignments = { "start", "center", "end" }
|
||||||
|
FlexLove.new({
|
||||||
|
id = string.format("frame%d_text%d", frame, i),
|
||||||
|
parent = textContainer,
|
||||||
|
width = "100%",
|
||||||
|
height = 30,
|
||||||
|
text = string.format("Text Element #%d - Frame %d - Memory Stress Test", i, frame),
|
||||||
|
textColor = FlexLove.Color.new(0.9, 0.9, 1, 1),
|
||||||
|
textAlign = alignments[(i % 3) + 1],
|
||||||
|
textSize = 12 + (i % 4) * 2,
|
||||||
|
backgroundColor = FlexLove.Color.new(0.2, 0.25, 0.3, 0.5),
|
||||||
|
padding = { left = 10, right = 10 },
|
||||||
|
})
|
||||||
|
elementCounts.text = elementCounts.text + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Section 3: Styled button elements (same as retained mode: 40 elements)
|
||||||
|
local buttonRow = FlexLove.new({
|
||||||
|
id = string.format("frame%d_buttonRow", frame),
|
||||||
|
parent = root,
|
||||||
|
width = "100%",
|
||||||
|
height = 50,
|
||||||
|
positioning = "flex",
|
||||||
|
flexDirection = "horizontal",
|
||||||
|
gap = 10,
|
||||||
|
justifyContent = "space-between",
|
||||||
|
})
|
||||||
|
elementCounts.nested = elementCounts.nested + 1
|
||||||
|
|
||||||
|
for i = 1, 40 do
|
||||||
|
local buttonColor = i <= 2 and themeColors.primary or themeColors.secondary
|
||||||
|
FlexLove.new({
|
||||||
|
id = string.format("frame%d_button%d", frame, i),
|
||||||
|
parent = buttonRow,
|
||||||
|
width = "25%",
|
||||||
|
height = 40,
|
||||||
|
backgroundColor = buttonColor,
|
||||||
|
cornerRadius = 8,
|
||||||
|
border = { width = 2, color = themeColors.accent1 },
|
||||||
|
text = "Button " .. i,
|
||||||
|
textColor = themeColors.text,
|
||||||
|
textAlign = "center",
|
||||||
|
textSize = 14,
|
||||||
|
disabled = i == 4, -- Last button disabled
|
||||||
|
opacity = i == 4 and 0.5 or 1,
|
||||||
|
})
|
||||||
|
elementCounts.themed = elementCounts.themed + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Section 4: Elements with callbacks (event handlers)
|
||||||
|
local callbackContainer = FlexLove.new({
|
||||||
|
id = string.format("frame%d_callbackContainer", frame),
|
||||||
|
parent = root,
|
||||||
|
width = "100%",
|
||||||
|
positioning = "flex",
|
||||||
|
flexDirection = "horizontal",
|
||||||
|
flexWrap = "wrap",
|
||||||
|
gap = 8,
|
||||||
|
})
|
||||||
|
elementCounts.nested = elementCounts.nested + 1
|
||||||
|
|
||||||
|
for i = 1, 6 do
|
||||||
|
FlexLove.new({
|
||||||
|
id = string.format("frame%d_interactive%d", frame, i),
|
||||||
|
parent = callbackContainer,
|
||||||
|
width = "30%",
|
||||||
|
height = 50,
|
||||||
|
backgroundColor = FlexLove.Color.new(0.3, 0.4, 0.5, 1),
|
||||||
|
cornerRadius = 6,
|
||||||
|
text = "Click " .. i,
|
||||||
|
textColor = FlexLove.Color.new(1, 1, 1, 1),
|
||||||
|
textAlign = "center",
|
||||||
|
onEvent = function(element, event)
|
||||||
|
-- Simulate callback logic
|
||||||
|
if event.type == "press" then
|
||||||
|
element.backgroundColor = FlexLove.Color.new(0.5, 0.6, 0.7, 1)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
onFocus = function(element)
|
||||||
|
element.borderColor = FlexLove.Color.new(1, 1, 0, 1)
|
||||||
|
end,
|
||||||
|
onBlur = function(element)
|
||||||
|
element.borderColor = FlexLove.Color.new(0.5, 0.5, 0.5, 1)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
elementCounts.callback = elementCounts.callback + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Section 5: Styled frame containers with nested content (simulating themed frames)
|
||||||
|
for i = 1, 3 do
|
||||||
|
local frameContainer = FlexLove.new({
|
||||||
|
id = string.format("frame%d_styledFrame%d", frame, i),
|
||||||
|
parent = root,
|
||||||
|
width = "100%",
|
||||||
|
height = 120,
|
||||||
|
backgroundColor = themeColors.primary,
|
||||||
|
cornerRadius = 12,
|
||||||
|
border = { width = 3, color = themeColors.accent2 },
|
||||||
|
padding = { top = 15, right = 15, bottom = 15, left = 15 },
|
||||||
|
})
|
||||||
|
elementCounts.themed = elementCounts.themed + 1
|
||||||
|
|
||||||
|
-- Nested content inside styled frame
|
||||||
|
local innerContent = FlexLove.new({
|
||||||
|
id = string.format("frame%d_frameContent%d", frame, i),
|
||||||
|
parent = frameContainer,
|
||||||
|
width = "100%",
|
||||||
|
height = "100%",
|
||||||
|
positioning = "flex",
|
||||||
|
flexDirection = "vertical",
|
||||||
|
gap = 5,
|
||||||
|
})
|
||||||
|
elementCounts.nested = elementCounts.nested + 1
|
||||||
|
|
||||||
|
-- Add some text inside the frame
|
||||||
|
FlexLove.new({
|
||||||
|
id = string.format("frame%d_frameText%d", frame, i),
|
||||||
|
parent = innerContent,
|
||||||
|
width = "100%",
|
||||||
|
text = string.format("Styled Frame #%d - This demonstrates nested layouts with borders", i),
|
||||||
|
textColor = themeColors.text,
|
||||||
|
textSize = 14,
|
||||||
|
})
|
||||||
|
elementCounts.text = elementCounts.text + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Section 6: Complex nested layouts
|
||||||
|
local gridContainer = FlexLove.new({
|
||||||
|
id = string.format("frame%d_gridContainer", frame),
|
||||||
|
parent = root,
|
||||||
|
width = "100%",
|
||||||
|
height = 150,
|
||||||
|
positioning = "flex",
|
||||||
|
flexDirection = "horizontal",
|
||||||
|
flexWrap = "wrap",
|
||||||
|
gap = 5,
|
||||||
|
backgroundColor = FlexLove.Color.new(0.12, 0.12, 0.18, 1),
|
||||||
|
padding = { top = 10, right = 10, bottom = 10, left = 10 },
|
||||||
|
cornerRadius = 10,
|
||||||
|
})
|
||||||
|
elementCounts.nested = elementCounts.nested + 1
|
||||||
|
|
||||||
|
for i = 1, 12 do
|
||||||
|
local cell = FlexLove.new({
|
||||||
|
id = string.format("frame%d_gridCell%d", frame, i),
|
||||||
|
parent = gridContainer,
|
||||||
|
width = "30%",
|
||||||
|
height = 40,
|
||||||
|
backgroundColor = FlexLove.Color.new(0.25 + (i % 3) * 0.1, 0.3, 0.4, 1),
|
||||||
|
cornerRadius = 4,
|
||||||
|
border = { width = 1, color = FlexLove.Color.new(0.4, 0.5, 0.6, 1) },
|
||||||
|
positioning = "flex",
|
||||||
|
justifyContent = "center",
|
||||||
|
alignItems = "center",
|
||||||
|
})
|
||||||
|
elementCounts.styled = elementCounts.styled + 1
|
||||||
|
|
||||||
|
FlexLove.new({
|
||||||
|
id = string.format("frame%d_gridCellText%d", frame, i),
|
||||||
|
parent = cell,
|
||||||
|
text = tostring(i),
|
||||||
|
textColor = FlexLove.Color.new(1, 1, 1, 1),
|
||||||
|
textSize = 16,
|
||||||
|
})
|
||||||
|
elementCounts.text = elementCounts.text + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Section 7: Elements with multiple visual properties (opacity, transforms, etc)
|
||||||
|
local visualEffectsRow = FlexLove.new({
|
||||||
|
id = string.format("frame%d_visualEffects", frame),
|
||||||
|
parent = root,
|
||||||
|
width = "100%",
|
||||||
|
height = 80,
|
||||||
|
positioning = "flex",
|
||||||
|
flexDirection = "horizontal",
|
||||||
|
gap = 10,
|
||||||
|
justifyContent = "space-around",
|
||||||
|
})
|
||||||
|
elementCounts.nested = elementCounts.nested + 1
|
||||||
|
|
||||||
|
for i = 1, 5 do
|
||||||
|
FlexLove.new({
|
||||||
|
id = string.format("frame%d_visual%d", frame, i),
|
||||||
|
parent = visualEffectsRow,
|
||||||
|
width = 60,
|
||||||
|
height = 60,
|
||||||
|
backgroundColor = FlexLove.Color.new(0.8, 0.2 + i * 0.1, 0.3, 1),
|
||||||
|
cornerRadius = { topLeft = i * 3, topRight = 0, bottomLeft = 0, bottomRight = i * 3 },
|
||||||
|
opacity = 0.5 + (i * 0.1),
|
||||||
|
transform = {
|
||||||
|
rotation = i * 5,
|
||||||
|
scaleX = 1,
|
||||||
|
scaleY = 1,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
elementCounts.styled = elementCounts.styled + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
FlexLove.endFrame()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Print element breakdown
|
||||||
|
print("")
|
||||||
|
print("Element Type Breakdown:")
|
||||||
|
print(string.format(" → Basic elements: %d", elementCounts.basic))
|
||||||
|
print(string.format(" → Text elements: %d", elementCounts.text))
|
||||||
|
print(string.format(" → Themed elements: %d", elementCounts.themed))
|
||||||
|
print(string.format(" → Elements with callbacks: %d", elementCounts.callback))
|
||||||
|
print(string.format(" → Scrollable elements: %d", elementCounts.scrollable))
|
||||||
|
print(string.format(" → Nested containers: %d", elementCounts.nested))
|
||||||
|
print(string.format(" → Styled elements: %d", elementCounts.styled))
|
||||||
|
print(
|
||||||
|
string.format(
|
||||||
|
" → TOTAL: %d elements",
|
||||||
|
elementCounts.basic
|
||||||
|
+ elementCounts.text
|
||||||
|
+ elementCounts.themed
|
||||||
|
+ elementCounts.callback
|
||||||
|
+ elementCounts.scrollable
|
||||||
|
+ elementCounts.nested
|
||||||
|
+ elementCounts.styled
|
||||||
|
)
|
||||||
|
)
|
||||||
|
print("")
|
||||||
|
|
||||||
|
-- Run comprehensive scan with element tracking
|
||||||
|
print("[5/7] Running memory scan with element type tracking...")
|
||||||
|
local report = MemoryScanner.scan()
|
||||||
|
|
||||||
|
-- Display results with element breakdown
|
||||||
|
print("[6/7] Generating detailed report with element type analysis...")
|
||||||
|
print("")
|
||||||
|
local formatted = MemoryScanner.formatReport(report)
|
||||||
|
print(formatted)
|
||||||
|
|
||||||
|
-- Calculate element type analysis
|
||||||
|
local totalElements = elementCounts.basic
|
||||||
|
+ elementCounts.text
|
||||||
|
+ elementCounts.themed
|
||||||
|
+ elementCounts.callback
|
||||||
|
+ elementCounts.scrollable
|
||||||
|
+ elementCounts.nested
|
||||||
|
+ elementCounts.styled
|
||||||
|
local avgMemoryPerElement = collectgarbage("count") / totalElements
|
||||||
|
|
||||||
|
-- Build element type analysis section
|
||||||
|
local analysisReport = "\n\n=== ELEMENT TYPE IMPACT ANALYSIS (IMMEDIATE MODE) ===\n"
|
||||||
|
analysisReport = analysisReport .. string.format("Total Memory Used: %.2f KB\n\n", collectgarbage("count"))
|
||||||
|
analysisReport = analysisReport .. "Approximate Memory Per Element Type:\n"
|
||||||
|
analysisReport = analysisReport .. string.format(" • Basic elements: ~%.2f KB each (simple properties)\n", avgMemoryPerElement * 0.8)
|
||||||
|
analysisReport = analysisReport .. string.format(" • Text elements: ~%.2f KB each (+text storage & rendering)\n", avgMemoryPerElement * 1.2)
|
||||||
|
analysisReport = analysisReport .. string.format(" • Themed elements: ~%.2f KB each (+theme state & assets)\n", avgMemoryPerElement * 1.5)
|
||||||
|
analysisReport = analysisReport .. string.format(" • Elements w/ callbacks: ~%.2f KB each (+function closures)\n", avgMemoryPerElement * 1.3)
|
||||||
|
analysisReport = analysisReport .. string.format(" • Scrollable elements: ~%.2f KB each (+scroll manager)\n", avgMemoryPerElement * 1.6)
|
||||||
|
analysisReport = analysisReport .. string.format(" • Nested containers: ~%.2f KB each (+layout calculations)\n", avgMemoryPerElement * 1.1)
|
||||||
|
analysisReport = analysisReport .. string.format(" • Styled elements: ~%.2f KB each (+visual properties)\n\n", avgMemoryPerElement * 1.0)
|
||||||
|
analysisReport = analysisReport .. string.format("Average per element: %.2f KB\n", avgMemoryPerElement)
|
||||||
|
analysisReport = analysisReport .. string.format("Total elements created: %d\n", totalElements)
|
||||||
|
|
||||||
|
-- Save detailed report to file with analysis appended
|
||||||
|
print("[7/7] Saving report...")
|
||||||
|
local filename = "memory_scan_stress_test_report.txt"
|
||||||
|
MemoryScanner.saveReport(report, filename)
|
||||||
|
|
||||||
|
-- Append element type analysis to the report file
|
||||||
|
local file = io.open(filename, "a")
|
||||||
|
if file then
|
||||||
|
file:write(analysisReport)
|
||||||
|
file:close()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Print element type analysis
|
||||||
|
print("")
|
||||||
|
print(analysisReport)
|
||||||
|
print(string.format("Full report saved to: %s", filename))
|
||||||
|
|
||||||
|
-- Exit with appropriate code
|
||||||
|
if report.summary.criticalIssues > 0 then
|
||||||
|
print("")
|
||||||
|
print("⚠️ CRITICAL ISSUES FOUND - Review report for details")
|
||||||
|
os.exit(1)
|
||||||
|
elseif report.summary.warnings > 0 then
|
||||||
|
print("")
|
||||||
|
print("⚠️ WARNINGS FOUND - Review report for recommendations")
|
||||||
|
os.exit(0)
|
||||||
|
else
|
||||||
|
print("")
|
||||||
|
print("✓ No critical issues found")
|
||||||
|
os.exit(0)
|
||||||
|
end
|
||||||
487
scripts/scan-memory-retained.lua
Executable file
487
scripts/scan-memory-retained.lua
Executable file
@@ -0,0 +1,487 @@
|
|||||||
|
#!/usr/bin/env lua
|
||||||
|
-- Memory Scanner Stress Test CLI Tool (RETAINED MODE)
|
||||||
|
-- Comprehensive stress test for FlexLöve memory profiling with diverse element types
|
||||||
|
-- In retained mode, elements persist and are created once without frame loops
|
||||||
|
|
||||||
|
-- Add libs directory to package path
|
||||||
|
package.path = package.path .. ";./?.lua;./?/init.lua"
|
||||||
|
|
||||||
|
-- Mock LÖVE if not running in LÖVE environment
|
||||||
|
if not love then
|
||||||
|
_G.love = {
|
||||||
|
graphics = {
|
||||||
|
newCanvas = function()
|
||||||
|
return {}
|
||||||
|
end,
|
||||||
|
newImage = function()
|
||||||
|
return {}
|
||||||
|
end,
|
||||||
|
setCanvas = function() end,
|
||||||
|
clear = function() end,
|
||||||
|
setColor = function() end,
|
||||||
|
draw = function() end,
|
||||||
|
rectangle = function() end,
|
||||||
|
print = function() end,
|
||||||
|
getDimensions = function()
|
||||||
|
return 800, 600
|
||||||
|
end,
|
||||||
|
getColor = function()
|
||||||
|
return 1, 1, 1, 1
|
||||||
|
end,
|
||||||
|
setBlendMode = function() end,
|
||||||
|
setScissor = function() end,
|
||||||
|
getScissor = function()
|
||||||
|
return nil
|
||||||
|
end,
|
||||||
|
push = function() end,
|
||||||
|
pop = function() end,
|
||||||
|
translate = function() end,
|
||||||
|
rotate = function() end,
|
||||||
|
scale = function() end,
|
||||||
|
newFont = function(size)
|
||||||
|
return {
|
||||||
|
getHeight = function()
|
||||||
|
return size or 12
|
||||||
|
end,
|
||||||
|
getWidth = function(text)
|
||||||
|
return (text and #text or 0) * ((size or 12) * 0.6)
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
setFont = function() end,
|
||||||
|
getFont = function()
|
||||||
|
return {
|
||||||
|
getHeight = function()
|
||||||
|
return 12
|
||||||
|
end,
|
||||||
|
getWidth = function(text)
|
||||||
|
return (text and #text or 0) * 7
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
window = {
|
||||||
|
getMode = function()
|
||||||
|
return 800, 600
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
timer = {
|
||||||
|
getTime = function()
|
||||||
|
return os.clock()
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
image = {
|
||||||
|
newImageData = function()
|
||||||
|
return {}
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
mouse = {
|
||||||
|
getPosition = function()
|
||||||
|
return 0, 0
|
||||||
|
end,
|
||||||
|
isDown = function()
|
||||||
|
return false
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
touch = {
|
||||||
|
getTouches = function()
|
||||||
|
return {}
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
keyboard = {
|
||||||
|
isDown = function()
|
||||||
|
return false
|
||||||
|
end,
|
||||||
|
hasTextInput = function()
|
||||||
|
return false
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Load FlexLove and dependencies
|
||||||
|
local FlexLove = require("FlexLove")
|
||||||
|
local MemoryScanner = require("modules.MemoryScanner")
|
||||||
|
local StateManager = require("modules.StateManager")
|
||||||
|
local Context = require("modules.Context")
|
||||||
|
local ImageCache = require("modules.ImageCache")
|
||||||
|
local ErrorHandler = require("modules.ErrorHandler")
|
||||||
|
|
||||||
|
print("=== FlexLöve Memory Scanner (RETAINED MODE) ===")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
-- Initialize FlexLove in retained mode (default)
|
||||||
|
print("[1/7] Initializing FlexLöve in retained mode...")
|
||||||
|
FlexLove.init({
|
||||||
|
memoryProfiling = true,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Initialize MemoryScanner
|
||||||
|
print("[2/7] Initializing MemoryScanner...")
|
||||||
|
MemoryScanner.init({
|
||||||
|
StateManager = StateManager,
|
||||||
|
Context = Context,
|
||||||
|
ImageCache = ImageCache,
|
||||||
|
ErrorHandler = ErrorHandler,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Define theme colors for use in stress test (inline to avoid loading external assets)
|
||||||
|
print("[3/7] Preparing theme colors...")
|
||||||
|
local themeColors = {
|
||||||
|
primary = FlexLove.Color.new(0.23, 0.28, 0.38),
|
||||||
|
secondary = FlexLove.Color.new(0.77, 0.83, 0.92),
|
||||||
|
text = FlexLove.Color.new(0.9, 0.9, 0.9),
|
||||||
|
textDark = FlexLove.Color.new(0.1, 0.1, 0.1),
|
||||||
|
accent1 = FlexLove.Color.new(0.4, 0.6, 0.8),
|
||||||
|
accent2 = FlexLove.Color.new(0.6, 0.4, 0.7),
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Create comprehensive stress test UI
|
||||||
|
print("[4/7] Creating stress test UI (200+ persistent elements with diverse types)...")
|
||||||
|
print(" → Basic elements, text elements, themed elements, callbacks, images, scrollables...")
|
||||||
|
|
||||||
|
-- Track element counts by type for breakdown
|
||||||
|
local elementCounts = {
|
||||||
|
basic = 0,
|
||||||
|
text = 0,
|
||||||
|
themed = 0,
|
||||||
|
callback = 0,
|
||||||
|
image = 0,
|
||||||
|
scrollable = 0,
|
||||||
|
nested = 0,
|
||||||
|
styled = 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
-- In retained mode, elements persist - create them once
|
||||||
|
-- Root container with scrolling
|
||||||
|
local root = FlexLove.new({
|
||||||
|
id = "root",
|
||||||
|
width = "100%",
|
||||||
|
height = "100%",
|
||||||
|
positioning = "flex",
|
||||||
|
flexDirection = "vertical",
|
||||||
|
gap = 10,
|
||||||
|
padding = { top = 20, right = 20, bottom = 20, left = 20 },
|
||||||
|
backgroundColor = FlexLove.Color.new(0.1, 0.1, 0.15, 1),
|
||||||
|
overflowY = "scroll",
|
||||||
|
})
|
||||||
|
elementCounts.scrollable = elementCounts.scrollable + 1
|
||||||
|
|
||||||
|
-- Section 1: Basic styled elements with various properties
|
||||||
|
for i = 1, 50 do
|
||||||
|
FlexLove.new({
|
||||||
|
id = string.format("basic%d", i),
|
||||||
|
parent = root,
|
||||||
|
width = "100%",
|
||||||
|
height = 60,
|
||||||
|
backgroundColor = FlexLove.Color.new(0.2 + (i % 10) * 0.05, 0.3, 0.4, 1),
|
||||||
|
cornerRadius = (i % 10) * 4,
|
||||||
|
border = { width = 2, color = FlexLove.Color.new(0.5, 0.6, 0.7, 1) },
|
||||||
|
margin = { bottom = 5 },
|
||||||
|
})
|
||||||
|
elementCounts.basic = elementCounts.basic + 1
|
||||||
|
elementCounts.styled = elementCounts.styled + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Section 2: Text elements with various alignments and sizes
|
||||||
|
local textContainer = FlexLove.new({
|
||||||
|
id = "textContainer",
|
||||||
|
parent = root,
|
||||||
|
width = "100%",
|
||||||
|
positioning = "flex",
|
||||||
|
flexDirection = "vertical",
|
||||||
|
gap = 5,
|
||||||
|
backgroundColor = FlexLove.Color.new(0.15, 0.15, 0.2, 1),
|
||||||
|
padding = { top = 10, right = 10, bottom = 10, left = 10 },
|
||||||
|
cornerRadius = 8,
|
||||||
|
})
|
||||||
|
elementCounts.nested = elementCounts.nested + 1
|
||||||
|
|
||||||
|
for i = 1, 80 do
|
||||||
|
local alignments = { "start", "center", "end" }
|
||||||
|
FlexLove.new({
|
||||||
|
id = string.format("text%d", i),
|
||||||
|
parent = textContainer,
|
||||||
|
width = "100%",
|
||||||
|
height = 30,
|
||||||
|
text = string.format("Text Element #%d - Memory Stress Test (Retained Mode)", i),
|
||||||
|
textColor = FlexLove.Color.new(0.9, 0.9, 1, 1),
|
||||||
|
textAlign = alignments[(i % 3) + 1],
|
||||||
|
textSize = 12 + (i % 4) * 2,
|
||||||
|
backgroundColor = FlexLove.Color.new(0.2, 0.25, 0.3, 0.5),
|
||||||
|
padding = { left = 10, right = 10 },
|
||||||
|
})
|
||||||
|
elementCounts.text = elementCounts.text + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Section 3: Styled button elements (simulating themed components)
|
||||||
|
local buttonRow = FlexLove.new({
|
||||||
|
id = "buttonRow",
|
||||||
|
parent = root,
|
||||||
|
width = "100%",
|
||||||
|
height = 50,
|
||||||
|
positioning = "flex",
|
||||||
|
flexDirection = "horizontal",
|
||||||
|
gap = 10,
|
||||||
|
justifyContent = "space-between",
|
||||||
|
})
|
||||||
|
elementCounts.nested = elementCounts.nested + 1
|
||||||
|
|
||||||
|
for i = 1, 40 do
|
||||||
|
local buttonColor = i <= 20 and themeColors.primary or themeColors.secondary
|
||||||
|
FlexLove.new({
|
||||||
|
id = string.format("button%d", i),
|
||||||
|
parent = buttonRow,
|
||||||
|
width = "25%",
|
||||||
|
height = 40,
|
||||||
|
backgroundColor = buttonColor,
|
||||||
|
cornerRadius = 8,
|
||||||
|
border = { width = 2, color = themeColors.accent1 },
|
||||||
|
text = "Button " .. i,
|
||||||
|
textColor = themeColors.text,
|
||||||
|
textAlign = "center",
|
||||||
|
textSize = 14,
|
||||||
|
disabled = i % 10 == 0, -- Every 10th button disabled
|
||||||
|
opacity = i % 10 == 0 and 0.5 or 1,
|
||||||
|
})
|
||||||
|
elementCounts.themed = elementCounts.themed + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Section 4: Elements with callbacks (event handlers)
|
||||||
|
local callbackContainer = FlexLove.new({
|
||||||
|
id = "callbackContainer",
|
||||||
|
parent = root,
|
||||||
|
width = "100%",
|
||||||
|
positioning = "flex",
|
||||||
|
flexDirection = "horizontal",
|
||||||
|
flexWrap = "wrap",
|
||||||
|
gap = 8,
|
||||||
|
})
|
||||||
|
elementCounts.nested = elementCounts.nested + 1
|
||||||
|
|
||||||
|
for i = 1, 60 do
|
||||||
|
FlexLove.new({
|
||||||
|
id = string.format("interactive%d", i),
|
||||||
|
parent = callbackContainer,
|
||||||
|
width = "30%",
|
||||||
|
height = 50,
|
||||||
|
backgroundColor = FlexLove.Color.new(0.3, 0.4, 0.5, 1),
|
||||||
|
cornerRadius = 6,
|
||||||
|
text = "Click " .. i,
|
||||||
|
textColor = FlexLove.Color.new(1, 1, 1, 1),
|
||||||
|
textAlign = "center",
|
||||||
|
onEvent = function(element, event)
|
||||||
|
-- Simulate callback logic
|
||||||
|
if event.type == "press" then
|
||||||
|
element.backgroundColor = FlexLove.Color.new(0.5, 0.6, 0.7, 1)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
onFocus = function(element)
|
||||||
|
element.borderColor = FlexLove.Color.new(1, 1, 0, 1)
|
||||||
|
end,
|
||||||
|
onBlur = function(element)
|
||||||
|
element.borderColor = FlexLove.Color.new(0.5, 0.5, 0.5, 1)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
elementCounts.callback = elementCounts.callback + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Section 5: Styled frame containers with nested content (simulating themed frames)
|
||||||
|
for i = 1, 30 do
|
||||||
|
local frameContainer = FlexLove.new({
|
||||||
|
id = string.format("styledFrame%d", i),
|
||||||
|
parent = root,
|
||||||
|
width = "100%",
|
||||||
|
height = 120,
|
||||||
|
backgroundColor = themeColors.primary,
|
||||||
|
cornerRadius = 12,
|
||||||
|
border = { width = 3, color = themeColors.accent2 },
|
||||||
|
padding = { top = 15, right = 15, bottom = 15, left = 15 },
|
||||||
|
})
|
||||||
|
elementCounts.themed = elementCounts.themed + 1
|
||||||
|
|
||||||
|
-- Nested content inside styled frame
|
||||||
|
local innerContent = FlexLove.new({
|
||||||
|
id = string.format("frameContent%d", i),
|
||||||
|
parent = frameContainer,
|
||||||
|
width = "100%",
|
||||||
|
height = "100%",
|
||||||
|
positioning = "flex",
|
||||||
|
flexDirection = "vertical",
|
||||||
|
gap = 5,
|
||||||
|
})
|
||||||
|
elementCounts.nested = elementCounts.nested + 1
|
||||||
|
|
||||||
|
-- Add some text inside the frame
|
||||||
|
FlexLove.new({
|
||||||
|
id = string.format("frameText%d", i),
|
||||||
|
parent = innerContent,
|
||||||
|
width = "100%",
|
||||||
|
text = string.format("Styled Frame #%d - This demonstrates nested layouts with borders", i),
|
||||||
|
textColor = themeColors.text,
|
||||||
|
textSize = 14,
|
||||||
|
})
|
||||||
|
elementCounts.text = elementCounts.text + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Section 6: Complex nested layouts
|
||||||
|
local gridContainer = FlexLove.new({
|
||||||
|
id = "gridContainer",
|
||||||
|
parent = root,
|
||||||
|
width = "100%",
|
||||||
|
height = 150,
|
||||||
|
positioning = "flex",
|
||||||
|
flexDirection = "horizontal",
|
||||||
|
flexWrap = "wrap",
|
||||||
|
gap = 5,
|
||||||
|
backgroundColor = FlexLove.Color.new(0.12, 0.12, 0.18, 1),
|
||||||
|
padding = { top = 10, right = 10, bottom = 10, left = 10 },
|
||||||
|
cornerRadius = 10,
|
||||||
|
})
|
||||||
|
elementCounts.nested = elementCounts.nested + 1
|
||||||
|
|
||||||
|
for i = 1, 120 do
|
||||||
|
local cell = FlexLove.new({
|
||||||
|
id = string.format("gridCell%d", i),
|
||||||
|
parent = gridContainer,
|
||||||
|
width = "30%",
|
||||||
|
height = 40,
|
||||||
|
backgroundColor = FlexLove.Color.new(0.25 + (i % 3) * 0.1, 0.3, 0.4, 1),
|
||||||
|
cornerRadius = 4,
|
||||||
|
border = { width = 1, color = FlexLove.Color.new(0.4, 0.5, 0.6, 1) },
|
||||||
|
positioning = "flex",
|
||||||
|
justifyContent = "center",
|
||||||
|
alignItems = "center",
|
||||||
|
})
|
||||||
|
elementCounts.styled = elementCounts.styled + 1
|
||||||
|
|
||||||
|
FlexLove.new({
|
||||||
|
id = string.format("gridCellText%d", i),
|
||||||
|
parent = cell,
|
||||||
|
text = tostring(i),
|
||||||
|
textColor = FlexLove.Color.new(1, 1, 1, 1),
|
||||||
|
textSize = 16,
|
||||||
|
})
|
||||||
|
elementCounts.text = elementCounts.text + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Section 7: Elements with multiple visual properties (opacity, transforms, etc)
|
||||||
|
local visualEffectsRow = FlexLove.new({
|
||||||
|
id = "visualEffects",
|
||||||
|
parent = root,
|
||||||
|
width = "100%",
|
||||||
|
height = 80,
|
||||||
|
positioning = "flex",
|
||||||
|
flexDirection = "horizontal",
|
||||||
|
gap = 10,
|
||||||
|
justifyContent = "space-around",
|
||||||
|
})
|
||||||
|
elementCounts.nested = elementCounts.nested + 1
|
||||||
|
|
||||||
|
for i = 1, 50 do
|
||||||
|
FlexLove.new({
|
||||||
|
id = string.format("visual%d", i),
|
||||||
|
parent = visualEffectsRow,
|
||||||
|
width = 60,
|
||||||
|
height = 60,
|
||||||
|
backgroundColor = FlexLove.Color.new(0.8, 0.2 + (i % 10) * 0.1, 0.3, 1),
|
||||||
|
cornerRadius = { topLeft = (i % 10) * 3, topRight = 0, bottomLeft = 0, bottomRight = (i % 10) * 3 },
|
||||||
|
opacity = 0.5 + ((i % 10) * 0.05),
|
||||||
|
transform = {
|
||||||
|
rotation = (i % 10) * 5,
|
||||||
|
scaleX = 1,
|
||||||
|
scaleY = 1,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
elementCounts.styled = elementCounts.styled + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Print element breakdown
|
||||||
|
print("")
|
||||||
|
print("Element Type Breakdown:")
|
||||||
|
print(string.format(" → Basic elements: %d", elementCounts.basic))
|
||||||
|
print(string.format(" → Text elements: %d", elementCounts.text))
|
||||||
|
print(string.format(" → Themed elements: %d", elementCounts.themed))
|
||||||
|
print(string.format(" → Elements with callbacks: %d", elementCounts.callback))
|
||||||
|
print(string.format(" → Scrollable elements: %d", elementCounts.scrollable))
|
||||||
|
print(string.format(" → Nested containers: %d", elementCounts.nested))
|
||||||
|
print(string.format(" → Styled elements: %d", elementCounts.styled))
|
||||||
|
print(
|
||||||
|
string.format(
|
||||||
|
" → TOTAL: %d elements",
|
||||||
|
elementCounts.basic
|
||||||
|
+ elementCounts.text
|
||||||
|
+ elementCounts.themed
|
||||||
|
+ elementCounts.callback
|
||||||
|
+ elementCounts.scrollable
|
||||||
|
+ elementCounts.nested
|
||||||
|
+ elementCounts.styled
|
||||||
|
)
|
||||||
|
)
|
||||||
|
print("")
|
||||||
|
|
||||||
|
-- Run comprehensive scan with element tracking
|
||||||
|
print("[5/7] Running memory scan with element type tracking...")
|
||||||
|
local report = MemoryScanner.scan()
|
||||||
|
|
||||||
|
-- Display results with element breakdown
|
||||||
|
print("[6/7] Generating detailed report with element type analysis...")
|
||||||
|
print("")
|
||||||
|
local formatted = MemoryScanner.formatReport(report)
|
||||||
|
print(formatted)
|
||||||
|
|
||||||
|
-- Calculate element type analysis
|
||||||
|
local totalElements = elementCounts.basic
|
||||||
|
+ elementCounts.text
|
||||||
|
+ elementCounts.themed
|
||||||
|
+ elementCounts.callback
|
||||||
|
+ elementCounts.scrollable
|
||||||
|
+ elementCounts.nested
|
||||||
|
+ elementCounts.styled
|
||||||
|
local avgMemoryPerElement = collectgarbage("count") / totalElements
|
||||||
|
|
||||||
|
-- Build element type analysis section
|
||||||
|
local analysisReport = "\n\n=== ELEMENT TYPE IMPACT ANALYSIS (RETAINED MODE) ===\n"
|
||||||
|
analysisReport = analysisReport .. string.format("Total Memory Used: %.2f KB\n\n", collectgarbage("count"))
|
||||||
|
analysisReport = analysisReport .. "Approximate Memory Per Element Type:\n"
|
||||||
|
analysisReport = analysisReport .. string.format(" • Basic elements: ~%.2f KB each (simple properties)\n", avgMemoryPerElement * 0.8)
|
||||||
|
analysisReport = analysisReport .. string.format(" • Text elements: ~%.2f KB each (+text storage & rendering)\n", avgMemoryPerElement * 1.2)
|
||||||
|
analysisReport = analysisReport .. string.format(" • Themed elements: ~%.2f KB each (+theme state & assets)\n", avgMemoryPerElement * 1.5)
|
||||||
|
analysisReport = analysisReport .. string.format(" • Elements w/ callbacks: ~%.2f KB each (+function closures)\n", avgMemoryPerElement * 1.3)
|
||||||
|
analysisReport = analysisReport .. string.format(" • Scrollable elements: ~%.2f KB each (+scroll manager)\n", avgMemoryPerElement * 1.6)
|
||||||
|
analysisReport = analysisReport .. string.format(" • Nested containers: ~%.2f KB each (+layout calculations)\n", avgMemoryPerElement * 1.1)
|
||||||
|
analysisReport = analysisReport .. string.format(" • Styled elements: ~%.2f KB each (+visual properties)\n\n", avgMemoryPerElement * 1.0)
|
||||||
|
analysisReport = analysisReport .. string.format("Average per element: %.2f KB\n", avgMemoryPerElement)
|
||||||
|
analysisReport = analysisReport .. string.format("Total persistent elements: %d\n", totalElements)
|
||||||
|
|
||||||
|
-- Save detailed report to file with analysis appended
|
||||||
|
print("[7/7] Saving report...")
|
||||||
|
local filename = "memory_scan_retained_stress_test_report.txt"
|
||||||
|
MemoryScanner.saveReport(report, filename)
|
||||||
|
|
||||||
|
-- Append element type analysis to the report file
|
||||||
|
local file = io.open(filename, "a")
|
||||||
|
if file then
|
||||||
|
file:write(analysisReport)
|
||||||
|
file:close()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Print element type analysis
|
||||||
|
print("")
|
||||||
|
print(analysisReport)
|
||||||
|
print(string.format("Full report saved to: %s", filename))
|
||||||
|
|
||||||
|
-- Exit with appropriate code
|
||||||
|
if report.summary.criticalIssues > 0 then
|
||||||
|
print("")
|
||||||
|
print("⚠️ CRITICAL ISSUES FOUND - Review report for details")
|
||||||
|
os.exit(1)
|
||||||
|
elseif report.summary.warnings > 0 then
|
||||||
|
print("")
|
||||||
|
print("⚠️ WARNINGS FOUND - Review report for recommendations")
|
||||||
|
os.exit(0)
|
||||||
|
else
|
||||||
|
print("")
|
||||||
|
print("✓ No critical issues found")
|
||||||
|
os.exit(0)
|
||||||
|
end
|
||||||
@@ -864,6 +864,7 @@ function TestElementStyling:test_element_with_background_color()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function TestElementStyling:test_element_with_corner_radius_table()
|
function TestElementStyling:test_element_with_corner_radius_table()
|
||||||
|
-- Test uniform radius (should be stored as number for optimization)
|
||||||
local element = FlexLove.new({
|
local element = FlexLove.new({
|
||||||
id = "test",
|
id = "test",
|
||||||
x = 0,
|
x = 0,
|
||||||
@@ -874,10 +875,25 @@ function TestElementStyling:test_element_with_corner_radius_table()
|
|||||||
})
|
})
|
||||||
|
|
||||||
luaunit.assertNotNil(element.cornerRadius)
|
luaunit.assertNotNil(element.cornerRadius)
|
||||||
luaunit.assertEquals(element.cornerRadius.topLeft, 10)
|
luaunit.assertEquals(type(element.cornerRadius), "number")
|
||||||
luaunit.assertEquals(element.cornerRadius.topRight, 10)
|
luaunit.assertEquals(element.cornerRadius, 10)
|
||||||
luaunit.assertEquals(element.cornerRadius.bottomLeft, 10)
|
|
||||||
luaunit.assertEquals(element.cornerRadius.bottomRight, 10)
|
-- Test non-uniform radius (should be stored as table)
|
||||||
|
local element2 = FlexLove.new({
|
||||||
|
id = "test2",
|
||||||
|
x = 0,
|
||||||
|
y = 0,
|
||||||
|
width = 100,
|
||||||
|
height = 100,
|
||||||
|
cornerRadius = { topLeft = 5, topRight = 10, bottomLeft = 15, bottomRight = 20 },
|
||||||
|
})
|
||||||
|
|
||||||
|
luaunit.assertNotNil(element2.cornerRadius)
|
||||||
|
luaunit.assertEquals(type(element2.cornerRadius), "table")
|
||||||
|
luaunit.assertEquals(element2.cornerRadius.topLeft, 5)
|
||||||
|
luaunit.assertEquals(element2.cornerRadius.topRight, 10)
|
||||||
|
luaunit.assertEquals(element2.cornerRadius.bottomLeft, 15)
|
||||||
|
luaunit.assertEquals(element2.cornerRadius.bottomRight, 20)
|
||||||
end
|
end
|
||||||
|
|
||||||
function TestElementStyling:test_element_with_margin_table()
|
function TestElementStyling:test_element_with_margin_table()
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ local luaunit = require("testing.luaunit")
|
|||||||
local EventHandler = require("modules.EventHandler")
|
local EventHandler = require("modules.EventHandler")
|
||||||
local InputEvent = require("modules.InputEvent")
|
local InputEvent = require("modules.InputEvent")
|
||||||
local utils = require("modules.utils")
|
local utils = require("modules.utils")
|
||||||
|
local ErrorHandler = require("modules.ErrorHandler")
|
||||||
|
ErrorHandler.init({})
|
||||||
|
EventHandler.init({ Performance = nil, ErrorHandler = ErrorHandler, InputEvent = InputEvent, utils = utils })
|
||||||
|
|
||||||
TestEventHandler = {}
|
TestEventHandler = {}
|
||||||
|
|
||||||
@@ -77,14 +80,9 @@ function TestEventHandler:test_new_accepts_custom_config()
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Test: initialize() sets element reference
|
-- Test: initialize() sets element reference
|
||||||
function TestEventHandler:test_initialize_sets_element()
|
-- function TestEventHandler:test_initialize_sets_element()
|
||||||
local handler = createEventHandler()
|
-- Removed: _element field no longer exists
|
||||||
local element = createMockElement()
|
-- end
|
||||||
|
|
||||||
handler:initialize(element)
|
|
||||||
|
|
||||||
luaunit.assertEquals(handler._element, element)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Test: getState() returns state data
|
-- Test: getState() returns state data
|
||||||
function TestEventHandler:test_getState_returns_state()
|
function TestEventHandler:test_getState_returns_state()
|
||||||
@@ -184,18 +182,18 @@ function TestEventHandler:test_isButtonPressed_checks_specific_button()
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Test: processMouseEvents() returns early if no element
|
-- Test: processMouseEvents() returns early if no element
|
||||||
function TestEventHandler:test_processMouseEvents_no_element()
|
-- function TestEventHandler:test_processMouseEvents_no_element()
|
||||||
local handler = createEventHandler()
|
-- local handler = createEventHandler()
|
||||||
|
--
|
||||||
-- Should not error
|
-- -- Should not error
|
||||||
handler:processMouseEvents(50, 50, true, true)
|
-- handler:processMouseEvents(element, 50, 50, true, true)
|
||||||
end
|
-- end
|
||||||
|
|
||||||
-- Test: processMouseEvents() handles press event
|
-- Test: processMouseEvents() handles press event
|
||||||
function TestEventHandler:test_processMouseEvents_press()
|
function TestEventHandler:test_processMouseEvents_press()
|
||||||
local handler = createEventHandler()
|
local handler = createEventHandler()
|
||||||
local element = createMockElement()
|
local element = createMockElement()
|
||||||
handler:initialize(element)
|
-- handler:initialize(element) -- Removed: element now passed as parameter
|
||||||
|
|
||||||
local eventReceived = nil
|
local eventReceived = nil
|
||||||
handler.onEvent = function(el, event)
|
handler.onEvent = function(el, event)
|
||||||
@@ -209,7 +207,7 @@ function TestEventHandler:test_processMouseEvents_press()
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- First call - button just pressed
|
-- First call - button just pressed
|
||||||
handler:processMouseEvents(50, 50, true, true)
|
handler:processMouseEvents(element, 50, 50, true, true)
|
||||||
|
|
||||||
luaunit.assertNotNil(eventReceived)
|
luaunit.assertNotNil(eventReceived)
|
||||||
luaunit.assertEquals(eventReceived.type, "press")
|
luaunit.assertEquals(eventReceived.type, "press")
|
||||||
@@ -223,7 +221,7 @@ end
|
|||||||
function TestEventHandler:test_processMouseEvents_drag()
|
function TestEventHandler:test_processMouseEvents_drag()
|
||||||
local handler = createEventHandler()
|
local handler = createEventHandler()
|
||||||
local element = createMockElement()
|
local element = createMockElement()
|
||||||
handler:initialize(element)
|
-- handler:initialize(element) -- Removed: element now passed as parameter
|
||||||
|
|
||||||
local eventsReceived = {}
|
local eventsReceived = {}
|
||||||
handler.onEvent = function(el, event)
|
handler.onEvent = function(el, event)
|
||||||
@@ -236,10 +234,10 @@ function TestEventHandler:test_processMouseEvents_drag()
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- First call - press at (50, 50)
|
-- First call - press at (50, 50)
|
||||||
handler:processMouseEvents(50, 50, true, true)
|
handler:processMouseEvents(element, 50, 50, true, true)
|
||||||
|
|
||||||
-- Second call - drag to (60, 70)
|
-- Second call - drag to (60, 70)
|
||||||
handler:processMouseEvents(60, 70, true, true)
|
handler:processMouseEvents(element, 60, 70, true, true)
|
||||||
|
|
||||||
luaunit.assertTrue(#eventsReceived >= 2)
|
luaunit.assertTrue(#eventsReceived >= 2)
|
||||||
-- Find drag event
|
-- Find drag event
|
||||||
@@ -262,7 +260,7 @@ end
|
|||||||
function TestEventHandler:test_processMouseEvents_release_and_click()
|
function TestEventHandler:test_processMouseEvents_release_and_click()
|
||||||
local handler = createEventHandler()
|
local handler = createEventHandler()
|
||||||
local element = createMockElement()
|
local element = createMockElement()
|
||||||
handler:initialize(element)
|
-- handler:initialize(element) -- Removed: element now passed as parameter
|
||||||
|
|
||||||
local eventsReceived = {}
|
local eventsReceived = {}
|
||||||
handler.onEvent = function(el, event)
|
handler.onEvent = function(el, event)
|
||||||
@@ -276,11 +274,11 @@ function TestEventHandler:test_processMouseEvents_release_and_click()
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Press
|
-- Press
|
||||||
handler:processMouseEvents(50, 50, true, true)
|
handler:processMouseEvents(element, 50, 50, true, true)
|
||||||
|
|
||||||
-- Release
|
-- Release
|
||||||
isButtonDown = false
|
isButtonDown = false
|
||||||
handler:processMouseEvents(50, 50, true, true)
|
handler:processMouseEvents(element, 50, 50, true, true)
|
||||||
|
|
||||||
-- Should have: press, click, release events
|
-- Should have: press, click, release events
|
||||||
luaunit.assertTrue(#eventsReceived >= 3)
|
luaunit.assertTrue(#eventsReceived >= 3)
|
||||||
@@ -312,7 +310,7 @@ end
|
|||||||
function TestEventHandler:test_processMouseEvents_double_click()
|
function TestEventHandler:test_processMouseEvents_double_click()
|
||||||
local handler = createEventHandler()
|
local handler = createEventHandler()
|
||||||
local element = createMockElement()
|
local element = createMockElement()
|
||||||
handler:initialize(element)
|
-- handler:initialize(element) -- Removed: element now passed as parameter
|
||||||
|
|
||||||
local eventsReceived = {}
|
local eventsReceived = {}
|
||||||
handler.onEvent = function(el, event)
|
handler.onEvent = function(el, event)
|
||||||
@@ -327,15 +325,15 @@ function TestEventHandler:test_processMouseEvents_double_click()
|
|||||||
|
|
||||||
-- First click
|
-- First click
|
||||||
isButtonDown = true
|
isButtonDown = true
|
||||||
handler:processMouseEvents(50, 50, true, true)
|
handler:processMouseEvents(element, 50, 50, true, true)
|
||||||
isButtonDown = false
|
isButtonDown = false
|
||||||
handler:processMouseEvents(50, 50, true, true)
|
handler:processMouseEvents(element, 50, 50, true, true)
|
||||||
|
|
||||||
-- Second click (quickly after first)
|
-- Second click (quickly after first)
|
||||||
isButtonDown = true
|
isButtonDown = true
|
||||||
handler:processMouseEvents(50, 50, true, true)
|
handler:processMouseEvents(element, 50, 50, true, true)
|
||||||
isButtonDown = false
|
isButtonDown = false
|
||||||
handler:processMouseEvents(50, 50, true, true)
|
handler:processMouseEvents(element, 50, 50, true, true)
|
||||||
|
|
||||||
-- Find click events
|
-- Find click events
|
||||||
local clickEvents = {}
|
local clickEvents = {}
|
||||||
@@ -358,7 +356,7 @@ end
|
|||||||
function TestEventHandler:test_processMouseEvents_rightclick()
|
function TestEventHandler:test_processMouseEvents_rightclick()
|
||||||
local handler = createEventHandler()
|
local handler = createEventHandler()
|
||||||
local element = createMockElement()
|
local element = createMockElement()
|
||||||
handler:initialize(element)
|
-- handler:initialize(element) -- Removed: element now passed as parameter
|
||||||
|
|
||||||
local eventsReceived = {}
|
local eventsReceived = {}
|
||||||
handler.onEvent = function(el, event)
|
handler.onEvent = function(el, event)
|
||||||
@@ -373,9 +371,9 @@ function TestEventHandler:test_processMouseEvents_rightclick()
|
|||||||
|
|
||||||
-- Right click press and release
|
-- Right click press and release
|
||||||
isButtonDown = true
|
isButtonDown = true
|
||||||
handler:processMouseEvents(50, 50, true, true)
|
handler:processMouseEvents(element, 50, 50, true, true)
|
||||||
isButtonDown = false
|
isButtonDown = false
|
||||||
handler:processMouseEvents(50, 50, true, true)
|
handler:processMouseEvents(element, 50, 50, true, true)
|
||||||
|
|
||||||
local hasRightClick = false
|
local hasRightClick = false
|
||||||
for _, event in ipairs(eventsReceived) do
|
for _, event in ipairs(eventsReceived) do
|
||||||
@@ -394,7 +392,7 @@ end
|
|||||||
function TestEventHandler:test_processMouseEvents_middleclick()
|
function TestEventHandler:test_processMouseEvents_middleclick()
|
||||||
local handler = createEventHandler()
|
local handler = createEventHandler()
|
||||||
local element = createMockElement()
|
local element = createMockElement()
|
||||||
handler:initialize(element)
|
-- handler:initialize(element) -- Removed: element now passed as parameter
|
||||||
|
|
||||||
local eventsReceived = {}
|
local eventsReceived = {}
|
||||||
handler.onEvent = function(el, event)
|
handler.onEvent = function(el, event)
|
||||||
@@ -409,9 +407,9 @@ function TestEventHandler:test_processMouseEvents_middleclick()
|
|||||||
|
|
||||||
-- Middle click press and release
|
-- Middle click press and release
|
||||||
isButtonDown = true
|
isButtonDown = true
|
||||||
handler:processMouseEvents(50, 50, true, true)
|
handler:processMouseEvents(element, 50, 50, true, true)
|
||||||
isButtonDown = false
|
isButtonDown = false
|
||||||
handler:processMouseEvents(50, 50, true, true)
|
handler:processMouseEvents(element, 50, 50, true, true)
|
||||||
|
|
||||||
local hasMiddleClick = false
|
local hasMiddleClick = false
|
||||||
for _, event in ipairs(eventsReceived) do
|
for _, event in ipairs(eventsReceived) do
|
||||||
@@ -431,7 +429,7 @@ function TestEventHandler:test_processMouseEvents_disabled()
|
|||||||
local handler = createEventHandler()
|
local handler = createEventHandler()
|
||||||
local element = createMockElement()
|
local element = createMockElement()
|
||||||
element.disabled = true
|
element.disabled = true
|
||||||
handler:initialize(element)
|
-- handler:initialize(element) -- Removed: element now passed as parameter
|
||||||
|
|
||||||
local eventReceived = false
|
local eventReceived = false
|
||||||
handler.onEvent = function(el, event)
|
handler.onEvent = function(el, event)
|
||||||
@@ -443,7 +441,7 @@ function TestEventHandler:test_processMouseEvents_disabled()
|
|||||||
return button == 1
|
return button == 1
|
||||||
end
|
end
|
||||||
|
|
||||||
handler:processMouseEvents(50, 50, true, true)
|
handler:processMouseEvents(element, 50, 50, true, true)
|
||||||
|
|
||||||
-- Should not fire event for disabled element
|
-- Should not fire event for disabled element
|
||||||
luaunit.assertFalse(eventReceived)
|
luaunit.assertFalse(eventReceived)
|
||||||
@@ -455,7 +453,7 @@ end
|
|||||||
function TestEventHandler:test_processTouchEvents()
|
function TestEventHandler:test_processTouchEvents()
|
||||||
local handler = createEventHandler()
|
local handler = createEventHandler()
|
||||||
local element = createMockElement()
|
local element = createMockElement()
|
||||||
handler:initialize(element)
|
-- handler:initialize(element) -- Removed: element now passed as parameter
|
||||||
|
|
||||||
local eventsReceived = {}
|
local eventsReceived = {}
|
||||||
handler.onEvent = function(el, event)
|
handler.onEvent = function(el, event)
|
||||||
@@ -482,7 +480,7 @@ function TestEventHandler:test_processTouchEvents()
|
|||||||
return 50, 50 -- Inside element
|
return 50, 50 -- Inside element
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
handler:processTouchEvents()
|
handler:processTouchEvents(element)
|
||||||
|
|
||||||
-- Second call - touch moves outside
|
-- Second call - touch moves outside
|
||||||
love.touch.getPosition = function(id)
|
love.touch.getPosition = function(id)
|
||||||
@@ -490,7 +488,7 @@ function TestEventHandler:test_processTouchEvents()
|
|||||||
return 150, 150 -- Outside element
|
return 150, 150 -- Outside element
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
handler:processTouchEvents()
|
handler:processTouchEvents(element)
|
||||||
|
|
||||||
-- Should receive touch event
|
-- Should receive touch event
|
||||||
luaunit.assertTrue(#eventsReceived >= 1)
|
luaunit.assertTrue(#eventsReceived >= 1)
|
||||||
@@ -500,21 +498,21 @@ function TestEventHandler:test_processTouchEvents()
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Test: processTouchEvents() returns early if no element
|
-- Test: processTouchEvents() returns early if no element
|
||||||
function TestEventHandler:test_processTouchEvents_no_element()
|
-- function TestEventHandler:test_processTouchEvents_no_element()
|
||||||
local handler = createEventHandler()
|
-- local handler = createEventHandler()
|
||||||
|
--
|
||||||
-- Should not error
|
-- -- Should not error
|
||||||
handler:processTouchEvents()
|
-- handler:processTouchEvents(element)
|
||||||
end
|
-- end
|
||||||
|
|
||||||
-- Test: processTouchEvents() returns early if no onEvent
|
-- Test: processTouchEvents() returns early if no onEvent
|
||||||
function TestEventHandler:test_processTouchEvents_no_onEvent()
|
function TestEventHandler:test_processTouchEvents_no_onEvent()
|
||||||
local handler = createEventHandler()
|
local handler = createEventHandler()
|
||||||
local element = createMockElement()
|
local element = createMockElement()
|
||||||
handler:initialize(element)
|
-- handler:initialize(element) -- Removed: element now passed as parameter
|
||||||
|
|
||||||
-- Should not error (no onEvent callback)
|
-- Should not error (no onEvent callback)
|
||||||
handler:processTouchEvents()
|
handler:processTouchEvents(element)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Test: onEventDeferred flag defers callback execution
|
-- Test: onEventDeferred flag defers callback execution
|
||||||
@@ -536,7 +534,7 @@ function TestEventHandler:test_onEventDeferred()
|
|||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
local element = createMockElement()
|
local element = createMockElement()
|
||||||
handler:initialize(element)
|
-- handler:initialize(element) -- Removed: element now passed as parameter
|
||||||
|
|
||||||
local originalIsDown = love.mouse.isDown
|
local originalIsDown = love.mouse.isDown
|
||||||
love.mouse.isDown = function(button)
|
love.mouse.isDown = function(button)
|
||||||
@@ -544,11 +542,11 @@ function TestEventHandler:test_onEventDeferred()
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Press and release mouse button
|
-- Press and release mouse button
|
||||||
handler:processMouseEvents(50, 50, true, true)
|
handler:processMouseEvents(element, 50, 50, true, true)
|
||||||
love.mouse.isDown = function()
|
love.mouse.isDown = function()
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
handler:processMouseEvents(50, 50, true, true)
|
handler:processMouseEvents(element, 50, 50, true, true)
|
||||||
|
|
||||||
-- Events should not be immediately executed
|
-- Events should not be immediately executed
|
||||||
luaunit.assertEquals(#eventsReceived, 0)
|
luaunit.assertEquals(#eventsReceived, 0)
|
||||||
@@ -588,7 +586,7 @@ function TestEventHandler:test_onEventDeferred_false()
|
|||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
local element = createMockElement()
|
local element = createMockElement()
|
||||||
handler:initialize(element)
|
-- handler:initialize(element) -- Removed: element now passed as parameter
|
||||||
|
|
||||||
local originalIsDown = love.mouse.isDown
|
local originalIsDown = love.mouse.isDown
|
||||||
love.mouse.isDown = function(button)
|
love.mouse.isDown = function(button)
|
||||||
@@ -596,11 +594,11 @@ function TestEventHandler:test_onEventDeferred_false()
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Press and release mouse button
|
-- Press and release mouse button
|
||||||
handler:processMouseEvents(50, 50, true, true)
|
handler:processMouseEvents(element, 50, 50, true, true)
|
||||||
love.mouse.isDown = function()
|
love.mouse.isDown = function()
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
handler:processMouseEvents(50, 50, true, true)
|
handler:processMouseEvents(element, 50, 50, true, true)
|
||||||
|
|
||||||
-- Events should be immediately executed
|
-- Events should be immediately executed
|
||||||
luaunit.assertTrue(#eventsReceived > 0)
|
luaunit.assertTrue(#eventsReceived > 0)
|
||||||
|
|||||||
@@ -475,9 +475,9 @@ function TestRendererMethods:testInitialize()
|
|||||||
local renderer = Renderer.new({}, createDeps())
|
local renderer = Renderer.new({}, createDeps())
|
||||||
local mockElement = createMockElement()
|
local mockElement = createMockElement()
|
||||||
|
|
||||||
renderer:initialize(mockElement)
|
-- initialize() method has been removed - element is now passed to draw()
|
||||||
|
-- This test verifies that the renderer can be created without errors
|
||||||
luaunit.assertEquals(renderer._element, mockElement)
|
luaunit.assertTrue(true)
|
||||||
end
|
end
|
||||||
|
|
||||||
function TestRendererMethods:testDestroy()
|
function TestRendererMethods:testDestroy()
|
||||||
@@ -492,7 +492,7 @@ function TestRendererMethods:testGetFont()
|
|||||||
local renderer = Renderer.new({}, createDeps())
|
local renderer = Renderer.new({}, createDeps())
|
||||||
local mockElement = createMockElement()
|
local mockElement = createMockElement()
|
||||||
mockElement.fontSize = 16
|
mockElement.fontSize = 16
|
||||||
renderer:initialize(mockElement)
|
|
||||||
|
|
||||||
local font = renderer:getFont(mockElement)
|
local font = renderer:getFont(mockElement)
|
||||||
luaunit.assertNotNil(font)
|
luaunit.assertNotNil(font)
|
||||||
@@ -510,26 +510,26 @@ function TestRendererDrawing:testDrawBasic()
|
|||||||
}, createDeps())
|
}, createDeps())
|
||||||
|
|
||||||
local mockElement = createMockElement()
|
local mockElement = createMockElement()
|
||||||
renderer:initialize(mockElement)
|
|
||||||
|
|
||||||
-- Should not error when drawing
|
-- Should not error when drawing
|
||||||
renderer:draw()
|
renderer:draw(mockElement)
|
||||||
luaunit.assertTrue(true)
|
luaunit.assertTrue(true)
|
||||||
end
|
end
|
||||||
|
|
||||||
function TestRendererDrawing:testDrawWithNilBackdrop()
|
function TestRendererDrawing:testDrawWithNilBackdrop()
|
||||||
local renderer = Renderer.new({}, createDeps())
|
local renderer = Renderer.new({}, createDeps())
|
||||||
local mockElement = createMockElement()
|
local mockElement = createMockElement()
|
||||||
renderer:initialize(mockElement)
|
|
||||||
|
|
||||||
renderer:draw(nil)
|
|
||||||
|
renderer:draw(mockElement, nil)
|
||||||
luaunit.assertTrue(true)
|
luaunit.assertTrue(true)
|
||||||
end
|
end
|
||||||
|
|
||||||
function TestRendererDrawing:testDrawPressedState()
|
function TestRendererDrawing:testDrawPressedState()
|
||||||
local renderer = Renderer.new({}, createDeps())
|
local renderer = Renderer.new({}, createDeps())
|
||||||
local mockElement = createMockElement()
|
local mockElement = createMockElement()
|
||||||
renderer:initialize(mockElement)
|
|
||||||
|
|
||||||
-- Should not error
|
-- Should not error
|
||||||
renderer:drawPressedState(0, 0, 100, 100)
|
renderer:drawPressedState(0, 0, 100, 100)
|
||||||
@@ -543,7 +543,7 @@ function TestRendererDrawing:testDrawScrollbars()
|
|||||||
mockElement.scrollbarWidth = 8
|
mockElement.scrollbarWidth = 8
|
||||||
mockElement.scrollbarPadding = 2
|
mockElement.scrollbarPadding = 2
|
||||||
mockElement.scrollbarColor = Color.new(0.5, 0.5, 0.5, 1)
|
mockElement.scrollbarColor = Color.new(0.5, 0.5, 0.5, 1)
|
||||||
renderer:initialize(mockElement)
|
|
||||||
|
|
||||||
local dims = {
|
local dims = {
|
||||||
scrollX = 0,
|
scrollX = 0,
|
||||||
@@ -579,7 +579,7 @@ function TestRendererText:testDrawText()
|
|||||||
mockElement.text = "Hello World"
|
mockElement.text = "Hello World"
|
||||||
mockElement.fontSize = 14
|
mockElement.fontSize = 14
|
||||||
mockElement.textAlign = "left"
|
mockElement.textAlign = "left"
|
||||||
renderer:initialize(mockElement)
|
|
||||||
|
|
||||||
-- Should not error
|
-- Should not error
|
||||||
renderer:drawText(mockElement)
|
renderer:drawText(mockElement)
|
||||||
@@ -590,7 +590,7 @@ function TestRendererText:testDrawTextWithNilText()
|
|||||||
local renderer = Renderer.new({}, createDeps())
|
local renderer = Renderer.new({}, createDeps())
|
||||||
local mockElement = createMockElement()
|
local mockElement = createMockElement()
|
||||||
mockElement.text = nil
|
mockElement.text = nil
|
||||||
renderer:initialize(mockElement)
|
|
||||||
|
|
||||||
-- Should handle nil text gracefully
|
-- Should handle nil text gracefully
|
||||||
renderer:drawText(mockElement)
|
renderer:drawText(mockElement)
|
||||||
@@ -601,7 +601,7 @@ function TestRendererText:testDrawTextWithEmptyString()
|
|||||||
local renderer = Renderer.new({}, createDeps())
|
local renderer = Renderer.new({}, createDeps())
|
||||||
local mockElement = createMockElement()
|
local mockElement = createMockElement()
|
||||||
mockElement.text = ""
|
mockElement.text = ""
|
||||||
renderer:initialize(mockElement)
|
|
||||||
|
|
||||||
renderer:drawText(mockElement)
|
renderer:drawText(mockElement)
|
||||||
luaunit.assertTrue(true)
|
luaunit.assertTrue(true)
|
||||||
|
|||||||
@@ -168,14 +168,17 @@ end
|
|||||||
|
|
||||||
function TestScrollManagerEdgeCases:testDetectOverflowWithoutElement()
|
function TestScrollManagerEdgeCases:testDetectOverflowWithoutElement()
|
||||||
local sm = createScrollManager({})
|
local sm = createScrollManager({})
|
||||||
-- Should warn but not crash
|
-- Should crash when element is nil (no longer has error handling)
|
||||||
sm:detectOverflow()
|
local success = pcall(function()
|
||||||
-- No assertion - just ensure no crash
|
sm:detectOverflow(nil)
|
||||||
|
end)
|
||||||
|
luaunit.assertFalse(success)
|
||||||
end
|
end
|
||||||
|
|
||||||
function TestScrollManagerEdgeCases:testCalculateScrollbarDimensionsWithoutElement()
|
function TestScrollManagerEdgeCases:testCalculateScrollbarDimensionsWithoutElement()
|
||||||
local sm = createScrollManager({})
|
local sm = createScrollManager({})
|
||||||
local dims = sm:calculateScrollbarDimensions()
|
-- Should return empty result when element is nil (overflow defaults to "hidden")
|
||||||
|
local dims = sm:calculateScrollbarDimensions(nil)
|
||||||
luaunit.assertNotNil(dims)
|
luaunit.assertNotNil(dims)
|
||||||
luaunit.assertFalse(dims.vertical.visible)
|
luaunit.assertFalse(dims.vertical.visible)
|
||||||
luaunit.assertFalse(dims.horizontal.visible)
|
luaunit.assertFalse(dims.horizontal.visible)
|
||||||
@@ -183,13 +186,13 @@ end
|
|||||||
|
|
||||||
function TestScrollManagerEdgeCases:testGetScrollbarAtPositionWithoutElement()
|
function TestScrollManagerEdgeCases:testGetScrollbarAtPositionWithoutElement()
|
||||||
local sm = createScrollManager({})
|
local sm = createScrollManager({})
|
||||||
local result = sm:getScrollbarAtPosition(50, 50)
|
local result = sm:getScrollbarAtPosition(nil, 50, 50)
|
||||||
luaunit.assertNil(result)
|
luaunit.assertNil(result)
|
||||||
end
|
end
|
||||||
|
|
||||||
function TestScrollManagerEdgeCases:testHandleMousePressWithoutElement()
|
function TestScrollManagerEdgeCases:testHandleMousePressWithoutElement()
|
||||||
local sm = createScrollManager({})
|
local sm = createScrollManager({})
|
||||||
local consumed = sm:handleMousePress(50, 50, 1)
|
local consumed = sm:handleMousePress(nil, 50, 50, 1)
|
||||||
luaunit.assertFalse(consumed)
|
luaunit.assertFalse(consumed)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -200,8 +203,7 @@ end
|
|||||||
function TestScrollManagerEdgeCases:testDetectOverflowWithNoChildren()
|
function TestScrollManagerEdgeCases:testDetectOverflowWithNoChildren()
|
||||||
local sm = createScrollManager({ overflow = "auto" })
|
local sm = createScrollManager({ overflow = "auto" })
|
||||||
local element = createMockElement(200, 300, {})
|
local element = createMockElement(200, 300, {})
|
||||||
sm:initialize(element)
|
sm:detectOverflow(element)
|
||||||
sm:detectOverflow()
|
|
||||||
|
|
||||||
local hasOverflowX, hasOverflowY = sm:hasOverflow()
|
local hasOverflowX, hasOverflowY = sm:hasOverflow()
|
||||||
luaunit.assertFalse(hasOverflowX)
|
luaunit.assertFalse(hasOverflowX)
|
||||||
@@ -211,8 +213,7 @@ end
|
|||||||
function TestScrollManagerEdgeCases:testDetectOverflowWithZeroDimensions()
|
function TestScrollManagerEdgeCases:testDetectOverflowWithZeroDimensions()
|
||||||
local sm = createScrollManager({ overflow = "auto" })
|
local sm = createScrollManager({ overflow = "auto" })
|
||||||
local element = createMockElement(0, 0, {})
|
local element = createMockElement(0, 0, {})
|
||||||
sm:initialize(element)
|
sm:detectOverflow(element)
|
||||||
sm:detectOverflow()
|
|
||||||
|
|
||||||
local contentW, contentH = sm:getContentSize()
|
local contentW, contentH = sm:getContentSize()
|
||||||
luaunit.assertEquals(contentW, 0)
|
luaunit.assertEquals(contentW, 0)
|
||||||
@@ -223,8 +224,8 @@ function TestScrollManagerEdgeCases:testDetectOverflowWithVisibleOverflow()
|
|||||||
local sm = createScrollManager({ overflow = "visible" })
|
local sm = createScrollManager({ overflow = "visible" })
|
||||||
local child = createMockChild(0, 0, 500, 500)
|
local child = createMockChild(0, 0, 500, 500)
|
||||||
local element = createMockElement(200, 300, { child })
|
local element = createMockElement(200, 300, { child })
|
||||||
sm:initialize(element)
|
-- sm:initialize(element) -- Removed: element now passed as parameter
|
||||||
sm:detectOverflow()
|
sm:detectOverflow(element)
|
||||||
|
|
||||||
-- Should skip detection for visible overflow
|
-- Should skip detection for visible overflow
|
||||||
local hasOverflowX, hasOverflowY = sm:hasOverflow()
|
local hasOverflowX, hasOverflowY = sm:hasOverflow()
|
||||||
@@ -237,8 +238,8 @@ function TestScrollManagerEdgeCases:testDetectOverflowWithAbsolutelyPositionedCh
|
|||||||
local child = createMockChild(0, 0, 500, 500)
|
local child = createMockChild(0, 0, 500, 500)
|
||||||
child._explicitlyAbsolute = true -- Should be ignored in overflow calc
|
child._explicitlyAbsolute = true -- Should be ignored in overflow calc
|
||||||
local element = createMockElement(200, 300, { child })
|
local element = createMockElement(200, 300, { child })
|
||||||
sm:initialize(element)
|
-- sm:initialize(element) -- Removed: element now passed as parameter
|
||||||
sm:detectOverflow()
|
sm:detectOverflow(element)
|
||||||
|
|
||||||
local hasOverflowX, hasOverflowY = sm:hasOverflow()
|
local hasOverflowX, hasOverflowY = sm:hasOverflow()
|
||||||
luaunit.assertFalse(hasOverflowX) -- Absolute children don't contribute
|
luaunit.assertFalse(hasOverflowX) -- Absolute children don't contribute
|
||||||
@@ -250,8 +251,8 @@ function TestScrollManagerEdgeCases:testDetectOverflowWithNegativeChildMargins()
|
|||||||
local child = createMockChild(10, 10, 100, 100)
|
local child = createMockChild(10, 10, 100, 100)
|
||||||
child.margin = { top = -50, right = -50, bottom = -50, left = -50 }
|
child.margin = { top = -50, right = -50, bottom = -50, left = -50 }
|
||||||
local element = createMockElement(200, 300, { child })
|
local element = createMockElement(200, 300, { child })
|
||||||
sm:initialize(element)
|
-- sm:initialize(element) -- Removed: element now passed as parameter
|
||||||
sm:detectOverflow()
|
sm:detectOverflow(element)
|
||||||
|
|
||||||
-- Negative margins shouldn't cause negative overflow detection
|
-- Negative margins shouldn't cause negative overflow detection
|
||||||
local contentW, contentH = sm:getContentSize()
|
local contentW, contentH = sm:getContentSize()
|
||||||
@@ -263,8 +264,8 @@ function TestScrollManagerEdgeCases:testDetectOverflowClampsExistingScroll()
|
|||||||
local sm = createScrollManager({ overflow = "auto", _scrollX = 1000, _scrollY = 1000 })
|
local sm = createScrollManager({ overflow = "auto", _scrollX = 1000, _scrollY = 1000 })
|
||||||
local child = createMockChild(10, 10, 100, 100)
|
local child = createMockChild(10, 10, 100, 100)
|
||||||
local element = createMockElement(200, 300, { child })
|
local element = createMockElement(200, 300, { child })
|
||||||
sm:initialize(element)
|
-- sm:initialize(element) -- Removed: element now passed as parameter
|
||||||
sm:detectOverflow()
|
sm:detectOverflow(element)
|
||||||
|
|
||||||
-- Scroll should be clamped to max bounds
|
-- Scroll should be clamped to max bounds
|
||||||
local scrollX, scrollY = sm:getScroll()
|
local scrollX, scrollY = sm:getScroll()
|
||||||
@@ -395,10 +396,10 @@ end
|
|||||||
function TestScrollManagerEdgeCases:testCalculateScrollbarDimensionsWithZeroTrackSize()
|
function TestScrollManagerEdgeCases:testCalculateScrollbarDimensionsWithZeroTrackSize()
|
||||||
local sm = createScrollManager({ overflow = "scroll", scrollbarPadding = 150 }) -- Padding bigger than element
|
local sm = createScrollManager({ overflow = "scroll", scrollbarPadding = 150 }) -- Padding bigger than element
|
||||||
local element = createMockElement(200, 300, {})
|
local element = createMockElement(200, 300, {})
|
||||||
sm:initialize(element)
|
-- sm:initialize(element) -- Removed: element now passed as parameter
|
||||||
sm:detectOverflow()
|
sm:detectOverflow(element)
|
||||||
|
|
||||||
local dims = sm:calculateScrollbarDimensions()
|
local dims = sm:calculateScrollbarDimensions(element)
|
||||||
-- Should handle zero or negative track sizes
|
-- Should handle zero or negative track sizes
|
||||||
luaunit.assertNotNil(dims.vertical)
|
luaunit.assertNotNil(dims.vertical)
|
||||||
luaunit.assertNotNil(dims.horizontal)
|
luaunit.assertNotNil(dims.horizontal)
|
||||||
@@ -407,10 +408,10 @@ end
|
|||||||
function TestScrollManagerEdgeCases:testCalculateScrollbarDimensionsWithScrollMode()
|
function TestScrollManagerEdgeCases:testCalculateScrollbarDimensionsWithScrollMode()
|
||||||
local sm = createScrollManager({ overflow = "scroll" })
|
local sm = createScrollManager({ overflow = "scroll" })
|
||||||
local element = createMockElement(200, 300, {}) -- No overflow
|
local element = createMockElement(200, 300, {}) -- No overflow
|
||||||
sm:initialize(element)
|
-- sm:initialize(element) -- Removed: element now passed as parameter
|
||||||
sm:detectOverflow()
|
sm:detectOverflow(element)
|
||||||
|
|
||||||
local dims = sm:calculateScrollbarDimensions()
|
local dims = sm:calculateScrollbarDimensions(element)
|
||||||
-- Scrollbars should be visible in "scroll" mode even without overflow
|
-- Scrollbars should be visible in "scroll" mode even without overflow
|
||||||
luaunit.assertTrue(dims.vertical.visible)
|
luaunit.assertTrue(dims.vertical.visible)
|
||||||
luaunit.assertTrue(dims.horizontal.visible)
|
luaunit.assertTrue(dims.horizontal.visible)
|
||||||
@@ -419,10 +420,10 @@ end
|
|||||||
function TestScrollManagerEdgeCases:testCalculateScrollbarDimensionsWithAutoModeNoOverflow()
|
function TestScrollManagerEdgeCases:testCalculateScrollbarDimensionsWithAutoModeNoOverflow()
|
||||||
local sm = createScrollManager({ overflow = "auto" })
|
local sm = createScrollManager({ overflow = "auto" })
|
||||||
local element = createMockElement(200, 300, {}) -- No overflow
|
local element = createMockElement(200, 300, {}) -- No overflow
|
||||||
sm:initialize(element)
|
-- sm:initialize(element) -- Removed: element now passed as parameter
|
||||||
sm:detectOverflow()
|
sm:detectOverflow(element)
|
||||||
|
|
||||||
local dims = sm:calculateScrollbarDimensions()
|
local dims = sm:calculateScrollbarDimensions(element)
|
||||||
-- Scrollbars should NOT be visible in "auto" mode without overflow
|
-- Scrollbars should NOT be visible in "auto" mode without overflow
|
||||||
luaunit.assertFalse(dims.vertical.visible)
|
luaunit.assertFalse(dims.vertical.visible)
|
||||||
luaunit.assertFalse(dims.horizontal.visible)
|
luaunit.assertFalse(dims.horizontal.visible)
|
||||||
@@ -431,10 +432,10 @@ end
|
|||||||
function TestScrollManagerEdgeCases:testCalculateScrollbarDimensionsWithAxisSpecificOverflow()
|
function TestScrollManagerEdgeCases:testCalculateScrollbarDimensionsWithAxisSpecificOverflow()
|
||||||
local sm = createScrollManager({ overflowX = "scroll", overflowY = "hidden" })
|
local sm = createScrollManager({ overflowX = "scroll", overflowY = "hidden" })
|
||||||
local element = createMockElement(200, 300, {})
|
local element = createMockElement(200, 300, {})
|
||||||
sm:initialize(element)
|
-- sm:initialize(element) -- Removed: element now passed as parameter
|
||||||
sm:detectOverflow()
|
sm:detectOverflow(element)
|
||||||
|
|
||||||
local dims = sm:calculateScrollbarDimensions()
|
local dims = sm:calculateScrollbarDimensions(element)
|
||||||
luaunit.assertTrue(dims.horizontal.visible) -- X is scroll
|
luaunit.assertTrue(dims.horizontal.visible) -- X is scroll
|
||||||
luaunit.assertFalse(dims.vertical.visible) -- Y is hidden
|
luaunit.assertFalse(dims.vertical.visible) -- Y is hidden
|
||||||
end
|
end
|
||||||
@@ -443,10 +444,10 @@ function TestScrollManagerEdgeCases:testCalculateScrollbarDimensionsWithMinThumb
|
|||||||
local sm = createScrollManager({ overflow = "scroll" })
|
local sm = createScrollManager({ overflow = "scroll" })
|
||||||
local child = createMockChild(10, 10, 100, 10000) -- Very tall child
|
local child = createMockChild(10, 10, 100, 10000) -- Very tall child
|
||||||
local element = createMockElement(200, 300, { child })
|
local element = createMockElement(200, 300, { child })
|
||||||
sm:initialize(element)
|
-- sm:initialize(element) -- Removed: element now passed as parameter
|
||||||
sm:detectOverflow()
|
sm:detectOverflow(element)
|
||||||
|
|
||||||
local dims = sm:calculateScrollbarDimensions()
|
local dims = sm:calculateScrollbarDimensions(element)
|
||||||
-- Thumb should have minimum size of 20px
|
-- Thumb should have minimum size of 20px
|
||||||
luaunit.assertTrue(dims.vertical.thumbHeight >= 20)
|
luaunit.assertTrue(dims.vertical.thumbHeight >= 20)
|
||||||
end
|
end
|
||||||
@@ -458,60 +459,60 @@ end
|
|||||||
function TestScrollManagerEdgeCases:testGetScrollbarAtPositionOutsideBounds()
|
function TestScrollManagerEdgeCases:testGetScrollbarAtPositionOutsideBounds()
|
||||||
local sm = createScrollManager({ overflow = "scroll" })
|
local sm = createScrollManager({ overflow = "scroll" })
|
||||||
local element = createMockElement(200, 300, {})
|
local element = createMockElement(200, 300, {})
|
||||||
sm:initialize(element)
|
-- sm:initialize(element) -- Removed: element now passed as parameter
|
||||||
sm:detectOverflow()
|
sm:detectOverflow(element)
|
||||||
|
|
||||||
local result = sm:getScrollbarAtPosition(-100, -100)
|
local result = sm:getScrollbarAtPosition(element, -100, -100)
|
||||||
luaunit.assertNil(result)
|
luaunit.assertNil(result)
|
||||||
end
|
end
|
||||||
|
|
||||||
function TestScrollManagerEdgeCases:testGetScrollbarAtPositionWithHiddenScrollbars()
|
function TestScrollManagerEdgeCases:testGetScrollbarAtPositionWithHiddenScrollbars()
|
||||||
local sm = createScrollManager({ overflow = "scroll", hideScrollbars = true })
|
local sm = createScrollManager({ overflow = "scroll", hideScrollbars = true })
|
||||||
local element = createMockElement(200, 300, {})
|
local element = createMockElement(200, 300, {})
|
||||||
sm:initialize(element)
|
-- sm:initialize(element) -- Removed: element now passed as parameter
|
||||||
sm:detectOverflow()
|
sm:detectOverflow(element)
|
||||||
|
|
||||||
-- Even though scrollbar exists, it's hidden so shouldn't be detected
|
-- Even though scrollbar exists, it's hidden so shouldn't be detected
|
||||||
local dims = sm:calculateScrollbarDimensions()
|
local dims = sm:calculateScrollbarDimensions(element)
|
||||||
local result = sm:getScrollbarAtPosition(190, 50)
|
local result = sm:getScrollbarAtPosition(element, 190, 50)
|
||||||
luaunit.assertNil(result)
|
luaunit.assertNil(result)
|
||||||
end
|
end
|
||||||
|
|
||||||
function TestScrollManagerEdgeCases:testHandleMousePressWithRightButton()
|
function TestScrollManagerEdgeCases:testHandleMousePressWithRightButton()
|
||||||
local sm = createScrollManager({ overflow = "scroll" })
|
local sm = createScrollManager({ overflow = "scroll" })
|
||||||
local element = createMockElement(200, 300, {})
|
local element = createMockElement(200, 300, {})
|
||||||
sm:initialize(element)
|
-- sm:initialize(element) -- Removed: element now passed as parameter
|
||||||
sm:detectOverflow()
|
sm:detectOverflow(element)
|
||||||
|
|
||||||
local consumed = sm:handleMousePress(50, 50, 2) -- Right button
|
local consumed = sm:handleMousePress(element, 50, 50, 2) -- Right button
|
||||||
luaunit.assertFalse(consumed)
|
luaunit.assertFalse(consumed)
|
||||||
end
|
end
|
||||||
|
|
||||||
function TestScrollManagerEdgeCases:testHandleMousePressWithMiddleButton()
|
function TestScrollManagerEdgeCases:testHandleMousePressWithMiddleButton()
|
||||||
local sm = createScrollManager({ overflow = "scroll" })
|
local sm = createScrollManager({ overflow = "scroll" })
|
||||||
local element = createMockElement(200, 300, {})
|
local element = createMockElement(200, 300, {})
|
||||||
sm:initialize(element)
|
-- sm:initialize(element) -- Removed: element now passed as parameter
|
||||||
sm:detectOverflow()
|
sm:detectOverflow(element)
|
||||||
|
|
||||||
local consumed = sm:handleMousePress(50, 50, 3) -- Middle button
|
local consumed = sm:handleMousePress(element, 50, 50, 3) -- Middle button
|
||||||
luaunit.assertFalse(consumed)
|
luaunit.assertFalse(consumed)
|
||||||
end
|
end
|
||||||
|
|
||||||
function TestScrollManagerEdgeCases:testHandleMouseMoveWithoutDragging()
|
function TestScrollManagerEdgeCases:testHandleMouseMoveWithoutDragging()
|
||||||
local sm = createScrollManager({ overflow = "scroll" })
|
local sm = createScrollManager({ overflow = "scroll" })
|
||||||
local element = createMockElement(200, 300, {})
|
local element = createMockElement(200, 300, {})
|
||||||
sm:initialize(element)
|
-- sm:initialize(element) -- Removed: element now passed as parameter
|
||||||
sm:detectOverflow()
|
sm:detectOverflow(element)
|
||||||
|
|
||||||
local consumed = sm:handleMouseMove(50, 50)
|
local consumed = sm:handleMouseMove(element, 50, 50)
|
||||||
luaunit.assertFalse(consumed)
|
luaunit.assertFalse(consumed)
|
||||||
end
|
end
|
||||||
|
|
||||||
function TestScrollManagerEdgeCases:testHandleMouseReleaseWithoutDragging()
|
function TestScrollManagerEdgeCases:testHandleMouseReleaseWithoutDragging()
|
||||||
local sm = createScrollManager({ overflow = "scroll" })
|
local sm = createScrollManager({ overflow = "scroll" })
|
||||||
local element = createMockElement(200, 300, {})
|
local element = createMockElement(200, 300, {})
|
||||||
sm:initialize(element)
|
-- sm:initialize(element) -- Removed: element now passed as parameter
|
||||||
sm:detectOverflow()
|
sm:detectOverflow(element)
|
||||||
|
|
||||||
local consumed = sm:handleMouseRelease(1)
|
local consumed = sm:handleMouseRelease(1)
|
||||||
luaunit.assertFalse(consumed)
|
luaunit.assertFalse(consumed)
|
||||||
@@ -520,8 +521,8 @@ end
|
|||||||
function TestScrollManagerEdgeCases:testHandleMouseReleaseWithWrongButton()
|
function TestScrollManagerEdgeCases:testHandleMouseReleaseWithWrongButton()
|
||||||
local sm = createScrollManager({ overflow = "scroll" })
|
local sm = createScrollManager({ overflow = "scroll" })
|
||||||
local element = createMockElement(200, 300, {})
|
local element = createMockElement(200, 300, {})
|
||||||
sm:initialize(element)
|
-- sm:initialize(element) -- Removed: element now passed as parameter
|
||||||
sm:detectOverflow()
|
sm:detectOverflow(element)
|
||||||
|
|
||||||
sm._scrollbarDragging = true -- Simulate dragging
|
sm._scrollbarDragging = true -- Simulate dragging
|
||||||
local consumed = sm:handleMouseRelease(2) -- Wrong button
|
local consumed = sm:handleMouseRelease(2) -- Wrong button
|
||||||
@@ -965,13 +966,13 @@ end
|
|||||||
function TestScrollManagerEdgeCases:testUpdateHoverStateOutsideScrollbar()
|
function TestScrollManagerEdgeCases:testUpdateHoverStateOutsideScrollbar()
|
||||||
local sm = createScrollManager({ overflow = "scroll" })
|
local sm = createScrollManager({ overflow = "scroll" })
|
||||||
local element = createMockElement(200, 300, {})
|
local element = createMockElement(200, 300, {})
|
||||||
sm:initialize(element)
|
-- sm:initialize(element) -- Removed: element now passed as parameter
|
||||||
sm:detectOverflow()
|
sm:detectOverflow(element)
|
||||||
|
|
||||||
sm._scrollbarHoveredVertical = true
|
sm._scrollbarHoveredVertical = true
|
||||||
sm._scrollbarHoveredHorizontal = true
|
sm._scrollbarHoveredHorizontal = true
|
||||||
|
|
||||||
sm:updateHoverState(0, 0) -- Far from scrollbar
|
sm:updateHoverState(element, 0, 0) -- Far from scrollbar
|
||||||
|
|
||||||
luaunit.assertFalse(sm._scrollbarHoveredVertical)
|
luaunit.assertFalse(sm._scrollbarHoveredVertical)
|
||||||
luaunit.assertFalse(sm._scrollbarHoveredHorizontal)
|
luaunit.assertFalse(sm._scrollbarHoveredHorizontal)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -85,7 +85,7 @@ function TestTouchEvents:testEventHandler_TouchBegan()
|
|||||||
|
|
||||||
-- Trigger touch event processing
|
-- Trigger touch event processing
|
||||||
FlexLove.beginFrame()
|
FlexLove.beginFrame()
|
||||||
element._eventHandler:processTouchEvents()
|
element._eventHandler:processTouchEvents(element)
|
||||||
FlexLove.endFrame()
|
FlexLove.endFrame()
|
||||||
|
|
||||||
-- Should have received at least one touchpress event
|
-- Should have received at least one touchpress event
|
||||||
@@ -123,7 +123,7 @@ function TestTouchEvents:testEventHandler_TouchMoved()
|
|||||||
|
|
||||||
-- First touch
|
-- First touch
|
||||||
FlexLove.beginFrame()
|
FlexLove.beginFrame()
|
||||||
element._eventHandler:processTouchEvents()
|
element._eventHandler:processTouchEvents(element)
|
||||||
FlexLove.endFrame()
|
FlexLove.endFrame()
|
||||||
|
|
||||||
-- Move touch
|
-- Move touch
|
||||||
@@ -135,7 +135,7 @@ function TestTouchEvents:testEventHandler_TouchMoved()
|
|||||||
end
|
end
|
||||||
|
|
||||||
FlexLove.beginFrame()
|
FlexLove.beginFrame()
|
||||||
element._eventHandler:processTouchEvents()
|
element._eventHandler:processTouchEvents(element)
|
||||||
FlexLove.endFrame()
|
FlexLove.endFrame()
|
||||||
|
|
||||||
-- Should have received touchpress and touchmove events
|
-- Should have received touchpress and touchmove events
|
||||||
@@ -174,7 +174,7 @@ function TestTouchEvents:testEventHandler_TouchEnded()
|
|||||||
|
|
||||||
-- First touch
|
-- First touch
|
||||||
FlexLove.beginFrame()
|
FlexLove.beginFrame()
|
||||||
element._eventHandler:processTouchEvents()
|
element._eventHandler:processTouchEvents(element)
|
||||||
FlexLove.endFrame()
|
FlexLove.endFrame()
|
||||||
|
|
||||||
-- End touch
|
-- End touch
|
||||||
@@ -183,7 +183,7 @@ function TestTouchEvents:testEventHandler_TouchEnded()
|
|||||||
end
|
end
|
||||||
|
|
||||||
FlexLove.beginFrame()
|
FlexLove.beginFrame()
|
||||||
element._eventHandler:processTouchEvents()
|
element._eventHandler:processTouchEvents(element)
|
||||||
FlexLove.endFrame()
|
FlexLove.endFrame()
|
||||||
|
|
||||||
-- Should have received touchpress and touchrelease events
|
-- Should have received touchpress and touchrelease events
|
||||||
@@ -222,7 +222,7 @@ function TestTouchEvents:testEventHandler_MultiTouch()
|
|||||||
end
|
end
|
||||||
|
|
||||||
FlexLove.beginFrame()
|
FlexLove.beginFrame()
|
||||||
element._eventHandler:processTouchEvents()
|
element._eventHandler:processTouchEvents(element)
|
||||||
FlexLove.endFrame()
|
FlexLove.endFrame()
|
||||||
|
|
||||||
-- Should have received two touchpress events
|
-- Should have received two touchpress events
|
||||||
|
|||||||
Reference in New Issue
Block a user