removed old cleanup, fix inputs

This commit is contained in:
Michael Freno
2025-12-04 00:17:04 -05:00
parent efce61d077
commit 3ee4bf1786
9 changed files with 152 additions and 150 deletions

View File

@@ -160,7 +160,7 @@ function flexlove.init(config)
end
Blur.init({
ErrorHandler = flexlove._ErrorHandler,
immediateModeOptimizations = blurOptimizations and config.immediateMode or false
immediateModeOptimizations = blurOptimizations and config.immediateMode or false,
})
end

View File

@@ -465,8 +465,7 @@ function Element.new(props)
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
local hasNonZero = props.cornerRadius.topLeft or props.cornerRadius.topRight or props.cornerRadius.bottomLeft or props.cornerRadius.bottomRight
if hasNonZero then
self.cornerRadius = {
topLeft = props.cornerRadius.topLeft or 0,
@@ -1458,11 +1457,6 @@ function Element.new(props)
Element._Context.registerElement(self)
end
-- Initialize TextEditor after element is fully constructed
if self._textEditor then
self._textEditor:restoreState(self)
end
return self
end
@@ -3172,26 +3166,15 @@ end
---@return ElementStateData state Complete state snapshot
function Element:saveState()
local state = {}
-- Element-owned state
state._focused = self._focused
-- EventHandler state (if exists)
if self._eventHandler then
state.eventHandler = self._eventHandler:getState()
end
-- TextEditor state (if exists)
if self._textEditor then
state.textEditor = self._textEditor:getState()
end
-- ScrollManager state (if exists)
if self._scrollManager then
state.scrollManager = self._scrollManager:getState()
end
-- Blur cache data (for cache invalidation)
if self.backdropBlur or self.contentBlur then
state.blur = {
_blurX = self.x,
@@ -3222,11 +3205,6 @@ function Element:restoreState(state)
return
end
-- Restore element-owned state
if state._focused ~= nil then
self._focused = state._focused
end
-- Restore EventHandler state (if exists)
if self._eventHandler and state.eventHandler then
self._eventHandler:setState(state.eventHandler)
@@ -3234,7 +3212,13 @@ function Element:restoreState(state)
-- Restore TextEditor state (if exists)
if self._textEditor and state.textEditor then
self._textEditor:setState(state.textEditor)
self._textEditor:setState(state.textEditor, self)
-- Sync TextEditor's focus state to Element for theme management
self._focused = self._textEditor._focused
self._cursorPosition = self._textEditor._cursorPosition
self._selectionStart = self._textEditor._selectionStart
self._selectionEnd = self._textEditor._selectionEnd
self._textBuffer = self._textEditor._textBuffer
end
-- Restore ScrollManager state (if exists)
@@ -3271,31 +3255,6 @@ 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

View File

@@ -218,7 +218,6 @@ end
---@param my number Mouse Y position
---@param button number Mouse button (1=left, 2=right, 3=middle)
function EventHandler:_handleMousePress(element, mx, my, button)
-- Check if press is on scrollbar first (skip if already handled)
if button == 1 and not self._scrollbarPressHandled and element._handleScrollbarPress then
if element:_handleScrollbarPress(mx, my, button) then
@@ -263,7 +262,6 @@ end
---@param button number Mouse button
---@param isHovering boolean Whether mouse is over element
function EventHandler:_handleMouseDrag(element, mx, my, button, isHovering)
local lastX = self._lastMouseX[button] or mx
local lastY = self._lastMouseY[button] or my
@@ -303,7 +301,6 @@ end
---@param my number Mouse Y position
---@param button number Mouse button
function EventHandler:_handleMouseRelease(element, mx, my, button)
local currentTime = love.timer.getTime()
local modifiers = EventHandler._utils.getModifiers()
@@ -471,7 +468,6 @@ end
---@param y number Touch Y position
---@param pressure number Touch pressure (0-1)
function EventHandler:_handleTouchBegan(element, touchId, x, y, pressure)
-- Create touch state
self._touches[touchId] = {
x = x,
@@ -642,13 +638,4 @@ function EventHandler:_invokeCallback(element, event)
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

View File

@@ -1025,10 +1025,12 @@ function LayoutEngine:_canSkipLayout()
local cache = self._layoutCache
-- Check if layout inputs have changed
if cache.childrenCount == childrenCount and
cache.containerWidth == containerWidth and
cache.containerHeight == containerHeight and
cache.childrenHash == childrenHash then
if
cache.childrenCount == childrenCount
and cache.containerWidth == containerWidth
and cache.containerHeight == containerHeight
and cache.childrenHash == childrenHash
then
return true -- Layout hasn't changed, can skip
end
@@ -1072,13 +1074,4 @@ function LayoutEngine:_trackLayoutRecalculation()
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

View File

@@ -120,8 +120,6 @@ function Renderer.new(config, deps)
return self
end
--- Get or create blur instance for this element
---@return table|nil Blur instance or nil
function Renderer:getBlurInstance()
@@ -202,10 +200,7 @@ function Renderer:_drawImage(x, y, paddingLeft, paddingTop, contentWidth, conten
if type(self.cornerRadius) == "number" then
hasCornerRadius = self.cornerRadius > 0
else
hasCornerRadius = self.cornerRadius.topLeft > 0
or self.cornerRadius.topRight > 0
or self.cornerRadius.bottomLeft > 0
or self.cornerRadius.bottomRight > 0
hasCornerRadius = self.cornerRadius.topLeft > 0 or self.cornerRadius.topRight > 0 or self.cornerRadius.bottomLeft > 0 or self.cornerRadius.bottomRight > 0
end
end
@@ -963,12 +958,4 @@ function Renderer:destroy()
self._blurInstance = nil
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

View File

@@ -894,14 +894,4 @@ function ScrollManager:isMomentumScrolling()
return self._momentumScrolling
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

View File

@@ -1123,7 +1123,6 @@ function TextEditor:handleTextInput(element, text)
-- Insert text at cursor position
self:insertText(element, text)
-- Trigger onTextChange callback
if self.onTextChange and self._textBuffer ~= oldText then
self.onTextChange(element, self._textBuffer, oldText)
@@ -1699,7 +1698,8 @@ end
--- Restore state from persistence
---@param state table State to restore
function TextEditor:setState(state)
---@param element Element? The parent element (needed for focus restoration)
function TextEditor:setState(state, element)
if not state then
return
end
@@ -1738,6 +1738,10 @@ function TextEditor:setState(state)
if state._focused ~= nil then
self._focused = state._focused
-- Restore focused element in Context if this element was focused
if self._focused and element then
self._Context._focusedElement = element
end
end
end
@@ -1748,7 +1752,12 @@ function TextEditor:_saveState(element)
return
end
self._StateManager.updateState(element._stateId, {
-- Get current state (may have other sub-modules like eventHandler, scrollManager)
local currentState = self._StateManager.getState(element._stateId) or {}
-- Update only the textEditor sub-table to match the nested structure
-- used by element:saveState() at endFrame
currentState.textEditor = {
_focused = self._focused,
_textBuffer = self._textBuffer,
_cursorPosition = self._cursorPosition,
@@ -1758,14 +1767,9 @@ function TextEditor:_saveState(element)
_cursorVisible = self._cursorVisible,
_cursorBlinkPaused = self._cursorBlinkPaused,
_cursorBlinkPauseTimer = self._cursorBlinkPauseTimer,
})
end
}
--- Cleanup method to break circular references (for immediate mode)
function TextEditor:_cleanup()
-- TextEditor → element is circular, but breaking it breaks functionality
-- Module refs are singletons, not circular
self._StateManager.updateState(element._stateId, currentState)
end
return TextEditor

View File

@@ -961,14 +961,6 @@ function ThemeManager:setTheme(themeName, componentName)
self.themeComponent = componentName
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
Theme.Manager = ThemeManager
--- Check theme definitions for correctness before use to catch configuration errors early

View File

@@ -1940,7 +1940,8 @@ function TestTextEditorStateSaving:test_saveState_immediate_mode()
editor:setText(element, "New text")
luaunit.assertNotNil(savedState)
luaunit.assertEquals(savedState._textBuffer, "New text")
luaunit.assertNotNil(savedState.textEditor, "State should have textEditor sub-table")
luaunit.assertEquals(savedState.textEditor._textBuffer, "New text")
end
function TestTextEditorStateSaving:test_saveState_not_immediate_mode()
@@ -1973,6 +1974,95 @@ function TestTextEditorStateSaving:test_saveState_not_immediate_mode()
luaunit.assertFalse(saveCalled)
end
-- ============================================================================
-- Immediate Mode State Persistence Tests
-- ============================================================================
TestTextEditorImmediateMode = {}
function TestTextEditorImmediateMode:test_focus_persists_across_frames()
-- Simulate immediate mode
MockContext._immediateMode = true
-- Create editor and focus it
local editor1 = createTextEditor({text = "Hello", editable = true})
local element1 = createMockElement()
editor1:focus(element1)
luaunit.assertTrue(editor1._focused)
luaunit.assertEquals(MockContext._focusedElement, element1)
-- Get state
local state = editor1:getState()
luaunit.assertTrue(state._focused)
-- Create new editor (simulating next frame)
local editor2 = createTextEditor({text = "Hello", editable = true})
local element2 = createMockElement()
-- Restore state
editor2:setState(state, element2)
-- Focus should be restored
luaunit.assertTrue(editor2._focused)
luaunit.assertEquals(MockContext._focusedElement, element2)
-- Reset
MockContext._immediateMode = false
MockContext._focusedElement = nil
end
function TestTextEditorImmediateMode:test_text_input_works_after_state_restore()
-- Simulate immediate mode
MockContext._immediateMode = true
-- Create editor and focus it
local editor1 = createTextEditor({text = "Hello", editable = true})
local element1 = createMockElement()
editor1:focus(element1)
-- Get state
local state = editor1:getState()
-- Create new editor (simulating next frame)
local editor2 = createTextEditor({text = "Hello", editable = true})
local element2 = createMockElement()
-- Restore state (this should restore focus)
editor2:setState(state, element2)
-- Text input should work because focus was restored
editor2:handleTextInput(element2, "X")
luaunit.assertEquals(editor2:getText(), "HelloX")
-- Reset
MockContext._immediateMode = false
MockContext._focusedElement = nil
end
function TestTextEditorImmediateMode:test_cursor_position_persists()
MockContext._immediateMode = true
local editor1 = createTextEditor({text = "Hello", editable = true})
local element1 = createMockElement()
editor1:focus(element1)
editor1:moveCursorBy(element1, -2) -- Move cursor to position 3
local state = editor1:getState()
luaunit.assertEquals(state._cursorPosition, 3)
-- Restore in new editor
local editor2 = createTextEditor({text = "Hello", editable = true})
local element2 = createMockElement()
editor2:setState(state, element2)
luaunit.assertEquals(editor2._cursorPosition, 3)
-- Reset
MockContext._immediateMode = false
MockContext._focusedElement = nil
end
-- ============================================================================
-- Run Tests
-- ============================================================================