- Updated DocsGenerator to support run code and item tables
- Updated Table to support new Collection System (could break things, sorry) - Updated Tree to support new Collection System - Added experimental ScrollFrame - Updated Menu to support Collection System
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -17,4 +17,5 @@ Drawer.lua
|
||||
Breadcrumb.lua
|
||||
Dialog.lua
|
||||
DockLayout.lua
|
||||
ContextMenu.lua
|
||||
ContextMenu.lua
|
||||
Toast.lua
|
||||
@@ -5,51 +5,50 @@ local tHex = require("libraries/colorHex")
|
||||
---@configDescription A DropDown menu that shows a list of selectable items
|
||||
---@configDefault false
|
||||
|
||||
--- Item Properties:
|
||||
--- Property|Type|Description
|
||||
--- -------|------|-------------
|
||||
--- text|string|The display text for the item
|
||||
--- separator|boolean|Makes item a divider line
|
||||
--- callback|function|Function called when selected
|
||||
--- foreground|color|Normal text color
|
||||
--- background|color|Normal background color
|
||||
--- selectedForeground|color|Text color when selected
|
||||
--- selectedBackground|color|Background when selected
|
||||
---@tableType ItemTable
|
||||
---@tableField text string The display text for the item
|
||||
---@tableField callback function Function called when selected
|
||||
---@tableField fg color Normal text color
|
||||
---@tableField bg color Normal background color
|
||||
---@tableField selectedFg color Text color when selected
|
||||
---@tableField selectedBg color Background when selected
|
||||
|
||||
--- A collapsible selection menu that expands to show multiple options when clicked. Supports single and multi-selection modes, custom item styling, separators, and item callbacks.
|
||||
--- @usage -- Create a styled dropdown menu
|
||||
--- @usage local dropdown = main:addDropDown()
|
||||
--- @usage :setPosition(5, 5)
|
||||
--- @usage :setSize(20, 1) -- Height expands when opened
|
||||
--- @usage :setSelectedText("Select an option...")
|
||||
--- @usage
|
||||
--- @usage -- Add items with different styles and callbacks
|
||||
--- @usage dropdown:setItems({
|
||||
--- @usage {
|
||||
--- @usage text = "Category A",
|
||||
--- @usage background = colors.blue,
|
||||
--- @usage foreground = colors.white
|
||||
--- @usage },
|
||||
--- @usage { separator = true, text = "-" }, -- Add a separator
|
||||
--- @usage {
|
||||
--- @usage text = "Option 1",
|
||||
--- @usage callback = function(self)
|
||||
--- @usage -- Handle selection
|
||||
--- @usage basalt.debug("Selected Option 1")
|
||||
--- @usage end
|
||||
--- @usage },
|
||||
--- @usage {
|
||||
--- @usage text = "Option 2",
|
||||
--- @usage -- Custom colors when selected
|
||||
--- @usage selectedBackground = colors.green,
|
||||
--- @usage selectedForeground = colors.white
|
||||
--- @usage }
|
||||
--- @usage })
|
||||
--- @usage
|
||||
--- @usage -- Listen for selections
|
||||
--- @usage dropdown:onChange(function(self, value)
|
||||
--- @usage basalt.debug("Selected:", value)
|
||||
--- @usage end)
|
||||
--- @usage [[
|
||||
--- -- Create a styled dropdown menu
|
||||
--- local dropdown = main:addDropDown()
|
||||
--- :setPosition(5, 5)
|
||||
--- :setSize(20, 1) -- Height expands when opened
|
||||
--- :setSelectedText("Select an option...")
|
||||
---
|
||||
--- -- Add items with different styles and callbacks
|
||||
--- dropdown:setItems({
|
||||
--- {
|
||||
--- text = "Category A",
|
||||
--- background = colors.blue,
|
||||
--- foreground = colors.white
|
||||
--- },
|
||||
--- { separator = true, text = "-" }, -- Add a separator
|
||||
--- {
|
||||
--- text = "Option 1",
|
||||
--- callback = function(self)
|
||||
--- -- Handle selection
|
||||
--- basalt.debug("Selected Option 1")
|
||||
--- end
|
||||
--- },
|
||||
--- {
|
||||
--- text = "Option 2",
|
||||
--- -- Custom colors when selected
|
||||
--- selectedBackground = colors.green,
|
||||
--- selectedForeground = colors.white
|
||||
--- }
|
||||
--- })
|
||||
---
|
||||
--- -- Listen for selections
|
||||
--- dropdown:onChange(function(self, value)
|
||||
--- basalt.debug("Selected:", value)
|
||||
--- end)
|
||||
--- ]]
|
||||
---@class DropDown : List
|
||||
local DropDown = setmetatable({}, List)
|
||||
DropDown.__index = DropDown
|
||||
@@ -107,7 +106,6 @@ function DropDown:mouse_click(button, x, y)
|
||||
end
|
||||
return true
|
||||
elseif isOpen and relY > 1 then
|
||||
-- Forward to List handler for scrollbar handling
|
||||
return List.mouse_click(self, button, x, y - 1)
|
||||
end
|
||||
return false
|
||||
@@ -135,19 +133,18 @@ end
|
||||
function DropDown:mouse_up(button, x, y)
|
||||
if self:hasState("opened") then
|
||||
local relX, relY = self:getRelativePosition(x, y)
|
||||
|
||||
-- Only handle item selection in mouse_up (relY > 1 = list area)
|
||||
|
||||
if relY > 1 and self.get("selectable") and not self._scrollBarDragging then
|
||||
local itemIndex = (relY - 1) + self.get("offset")
|
||||
local items = self.get("items")
|
||||
|
||||
|
||||
if itemIndex <= #items then
|
||||
local item = items[itemIndex]
|
||||
if type(item) == "string" then
|
||||
item = {text = item}
|
||||
items[itemIndex] = item
|
||||
end
|
||||
|
||||
|
||||
if not self.get("multiSelection") then
|
||||
for _, otherItem in ipairs(items) do
|
||||
if type(otherItem) == "table" then
|
||||
@@ -155,24 +152,23 @@ function DropDown:mouse_up(button, x, y)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
item.selected = not item.selected
|
||||
|
||||
|
||||
if item.callback then
|
||||
item.callback(self)
|
||||
end
|
||||
|
||||
|
||||
self:fireEvent("select", itemIndex, item)
|
||||
self:unsetState("opened")
|
||||
self:unsetState("clicked")
|
||||
self.set("height", 1)
|
||||
self:updateRender()
|
||||
|
||||
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
-- Always forward to List for cleanup and unset clicked state
|
||||
|
||||
List.mouse_up(self, button, x, y - 1)
|
||||
self:unsetState("clicked")
|
||||
return true
|
||||
|
||||
@@ -44,6 +44,14 @@ List.defineEvent(List, "mouse_drag")
|
||||
List.defineEvent(List, "mouse_scroll")
|
||||
List.defineEvent(List, "key")
|
||||
|
||||
---@tableType ItemTable
|
||||
---@tableField text string The display text for the item
|
||||
---@tableField callback function Function called when selected
|
||||
---@tableField fg color Normal text color
|
||||
---@tableField bg color Normal background color
|
||||
---@tableField selectedFg color Text color when selected
|
||||
---@tableField selectedBg color Background when selected
|
||||
|
||||
local entrySchema = {
|
||||
text = { type = "string", default = "Entry" },
|
||||
bg = { type = "number", default = nil },
|
||||
@@ -265,7 +273,7 @@ function List:key(keyCode)
|
||||
if Collection.key(self, keyCode) and self.get("selectable") then
|
||||
local items = self.get("items")
|
||||
local currentIndex = self:getSelectedIndex()
|
||||
|
||||
|
||||
if keyCode == keys.up then
|
||||
self:selectPrevious()
|
||||
if currentIndex and currentIndex > 1 then
|
||||
|
||||
@@ -3,15 +3,39 @@ local List = require("elements/List")
|
||||
local tHex = require("libraries/colorHex")
|
||||
---@configDescription A horizontal menu bar with selectable items.
|
||||
|
||||
--- This is the menu class. It provides a horizontal menu bar with selectable items.
|
||||
--- Menu items are displayed in a single row and can have custom colors and callbacks.
|
||||
--- This is the menu class. It provides a horizontal menu bar with selectable items. Menu items are displayed in a single row and can have custom colors and callbacks.
|
||||
---@class Menu : List
|
||||
local Menu = setmetatable({}, List)
|
||||
Menu.__index = Menu
|
||||
|
||||
---@tableType ItemTable
|
||||
---@tableField text string The display text for the item
|
||||
---@tableField callback function Function called when selected
|
||||
---@tableField fg color Normal text color
|
||||
---@tableField bg color Normal background color
|
||||
---@tableField selectedFg color Text color when selected
|
||||
---@tableField selectedBg color Background when selected
|
||||
|
||||
---@property separatorColor color gray The color used for separator items in the menu
|
||||
Menu.defineProperty(Menu, "separatorColor", {default = colors.gray, type = "color"})
|
||||
|
||||
---@property spacing number 0 The number of spaces between menu items
|
||||
Menu.defineProperty(Menu, "spacing", {default = 1, type = "number", canTriggerRender = true})
|
||||
|
||||
---@property horizontalOffset number 0 Current horizontal scroll offset
|
||||
Menu.defineProperty(Menu, "horizontalOffset", {
|
||||
default = 0,
|
||||
type = "number",
|
||||
canTriggerRender = true,
|
||||
setter = function(self, value)
|
||||
local maxOffset = math.max(0, self:getTotalWidth() - self.get("width"))
|
||||
return math.min(maxOffset, math.max(0, value))
|
||||
end
|
||||
})
|
||||
|
||||
---@property maxWidth number nil Maximum width before scrolling is enabled (nil = auto-size to items)
|
||||
Menu.defineProperty(Menu, "maxWidth", {default = nil, type = "number", canTriggerRender = true})
|
||||
|
||||
--- Creates a new Menu instance
|
||||
--- @shortDescription Creates a new Menu instance
|
||||
--- @return Menu self The newly created Menu instance
|
||||
@@ -33,83 +57,151 @@ end
|
||||
function Menu:init(props, basalt)
|
||||
List.init(self, props, basalt)
|
||||
self.set("type", "Menu")
|
||||
|
||||
self:observe("items", function()
|
||||
local maxWidth = self.get("maxWidth")
|
||||
if maxWidth then
|
||||
self.set("width", math.min(maxWidth, self:getTotalWidth()), true)
|
||||
else
|
||||
self.set("width", self:getTotalWidth(), true)
|
||||
end
|
||||
end)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Sets the menu items
|
||||
--- @shortDescription Sets the menu items and calculates total width
|
||||
--- @param items table[] List of items with {text, separator, callback, foreground, background} properties
|
||||
--- @return Menu self The Menu instance
|
||||
--- @usage menu:setItems({{text="File"}, {separator=true}, {text="Edit"}})
|
||||
function Menu:setItems(items)
|
||||
local listItems = {}
|
||||
--- Calculates the total width of all menu items with spacing
|
||||
--- @shortDescription Calculates total width of menu items
|
||||
--- @return number totalWidth The total width of all items
|
||||
function Menu:getTotalWidth()
|
||||
local items = self.get("items")
|
||||
local spacing = self.get("spacing")
|
||||
local totalWidth = 0
|
||||
for _, item in ipairs(items) do
|
||||
if item.separator then
|
||||
table.insert(listItems, {text = item.text or "|", selectable = false})
|
||||
totalWidth = totalWidth + 1
|
||||
|
||||
for i, item in ipairs(items) do
|
||||
if type(item) == "table" then
|
||||
totalWidth = totalWidth + #item.text
|
||||
else
|
||||
local text = " " .. item.text .. " "
|
||||
item.text = text
|
||||
table.insert(listItems, item)
|
||||
totalWidth = totalWidth + #text
|
||||
totalWidth = totalWidth + #tostring(item) + 2
|
||||
end
|
||||
|
||||
if i < #items then
|
||||
totalWidth = totalWidth + spacing
|
||||
end
|
||||
end
|
||||
self.set("width", totalWidth)
|
||||
return List.setItems(self, listItems)
|
||||
|
||||
return totalWidth
|
||||
end
|
||||
|
||||
--- @shortDescription Renders the menu horizontally with proper spacing and colors
|
||||
--- @protected
|
||||
function Menu:render()
|
||||
VisualElement.render(self)
|
||||
local viewportWidth = self.get("width")
|
||||
local spacing = self.get("spacing")
|
||||
local offset = self.get("horizontalOffset")
|
||||
local items = self.get("items")
|
||||
|
||||
local itemPositions = {}
|
||||
local currentX = 1
|
||||
|
||||
for i, item in ipairs(self.get("items")) do
|
||||
for i, item in ipairs(items) do
|
||||
if type(item) == "string" then
|
||||
item = {text = " "..item.." "}
|
||||
self.get("items")[i] = item
|
||||
items[i] = item
|
||||
end
|
||||
|
||||
local isSelected = item.selected
|
||||
local fg = item.selectable == false and self.get("separatorColor") or
|
||||
(isSelected and (item.selectedForeground or self.get("selectedForeground")) or
|
||||
(item.foreground or self.get("foreground")))
|
||||
|
||||
local bg = isSelected and
|
||||
(item.selectedBackground or self.get("selectedBackground")) or
|
||||
(item.background or self.get("background"))
|
||||
|
||||
self:blit(currentX, 1, item.text,
|
||||
string.rep(tHex[fg], #item.text),
|
||||
string.rep(tHex[bg], #item.text))
|
||||
itemPositions[i] = {
|
||||
startX = currentX,
|
||||
endX = currentX + #item.text - 1,
|
||||
text = item.text,
|
||||
item = item
|
||||
}
|
||||
|
||||
currentX = currentX + #item.text
|
||||
|
||||
if i < #items and spacing > 0 then
|
||||
currentX = currentX + spacing
|
||||
end
|
||||
end
|
||||
|
||||
for i, pos in ipairs(itemPositions) do
|
||||
local item = pos.item
|
||||
local itemStartInViewport = pos.startX - offset
|
||||
local itemEndInViewport = pos.endX - offset
|
||||
|
||||
if itemStartInViewport > viewportWidth then
|
||||
break
|
||||
end
|
||||
|
||||
if itemEndInViewport >= 1 then
|
||||
local visibleStart = math.max(1, itemStartInViewport)
|
||||
local visibleEnd = math.min(viewportWidth, itemEndInViewport)
|
||||
local textStartIdx = math.max(1, 1 - itemStartInViewport + 1)
|
||||
local textEndIdx = math.min(#pos.text, #pos.text - (itemEndInViewport - viewportWidth))
|
||||
local visibleText = pos.text:sub(textStartIdx, textEndIdx)
|
||||
|
||||
if #visibleText > 0 then
|
||||
local isSelected = item.selected
|
||||
local fg = item.selectable == false and self.get("separatorColor") or
|
||||
(isSelected and (item.selectedForeground or self.get("selectedForeground")) or
|
||||
(item.foreground or self.get("foreground")))
|
||||
|
||||
local bg = isSelected and
|
||||
(item.selectedBackground or self.get("selectedBackground")) or
|
||||
(item.background or self.get("background"))
|
||||
|
||||
self:blit(visibleStart, 1, visibleText,
|
||||
string.rep(tHex[fg], #visibleText),
|
||||
string.rep(tHex[bg], #visibleText))
|
||||
end
|
||||
|
||||
if i < #items and spacing > 0 then
|
||||
local spacingStart = pos.endX + 1 - offset
|
||||
local spacingEnd = spacingStart + spacing - 1
|
||||
|
||||
if spacingEnd >= 1 and spacingStart <= viewportWidth then
|
||||
local visibleSpacingStart = math.max(1, spacingStart)
|
||||
local visibleSpacingEnd = math.min(viewportWidth, spacingEnd)
|
||||
local spacingWidth = visibleSpacingEnd - visibleSpacingStart + 1
|
||||
|
||||
if spacingWidth > 0 then
|
||||
local spacingText = string.rep(" ", spacingWidth)
|
||||
self:blit(visibleSpacingStart, 1, spacingText,
|
||||
string.rep(tHex[self.get("foreground")], spacingWidth),
|
||||
string.rep(tHex[self.get("background")], spacingWidth))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- @shortDescription Handles mouse click events and item selection
|
||||
--- @param button number The button that was clicked
|
||||
--- @param x number The x position of the click
|
||||
--- @param y number The y position of the click
|
||||
--- @return boolean Whether the event was handled
|
||||
--- @protected
|
||||
function Menu:mouse_click(button, x, y)
|
||||
if not VisualElement.mouse_click(self, button, x, y) then return false end
|
||||
if(self.get("selectable") == false) then return false end
|
||||
local relX = select(1, self:getRelativePosition(x, y))
|
||||
local offset = self.get("horizontalOffset")
|
||||
local spacing = self.get("spacing")
|
||||
local items = self.get("items")
|
||||
|
||||
local virtualX = relX + offset
|
||||
local currentX = 1
|
||||
|
||||
for i, item in ipairs(self.get("items")) do
|
||||
if relX >= currentX and relX < currentX + #item.text then
|
||||
for i, item in ipairs(items) do
|
||||
local itemWidth = #item.text
|
||||
|
||||
if virtualX >= currentX and virtualX < currentX + itemWidth then
|
||||
if item.selectable ~= false then
|
||||
if type(item) == "string" then
|
||||
item = {text = item}
|
||||
self.get("items")[i] = item
|
||||
items[i] = item
|
||||
end
|
||||
|
||||
if not self.get("multiSelection") then
|
||||
for _, otherItem in ipairs(self.get("items")) do
|
||||
for _, otherItem in ipairs(items) do
|
||||
if type(otherItem) == "table" then
|
||||
otherItem.selected = false
|
||||
end
|
||||
@@ -125,7 +217,25 @@ function Menu:mouse_click(button, x, y)
|
||||
end
|
||||
return true
|
||||
end
|
||||
currentX = currentX + #item.text
|
||||
currentX = currentX + itemWidth
|
||||
|
||||
if i < #items and spacing > 0 then
|
||||
currentX = currentX + spacing
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
--- @shortDescription Handles mouse scroll events for horizontal scrolling
|
||||
--- @protected
|
||||
function Menu:mouse_scroll(direction, x, y)
|
||||
if VisualElement.mouse_scroll(self, direction, x, y) then
|
||||
local offset = self.get("horizontalOffset")
|
||||
local maxOffset = math.max(0, self:getTotalWidth() - self.get("width"))
|
||||
|
||||
offset = math.min(maxOffset, math.max(0, offset + (direction * 3)))
|
||||
self.set("horizontalOffset", offset)
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
330
src/elements/ScrollFrame.lua
Normal file
330
src/elements/ScrollFrame.lua
Normal file
@@ -0,0 +1,330 @@
|
||||
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.
|
||||
---@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)
|
||||
local height = self.get("height")
|
||||
local width = self.get("width")
|
||||
local offsetY = self.get("offsetY")
|
||||
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 maxScroll = math.max(0, contentHeight - viewportHeight)
|
||||
|
||||
local newScroll = math.min(maxScroll, math.max(0, offsetY + direction))
|
||||
self.set("offsetY", newScroll)
|
||||
|
||||
return true
|
||||
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
|
||||
@@ -1,13 +1,12 @@
|
||||
local VisualElement = require("elements/VisualElement")
|
||||
local Collection = require("elements/Collection")
|
||||
local tHex = require("libraries/colorHex")
|
||||
|
||||
--- This is the table class. It provides a sortable data grid with customizable columns,
|
||||
--- row selection, and scrolling capabilities.
|
||||
--- This is the table class. It provides a sortable data grid with customizable columns, row selection, and scrolling capabilities. Built on Collection for consistent item management.
|
||||
--- @usage local people = container:addTable():setWidth(40)
|
||||
--- @usage people:setColumns({{name="Name",width=12}, {name="Age",width=10}, {name="Country",width=15}})
|
||||
--- @usage people:setData({{"Alice", 30, "USA"}, {"Bob", 25, "UK"}})
|
||||
---@class Table : VisualElement
|
||||
local Table = setmetatable({}, VisualElement)
|
||||
--- @usage people:addRow("Alice", 30, "USA"):addRow("Bob", 25, "UK")
|
||||
---@class Table : Collection
|
||||
local Table = setmetatable({}, Collection)
|
||||
Table.__index = Table
|
||||
|
||||
---@property columns table {} List of column definitions with {name, width} properties
|
||||
@@ -27,34 +26,55 @@ Table.defineProperty(Table, "columns", {default = {}, type = "table", canTrigger
|
||||
end
|
||||
return t
|
||||
end})
|
||||
---@property data table {} The table data as array of row arrays
|
||||
Table.defineProperty(Table, "data", {default = {}, type = "table", canTriggerRender = true, setter=function(self, value)
|
||||
self.set("scrollOffset", 0)
|
||||
self.set("selectedRow", nil)
|
||||
self.set("sortColumn", nil)
|
||||
self.set("sortDirection", "asc")
|
||||
return value
|
||||
end})
|
||||
---@property selectedRow number? nil Currently selected row index
|
||||
Table.defineProperty(Table, "selectedRow", {default = nil, type = "number", canTriggerRender = true})
|
||||
---@property headerColor color blue Color of the column headers
|
||||
Table.defineProperty(Table, "headerColor", {default = colors.blue, type = "color"})
|
||||
---@property selectedColor color lightBlue Background color of selected row
|
||||
Table.defineProperty(Table, "selectedColor", {default = colors.lightBlue, type = "color"})
|
||||
---@property gridColor color gray Color of grid lines
|
||||
Table.defineProperty(Table, "gridColor", {default = colors.gray, type = "color"})
|
||||
---@property sortColumn number? nil Currently sorted column index
|
||||
Table.defineProperty(Table, "sortColumn", {default = nil, type = "number", canTriggerRender = true})
|
||||
---@property sortDirection string "asc" Sort direction ("asc" or "desc")
|
||||
Table.defineProperty(Table, "sortDirection", {default = "asc", type = "string", canTriggerRender = true})
|
||||
---@property scrollOffset number 0 Current scroll position
|
||||
Table.defineProperty(Table, "scrollOffset", {default = 0, type = "number", canTriggerRender = true})
|
||||
---@property customSortFunction table {} Custom sort functions for columns
|
||||
Table.defineProperty(Table, "customSortFunction", {default = {}, type = "table"})
|
||||
---@property offset number 0 Scroll offset for vertical scrolling
|
||||
Table.defineProperty(Table, "offset", {
|
||||
default = 0,
|
||||
type = "number",
|
||||
canTriggerRender = true,
|
||||
setter = function(self, value)
|
||||
local maxOffset = math.max(0, #self.get("items") - (self.get("height") - 1))
|
||||
return math.min(maxOffset, math.max(0, value))
|
||||
end
|
||||
})
|
||||
|
||||
---@property showScrollBar boolean true Whether to show the scrollbar when items exceed height
|
||||
Table.defineProperty(Table, "showScrollBar", {default = true, type = "boolean", canTriggerRender = true})
|
||||
|
||||
---@property scrollBarSymbol string " " Symbol used for the scrollbar handle
|
||||
Table.defineProperty(Table, "scrollBarSymbol", {default = " ", type = "string", canTriggerRender = true})
|
||||
|
||||
---@property scrollBarBackground string "\127" Symbol used for the scrollbar background
|
||||
Table.defineProperty(Table, "scrollBarBackground", {default = "\127", type = "string", canTriggerRender = true})
|
||||
|
||||
---@property scrollBarColor color lightGray Color of the scrollbar handle
|
||||
Table.defineProperty(Table, "scrollBarColor", {default = colors.lightGray, type = "color", canTriggerRender = true})
|
||||
|
||||
---@property scrollBarBackgroundColor color gray Background color of the scrollbar
|
||||
Table.defineProperty(Table, "scrollBarBackgroundColor", {default = colors.gray, type = "color", canTriggerRender = true})
|
||||
|
||||
---@event onRowSelect {rowIndex number, row table} Fired when a row is selected
|
||||
Table.defineEvent(Table, "mouse_click")
|
||||
Table.defineEvent(Table, "mouse_drag")
|
||||
Table.defineEvent(Table, "mouse_up")
|
||||
Table.defineEvent(Table, "mouse_scroll")
|
||||
|
||||
local entrySchema = {
|
||||
cells = { type = "table", default = {} },
|
||||
_sortValues = { type = "table", default = {} },
|
||||
selected = { type = "boolean", default = false },
|
||||
text = { type = "string", default = "" }
|
||||
}
|
||||
|
||||
--- Creates a new Table instance
|
||||
--- @shortDescription Creates a new Table instance
|
||||
--- @return Table self The newly created Table instance
|
||||
@@ -74,15 +94,97 @@ end
|
||||
--- @return Table self The initialized instance
|
||||
--- @protected
|
||||
function Table:init(props, basalt)
|
||||
VisualElement.init(self, props, basalt)
|
||||
Collection.init(self, props, basalt)
|
||||
self._entrySchema = entrySchema
|
||||
self.set("type", "Table")
|
||||
|
||||
self:observe("sortColumn", function()
|
||||
if self.get("sortColumn") then
|
||||
self:sortByColumn(self.get("sortColumn"))
|
||||
end
|
||||
end)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Adds a new row to the table
|
||||
--- @shortDescription Adds a new row with cell values
|
||||
--- @param ... any The cell values for the new row
|
||||
--- @return Table self The Table instance
|
||||
--- @usage table:addRow("Alice", 30, "USA")
|
||||
function Table:addRow(...)
|
||||
local cells = {...}
|
||||
Collection.addItem(self, {
|
||||
cells = cells,
|
||||
_sortValues = cells, -- Store original values for sorting
|
||||
text = table.concat(cells, " ") -- For compatibility if needed
|
||||
})
|
||||
return self
|
||||
end
|
||||
|
||||
--- Removes a row by index
|
||||
--- @shortDescription Removes a row at the specified index
|
||||
--- @param rowIndex number The index of the row to remove
|
||||
--- @return Table self The Table instance
|
||||
function Table:removeRow(rowIndex)
|
||||
local items = self.get("items")
|
||||
if items[rowIndex] then
|
||||
table.remove(items, rowIndex)
|
||||
self.set("items", items)
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
--- Gets a row by index
|
||||
--- @shortDescription Gets the row data at the specified index
|
||||
--- @param rowIndex number The index of the row
|
||||
--- @return table? row The row data or nil
|
||||
function Table:getRow(rowIndex)
|
||||
local items = self.get("items")
|
||||
return items[rowIndex]
|
||||
end
|
||||
|
||||
--- Updates a specific cell value
|
||||
--- @shortDescription Updates a cell value at row and column
|
||||
--- @param rowIndex number The row index
|
||||
--- @param colIndex number The column index
|
||||
--- @param value any The new value
|
||||
--- @return Table self The Table instance
|
||||
function Table:updateCell(rowIndex, colIndex, value)
|
||||
local items = self.get("items")
|
||||
if items[rowIndex] and items[rowIndex].cells then
|
||||
items[rowIndex].cells[colIndex] = value
|
||||
self.set("items", items)
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
--- Gets the currently selected row
|
||||
--- @shortDescription Gets the currently selected row data
|
||||
--- @return table? row The selected row or nil
|
||||
function Table:getSelectedRow()
|
||||
local items = self.get("items")
|
||||
for _, item in ipairs(items) do
|
||||
local isSelected = item._data and item._data.selected or item.selected
|
||||
if isSelected then
|
||||
return item
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Clears all table data
|
||||
--- @shortDescription Removes all rows from the table
|
||||
--- @return Table self The Table instance
|
||||
function Table:clearData()
|
||||
self.set("items", {})
|
||||
return self
|
||||
end
|
||||
|
||||
--- Adds a new column to the table
|
||||
--- @shortDescription Adds a new column to the table
|
||||
--- @param name string The name of the column
|
||||
--- @param width number The width of the column
|
||||
--- @param width number|string The width of the column (number, "auto", or "30%")
|
||||
--- @return Table self The Table instance
|
||||
function Table:addColumn(name, width)
|
||||
local columns = self.get("columns")
|
||||
@@ -91,17 +193,6 @@ function Table:addColumn(name, width)
|
||||
return self
|
||||
end
|
||||
|
||||
--- Adds a new row of data to the table
|
||||
--- @shortDescription Adds a new row of data to the table
|
||||
--- @param ... any The data for the new row
|
||||
--- @return Table self The Table instance
|
||||
function Table:addData(...)
|
||||
local data = self.get("data")
|
||||
table.insert(data, {...})
|
||||
self.set("data", data)
|
||||
return self
|
||||
end
|
||||
|
||||
--- Sets a custom sort function for a specific column
|
||||
--- @shortDescription Sets a custom sort function for a column
|
||||
--- @param columnIndex number The index of the column
|
||||
@@ -114,56 +205,54 @@ function Table:setColumnSortFunction(columnIndex, sortFn)
|
||||
return self
|
||||
end
|
||||
|
||||
--- Adds data with both display and sort values
|
||||
--- @shortDescription Adds formatted data with raw sort values
|
||||
--- @param displayData table The formatted data for display
|
||||
--- @param sortData table The raw data for sorting (optional)
|
||||
--- Set data with automatic formatting
|
||||
--- @shortDescription Sets table data with optional column formatters
|
||||
--- @param rawData table The raw data array (array of row arrays)
|
||||
--- @param formatters table? Optional formatter functions for columns {[2] = function(value) return value end}
|
||||
--- @return Table self The Table instance
|
||||
function Table:setFormattedData(displayData, sortData)
|
||||
local enrichedData = {}
|
||||
--- @usage table:setData({{...}}, {[1] = tostring, [2] = function(age) return age.."y" end})
|
||||
function Table:setData(rawData, formatters)
|
||||
self:clearData()
|
||||
|
||||
for i, row in ipairs(displayData) do
|
||||
local enrichedRow = {}
|
||||
for j, cell in ipairs(row) do
|
||||
enrichedRow[j] = cell
|
||||
for _, row in ipairs(rawData) do
|
||||
local cells = {}
|
||||
local sortValues = {}
|
||||
|
||||
for j, cellValue in ipairs(row) do
|
||||
sortValues[j] = cellValue
|
||||
|
||||
if formatters and formatters[j] then
|
||||
cells[j] = formatters[j](cellValue)
|
||||
else
|
||||
cells[j] = cellValue
|
||||
end
|
||||
end
|
||||
|
||||
if sortData and sortData[i] then
|
||||
enrichedRow._sortValues = sortData[i]
|
||||
end
|
||||
|
||||
table.insert(enrichedData, enrichedRow)
|
||||
Collection.addItem(self, {
|
||||
cells = cells,
|
||||
_sortValues = sortValues,
|
||||
text = table.concat(cells, " ")
|
||||
})
|
||||
end
|
||||
|
||||
self.set("data", enrichedData)
|
||||
return self
|
||||
end
|
||||
|
||||
--- Set data with automatic formatting
|
||||
--- @shortDescription Sets table data with optional column formatters
|
||||
--- @param rawData table The raw data array
|
||||
--- @param formatters table Optional formatter functions for columns {[2] = function(value) return value end}
|
||||
--- @return Table self The Table instance
|
||||
function Table:setData(rawData, formatters)
|
||||
if not formatters then
|
||||
self.set("data", rawData)
|
||||
return self
|
||||
end
|
||||
--- Gets all table data
|
||||
--- @shortDescription Gets all rows as array of cell arrays
|
||||
--- @return table data Array of row cell arrays
|
||||
function Table:getData()
|
||||
local items = self.get("items")
|
||||
local data = {}
|
||||
|
||||
local formattedData = {}
|
||||
for i, row in ipairs(rawData) do
|
||||
local formattedRow = {}
|
||||
for j, cell in ipairs(row) do
|
||||
if formatters[j] then
|
||||
formattedRow[j] = formatters[j](cell)
|
||||
else
|
||||
formattedRow[j] = cell
|
||||
end
|
||||
for _, item in ipairs(items) do
|
||||
local cells = item._data and item._data.cells or item.cells
|
||||
if cells then
|
||||
table.insert(data, cells)
|
||||
end
|
||||
table.insert(formattedData, formattedRow)
|
||||
end
|
||||
|
||||
return self:setFormattedData(formattedData, rawData)
|
||||
return data
|
||||
end
|
||||
|
||||
--- @shortDescription Calculates column widths for rendering
|
||||
@@ -241,33 +330,38 @@ end
|
||||
--- @param columnIndex number The index of the column to sort by
|
||||
--- @param fn function? Optional custom sorting function
|
||||
--- @return Table self The Table instance
|
||||
function Table:sortData(columnIndex, fn)
|
||||
local data = self.get("data")
|
||||
function Table:sortByColumn(columnIndex, fn)
|
||||
local items = self.get("items")
|
||||
local direction = self.get("sortDirection")
|
||||
local customSorts = self.get("customSortFunction")
|
||||
|
||||
local sortFn = fn or customSorts[columnIndex]
|
||||
|
||||
if sortFn then
|
||||
table.sort(data, function(a, b)
|
||||
table.sort(items, function(a, b)
|
||||
return sortFn(a, b, direction)
|
||||
end)
|
||||
else
|
||||
table.sort(data, function(a, b)
|
||||
if not a or not b then return false end
|
||||
table.sort(items, function(a, b)
|
||||
local aCells = a._data and a._data.cells or a.cells
|
||||
local bCells = b._data and b._data.cells or b.cells
|
||||
local aSortValues = a._data and a._data._sortValues or a._sortValues
|
||||
local bSortValues = b._data and b._data._sortValues or b._sortValues
|
||||
|
||||
if not a or not b or not aCells or not bCells then return false end
|
||||
|
||||
local valueA, valueB
|
||||
|
||||
if a._sortValues and a._sortValues[columnIndex] then
|
||||
valueA = a._sortValues[columnIndex]
|
||||
if aSortValues and aSortValues[columnIndex] then
|
||||
valueA = aSortValues[columnIndex]
|
||||
else
|
||||
valueA = a[columnIndex]
|
||||
valueA = aCells[columnIndex]
|
||||
end
|
||||
|
||||
if b._sortValues and b._sortValues[columnIndex] then
|
||||
valueB = b._sortValues[columnIndex]
|
||||
if bSortValues and bSortValues[columnIndex] then
|
||||
valueB = bSortValues[columnIndex]
|
||||
else
|
||||
valueB = b[columnIndex]
|
||||
valueB = bCells[columnIndex]
|
||||
end
|
||||
|
||||
if type(valueA) == "number" and type(valueB) == "number" then
|
||||
@@ -287,23 +381,55 @@ function Table:sortData(columnIndex, fn)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
self.set("items", items)
|
||||
return self
|
||||
end
|
||||
|
||||
--- Registers callback for row selection
|
||||
--- @shortDescription Registers a callback when a row is selected
|
||||
--- @param callback function The callback function(rowIndex, row)
|
||||
--- @return Table self The Table instance
|
||||
function Table:onRowSelect(callback)
|
||||
self:registerCallback("rowSelect", callback)
|
||||
return self
|
||||
end
|
||||
|
||||
--- @shortDescription Handles header clicks for sorting and row selection
|
||||
--- @param button number The button that was clicked
|
||||
--- @param x number The x position of the click
|
||||
--- @param y number The y position of the click
|
||||
--- @return boolean handled Whether the event was handled
|
||||
--- @protected
|
||||
function Table:mouse_click(button, x, y)
|
||||
if not VisualElement.mouse_click(self, button, x, y) then return false end
|
||||
if not Collection.mouse_click(self, button, x, y) then return false end
|
||||
|
||||
local relX, relY = self:getRelativePosition(x, y)
|
||||
local width = self.get("width")
|
||||
local height = self.get("height")
|
||||
local items = self.get("items")
|
||||
local showScrollBar = self.get("showScrollBar")
|
||||
local visibleRows = height - 1
|
||||
|
||||
if showScrollBar and #items > visibleRows and relX == width and relY > 1 then
|
||||
local scrollBarHeight = height - 1
|
||||
local maxOffset = #items - visibleRows
|
||||
local handleSize = math.max(1, math.floor((visibleRows / #items) * scrollBarHeight))
|
||||
|
||||
local currentPercent = maxOffset > 0 and (self.get("offset") / maxOffset * 100) or 0
|
||||
local handlePos = math.floor((currentPercent / 100) * (scrollBarHeight - handleSize)) + 1
|
||||
|
||||
local scrollBarRelY = relY - 1
|
||||
|
||||
if scrollBarRelY >= handlePos and scrollBarRelY < handlePos + handleSize then
|
||||
self._scrollBarDragging = true
|
||||
self._scrollBarDragOffset = scrollBarRelY - handlePos
|
||||
else
|
||||
local newPercent = ((scrollBarRelY - 1) / (scrollBarHeight - handleSize)) * 100
|
||||
local newOffset = math.floor((newPercent / 100) * maxOffset + 0.5)
|
||||
self.set("offset", math.max(0, math.min(maxOffset, newOffset)))
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
if relY == 1 then
|
||||
local columns = self.get("columns")
|
||||
local width = self.get("width")
|
||||
local calculatedColumns = self:calculateColumnWidths(columns, width)
|
||||
|
||||
local currentX = 1
|
||||
@@ -316,38 +442,93 @@ function Table:mouse_click(button, x, y)
|
||||
self.set("sortColumn", i)
|
||||
self.set("sortDirection", "asc")
|
||||
end
|
||||
self:sortData(i)
|
||||
break
|
||||
self:sortByColumn(i)
|
||||
self:updateRender()
|
||||
return true
|
||||
end
|
||||
currentX = currentX + colWidth
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
if relY > 1 then
|
||||
local rowIndex = relY - 2 + self.get("scrollOffset")
|
||||
if rowIndex >= 0 and rowIndex < #self.get("data") then
|
||||
self.set("selectedRow", rowIndex + 1)
|
||||
local rowIndex = relY - 2 + self.get("offset")
|
||||
|
||||
if rowIndex >= 0 and rowIndex < #items then
|
||||
local actualIndex = rowIndex + 1
|
||||
|
||||
for _, item in ipairs(items) do
|
||||
if item._data then
|
||||
item._data.selected = false
|
||||
else
|
||||
item.selected = false
|
||||
end
|
||||
end
|
||||
|
||||
if items[actualIndex] then
|
||||
if items[actualIndex]._data then
|
||||
items[actualIndex]._data.selected = true
|
||||
else
|
||||
items[actualIndex].selected = true
|
||||
end
|
||||
self:fireEvent("rowSelect", actualIndex, items[actualIndex])
|
||||
self:updateRender()
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
--- @shortDescription Handles mouse drag events for scrollbar
|
||||
--- @protected
|
||||
function Table:mouse_drag(button, x, y)
|
||||
if self._scrollBarDragging then
|
||||
local _, relY = self:getRelativePosition(x, y)
|
||||
local items = self.get("items")
|
||||
local height = self.get("height")
|
||||
local visibleRows = height - 1
|
||||
local scrollBarHeight = height - 1
|
||||
local handleSize = math.max(1, math.floor((visibleRows / #items) * scrollBarHeight))
|
||||
local maxOffset = #items - visibleRows
|
||||
|
||||
local scrollBarRelY = relY - 1
|
||||
scrollBarRelY = math.max(1, math.min(scrollBarHeight, scrollBarRelY))
|
||||
|
||||
local newPos = scrollBarRelY - (self._scrollBarDragOffset or 0)
|
||||
local newPercent = ((newPos - 1) / (scrollBarHeight - handleSize)) * 100
|
||||
local newOffset = math.floor((newPercent / 100) * maxOffset + 0.5)
|
||||
|
||||
self.set("offset", math.max(0, math.min(maxOffset, newOffset)))
|
||||
return true
|
||||
end
|
||||
return Collection.mouse_drag and Collection.mouse_drag(self, button, x, y) or false
|
||||
end
|
||||
|
||||
--- @shortDescription Handles mouse up events to stop scrollbar dragging
|
||||
--- @protected
|
||||
function Table:mouse_up(button, x, y)
|
||||
if self._scrollBarDragging then
|
||||
self._scrollBarDragging = false
|
||||
self._scrollBarDragOffset = nil
|
||||
return true
|
||||
end
|
||||
return Collection.mouse_up and Collection.mouse_up(self, button, x, y) or false
|
||||
end
|
||||
|
||||
--- @shortDescription Handles scrolling through the table data
|
||||
--- @param direction number The scroll direction (-1 up, 1 down)
|
||||
--- @param x number The x position of the scroll
|
||||
--- @param y number The y position of the scroll
|
||||
--- @return boolean handled Whether the event was handled
|
||||
--- @protected
|
||||
function Table:mouse_scroll(direction, x, y)
|
||||
if(VisualElement.mouse_scroll(self, direction, x, y))then
|
||||
local data = self.get("data")
|
||||
if Collection.mouse_scroll(self, direction, x, y) then
|
||||
local items = self.get("items")
|
||||
local height = self.get("height")
|
||||
local visibleRows = height - 2
|
||||
local maxScroll = math.max(0, #data - visibleRows - 1)
|
||||
local newOffset = math.min(maxScroll, math.max(0, self.get("scrollOffset") + direction))
|
||||
local visibleRows = height - 1 -- Subtract header
|
||||
local maxOffset = math.max(0, #items - visibleRows)
|
||||
local newOffset = math.min(maxOffset, math.max(0, self.get("offset") + direction))
|
||||
|
||||
self.set("scrollOffset", newOffset)
|
||||
self.set("offset", newOffset)
|
||||
self:updateRender()
|
||||
return true
|
||||
end
|
||||
return false
|
||||
@@ -356,21 +537,25 @@ end
|
||||
--- @shortDescription Renders the table with headers, data and scrollbar
|
||||
--- @protected
|
||||
function Table:render()
|
||||
VisualElement.render(self)
|
||||
local columns = self.get("columns")
|
||||
local data = self.get("data")
|
||||
local selected = self.get("selectedRow")
|
||||
local sortCol = self.get("sortColumn")
|
||||
local scrollOffset = self.get("scrollOffset")
|
||||
Collection.render(self)
|
||||
local columns = self.getResolved("columns")
|
||||
local items = self.getResolved("items")
|
||||
local sortCol = self.getResolved("sortColumn")
|
||||
local offset = self.getResolved("offset")
|
||||
local height = self.get("height")
|
||||
local width = self.get("width")
|
||||
local showScrollBar = self.getResolved("showScrollBar")
|
||||
local visibleRows = height - 1
|
||||
|
||||
local calculatedColumns = self:calculateColumnWidths(columns, width)
|
||||
local needsScrollBar = showScrollBar and #items > visibleRows
|
||||
local contentWidth = needsScrollBar and width - 1 or width
|
||||
|
||||
local calculatedColumns = self:calculateColumnWidths(columns, contentWidth)
|
||||
|
||||
local totalWidth = 0
|
||||
local lastVisibleColumn = #calculatedColumns
|
||||
for i, col in ipairs(calculatedColumns) do
|
||||
if totalWidth + col.visibleWidth > width then
|
||||
if totalWidth + col.visibleWidth > contentWidth then
|
||||
lastVisibleColumn = i - 1
|
||||
break
|
||||
end
|
||||
@@ -388,32 +573,68 @@ function Table:render()
|
||||
currentX = currentX + col.visibleWidth
|
||||
end
|
||||
|
||||
if currentX <= contentWidth then
|
||||
self:textBg(currentX, 1, string.rep(" ", contentWidth - currentX + 1), self.get("background"))
|
||||
end
|
||||
|
||||
for y = 2, height do
|
||||
local rowIndex = y - 2 + scrollOffset
|
||||
local rowData = data[rowIndex + 1]
|
||||
local rowIndex = y - 2 + offset
|
||||
local item = items[rowIndex + 1]
|
||||
|
||||
if rowData and (rowIndex + 1) <= #data then
|
||||
currentX = 1
|
||||
local bg = (rowIndex + 1) == selected and self.get("selectedColor") or self.get("background")
|
||||
if item then
|
||||
local cells = item._data and item._data.cells or item.cells
|
||||
local isSelected = item._data and item._data.selected or item.selected
|
||||
|
||||
for i, col in ipairs(calculatedColumns) do
|
||||
if i > lastVisibleColumn then break end
|
||||
local cellText = tostring(rowData[i] or "")
|
||||
local paddedText = cellText .. string.rep(" ", col.visibleWidth - #cellText)
|
||||
if i < lastVisibleColumn then
|
||||
paddedText = string.sub(paddedText, 1, col.visibleWidth - 1) .. " "
|
||||
if cells then
|
||||
currentX = 1
|
||||
local bg = isSelected and self.get("selectedBackground") or self.get("background")
|
||||
|
||||
for i, col in ipairs(calculatedColumns) do
|
||||
if i > lastVisibleColumn then break end
|
||||
local cellText = tostring(cells[i] or "")
|
||||
local paddedText = cellText .. string.rep(" ", col.visibleWidth - #cellText)
|
||||
if i < lastVisibleColumn then
|
||||
paddedText = string.sub(paddedText, 1, col.visibleWidth - 1) .. " "
|
||||
end
|
||||
local finalText = string.sub(paddedText, 1, col.visibleWidth)
|
||||
local finalForeground = string.rep(tHex[self.get("foreground")], col.visibleWidth)
|
||||
local finalBackground = string.rep(tHex[bg], col.visibleWidth)
|
||||
|
||||
self:blit(currentX, y, finalText, finalForeground, finalBackground)
|
||||
currentX = currentX + col.visibleWidth
|
||||
end
|
||||
local finalText = string.sub(paddedText, 1, col.visibleWidth)
|
||||
local finalForeground = string.rep(tHex[self.get("foreground")], col.visibleWidth)
|
||||
local finalBackground = string.rep(tHex[bg], col.visibleWidth)
|
||||
|
||||
self:blit(currentX, y, finalText, finalForeground, finalBackground)
|
||||
currentX = currentX + col.visibleWidth
|
||||
if currentX <= contentWidth then
|
||||
self:textBg(currentX, y, string.rep(" ", contentWidth - currentX + 1), bg)
|
||||
end
|
||||
end
|
||||
else
|
||||
self:blit(1, y, string.rep(" ", self.get("width")),
|
||||
string.rep(tHex[self.get("foreground")], self.get("width")),
|
||||
string.rep(tHex[self.get("background")], self.get("width")))
|
||||
self:blit(1, y, string.rep(" ", contentWidth),
|
||||
string.rep(tHex[self.get("foreground")], contentWidth),
|
||||
string.rep(tHex[self.get("background")], contentWidth))
|
||||
end
|
||||
end
|
||||
|
||||
if needsScrollBar then
|
||||
local scrollBarHeight = height - 1
|
||||
local handleSize = math.max(1, math.floor((visibleRows / #items) * scrollBarHeight))
|
||||
local maxOffset = #items - visibleRows
|
||||
|
||||
local currentPercent = maxOffset > 0 and (offset / maxOffset * 100) or 0
|
||||
local handlePos = math.floor((currentPercent / 100) * (scrollBarHeight - handleSize)) + 1
|
||||
|
||||
local scrollBarSymbol = self.getResolved("scrollBarSymbol")
|
||||
local scrollBarBg = self.getResolved("scrollBarBackground")
|
||||
local scrollBarColor = self.getResolved("scrollBarColor")
|
||||
local scrollBarBgColor = self.getResolved("scrollBarBackgroundColor")
|
||||
local foreground = self.getResolved("foreground")
|
||||
|
||||
for i = 2, height do
|
||||
self:blit(width, i, scrollBarBg, tHex[foreground], tHex[scrollBarBgColor])
|
||||
end
|
||||
|
||||
for i = handlePos, math.min(scrollBarHeight, handlePos + handleSize - 1) do
|
||||
self:blit(width, i + 1, scrollBarSymbol, tHex[scrollBarColor], tHex[scrollBarBgColor])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,6 +3,18 @@ local sub = string.sub
|
||||
local tHex = require("libraries/colorHex")
|
||||
---@cofnigDescription The tree element provides a hierarchical view of nodes that can be expanded and collapsed, with support for selection and scrolling.
|
||||
|
||||
local function flattenTree(nodes, expandedNodes, level, result)
|
||||
result = result or {}
|
||||
level = level or 0
|
||||
|
||||
for _, node in ipairs(nodes) do
|
||||
table.insert(result, {node = node, level = level})
|
||||
if expandedNodes[node] and node.children then
|
||||
flattenTree(node.children, expandedNodes, level + 1, result)
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
--- This is the tree class. It provides a hierarchical view of nodes that can be expanded and collapsed,
|
||||
--- with support for selection and scrolling.
|
||||
@@ -21,16 +33,47 @@ end})
|
||||
Tree.defineProperty(Tree, "selectedNode", {default = nil, type = "table", canTriggerRender = true})
|
||||
---@property expandedNodes table {} Table of nodes that are currently expanded
|
||||
Tree.defineProperty(Tree, "expandedNodes", {default = {}, type = "table", canTriggerRender = true})
|
||||
---@property scrollOffset number 0 Current vertical scroll position
|
||||
Tree.defineProperty(Tree, "scrollOffset", {default = 0, type = "number", canTriggerRender = true})
|
||||
---@property offset number 0 Current vertical scroll position
|
||||
Tree.defineProperty(Tree, "offset", {
|
||||
default = 0,
|
||||
type = "number",
|
||||
canTriggerRender = true,
|
||||
setter = function(self, value)
|
||||
return math.max(0, value)
|
||||
end
|
||||
})
|
||||
---@property horizontalOffset number 0 Current horizontal scroll position
|
||||
Tree.defineProperty(Tree, "horizontalOffset", {default = 0, type = "number", canTriggerRender = true})
|
||||
Tree.defineProperty(Tree, "horizontalOffset", {
|
||||
default = 0,
|
||||
type = "number",
|
||||
canTriggerRender = true,
|
||||
setter = function(self, value)
|
||||
return math.max(0, value)
|
||||
end
|
||||
})
|
||||
---@property selectedForegroundColor color white foreground color of selected node
|
||||
Tree.defineProperty(Tree, "selectedForegroundColor", {default = colors.white, type = "color"})
|
||||
---@property selectedBackgroundColor color lightBlue background color of selected node
|
||||
Tree.defineProperty(Tree, "selectedBackgroundColor", {default = colors.lightBlue, type = "color"})
|
||||
|
||||
---@property showScrollBar boolean true Whether to show the scrollbar when nodes exceed height
|
||||
Tree.defineProperty(Tree, "showScrollBar", {default = true, type = "boolean", canTriggerRender = true})
|
||||
|
||||
---@property scrollBarSymbol string " " Symbol used for the scrollbar handle
|
||||
Tree.defineProperty(Tree, "scrollBarSymbol", {default = " ", type = "string", canTriggerRender = true})
|
||||
|
||||
---@property scrollBarBackground string "\127" Symbol used for the scrollbar background
|
||||
Tree.defineProperty(Tree, "scrollBarBackground", {default = "\127", type = "string", canTriggerRender = true})
|
||||
|
||||
---@property scrollBarColor color lightGray Color of the scrollbar handle
|
||||
Tree.defineProperty(Tree, "scrollBarColor", {default = colors.lightGray, type = "color", canTriggerRender = true})
|
||||
|
||||
---@property scrollBarBackgroundColor color gray Background color of the scrollbar
|
||||
Tree.defineProperty(Tree, "scrollBarBackgroundColor", {default = colors.gray, type = "color", canTriggerRender = true})
|
||||
|
||||
Tree.defineEvent(Tree, "mouse_click")
|
||||
Tree.defineEvent(Tree, "mouse_drag")
|
||||
Tree.defineEvent(Tree, "mouse_up")
|
||||
Tree.defineEvent(Tree, "mouse_scroll")
|
||||
|
||||
--- Creates a new Tree instance
|
||||
@@ -91,19 +134,6 @@ function Tree:toggleNode(node)
|
||||
return self
|
||||
end
|
||||
|
||||
local function flattenTree(nodes, expandedNodes, level, result)
|
||||
result = result or {}
|
||||
level = level or 0
|
||||
|
||||
for _, node in ipairs(nodes) do
|
||||
table.insert(result, {node = node, level = level})
|
||||
if expandedNodes[node] and node.children then
|
||||
flattenTree(node.children, expandedNodes, level + 1, result)
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
--- Handles mouse click events
|
||||
--- @shortDescription Handles mouse click events for node selection and expansion
|
||||
--- @param button number The button that was clicked
|
||||
@@ -114,8 +144,54 @@ end
|
||||
function Tree:mouse_click(button, x, y)
|
||||
if VisualElement.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 flatNodes = flattenTree(self.get("nodes"), self.get("expandedNodes"))
|
||||
local visibleIndex = relY + self.get("scrollOffset")
|
||||
local showScrollBar = self.get("showScrollBar")
|
||||
local maxContentWidth, _ = self:getNodeSize()
|
||||
local needsHorizontalScrollBar = showScrollBar and maxContentWidth > width
|
||||
local contentHeight = needsHorizontalScrollBar and height - 1 or height
|
||||
local needsVerticalScrollBar = showScrollBar and #flatNodes > contentHeight
|
||||
|
||||
if needsVerticalScrollBar and relX == width and (not needsHorizontalScrollBar or relY < height) then
|
||||
local scrollHeight = needsHorizontalScrollBar and height - 1 or height
|
||||
local handleSize = math.max(1, math.floor((contentHeight / #flatNodes) * scrollHeight))
|
||||
local maxOffset = #flatNodes - contentHeight
|
||||
|
||||
local currentPercent = maxOffset > 0 and (self.get("offset") / 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("offset", math.max(0, math.min(maxOffset, newOffset)))
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
if needsHorizontalScrollBar and relY == height and (not needsVerticalScrollBar or relX < width) then
|
||||
local contentWidth = needsVerticalScrollBar and width - 1 or width
|
||||
local handleSize = math.max(1, math.floor((contentWidth / maxContentWidth) * contentWidth))
|
||||
local maxOffset = maxContentWidth - contentWidth
|
||||
|
||||
local currentPercent = maxOffset > 0 and (self.get("horizontalOffset") / maxOffset * 100) or 0
|
||||
local handlePos = math.floor((currentPercent / 100) * (contentWidth - handleSize)) + 1
|
||||
|
||||
if relX >= handlePos and relX < handlePos + handleSize then
|
||||
self._hScrollBarDragging = true
|
||||
self._hScrollBarDragOffset = relX - handlePos
|
||||
else
|
||||
local newPercent = ((relX - 1) / (contentWidth - handleSize)) * 100
|
||||
local newOffset = math.floor((newPercent / 100) * maxOffset + 0.5)
|
||||
self.set("horizontalOffset", math.max(0, math.min(maxOffset, newOffset)))
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local visibleIndex = relY + self.get("offset")
|
||||
|
||||
if flatNodes[visibleIndex] then
|
||||
local nodeInfo = flatNodes[visibleIndex]
|
||||
@@ -142,6 +218,82 @@ function Tree:onSelect(callback)
|
||||
return self
|
||||
end
|
||||
|
||||
--- @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 Tree:mouse_drag(button, x, y)
|
||||
if self._scrollBarDragging then
|
||||
local _, relY = self:getRelativePosition(x, y)
|
||||
local flatNodes = flattenTree(self.get("nodes"), self.get("expandedNodes"))
|
||||
local height = self.get("height")
|
||||
local maxContentWidth, _ = self:getNodeSize()
|
||||
local needsHorizontalScrollBar = self.get("showScrollBar") and maxContentWidth > self.get("width")
|
||||
local contentHeight = needsHorizontalScrollBar and height - 1 or height
|
||||
local scrollHeight = contentHeight
|
||||
local handleSize = math.max(1, math.floor((contentHeight / #flatNodes) * scrollHeight))
|
||||
local maxOffset = #flatNodes - contentHeight
|
||||
|
||||
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("offset", 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 maxContentWidth, _ = self:getNodeSize()
|
||||
local flatNodes = flattenTree(self.get("nodes"), self.get("expandedNodes"))
|
||||
local height = self.get("height")
|
||||
local needsHorizontalScrollBar = self.get("showScrollBar") and maxContentWidth > width
|
||||
local contentHeight = needsHorizontalScrollBar and height - 1 or height
|
||||
local needsVerticalScrollBar = self.get("showScrollBar") and #flatNodes > contentHeight
|
||||
local contentWidth = needsVerticalScrollBar and width - 1 or width
|
||||
local handleSize = math.max(1, math.floor((contentWidth / maxContentWidth) * contentWidth))
|
||||
local maxOffset = maxContentWidth - contentWidth
|
||||
|
||||
relX = math.max(1, math.min(contentWidth, relX))
|
||||
|
||||
local newPos = relX - (self._hScrollBarDragOffset or 0)
|
||||
local newPercent = ((newPos - 1) / (contentWidth - handleSize)) * 100
|
||||
local newOffset = math.floor((newPercent / 100) * maxOffset + 0.5)
|
||||
|
||||
self.set("horizontalOffset", math.max(0, math.min(maxOffset, newOffset)))
|
||||
return true
|
||||
end
|
||||
|
||||
return VisualElement.mouse_drag and VisualElement.mouse_drag(self, button, x, y) or false
|
||||
end
|
||||
|
||||
--- @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 Tree: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 VisualElement.mouse_up and VisualElement.mouse_up(self, button, x, y) or false
|
||||
end
|
||||
|
||||
--- @shortDescription Handles mouse scroll events for vertical scrolling
|
||||
--- @param direction number The scroll direction (1 for up, -1 for down)
|
||||
--- @param x number The x position of the scroll
|
||||
@@ -151,10 +303,16 @@ end
|
||||
function Tree:mouse_scroll(direction, x, y)
|
||||
if VisualElement.mouse_scroll(self, direction, x, y) then
|
||||
local flatNodes = flattenTree(self.get("nodes"), self.get("expandedNodes"))
|
||||
local maxScroll = math.max(0, #flatNodes - self.get("height"))
|
||||
local newScroll = math.min(maxScroll, math.max(0, self.get("scrollOffset") + direction))
|
||||
local height = self.get("height")
|
||||
local width = self.get("width")
|
||||
local showScrollBar = self.get("showScrollBar")
|
||||
local maxContentWidth, _ = self:getNodeSize()
|
||||
local needsHorizontalScrollBar = showScrollBar and maxContentWidth > width
|
||||
local contentHeight = needsHorizontalScrollBar and height - 1 or height
|
||||
local maxScroll = math.max(0, #flatNodes - contentHeight)
|
||||
local newScroll = math.min(maxScroll, math.max(0, self.get("offset") + direction))
|
||||
|
||||
self.set("scrollOffset", newScroll)
|
||||
self.set("offset", newScroll)
|
||||
return true
|
||||
end
|
||||
return false
|
||||
@@ -167,8 +325,20 @@ end
|
||||
function Tree:getNodeSize()
|
||||
local width, height = 0, 0
|
||||
local flatNodes = flattenTree(self.get("nodes"), self.get("expandedNodes"))
|
||||
local expandedNodes = self.get("expandedNodes")
|
||||
|
||||
for _, nodeInfo in ipairs(flatNodes) do
|
||||
width = math.max(width, nodeInfo.level + #nodeInfo.node.text)
|
||||
local node = nodeInfo.node
|
||||
local level = nodeInfo.level
|
||||
local indent = string.rep(" ", level)
|
||||
|
||||
local symbol = " "
|
||||
if node.children and #node.children > 0 then
|
||||
symbol = expandedNodes[node] and "\31" or "\16"
|
||||
end
|
||||
|
||||
local fullText = indent .. symbol .. " " .. (node.text or "Node")
|
||||
width = math.max(width, #fullText)
|
||||
end
|
||||
height = #flatNodes
|
||||
return width, height
|
||||
@@ -181,13 +351,20 @@ function Tree:render()
|
||||
|
||||
local flatNodes = flattenTree(self.get("nodes"), self.get("expandedNodes"))
|
||||
local height = self.get("height")
|
||||
local width = self.get("width")
|
||||
local selectedNode = self.get("selectedNode")
|
||||
local expandedNodes = self.get("expandedNodes")
|
||||
local scrollOffset = self.get("scrollOffset")
|
||||
local offset = self.get("offset")
|
||||
local horizontalOffset = self.get("horizontalOffset")
|
||||
local showScrollBar = self.get("showScrollBar")
|
||||
local maxContentWidth, _ = self:getNodeSize()
|
||||
local needsHorizontalScrollBar = showScrollBar and maxContentWidth > width
|
||||
local contentHeight = needsHorizontalScrollBar and height - 1 or height
|
||||
local needsVerticalScrollBar = showScrollBar and #flatNodes > contentHeight
|
||||
local contentWidth = needsVerticalScrollBar and width - 1 or width
|
||||
|
||||
for y = 1, height do
|
||||
local nodeInfo = flatNodes[y + scrollOffset]
|
||||
for y = 1, contentHeight do
|
||||
local nodeInfo = flatNodes[y + offset]
|
||||
if nodeInfo then
|
||||
local node = nodeInfo.node
|
||||
local level = nodeInfo.level
|
||||
@@ -203,17 +380,61 @@ function Tree:render()
|
||||
local _fg = isSelected and self.get("selectedForegroundColor") or (node.foreground or node.fg or self.get("foreground"))
|
||||
|
||||
local fullText = indent .. symbol .. " " .. (node.text or "Node")
|
||||
local text = sub(fullText, horizontalOffset + 1, horizontalOffset + self.get("width"))
|
||||
local paddedText = text .. string.rep(" ", self.get("width") - #text)
|
||||
local text = sub(fullText, horizontalOffset + 1, horizontalOffset + contentWidth)
|
||||
local paddedText = text .. string.rep(" ", contentWidth - #text)
|
||||
|
||||
local bg = tHex[_bg]:rep(#paddedText) or tHex[colors.black]:rep(#paddedText)
|
||||
local fg = tHex[_fg]:rep(#paddedText) or tHex[colors.white]:rep(#paddedText)
|
||||
|
||||
self:blit(1, y, paddedText, fg, bg)
|
||||
else
|
||||
self:blit(1, y, string.rep(" ", self.get("width")), tHex[self.get("foreground")]:rep(self.get("width")), tHex[self.get("background")]:rep(self.get("width")))
|
||||
self:blit(1, y, string.rep(" ", contentWidth), tHex[self.get("foreground")]:rep(contentWidth), tHex[self.get("background")]:rep(contentWidth))
|
||||
end
|
||||
end
|
||||
|
||||
local scrollBarSymbol = self.getResolved("scrollBarSymbol")
|
||||
local scrollBarBg = self.getResolved("scrollBarBackground")
|
||||
local scrollBarColor = self.getResolved("scrollBarColor")
|
||||
local scrollBarBgColor = self.getResolved("scrollBarBackgroundColor")
|
||||
local foreground = self.getResolved("foreground")
|
||||
|
||||
if needsVerticalScrollBar then
|
||||
local scrollHeight = needsHorizontalScrollBar and height - 1 or height
|
||||
local handleSize = math.max(1, math.floor((contentHeight / #flatNodes) * scrollHeight))
|
||||
local maxOffset = #flatNodes - contentHeight
|
||||
|
||||
local currentPercent = maxOffset > 0 and (offset / maxOffset * 100) or 0
|
||||
local handlePos = math.floor((currentPercent / 100) * (scrollHeight - handleSize)) + 1
|
||||
|
||||
for i = 1, scrollHeight do
|
||||
self:blit(width, i, scrollBarBg, tHex[foreground], tHex[scrollBarBgColor])
|
||||
end
|
||||
|
||||
for i = handlePos, math.min(scrollHeight, handlePos + handleSize - 1) do
|
||||
self:blit(width, i, scrollBarSymbol, tHex[scrollBarColor], tHex[scrollBarBgColor])
|
||||
end
|
||||
end
|
||||
|
||||
if needsHorizontalScrollBar then
|
||||
local scrollWidth = needsVerticalScrollBar and width - 1 or width
|
||||
local handleSize = math.max(1, math.floor((scrollWidth / maxContentWidth) * scrollWidth))
|
||||
local maxOffset = maxContentWidth - contentWidth
|
||||
|
||||
local currentPercent = maxOffset > 0 and (horizontalOffset / maxOffset * 100) or 0
|
||||
local handlePos = math.floor((currentPercent / 100) * (scrollWidth - handleSize)) + 1
|
||||
|
||||
for i = 1, scrollWidth do
|
||||
self:blit(i, height, scrollBarBg, tHex[foreground], tHex[scrollBarBgColor])
|
||||
end
|
||||
|
||||
for i = handlePos, math.min(scrollWidth, handlePos + handleSize - 1) do
|
||||
self:blit(i, height, scrollBarSymbol, tHex[scrollBarColor], tHex[scrollBarBgColor])
|
||||
end
|
||||
end
|
||||
|
||||
if needsVerticalScrollBar and needsHorizontalScrollBar then
|
||||
self:blit(width, height, " ", tHex[foreground], tHex[self.get("background")])
|
||||
end
|
||||
end
|
||||
|
||||
return Tree
|
||||
@@ -889,7 +889,7 @@ function VisualElement:isFocused()
|
||||
return self:hasState("focused")
|
||||
end
|
||||
|
||||
--- Adds or updates a drawable character border around the element using the canvas plugin. The border will automatically adapt to size/background changes because the command reads current properties each render.
|
||||
--- Adds or updates a drawable character border around the element. The border will automatically adapt to size/background changes because the command reads current properties each render.
|
||||
--- @param colorOrOptions any Border color or options table
|
||||
--- @param sideOptions? table Side options table (if color is provided as first argument)
|
||||
--- @return VisualElement self
|
||||
|
||||
@@ -23,6 +23,8 @@ local eventParser = require("parsers.eventParser")
|
||||
|
||||
local globalParser = require("parsers.globalParser")
|
||||
|
||||
local helper = require("utils.helper")
|
||||
|
||||
local markdownGenerator = require("utils.markdownGenerator")
|
||||
|
||||
BasaltDoc.annotationHandlers = {}
|
||||
@@ -145,6 +147,31 @@ BasaltDoc.registerAnnotation("@globalDescription", function(target, args)
|
||||
end
|
||||
end)
|
||||
|
||||
BasaltDoc.registerAnnotation("@tableType", function(target, args)
|
||||
if not target.tableTypes then target.tableTypes = {} end
|
||||
local tableName = args:match("^%s*(%S+)")
|
||||
if tableName then
|
||||
target._currentTableType = {
|
||||
name = tableName,
|
||||
fields = {}
|
||||
}
|
||||
table.insert(target.tableTypes, target._currentTableType)
|
||||
end
|
||||
end)
|
||||
|
||||
BasaltDoc.registerAnnotation("@tableField", function(target, args)
|
||||
if target._currentTableType then
|
||||
local fieldName, fieldType, fieldDesc = args:match("^%s*([%w_]+)%s+([%w_|]+)%s+(.*)")
|
||||
if fieldName and fieldType then
|
||||
table.insert(target._currentTableType.fields, {
|
||||
name = fieldName,
|
||||
type = fieldType,
|
||||
description = fieldDesc or ""
|
||||
})
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
if classParser then classParser.setHandlers(BasaltDoc.annotationHandlers) end
|
||||
if functionParser then functionParser.setHandlers(BasaltDoc.annotationHandlers) end
|
||||
if propertyParser then propertyParser.setHandlers(BasaltDoc.annotationHandlers) end
|
||||
@@ -192,12 +219,14 @@ function BasaltDoc.parse(content)
|
||||
local annotationBuffer = {}
|
||||
local currentClass = nil
|
||||
local firstTag = nil
|
||||
local pendingTableTypes = {}
|
||||
|
||||
local blockStartTags = {
|
||||
["@class"] = true,
|
||||
["@property"] = true,
|
||||
["@event"] = true,
|
||||
["@skip"] = true
|
||||
["@skip"] = true,
|
||||
["@tableType"] = true
|
||||
}
|
||||
|
||||
local i = 1
|
||||
@@ -225,9 +254,25 @@ function BasaltDoc.parse(content)
|
||||
if firstTag == "@class" and classParser then
|
||||
local class = classParser.parse(annotationBuffer, table.concat(annotationBuffer, "\n"))
|
||||
if class and not class.skip then
|
||||
if #pendingTableTypes > 0 then
|
||||
for _, tableType in ipairs(pendingTableTypes) do
|
||||
table.insert(class.tableTypes, tableType)
|
||||
end
|
||||
pendingTableTypes = {}
|
||||
end
|
||||
table.insert(ast.classes, class)
|
||||
currentClass = class
|
||||
end
|
||||
elseif firstTag == "@tableType" then
|
||||
local tempTarget = {tableTypes = {}}
|
||||
if classParser and classParser.handlers then
|
||||
helper.applyAnnotations(annotationBuffer, tempTarget, classParser.handlers)
|
||||
end
|
||||
if tempTarget.tableTypes and #tempTarget.tableTypes > 0 then
|
||||
for _, tt in ipairs(tempTarget.tableTypes) do
|
||||
table.insert(pendingTableTypes, tt)
|
||||
end
|
||||
end
|
||||
elseif firstTag == "@property" and currentClass and propertyParser then
|
||||
local prop = propertyParser.parse(annotationBuffer, table.concat(annotationBuffer, "\n"))
|
||||
if prop then
|
||||
|
||||
@@ -17,6 +17,7 @@ function classParser.parse(annotations, line)
|
||||
properties = {},
|
||||
events = {},
|
||||
functions = {},
|
||||
tableTypes = {},
|
||||
skip = false
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,36 @@ function helper.applyAnnotations(annotations, target, handlers)
|
||||
|
||||
local tag, args = ann:match("^%-%-%-?%s*(@%S+)%s*(.*)")
|
||||
if tag then
|
||||
if args == ">" then
|
||||
if args and args:match("^%s*%[%[") then
|
||||
local blockContent = args:gsub("^%s*%[%[%s*", "")
|
||||
|
||||
if blockContent:match("%]%]%s*$") then
|
||||
args = blockContent:gsub("%]%]%s*$", "")
|
||||
else
|
||||
local multiArgs = {}
|
||||
if blockContent ~= "" then
|
||||
table.insert(multiArgs, blockContent)
|
||||
end
|
||||
i = i + 1
|
||||
|
||||
while i <= #annotations do
|
||||
local nextAnn = annotations[i]
|
||||
local content = nextAnn:match("^%-%-%-?%s*(.*)") or nextAnn
|
||||
|
||||
if content:match("%]%]%s*$") then
|
||||
local finalContent = content:gsub("%]%]%s*$", "")
|
||||
if finalContent ~= "" then
|
||||
table.insert(multiArgs, finalContent)
|
||||
end
|
||||
break
|
||||
else
|
||||
table.insert(multiArgs, content)
|
||||
end
|
||||
i = i + 1
|
||||
end
|
||||
args = table.concat(multiArgs, "\n")
|
||||
end
|
||||
elseif args == ">" then
|
||||
local multiArgs = ""
|
||||
i = i + 1
|
||||
|
||||
|
||||
@@ -72,16 +72,21 @@ local function generateFunctionMarkdown(class, functions)
|
||||
|
||||
if f.usage then
|
||||
table.insert(md, "### Usage")
|
||||
table.insert(md, "```lua")
|
||||
for _, usage in ipairs(f.usage) do
|
||||
if usage == "" then
|
||||
table.insert(md, "")
|
||||
else
|
||||
table.insert(md, usage)
|
||||
for _, usageBlock in ipairs(f.usage) do
|
||||
table.insert(md, "```lua run")
|
||||
-- Check if usageBlock is already multi-line
|
||||
if type(usageBlock) == "string" then
|
||||
if usageBlock:match("\n") then
|
||||
-- Multi-line block
|
||||
table.insert(md, usageBlock)
|
||||
else
|
||||
-- Single line
|
||||
table.insert(md, usageBlock)
|
||||
end
|
||||
end
|
||||
table.insert(md, "```")
|
||||
table.insert(md, "")
|
||||
end
|
||||
table.insert(md, "```")
|
||||
table.insert(md, "")
|
||||
end
|
||||
|
||||
if f.run then
|
||||
@@ -157,6 +162,38 @@ function markdownGenerator.generate(ast)
|
||||
end
|
||||
table.insert(md, "")
|
||||
|
||||
if class.usage then
|
||||
table.insert(md, "## Usage")
|
||||
for _, usageBlock in ipairs(class.usage) do
|
||||
table.insert(md, "```lua run")
|
||||
if type(usageBlock) == "string" then
|
||||
table.insert(md, usageBlock)
|
||||
end
|
||||
table.insert(md, "```")
|
||||
table.insert(md, "")
|
||||
end
|
||||
end
|
||||
|
||||
if #class.tableTypes > 0 then
|
||||
table.insert(md, "## Table Types")
|
||||
table.insert(md, "")
|
||||
for _, tableType in ipairs(class.tableTypes) do
|
||||
table.insert(md, "### " .. tableType.name)
|
||||
table.insert(md, "")
|
||||
if #tableType.fields > 0 then
|
||||
table.insert(md, "|Property|Type|Description|")
|
||||
table.insert(md, "|---|---|---|")
|
||||
for _, field in ipairs(tableType.fields) do
|
||||
table.insert(md, string.format("|%s|%s|%s|",
|
||||
field.name or "",
|
||||
field.type or "any",
|
||||
field.description or ""))
|
||||
end
|
||||
table.insert(md, "")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not class.skipPropertyList and #class.properties > 0 then
|
||||
table.insert(md, "## Properties")
|
||||
table.insert(md, "")
|
||||
|
||||
Reference in New Issue
Block a user