- 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:
Robert Jelic
2025-10-29 17:55:29 +01:00
parent 41bd5bdf04
commit 6f14eadf0a
12 changed files with 1264 additions and 265 deletions

1
.gitignore vendored
View File

@@ -18,3 +18,4 @@ Breadcrumb.lua
Dialog.lua Dialog.lua
DockLayout.lua DockLayout.lua
ContextMenu.lua ContextMenu.lua
Toast.lua

View File

@@ -5,51 +5,50 @@ local tHex = require("libraries/colorHex")
---@configDescription A DropDown menu that shows a list of selectable items ---@configDescription A DropDown menu that shows a list of selectable items
---@configDefault false ---@configDefault false
--- Item Properties: ---@tableType ItemTable
--- Property|Type|Description ---@tableField text string The display text for the item
--- -------|------|------------- ---@tableField callback function Function called when selected
--- text|string|The display text for the item ---@tableField fg color Normal text color
--- separator|boolean|Makes item a divider line ---@tableField bg color Normal background color
--- callback|function|Function called when selected ---@tableField selectedFg color Text color when selected
--- foreground|color|Normal text color ---@tableField selectedBg color Background when selected
--- background|color|Normal background color
--- selectedForeground|color|Text color when selected
--- selectedBackground|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. --- 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 [[
--- @usage local dropdown = main:addDropDown() --- -- Create a styled dropdown menu
--- @usage :setPosition(5, 5) --- local dropdown = main:addDropDown()
--- @usage :setSize(20, 1) -- Height expands when opened --- :setPosition(5, 5)
--- @usage :setSelectedText("Select an option...") --- :setSize(20, 1) -- Height expands when opened
--- @usage --- :setSelectedText("Select an option...")
--- @usage -- Add items with different styles and callbacks ---
--- @usage dropdown:setItems({ --- -- Add items with different styles and callbacks
--- @usage { --- dropdown:setItems({
--- @usage text = "Category A", --- {
--- @usage background = colors.blue, --- text = "Category A",
--- @usage foreground = colors.white --- background = colors.blue,
--- @usage }, --- foreground = colors.white
--- @usage { separator = true, text = "-" }, -- Add a separator --- },
--- @usage { --- { separator = true, text = "-" }, -- Add a separator
--- @usage text = "Option 1", --- {
--- @usage callback = function(self) --- text = "Option 1",
--- @usage -- Handle selection --- callback = function(self)
--- @usage basalt.debug("Selected Option 1") --- -- Handle selection
--- @usage end --- basalt.debug("Selected Option 1")
--- @usage }, --- end
--- @usage { --- },
--- @usage text = "Option 2", --- {
--- @usage -- Custom colors when selected --- text = "Option 2",
--- @usage selectedBackground = colors.green, --- -- Custom colors when selected
--- @usage selectedForeground = colors.white --- selectedBackground = colors.green,
--- @usage } --- selectedForeground = colors.white
--- @usage }) --- }
--- @usage --- })
--- @usage -- Listen for selections ---
--- @usage dropdown:onChange(function(self, value) --- -- Listen for selections
--- @usage basalt.debug("Selected:", value) --- dropdown:onChange(function(self, value)
--- @usage end) --- basalt.debug("Selected:", value)
--- end)
--- ]]
---@class DropDown : List ---@class DropDown : List
local DropDown = setmetatable({}, List) local DropDown = setmetatable({}, List)
DropDown.__index = DropDown DropDown.__index = DropDown
@@ -107,7 +106,6 @@ function DropDown:mouse_click(button, x, y)
end end
return true return true
elseif isOpen and relY > 1 then elseif isOpen and relY > 1 then
-- Forward to List handler for scrollbar handling
return List.mouse_click(self, button, x, y - 1) return List.mouse_click(self, button, x, y - 1)
end end
return false return false
@@ -136,7 +134,6 @@ function DropDown:mouse_up(button, x, y)
if self:hasState("opened") then if self:hasState("opened") then
local relX, relY = self:getRelativePosition(x, y) 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 if relY > 1 and self.get("selectable") and not self._scrollBarDragging then
local itemIndex = (relY - 1) + self.get("offset") local itemIndex = (relY - 1) + self.get("offset")
local items = self.get("items") local items = self.get("items")
@@ -172,7 +169,6 @@ function DropDown:mouse_up(button, x, y)
end end
end end
-- Always forward to List for cleanup and unset clicked state
List.mouse_up(self, button, x, y - 1) List.mouse_up(self, button, x, y - 1)
self:unsetState("clicked") self:unsetState("clicked")
return true return true

