Compare commits

...

6 Commits

Author SHA1 Message Date
joejulian a9c15c2d23 Merge pull request 'Local-first remote sync and cross-platform UI parity' (#2) from feature/local-first-remote-sync into main
ci / lint-test (push) Successful in 1m13s
ci / build (push) Successful in 2m34s
2026-04-11 06:15:41 +00:00
Joe Julian b7d6dbdc97 Keep desktop detail pane visible 2026-04-10 23:09:47 -07:00
Joe Julian 2deca549f5 Fix desktop header menu layout 2026-04-10 23:05:30 -07:00
Joe Julian fe3c07e3dd Unify action menus and collapse empty detail pane 2026-04-10 22:12:50 -07:00
Joe Julian c4f110e0ad Refine compact header menus 2026-04-10 21:48:05 -07:00
Joe Julian 56a0711860 Right-align compact header menus 2026-04-10 19:23:13 -07:00
6 changed files with 183 additions and 125 deletions
+15 -35
View File
@@ -489,10 +489,10 @@ type ui struct {
deleteGroupPath []string deleteGroupPath []string
apiPolicyGroupScope bool apiPolicyGroupScope bool
apiTokenSecret string apiTokenSecret string
phoneSyncMenuAnchor image.Point
phoneMainMenuAnchor image.Point
phoneSyncMenuOrigin image.Point phoneSyncMenuOrigin image.Point
phoneMainMenuOrigin image.Point phoneMainMenuOrigin image.Point
phoneSyncMenuSize image.Point
phoneMainMenuSize image.Point
phoneSyncMenuCall op.CallOp phoneSyncMenuCall op.CallOp
phoneMainMenuCall op.CallOp phoneMainMenuCall op.CallOp
phoneSyncMenuVisible bool phoneSyncMenuVisible bool
@@ -1850,8 +1850,17 @@ func (u *ui) navigationHeaderLabel() string {
func (u *ui) entryRow(gtx layout.Context, click *widget.Clickable, idx int, item entry) layout.Dimensions { func (u *ui) entryRow(gtx layout.Context, click *widget.Clickable, idx int, item entry) layout.Dimensions {
for click.Clicked(gtx) { for click.Clicked(gtx) {
_ = u.state.ToggleVisibleIndex(idx) if !u.shouldShowDetailPane() {
if idx >= 0 && idx < len(u.visible) {
u.state.SelectedEntryID = u.visible[idx].ID
}
} else {
_ = u.state.ToggleVisibleIndex(idx)
}
u.loadSelectedEntryIntoEditor() u.loadSelectedEntryIntoEditor()
if u.invalidate != nil {
u.invalidate()
}
} }
return click.Layout(gtx, func(gtx layout.Context) layout.Dimensions { return click.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
selected := item.ID == u.state.SelectedEntryID selected := item.ID == u.state.SelectedEntryID
@@ -2014,21 +2023,8 @@ func (u *ui) detailPanel(gtx layout.Context) layout.Dimensions {
return panel(gtx, func(gtx layout.Context) layout.Dimensions { return panel(gtx, func(gtx layout.Context) layout.Dimensions {
if u.shouldShowDesktopWorkingHeader() { if u.shouldShowDesktopWorkingHeader() {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(u.headerActions),
return layout.Flex{Alignment: layout.Middle, Spacing: layout.SpaceStart}.Layout(gtx, layout.Rigid(u.desktopHeaderMenus),
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
return layout.Dimensions{}
}),
layout.Rigid(u.syncButtonGroup),
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")
return btn.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
layout.Rigid(u.mainMenuButtonGroup),
)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout), layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout),
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
return u.detailPanelContent(gtx) return u.detailPanelContent(gtx)
@@ -2049,7 +2045,7 @@ func (u *ui) detailPanelContent(gtx layout.Context) layout.Dimensions {
panel := u.staticDetailPanel() panel := u.staticDetailPanel()
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, panel...) return layout.Flex{Axis: layout.Vertical}.Layout(gtx, panel...)
case detaillayout.ModeEmpty: case detaillayout.ModeEmpty:
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, u.emptyDetailChildren()...) return layout.Dimensions{}
} }
item, ok := u.selectedEntry() item, ok := u.selectedEntry()
if mode == detaillayout.ModeEditor { if mode == detaillayout.ModeEditor {
@@ -2089,22 +2085,6 @@ func (u *ui) staticDetailPanel() []layout.FlexChild {
} }
} }
func (u *ui) emptyDetailChildren() []layout.FlexChild {
return []layout.FlexChild{
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(18), "Entry details")
lbl.Color = accentColor
return lbl.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(16), u.detailPlaceholderMessage())
lbl.Color = mutedColor
return lbl.Layout(gtx)
}),
}
}
func (u *ui) detailEditorContent(gtx layout.Context, hasSelected bool) layout.Dimensions { func (u *ui) detailEditorContent(gtx layout.Context, hasSelected bool) layout.Dimensions {
rows := []layout.Widget{ rows := []layout.Widget{
func(gtx layout.Context) layout.Dimensions { func(gtx layout.Context) layout.Dimensions {
+20 -3
View File
@@ -20,6 +20,7 @@ import (
"git.julianfamily.org/keepassgo/internal/apiaudit" "git.julianfamily.org/keepassgo/internal/apiaudit"
"git.julianfamily.org/keepassgo/internal/apitokens" "git.julianfamily.org/keepassgo/internal/apitokens"
"git.julianfamily.org/keepassgo/internal/appstate" "git.julianfamily.org/keepassgo/internal/appstate"
detaillayout "git.julianfamily.org/keepassgo/internal/appui/detail/layout"
"git.julianfamily.org/keepassgo/internal/clipboard" "git.julianfamily.org/keepassgo/internal/clipboard"
"git.julianfamily.org/keepassgo/internal/session" "git.julianfamily.org/keepassgo/internal/session"
) )
@@ -593,6 +594,8 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
u.phoneMainMenuVisible = false u.phoneMainMenuVisible = false
u.phoneSyncMenuOrigin = image.Point{} u.phoneSyncMenuOrigin = image.Point{}
u.phoneMainMenuOrigin = image.Point{} u.phoneMainMenuOrigin = image.Point{}
u.phoneSyncMenuSize = image.Point{}
u.phoneMainMenuSize = image.Point{}
u.phoneSyncMenuCall = op.CallOp{} u.phoneSyncMenuCall = op.CallOp{}
u.phoneMainMenuCall = op.CallOp{} u.phoneMainMenuCall = op.CallOp{}
u.syncHostedAPI() u.syncHostedAPI()
@@ -620,9 +623,7 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
layout.Stacked(u.securityDialogOverlay), layout.Stacked(u.securityDialogOverlay),
layout.Stacked(u.remotePrefsDialogOverlay), layout.Stacked(u.remotePrefsDialogOverlay),
layout.Stacked(u.approvalDialogOverlay), layout.Stacked(u.approvalDialogOverlay),
layout.Expanded(func(gtx layout.Context) layout.Dimensions { layout.Expanded(u.phoneHeaderMenus),
return u.phoneHeaderMenus(gtx)
}),
layout.Stacked(u.statusToast), layout.Stacked(u.statusToast),
) )
} }
@@ -1310,6 +1311,11 @@ func (u *ui) primaryContent(gtx layout.Context) layout.Dimensions {
return u.lifecycleScreen(gtx) return u.lifecycleScreen(gtx)
case u.shouldUseLockedSinglePane(): case u.shouldUseLockedSinglePane():
return u.detailPanel(gtx) return u.detailPanel(gtx)
case !u.shouldShowDetailPane():
if u.usesCompactViewport() {
return u.listPanel(gtx)
}
return u.widePrimaryContent(gtx)
case u.usesCompactViewport(): case u.usesCompactViewport():
return u.compactPrimaryContent(gtx) return u.compactPrimaryContent(gtx)
default: default:
@@ -1317,6 +1323,17 @@ func (u *ui) primaryContent(gtx layout.Context) layout.Dimensions {
} }
} }
func (u *ui) shouldShowDetailPane() bool {
_, hasSelectedEntry := u.selectedEntry()
mode := detaillayout.Resolve(
u.isVaultLocked(),
u.state.Section == appstate.SectionAPITokens || u.state.Section == appstate.SectionAPIAudit || u.state.Section == appstate.SectionAbout,
hasSelectedEntry,
u.editingEntry,
)
return mode != detaillayout.ModeEmpty
}
func (u *ui) compactPrimaryContent(gtx layout.Context) layout.Dimensions { func (u *ui) compactPrimaryContent(gtx layout.Context) layout.Dimensions {
u.phoneSpan = gtx.Constraints.Max.Y u.phoneSpan = gtx.Constraints.Max.Y
listHeight := int(float32(gtx.Constraints.Max.Y) * u.phoneSplit.Value) listHeight := int(float32(gtx.Constraints.Max.Y) * u.phoneSplit.Value)
+82 -65
View File
@@ -12,7 +12,6 @@ import (
"gioui.org/widget/material" "gioui.org/widget/material"
headerview "git.julianfamily.org/keepassgo/internal/appui/header" headerview "git.julianfamily.org/keepassgo/internal/appui/header"
headerlayout "git.julianfamily.org/keepassgo/internal/appui/header/layout" 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 { func (u *ui) header(gtx layout.Context) layout.Dimensions {
@@ -37,58 +36,19 @@ func (u *ui) header(gtx layout.Context) layout.Dimensions {
} }
func (u *ui) headerActions(gtx layout.Context) layout.Dimensions { func (u *ui) headerActions(gtx layout.Context) layout.Dimensions {
if u.shouldShowLifecycleSetup() || u.isVaultLocked() || u.shouldShowDesktopWorkingHeader() { if u.shouldShowLifecycleSetup() || u.isVaultLocked() {
return layout.Dimensions{} return layout.Dimensions{}
} }
cluster := u.newHeaderActionCluster(gtx) cluster := u.newHeaderActionCluster(gtx)
surface := headerlayout.DropdownSurface{ContainerWidth: gtx.Constraints.Max.X, LeftInset: 0, TopInset: 0}
rowDims := cluster.layout(gtx, u) rowDims := cluster.layout(gtx, u)
if u.usesCompactViewport() { if u.usesCompactViewport() {
u.maybeLogHeaderBounds(newHeaderButtonBounds(image.Pt(u.frameInsetPx, u.frameInsetPx), cluster.Metrics.Bounds())) u.maybeLogHeaderBounds(newHeaderButtonBounds(image.Pt(u.frameInsetPx, u.frameInsetPx), cluster.Metrics.Bounds()))
} }
if u.usesCompactViewport() { if u.usesCompactViewport() {
compactSurface := headerlayout.DropdownSurface{ cluster.prepareCompactMenus(gtx, u)
ContainerWidth: gtx.Constraints.Max.X,
LeftInset: u.frameInsetPx,
TopInset: u.frameInsetPx,
}
if u.syncMenuOpen {
u.phoneSyncMenuVisible = true
u.phoneSyncMenuAnchor = cluster.Metrics.SyncAnchor().Point()
u.maybeLogHeaderMenuToggle("sync-visible", true)
placement, menuCall := compactSurface.Place(gtx, cluster.Metrics.SyncAnchor(), u.syncMenu)
u.phoneSyncMenuOrigin = placement.Origin
u.phoneSyncMenuCall = menuCall
u.maybeLogHeaderMenuPlacement("sync-phone", compactSurface, placement)
}
if u.mainMenuOpen {
u.phoneMainMenuVisible = true
u.phoneMainMenuAnchor = cluster.Metrics.MainAnchor().Point()
u.maybeLogHeaderMenuToggle("main-visible", true)
placement, menuCall := compactSurface.Place(gtx, cluster.Metrics.MainAnchor(), u.mainMenu)
u.phoneMainMenuOrigin = placement.Origin
u.phoneMainMenuCall = menuCall
u.maybeLogHeaderMenuPlacement("main-phone", compactSurface, placement)
}
return layout.Dimensions{Size: image.Pt(gtx.Constraints.Max.X, rowDims.Size.Y)} return layout.Dimensions{Size: image.Pt(gtx.Constraints.Max.X, rowDims.Size.Y)}
} }
if cluster.ShowSyncMenu() {
placement, menuCall := surface.Place(gtx, cluster.Metrics.SyncAnchor(), cluster.SyncMenu)
u.maybeLogHeaderMenuPlacement("sync", surface, placement)
stack := op.Offset(placement.Origin).Push(gtx.Ops)
menuCall.Add(gtx.Ops)
stack.Pop()
}
if cluster.ShowMainMenu() {
placement, menuCall := surface.Place(gtx, cluster.Metrics.MainAnchor(), cluster.MainMenu)
u.maybeLogHeaderMenuPlacement("main", surface, placement)
stack := op.Offset(placement.Origin).Push(gtx.Ops)
menuCall.Add(gtx.Ops)
stack.Pop()
}
return rowDims return rowDims
} }
@@ -126,6 +86,17 @@ func (c *headerActionCluster) layout(gtx layout.Context, u *ui) layout.Dimension
}) })
} }
func (c headerActionCluster) activeMenu() layout.Widget {
switch {
case c.ShowSyncMenu():
return c.SyncMenu
case c.ShowMainMenu():
return c.MainMenu
default:
return nil
}
}
func (c *headerActionCluster) layoutRow(gtx layout.Context, u *ui) layout.Dimensions { func (c *headerActionCluster) layoutRow(gtx layout.Context, u *ui) layout.Dimensions {
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx, return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
@@ -146,6 +117,44 @@ func (c *headerActionCluster) layoutRow(gtx layout.Context, u *ui) layout.Dimens
) )
} }
func (c *headerActionCluster) prepareCompactMenus(gtx layout.Context, u *ui) {
compactSurface := headerlayout.DropdownSurface{
ContainerWidth: gtx.Constraints.Max.X,
LeftInset: u.frameInsetPx,
TopInset: u.frameInsetPx,
}
if c.ShowSyncMenu() {
u.phoneSyncMenuVisible = true
u.maybeLogHeaderMenuToggle("sync-visible", true)
placement, menuCall := compactSurface.Place(gtx, c.Metrics.SyncAnchor(), c.SyncMenu)
u.phoneSyncMenuOrigin = placement.Origin
u.phoneSyncMenuSize = placement.Size
u.phoneSyncMenuCall = menuCall
u.maybeLogHeaderMenuPlacement("sync-phone", compactSurface, placement)
}
if c.ShowMainMenu() {
u.phoneMainMenuVisible = true
u.maybeLogHeaderMenuToggle("main-visible", true)
placement, menuCall := compactSurface.Place(gtx, c.Metrics.MainAnchor(), c.MainMenu)
u.phoneMainMenuOrigin = placement.Origin
u.phoneMainMenuSize = placement.Size
u.phoneMainMenuCall = menuCall
u.maybeLogHeaderMenuPlacement("main-phone", compactSurface, placement)
}
}
func (c headerActionCluster) layoutMenuRow(gtx layout.Context) layout.Dimensions {
menu := c.activeMenu()
if menu == nil {
return layout.Dimensions{}
}
fullWidthGTX := gtx
fullWidthGTX.Constraints.Min = image.Point{}
fullWidthGTX.Constraints.Min.X = fullWidthGTX.Constraints.Max.X
dims := layout.E.Layout(fullWidthGTX, menu)
return layout.Dimensions{Size: image.Pt(fullWidthGTX.Constraints.Max.X, dims.Size.Y)}
}
type headerButtonBounds struct { type headerButtonBounds struct {
SyncPrimary image.Rectangle SyncPrimary image.Rectangle
SyncToggle image.Rectangle SyncToggle image.Rectangle
@@ -185,35 +194,43 @@ func (u *ui) topRightActionOrder() []string {
} }
func (u *ui) phoneHeaderMenus(gtx layout.Context) layout.Dimensions { 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()) { if !u.usesCompactViewport() || (!u.syncMenuVisibleOnPhone() && !u.mainMenuVisibleOnPhone()) {
return layout.Dimensions{} return layout.Dimensions{}
} }
cluster := u.newHeaderActionCluster(gtx)
if u.syncMenuVisibleOnPhone() { if u.syncMenuVisibleOnPhone() {
stack := op.Offset(u.phoneSyncMenuOrigin).Push(gtx.Ops) return layout.UniformInset(unit.Dp(16)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
u.phoneSyncMenuCall.Add(gtx.Ops) stack := op.Offset(image.Pt(0, max(0, u.phoneSyncMenuOrigin.Y-u.frameInsetPx))).Push(gtx.Ops)
stack.Pop() defer stack.Pop()
dims := cluster.layoutMenuRow(gtx)
return layout.Dimensions{Size: image.Pt(gtx.Constraints.Max.X, max(dims.Size.Y, u.phoneSyncMenuOrigin.Y))}
})
} }
if u.mainMenuVisibleOnPhone() { if u.mainMenuVisibleOnPhone() {
stack := op.Offset(u.phoneMainMenuOrigin).Push(gtx.Ops) return layout.UniformInset(unit.Dp(16)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
u.phoneMainMenuCall.Add(gtx.Ops) stack := op.Offset(image.Pt(0, max(0, u.phoneMainMenuOrigin.Y-u.frameInsetPx))).Push(gtx.Ops)
stack.Pop() defer stack.Pop()
dims := cluster.layoutMenuRow(gtx)
return layout.Dimensions{Size: image.Pt(gtx.Constraints.Max.X, max(dims.Size.Y, u.phoneMainMenuOrigin.Y))}
})
} }
return layout.Dimensions{Size: gtx.Constraints.Max} return layout.Dimensions{}
}
func (u *ui) desktopHeaderMenus(gtx layout.Context) layout.Dimensions {
if u.usesCompactViewport() || (!u.syncMenuOpen && !u.mainMenuOpen) {
return layout.Dimensions{}
}
cluster := u.newHeaderActionCluster(gtx)
dims := cluster.layoutMenuRow(gtx)
if dims.Size.Y == 0 {
return dims
}
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { return dims }),
)
} }
func (u *ui) syncMenuVisibleOnPhone() bool { func (u *ui) syncMenuVisibleOnPhone() bool {
@@ -294,7 +311,7 @@ func (u *ui) mainMenu(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.openSecuritySettings, "Settings") 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 { func (u *ui) mainMenuButtonGroup(gtx layout.Context) layout.Dimensions {
@@ -306,7 +323,7 @@ func (u *ui) mainMenuButtonGroup(gtx layout.Context) layout.Dimensions {
} }
func intrinsicCompactCard(gtx layout.Context, w layout.Widget) 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 { func menuActionWidth(gtx layout.Context, rows []layout.Widget) int {
+9 -2
View File
@@ -8,13 +8,16 @@ import (
"gioui.org/unit" "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 := gtx
measureGTX.Constraints.Min = image.Point{} measureGTX.Constraints.Min = image.Point{}
measureGTX.Constraints.Max.X = gtx.Constraints.Max.X measureGTX.Constraints.Max.X = gtx.Constraints.Max.X
macro := op.Record(gtx.Ops) macro := op.Record(gtx.Ops)
contentDims := w(measureGTX) contentDims := w(measureGTX)
_ = macro.Stop() _ = macro.Stop()
if logger != nil {
logger("intrinsic-measure", measureGTX.Constraints, contentDims)
}
width := contentDims.Size.X + gtx.Dp(unit.Dp(20)) width := contentDims.Size.X + gtx.Dp(unit.Dp(20))
maxWidth := gtx.Constraints.Max.X maxWidth := gtx.Constraints.Max.X
if maxWidth > 0 && width > maxWidth { 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.Min.X = width
gtx.Constraints.Max.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 { func MenuActionWidth(gtx layout.Context, rows []layout.Widget) int {
+16 -4
View File
@@ -2,6 +2,7 @@ package header
import ( import (
"image/color" "image/color"
"image"
"gioui.org/layout" "gioui.org/layout"
"gioui.org/unit" "gioui.org/unit"
@@ -10,9 +11,12 @@ import (
headerlayout "git.julianfamily.org/keepassgo/internal/appui/header/layout" 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) 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) children := make([]layout.FlexChild, 0, (len(rows)*2)-1)
for i, row := range rows { for i, row := range rows {
if i > 0 { 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 headerlayout.RightAlignedAction(gtx, rowWidth, current)
})) }))
} }
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, children...) dims := layout.Flex{Axis: layout.Vertical}.Layout(gtx, children...)
}, card) 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 { func MainMenuButtonGroup(gtx layout.Context, theme *material.Theme, click *widget.Clickable, icon *widget.Icon, open bool, selectedColor, accentColor color.NRGBA) layout.Dimensions {
+41 -16
View File
@@ -1,11 +1,13 @@
package appui package appui
import ( import (
"image"
"image/color" "image/color"
"runtime" "runtime"
"strings" "strings"
"gioui.org/layout" "gioui.org/layout"
"gioui.org/op"
"gioui.org/unit" "gioui.org/unit"
"gioui.org/widget" "gioui.org/widget"
"gioui.org/widget/material" "gioui.org/widget/material"
@@ -14,11 +16,6 @@ import (
"git.julianfamily.org/keepassgo/internal/vault" "git.julianfamily.org/keepassgo/internal/vault"
) )
func (u *ui) syncButtonGroup(gtx layout.Context) layout.Dimensions {
group, _, _ := u.syncButtonGroupWithMetrics(gtx)
return group
}
func (u *ui) syncButtonGroupWithMetrics(gtx layout.Context) (layout.Dimensions, layout.Dimensions, layout.Dimensions) { func (u *ui) syncButtonGroupWithMetrics(gtx layout.Context) (layout.Dimensions, layout.Dimensions, layout.Dimensions) {
spacing := unit.Dp(4) spacing := unit.Dp(4)
if u.usesCompactViewport() { if u.usesCompactViewport() {
@@ -77,9 +74,44 @@ func (u *ui) syncMenu(gtx layout.Context) layout.Dimensions {
} }
actionRows := u.syncMenuActionRows(model) actionRows := u.syncMenuActionRows(model)
actionWidth := menuActionWidth(gtx, actionRows) actionWidth := menuActionWidth(gtx, actionRows)
return intrinsicCompactCard(gtx, func(gtx layout.Context) layout.Dimensions { menu := func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, u.syncMenuRows(model, profiles, credentials, actionWidth)...) 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 { func (u *ui) syncMenuActionRows(model syncmodel.MenuModel) []layout.Widget {
@@ -126,14 +158,7 @@ func (u *ui) syncMenuRows(model syncmodel.MenuModel, profiles []vault.RemoteProf
} }
func (u *ui) syncMenuPrimaryRows(model syncmodel.MenuModel, actionWidth int) []layout.FlexChild { func (u *ui) syncMenuPrimaryRows(model syncmodel.MenuModel, actionWidth int) []layout.FlexChild {
rows := []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),
}
if model.ShowShare { if model.ShowShare {
rows = append(rows, layout.Rigid(func(gtx layout.Context) layout.Dimensions { rows = append(rows, layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, return layout.Flex{Axis: layout.Vertical}.Layout(gtx,