start of scrollable work

This commit is contained in:
Michael Freno
2025-10-30 15:43:04 -04:00
parent 6e9b17b7dc
commit 9e12f7aece
2 changed files with 785 additions and 13 deletions

View File

@@ -2563,6 +2563,49 @@ function Gui.keypressed(key, scancode, isrepeat)
end end
end end
--- Handle mouse wheel scrolling
function Gui.wheelmoved(x, y)
-- Get mouse position
local mx, my = love.mouse.getPosition()
-- Find the deepest scrollable element at mouse position
local function findScrollableAtPosition(elements, mx, my)
-- Check in reverse z-order (top to bottom)
for i = #elements, 1, -1 do
local element = elements[i]
-- Check if mouse is over element
local bx = element.x
local by = element.y
local bw = element._borderBoxWidth or (element.width + element.padding.left + element.padding.right)
local bh = element._borderBoxHeight or (element.height + element.padding.top + element.padding.bottom)
if mx >= bx and mx <= bx + bw and my >= by and my <= by + bh then
-- Check children first (depth-first)
if #element.children > 0 then
local childResult = findScrollableAtPosition(element.children, mx, my)
if childResult then return childResult end
end
-- Check if this element is scrollable
local overflowX = element.overflowX or element.overflow
local overflowY = element.overflowY or element.overflow
if (overflowX == "scroll" or overflowX == "auto" or overflowY == "scroll" or overflowY == "auto") and
(element._overflowX or element._overflowY) then
return element
end
end
end
return nil
end
local scrollableElement = findScrollableAtPosition(Gui.topElements, mx, my)
if scrollableElement then
scrollableElement:_handleWheelScroll(x, y)
end
end
--- Destroy all elements and their children --- Destroy all elements and their children
function Gui.destroy() function Gui.destroy()
for _, win in ipairs(Gui.topElements) do for _, win in ipairs(Gui.topElements) do
@@ -3113,6 +3156,15 @@ Element.__index = Element
---@field cursorColor Color? -- Cursor color (default: nil, uses textColor) ---@field cursorColor Color? -- Cursor color (default: nil, uses textColor)
---@field selectionColor Color? -- Selection background color (default: nil, uses theme or default) ---@field selectionColor Color? -- Selection background color (default: nil, uses theme or default)
---@field cursorBlinkRate number? -- Cursor blink rate in seconds (default: 0.5) ---@field cursorBlinkRate number? -- Cursor blink rate in seconds (default: 0.5)
---@field overflow "visible"|"hidden"|"scroll"|"auto"? -- Overflow behavior (default: "visible")
---@field overflowX "visible"|"hidden"|"scroll"|"auto"? -- X-axis overflow (overrides overflow)
---@field overflowY "visible"|"hidden"|"scroll"|"auto"? -- Y-axis overflow (overrides overflow)
---@field scrollbarWidth number? -- Width of scrollbar track in pixels (default: 12)
---@field scrollbarColor Color? -- Scrollbar thumb color
---@field scrollbarTrackColor Color? -- Scrollbar track color
---@field scrollbarRadius number? -- Corner radius for scrollbar (default: 6)
---@field scrollbarPadding number? -- Padding between scrollbar and edge (default: 2)
---@field scrollSpeed number? -- Pixels per wheel notch (default: 20)
local ElementProps = {} local ElementProps = {}
---@param props ElementProps ---@param props ElementProps
@@ -4055,6 +4107,37 @@ function Element.new(props)
self.transform = props.transform or {} self.transform = props.transform or {}
self.transition = props.transition or {} self.transition = props.transition or {}
-- Overflow and scroll properties
self.overflow = props.overflow or "visible"
self.overflowX = props.overflowX
self.overflowY = props.overflowY
-- Scrollbar configuration
self.scrollbarWidth = props.scrollbarWidth or 12
self.scrollbarColor = props.scrollbarColor or Color.new(0.5, 0.5, 0.5, 0.8)
self.scrollbarTrackColor = props.scrollbarTrackColor or Color.new(0.2, 0.2, 0.2, 0.5)
self.scrollbarRadius = props.scrollbarRadius or 6
self.scrollbarPadding = props.scrollbarPadding or 2
self.scrollSpeed = props.scrollSpeed or 20
-- Internal overflow state
self._overflowX = false
self._overflowY = false
self._contentWidth = 0
self._contentHeight = 0
-- Scroll state
self._scrollX = 0
self._scrollY = 0
self._maxScrollX = 0
self._maxScrollY = 0
-- Scrollbar interaction state
self._scrollbarHovered = false
self._scrollbarDragging = false
self._hoveredScrollbar = nil -- "vertical" or "horizontal"
self._scrollbarDragOffset = 0 -- Offset from thumb top when drag started
return self return self
end end
@@ -4076,6 +4159,494 @@ function Element:getBorderBoxHeight()
return self._borderBoxHeight or (self.height + self.padding.top + self.padding.bottom) return self._borderBoxHeight or (self.height + self.padding.top + self.padding.bottom)
end end
--- Detect if content overflows container bounds
function Element:_detectOverflow()
-- Reset overflow state
self._overflowX = false
self._overflowY = false
self._contentWidth = self.width
self._contentHeight = self.height
-- Skip detection if overflow is visible (no clipping needed)
local overflowX = self.overflowX or self.overflow
local overflowY = self.overflowY or self.overflow
if overflowX == "visible" and overflowY == "visible" then
return
end
-- Calculate content bounds based on children
if #self.children == 0 then
return -- No children, no overflow
end
local minX, minY = math.huge, math.huge
local maxX, maxY = -math.huge, -math.huge
for _, child in ipairs(self.children) do
-- Skip absolutely positioned children (they don't contribute to overflow)
if not child._explicitlyAbsolute then
local childLeft = child.x - self.x
local childTop = child.y - self.y
local childRight = childLeft + child:getBorderBoxWidth() + child.margin.left + child.margin.right
local childBottom = childTop + child:getBorderBoxHeight() + child.margin.top + child.margin.bottom
minX = math.min(minX, childLeft)
minY = math.min(minY, childTop)
maxX = math.max(maxX, childRight)
maxY = math.max(maxY, childBottom)
end
end
-- If no non-absolute children, no overflow
if minX == math.huge then
return
end
-- Calculate content dimensions
self._contentWidth = math.max(0, maxX - minX)
self._contentHeight = math.max(0, maxY - minY)
-- Detect overflow
local containerWidth = self.width
local containerHeight = self.height
self._overflowX = self._contentWidth > containerWidth
self._overflowY = self._contentHeight > containerHeight
-- Calculate maximum scroll bounds
self._maxScrollX = math.max(0, self._contentWidth - containerWidth)
self._maxScrollY = math.max(0, self._contentHeight - containerHeight)
-- Clamp current scroll position to new bounds
self._scrollX = math.max(0, math.min(self._scrollX, self._maxScrollX))
self._scrollY = math.max(0, math.min(self._scrollY, self._maxScrollY))
end
--- Set scroll position with bounds clamping
---@param x number? -- X scroll position (nil to keep current)
---@param y number? -- Y scroll position (nil to keep current)
function Element:setScrollPosition(x, y)
if x ~= nil then
self._scrollX = math.max(0, math.min(x, self._maxScrollX))
end
if y ~= nil then
self._scrollY = math.max(0, math.min(y, self._maxScrollY))
end
end
--- Calculate scrollbar dimensions and positions
---@return table -- {vertical: {visible, trackHeight, thumbHeight, thumbY}, horizontal: {visible, trackWidth, thumbWidth, thumbX}}
function Element:_calculateScrollbarDimensions()
local result = {
vertical = { visible = false, trackHeight = 0, thumbHeight = 0, thumbY = 0 },
horizontal = { visible = false, trackWidth = 0, thumbWidth = 0, thumbX = 0 }
}
local overflowX = self.overflowX or self.overflow
local overflowY = self.overflowY or self.overflow
-- Vertical scrollbar
if self._overflowY and (overflowY == "scroll" or overflowY == "auto") then
result.vertical.visible = true
result.vertical.trackHeight = self.height - (self.scrollbarPadding * 2)
-- Calculate thumb height based on content ratio
local contentRatio = self.height / math.max(self._contentHeight, self.height)
result.vertical.thumbHeight = math.max(20, result.vertical.trackHeight * contentRatio)
-- Calculate thumb position based on scroll ratio
local scrollRatio = self._maxScrollY > 0 and (self._scrollY / self._maxScrollY) or 0
local maxThumbY = result.vertical.trackHeight - result.vertical.thumbHeight
result.vertical.thumbY = maxThumbY * scrollRatio
elseif overflowY == "scroll" then
-- Always show scrollbar for "scroll" mode even without overflow
result.vertical.visible = true
result.vertical.trackHeight = self.height - (self.scrollbarPadding * 2)
result.vertical.thumbHeight = result.vertical.trackHeight
result.vertical.thumbY = 0
end
-- Horizontal scrollbar
if self._overflowX and (overflowX == "scroll" or overflowX == "auto") then
result.horizontal.visible = true
result.horizontal.trackWidth = self.width - (self.scrollbarPadding * 2)
-- Calculate thumb width based on content ratio
local contentRatio = self.width / math.max(self._contentWidth, self.width)
result.horizontal.thumbWidth = math.max(20, result.horizontal.trackWidth * contentRatio)
-- Calculate thumb position based on scroll ratio
local scrollRatio = self._maxScrollX > 0 and (self._scrollX / self._maxScrollX) or 0
local maxThumbX = result.horizontal.trackWidth - result.horizontal.thumbWidth
result.horizontal.thumbX = maxThumbX * scrollRatio
elseif overflowX == "scroll" then
-- Always show scrollbar for "scroll" mode even without overflow
result.horizontal.visible = true
result.horizontal.trackWidth = self.width - (self.scrollbarPadding * 2)
result.horizontal.thumbWidth = result.horizontal.trackWidth
result.horizontal.thumbX = 0
end
return result
end
--- Draw scrollbars
---@param dims table -- Scrollbar dimensions from _calculateScrollbarDimensions()
function Element:_drawScrollbars(dims)
local x, y = self.x, self.y
local w, h = self.width, self.height
-- Determine thumb color based on state
local thumbColor = self.scrollbarColor
if self._scrollbarDragging then
-- Active state: brighter
thumbColor = Color.new(
math.min(1, thumbColor.r * 1.4),
math.min(1, thumbColor.g * 1.4),
math.min(1, thumbColor.b * 1.4),
thumbColor.a
)
elseif self._scrollbarHovered then
-- Hover state: slightly brighter
thumbColor = Color.new(
math.min(1, thumbColor.r * 1.2),
math.min(1, thumbColor.g * 1.2),
math.min(1, thumbColor.b * 1.2),
thumbColor.a
)
end
-- Vertical scrollbar
if dims.vertical.visible then
local trackX = x + w - self.scrollbarWidth - self.scrollbarPadding + self.padding.left
local trackY = y + self.scrollbarPadding + self.padding.top
-- Draw track
love.graphics.setColor(self.scrollbarTrackColor:toRGBA())
love.graphics.rectangle("fill", trackX, trackY,
self.scrollbarWidth, dims.vertical.trackHeight, self.scrollbarRadius)
-- Draw thumb with state-based color
love.graphics.setColor(thumbColor:toRGBA())
love.graphics.rectangle("fill", trackX, trackY + dims.vertical.thumbY,
self.scrollbarWidth, dims.vertical.thumbHeight, self.scrollbarRadius)
end
-- Horizontal scrollbar
if dims.horizontal.visible then
local trackX = x + self.scrollbarPadding + self.padding.left
local trackY = y + h - self.scrollbarWidth - self.scrollbarPadding + self.padding.top
-- Draw track
love.graphics.setColor(self.scrollbarTrackColor:toRGBA())
love.graphics.rectangle("fill", trackX, trackY,
dims.horizontal.trackWidth, self.scrollbarWidth, self.scrollbarRadius)
-- Draw thumb with state-based color
love.graphics.setColor(thumbColor:toRGBA())
love.graphics.rectangle("fill", trackX + dims.horizontal.thumbX, trackY,
dims.horizontal.thumbWidth, self.scrollbarWidth, self.scrollbarRadius)
end
-- Reset color
love.graphics.setColor(1, 1, 1, 1)
end
--- Get scrollbar at mouse position
---@param mouseX number
---@param mouseY number
---@return table|nil -- {component: "vertical"|"horizontal", region: "thumb"|"track"}
function Element:_getScrollbarAtPosition(mouseX, mouseY)
local overflowX = self.overflowX or self.overflow
local overflowY = self.overflowY or self.overflow
if not (overflowX == "scroll" or overflowX == "auto" or overflowY == "scroll" or overflowY == "auto") then
return nil
end
local dims = self:_calculateScrollbarDimensions()
local x, y = self.x, self.y
local w, h = self.width, self.height
-- Check vertical scrollbar
if dims.vertical.visible then
local trackX = x + w - self.scrollbarWidth - self.scrollbarPadding + self.padding.left
local trackY = y + self.scrollbarPadding + self.padding.top
local trackW = self.scrollbarWidth
local trackH = dims.vertical.trackHeight
if mouseX >= trackX and mouseX <= trackX + trackW and
mouseY >= trackY and mouseY <= trackY + trackH then
-- Check if over thumb
local thumbY = trackY + dims.vertical.thumbY
local thumbH = dims.vertical.thumbHeight
if mouseY >= thumbY and mouseY <= thumbY + thumbH then
return { component = "vertical", region = "thumb" }
else
return { component = "vertical", region = "track" }
end
end
end
-- Check horizontal scrollbar
if dims.horizontal.visible then
local trackX = x + self.scrollbarPadding + self.padding.left
local trackY = y + h - self.scrollbarWidth - self.scrollbarPadding + self.padding.top
local trackW = dims.horizontal.trackWidth
local trackH = self.scrollbarWidth
if mouseX >= trackX and mouseX <= trackX + trackW and
mouseY >= trackY and mouseY <= trackY + trackH then
-- Check if over thumb
local thumbX = trackX + dims.horizontal.thumbX
local thumbW = dims.horizontal.thumbWidth
if mouseX >= thumbX and mouseX <= thumbX + thumbW then
return { component = "horizontal", region = "thumb" }
else
return { component = "horizontal", region = "track" }
end
end
end
return nil
end
--- Handle scrollbar mouse press
---@param mouseX number
---@param mouseY number
---@param button number
---@return boolean -- True if event was consumed
function Element:_handleScrollbarPress(mouseX, mouseY, button)
if button ~= 1 then return false end -- Only left click
local scrollbar = self:_getScrollbarAtPosition(mouseX, mouseY)
if not scrollbar then return false end
if scrollbar.region == "thumb" then
-- Start dragging thumb
self._scrollbarDragging = true
self._hoveredScrollbar = scrollbar.component
local dims = self:_calculateScrollbarDimensions()
if scrollbar.component == "vertical" then
local trackY = self.y + self.scrollbarPadding + self.padding.top
local thumbY = trackY + dims.vertical.thumbY
self._scrollbarDragOffset = mouseY - thumbY
elseif scrollbar.component == "horizontal" then
local trackX = self.x + self.scrollbarPadding + self.padding.left
local thumbX = trackX + dims.horizontal.thumbX
self._scrollbarDragOffset = mouseX - thumbX
end
return true -- Event consumed
elseif scrollbar.region == "track" then
-- Click on track - jump to position
self:_scrollToTrackPosition(mouseX, mouseY, scrollbar.component)
return true
end
return false
end
--- Handle scrollbar drag
---@param mouseX number
---@param mouseY number
---@return boolean -- True if event was consumed
function Element:_handleScrollbarDrag(mouseX, mouseY)
if not self._scrollbarDragging then return false end
local dims = self:_calculateScrollbarDimensions()
if self._hoveredScrollbar == "vertical" then
local trackY = self.y + self.scrollbarPadding + self.padding.top
local trackH = dims.vertical.trackHeight
local thumbH = dims.vertical.thumbHeight
-- Calculate new thumb position
local newThumbY = mouseY - self._scrollbarDragOffset - trackY
newThumbY = math.max(0, math.min(newThumbY, trackH - thumbH))
-- Convert thumb position to scroll position
local scrollRatio = (trackH - thumbH) > 0 and (newThumbY / (trackH - thumbH)) or 0
local newScrollY = scrollRatio * self._maxScrollY
self:setScrollPosition(nil, newScrollY)
return true
elseif self._hoveredScrollbar == "horizontal" then
local trackX = self.x + self.scrollbarPadding + self.padding.left
local trackW = dims.horizontal.trackWidth
local thumbW = dims.horizontal.thumbWidth
-- Calculate new thumb position
local newThumbX = mouseX - self._scrollbarDragOffset - trackX
newThumbX = math.max(0, math.min(newThumbX, trackW - thumbW))
-- Convert thumb position to scroll position
local scrollRatio = (trackW - thumbW) > 0 and (newThumbX / (trackW - thumbW)) or 0
local newScrollX = scrollRatio * self._maxScrollX
self:setScrollPosition(newScrollX, nil)
return true
end
return false
end
--- Handle scrollbar release
---@param button number
---@return boolean -- True if event was consumed
function Element:_handleScrollbarRelease(button)
if button ~= 1 then return false end
if self._scrollbarDragging then
self._scrollbarDragging = false
return true
end
return false
end
--- Scroll to track click position
---@param mouseX number
---@param mouseY number
---@param component string -- "vertical" or "horizontal"
function Element:_scrollToTrackPosition(mouseX, mouseY, component)
local dims = self:_calculateScrollbarDimensions()
if component == "vertical" then
local trackY = self.y + self.scrollbarPadding + self.padding.top
local trackH = dims.vertical.trackHeight
local thumbH = dims.vertical.thumbHeight
-- Calculate target thumb position (centered on click)
local targetThumbY = mouseY - trackY - (thumbH / 2)
targetThumbY = math.max(0, math.min(targetThumbY, trackH - thumbH))
-- Convert to scroll position
local scrollRatio = (trackH - thumbH) > 0 and (targetThumbY / (trackH - thumbH)) or 0
local newScrollY = scrollRatio * self._maxScrollY
self:setScrollPosition(nil, newScrollY)
elseif component == "horizontal" then
local trackX = self.x + self.scrollbarPadding + self.padding.left
local trackW = dims.horizontal.trackWidth
local thumbW = dims.horizontal.thumbWidth
-- Calculate target thumb position (centered on click)
local targetThumbX = mouseX - trackX - (thumbW / 2)
targetThumbX = math.max(0, math.min(targetThumbX, trackW - thumbW))
-- Convert to scroll position
local scrollRatio = (trackW - thumbW) > 0 and (targetThumbX / (trackW - thumbW)) or 0
local newScrollX = scrollRatio * self._maxScrollX
self:setScrollPosition(newScrollX, nil)
end
end
--- Handle mouse wheel scrolling
---@param x number -- Horizontal scroll amount
---@param y number -- Vertical scroll amount
---@return boolean -- True if scroll was handled
function Element:_handleWheelScroll(x, y)
local overflowX = self.overflowX or self.overflow
local overflowY = self.overflowY or self.overflow
if not (overflowX == "scroll" or overflowX == "auto" or overflowY == "scroll" or overflowY == "auto") then
return false
end
local hasVerticalOverflow = self._overflowY and self._maxScrollY > 0
local hasHorizontalOverflow = self._overflowX and self._maxScrollX > 0
local scrolled = false
-- Vertical scrolling
if y ~= 0 and hasVerticalOverflow then
local delta = -y * self.scrollSpeed -- Negative because wheel up = scroll up
local newScrollY = self._scrollY + delta
self:setScrollPosition(nil, newScrollY)
scrolled = true
end
-- Horizontal scrolling
if x ~= 0 and hasHorizontalOverflow then
local delta = -x * self.scrollSpeed
local newScrollX = self._scrollX + delta
self:setScrollPosition(newScrollX, nil)
scrolled = true
end
return scrolled
end
--- Get current scroll position
---@return number scrollX, number scrollY
function Element:getScrollPosition()
return self._scrollX, self._scrollY
end
--- Get maximum scroll bounds
---@return number maxScrollX, number maxScrollY
function Element:getMaxScroll()
return self._maxScrollX, self._maxScrollY
end
--- Get scroll percentage (0-1)
---@return number percentX, number percentY
function Element:getScrollPercentage()
local percentX = self._maxScrollX > 0 and (self._scrollX / self._maxScrollX) or 0
local percentY = self._maxScrollY > 0 and (self._scrollY / self._maxScrollY) or 0
return percentX, percentY
end
--- Check if element has overflow
---@return boolean hasOverflowX, boolean hasOverflowY
function Element:hasOverflow()
return self._overflowX, self._overflowY
end
--- Get content dimensions (including overflow)
---@return number contentWidth, number contentHeight
function Element:getContentSize()
return self._contentWidth, self._contentHeight
end
--- Scroll by delta amount
---@param dx number? -- X delta (nil for no change)
---@param dy number? -- Y delta (nil for no change)
function Element:scrollBy(dx, dy)
if dx then
self._scrollX = math.max(0, math.min(self._scrollX + dx, self._maxScrollX))
end
if dy then
self._scrollY = math.max(0, math.min(self._scrollY + dy, self._maxScrollY))
end
end
--- Scroll to top
function Element:scrollToTop()
self:setScrollPosition(nil, 0)
end
--- Scroll to bottom
function Element:scrollToBottom()
self:setScrollPosition(nil, self._maxScrollY)
end
--- Scroll to left
function Element:scrollToLeft()
self:setScrollPosition(0, nil)
end
--- Scroll to right
function Element:scrollToRight()
self:setScrollPosition(self._maxScrollX, nil)
end
--- Get the current state's scaled content padding --- Get the current state's scaled content padding
--- Returns the contentPadding for the current theme state, scaled to the element's size --- Returns the contentPadding for the current theme state, scaled to the element's size
---@return table|nil -- {left, top, right, bottom} or nil if no contentPadding ---@return table|nil -- {left, top, right, bottom} or nil if no contentPadding
@@ -4673,6 +5244,9 @@ function Element:layoutChildren()
end end
end end
end end
-- Detect overflow after children are laid out
self:_detectOverflow()
end end
--- Destroy element and its children --- Destroy element and its children
@@ -5013,6 +5587,19 @@ function Element:draw(backdropCanvas)
-- Helper function to draw children (with or without clipping) -- Helper function to draw children (with or without clipping)
local function drawChildren() local function drawChildren()
-- Determine if we need overflow clipping
local overflowX = self.overflowX or self.overflow
local overflowY = self.overflowY or self.overflow
local needsOverflowClipping = (overflowX ~= "visible" or overflowY ~= "visible") and (overflowX ~= nil or overflowY ~= nil)
-- Apply scroll offset if overflow is not visible
local hasScrollOffset = needsOverflowClipping and (self._scrollX ~= 0 or self._scrollY ~= 0)
if hasScrollOffset then
love.graphics.push()
love.graphics.translate(-self._scrollX, -self._scrollY)
end
if hasRoundedCorners and #sortedChildren > 0 then if hasRoundedCorners and #sortedChildren > 0 then
-- Use stencil to clip children to rounded rectangle -- Use stencil to clip children to rounded rectangle
-- BORDER-BOX MODEL: Use stored border-box dimensions for clipping -- BORDER-BOX MODEL: Use stored border-box dimensions for clipping
@@ -5028,12 +5615,30 @@ function Element:draw(backdropCanvas)
end end
love.graphics.setStencilTest() love.graphics.setStencilTest()
elseif needsOverflowClipping and #sortedChildren > 0 then
-- Clip content for overflow hidden/scroll/auto without rounded corners
local contentX = self.x + self.padding.left
local contentY = self.y + self.padding.top
local contentWidth = self.width
local contentHeight = self.height
love.graphics.setScissor(contentX, contentY, contentWidth, contentHeight)
for _, child in ipairs(sortedChildren) do
child:draw(backdropCanvas)
end
love.graphics.setScissor()
else else
-- No clipping needed -- No clipping needed
for _, child in ipairs(sortedChildren) do for _, child in ipairs(sortedChildren) do
child:draw(backdropCanvas) child:draw(backdropCanvas)
end end
end end
if hasScrollOffset then
love.graphics.pop()
end
end end
-- Apply content blur if configured -- Apply content blur if configured
@@ -5047,6 +5652,16 @@ function Element:draw(backdropCanvas)
else else
drawChildren() drawChildren()
end end
-- Draw scrollbars if overflow is scroll or auto
local overflowX = self.overflowX or self.overflow
local overflowY = self.overflowY or self.overflow
if overflowX == "scroll" or overflowX == "auto" or overflowY == "scroll" or overflowY == "auto" then
local scrollbarDims = self:_calculateScrollbarDimensions()
if scrollbarDims.vertical.visible or scrollbarDims.horizontal.visible then
self:_drawScrollbars(scrollbarDims)
end
end
end end
--- Update element (propagate to children) --- Update element (propagate to children)
@@ -5083,9 +5698,30 @@ function Element:update(dt)
end end
end end
-- Handle scrollbar hover detection
local mx, my = love.mouse.getPosition()
local scrollbar = self:_getScrollbarAtPosition(mx, my)
local wasHovered = self._scrollbarHovered
if scrollbar then
self._scrollbarHovered = true
self._hoveredScrollbar = scrollbar.component
else
if not self._scrollbarDragging then
self._scrollbarHovered = false
self._hoveredScrollbar = nil
end
end
-- Handle scrollbar dragging
if self._scrollbarDragging and love.mouse.isDown(1) then
self:_handleScrollbarDrag(mx, my)
elseif self._scrollbarDragging then
-- Mouse button released
self._scrollbarDragging = false
end
-- Handle click detection for element with enhanced event system -- Handle click detection for element with enhanced event system
if self.callback or self.themeComponent then if self.callback or self.themeComponent then
local mx, my = love.mouse.getPosition()
-- Clickable area is the border box (x, y already includes padding) -- Clickable area is the border box (x, y already includes padding)
-- BORDER-BOX MODEL: Use stored border-box dimensions for hit detection -- BORDER-BOX MODEL: Use stored border-box dimensions for hit detection
local bx = self.x local bx = self.x
@@ -5134,6 +5770,11 @@ function Element:update(dt)
if love.mouse.isDown(button) then if love.mouse.isDown(button) then
-- Button is pressed down -- Button is pressed down
if not self._pressed[button] then if not self._pressed[button] then
-- Check if press is on scrollbar first
if button == 1 and self:_handleScrollbarPress(mx, my, button) then
-- Scrollbar consumed the event, mark as pressed to prevent callback
self._pressed[button] = true
else
-- Just pressed - fire press event and record drag start position -- Just pressed - fire press event and record drag start position
local modifiers = getModifiers() local modifiers = getModifiers()
local pressEvent = InputEvent.new({ local pressEvent = InputEvent.new({
@@ -5146,6 +5787,7 @@ function Element:update(dt)
}) })
self.callback(self, pressEvent) self.callback(self, pressEvent)
self._pressed[button] = true self._pressed[button] = true
end
-- Record drag start position per button -- Record drag start position per button
self._dragStartX[button] = mx self._dragStartX[button] = mx
@@ -5525,6 +6167,9 @@ function Element:recalculateUnits(newViewportWidth, newViewportHeight)
self._borderBoxHeight = self.height + self.padding.top + self.padding.bottom self._borderBoxHeight = self.height + self.padding.top + self.padding.bottom
end end
-- For pixel units, height stays as-is (may have been manually modified) -- For pixel units, height stays as-is (may have been manually modified)
-- Detect overflow after layout calculations
self:_detectOverflow()
end end
--- Resize element and its children based on game window size change --- Resize element and its children based on game window size change

View File

@@ -0,0 +1,127 @@
-- Example 15: Scrollable Elements
-- Demonstrates scrollable containers with overflow detection and visual scrollbars
local FlexLove = require("FlexLove")
local Gui = FlexLove.Gui
local Color = FlexLove.Color
local enums = FlexLove.enums
local Lv = love
function Lv.load()
Gui.init({
baseScale = { width = 1920, height = 1080 },
})
-- Title
Gui.new({
x = "2vw",
y = "2vh",
width = "96vw",
height = "6vh",
text = "FlexLove Example 15: Scrollable Elements",
textSize = "4vh",
textColor = Color.new(1, 1, 1, 1),
textAlign = enums.TextAlign.CENTER,
})
-- Example 1: Vertical scroll with auto scrollbars
local verticalScroll = Gui.new({
x = "5vw",
y = "12vh",
width = "25vw",
height = "35vh",
overflow = "auto",
backgroundColor = Color.new(0.15, 0.15, 0.2, 1),
cornerRadius = 8,
positioning = enums.Positioning.FLEX,
flexDirection = enums.FlexDirection.VERTICAL,
gap = 5,
padding = { top = 10, right = 10, bottom = 10, left = 10 },
})
-- Add many items to create overflow
for i = 1, 20 do
Gui.new({
parent = verticalScroll,
height = "5vh",
backgroundColor = Color.new(0.3 + (i % 3) * 0.1, 0.4, 0.6, 1),
cornerRadius = 4,
text = "Item " .. i,
textColor = Color.new(1, 1, 1, 1),
textAlign = enums.TextAlign.CENTER,
})
end
-- Example 2: Custom styled scrollbar
local customScroll = Gui.new({
x = "35vw",
y = "12vh",
width = "60vw",
height = "35vh",
overflow = "auto",
backgroundColor = Color.new(0.1, 0.1, 0.15, 1),
cornerRadius = 8,
scrollbarWidth = 16,
scrollbarColor = Color.new(0.3, 0.6, 0.9, 1),
scrollbarTrackColor = Color.new(0.15, 0.15, 0.2, 0.8),
scrollbarRadius = 8,
positioning = enums.Positioning.FLEX,
flexDirection = enums.FlexDirection.VERTICAL,
gap = 10,
padding = { top = 15, right = 15, bottom = 15, left = 15 },
})
-- Add content
for i = 1, 25 do
Gui.new({
parent = customScroll,
height = "6vh",
backgroundColor = Color.new(0.2, 0.25, 0.3, 1),
cornerRadius = 6,
text = "Custom Scrollbar Item " .. i,
textColor = Color.new(0.9, 0.9, 1, 1),
textSize = "2vh",
})
end
-- Instructions
Gui.new({
x = "5vw",
y = "52vh",
width = "90vw",
height = "40vh",
backgroundColor = Color.new(0.1, 0.15, 0.2, 0.9),
cornerRadius = 8,
padding = { top = 15, right = 15, bottom = 15, left = 15 },
text = [[Instructions:
• Use mouse wheel to scroll elements under cursor
• Click and drag scrollbar thumb to scroll
• Click on scrollbar track to jump to position
• Scrollbars automatically appear when content overflows
• overflow="auto" shows scrollbars only when needed
• overflow="scroll" always shows scrollbars
• overflow="hidden" clips without scrollbars
• overflow="visible" shows all content (default)
Scrollbar colors change on hover and when dragging!]],
textColor = Color.new(0.9, 0.9, 1, 1),
textSize = "2vh",
})
end
function Lv.update(dt)
Gui.update(dt)
end
function Lv.draw()
love.graphics.clear(0.05, 0.05, 0.08, 1)
Gui.draw()
end
function Lv.resize(w, h)
Gui.resize(w, h)
end
function Lv.wheelmoved(x, y)
Gui.wheelmoved(x, y)
end