Add Breadcrumb, ContextMenu, Dialog, and Toast elements with event handling and rendering capabilities
- Implemented Breadcrumb element for navigation with selectable segments. - Created ContextMenu element for displaying clickable items and submenus. - Developed Dialog element for alert, confirm, and prompt dialogs with modal support. - Introduced Toast element for temporary message notifications with auto-hide functionality.
This commit is contained in:
363
src/elements/ContextMenu.lua
Normal file
363
src/elements/ContextMenu.lua
Normal file
@@ -0,0 +1,363 @@
|
||||
local elementManager = require("elementManager")
|
||||
local VisualElement = require("elements/VisualElement")
|
||||
local Container = elementManager.getElement("Container")
|
||||
local tHex = require("libraries/colorHex")
|
||||
---@configDescription A ContextMenu element that displays a menu with items and submenus.
|
||||
|
||||
--- The ContextMenu displays a list of clickable items with optional submenus
|
||||
--- @run [[
|
||||
--- local basalt = require("basalt")
|
||||
---
|
||||
--- local main = basalt.getMainFrame()
|
||||
---
|
||||
--- -- Create a label that shows the selected action
|
||||
--- local statusLabel = main:addLabel({
|
||||
--- x = 2,
|
||||
--- y = 2,
|
||||
--- text = "Right-click anywhere!",
|
||||
--- foreground = colors.yellow
|
||||
--- })
|
||||
---
|
||||
--- -- Create a ContextMenu
|
||||
--- local contextMenu = main:addContextMenu({
|
||||
--- x = 10,
|
||||
--- y = 5,
|
||||
--- background = colors.black,
|
||||
--- foreground = colors.white,
|
||||
--- })
|
||||
---
|
||||
--- contextMenu:setItems({
|
||||
--- {
|
||||
--- label = "Copy",
|
||||
--- onClick = function()
|
||||
--- statusLabel:setText("Action: Copy")
|
||||
--- end
|
||||
--- },
|
||||
--- {
|
||||
--- label = "Paste",
|
||||
--- onClick = function()
|
||||
--- statusLabel:setText("Action: Paste")
|
||||
--- end
|
||||
--- },
|
||||
--- {
|
||||
--- label = "Delete",
|
||||
--- background = colors.red,
|
||||
--- foreground = colors.white,
|
||||
--- onClick = function()
|
||||
--- statusLabel:setText("Action: Delete")
|
||||
--- end
|
||||
--- },
|
||||
--- {label = "---", disabled = true},
|
||||
--- {
|
||||
--- label = "More Options",
|
||||
--- submenu = {
|
||||
--- {
|
||||
--- label = "Option 1",
|
||||
--- onClick = function()
|
||||
--- statusLabel:setText("Action: Option 1")
|
||||
--- end
|
||||
--- },
|
||||
--- {
|
||||
--- label = "Option 2",
|
||||
--- onClick = function()
|
||||
--- statusLabel:setText("Action: Option 2")
|
||||
--- end
|
||||
--- },
|
||||
--- {label = "---", disabled = true},
|
||||
--- {
|
||||
--- label = "Nested",
|
||||
--- submenu = {
|
||||
--- {
|
||||
--- label = "Deep 1",
|
||||
--- onClick = function()
|
||||
--- statusLabel:setText("Action: Deep 1")
|
||||
--- end
|
||||
--- }
|
||||
--- }
|
||||
--- }
|
||||
--- }
|
||||
--- },
|
||||
--- {label = "---", disabled = true},
|
||||
--- {
|
||||
--- label = "Exit",
|
||||
--- onClick = function()
|
||||
--- statusLabel:setText("Action: Exit")
|
||||
--- end
|
||||
--- }
|
||||
--- })
|
||||
---
|
||||
--- -- Open menu on right-click anywhere
|
||||
--- main:onClick(function(self, button, x, y)
|
||||
--- if button == 2 then
|
||||
--- contextMenu.set("x", x)
|
||||
--- contextMenu.set("y", y)
|
||||
--- contextMenu:open()
|
||||
--- basalt.LOGGER.info("Context menu opened at (" .. x .. ", " .. y .. ")")
|
||||
--- end
|
||||
--- end)
|
||||
---
|
||||
--- basalt.run()
|
||||
--- ]]
|
||||
---@class ContextMenu : Container
|
||||
local ContextMenu = setmetatable({}, Container)
|
||||
ContextMenu.__index = ContextMenu
|
||||
|
||||
---@property items table {} List of menu items
|
||||
ContextMenu.defineProperty(ContextMenu, "items", {default = {}, type = "table", canTriggerRender = true})
|
||||
---@property isOpen boolean false Whether the menu is currently open
|
||||
ContextMenu.defineProperty(ContextMenu, "isOpen", {default = false, type = "boolean", canTriggerRender = true})
|
||||
---@property openSubmenu table nil Currently open submenu data
|
||||
ContextMenu.defineProperty(ContextMenu, "openSubmenu", {default = nil, type = "table", allowNil = true})
|
||||
---@property itemHeight number 1 Height of each menu item
|
||||
ContextMenu.defineProperty(ContextMenu, "itemHeight", {default = 1, type = "number", canTriggerRender = true})
|
||||
|
||||
ContextMenu.defineEvent(ContextMenu, "mouse_click")
|
||||
|
||||
--- @shortDescription Creates a new ContextMenu instance
|
||||
--- @return ContextMenu self The created instance
|
||||
--- @private
|
||||
function ContextMenu.new()
|
||||
local self = setmetatable({}, ContextMenu):__init()
|
||||
self.class = ContextMenu
|
||||
self.set("width", 10)
|
||||
self.set("height", 10)
|
||||
self.set("visible", false)
|
||||
return self
|
||||
end
|
||||
|
||||
--- @shortDescription Initializes the ContextMenu instance
|
||||
--- @param props table The properties to initialize the element with
|
||||
--- @param basalt table The basalt instance
|
||||
--- @protected
|
||||
function ContextMenu:init(props, basalt)
|
||||
Container.init(self, props, basalt)
|
||||
self.set("type", "ContextMenu")
|
||||
end
|
||||
|
||||
--- Sets the menu items
|
||||
--- @shortDescription Sets the menu items from a table
|
||||
--- @param items table Array of item definitions
|
||||
--- @return ContextMenu self For method chaining
|
||||
function ContextMenu:setItems(items)
|
||||
self.set("items", items or {})
|
||||
self:calculateSize()
|
||||
return self
|
||||
end
|
||||
|
||||
--- @shortDescription Calculates menu size based on items
|
||||
--- @private
|
||||
function ContextMenu:calculateSize()
|
||||
local items = self.get("items")
|
||||
local itemHeight = self.get("itemHeight")
|
||||
|
||||
if #items == 0 then
|
||||
self.set("width", 10)
|
||||
self.set("height", 2)
|
||||
return
|
||||
end
|
||||
|
||||
local maxWidth = 8
|
||||
for _, item in ipairs(items) do
|
||||
if item.label then
|
||||
local labelLen = #item.label
|
||||
local itemWidth = labelLen + 3
|
||||
if item.submenu then
|
||||
itemWidth = itemWidth + 1 -- " >"
|
||||
end
|
||||
if itemWidth > maxWidth then
|
||||
maxWidth = itemWidth
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local height = #items * itemHeight
|
||||
|
||||
self.set("width", maxWidth)
|
||||
self.set("height", height)
|
||||
end
|
||||
|
||||
--- Opens the menu
|
||||
--- @shortDescription Opens the context menu
|
||||
--- @return ContextMenu self For method chaining
|
||||
function ContextMenu:open()
|
||||
self.set("isOpen", true)
|
||||
self.set("visible", true)
|
||||
self:updateRender()
|
||||
self:dispatchEvent("opened")
|
||||
return self
|
||||
end
|
||||
|
||||
--- Closes the menu and any submenus
|
||||
--- @shortDescription Closes the context menu
|
||||
--- @return ContextMenu self For method chaining
|
||||
function ContextMenu:close()
|
||||
self.set("isOpen", false)
|
||||
self.set("visible", false)
|
||||
|
||||
local openSubmenu = self.get("openSubmenu")
|
||||
if openSubmenu and openSubmenu.menu then
|
||||
openSubmenu.menu:close()
|
||||
end
|
||||
self.set("openSubmenu", nil)
|
||||
|
||||
self:updateRender()
|
||||
self:dispatchEvent("closed")
|
||||
return self
|
||||
end
|
||||
|
||||
--- Closes the entire menu chain (parent and all submenus)
|
||||
--- @shortDescription Closes the root menu and all child menus
|
||||
--- @return ContextMenu self For method chaining
|
||||
function ContextMenu:closeAll()
|
||||
local root = self
|
||||
while root.parentMenu do
|
||||
root = root.parentMenu
|
||||
end
|
||||
|
||||
root:close()
|
||||
return self
|
||||
end
|
||||
|
||||
--- @shortDescription Gets item at Y position
|
||||
--- @param y number Relative Y position
|
||||
--- @return number? index Item index or nil
|
||||
--- @return table? item Item data or nil
|
||||
--- @private
|
||||
function ContextMenu:getItemAt(y)
|
||||
local items = self.get("items")
|
||||
local itemHeight = self.get("itemHeight")
|
||||
|
||||
local index = math.floor((y - 1) / itemHeight) + 1
|
||||
|
||||
if index >= 1 and index <= #items then
|
||||
return index, items[index]
|
||||
end
|
||||
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
--- @shortDescription Creates a submenu
|
||||
--- @private
|
||||
function ContextMenu:createSubmenu(submenuItems, parentItem)
|
||||
local submenu = self.parent:addContextMenu()
|
||||
submenu:setItems(submenuItems)
|
||||
|
||||
submenu.set("background", self.get("background"))
|
||||
submenu.set("foreground", self.get("foreground"))
|
||||
|
||||
submenu.parentMenu = self
|
||||
|
||||
local parentX = self.get("x")
|
||||
local parentY = self.get("y")
|
||||
local parentWidth = self.get("width")
|
||||
local itemHeight = self.get("itemHeight")
|
||||
local itemIndex = parentItem._index or 1
|
||||
|
||||
submenu.set("x", parentX + parentWidth)
|
||||
submenu.set("y", parentY + (itemIndex - 1) * itemHeight)
|
||||
submenu.set("z", self.get("z") + 1)
|
||||
|
||||
return submenu
|
||||
end
|
||||
|
||||
--- @shortDescription Handles mouse click events
|
||||
--- @protected
|
||||
function ContextMenu:mouse_click(button, x, y)
|
||||
if not VisualElement.mouse_click(self, button, x, y) then
|
||||
self:close()
|
||||
return false
|
||||
end
|
||||
|
||||
local relX, relY = VisualElement.getRelativePosition(self, x, y)
|
||||
local index, item = self:getItemAt(relY)
|
||||
|
||||
if item then
|
||||
if item.disabled then
|
||||
return true
|
||||
end
|
||||
|
||||
if item.submenu then
|
||||
local openSubmenu = self.get("openSubmenu")
|
||||
if openSubmenu and openSubmenu.index == index then
|
||||
openSubmenu.menu:close()
|
||||
self.set("openSubmenu", nil)
|
||||
else
|
||||
if openSubmenu and openSubmenu.menu then
|
||||
openSubmenu.menu:close()
|
||||
end
|
||||
|
||||
item._index = index
|
||||
local submenu = self:createSubmenu(item.submenu, item)
|
||||
submenu:open()
|
||||
|
||||
self.set("openSubmenu", {
|
||||
index = index,
|
||||
menu = submenu
|
||||
})
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
if item.onClick then
|
||||
item.onClick(item)
|
||||
end
|
||||
|
||||
self:closeAll()
|
||||
return true
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
--- @shortDescription Renders the ContextMenu
|
||||
--- @protected
|
||||
function ContextMenu:render()
|
||||
local items = self.get("items")
|
||||
local width = self.get("width")
|
||||
local height = self.get("height")
|
||||
local itemHeight = self.get("itemHeight")
|
||||
local menuBg = self.get("background")
|
||||
local menuFg = self.get("foreground")
|
||||
|
||||
for i, item in ipairs(items) do
|
||||
local y = (i - 1) * itemHeight + 1
|
||||
local itemBg = item.background or menuBg
|
||||
local itemFg = item.foreground or menuFg
|
||||
local bgHex = tHex[itemBg]
|
||||
local fgHex = tHex[itemFg]
|
||||
|
||||
local spaces = string.rep(" ", width)
|
||||
local bgColors = string.rep(bgHex, width)
|
||||
local fgColors = string.rep(fgHex, width)
|
||||
self:blit(1, y, spaces, fgColors, bgColors)
|
||||
|
||||
local label = item.label or ""
|
||||
if #label > width - 3 then
|
||||
label = label:sub(1, width - 3)
|
||||
end
|
||||
|
||||
self:textFg(2, y, label, itemFg)
|
||||
if item.submenu then
|
||||
self:textFg(width - 1, y, ">", itemFg)
|
||||
end
|
||||
end
|
||||
|
||||
if not self.get("childrenSorted") then
|
||||
self:sortChildren()
|
||||
end
|
||||
if not self.get("childrenEventsSorted") then
|
||||
for eventName in pairs(self._values.childrenEvents or {}) do
|
||||
self:sortChildrenEvents(eventName)
|
||||
end
|
||||
end
|
||||
|
||||
for _, child in ipairs(self.get("visibleChildren") or {}) do
|
||||
if child == self then
|
||||
error("CIRCULAR REFERENCE DETECTED!")
|
||||
return
|
||||
end
|
||||
child:render()
|
||||
child:postRender()
|
||||
end
|
||||
end
|
||||
|
||||
return ContextMenu
|
||||
Reference in New Issue
Block a user