From db2f5b43c9e14ff406bf6ea71b309d30bb4df6b5 Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Fri, 14 Nov 2025 10:26:46 -0500 Subject: [PATCH] fix immediate mode state update/draw ordering --- FlexLove.lua | 11 ++++--- modules/Element.lua | 57 ++++++++++++++++++++++++++-------- modules/EventHandler.lua | 66 ++++++++++++++++++++++++++++++++-------- 3 files changed, 105 insertions(+), 29 deletions(-) diff --git a/FlexLove.lua b/FlexLove.lua index 168e803..772a87b 100644 --- a/FlexLove.lua +++ b/FlexLove.lua @@ -176,8 +176,8 @@ function flexlove.endFrame() end end - -- Auto-update all top-level elements (triggers additional state updates) - -- This must happen BEFORE saving state so that scroll positions and overflow are calculated + -- Auto-update all top-level elements created this frame + -- This happens AFTER layout so positions are correct for _, element in ipairs(flexlove._currentFrameElements) do if not element.parent then element:update(0) -- dt=0 since we're not doing animation updates here @@ -442,8 +442,11 @@ function flexlove.update(dt) flexlove._activeEventElement = topElement - for _, win in ipairs(flexlove.topElements) do - win:update(dt) + -- In immediate mode, skip updating here - elements will be updated in endFrame after layout + if not flexlove._immediateMode then + for _, win in ipairs(flexlove.topElements) do + win:update(dt) + end end flexlove._activeEventElement = nil diff --git a/modules/Element.lua b/modules/Element.lua index eacc1e7..b79e609 100644 --- a/modules/Element.lua +++ b/modules/Element.lua @@ -253,17 +253,33 @@ function Element.new(props) self.onTextChange = props.onTextChange self.onEnter = props.onEnter - self._eventHandler = EventHandler.new({ - onEvent = self.onEvent, - }, { + -- Initialize state manager ID for immediate mode (use self.id which may be auto-generated) + 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, Context = Context, }) 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({ theme = props.theme or Context.defaultTheme, themeComponent = props.themeComponent or nil, @@ -2096,6 +2112,29 @@ function Element:update(dt) isActiveElement = (Context._activeEventElement == nil or Context._activeEventElement == self) 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 if self.themeComponent then -- Check if any button is pressed via EventHandler @@ -2128,12 +2167,6 @@ function Element:update(dt) 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 self._eventHandler:processTouchEvents() end diff --git a/modules/EventHandler.lua b/modules/EventHandler.lua index 51cbaee..45e004e 100644 --- a/modules/EventHandler.lua +++ b/modules/EventHandler.lua @@ -123,10 +123,30 @@ function EventHandler:processMouseEvents(mx, my, isHovering, isActiveElement) end end - -- Can only process events if we have handler, element is enabled, and is active or dragging - local canProcessEvents = (self.onEvent or element.editable) and not element.disabled and (isActiveElement or isDragging) + -- Check if any button is currently pressed (tracked state) + 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 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 end @@ -134,23 +154,43 @@ function EventHandler:processMouseEvents(mx, my, isHovering, isActiveElement) local buttons = { 1, 2, 3 } -- left, right, middle for _, button in ipairs(buttons) do - if isHovering or isDragging then - if love.mouse.isDown(button) then + -- Check if this button was tracked as pressed + 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 - if not self._pressed[button] then - -- Just pressed - fire press event - self:_handleMousePress(mx, my, button) + if not wasPressed then + -- Just pressed - fire press event (only if hovering) + if isHovering then + self:_handleMousePress(mx, my, button) + end else -- Button is still pressed - check for drag self:_handleMouseDrag(mx, my, button, isHovering) end - elseif self._pressed[button] then - -- Button was just released - fire click and release events - self:_handleMouseRelease(mx, my, button) + elseif wasPressed then + -- Button was just released + -- 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 - else - -- Mouse left the element - reset pressed state and drag tracking - if self._pressed[button] then + end + end + + -- 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._dragStartX[button] = nil self._dragStartY[button] = nil