Skip to content

Commit 5ddca68

Browse files
committed
Enhance Menu component with dropdown functionality and properties
1 parent 0fbd348 commit 5ddca68

File tree

1 file changed

+171
-10
lines changed

1 file changed

+171
-10
lines changed

src/elements/Menu.lua

Lines changed: 171 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,21 @@ local tHex = require("libraries/colorHex")
88
local Menu = setmetatable({}, List)
99
Menu.__index = Menu
1010

11-
---@tableType ItemTable
12-
---@tableField text string The display text for the item
13-
---@tableField callback function Function called when selected
14-
---@tableField fg color Normal text color
15-
---@tableField bg color Normal background color
16-
---@tableField selectedFg color Text color when selected
17-
---@tableField selectedBg color Background when selected
18-
1911
---@property separatorColor color gray The color used for separator items in the menu
2012
Menu.defineProperty(Menu, "separatorColor", {default = colors.gray, type = "color"})
2113

2214
---@property spacing number 0 The number of spaces between menu items
2315
Menu.defineProperty(Menu, "spacing", {default = 1, type = "number", canTriggerRender = true})
2416

17+
---@property openDropdown table nil Currently open dropdown data {index, items, x, y, width, height}
18+
Menu.defineProperty(Menu, "openDropdown", {default = nil, type = "table", allowNil = true, canTriggerRender = true})
19+
20+
---@property dropdownBackground color black Background color for dropdown menus
21+
Menu.defineProperty(Menu, "dropdownBackground", {default = colors.black, type = "color", canTriggerRender = true})
22+
23+
---@property dropdownForeground color white Foreground color for dropdown menus
24+
Menu.defineProperty(Menu, "dropdownForeground", {default = colors.white, type = "color", canTriggerRender = true})
25+
2526
---@property horizontalOffset number 0 Current horizontal scroll offset
2627
Menu.defineProperty(Menu, "horizontalOffset", {
2728
default = 0,
@@ -36,6 +37,25 @@ Menu.defineProperty(Menu, "horizontalOffset", {
3637
---@property maxWidth number nil Maximum width before scrolling is enabled (nil = auto-size to items)
3738
Menu.defineProperty(Menu, "maxWidth", {default = nil, type = "number", canTriggerRender = true})
3839

40+
---@tableType ItemTable
41+
---@tableField text string The display text for the item
42+
---@tableField callback function Function called when selected
43+
---@tableField fg color Normal text color
44+
---@tableField bg color Normal background color
45+
---@tableField selectedFg color Text color when selected
46+
---@tableField selectedBg color Background when selected
47+
---@tableField dropdown table Array of dropdown items
48+
49+
local entrySchema = {
50+
text = { type = "string", default = "Entry" },
51+
bg = { type = "number", default = nil },
52+
fg = { type = "number", default = nil },
53+
selectedBg = { type = "number", default = nil },
54+
selectedFg = { type = "number", default = nil },
55+
callback = { type = "function", default = nil },
56+
dropdown = { type = "table", default = nil },
57+
}
58+
3959
--- Creates a new Menu instance
4060
--- @shortDescription Creates a new Menu instance
4161
--- @return Menu self The newly created Menu instance
@@ -45,7 +65,7 @@ function Menu.new()
4565
self.class = Menu
4666
self.set("width", 30)
4767
self.set("height", 1)
48-
self.set("background", colors.gray)
68+
self.set("z", 8)
4969
return self
5070
end
5171

@@ -56,6 +76,7 @@ end
5676
--- @protected
5777
function Menu:init(props, basalt)
5878
List.init(self, props, basalt)
79+
self._entrySchema = entrySchema
5980
self.set("type", "Menu")
6081

6182
self:observe("items", function()
@@ -175,12 +196,67 @@ function Menu:render()
175196
end
176197
end
177198
end
199+
200+
local openDropdown = self.getResolved("openDropdown")
201+
if openDropdown then
202+
self:renderDropdown(openDropdown)
203+
end
204+
end
205+
206+
--- Renders the dropdown menu
207+
--- @shortDescription Renders dropdown overlay
208+
--- @param dropdown table Dropdown data
209+
--- @protected
210+
function Menu:renderDropdown(dropdown)
211+
local dropdownBg = self.getResolved("dropdownBackground")
212+
local dropdownFg = self.getResolved("dropdownForeground")
213+
214+
for i, item in ipairs(dropdown.items) do
215+
local y = dropdown.y + i - 1
216+
local label = item.text or item.label or ""
217+
218+
local isSeparator = label == "---"
219+
220+
local bgHex = tHex[item.background or dropdownBg]
221+
local fgHex = tHex[item.foreground or dropdownFg]
222+
local spaces = string.rep(" ", dropdown.width)
223+
224+
self:blit(dropdown.x, y, spaces,
225+
string.rep(fgHex, dropdown.width),
226+
string.rep(bgHex, dropdown.width))
227+
228+
if isSeparator then
229+
local separator = string.rep("-", dropdown.width)
230+
self:blit(dropdown.x, y, separator,
231+
string.rep(tHex[colors.gray], dropdown.width),
232+
string.rep(bgHex, dropdown.width))
233+
else
234+
if #label > dropdown.width - 2 then
235+
label = label:sub(1, dropdown.width - 2)
236+
end
237+
self:textFg(dropdown.x + 1, y, label, item.foreground or dropdownFg)
238+
end
239+
end
178240
end
179241

180242
--- @shortDescription Handles mouse click events and item selection
181243
--- @protected
182244
function Menu:mouse_click(button, x, y)
183-
if not VisualElement.mouse_click(self, button, x, y) then return false end
245+
local openDropdown = self.getResolved("openDropdown")
246+
if openDropdown then
247+
local relX, relY = self:getRelativePosition(x, y)
248+
249+
if self:isInsideDropdown(relX, relY, openDropdown) then
250+
return self:handleDropdownClick(relX, relY, openDropdown)
251+
else
252+
self:hideDropdown()
253+
end
254+
end
255+
256+
if not VisualElement.mouse_click(self, button, x, y) then
257+
return false
258+
end
259+
184260
if(self.getResolved("selectable") == false) then return false end
185261
local relX = select(1, self:getRelativePosition(x, y))
186262
local offset = self.getResolved("horizontalOffset")
@@ -200,6 +276,11 @@ function Menu:mouse_click(button, x, y)
200276
items[i] = item
201277
end
202278

279+
if item.dropdown and #item.dropdown > 0 then
280+
self:showDropdown(i, item, currentX - offset)
281+
return true
282+
end
283+
203284
if not self.getResolved("multiSelection") then
204285
for _, otherItem in ipairs(items) do
205286
if type(otherItem) == "table" then
@@ -240,4 +321,84 @@ function Menu:mouse_scroll(direction, x, y)
240321
return false
241322
end
242323

324+
--- Shows a dropdown menu for a specific item
325+
--- @shortDescription Shows dropdown menu
326+
--- @param index number The item index
327+
--- @param item table The menu item
328+
--- @param itemX number The X position of the item
329+
function Menu:showDropdown(index, item, itemX)
330+
local dropdown = item.dropdown
331+
if not dropdown or #dropdown == 0 then return end
332+
333+
local maxWidth = 8
334+
for _, dropItem in ipairs(dropdown) do
335+
local label = dropItem.text or dropItem.label or ""
336+
if #label + 2 > maxWidth then
337+
maxWidth = #label + 2
338+
end
339+
end
340+
341+
local height = #dropdown
342+
local menuHeight = self.getResolved("height")
343+
344+
self.set("openDropdown", {
345+
index = index,
346+
items = dropdown,
347+
x = itemX,
348+
y = menuHeight + 1,
349+
width = maxWidth,
350+
height = height
351+
})
352+
353+
self:updateRender()
354+
end
355+
356+
--- Closes the currently open dropdown
357+
--- @shortDescription Closes dropdown menu
358+
function Menu:hideDropdown()
359+
self.set("openDropdown", nil)
360+
self:updateRender()
361+
end
362+
363+
--- Checks if a position is inside the dropdown
364+
--- @shortDescription Checks if position is in dropdown
365+
--- @param relX number Relative X position
366+
--- @param relY number Relative Y position
367+
--- @param dropdown table Dropdown data
368+
--- @return boolean inside Whether position is inside dropdown
369+
function Menu:isInsideDropdown(relX, relY, dropdown)
370+
return relX >= dropdown.x and
371+
relX < dropdown.x + dropdown.width and
372+
relY >= dropdown.y and
373+
relY < dropdown.y + dropdown.height
374+
end
375+
376+
--- Handles click inside dropdown
377+
--- @shortDescription Handles dropdown click
378+
--- @param relX number Relative X position
379+
--- @param relY number Relative Y position
380+
--- @param dropdown table Dropdown data
381+
--- @return boolean handled Whether click was handled
382+
function Menu:handleDropdownClick(relX, relY, dropdown)
383+
local itemIndex = relY - dropdown.y + 1
384+
385+
if itemIndex >= 1 and itemIndex <= #dropdown.items then
386+
local item = dropdown.items[itemIndex]
387+
388+
if item.text == "---" or item.label == "---" or item.disabled then
389+
return true
390+
end
391+
392+
if item.callback then
393+
item.callback(self, item)
394+
elseif item.onClick then
395+
item.onClick(self, item)
396+
end
397+
398+
self:hideDropdown()
399+
return true
400+
end
401+
return false
402+
end
403+
243404
return Menu

0 commit comments

Comments
 (0)