Enhance Menu component with dropdown functionality and properties
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user