252 lines
9.5 KiB
Lua
252 lines
9.5 KiB
Lua
local VisualElement = require("elements/VisualElement")
|
|
local tHex = require("libraries/colorHex")
|
|
|
|
--- This is the table class. It provides a sortable data grid with customizable columns,
|
|
--- row selection, and scrolling capabilities.
|
|
--- @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)
|
|
Table.__index = Table
|
|
|
|
---@property columns table {} List of column definitions with {name, width} properties
|
|
Table.defineProperty(Table, "columns", {default = {}, type = "table", canTriggerRender = true, setter=function(self, value)
|
|
local t = {}
|
|
for i, col in ipairs(value) do
|
|
if type(col) == "string" then
|
|
t[i] = {name = col, width = #col+1}
|
|
elseif type(col) == "table" then
|
|
t[i] = {name = col.name or "", width = col.width or #col.name+1}
|
|
end
|
|
end
|
|
return t
|
|
end})
|
|
---@property data table {} The table data as array of row arrays
|
|
Table.defineProperty(Table, "data", {default = {}, type = "table", canTriggerRender = true})
|
|
---@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"})
|
|
---@property sortDirection string "asc" Sort direction ("asc" or "desc")
|
|
Table.defineProperty(Table, "sortDirection", {default = "asc", type = "string"})
|
|
---@property scrollOffset number 0 Current scroll position
|
|
Table.defineProperty(Table, "scrollOffset", {default = 0, type = "number", canTriggerRender = true})
|
|
|
|
Table.defineEvent(Table, "mouse_click")
|
|
Table.defineEvent(Table, "mouse_scroll")
|
|
|
|
--- Creates a new Table instance
|
|
--- @shortDescription Creates a new Table instance
|
|
--- @return Table self The newly created Table instance
|
|
--- @private
|
|
function Table.new()
|
|
local self = setmetatable({}, Table):__init()
|
|
self.class = Table
|
|
self.set("width", 30)
|
|
self.set("height", 10)
|
|
self.set("z", 5)
|
|
return self
|
|
end
|
|
|
|
--- @shortDescription Initializes the Table instance
|
|
--- @param props table The properties to initialize the element with
|
|
--- @param basalt table The basalt instance
|
|
--- @return Table self The initialized instance
|
|
--- @protected
|
|
function Table:init(props, basalt)
|
|
VisualElement.init(self, props, basalt)
|
|
self.set("type", "Table")
|
|
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
|
|
--- @return Table self The Table instance
|
|
function Table:addColumn(name, width)
|
|
local columns = self.get("columns")
|
|
table.insert(columns, {name = name, width = width})
|
|
self.set("columns", columns)
|
|
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
|
|
|
|
--- Sorts the table data by column
|
|
--- @shortDescription Sorts the table data by the specified column
|
|
--- @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")
|
|
local direction = self.get("sortDirection")
|
|
if not fn then
|
|
table.sort(data, function(a, b)
|
|
if direction == "asc" then
|
|
return a[columnIndex] < b[columnIndex]
|
|
else
|
|
return a[columnIndex] > b[columnIndex]
|
|
end
|
|
end)
|
|
else
|
|
table.sort(data, function(a, b)
|
|
return fn(a[columnIndex], b[columnIndex])
|
|
end)
|
|
end
|
|
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
|
|
|
|
local relX, relY = self:getRelativePosition(x, y)
|
|
|
|
if relY == 1 then
|
|
local currentX = 1
|
|
for i, col in ipairs(self.get("columns")) do
|
|
if relX >= currentX and relX < currentX + col.width then
|
|
if self.get("sortColumn") == i then
|
|
self.set("sortDirection", self.get("sortDirection") == "asc" and "desc" or "asc")
|
|
else
|
|
self.set("sortColumn", i)
|
|
self.set("sortDirection", "asc")
|
|
end
|
|
self:sortData(i)
|
|
break
|
|
end
|
|
currentX = currentX + col.width
|
|
end
|
|
end
|
|
|
|
if relY > 1 then
|
|
local rowIndex = relY - 2 + self.get("scrollOffset")
|
|
if rowIndex >= 0 and rowIndex < #self.get("data") then
|
|
local newIndex = rowIndex + 1
|
|
self.set("selectedRow", newIndex)
|
|
self:fireEvent("select", newIndex, self.get("data")[newIndex])
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
function Table:onSelect(callback)
|
|
self:registerCallback("select", callback)
|
|
return self
|
|
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")
|
|
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))
|
|
|
|
self.set("scrollOffset", newOffset)
|
|
return true
|
|
end
|
|
return false
|
|
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")
|
|
local height = self.get("height")
|
|
local width = self.get("width")
|
|
|
|
local currentX = 1
|
|
for i, col in ipairs(columns) do
|
|
local text = col.name
|
|
if i == sortCol then
|
|
text = text .. (self.get("sortDirection") == "asc" and "\30" or "\31")
|
|
end
|
|
self:textFg(currentX, 1, text:sub(1, col.width), self.get("headerColor"))
|
|
currentX = currentX + col.width
|
|
end
|
|
|
|
for y = 2, height do
|
|
local rowIndex = y - 2 + scrollOffset
|
|
local rowData = data[rowIndex + 1]
|
|
|
|
if rowData and (rowIndex + 1) <= #data then
|
|
currentX = 1
|
|
local bg = (rowIndex + 1) == selected and self.get("selectedColor") or self.get("background")
|
|
|
|
for i, col in ipairs(columns) do
|
|
local cellText = tostring(rowData[i] or "")
|
|
local paddedText = cellText .. string.rep(" ", col.width - #cellText)
|
|
if i < #columns then
|
|
paddedText = string.sub(paddedText, 1, col.width - 1) .. " "
|
|
end
|
|
local finalText = string.sub(paddedText, 1, col.width)
|
|
local finalForeground = string.rep(tHex[self.get("foreground")], #finalText)
|
|
local finalBackground = string.rep(tHex[bg], #finalText)
|
|
|
|
self:blit(currentX, y, finalText, finalForeground, finalBackground)
|
|
currentX = currentX + col.width
|
|
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")))
|
|
end
|
|
end
|
|
|
|
if #data > height - 2 then
|
|
local scrollbarHeight = height - 2
|
|
local thumbSize = math.max(1, math.floor(scrollbarHeight * (height - 2) / #data))
|
|
|
|
local maxScroll = #data - (height - 2) + 1
|
|
local scrollPercent = scrollOffset / maxScroll
|
|
local thumbPos = 2 + math.floor(scrollPercent * (scrollbarHeight - thumbSize))
|
|
|
|
if scrollOffset >= maxScroll then
|
|
thumbPos = height - thumbSize
|
|
end
|
|
|
|
for y = 2, height do
|
|
self:blit(self.get("width"), y, "\127", tHex[colors.gray], tHex[colors.gray])
|
|
end
|
|
|
|
for y = thumbPos, math.min(height, thumbPos + thumbSize - 1) do
|
|
self:blit(self.get("width"), y, "\127", tHex[colors.white], tHex[colors.white])
|
|
end
|
|
end
|
|
end
|
|
|
|
return Table |