fixed scrollframe not sending scroll events to its children fixed scrollframe scrolling even if mouse is not hovering over the element improved the behaviour of the flow layout
428 lines
17 KiB
Lua
428 lines
17 KiB
Lua
local elementManager = require("elementManager")
|
|
local Container = elementManager.getElement("Container")
|
|
local tHex = require("libraries/colorHex")
|
|
---@configDescription A scrollable container that automatically displays scrollbars when content overflows.
|
|
|
|
--- A container that provides automatic scrolling capabilities with visual scrollbars. Displays vertical and/or horizontal scrollbars when child content exceeds the container's dimensions.
|
|
--- @run [[
|
|
--- local basalt = require("basalt")
|
|
---
|
|
--- local main = basalt.getMainFrame()
|
|
---
|
|
--- -- Create a ScrollFrame with content larger than the frame
|
|
--- local scrollFrame = main:addScrollFrame({
|
|
--- x = 2,
|
|
--- y = 2,
|
|
--- width = 30,
|
|
--- height = 12,
|
|
--- background = colors.lightGray
|
|
--- })
|
|
---
|
|
--- -- Add a title
|
|
--- scrollFrame:addLabel({
|
|
--- x = 2,
|
|
--- y = 1,
|
|
--- text = "ScrollFrame Example",
|
|
--- foreground = colors.yellow
|
|
--- })
|
|
---
|
|
--- -- Add multiple labels that exceed the frame height
|
|
--- for i = 1, 20 do
|
|
--- scrollFrame:addLabel({
|
|
--- x = 2,
|
|
--- y = i + 2,
|
|
--- text = "Line " .. i .. " - Scroll to see more",
|
|
--- foreground = i % 2 == 0 and colors.white or colors.lightGray
|
|
--- })
|
|
--- end
|
|
---
|
|
--- -- Add some interactive buttons at different positions
|
|
--- scrollFrame:addButton({
|
|
--- x = 2,
|
|
--- y = 24,
|
|
--- width = 15,
|
|
--- height = 3,
|
|
--- text = "Button 1",
|
|
--- background = colors.blue
|
|
--- })
|
|
--- :onClick(function()
|
|
--- scrollFrame:addLabel({
|
|
--- x = 18,
|
|
--- y = 24,
|
|
--- text = "Clicked!",
|
|
--- foreground = colors.lime
|
|
--- })
|
|
--- end)
|
|
---
|
|
--- scrollFrame:addButton({
|
|
--- x = 2,
|
|
--- y = 28,
|
|
--- width = 15,
|
|
--- height = 3,
|
|
--- text = "Button 2",
|
|
--- background = colors.green
|
|
--- })
|
|
--- :onClick(function()
|
|
--- scrollFrame:addLabel({
|
|
--- x = 18,
|
|
--- y = 28,
|
|
--- text = "Nice!",
|
|
--- foreground = colors.orange
|
|
--- })
|
|
--- end)
|
|
---
|
|
--- -- Info label outside the scroll frame
|
|
--- main:addLabel({
|
|
--- x = 2,
|
|
--- y = 15,
|
|
--- text = "Use mouse wheel to scroll!",
|
|
--- foreground = colors.gray
|
|
--- })
|
|
---
|
|
--- basalt.run()
|
|
--- ]]
|
|
---@class ScrollFrame : Container
|
|
local ScrollFrame = setmetatable({}, Container)
|
|
ScrollFrame.__index = ScrollFrame
|
|
|
|
---@property showScrollBar boolean true Whether to show scrollbars
|
|
ScrollFrame.defineProperty(ScrollFrame, "showScrollBar", {default = true, type = "boolean", canTriggerRender = true})
|
|
|
|
---@property scrollBarSymbol string "_" The symbol used for the scrollbar handle
|
|
ScrollFrame.defineProperty(ScrollFrame, "scrollBarSymbol", {default = " ", type = "string", canTriggerRender = true})
|
|
|
|
---@property scrollBarBackground string "\127" The symbol used for the scrollbar background
|
|
ScrollFrame.defineProperty(ScrollFrame, "scrollBarBackground", {default = "\127", type = "string", canTriggerRender = true})
|
|
|
|
---@property scrollBarColor color lightGray Color of the scrollbar handle
|
|
ScrollFrame.defineProperty(ScrollFrame, "scrollBarColor", {default = colors.lightGray, type = "color", canTriggerRender = true})
|
|
|
|
---@property scrollBarBackgroundColor color gray Background color of the scrollbar
|
|
ScrollFrame.defineProperty(ScrollFrame, "scrollBarBackgroundColor", {default = colors.gray, type = "color", canTriggerRender = true})
|
|
|
|
---@property contentWidth number 0 The total width of the content (calculated from children)
|
|
ScrollFrame.defineProperty(ScrollFrame, "contentWidth", {
|
|
default = 0,
|
|
type = "number",
|
|
getter = function(self)
|
|
local maxWidth = 0
|
|
local children = self.get("children")
|
|
for _, child in ipairs(children) do
|
|
local childX = child.get("x")
|
|
local childWidth = child.get("width")
|
|
local childRight = childX + childWidth - 1
|
|
if childRight > maxWidth then
|
|
maxWidth = childRight
|
|
end
|
|
end
|
|
return maxWidth
|
|
end
|
|
})
|
|
|
|
---@property contentHeight number 0 The total height of the content (calculated from children)
|
|
ScrollFrame.defineProperty(ScrollFrame, "contentHeight", {
|
|
default = 0,
|
|
type = "number",
|
|
getter = function(self)
|
|
local maxHeight = 0
|
|
local children = self.get("children")
|
|
for _, child in ipairs(children) do
|
|
local childY = child.get("y")
|
|
local childHeight = child.get("height")
|
|
local childBottom = childY + childHeight - 1
|
|
if childBottom > maxHeight then
|
|
maxHeight = childBottom
|
|
end
|
|
end
|
|
return maxHeight
|
|
end
|
|
})
|
|
|
|
ScrollFrame.defineEvent(ScrollFrame, "mouse_click")
|
|
ScrollFrame.defineEvent(ScrollFrame, "mouse_drag")
|
|
ScrollFrame.defineEvent(ScrollFrame, "mouse_up")
|
|
ScrollFrame.defineEvent(ScrollFrame, "mouse_scroll")
|
|
|
|
--- Creates a new ScrollFrame instance
|
|
--- @shortDescription Creates a new ScrollFrame instance
|
|
--- @return ScrollFrame self The newly created ScrollFrame instance
|
|
--- @private
|
|
function ScrollFrame.new()
|
|
local self = setmetatable({}, ScrollFrame):__init()
|
|
self.class = ScrollFrame
|
|
self.set("width", 20)
|
|
self.set("height", 10)
|
|
self.set("z", 5)
|
|
return self
|
|
end
|
|
|
|
--- Initializes a ScrollFrame instance
|
|
--- @shortDescription Initializes a ScrollFrame instance
|
|
--- @param props table Initial properties
|
|
--- @param basalt table The basalt instance
|
|
--- @return ScrollFrame self The initialized ScrollFrame instance
|
|
--- @private
|
|
function ScrollFrame:init(props, basalt)
|
|
Container.init(self, props, basalt)
|
|
self.set("type", "ScrollFrame")
|
|
return self
|
|
end
|
|
|
|
--- Handles mouse click events for scrollbars and content
|
|
--- @shortDescription Handles mouse click events
|
|
--- @param button number The mouse button (1=left, 2=right, 3=middle)
|
|
--- @param x number The x-coordinate of the click
|
|
--- @param y number The y-coordinate of the click
|
|
--- @return boolean Whether the event was handled
|
|
--- @protected
|
|
function ScrollFrame:mouse_click(button, x, y)
|
|
if Container.mouse_click(self, button, x, y) then
|
|
local relX, relY = self:getRelativePosition(x, y)
|
|
local width = self.get("width")
|
|
local height = self.get("height")
|
|
local showScrollBar = self.get("showScrollBar")
|
|
local contentWidth = self.get("contentWidth")
|
|
local contentHeight = self.get("contentHeight")
|
|
local needsHorizontalScrollBar = showScrollBar and contentWidth > width
|
|
local viewportHeight = needsHorizontalScrollBar and height - 1 or height
|
|
local needsVerticalScrollBar = showScrollBar and contentHeight > viewportHeight
|
|
local viewportWidth = needsVerticalScrollBar and width - 1 or width
|
|
|
|
if needsVerticalScrollBar and relX == width and (not needsHorizontalScrollBar or relY < height) then
|
|
local scrollHeight = viewportHeight
|
|
local handleSize = math.max(1, math.floor((viewportHeight / contentHeight) * scrollHeight))
|
|
local maxOffset = contentHeight - viewportHeight
|
|
|
|
local currentPercent = maxOffset > 0 and (self.get("offsetY") / maxOffset * 100) or 0
|
|
local handlePos = math.floor((currentPercent / 100) * (scrollHeight - handleSize)) + 1
|
|
|
|
if relY >= handlePos and relY < handlePos + handleSize then
|
|
self._scrollBarDragging = true
|
|
self._scrollBarDragOffset = relY - handlePos
|
|
else
|
|
local newPercent = ((relY - 1) / (scrollHeight - handleSize)) * 100
|
|
local newOffset = math.floor((newPercent / 100) * maxOffset + 0.5)
|
|
self.set("offsetY", math.max(0, math.min(maxOffset, newOffset)))
|
|
end
|
|
return true
|
|
end
|
|
|
|
if needsHorizontalScrollBar and relY == height and (not needsVerticalScrollBar or relX < width) then
|
|
local scrollWidth = viewportWidth
|
|
local handleSize = math.max(1, math.floor((viewportWidth / contentWidth) * scrollWidth))
|
|
local maxOffset = contentWidth - viewportWidth
|
|
|
|
local currentPercent = maxOffset > 0 and (self.get("offsetX") / maxOffset * 100) or 0
|
|
local handlePos = math.floor((currentPercent / 100) * (scrollWidth - handleSize)) + 1
|
|
|
|
if relX >= handlePos and relX < handlePos + handleSize then
|
|
self._hScrollBarDragging = true
|
|
self._hScrollBarDragOffset = relX - handlePos
|
|
else
|
|
local newPercent = ((relX - 1) / (scrollWidth - handleSize)) * 100
|
|
local newOffset = math.floor((newPercent / 100) * maxOffset + 0.5)
|
|
self.set("offsetX", math.max(0, math.min(maxOffset, newOffset)))
|
|
end
|
|
return true
|
|
end
|
|
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
--- Handles mouse drag events for scrollbar
|
|
--- @shortDescription Handles mouse drag events for scrollbar
|
|
--- @param button number The mouse button being dragged
|
|
--- @param x number The x-coordinate of the drag
|
|
--- @param y number The y-coordinate of the drag
|
|
--- @return boolean Whether the event was handled
|
|
--- @protected
|
|
function ScrollFrame:mouse_drag(button, x, y)
|
|
if self._scrollBarDragging then
|
|
local _, relY = self:getRelativePosition(x, y)
|
|
local height = self.get("height")
|
|
local contentWidth = self.get("contentWidth")
|
|
local contentHeight = self.get("contentHeight")
|
|
local width = self.get("width")
|
|
local needsHorizontalScrollBar = self.get("showScrollBar") and contentWidth > width
|
|
|
|
local viewportHeight = needsHorizontalScrollBar and height - 1 or height
|
|
local scrollHeight = viewportHeight
|
|
local handleSize = math.max(1, math.floor((viewportHeight / contentHeight) * scrollHeight))
|
|
local maxOffset = contentHeight - viewportHeight
|
|
|
|
relY = math.max(1, math.min(scrollHeight, relY))
|
|
|
|
local newPos = relY - (self._scrollBarDragOffset or 0)
|
|
local newPercent = ((newPos - 1) / (scrollHeight - handleSize)) * 100
|
|
local newOffset = math.floor((newPercent / 100) * maxOffset + 0.5)
|
|
|
|
self.set("offsetY", math.max(0, math.min(maxOffset, newOffset)))
|
|
return true
|
|
end
|
|
|
|
if self._hScrollBarDragging then
|
|
local relX, _ = self:getRelativePosition(x, y)
|
|
local width = self.get("width")
|
|
local contentWidth = self.get("contentWidth")
|
|
local contentHeight = self.get("contentHeight")
|
|
local height = self.get("height")
|
|
local needsHorizontalScrollBar = self.get("showScrollBar") and contentWidth > width
|
|
local viewportHeight = needsHorizontalScrollBar and height - 1 or height
|
|
local needsVerticalScrollBar = self.get("showScrollBar") and contentHeight > viewportHeight
|
|
local viewportWidth = needsVerticalScrollBar and width - 1 or width
|
|
local scrollWidth = viewportWidth
|
|
local handleSize = math.max(1, math.floor((viewportWidth / contentWidth) * scrollWidth))
|
|
local maxOffset = contentWidth - viewportWidth
|
|
|
|
relX = math.max(1, math.min(scrollWidth, relX))
|
|
|
|
local newPos = relX - (self._hScrollBarDragOffset or 0)
|
|
local newPercent = ((newPos - 1) / (scrollWidth - handleSize)) * 100
|
|
local newOffset = math.floor((newPercent / 100) * maxOffset + 0.5)
|
|
|
|
self.set("offsetX", math.max(0, math.min(maxOffset, newOffset)))
|
|
return true
|
|
end
|
|
|
|
return Container.mouse_drag and Container.mouse_drag(self, button, x, y) or false
|
|
end
|
|
|
|
--- Handles mouse up events to stop scrollbar dragging
|
|
--- @shortDescription Handles mouse up events to stop scrollbar dragging
|
|
--- @param button number The mouse button that was released
|
|
--- @param x number The x-coordinate of the release
|
|
--- @param y number The y-coordinate of the release
|
|
--- @return boolean Whether the event was handled
|
|
--- @protected
|
|
function ScrollFrame:mouse_up(button, x, y)
|
|
if self._scrollBarDragging then
|
|
self._scrollBarDragging = false
|
|
self._scrollBarDragOffset = nil
|
|
return true
|
|
end
|
|
|
|
if self._hScrollBarDragging then
|
|
self._hScrollBarDragging = false
|
|
self._hScrollBarDragOffset = nil
|
|
return true
|
|
end
|
|
|
|
return Container.mouse_up and Container.mouse_up(self, button, x, y) or false
|
|
end
|
|
|
|
--- Handles mouse scroll events
|
|
--- @shortDescription Handles mouse scroll events
|
|
--- @param direction number 1 for up, -1 for down
|
|
--- @param x number Mouse x position relative to element
|
|
--- @param y number Mouse y position relative to element
|
|
--- @return boolean Whether the event was handled
|
|
--- @protected
|
|
function ScrollFrame:mouse_scroll(direction, x, y)
|
|
if self:isInBounds(x, y) then
|
|
local xOffset, yOffset = self.get("offsetX"), self.get("offsetY")
|
|
local relX, relY = self:getRelativePosition(x + xOffset, y + yOffset)
|
|
|
|
local success, child = self:callChildrenEvent(true, "mouse_scroll", direction, relX, relY)
|
|
if success then
|
|
return true
|
|
end
|
|
|
|
local height = self.get("height")
|
|
local width = self.get("width")
|
|
local offsetY = self.get("offsetY")
|
|
local offsetX = self.get("offsetX")
|
|
local contentWidth = self.get("contentWidth")
|
|
local contentHeight = self.get("contentHeight")
|
|
|
|
local needsHorizontalScrollBar = self.get("showScrollBar") and contentWidth > width
|
|
local viewportHeight = needsHorizontalScrollBar and height - 1 or height
|
|
local needsVerticalScrollBar = self.get("showScrollBar") and contentHeight > viewportHeight
|
|
local viewportWidth = needsVerticalScrollBar and width - 1 or width
|
|
|
|
if needsVerticalScrollBar then
|
|
local maxScroll = math.max(0, contentHeight - viewportHeight)
|
|
local newScroll = math.min(maxScroll, math.max(0, offsetY + direction))
|
|
self.set("offsetY", newScroll)
|
|
elseif needsHorizontalScrollBar then
|
|
local maxScroll = math.max(0, contentWidth - viewportWidth)
|
|
local newScroll = math.min(maxScroll, math.max(0, offsetX + direction))
|
|
self.set("offsetX", newScroll)
|
|
end
|
|
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
--- Renders the ScrollFrame and its scrollbars
|
|
--- @shortDescription Renders the ScrollFrame and its scrollbars
|
|
--- @protected
|
|
function ScrollFrame:render()
|
|
Container.render(self)
|
|
|
|
local height = self.get("height")
|
|
local width = self.get("width")
|
|
local offsetY = self.get("offsetY")
|
|
local offsetX = self.get("offsetX")
|
|
local showScrollBar = self.get("showScrollBar")
|
|
local contentWidth = self.get("contentWidth")
|
|
local contentHeight = self.get("contentHeight")
|
|
local needsHorizontalScrollBar = showScrollBar and contentWidth > width
|
|
local viewportHeight = needsHorizontalScrollBar and height - 1 or height
|
|
local needsVerticalScrollBar = showScrollBar and contentHeight > viewportHeight
|
|
local viewportWidth = needsVerticalScrollBar and width - 1 or width
|
|
|
|
if needsVerticalScrollBar then
|
|
local scrollHeight = viewportHeight
|
|
local handleSize = math.max(1, math.floor((viewportHeight / contentHeight) * scrollHeight))
|
|
local maxOffset = contentHeight - viewportHeight
|
|
local scrollBarSymbol = self.get("scrollBarSymbol")
|
|
local scrollBarBg = self.get("scrollBarBackground")
|
|
local scrollBarColor = self.get("scrollBarColor")
|
|
local scrollBarBgColor = self.get("scrollBarBackgroundColor")
|
|
local foreground = self.get("foreground")
|
|
|
|
local currentPercent = maxOffset > 0 and (offsetY / maxOffset * 100) or 0
|
|
local handlePos = math.floor((currentPercent / 100) * (scrollHeight - handleSize)) + 1
|
|
|
|
for i = 1, scrollHeight do
|
|
if i >= handlePos and i < handlePos + handleSize then
|
|
self:blit(width, i, scrollBarSymbol, tHex[scrollBarColor], tHex[scrollBarBgColor])
|
|
else
|
|
self:blit(width, i, scrollBarBg, tHex[foreground], tHex[scrollBarBgColor])
|
|
end
|
|
end
|
|
end
|
|
|
|
if needsHorizontalScrollBar then
|
|
local scrollWidth = viewportWidth
|
|
local handleSize = math.max(1, math.floor((viewportWidth / contentWidth) * scrollWidth))
|
|
local maxOffset = contentWidth - viewportWidth
|
|
local scrollBarSymbol = self.get("scrollBarSymbol")
|
|
local scrollBarBg = self.get("scrollBarBackground")
|
|
local scrollBarColor = self.get("scrollBarColor")
|
|
local scrollBarBgColor = self.get("scrollBarBackgroundColor")
|
|
local foreground = self.get("foreground")
|
|
|
|
local currentPercent = maxOffset > 0 and (offsetX / maxOffset * 100) or 0
|
|
local handlePos = math.floor((currentPercent / 100) * (scrollWidth - handleSize)) + 1
|
|
|
|
for i = 1, scrollWidth do
|
|
if i >= handlePos and i < handlePos + handleSize then
|
|
self:blit(i, height, scrollBarSymbol, tHex[scrollBarColor], tHex[scrollBarBgColor])
|
|
else
|
|
self:blit(i, height, scrollBarBg, tHex[foreground], tHex[scrollBarBgColor])
|
|
end
|
|
end
|
|
end
|
|
|
|
if needsVerticalScrollBar and needsHorizontalScrollBar then
|
|
local background = self.get("background")
|
|
self:blit(width, height, " ", tHex[background], tHex[background])
|
|
end
|
|
end
|
|
|
|
return ScrollFrame
|