fix immediate mode state update/draw ordering

This commit is contained in:
Michael Freno
2025-11-14 10:26:46 -05:00
parent 93af33825d
commit db2f5b43c9
3 changed files with 105 additions and 29 deletions

View File

@@ -176,8 +176,8 @@ function flexlove.endFrame()
end end
end end
-- Auto-update all top-level elements (triggers additional state updates) -- Auto-update all top-level elements created this frame
-- This must happen BEFORE saving state so that scroll positions and overflow are calculated -- This happens AFTER layout so positions are correct
for _, element in ipairs(flexlove._currentFrameElements) do for _, element in ipairs(flexlove._currentFrameElements) do
if not element.parent then if not element.parent then
element:update(0) -- dt=0 since we're not doing animation updates here element:update(0) -- dt=0 since we're not doing animation updates here
@@ -442,8 +442,11 @@ function flexlove.update(dt)
flexlove._activeEventElement = topElement flexlove._activeEventElement = topElement
for _, win in ipairs(flexlove.topElements) do -- In immediate mode, skip updating here - elements will be updated in endFrame after layout
win:update(dt) if not flexlove._immediateMode then
for _, win in ipairs(flexlove.topElements) do
win:update(dt)
end
end end
flexlove._activeEventElement = nil flexlove._activeEventElement = nil

View File

