Files
Basalt2/src/elements/Tree.lua
2025-09-13 10:06:33 +02:00

211 lines
7.8 KiB
Lua

local VisualElement = require("elements/VisualElement")
local sub = string.sub
---@cofnigDescription The tree element provides a hierarchical view of nodes that can be expanded and collapsed, with support for selection and scrolling.
--- This is the tree class. It provides a hierarchical view of nodes that can be expanded and collapsed,
--- with support for selection and scrolling.
---@class Tree : VisualElement
local Tree = setmetatable({}, VisualElement)
Tree.__index = Tree
---@property nodes table {} The tree structure containing node objects with {text, children} properties
Tree.defineProperty(Tree, "nodes", {default = {}, type = "table", canTriggerRender = true, setter = function(self, value)
if #value > 0 then
self.get("expandedNodes")[value[1]] = true
end
return value
end})
---@property selectedNode table? nil Currently selected node
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 horizontalOffset number 0 Current horizontal scroll position
Tree.defineProperty(Tree, "horizontalOffset", {default = 0, type = "number", canTriggerRender = true})
---@property nodeColor color white Color of unselected nodes
Tree.defineProperty(Tree, "nodeColor", {default = colors.white, type = "color"})
---@property selectedColor color lightBlue Background color of selected node
Tree.defineProperty(Tree, "selectedColor", {default = colors.lightBlue, type = "color"})
Tree.defineEvent(Tree, "mouse_click")
Tree.defineEvent(Tree, "mouse_scroll")
--- Creates a new Tree instance
--- @shortDescription Creates a new Tree instance
--- @return Tree self The newly created Tree instance
--- @private
function Tree.new()
local self = setmetatable({}, Tree):__init()
self.class = Tree
self.set("width", 30)
self.set("height", 10)
self.set("z", 5)
return self
end
--- Initializes the Tree instance
--- @shortDescription Initializes the Tree instance
--- @param props table The properties to initialize the element with
--- @param basalt table The basalt instance
--- @return Tree self The initialized instance
--- @protected
function Tree:init(props, basalt)
VisualElement.init(self, props, basalt)
self.set("type", "Tree")
return self
end
--- Expands a node
--- @shortDescription Expands a node to show its children
--- @param node table The node to expand
--- @return Tree self The Tree instance
function Tree:expandNode(node)
self.get("expandedNodes")[node] = true
self:updateRender()
return self
end
--- Collapses a node
--- @shortDescription Collapses a node to hide its children
--- @param node table The node to collapse
--- @return Tree self The Tree instance
function Tree:collapseNode(node)
self.get("expandedNodes")[node] = nil
self:updateRender()
return self
end
--- Toggles a node's expanded state
--- @shortDescription Toggles between expanded and collapsed state
--- @param node table The node to toggle
--- @return Tree self The Tree instance
function Tree:toggleNode(node)
if self.get("expandedNodes")[node] then
self:collapseNode(node)
else
self:expandNode(node)
end
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
--- @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 Tree:mouse_click(button, x, y)
if VisualElement.mouse_click(self, button, x, y) then
local relX, relY = self:getRelativePosition(x, y)
local flatNodes = flattenTree(self.get("nodes"), self.get("expandedNodes"))
local visibleIndex = relY + self.get("scrollOffset")
if flatNodes[visibleIndex] then
local nodeInfo = flatNodes[visibleIndex]
local node = nodeInfo.node
if relX <= nodeInfo.level * 2 + 2 then
self:toggleNode(node)
end
self.set("selectedNode", node)
self:fireEvent("node_select", node)
end
return true
end
return false
end
--- Registers a callback for when a node is selected
--- @shortDescription Registers a callback for when a node is selected
--- @param callback function The callback function
--- @return Tree self The Tree instance
function Tree:onSelect(callback)
self:registerCallback("node_select", callback)
return self
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
--- @param y number The y position of the scroll
--- @return boolean handled Whether the event was handled
--- @protected
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))
self.set("scrollOffset", newScroll)
return true
end
return false
end
--- Gets the size of the tree
--- @shortDescription Gets the size of the tree
--- @return number width The width of the tree
--- @return number height The height of the tree
function Tree:getNodeSize()
local width, height = 0, 0
local flatNodes = flattenTree(self.get("nodes"), self.get("expandedNodes"))
for _, nodeInfo in ipairs(flatNodes) do
width = math.max(width, nodeInfo.level + #nodeInfo.node.text)
end
height = #flatNodes
return width, height
end
--- @shortDescription Renders the tree with nodes, selection and scrolling
--- @protected
function Tree:render()
VisualElement.render(self)
local flatNodes = flattenTree(self.get("nodes"), self.get("expandedNodes"))
local height = self.get("height")
local selectedNode = self.get("selectedNode")
local expandedNodes = self.get("expandedNodes")
local scrollOffset = self.get("scrollOffset")
local horizontalOffset = self.get("horizontalOffset")
for y = 1, height do
local nodeInfo = flatNodes[y + scrollOffset]
if nodeInfo then
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 bg = node == selectedNode and self.get("selectedColor") or self.get("background")
local fullText = indent .. symbol .." " .. (node.text or "Node")
local text = sub(fullText, horizontalOffset + 1, horizontalOffset + self.get("width"))
self:textFg(1, y, text .. string.rep(" ", self.get("width") - #text), self.get("foreground"))
else
self:textFg(1, y, string.rep(" ", self.get("width")), self.get("foreground"), self.get("background"))
end
end
end
return Tree