diff --git a/modules/Element.lua b/modules/Element.lua index 348e37d..7e8d5ba 100644 --- a/modules/Element.lua +++ b/modules/Element.lua @@ -314,7 +314,7 @@ function Element.new(props) scaleCorners = props.scaleCorners, scalingAlgorithm = props.scalingAlgorithm, }) - + -- Validate themeStateLock after ThemeManager is created if props.themeStateLock and props.themeComponent then self._themeManager:validateThemeStateLock() @@ -1458,6 +1458,32 @@ function Element.new(props) self._scrollbarDragging = false self._hoveredScrollbar = nil self._scrollbarDragOffset = 0 + + -- Restore scrollbar state from StateManager in immediate mode (must happen before layout) + if Element._Context._immediateMode and self._stateId and self._stateId ~= "" then + local state = Element._StateManager.getState(self._stateId) + if state and state.scrollManager then + -- Restore from nested scrollManager state (saved via saveState()) + self._scrollbarHoveredVertical = state.scrollManager._scrollbarHoveredVertical or false + self._scrollbarHoveredHorizontal = state.scrollManager._scrollbarHoveredHorizontal or false + self._scrollbarDragging = state.scrollManager._scrollbarDragging or false + self._hoveredScrollbar = state.scrollManager._hoveredScrollbar + self._scrollbarDragOffset = state.scrollManager._scrollbarDragOffset or 0 + + -- Apply to ScrollManager immediately + self._scrollManager._scrollbarHoveredVertical = self._scrollbarHoveredVertical + self._scrollManager._scrollbarHoveredHorizontal = self._scrollbarHoveredHorizontal + self._scrollManager._scrollbarDragging = self._scrollbarDragging + self._scrollManager._hoveredScrollbar = self._hoveredScrollbar + self._scrollManager._scrollbarDragOffset = self._scrollbarDragOffset + + -- Restore drag start positions for relative movement tracking + self._scrollManager._dragStartMouseX = state.scrollManager._dragStartMouseX or 0 + self._scrollManager._dragStartMouseY = state.scrollManager._dragStartMouseY or 0 + self._scrollManager._dragStartScrollX = state.scrollManager._dragStartScrollX or 0 + self._scrollManager._dragStartScrollY = state.scrollManager._dragStartScrollY or 0 + end + end else self._scrollManager = nil end @@ -1510,11 +1536,11 @@ end --- Call this when element properties change that affect layout function Element:invalidateLayout() self._dirty = true - + -- Invalidate dimension caches self._borderBoxWidthCache = nil self._borderBoxHeightCache = nil - + -- Mark parent as having dirty children if self.parent then self.parent._childrenDirty = true @@ -2190,12 +2216,13 @@ function Element:update(dt) -- Restore scrollbar state from StateManager in immediate mode if self._stateId and Element._Context._immediateMode then local state = Element._StateManager.getState(self._stateId) - if state then - self._scrollbarHoveredVertical = state.scrollbarHoveredVertical or false - self._scrollbarHoveredHorizontal = state.scrollbarHoveredHorizontal or false - self._scrollbarDragging = state.scrollbarDragging or false - self._hoveredScrollbar = state.hoveredScrollbar - self._scrollbarDragOffset = state.scrollbarDragOffset or 0 + if state and state.scrollManager then + -- Restore from nested scrollManager state (saved via saveState()) + self._scrollbarHoveredVertical = state.scrollManager._scrollbarHoveredVertical or false + self._scrollbarHoveredHorizontal = state.scrollManager._scrollbarHoveredHorizontal or false + self._scrollbarDragging = state.scrollManager._scrollbarDragging or false + self._hoveredScrollbar = state.scrollManager._hoveredScrollbar + self._scrollbarDragOffset = state.scrollManager._scrollbarDragOffset or 0 if self._scrollManager then self._scrollManager._scrollbarHoveredVertical = self._scrollbarHoveredVertical @@ -2203,6 +2230,12 @@ function Element:update(dt) self._scrollManager._scrollbarDragging = self._scrollbarDragging self._scrollManager._hoveredScrollbar = self._hoveredScrollbar self._scrollManager._scrollbarDragOffset = self._scrollbarDragOffset + + -- Restore drag start positions for relative movement tracking + self._scrollManager._dragStartMouseX = state.scrollManager._dragStartMouseX or 0 + self._scrollManager._dragStartMouseY = state.scrollManager._dragStartMouseY or 0 + self._scrollManager._dragStartScrollX = state.scrollManager._dragStartScrollX or 0 + self._scrollManager._dragStartScrollY = state.scrollManager._dragStartScrollY or 0 end end end @@ -2317,14 +2350,8 @@ function Element:update(dt) self:_syncScrollManagerState() end - if self._stateId and Element._Context._immediateMode then - Element._StateManager.updateState(self._stateId, { - scrollbarHoveredVertical = self._scrollbarHoveredVertical, - scrollbarHoveredHorizontal = self._scrollbarHoveredHorizontal, - scrollbarDragging = self._scrollbarDragging, - hoveredScrollbar = self._hoveredScrollbar, - }) - end + -- Note: Scrollbar state is saved via saveState() -> ScrollManager:getState() at end of frame + -- This intermediate save is kept for backward compatibility with hover states if self._scrollbarDragging and love.mouse.isDown(1) then self:_handleScrollbarDrag(mx, my) @@ -2439,9 +2466,7 @@ function Element:update(dt) -- Update theme state via ThemeManager local newThemeState = self._themeManager:updateState(isHovering and isActiveElement, anyPressed, self._focused, self.disabled) - -- Update state (in StateManager if in immediate mode, otherwise locally) if self._stateId and Element._Context._immediateMode then - -- Update in StateManager for immediate mode local hover = newThemeState == "hover" local pressed = newThemeState == "pressed" local focused = newThemeState == "active" or self._focused @@ -2455,9 +2480,6 @@ function Element:update(dt) }) end - -- Always update local state for backward compatibility - self._themeState = newThemeState - -- Sync theme state with Renderer module if self._renderer then self._renderer:setThemeState(newThemeState) end @@ -3177,14 +3199,23 @@ function Element:setProperty(property, value) -- Properties that affect layout and require invalidation local layoutProperties = { - width = true, height = true, - padding = true, margin = true, + width = true, + height = true, + padding = true, + margin = true, gap = true, - flexDirection = true, flexWrap = true, - justifyContent = true, alignItems = true, alignContent = true, + flexDirection = true, + flexWrap = true, + justifyContent = true, + alignItems = true, + alignContent = true, positioning = true, - gridRows = true, gridColumns = true, - top = true, right = true, bottom = true, left = true, + gridRows = true, + gridColumns = true, + top = true, + right = true, + bottom = true, + left = true, } if shouldTransition and transitionConfig then diff --git a/modules/ScrollManager.lua b/modules/ScrollManager.lua index eb03862..7a0f9ad 100644 --- a/modules/ScrollManager.lua +++ b/modules/ScrollManager.lua @@ -31,7 +31,11 @@ ---@field _scrollbarHoveredHorizontal boolean -- True if mouse is over horizontal scrollbar ---@field _scrollbarDragging boolean -- True if currently dragging a scrollbar ---@field _hoveredScrollbar string? -- "vertical" or "horizontal" when dragging ----@field _scrollbarDragOffset number -- Offset from thumb top when drag started +---@field _scrollbarDragOffset number -- DEPRECATED: Offset from thumb top when drag started (kept for compatibility) +---@field _dragStartMouseX number -- Mouse X position when drag started +---@field _dragStartMouseY number -- Mouse Y position when drag started +---@field _dragStartScrollX number -- Scroll X position when drag started +---@field _dragStartScrollY number -- Scroll Y position when drag started ---@field _scrollbarPressHandled boolean -- Track if scrollbar press was handled this frame ---@field _touchScrolling boolean -- True if currently touch scrolling ---@field _scrollVelocityX number -- Current horizontal scroll velocity (px/s) @@ -112,7 +116,11 @@ function ScrollManager.new(config, deps) self._scrollbarHoveredHorizontal = false self._scrollbarDragging = false self._hoveredScrollbar = nil -- "vertical" or "horizontal" - self._scrollbarDragOffset = 0 + self._scrollbarDragOffset = 0 -- DEPRECATED: kept for backward compatibility + self._dragStartMouseX = 0 -- Mouse X position when drag started + self._dragStartMouseY = 0 -- Mouse Y position when drag started + self._dragStartScrollX = 0 -- Scroll X position when drag started + self._dragStartScrollY = 0 -- Scroll Y position when drag started self._scrollbarPressHandled = false -- Touch scrolling state @@ -416,26 +424,18 @@ function ScrollManager:handleMousePress(element, mouseX, mouseY, button) end if scrollbar.region == "thumb" then - -- Start dragging thumb + -- Start dragging thumb - store start positions for relative movement tracking self._scrollbarDragging = true self._hoveredScrollbar = scrollbar.component - local dims = self:calculateScrollbarDimensions(element) - if scrollbar.component == "vertical" then - local contentY = element.y + element.padding.top - local trackY = contentY + self.scrollbarPadding - local thumbY = trackY + dims.vertical.thumbY - self._scrollbarDragOffset = mouseY - thumbY - elseif scrollbar.component == "horizontal" then - local contentX = element.x + element.padding.left - local trackX = contentX + self.scrollbarPadding - local thumbX = trackX + dims.horizontal.thumbX - self._scrollbarDragOffset = mouseX - thumbX - end + -- Store drag start positions for relative movement calculation + self._dragStartMouseX = mouseX + self._dragStartMouseY = mouseY + self._dragStartScrollX = self._scrollX + self._dragStartScrollY = self._scrollY return true -- Event consumed elseif scrollbar.region == "track" then - -- Click on track - jump to position self:_scrollToTrackPosition(element, mouseX, mouseY, scrollbar.component) return true end @@ -456,34 +456,36 @@ function ScrollManager:handleMouseMove(element, mouseX, mouseY) local dims = self:calculateScrollbarDimensions(element) if self._hoveredScrollbar == "vertical" then - local contentY = element.y + element.padding.top - local trackY = contentY + self.scrollbarPadding local trackH = dims.vertical.trackHeight local thumbH = dims.vertical.thumbHeight - -- Calculate new thumb position - local newThumbY = mouseY - self._scrollbarDragOffset - trackY - newThumbY = self._utils.clamp(newThumbY, 0, trackH - thumbH) + -- Calculate relative mouse movement from drag start + local mouseDeltaY = mouseY - self._dragStartMouseY - -- Convert thumb position to scroll position - local scrollRatio = (trackH - thumbH) > 0 and (newThumbY / (trackH - thumbH)) or 0 - local newScrollY = scrollRatio * self._maxScrollY + -- Convert mouse delta to scroll delta + -- scrollDelta / maxScroll = thumbDelta / (trackHeight - thumbHeight) + local scrollableTrackHeight = trackH - thumbH + local scrollDelta = scrollableTrackHeight > 0 and (mouseDeltaY / scrollableTrackHeight) * self._maxScrollY or 0 + + local newScrollY = self._dragStartScrollY + scrollDelta + newScrollY = self._utils.clamp(newScrollY, 0, self._maxScrollY) self:setScroll(nil, newScrollY) return true elseif self._hoveredScrollbar == "horizontal" then - local contentX = element.x + element.padding.left - local trackX = contentX + self.scrollbarPadding local trackW = dims.horizontal.trackWidth local thumbW = dims.horizontal.thumbWidth - -- Calculate new thumb position - local newThumbX = mouseX - self._scrollbarDragOffset - trackX - newThumbX = self._utils.clamp(newThumbX, 0, trackW - thumbW) + -- Calculate relative mouse movement from drag start + local mouseDeltaX = mouseX - self._dragStartMouseX - -- Convert thumb position to scroll position - local scrollRatio = (trackW - thumbW) > 0 and (newThumbX / (trackW - thumbW)) or 0 - local newScrollX = scrollRatio * self._maxScrollX + -- Convert mouse delta to scroll delta + local scrollableTrackWidth = trackW - thumbW + local scrollDelta = scrollableTrackWidth > 0 and (mouseDeltaX / scrollableTrackWidth) * self._maxScrollX or 0 + + -- Apply delta to starting scroll position + local newScrollX = self._dragStartScrollX + scrollDelta + newScrollX = self._utils.clamp(newScrollX, 0, self._maxScrollX) self:setScroll(newScrollX, nil) return true @@ -646,7 +648,11 @@ function ScrollManager:getState() _targetScrollY = self._targetScrollY, _scrollbarDragging = self._scrollbarDragging or false, _hoveredScrollbar = self._hoveredScrollbar, - _scrollbarDragOffset = self._scrollbarDragOffset or 0, + _scrollbarDragOffset = self._scrollbarDragOffset or 0, -- Deprecated but kept for compatibility + _dragStartMouseX = self._dragStartMouseX or 0, + _dragStartMouseY = self._dragStartMouseY or 0, + _dragStartScrollX = self._dragStartScrollX or 0, + _dragStartScrollY = self._dragStartScrollY or 0, _scrollbarHoveredVertical = self._scrollbarHoveredVertical or false, _scrollbarHoveredHorizontal = self._scrollbarHoveredHorizontal or false, scrollBarStyle = self.scrollBarStyle, @@ -695,6 +701,23 @@ function ScrollManager:setState(state) self._scrollbarDragOffset = state.scrollbarDragOffset end + -- Restore drag start positions for relative movement tracking + if state._dragStartMouseX ~= nil then + self._dragStartMouseX = state._dragStartMouseX + end + + if state._dragStartMouseY ~= nil then + self._dragStartMouseY = state._dragStartMouseY + end + + if state._dragStartScrollX ~= nil then + self._dragStartScrollX = state._dragStartScrollX + end + + if state._dragStartScrollY ~= nil then + self._dragStartScrollY = state._dragStartScrollY + end + if state._scrollbarHoveredVertical ~= nil then self._scrollbarHoveredVertical = state._scrollbarHoveredVertical end @@ -722,11 +745,11 @@ function ScrollManager:setState(state) if state._contentHeight ~= nil then self._contentHeight = state._contentHeight end - + if state._targetScrollX ~= nil then self._targetScrollX = state._targetScrollX end - + if state._targetScrollY ~= nil then self._targetScrollY = state._targetScrollY end @@ -864,7 +887,7 @@ function ScrollManager:update(dt) self._targetScrollY = nil end end - + if self._targetScrollX then local diff = self._targetScrollX - self._scrollX if math.abs(diff) > 0.5 then @@ -875,7 +898,7 @@ function ScrollManager:update(dt) end end end - + if not self._momentumScrolling then -- Handle bounce back if overscrolled if self.bounceEnabled then diff --git a/modules/StateManager.lua b/modules/StateManager.lua index c30ae9c..de47e7f 100644 --- a/modules/StateManager.lua +++ b/modules/StateManager.lua @@ -37,6 +37,10 @@ local stateDefaults = { scrollbarDragging = false, hoveredScrollbar = nil, scrollbarDragOffset = 0, + dragStartMouseX = 0, + dragStartMouseY = 0, + dragStartScrollX = 0, + dragStartScrollY = 0, -- Scroll position scrollX = 0,