View File

@@ -44,6 +44,14 @@ List.defineEvent(List, "mouse_drag")
List.defineEvent(List, "mouse_scroll") List.defineEvent(List, "mouse_scroll")
List.defineEvent(List, "key") 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 = { local entrySchema = {
text = { type = "string", default = "Entry" }, text = { type = "string", default = "Entry" },
bg = { type = "number", default = nil }, bg = { type = "number", default = nil },

View File

@@ -3,15 +3,39 @@ local List = require("elements/List")
local tHex = require("libraries/colorHex") local tHex = require("libraries/colorHex")
---@configDescription A horizontal menu bar with selectable items. ---@configDescription A horizontal menu bar with selectable items.
--- This is the menu class. It provides 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.
--- Menu items are displayed in a single row and can have custom colors and callbacks.
---@class Menu : List ---@class Menu : List
local Menu = setmetatable({}, List) local Menu = setmetatable({}, List)
Menu.__index = Menu 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 ---@property separatorColor color gray The color used for separator items in the menu
Menu.defineProperty(Menu, "separatorColor", {default = colors.gray, type = "color"}) 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 --- Creates a new Menu instance
--- @shortDescription Creates a new Menu instance --- @shortDescription Creates a new Menu instance
--- @return Menu self The newly created Menu instance --- @return Menu self The newly created Menu instance
@@ -33,83 +57,151 @@ end
function Menu:init(props, basalt) function Menu:init(props, basalt)
List.init(self, props, basalt) List.init(self, props, basalt)
self.set("type", "Menu") 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 return self
end end
--- Sets the menu items --- Calculates the total width of all menu items with spacing
--- @shortDescription Sets the menu items and calculates total width --- @shortDescription Calculates total width of menu items
--- @param items table[] List of items with {text, separator, callback, foreground, background} properties --- @return number totalWidth The total width of all items
--- @return Menu self The Menu instance function Menu:getTotalWidth()
--- @usage menu:setItems({{text="File"}, {separator=true}, {text="Edit"}}) local items = self.get("items")
function Menu:setItems(items) local spacing = self.get("spacing")
local listItems = {}
local totalWidth = 0 local totalWidth = 0
for _, item in ipairs(items) do
if item.separator then for i, item in ipairs(items) do
table.insert(listItems, {text = item.text or "|", selectable = false}) if type(item) == "table" then
totalWidth = totalWidth + 1 totalWidth = totalWidth + #item.text
else else
local text = " " .. item.text .. " " totalWidth = totalWidth + #tostring(item) + 2
item.text = text end
table.insert(listItems, item)
totalWidth = totalWidth + #text if i < #items then
totalWidth = totalWidth + spacing
end end
end end
self.set("width", totalWidth)
return List.setItems(self, listItems) return totalWidth
end end
--- @shortDescription Renders the menu horizontally with proper spacing and colors --- @shortDescription Renders the menu horizontally with proper spacing and colors
--- @protected --- @protected
function Menu:render() function Menu:render()
VisualElement.render(self) 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 local currentX = 1
for i, item in ipairs(self.get("items")) do for i, item in ipairs(items) do
if type(item) == "string" then if type(item) == "string" then
item = {text = " "..item.." "} item = {text = " "..item.." "}
self.get("items")[i] = item items[i] = item
end end
local isSelected = item.selected itemPositions[i] = {
local fg = item.selectable == false and self.get("separatorColor") or startX = currentX,
(isSelected and (item.selectedForeground or self.get("selectedForeground")) or endX = currentX + #item.text - 1,
(item.foreground or self.get("foreground"))) text = item.text,
item = item
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))
currentX = currentX + #item.text 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
end end
--- @shortDescription Handles mouse click events and item selection --- @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 --- @protected
function Menu:mouse_click(button, x, y) function Menu:mouse_click(button, x, y)
if not VisualElement.mouse_click(self, button, x, y) then return false end if not VisualElement.mouse_click(self, button, x, y) then return false end
if(self.get("selectable") == false) then return false end if(self.get("selectable") == false) then return false end
local relX = select(1, self:getRelativePosition(x, y)) 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 local currentX = 1
for i, item in ipairs(self.get("items")) do for i, item in ipairs(items) do
if relX >= currentX and relX < currentX + #item.text then local itemWidth = #item.text
if virtualX >= currentX and virtualX < currentX + itemWidth then
if item.selectable ~= false then if item.selectable ~= false then
if type(item) == "string" then if type(item) == "string" then
item = {text = item} item = {text = item}
self.get("items")[i] = item items[i] = item
end end
if not self.get("multiSelection") then 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 if type(otherItem) == "table" then
otherItem.selected = false otherItem.selected = false
end end
@@ -125,7 +217,25 @@ function Menu:mouse_click(button, x, y)
end end
return true return true
end 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 end
return false return false
end end

View 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

View File

@@ -1,13 +1,12 @@
local VisualElement = require("elements/VisualElement") local Collection = require("elements/Collection")
local tHex = require("libraries/colorHex") local tHex = require("libraries/colorHex")
--- This is the table class. It provides a sortable data grid with customizable columns, --- 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.
--- row selection, and scrolling capabilities.
--- @usage local people = container:addTable():setWidth(40) --- @usage local people = container:addTable():setWidth(40)
--- @usage people:setColumns({{name="Name",width=12}, {name="Age",width=10}, {name="Country",width=15}}) --- @usage people:setColumns({{name="Name",width=12}, {name="Age",width=10}, {name="Country",width=15}})
--- @usage people:setData({{"Alice", 30, "USA"}, {"Bob", 25, "UK"}}) --- @usage people:addRow("Alice", 30, "USA"):addRow("Bob", 25, "UK")
---@class Table : VisualElement ---@class Table : Collection
local Table = setmetatable({}, VisualElement) local Table = setmetatable({}, Collection)
Table.__index = Table Table.__index = Table
---@property columns table {} List of column definitions with {name, width} properties ---@property columns table {} List of column definitions with {name, width} properties
@@ -27,34 +26,55 @@ Table.defineProperty(Table, "columns", {default = {}, type = "table", canTrigger
end end
return t return t
end}) 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 ---@property headerColor color blue Color of the column headers
Table.defineProperty(Table, "headerColor", {default = colors.blue, type = "color"}) 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 ---@property gridColor color gray Color of grid lines
Table.defineProperty(Table, "gridColor", {default = colors.gray, type = "color"}) Table.defineProperty(Table, "gridColor", {default = colors.gray, type = "color"})
---@property sortColumn number? nil Currently sorted column index ---@property sortColumn number? nil Currently sorted column index
Table.defineProperty(Table, "sortColumn", {default = nil, type = "number", canTriggerRender = true}) Table.defineProperty(Table, "sortColumn", {default = nil, type = "number", canTriggerRender = true})
---@property sortDirection string "asc" Sort direction ("asc" or "desc") ---@property sortDirection string "asc" Sort direction ("asc" or "desc")
Table.defineProperty(Table, "sortDirection", {default = "asc", type = "string", canTriggerRender = true}) 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 ---@property customSortFunction table {} Custom sort functions for columns
Table.defineProperty(Table, "customSortFunction", {default = {}, type = "table"}) 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_click")
Table.defineEvent(Table, "mouse_drag")
Table.defineEvent(Table, "mouse_up")
Table.defineEvent(Table, "mouse_scroll") 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 --- Creates a new Table instance
--- @shortDescription Creates a new Table instance --- @shortDescription Creates a new Table instance
--- @return Table self The newly created Table instance --- @return Table self The newly created Table instance
@@ -74,15 +94,97 @@ end
--- @return Table self The initialized instance --- @return Table self The initialized instance
--- @protected --- @protected
function Table:init(props, basalt) function Table:init(props, basalt)
VisualElement.init(self, props, basalt) Collection.init(self, props, basalt)
self._entrySchema = entrySchema
self.set("type", "Table") 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 return self
end end
--- Adds a new column to the table --- Adds a new column to the table
--- @shortDescription Adds a new column to the table --- @shortDescription Adds a new column to the table
--- @param name string The name of the column --- @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 --- @return Table self The Table instance
function Table:addColumn(name, width) function Table:addColumn(name, width)
local columns = self.get("columns") local columns = self.get("columns")
@@ -91,17 +193,6 @@ function Table:addColumn(name, width)
return self return self
end 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 --- Sets a custom sort function for a specific column
--- @shortDescription Sets a custom sort function for a column --- @shortDescription Sets a custom sort function for a column
--- @param columnIndex number The index of the column --- @param columnIndex number The index of the column
@@ -114,56 +205,54 @@ function Table:setColumnSortFunction(columnIndex, sortFn)
return self return self
end end
--- Adds data with both display and sort values --- Set data with automatic formatting
--- @shortDescription Adds formatted data with raw sort values --- @shortDescription Sets table data with optional column formatters
--- @param displayData table The formatted data for display --- @param rawData table The raw data array (array of row arrays)
--- @param sortData table The raw data for sorting (optional) --- @param formatters table? Optional formatter functions for columns {[2] = function(value) return value end}
--- @return Table self The Table instance --- @return Table self The Table instance
function Table:setFormattedData(displayData, sortData) --- @usage table:setData({{...}}, {[1] = tostring, [2] = function(age) return age.."y" end})
local enrichedData = {} function Table:setData(rawData, formatters)
self:clearData()
for i, row in ipairs(displayData) do for _, row in ipairs(rawData) do
local enrichedRow = {} local cells = {}
for j, cell in ipairs(row) do local sortValues = {}
enrichedRow[j] = cell
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 end
if sortData and sortData[i] then Collection.addItem(self, {
enrichedRow._sortValues = sortData[i] cells = cells,
end _sortValues = sortValues,
text = table.concat(cells, " ")
table.insert(enrichedData, enrichedRow) })
end end
self.set("data", enrichedData)
return self return self
end end
--- Set data with automatic formatting --- Gets all table data
--- @shortDescription Sets table data with optional column formatters --- @shortDescription Gets all rows as array of cell arrays
--- @param rawData table The raw data array --- @return table data Array of row cell arrays
--- @param formatters table Optional formatter functions for columns {[2] = function(value) return value end} function Table:getData()
--- @return Table self The Table instance local items = self.get("items")
function Table:setData(rawData, formatters) local data = {}
if not formatters then
self.set("data", rawData)
return self
end
local formattedData = {} for _, item in ipairs(items) do
for i, row in ipairs(rawData) do local cells = item._data and item._data.cells or item.cells
local formattedRow = {} if cells then
for j, cell in ipairs(row) do table.insert(data, cells)
if formatters[j] then
formattedRow[j] = formatters[j](cell)
else
formattedRow[j] = cell
end
end end
table.insert(formattedData, formattedRow)
end end
return self:setFormattedData(formattedData, rawData) return data
end end
--- @shortDescription Calculates column widths for rendering --- @shortDescription Calculates column widths for rendering
@@ -241,33 +330,38 @@ end
--- @param columnIndex number The index of the column to sort by --- @param columnIndex number The index of the column to sort by
--- @param fn function? Optional custom sorting function --- @param fn function? Optional custom sorting function
--- @return Table self The Table instance --- @return Table self The Table instance
function Table:sortData(columnIndex, fn) function Table:sortByColumn(columnIndex, fn)
local data = self.get("data") local items = self.get("items")
local direction = self.get("sortDirection") local direction = self.get("sortDirection")
local customSorts = self.get("customSortFunction") local customSorts = self.get("customSortFunction")
local sortFn = fn or customSorts[columnIndex] local sortFn = fn or customSorts[columnIndex]
if sortFn then if sortFn then
table.sort(data, function(a, b) table.sort(items, function(a, b)
return sortFn(a, b, direction) return sortFn(a, b, direction)
end) end)
else else
table.sort(data, function(a, b) table.sort(items, function(a, b)
if not a or not b then return false end 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 local valueA, valueB
if a._sortValues and a._sortValues[columnIndex] then if aSortValues and aSortValues[columnIndex] then
valueA = a._sortValues[columnIndex] valueA = aSortValues[columnIndex]
else else
valueA = a[columnIndex] valueA = aCells[columnIndex]
end end
if b._sortValues and b._sortValues[columnIndex] then if bSortValues and bSortValues[columnIndex] then
valueB = b._sortValues[columnIndex] valueB = bSortValues[columnIndex]
else else
valueB = b[columnIndex] valueB = bCells[columnIndex]
end end
if type(valueA) == "number" and type(valueB) == "number" then if type(valueA) == "number" and type(valueB) == "number" then
@@ -287,23 +381,55 @@ function Table:sortData(columnIndex, fn)
end end
end) 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 return self
end end
--- @shortDescription Handles header clicks for sorting and row selection --- @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 --- @protected
function Table:mouse_click(button, x, y) 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 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 if relY == 1 then
local columns = self.get("columns") local columns = self.get("columns")
local width = self.get("width")
local calculatedColumns = self:calculateColumnWidths(columns, width) local calculatedColumns = self:calculateColumnWidths(columns, width)
local currentX = 1 local currentX = 1
@@ -316,38 +442,93 @@ function Table:mouse_click(button, x, y)
self.set("sortColumn", i) self.set("sortColumn", i)
self.set("sortDirection", "asc") self.set("sortDirection", "asc")
end end
self:sortData(i) self:sortByColumn(i)
break self:updateRender()
return true
end end
currentX = currentX + colWidth currentX = currentX + colWidth
end end
return true
end end
if relY > 1 then if relY > 1 then
local rowIndex = relY - 2 + self.get("scrollOffset") local rowIndex = relY - 2 + self.get("offset")
if rowIndex >= 0 and rowIndex < #self.get("data") then
self.set("selectedRow", rowIndex + 1) 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 end
return true
end end
return true return true
end 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 --- @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 --- @protected
function Table:mouse_scroll(direction, x, y) function Table:mouse_scroll(direction, x, y)
if(VisualElement.mouse_scroll(self, direction, x, y))then if Collection.mouse_scroll(self, direction, x, y) then
local data = self.get("data") local items = self.get("items")
local height = self.get("height") local height = self.get("height")
local visibleRows = height - 2 local visibleRows = height - 1 -- Subtract header
local maxScroll = math.max(0, #data - visibleRows - 1) local maxOffset = math.max(0, #items - visibleRows)
local newOffset = math.min(maxScroll, math.max(0, self.get("scrollOffset") + direction)) local newOffset = math.min(maxOffset, math.max(0, self.get("offset") + direction))
self.set("scrollOffset", newOffset) self.set("offset", newOffset)
self:updateRender()
return true return true
end end
return false return false
@@ -356,21 +537,25 @@ end
--- @shortDescription Renders the table with headers, data and scrollbar --- @shortDescription Renders the table with headers, data and scrollbar
--- @protected --- @protected
function Table:render() function Table:render()
VisualElement.render(self) Collection.render(self)
local columns = self.get("columns") local columns = self.getResolved("columns")
local data = self.get("data") local items = self.getResolved("items")
local selected = self.get("selectedRow") local sortCol = self.getResolved("sortColumn")
local sortCol = self.get("sortColumn") local offset = self.getResolved("offset")
local scrollOffset = self.get("scrollOffset")
local height = self.get("height") local height = self.get("height")
local width = self.get("width") 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 totalWidth = 0
local lastVisibleColumn = #calculatedColumns local lastVisibleColumn = #calculatedColumns
for i, col in ipairs(calculatedColumns) do for i, col in ipairs(calculatedColumns) do
if totalWidth + col.visibleWidth > width then if totalWidth + col.visibleWidth > contentWidth then
lastVisibleColumn = i - 1 lastVisibleColumn = i - 1
break break
end end
@@ -388,32 +573,68 @@ function Table:render()
currentX = currentX + col.visibleWidth currentX = currentX + col.visibleWidth
end end
if currentX <= contentWidth then
self:textBg(currentX, 1, string.rep(" ", contentWidth - currentX + 1), self.get("background"))
end
for y = 2, height do for y = 2, height do
local rowIndex = y - 2 + scrollOffset local rowIndex = y - 2 + offset
local rowData = data[rowIndex + 1] local item = items[rowIndex + 1]
if rowData and (rowIndex + 1) <= #data then if item then
currentX = 1 local cells = item._data and item._data.cells or item.cells
local bg = (rowIndex + 1) == selected and self.get("selectedColor") or self.get("background") local isSelected = item._data and item._data.selected or item.selected
for i, col in ipairs(calculatedColumns) do if cells then
if i > lastVisibleColumn then break end currentX = 1
local cellText = tostring(rowData[i] or "") local bg = isSelected and self.get("selectedBackground") or self.get("background")
local paddedText = cellText .. string.rep(" ", col.visibleWidth - #cellText)
if i < lastVisibleColumn then for i, col in ipairs(calculatedColumns) do
paddedText = string.sub(paddedText, 1, col.visibleWidth - 1) .. " " 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 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) if currentX <= contentWidth then
currentX = currentX + col.visibleWidth self:textBg(currentX, y, string.rep(" ", contentWidth - currentX + 1), bg)
end
end end
else else
self:blit(1, y, string.rep(" ", self.get("width")), self:blit(1, y, string.rep(" ", contentWidth),
string.rep(tHex[self.get("foreground")], self.get("width")), string.rep(tHex[self.get("foreground")], contentWidth),
string.rep(tHex[self.get("background")], self.get("width"))) 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 end
end end

View File

@@ -3,6 +3,18 @@ local sub = string.sub
local tHex = require("libraries/colorHex") 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. ---@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, --- This is the tree class. It provides a hierarchical view of nodes that can be expanded and collapsed,
--- with support for selection and scrolling. --- with support for selection and scrolling.
@@ -21,16 +33,47 @@ end})
Tree.defineProperty(Tree, "selectedNode", {default = nil, type = "table", canTriggerRender = true}) Tree.defineProperty(Tree, "selectedNode", {default = nil, type = "table", canTriggerRender = true})
---@property expandedNodes table {} Table of nodes that are currently expanded ---@property expandedNodes table {} Table of nodes that are currently expanded
Tree.defineProperty(Tree, "expandedNodes", {default = {}, type = "table", canTriggerRender = true}) Tree.defineProperty(Tree, "expandedNodes", {default = {}, type = "table", canTriggerRender = true})
---@property scrollOffset number 0 Current vertical scroll position ---@property offset number 0 Current vertical scroll position
Tree.defineProperty(Tree, "scrollOffset", {default = 0, type = "number", canTriggerRender = true}) 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 ---@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 ---@property selectedForegroundColor color white foreground color of selected node
Tree.defineProperty(Tree, "selectedForegroundColor", {default = colors.white, type = "color"}) Tree.defineProperty(Tree, "selectedForegroundColor", {default = colors.white, type = "color"})
---@property selectedBackgroundColor color lightBlue background color of selected node ---@property selectedBackgroundColor color lightBlue background color of selected node
Tree.defineProperty(Tree, "selectedBackgroundColor", {default = colors.lightBlue, type = "color"}) 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_click")
Tree.defineEvent(Tree, "mouse_drag")
Tree.defineEvent(Tree, "mouse_up")
Tree.defineEvent(Tree, "mouse_scroll") Tree.defineEvent(Tree, "mouse_scroll")
--- Creates a new Tree instance --- Creates a new Tree instance
@@ -91,19 +134,6 @@ function Tree:toggleNode(node)
return self return self
end 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 --- Handles mouse click events
--- @shortDescription Handles mouse click events for node selection and expansion --- @shortDescription Handles mouse click events for node selection and expansion
--- @param button number The button that was clicked --- @param button number The button that was clicked
@@ -114,8 +144,54 @@ end
function Tree:mouse_click(button, x, y) function Tree:mouse_click(button, x, y)
if VisualElement.mouse_click(self, button, x, y) then if VisualElement.mouse_click(self, button, x, y) then
local relX, relY = self:getRelativePosition(x, y) 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 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 if flatNodes[visibleIndex] then
local nodeInfo = flatNodes[visibleIndex] local nodeInfo = flatNodes[visibleIndex]
@@ -142,6 +218,82 @@ function Tree:onSelect(callback)
return self return self
end 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 --- @shortDescription Handles mouse scroll events for vertical scrolling
--- @param direction number The scroll direction (1 for up, -1 for down) --- @param direction number The scroll direction (1 for up, -1 for down)
--- @param x number The x position of the scroll --- @param x number The x position of the scroll
@@ -151,10 +303,16 @@ end
function Tree:mouse_scroll(direction, x, y) function Tree:mouse_scroll(direction, x, y)
if VisualElement.mouse_scroll(self, direction, x, y) then if VisualElement.mouse_scroll(self, direction, x, y) then
local flatNodes = flattenTree(self.get("nodes"), self.get("expandedNodes")) local flatNodes = flattenTree(self.get("nodes"), self.get("expandedNodes"))
local maxScroll = math.max(0, #flatNodes - self.get("height")) local height = self.get("height")
local newScroll = math.min(maxScroll, math.max(0, self.get("scrollOffset") + direction)) 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 return true
end end
return false return false
@@ -167,8 +325,20 @@ end
function Tree:getNodeSize() function Tree:getNodeSize()
local width, height = 0, 0 local width, height = 0, 0
local flatNodes = flattenTree(self.get("nodes"), self.get("expandedNodes")) local flatNodes = flattenTree(self.get("nodes"), self.get("expandedNodes"))
local expandedNodes = self.get("expandedNodes")
for _, nodeInfo in ipairs(flatNodes) do 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 end
height = #flatNodes height = #flatNodes
return width, height return width, height
@@ -181,13 +351,20 @@ function Tree:render()
local flatNodes = flattenTree(self.get("nodes"), self.get("expandedNodes")) local flatNodes = flattenTree(self.get("nodes"), self.get("expandedNodes"))
local height = self.get("height") local height = self.get("height")
local width = self.get("width")
local selectedNode = self.get("selectedNode") local selectedNode = self.get("selectedNode")
local expandedNodes = self.get("expandedNodes") local expandedNodes = self.get("expandedNodes")
local scrollOffset = self.get("scrollOffset") local offset = self.get("offset")
local horizontalOffset = self.get("horizontalOffset") 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 for y = 1, contentHeight do
local nodeInfo = flatNodes[y + scrollOffset] local nodeInfo = flatNodes[y + offset]
if nodeInfo then if nodeInfo then
local node = nodeInfo.node local node = nodeInfo.node
local level = nodeInfo.level 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 _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 fullText = indent .. symbol .. " " .. (node.text or "Node")
local text = sub(fullText, horizontalOffset + 1, horizontalOffset + self.get("width")) local text = sub(fullText, horizontalOffset + 1, horizontalOffset + contentWidth)
local paddedText = text .. string.rep(" ", self.get("width") - #text) local paddedText = text .. string.rep(" ", contentWidth - #text)
local bg = tHex[_bg]:rep(#paddedText) or tHex[colors.black]:rep(#paddedText) local bg = tHex[_bg]:rep(#paddedText) or tHex[colors.black]:rep(#paddedText)
local fg = tHex[_fg]:rep(#paddedText) or tHex[colors.white]:rep(#paddedText) local fg = tHex[_fg]:rep(#paddedText) or tHex[colors.white]:rep(#paddedText)
self:blit(1, y, paddedText, fg, bg) self:blit(1, y, paddedText, fg, bg)
else 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
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 end
return Tree return Tree

View File

@@ -889,7 +889,7 @@ function VisualElement:isFocused()
return self:hasState("focused") return self:hasState("focused")
end 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 colorOrOptions any Border color or options table
--- @param sideOptions? table Side options table (if color is provided as first argument) --- @param sideOptions? table Side options table (if color is provided as first argument)
--- @return VisualElement self --- @return VisualElement self

View File

@@ -23,6 +23,8 @@ local eventParser = require("parsers.eventParser")
local globalParser = require("parsers.globalParser") local globalParser = require("parsers.globalParser")
local helper = require("utils.helper")
local markdownGenerator = require("utils.markdownGenerator") local markdownGenerator = require("utils.markdownGenerator")
BasaltDoc.annotationHandlers = {} BasaltDoc.annotationHandlers = {}
@@ -145,6 +147,31 @@ BasaltDoc.registerAnnotation("@globalDescription", function(target, args)
end end
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 classParser then classParser.setHandlers(BasaltDoc.annotationHandlers) end
if functionParser then functionParser.setHandlers(BasaltDoc.annotationHandlers) end if functionParser then functionParser.setHandlers(BasaltDoc.annotationHandlers) end
if propertyParser then propertyParser.setHandlers(BasaltDoc.annotationHandlers) end if propertyParser then propertyParser.setHandlers(BasaltDoc.annotationHandlers) end
@@ -192,12 +219,14 @@ function BasaltDoc.parse(content)
local annotationBuffer = {} local annotationBuffer = {}
local currentClass = nil local currentClass = nil
local firstTag = nil local firstTag = nil
local pendingTableTypes = {}
local blockStartTags = { local blockStartTags = {
["@class"] = true, ["@class"] = true,
["@property"] = true, ["@property"] = true,
["@event"] = true, ["@event"] = true,
["@skip"] = true ["@skip"] = true,
["@tableType"] = true
} }
local i = 1 local i = 1
@@ -225,9 +254,25 @@ function BasaltDoc.parse(content)
if firstTag == "@class" and classParser then if firstTag == "@class" and classParser then
local class = classParser.parse(annotationBuffer, table.concat(annotationBuffer, "\n")) local class = classParser.parse(annotationBuffer, table.concat(annotationBuffer, "\n"))
if class and not class.skip then 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) table.insert(ast.classes, class)
currentClass = class currentClass = class
end 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 elseif firstTag == "@property" and currentClass and propertyParser then
local prop = propertyParser.parse(annotationBuffer, table.concat(annotationBuffer, "\n")) local prop = propertyParser.parse(annotationBuffer, table.concat(annotationBuffer, "\n"))
if prop then if prop then

View File

@@ -17,6 +17,7 @@ function classParser.parse(annotations, line)
properties = {}, properties = {},
events = {}, events = {},
functions = {}, functions = {},
tableTypes = {},
skip = false skip = false
} }

View File

@@ -7,7 +7,36 @@ function helper.applyAnnotations(annotations, target, handlers)
local tag, args = ann:match("^%-%-%-?%s*(@%S+)%s*(.*)") local tag, args = ann:match("^%-%-%-?%s*(@%S+)%s*(.*)")
if tag then 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 = "" local multiArgs = ""
i = i + 1 i = i + 1

View File

@@ -72,16 +72,21 @@ local function generateFunctionMarkdown(class, functions)
if f.usage then if f.usage then
table.insert(md, "### Usage") table.insert(md, "### Usage")
table.insert(md, "```lua") for _, usageBlock in ipairs(f.usage) do
for _, usage in ipairs(f.usage) do table.insert(md, "```lua run")
if usage == "" then -- Check if usageBlock is already multi-line
table.insert(md, "") if type(usageBlock) == "string" then
else if usageBlock:match("\n") then
table.insert(md, usage) -- Multi-line block
table.insert(md, usageBlock)
else
-- Single line
table.insert(md, usageBlock)
end
end end
table.insert(md, "```")
table.insert(md, "")
end end
table.insert(md, "```")
table.insert(md, "")
end end
if f.run then if f.run then
@@ -157,6 +162,38 @@ function markdownGenerator.generate(ast)
end end
table.insert(md, "") 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 if not class.skipPropertyList and #class.properties > 0 then
table.insert(md, "## Properties") table.insert(md, "## Properties")
table.insert(md, "") table.insert(md, "")