@@ -8,20 +8,21 @@ local tHex = require("libraries/colorHex")
88local Menu = setmetatable ({}, List )
99Menu .__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
2012Menu .defineProperty (Menu , " separatorColor" , {default = colors .gray , type = " color" })
2113
2214--- @property spacing number 0 The number of spaces between menu items
2315Menu .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
2627Menu .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)
3738Menu .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
5070end
5171
5676--- @protected
5777function 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
178240end
179241
180242--- @shortDescription Handles mouse click events and item selection
181243--- @protected
182244function 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
241322end
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+
243404return Menu
0 commit comments