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)
|
local Menu = setmetatable({}, List)
|
||||||
Menu.__index = Menu
|
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
|
---@property separatorColor color gray The color used for separator items in the menu
|
||||||
Menu.defineProperty(Menu, "separatorColor", {default = colors.gray, type = "color"})
|
Menu.defineProperty(Menu, "separatorColor", {default = colors.gray, type = "color"})
|
||||||
|
|
||||||
---@property spacing number 0 The number of spaces between menu items
|
---@property spacing number 0 The number of spaces between menu items
|
||||||
Menu.defineProperty(Menu, "spacing", {default = 1, type = "number", canTriggerRender = true})
|
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
|
---@property horizontalOffset number 0 Current horizontal scroll offset
|
||||||
Menu.defineProperty(Menu, "horizontalOffset", {
|
Menu.defineProperty(Menu, "horizontalOffset", {
|
||||||
default = 0,
|
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)
|
---@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})
|
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
|
--- Creates a new Menu instance
|
||||||
--- @shortDescription Creates a new Menu instance
|
--- @shortDescription Creates a new Menu instance
|
||||||
--- @return Menu self The newly created Menu instance
|
--- @return Menu self The newly created Menu instance
|
||||||
@@ -45,7 +65,7 @@ function Menu.new()
|
|||||||
self.class = Menu
|
self.class = Menu
|
||||||
self.set("width", 30)
|
self.set("width", 30)
|
||||||
self.set("height", 1)
|
self.set("height", 1)
|
||||||
self.set("background", colors.gray)
|
self.set("z", 8)
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -56,6 +76,7 @@ end
|
|||||||
--- @protected
|
--- @protected
|
||||||
function Menu:init(props, basalt)
|
function Menu:init(props, basalt)
|
||||||
List.init(self, props, basalt)
|
List.init(self, props, basalt)
|
||||||
|
self._entrySchema = entrySchema
|
||||||
self.set("type", "Menu")
|
self.set("type", "Menu")
|
||||||
|
|
||||||
self:observe("items", function()
|
self:observe("items", function()
|
||||||
@@ -175,12 +196,67 @@ function Menu:render()
|
|||||||
end
|
end
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
--- @shortDescription Handles mouse click events and item selection
|
--- @shortDescription Handles mouse click events and item selection
|
||||||
--- @protected
|
--- @protected
|
||||||
function Menu:mouse_click(button, x, y)
|
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
|
if(self.getResolved("selectable") == false) then return false end
|
||||||
local relX = select(1, self:getRelativePosition(x, y))
|
local relX = select(1, self:getRelativePosition(x, y))
|
||||||
local offset = self.getResolved("horizontalOffset")
|
local offset = self.getResolved("horizontalOffset")
|
||||||
@@ -200,6 +276,11 @@ function Menu:mouse_click(button, x, y)
|
|||||||
items[i] = item
|
items[i] = item
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if item.dropdown and #item.dropdown > 0 then
|
||||||
|
self:showDropdown(i, item, currentX - offset)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
if not self.getResolved("multiSelection") then
|
if not self.getResolved("multiSelection") then
|
||||||
for _, otherItem in ipairs(items) do
|
for _, otherItem in ipairs(items) do
|
||||||
if type(otherItem) == "table" then
|
if type(otherItem) == "table" then
|
||||||
@@ -240,4 +321,84 @@ function Menu:mouse_scroll(direction, x, y)
|
|||||||
return false
|
return false
|
||||||
end
|
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
|
return Menu
|
||||||
|
|||||||
Reference in New Issue
Block a user