From b482c26787e18d5e75ea68672a238f307504dda2 Mon Sep 17 00:00:00 2001 From: Yao Jingxi <23722032@bjtu.edu.cn> Date: Sat, 14 Nov 2026 12:40:06 +0800 Subject: [PATCH] fix(components): size menu rows by the theme's control height Menu item rows were always 32px, so a compact theme (control height 26) packed its buttons and inputs densely but left its context, dropdown and menubar menus as loose as the default one. A row now takes the theme's control height: 32 at the default density, as before, 26 at the compact one and 40 at the large one. A RowHeight option still wins, and a theme without a control height keeps 32. Labels and separators keep their size. Select and Combobox lists keep their own 32px rows. --- cmd/viva/templates/context_menu.go.txt | 9 +++--- cmd/viva/templates/dropdown_menu.go.txt | 22 ++++++++++--- components/context_menu.go | 9 +++--- components/context_menu_test.go | 43 +++++++++++++++++++++++++ components/dropdown_menu.go | 22 ++++++++++--- 5 files changed, 89 insertions(+), 16 deletions(-) diff --git a/cmd/viva/templates/context_menu.go.txt b/cmd/viva/templates/context_menu.go.txt index 2ae972f6..f08c5473 100644 --- a/cmd/viva/templates/context_menu.go.txt +++ b/cmd/viva/templates/context_menu.go.txt @@ -68,9 +68,10 @@ type ContextMenuOptions struct { // Submenu makes this the content of a submenu, which has a smaller minimum // width (128) than a context menu (144). Submenu bool - // RowHeight is the height of an item row. Zero is the standard 32px; a menu - // with many items can ask for a denser one (24, say). Group labels, separators - // and everything else keep their size. An icon never grows past the row. + // RowHeight is the height of an item row. Zero is the theme's control height + // (32 at the default density, 26 at the compact one); a menu with many items + // can ask for a denser one (24, say). Group labels, separators and everything + // else keep their size. An icon never grows past the row. RowHeight float32 } @@ -121,7 +122,7 @@ func ContextMenuItemWidth(canvas *gfx.Canvas, theme gfx.Theme, item ContextMenuI } // ContextMenuItemHeight is the height AddItem reserves for item. rowHeight is -// ContextMenuOptions.RowHeight (zero selects the standard 32px label row). +// ContextMenuOptions.RowHeight (zero selects the theme's control height). func ContextMenuItemHeight(theme gfx.Theme, item ContextMenuItem, rowHeight float32) float32 { return menuItemHeight(theme, item.common(), rowHeight) } diff --git a/cmd/viva/templates/dropdown_menu.go.txt b/cmd/viva/templates/dropdown_menu.go.txt index 87871b41..5ef14680 100644 --- a/cmd/viva/templates/dropdown_menu.go.txt +++ b/cmd/viva/templates/dropdown_menu.go.txt @@ -237,8 +237,8 @@ func BeginDropdownMenu(canvas *gfx.Canvas, theme gfx.Theme, bounds gfx.Rect, opt // Bounds returns the normalized overlay geometry. func (menu DropdownMenu) Bounds() gfx.Rect { return menu.surface.bounds } -// NextItemBounds returns the next 32px keyboard-navigation row without -// advancing the declaration cursor. +// NextItemBounds returns the next keyboard-navigation row, as tall as the +// theme's control height, without advancing the declaration cursor. func (menu DropdownMenu) NextItemBounds() gfx.Rect { return menu.surface.rowBounds(menu.surface.itemHeight()) } @@ -606,6 +606,8 @@ func (scroll *MenuScroll) follow(canvas *gfx.Canvas) { } const ( + // menuRowHeight is the height of an item row in a theme without a control + // height; a theme with one sizes its rows by it (menuThemeRowHeight). menuRowHeight = float32(32) menuLabelHeight = float32(28) menuSeparatorHeight = float32(9) @@ -675,7 +677,8 @@ type menuSurfaceOptions struct { indicatorLeft bool // tabCloses is MenuScroll.TabCloses for this surface. tabCloses bool - // rowHeight is the height of an item row; zero is menuRowHeight. + // rowHeight is the height of an item row; zero is the theme's + // (menuThemeRowHeight). rowHeight float32 } @@ -833,6 +836,17 @@ func (menu *menuSurface) itemHeight() float32 { if menu.options.rowHeight > 0 { return menu.options.rowHeight } + return menuThemeRowHeight(menu.theme) +} + +// menuThemeRowHeight is the height of an item row in theme: its control +// height, so a compact theme packs its menus as densely as its buttons and +// inputs (32 at the default density, 26 at the compact one), and menuRowHeight +// for a theme without one. +func menuThemeRowHeight(theme gfx.Theme) float32 { + if height := theme.Metrics.ControlHeight; height > 0 { + return height + } return menuRowHeight } @@ -932,7 +946,7 @@ func menuLineHeight(size float32) float32 { return size + 7 } func menuItemHeight(theme gfx.Theme, item menuItem, rowHeight float32) float32 { if rowHeight <= 0 { - rowHeight = menuRowHeight + rowHeight = menuThemeRowHeight(theme) } if item.Description != "" { lines := menuLineHeight(theme.Metrics.FontSizeM) + menuDescriptionGap + menuLineHeight(theme.Metrics.FontSizeS) diff --git a/components/context_menu.go b/components/context_menu.go index 17b2baf7..d917b826 100644 --- a/components/context_menu.go +++ b/components/context_menu.go @@ -68,9 +68,10 @@ type ContextMenuOptions struct { // Submenu makes this the content of a submenu, which has a smaller minimum // width (128) than a context menu (144). Submenu bool - // RowHeight is the height of an item row. Zero is the standard 32px; a menu - // with many items can ask for a denser one (24, say). Group labels, separators - // and everything else keep their size. An icon never grows past the row. + // RowHeight is the height of an item row. Zero is the theme's control height + // (32 at the default density, 26 at the compact one); a menu with many items + // can ask for a denser one (24, say). Group labels, separators and everything + // else keep their size. An icon never grows past the row. RowHeight float32 } @@ -121,7 +122,7 @@ func ContextMenuItemWidth(canvas *gfx.Canvas, theme gfx.Theme, item ContextMenuI } // ContextMenuItemHeight is the height AddItem reserves for item. rowHeight is -// ContextMenuOptions.RowHeight (zero selects the standard 32px label row). +// ContextMenuOptions.RowHeight (zero selects the theme's control height). func ContextMenuItemHeight(theme gfx.Theme, item ContextMenuItem, rowHeight float32) float32 { return menuItemHeight(theme, item.common(), rowHeight) } diff --git a/components/context_menu_test.go b/components/context_menu_test.go index b18b3f8c..2c72922b 100644 --- a/components/context_menu_test.go +++ b/components/context_menu_test.go @@ -178,6 +178,49 @@ func TestContextMenuRowHeightOptionPacksItemsDenser(t *testing.T) { } } +func TestMenuRowsFollowTheThemeControlHeight(t *testing.T) { + compact := gfx.LightTheme() + compact.Metrics = compact.Metrics.WithDensity(gfx.DensityCompact) + unsized := gfx.LightTheme() + unsized.Metrics.ControlHeight = 0 + for _, test := range []struct { + name string + theme gfx.Theme + rowHeight float32 + want float32 + }{ + {"default density", gfx.LightTheme(), 0, 32}, + {"compact density", compact, 0, 26}, + {"RowHeight wins over the theme", compact, 24, 24}, + {"theme without a control height", unsized, 0, menuRowHeight}, + } { + t.Run(test.name, func(t *testing.T) { + var canvas gfx.Canvas + menu := BeginContextMenu(&canvas, test.theme, gfx.Rect{X: 10, Y: 20, Width: 160, Height: 400}, ContextMenuOptions{Layer: 1, Scope: gfx.Item(2), DismissID: gfx.Item(3), RowHeight: test.rowHeight}) + item := ContextMenuItem{ID: gfx.Item(11), Label: "Copy Path"} + first := menu.AddItem(item) + label := menu.AddLabel("Group", false) + second := menu.AddItem(ContextMenuItem{ID: gfx.Item(12), Label: "Delete…"}) + if first.Height != test.want || second.Height != test.want || menu.NextItemBounds().Height != test.want { + t.Fatalf("rows = %+v %+v next %+v, want %v high", first, second, menu.NextItemBounds(), test.want) + } + if label.Height != menuLabelHeight { + t.Fatalf("label = %+v, want %v high whatever the rows", label, menuLabelHeight) + } + if got := ContextMenuItemHeight(test.theme, item, test.rowHeight); got != test.want { + t.Fatalf("ContextMenuItemHeight = %v, want %v", got, test.want) + } + }) + } + + var canvas gfx.Canvas + dropdown := BeginDropdownMenu(&canvas, compact, gfx.Rect{X: 10, Y: 20, Width: 160, Height: 400}, DropdownMenuOptions{Layer: 1, Scope: gfx.Item(4), DismissID: gfx.Item(5)}) + row := DropdownMenuItem{ID: gfx.Item(21), Label: "Hide"} + if bounds := dropdown.AddItem(row); bounds.Height != 26 || DropdownMenuItemHeight(compact, row) != 26 { + t.Fatalf("compact dropdown row = %+v, reserved %v, want 26", bounds, DropdownMenuItemHeight(compact, row)) + } +} + func TestContextMenuItemWidthKeepsTheLabelWhole(t *testing.T) { var canvas gfx.Canvas theme := gfx.LightTheme() diff --git a/components/dropdown_menu.go b/components/dropdown_menu.go index c6cc5390..d5abdb04 100644 --- a/components/dropdown_menu.go +++ b/components/dropdown_menu.go @@ -237,8 +237,8 @@ func BeginDropdownMenu(canvas *gfx.Canvas, theme gfx.Theme, bounds gfx.Rect, opt // Bounds returns the normalized overlay geometry. func (menu DropdownMenu) Bounds() gfx.Rect { return menu.surface.bounds } -// NextItemBounds returns the next 32px keyboard-navigation row without -// advancing the declaration cursor. +// NextItemBounds returns the next keyboard-navigation row, as tall as the +// theme's control height, without advancing the declaration cursor. func (menu DropdownMenu) NextItemBounds() gfx.Rect { return menu.surface.rowBounds(menu.surface.itemHeight()) } @@ -606,6 +606,8 @@ func (scroll *MenuScroll) follow(canvas *gfx.Canvas) { } const ( + // menuRowHeight is the height of an item row in a theme without a control + // height; a theme with one sizes its rows by it (menuThemeRowHeight). menuRowHeight = float32(32) menuLabelHeight = float32(28) menuSeparatorHeight = float32(9) @@ -675,7 +677,8 @@ type menuSurfaceOptions struct { indicatorLeft bool // tabCloses is MenuScroll.TabCloses for this surface. tabCloses bool - // rowHeight is the height of an item row; zero is menuRowHeight. + // rowHeight is the height of an item row; zero is the theme's + // (menuThemeRowHeight). rowHeight float32 } @@ -833,6 +836,17 @@ func (menu *menuSurface) itemHeight() float32 { if menu.options.rowHeight > 0 { return menu.options.rowHeight } + return menuThemeRowHeight(menu.theme) +} + +// menuThemeRowHeight is the height of an item row in theme: its control +// height, so a compact theme packs its menus as densely as its buttons and +// inputs (32 at the default density, 26 at the compact one), and menuRowHeight +// for a theme without one. +func menuThemeRowHeight(theme gfx.Theme) float32 { + if height := theme.Metrics.ControlHeight; height > 0 { + return height + } return menuRowHeight } @@ -932,7 +946,7 @@ func menuLineHeight(size float32) float32 { return size + 7 } func menuItemHeight(theme gfx.Theme, item menuItem, rowHeight float32) float32 { if rowHeight <= 0 { - rowHeight = menuRowHeight + rowHeight = menuThemeRowHeight(theme) } if item.Description != "" { lines := menuLineHeight(theme.Metrics.FontSizeM) + menuDescriptionGap + menuLineHeight(theme.Metrics.FontSizeS)