feat(map-editor): submenu support — View > Layers, Tools > Mode

One level of submenu nesting. View > Layers exposes all eight
terrain-stack layers as toggles, sharing state with the right-side
Layers panel. Tools > Mode mirrors the Mode-pill's auto/direct
toggle with a Tab hotkey shown on Direct. Submenu opens by clicking
the parent item; clicking elsewhere closes everything.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Calic
2026-06-01 19:58:30 +02:00
parent 211056ac8c
commit 9213d3f91b

115
init.lua
View File

@@ -67,6 +67,7 @@ local state = (function()
dirty = false, dirty = false,
picker_open = false, -- atlas+tile picker expanded? (legacy 0.1.0) picker_open = false, -- atlas+tile picker expanded? (legacy 0.1.0)
menu_open = nil, -- 0.3.0: nil | <category_name> (e.g. "File") menu_open = nil, -- 0.3.0: nil | <category_name> (e.g. "File")
submenu_open = nil, -- 0.3.0: label of the open submenu item within menu_open
modal_open = nil, -- 0.3.0: nil | <modal_id> (single-stack) modal_open = nil, -- 0.3.0: nil | <modal_id> (single-stack)
show_layers_panel = true, -- 0.3.0: Window > Layers Panel toggle show_layers_panel = true, -- 0.3.0: Window > Layers Panel toggle
show_palette = true, -- 0.3.0: Window > Palette toggle show_palette = true, -- 0.3.0: Window > Palette toggle
@@ -149,7 +150,14 @@ local state = (function()
function M.set_menu_open(name) state.menu_open = name end function M.set_menu_open(name) state.menu_open = name end
function M.get_menu_open() return state.menu_open end function M.get_menu_open() return state.menu_open end
function M.close_menu() state.menu_open = nil end function M.close_menu()
state.menu_open = nil
state.submenu_open = nil
end
function M.set_submenu_open(label) state.submenu_open = label end
function M.get_submenu_open() return state.submenu_open end
function M.close_submenu() state.submenu_open = nil end
function M.is_layers_panel_shown() return state.show_layers_panel end function M.is_layers_panel_shown() return state.show_layers_panel end
function M.toggle_layers_panel() state.show_layers_panel = not state.show_layers_panel end function M.toggle_layers_panel() state.show_layers_panel = not state.show_layers_panel end
@@ -200,6 +208,7 @@ local state = (function()
state.snap_vertex = nil state.snap_vertex = nil
state.drag_paint = nil state.drag_paint = nil
state.menu_open = nil state.menu_open = nil
state.submenu_open = nil
state.modal_open = nil state.modal_open = nil
state.show_layers_panel = true state.show_layers_panel = true
state.show_palette = true state.show_palette = true
@@ -511,6 +520,42 @@ local ui = (function()
{ type = "submenu", label = "Mode", items = nil }, -- populated in T10 { type = "submenu", label = "Mode", items = nil }, -- populated in T10
} }
do
local layer_items = {}
for _, name in ipairs(LAYER_PANEL_ROWS) do
local layer_name = name
layer_items[#layer_items + 1] = {
type = "toggle",
label = name,
is_on = function() return state.is_layer_visible(layer_name) end,
fire = function() state.toggle_layer_visible(layer_name) end,
}
end
for _, it in ipairs(VIEW_ITEMS) do
if it.type == "submenu" and it.label == "Layers" then
it.items = layer_items
break
end
end
end
do
local mode_items = {
{ type = "toggle", label = "Auto-Tile",
is_on = function() return mode_is("auto-tile") end,
fire = function() actions.set_mode("auto-tile") end },
{ type = "toggle", label = "Direct", hotkey = "Tab",
is_on = function() return mode_is("direct") end,
fire = function() actions.set_mode("direct") end },
}
for _, it in ipairs(TOOLS_ITEMS) do
if it.type == "submenu" and it.label == "Mode" then
it.items = mode_items
break
end
end
end
local WINDOW_ITEMS = { local WINDOW_ITEMS = {
{ type = "toggle", label = "Layers Panel", { type = "toggle", label = "Layers Panel",
is_on = function() return state.is_layers_panel_shown() end, is_on = function() return state.is_layers_panel_shown() end,
@@ -768,6 +813,37 @@ local ui = (function()
local hovered = in_rect(mx, my, row.x, row.y, row.w, row.h) local hovered = in_rect(mx, my, row.x, row.y, row.w, row.h)
draw_dropdown_item(row, hovered) draw_dropdown_item(row, hovered)
end end
-- Submenu render (one level deep)
local sub_label = state.get_submenu_open()
if sub_label then
for _, row in ipairs(dd.rows) do
local it = row.item
if it.type == "submenu" and it.label == sub_label and it.items then
local sub_w = dropdown_width_for(it.items)
local sub_x = dd.x + dd.w
local sub_y = row.y
local sub_h = 0
for _, si in ipairs(it.items) do
sub_h = sub_h + ((si.type == "separator") and 6 or DROPDOWN_ITEM_H)
end
sub_h = sub_h + DROPDOWN_PAD_Y * 2
engine.render.draw_rect(sub_x, sub_y, sub_w, sub_h,
rgba(COL_MODAL_BG, 0xFA))
engine.render.draw_rect_lines(sub_x, sub_y, sub_w, sub_h,
rgba(COL_MODAL_BORDER, 0xFF))
local sy = sub_y + DROPDOWN_PAD_Y
for _, si in ipairs(it.items) do
local row_h = (si.type == "separator") and 6 or DROPDOWN_ITEM_H
local sub_row = { item = si, x = sub_x, y = sy, w = sub_w, h = row_h }
local hovered = in_rect(mx, my, sub_row.x, sub_row.y, sub_row.w, sub_row.h)
draw_dropdown_item(sub_row, hovered)
sy = sy + row_h
end
break
end
end
end
end end
local function fire_item(it) local function fire_item(it)
@@ -1215,7 +1291,9 @@ local ui = (function()
if button == "left" then if button == "left" then
for _, row in ipairs(dd.rows) do for _, row in ipairs(dd.rows) do
if in_rect(mx, my, row.x, row.y, row.w, row.h) then if in_rect(mx, my, row.x, row.y, row.w, row.h) then
if row.item.type ~= "separator" then if row.item.type == "submenu" then
state.set_submenu_open(row.item.label)
elseif row.item.type ~= "separator" then
if fire_item(row.item) then if fire_item(row.item) then
state.close_menu() state.close_menu()
end end
@@ -1229,6 +1307,39 @@ local ui = (function()
break break
end end
end end
-- Submenu items (one level deep)
local sub_label = state.get_submenu_open()
if sub_label then
for _, cat in ipairs(menu_bar_categories_layout()) do
if cat.name == open_name then
local dd2 = dropdown_layout(cat)
for _, row in ipairs(dd2.rows) do
local it = row.item
if it.type == "submenu" and it.label == sub_label and it.items then
local sub_x = dd2.x + dd2.w
local sub_y = row.y
local sy = sub_y + DROPDOWN_PAD_Y
for _, si in ipairs(it.items) do
local row_h = (si.type == "separator") and 6 or DROPDOWN_ITEM_H
if in_rect(mx, my, sub_x, sy, dropdown_width_for(it.items), row_h) then
if si.type ~= "separator" then
if fire_item(si) then
state.close_menu()
end
end
return true
end
sy = sy + row_h
end
break
end
end
break
end
end
end
-- Click outside the dropdown closes it (no return — fall through so -- Click outside the dropdown closes it (no return — fall through so
-- the click can also hit other UI like canvas). -- the click can also hit other UI like canvas).
state.close_menu() state.close_menu()