@@ -253,17 +253,33 @@ function Element.new(props)
self.onTextChange = props.onTextChange self.onTextChange = props.onTextChange
self.onEnter = props.onEnter self.onEnter = props.onEnter
self._eventHandler = EventHandler.new({ -- Initialize state manager ID for immediate mode (use self.id which may be auto-generated)
onEvent = self.onEvent, self._stateId = self.id
}, {
-- In immediate mode, restore EventHandler state from StateManager
local eventHandlerConfig = { onEvent = self.onEvent }
if Context._immediateMode and self._stateId and self._stateId ~= "" then
local state = StateManager.getState(self._stateId)
if state then
-- Restore EventHandler state from StateManager
eventHandlerConfig._pressed = state._pressed
eventHandlerConfig._lastClickTime = state._lastClickTime
eventHandlerConfig._lastClickButton = state._lastClickButton
eventHandlerConfig._clickCount = state._clickCount
eventHandlerConfig._dragStartX = state._dragStartX
eventHandlerConfig._dragStartY = state._dragStartY
eventHandlerConfig._lastMouseX = state._lastMouseX
eventHandlerConfig._lastMouseY = state._lastMouseY
eventHandlerConfig._hovered = state._hovered
end
end
self._eventHandler = EventHandler.new(eventHandlerConfig, {
InputEvent = InputEvent, InputEvent = InputEvent,
Context = Context, Context = Context,
}) })
self._eventHandler:initialize(self) self._eventHandler:initialize(self)
-- Initialize state manager ID for immediate mode (use self.id which may be auto-generated)
self._stateId = self.id
self._themeManager = Theme.Manager.new({ self._themeManager = Theme.Manager.new({
theme = props.theme or Context.defaultTheme, theme = props.theme or Context.defaultTheme,
themeComponent = props.themeComponent or nil, themeComponent = props.themeComponent or nil,
@@ -2096,6 +2112,29 @@ function Element:update(dt)
isActiveElement = (Context._activeEventElement == nil or Context._activeEventElement == self) isActiveElement = (Context._activeEventElement == nil or Context._activeEventElement == self)
end end
-- Reset scrollbar press flag at start of each frame
self._eventHandler:resetScrollbarPressFlag()
-- Process mouse events through EventHandler FIRST
-- This ensures pressed states are updated before theme state is calculated
self._eventHandler:processMouseEvents(mx, my, isHovering, isActiveElement)
-- In immediate mode, save EventHandler state to StateManager after processing events
if self._stateId and Context._immediateMode and self._stateId ~= "" then
local eventHandlerState = self._eventHandler:getState()
StateManager.updateState(self._stateId, {
_pressed = eventHandlerState._pressed,
_lastClickTime = eventHandlerState._lastClickTime,
_lastClickButton = eventHandlerState._lastClickButton,
_clickCount = eventHandlerState._clickCount,
_dragStartX = eventHandlerState._dragStartX,
_dragStartY = eventHandlerState._dragStartY,
_lastMouseX = eventHandlerState._lastMouseX,
_lastMouseY = eventHandlerState._lastMouseY,
_hovered = eventHandlerState._hovered,
})
end
-- Update theme state based on interaction -- Update theme state based on interaction
if self.themeComponent then if self.themeComponent then
-- Check if any button is pressed via EventHandler -- Check if any button is pressed via EventHandler
@@ -2128,12 +2167,6 @@ function Element:update(dt)
end end
end end
-- Reset scrollbar press flag at start of each frame
self._eventHandler:resetScrollbarPressFlag()
-- Process mouse events through EventHandler
self._eventHandler:processMouseEvents(mx, my, isHovering, isActiveElement)
-- Process touch events through EventHandler -- Process touch events through EventHandler
self._eventHandler:processTouchEvents() self._eventHandler:processTouchEvents()
end end

View File

@@ -123,10 +123,30 @@ function EventHandler:processMouseEvents(mx, my, isHovering, isActiveElement)
end end
end end
-- Can only process events if we have handler, element is enabled, and is active or dragging -- Check if any button is currently pressed (tracked state)
local canProcessEvents = (self.onEvent or element.editable) and not element.disabled and (isActiveElement or isDragging) local hasTrackedPress = false
for _, button in ipairs({ 1, 2, 3 }) do
if self._pressed[button] then
hasTrackedPress = true
break
end
end
-- Can only process events if we have handler, element is enabled, and is active or dragging or has tracked press
local canProcessEvents = (self.onEvent or element.editable) and not element.disabled and (isActiveElement or isDragging or hasTrackedPress)
if not canProcessEvents then if not canProcessEvents then
-- If not hovering and no buttons are physically pressed, reset all pressed states
-- This ensures the pressed state is cleared when mouse leaves without button held
if not isHovering and not isDragging then
for _, button in ipairs({ 1, 2, 3 }) do
if self._pressed[button] and not love.mouse.isDown(button) then
self._pressed[button] = false
self._dragStartX[button] = nil
self._dragStartY[button] = nil
end
end
end
return return
end end
@@ -134,23 +154,43 @@ function EventHandler:processMouseEvents(mx, my, isHovering, isActiveElement)
local buttons = { 1, 2, 3 } -- left, right, middle local buttons = { 1, 2, 3 } -- left, right, middle
for _, button in ipairs(buttons) do for _, button in ipairs(buttons) do
if isHovering or isDragging then -- Check if this button was tracked as pressed
if love.mouse.isDown(button) then local wasPressed = self._pressed[button]
local isPhysicallyPressed = love.mouse.isDown(button)
if isHovering or isDragging or wasPressed then
if isPhysicallyPressed then
-- Button is pressed down -- Button is pressed down
if not self._pressed[button] then if not wasPressed then
-- Just pressed - fire press event -- Just pressed - fire press event (only if hovering)
self:_handleMousePress(mx, my, button) if isHovering then
self:_handleMousePress(mx, my, button)
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(mx, my, button, isHovering)
end end
elseif self._pressed[button] then elseif wasPressed then
-- Button was just released - fire click and release events -- Button was just released
self:_handleMouseRelease(mx, my, button) -- Only fire click and release events if mouse is still hovering AND element is active
-- (not occluded by another element)
if isHovering and isActiveElement then
self:_handleMouseRelease(mx, my, button)
else
-- Mouse left before release OR element is occluded - just clear the pressed state without firing events
self._pressed[button] = false
self._dragStartX[button] = nil
self._dragStartY[button] = nil
end
end end
else end
-- Mouse left the element - reset pressed state and drag tracking end
if self._pressed[button] then
-- After processing events, reset pressed states for buttons that are no longer held
-- This handles the case where mouse leaves while button is held, then released
if not isHovering and not isDragging then
for _, button in ipairs({ 1, 2, 3 }) do
if self._pressed[button] and not love.mouse.isDown(button) then
self._pressed[button] = false self._pressed[button] = false
self._dragStartX[button] = nil self._dragStartX[button] = nil
self._dragStartY[button] = nil self._dragStartY[button] = nil