Enhance Menu component with dropdown functionality and properties

This commit is contained in:
Robert Jelic
2025-11-05 16:20:32 +01:00
parent 0fbd348bba
commit 5ddca6850a

View File

@@ -8,20 +8,21 @@ local tHex = require("libraries/colorHex")
local Menu = setmetatable({}, List)
Menu.__index = Menu
---@tableType ItemTable
---@tableField text string The display text for the item
---@tableField callback function Function called when selected
---@tableField fg color Normal text color
---@tableField bg color Normal background color
---@tableField selectedFg color Text color when selected
---@tableField selectedBg color Background when selected
---@property separatorColor color gray The color used for separator items in the menu
Menu.defineProperty(Menu, "separatorColor", {default = colors.gray, type = "color"})
---@property spacing number 0 The number of spaces between menu items
Menu.defineProperty(Menu, "spacing", {default = 1, type = "number", canTriggerRender = true})
---@property openDropdown table nil Currently open dropdown data {index, items, x, y, width, height}
Menu.defineProperty(Menu, "openDropdown", {default = nil, type = "table", allowNil = true, canTriggerRender = true})
---@property dropdownBackground color black Background color for dropdown menus
Menu.defineProperty(Menu, "dropdownBackground", {default = colors.black, type = "color", canTriggerRender = true})
---@property dropdownForeground color white Foreground color for dropdown menus
Menu.defineProperty(Menu, "dropdownForeground", {default = colors.white, type = "color", canTriggerRender = true})
---@property horizontalOffset number 0 Current horizontal scroll offset
Menu.defineProperty(Menu, "horizontalOffset", {
default = 0,
@@ -36,6 +37,25 @@ Menu.defineProperty(Menu, "horizontalOffset", {
---@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})
---@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
---@tableField dropdown table Array of dropdown items
local entrySchema = {
text = { type = "string", default = "Entry" },
bg = { type = "number", default = nil },
fg = { type = "number", default = nil },
selectedBg = { type = "number", default = nil },
selectedFg = { type = "number", default = nil },
callback = { type = "function", default = nil },
dropdown = { type = "table", default = nil },
}
--- Creates a new Menu instance
--- @shortDescription Creates a new Menu instance
--- @return Menu self The newly created Menu instance
@@ -45,7 +65,7 @@ function Menu.new()
self.class = Menu
self.set("width", 30)
self.set("height", 1)
self.set("background", colors.gray)
self.set("z", 8)
return self
end
@@ -56,6 +76,7 @@ end
--- @protected
function Menu:init(props, basalt)
List.init(self, props, basalt)
self._entrySchema = entrySchema
self.set("type", "Menu")
self:observe("items", function()
@@ -175,12 +196,67 @@ function Menu:render()
end
end
end
local openDropdown = self.getResolved("openDropdown")
if openDropdown then
self:renderDropdown(openDropdown)
end
end
--- Renders the dropdown menu
--- @shortDescription Renders dropdown overlay
--- @param dropdown table Dropdown data
--- @protected
function Menu:renderDropdown(dropdown)
local dropdownBg = self.getResolved("dropdownBackground")
local dropdownFg = self.getResolved("dropdownForeground")
for i, item in ipairs(dropdown.items) do
local y = dropdown.y + i - 1
local label = item.text or item.label or ""
local isSeparator = label == "---"
local bgHex = tHex[item.background or dropdownBg]
local fgHex = tHex[item.foreground or dropdownFg]
local spaces = string.rep(" ", dropdown.width)
self:blit(dropdown.x, y, spaces,
string.rep(fgHex, dropdown.width),
string.rep(bgHex, dropdown.width))
if isSeparator then
local separator = string.rep("-", dropdown.width)
self:blit(dropdown.x, y, separator,
string.rep(tHex[colors.gray], dropdown.width),
string.rep(bgHex, dropdown.width))
else
if #label > dropdown.width - 2 then
label = label:sub(1, dropdown.width - 2)
end
self:textFg(dropdown.x + 1, y, label, item.foreground or dropdownFg)
end
end
end
--- @shortDescription Handles mouse click events and item selection
--- @protected
function Menu:mouse_click(button, x, y)
if not VisualElement.mouse_click(self, button, x, y) then return false end
local openDropdown = self.getResolved("openDropdown")
if openDropdown then
local relX, relY = self:getRelativePosition(x, y)
if self:isInsideDropdown(relX, relY, openDropdown) then
return self:handleDropdownClick(relX, relY, openDropdown)
else
self:hideDropdown()
end
end
if not VisualElement.mouse_click(self, button, x, y) then
return false
end
if(self.getResolved("selectable") == false) then return false end
local relX = select(1, self:getRelativePosition(x, y))
local offset = self.getResolved("horizontalOffset")
@@ -200,6 +276,11 @@ function Menu:mouse_click(button, x, y)
items[i] = item
end
if item.dropdown and #item.dropdown > 0 then
self:showDropdown(i, item, currentX - offset)
return true
end
if not self.getResolved("multiSelection") then
for _, otherItem in ipairs(items) do
if type(otherItem) == "table" then
@@ -240,4 +321,84 @@ function Menu:mouse_scroll(direction, x, y)
return false
end
--- Shows a dropdown menu for a specific item
--- @shortDescription Shows dropdown menu
--- @param index number The item index
--- @param item table The menu item
--- @param itemX number The X position of the item
function Menu:showDropdown(index, item, itemX)
local dropdown = item.dropdown
if not dropdown or #dropdown == 0 then return end
local maxWidth = 8
for _, dropItem in ipairs(dropdown) do
local label = dropItem.text or dropItem.label or ""
if #label + 2 > maxWidth then
maxWidth = #label + 2
end
end
local height = #dropdown
local menuHeight = self.getResolved("height")
self.set("openDropdown", {
index = index,
items = dropdown,
x = itemX,
y = menuHeight + 1,
width = maxWidth,
height = height
})
self:updateRender()
end
--- Closes the currently open dropdown
--- @shortDescription Closes dropdown menu
function Menu:hideDropdown()
self.set("openDropdown", nil)
self:updateRender()
end
--- Checks if a position is inside the dropdown
--- @shortDescription Checks if position is in dropdown
--- @param relX number Relative X position
--- @param relY number Relative Y position
--- @param dropdown table Dropdown data
--- @return boolean inside Whether position is inside dropdown
function Menu:isInsideDropdown(relX, relY, dropdown)
return relX >= dropdown.x and
relX < dropdown.x + dropdown.width and
relY >= dropdown.y and
relY < dropdown.y + dropdown.height
end
--- Handles click inside dropdown
--- @shortDescription Handles dropdown click
--- @param relX number Relative X position
--- @param relY number Relative Y position
--- @param dropdown table Dropdown data
--- @return boolean handled Whether click was handled
function Menu:handleDropdownClick(relX, relY, dropdown)
local itemIndex = relY - dropdown.y + 1
if itemIndex >= 1 and itemIndex <= #dropdown.items then
local item = dropdown.items[itemIndex]
if item.text == "---" or item.label == "---" or item.disabled then
return true
end
if item.callback then
item.callback(self, item)
elseif item.onClick then
item.onClick(self, item)
end
self:hideDropdown()
return true
end
return false
end
return Menu