From 0a9201e0d1b5e974f961038c8d9a3bba997419f3 Mon Sep 17 00:00:00 2001 From: Joe Julian Date: Wed, 8 Apr 2026 23:19:04 -0700 Subject: [PATCH] Add explicit header dropdown layout types --- main.go | 73 ++++++++++++++----------------------------- main_test.go | 36 +++++++++++++++++++++ ui_header_dropdown.go | 73 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 133 insertions(+), 49 deletions(-) create mode 100644 ui_header_dropdown.go diff --git a/main.go b/main.go index 3e3072c..dcc63c8 100644 --- a/main.go +++ b/main.go @@ -5311,77 +5311,63 @@ func (u *ui) headerActions(gtx layout.Context) layout.Dimensions { return layout.Dimensions{} } spacing := gtx.Dp(unit.Dp(8)) - rowOriginX := 0 - var syncDims, lockDims, mainDims layout.Dimensions + metrics := headerActionMetrics{Spacing: spacing} row := func(gtx layout.Context) layout.Dimensions { return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx, layout.Rigid(func(gtx layout.Context) layout.Dimensions { - syncDims = u.syncButtonGroup(gtx) - return syncDims + metrics.SyncDims = u.syncButtonGroup(gtx) + return metrics.SyncDims }), layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { btn := material.Button(u.theme, &u.lockVault, "Lock") - lockDims = btn.Layout(gtx) - return lockDims + metrics.LockDims = btn.Layout(gtx) + return metrics.LockDims }), layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { - mainDims = u.mainMenuButtonGroup(gtx) - return mainDims + metrics.MainDims = u.mainMenuButtonGroup(gtx) + return metrics.MainDims }), ) } rowOps := op.Record(gtx.Ops) - rowDims := row(gtx) + metrics.RowDims = row(gtx) rowCall := rowOps.Stop() if u.mode == "phone" { - rowOriginX = max(0, gtx.Constraints.Max.X-rowDims.Size.X) + metrics.RowOriginX = max(0, gtx.Constraints.Max.X-metrics.RowDims.Size.X) } - drawMenu := func(menu layout.Widget, triggerRightX, triggerBottomY int) { - menuGTX := gtx - menuGTX.Constraints.Min = image.Point{} - menuGTX.Constraints.Max.X = gtx.Constraints.Max.X - menuOps := op.Record(gtx.Ops) - menuDims := layout.Inset{Top: unit.Dp(6)}.Layout(menuGTX, menu) - menuCall := menuOps.Stop() - menuX := anchoredMenuOriginX(gtx.Constraints.Max.X, rowOriginX, triggerRightX, menuDims.Size.X) - stack := op.Offset(image.Pt(menuX, triggerBottomY)).Push(gtx.Ops) - menuCall.Add(gtx.Ops) - stack.Pop() - } + surface := dropdownSurface{ContainerWidth: gtx.Constraints.Max.X, LeftInset: 0, TopInset: 0} - rowStack := op.Offset(image.Pt(rowOriginX, 0)).Push(gtx.Ops) + rowStack := op.Offset(image.Pt(metrics.RowOriginX, 0)).Push(gtx.Ops) rowCall.Add(gtx.Ops) rowStack.Pop() if u.mode == "phone" { if u.syncMenuOpen { u.phoneSyncMenuVisible = true - u.phoneSyncMenuAnchor = image.Pt(rowOriginX+syncDims.Size.X, rowDims.Size.Y) + u.phoneSyncMenuAnchor = metrics.syncAnchor().point() } if u.mainMenuOpen { - triggerRightX := syncDims.Size.X + spacing + lockDims.Size.X + spacing + mainDims.Size.X u.phoneMainMenuVisible = true - u.phoneMainMenuAnchor = image.Pt(rowOriginX+triggerRightX, rowDims.Size.Y) + u.phoneMainMenuAnchor = metrics.mainAnchor().point() } width := gtx.Constraints.Max.X - return layout.Dimensions{Size: image.Pt(width, rowDims.Size.Y)} + return layout.Dimensions{Size: image.Pt(width, metrics.RowDims.Size.Y)} } if u.syncMenuOpen { - drawMenu(u.syncMenu, syncDims.Size.X, rowDims.Size.Y) + surface.draw(gtx, metrics.syncAnchor(), u.syncMenu) } if u.mainMenuOpen { - triggerRightX := syncDims.Size.X + spacing + lockDims.Size.X + spacing + mainDims.Size.X - drawMenu(u.mainMenu, triggerRightX, rowDims.Size.Y) + surface.draw(gtx, metrics.mainAnchor(), u.mainMenu) } - width := rowDims.Size.X - return layout.Dimensions{Size: image.Pt(width, rowDims.Size.Y)} + width := metrics.RowDims.Size.X + return layout.Dimensions{Size: image.Pt(width, metrics.RowDims.Size.Y)} } func (u *ui) mainMenu(gtx layout.Context) layout.Dimensions { @@ -7105,28 +7091,17 @@ func (u *ui) phoneHeaderMenus(gtx layout.Context) layout.Dimensions { } gtx.Constraints.Min = gtx.Constraints.Max contentInsetPx := gtx.Dp(unit.Dp(16)) - contentWidth := max(0, gtx.Constraints.Max.X-(contentInsetPx*2)) - - drawMenu := func(anchor image.Point, menu layout.Widget) layout.Dimensions { - menuGTX := gtx - menuGTX.Constraints.Min = image.Point{} - menuGTX.Constraints.Max.X = contentWidth - menuOps := op.Record(gtx.Ops) - menuDims := layout.Inset{Top: unit.Dp(6)}.Layout(menuGTX, menu) - menuCall := menuOps.Stop() - menuX := contentInsetPx + anchoredMenuOriginX(contentWidth, 0, anchor.X, menuDims.Size.X) - menuY := contentInsetPx + anchor.Y - stack := op.Offset(image.Pt(menuX, menuY)).Push(gtx.Ops) - menuCall.Add(gtx.Ops) - stack.Pop() - return layout.Dimensions{Size: gtx.Constraints.Max} + surface := dropdownSurface{ + ContainerWidth: max(0, gtx.Constraints.Max.X-(contentInsetPx*2)), + LeftInset: contentInsetPx, + TopInset: contentInsetPx, } if u.syncMenuVisibleOnPhone() { - _ = drawMenu(u.phoneSyncMenuAnchor, u.syncMenu) + surface.draw(gtx, dropdownAnchor{TriggerRightX: u.phoneSyncMenuAnchor.X, TriggerBottomY: u.phoneSyncMenuAnchor.Y}, u.syncMenu) } if u.mainMenuVisibleOnPhone() { - _ = drawMenu(u.phoneMainMenuAnchor, u.mainMenu) + surface.draw(gtx, dropdownAnchor{TriggerRightX: u.phoneMainMenuAnchor.X, TriggerBottomY: u.phoneMainMenuAnchor.Y}, u.mainMenu) } return layout.Dimensions{Size: gtx.Constraints.Max} } diff --git a/main_test.go b/main_test.go index eeefb47..a809dc9 100644 --- a/main_test.go +++ b/main_test.go @@ -391,6 +391,42 @@ func TestAnchoredMenuOriginXClampsToVisibleContainer(t *testing.T) { } } +func TestHeaderActionMetricsComputeTriggerAnchors(t *testing.T) { + t.Parallel() + + metrics := headerActionMetrics{ + RowOriginX: 24, + Spacing: 8, + RowDims: layout.Dimensions{Size: image.Pt(180, 40)}, + SyncDims: layout.Dimensions{Size: image.Pt(52, 40)}, + LockDims: layout.Dimensions{Size: image.Pt(44, 40)}, + MainDims: layout.Dimensions{Size: image.Pt(36, 40)}, + } + + if got := metrics.syncAnchor(); got != (dropdownAnchor{TriggerRightX: 76, TriggerBottomY: 40}) { + t.Fatalf("metrics.syncAnchor() = %+v, want right=76 bottom=40", got) + } + if got := metrics.mainAnchor(); got != (dropdownAnchor{TriggerRightX: 172, TriggerBottomY: 40}) { + t.Fatalf("metrics.mainAnchor() = %+v, want right=172 bottom=40", got) + } +} + +func TestDropdownSurfaceOriginKeepsMenusWithinVisibleArea(t *testing.T) { + t.Parallel() + + surface := dropdownSurface{ContainerWidth: 320, LeftInset: 16, TopInset: 16} + anchor := dropdownAnchor{TriggerRightX: 300, TriggerBottomY: 42} + + if got := surface.origin(anchor, 140); got != image.Pt(176, 58) { + t.Fatalf("surface.origin(anchor, 140) = %v, want (176,58)", got) + } + + leftAnchor := dropdownAnchor{TriggerRightX: 36, TriggerBottomY: 42} + if got := surface.origin(leftAnchor, 120); got != image.Pt(16, 58) { + t.Fatalf("surface.origin(leftAnchor, 120) = %v, want (16,58)", got) + } +} + func TestUICurrentVaultSummary(t *testing.T) { t.Parallel() diff --git a/ui_header_dropdown.go b/ui_header_dropdown.go new file mode 100644 index 0000000..4e1bbe2 --- /dev/null +++ b/ui_header_dropdown.go @@ -0,0 +1,73 @@ +package main + +import ( + "image" + + "gioui.org/layout" + "gioui.org/op" + "gioui.org/unit" +) + +type dropdownAnchor struct { + TriggerRightX int + TriggerBottomY int +} + +func (a dropdownAnchor) point() image.Point { + return image.Pt(a.TriggerRightX, a.TriggerBottomY) +} + +type dropdownSurface struct { + ContainerWidth int + LeftInset int + TopInset int +} + +func (s dropdownSurface) menuConstraints(gtx layout.Context) layout.Context { + menuGTX := gtx + menuGTX.Constraints.Min = image.Point{} + menuGTX.Constraints.Max.X = max(0, s.ContainerWidth) + return menuGTX +} + +func (s dropdownSurface) origin(anchor dropdownAnchor, menuWidth int) image.Point { + x := s.LeftInset + anchoredMenuOriginX(s.ContainerWidth, 0, anchor.TriggerRightX, menuWidth) + y := s.TopInset + anchor.TriggerBottomY + return image.Pt(x, y) +} + +func (s dropdownSurface) draw(gtx layout.Context, anchor dropdownAnchor, menu layout.Widget) layout.Dimensions { + menuGTX := s.menuConstraints(gtx) + menuOps := op.Record(gtx.Ops) + menuDims := layout.Inset{Top: unit.Dp(6)}.Layout(menuGTX, menu) + menuCall := menuOps.Stop() + menuOrigin := s.origin(anchor, menuDims.Size.X) + stack := op.Offset(menuOrigin).Push(gtx.Ops) + menuCall.Add(gtx.Ops) + stack.Pop() + return layout.Dimensions{Size: gtx.Constraints.Max} +} + +type headerActionMetrics struct { + RowOriginX int + Spacing int + RowDims layout.Dimensions + SyncDims layout.Dimensions + LockDims layout.Dimensions + MainDims layout.Dimensions +} + +func (m headerActionMetrics) syncAnchor() dropdownAnchor { + return dropdownAnchor{ + TriggerRightX: m.RowOriginX + m.SyncDims.Size.X, + TriggerBottomY: m.RowDims.Size.Y, + } +} + +func (m headerActionMetrics) mainAnchor() dropdownAnchor { + triggerRightX := m.SyncDims.Size.X + m.Spacing + m.LockDims.Size.X + m.Spacing + m.MainDims.Size.X + return dropdownAnchor{ + TriggerRightX: m.RowOriginX + triggerRightX, + TriggerBottomY: m.RowDims.Size.Y, + } +}