From c4f110e0ad1d3633cbbe723fd278124ff524f00e Mon Sep 17 00:00:00 2001 From: Joe Julian Date: Fri, 10 Apr 2026 21:48:05 -0700 Subject: [PATCH] Refine compact header menus --- internal/appui/frame.go | 4 +-- internal/appui/header.go | 53 ++++++++++------------------ internal/appui/header/layout/menu.go | 11 ++++-- internal/appui/header/menu.go | 20 ++++++++--- internal/appui/header_sync_menu.go | 52 +++++++++++++++++++++------ 5 files changed, 86 insertions(+), 54 deletions(-) diff --git a/internal/appui/frame.go b/internal/appui/frame.go index e1f2512..a731660 100644 --- a/internal/appui/frame.go +++ b/internal/appui/frame.go @@ -622,9 +622,7 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions { layout.Stacked(u.securityDialogOverlay), layout.Stacked(u.remotePrefsDialogOverlay), layout.Stacked(u.approvalDialogOverlay), - layout.Expanded(func(gtx layout.Context) layout.Dimensions { - return u.phoneHeaderMenus(gtx) - }), + layout.Expanded(u.phoneHeaderMenus), layout.Stacked(u.statusToast), ) } diff --git a/internal/appui/header.go b/internal/appui/header.go index 0e5f805..0e7bd25 100644 --- a/internal/appui/header.go +++ b/internal/appui/header.go @@ -12,7 +12,6 @@ import ( "gioui.org/widget/material" headerview "git.julianfamily.org/keepassgo/internal/appui/header" headerlayout "git.julianfamily.org/keepassgo/internal/appui/header/layout" - "git.julianfamily.org/keepassgo/internal/appui/platform" ) func (u *ui) header(gtx layout.Context) layout.Dimensions { @@ -185,47 +184,33 @@ func (u *ui) topRightActionOrder() []string { } func (u *ui) phoneHeaderMenus(gtx layout.Context) layout.Dimensions { - if u.debugLogHeaderBounds { - platform.LogInfo("KeePassGO", fmt.Sprintf( - "keepassgo phone-header-menus compact=%t syncVisible=%t syncOpen=%t mainVisible=%t mainOpen=%t syncCall=%t mainCall=%t max=%dx%d", - u.usesCompactViewport(), - u.phoneSyncMenuVisible, - u.syncMenuOpen, - u.phoneMainMenuVisible, - u.mainMenuOpen, - u.phoneSyncMenuCall != (op.CallOp{}), - u.phoneMainMenuCall != (op.CallOp{}), - gtx.Constraints.Max.X, - gtx.Constraints.Max.Y, - )) - } if !u.usesCompactViewport() || (!u.syncMenuVisibleOnPhone() && !u.mainMenuVisibleOnPhone()) { return layout.Dimensions{} } if u.syncMenuVisibleOnPhone() { - stack := op.Offset(image.Pt(u.frameInsetPx, u.phoneSyncMenuOrigin.Y)).Push(gtx.Ops) - menuGTX := gtx - menuGTX.Constraints.Min = image.Point{} - menuGTX.Constraints.Max.X = max(0, gtx.Constraints.Max.X-(u.frameInsetPx*2)) - layout.E.Layout(menuGTX, func(gtx layout.Context) layout.Dimensions { - u.phoneSyncMenuCall.Add(gtx.Ops) - return layout.Dimensions{Size: u.phoneSyncMenuSize} + return layout.UniformInset(unit.Dp(16)).Layout(gtx, func(gtx layout.Context) layout.Dimensions { + stack := op.Offset(image.Pt(0, max(0, u.phoneSyncMenuOrigin.Y-u.frameInsetPx))).Push(gtx.Ops) + defer stack.Pop() + fullWidthGTX := gtx + fullWidthGTX.Constraints.Min = image.Point{} + fullWidthGTX.Constraints.Min.X = fullWidthGTX.Constraints.Max.X + dims := layout.E.Layout(fullWidthGTX, u.syncMenu) + return layout.Dimensions{Size: image.Pt(fullWidthGTX.Constraints.Max.X, max(dims.Size.Y, u.phoneSyncMenuOrigin.Y))} }) - stack.Pop() } if u.mainMenuVisibleOnPhone() { - stack := op.Offset(image.Pt(u.frameInsetPx, u.phoneMainMenuOrigin.Y)).Push(gtx.Ops) - menuGTX := gtx - menuGTX.Constraints.Min = image.Point{} - menuGTX.Constraints.Max.X = max(0, gtx.Constraints.Max.X-(u.frameInsetPx*2)) - layout.E.Layout(menuGTX, func(gtx layout.Context) layout.Dimensions { - u.phoneMainMenuCall.Add(gtx.Ops) - return layout.Dimensions{Size: u.phoneMainMenuSize} + return layout.UniformInset(unit.Dp(16)).Layout(gtx, func(gtx layout.Context) layout.Dimensions { + stack := op.Offset(image.Pt(0, max(0, u.phoneMainMenuOrigin.Y-u.frameInsetPx))).Push(gtx.Ops) + defer stack.Pop() + fullWidthGTX := gtx + fullWidthGTX.Constraints.Min = image.Point{} + fullWidthGTX.Constraints.Min.X = fullWidthGTX.Constraints.Max.X + dims := layout.E.Layout(fullWidthGTX, u.mainMenu) + return layout.Dimensions{Size: image.Pt(fullWidthGTX.Constraints.Max.X, max(dims.Size.Y, u.phoneMainMenuOrigin.Y))} }) - stack.Pop() } - return layout.Dimensions{Size: gtx.Constraints.Max} + return layout.Dimensions{} } func (u *ui) syncMenuVisibleOnPhone() bool { @@ -306,7 +291,7 @@ func (u *ui) mainMenu(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.openSecuritySettings, "Settings") }, } - return headerview.MainMenu(gtx, u.theme, rows, compactCard) + return headerview.MainMenu(gtx, u.theme, rows, compactCard, nil) } func (u *ui) mainMenuButtonGroup(gtx layout.Context) layout.Dimensions { @@ -318,7 +303,7 @@ func (u *ui) mainMenuButtonGroup(gtx layout.Context) layout.Dimensions { } func intrinsicCompactCard(gtx layout.Context, w layout.Widget) layout.Dimensions { - return headerlayout.IntrinsicCompactCard(gtx, w, compactCard) + return headerlayout.IntrinsicCompactCard(gtx, w, compactCard, nil) } func menuActionWidth(gtx layout.Context, rows []layout.Widget) int { diff --git a/internal/appui/header/layout/menu.go b/internal/appui/header/layout/menu.go index baaee43..2cae01b 100644 --- a/internal/appui/header/layout/menu.go +++ b/internal/appui/header/layout/menu.go @@ -8,13 +8,16 @@ import ( "gioui.org/unit" ) -func IntrinsicCompactCard(gtx layout.Context, w layout.Widget, card func(layout.Context, layout.Widget) layout.Dimensions) layout.Dimensions { +func IntrinsicCompactCard(gtx layout.Context, w layout.Widget, card func(layout.Context, layout.Widget) layout.Dimensions, logger func(name string, constraints layout.Constraints, dims layout.Dimensions)) layout.Dimensions { measureGTX := gtx measureGTX.Constraints.Min = image.Point{} measureGTX.Constraints.Max.X = gtx.Constraints.Max.X macro := op.Record(gtx.Ops) contentDims := w(measureGTX) _ = macro.Stop() + if logger != nil { + logger("intrinsic-measure", measureGTX.Constraints, contentDims) + } width := contentDims.Size.X + gtx.Dp(unit.Dp(20)) maxWidth := gtx.Constraints.Max.X if maxWidth > 0 && width > maxWidth { @@ -24,7 +27,11 @@ func IntrinsicCompactCard(gtx layout.Context, w layout.Widget, card func(layout. gtx.Constraints.Min.X = width gtx.Constraints.Max.X = width } - return card(gtx, w) + dims := card(gtx, w) + if logger != nil { + logger("intrinsic-card", gtx.Constraints, dims) + } + return dims } func MenuActionWidth(gtx layout.Context, rows []layout.Widget) int { diff --git a/internal/appui/header/menu.go b/internal/appui/header/menu.go index 2716dbb..14f9f8f 100644 --- a/internal/appui/header/menu.go +++ b/internal/appui/header/menu.go @@ -2,6 +2,7 @@ package header import ( "image/color" + "image" "gioui.org/layout" "gioui.org/unit" @@ -10,9 +11,12 @@ import ( headerlayout "git.julianfamily.org/keepassgo/internal/appui/header/layout" ) -func MainMenu(gtx layout.Context, theme *material.Theme, rows []layout.Widget, card func(layout.Context, layout.Widget) layout.Dimensions) layout.Dimensions { +func MainMenu(gtx layout.Context, theme *material.Theme, rows []layout.Widget, card func(layout.Context, layout.Widget) layout.Dimensions, logger func(name string, constraints layout.Constraints, dims layout.Dimensions)) layout.Dimensions { rowWidth := headerlayout.MenuActionWidth(gtx, rows) - return headerlayout.IntrinsicCompactCard(gtx, func(gtx layout.Context) layout.Dimensions { + if logger != nil { + logger("row-width", gtx.Constraints, layout.Dimensions{Size: image.Pt(rowWidth, 0)}) + } + dims := headerlayout.IntrinsicCompactCard(gtx, func(gtx layout.Context) layout.Dimensions { children := make([]layout.FlexChild, 0, (len(rows)*2)-1) for i, row := range rows { if i > 0 { @@ -23,8 +27,16 @@ func MainMenu(gtx layout.Context, theme *material.Theme, rows []layout.Widget, c return headerlayout.RightAlignedAction(gtx, rowWidth, current) })) } - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, children...) - }, card) + dims := layout.Flex{Axis: layout.Vertical}.Layout(gtx, children...) + if logger != nil { + logger("rows", gtx.Constraints, dims) + } + return dims + }, card, logger) + if logger != nil { + logger("card", gtx.Constraints, dims) + } + return dims } func MainMenuButtonGroup(gtx layout.Context, theme *material.Theme, click *widget.Clickable, icon *widget.Icon, open bool, selectedColor, accentColor color.NRGBA) layout.Dimensions { diff --git a/internal/appui/header_sync_menu.go b/internal/appui/header_sync_menu.go index e289188..22b7ed1 100644 --- a/internal/appui/header_sync_menu.go +++ b/internal/appui/header_sync_menu.go @@ -1,11 +1,13 @@ package appui import ( + "image" "image/color" "runtime" "strings" "gioui.org/layout" + "gioui.org/op" "gioui.org/unit" "gioui.org/widget" "gioui.org/widget/material" @@ -77,9 +79,44 @@ func (u *ui) syncMenu(gtx layout.Context) layout.Dimensions { } actionRows := u.syncMenuActionRows(model) actionWidth := menuActionWidth(gtx, actionRows) - return intrinsicCompactCard(gtx, func(gtx layout.Context) layout.Dimensions { - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, u.syncMenuRows(model, profiles, credentials, actionWidth)...) - }) + menu := func(gtx layout.Context) layout.Dimensions { + return intrinsicCompactCard(gtx, func(gtx layout.Context) layout.Dimensions { + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, u.syncMenuRows(model, profiles, credentials, actionWidth)...) + }) + } + reserveWidth := u.syncMenuTrailingReserveWidth(gtx) + if reserveWidth <= 0 { + return menu(gtx) + } + return layout.Flex{}.Layout(gtx, + layout.Rigid(menu), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return layout.Dimensions{Size: image.Pt(reserveWidth, 0)} + }), + ) +} + +func (u *ui) syncMenuTrailingReserveWidth(gtx layout.Context) int { + spacing := gtx.Dp(unit.Dp(8)) + if u.usesCompactViewport() { + spacing = gtx.Dp(unit.Dp(8)) + } + + measureGTX := gtx + measureGTX.Constraints.Min = image.Point{} + + lockOps := op.Record(gtx.Ops) + lockDims := func(gtx layout.Context) layout.Dimensions { + btn := material.Button(u.theme, &u.lockVault, "Lock") + return btn.Layout(gtx) + }(measureGTX) + _ = lockOps.Stop() + + menuOps := op.Record(gtx.Ops) + menuDims := u.mainMenuButtonGroup(measureGTX) + _ = menuOps.Stop() + + return spacing + lockDims.Size.X + spacing + menuDims.Size.X } func (u *ui) syncMenuActionRows(model syncmodel.MenuModel) []layout.Widget { @@ -126,14 +163,7 @@ func (u *ui) syncMenuRows(model syncmodel.MenuModel, profiles []vault.RemoteProf } func (u *ui) syncMenuPrimaryRows(model syncmodel.MenuModel, actionWidth int) []layout.FlexChild { - rows := []layout.FlexChild{ - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - lbl := material.Label(u.theme, unit.Sp(11), "Need another source or direction?") - lbl.Color = mutedColor - return lbl.Layout(gtx) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), - } + rows := []layout.FlexChild{} if model.ShowShare { rows = append(rows, layout.Rigid(func(gtx layout.Context) layout.Dimensions { return layout.Flex{Axis: layout.Vertical}.Layout(gtx,