From 9660369851bf6f40c8c09372f316472deb5bafe5 Mon Sep 17 00:00:00 2001 From: Joe Julian Date: Wed, 8 Apr 2026 23:23:47 -0700 Subject: [PATCH] Extract header and menu rendering --- main.go | 543 ------------------------------------------ ui_layout_header.go | 557 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 557 insertions(+), 543 deletions(-) create mode 100644 ui_layout_header.go diff --git a/main.go b/main.go index dcc63c8..fd92f4e 100644 --- a/main.go +++ b/main.go @@ -5279,146 +5279,6 @@ func approvalFact(theme *material.Theme, title, primary, secondary string) layou } } -func (u *ui) header(gtx layout.Context) layout.Dimensions { - if u.mode == "phone" { - if u.shouldShowLifecycleSetup() || u.isVaultLocked() { - return layout.Dimensions{} - } - gtx.Constraints.Min.X = gtx.Constraints.Max.X - return u.headerActions(gtx) - } - if u.shouldShowDesktopWorkingHeader() { - return layout.Dimensions{} - } - return card(gtx, func(gtx layout.Context) layout.Dimensions { - return layout.Flex{Alignment: layout.Middle}.Layout(gtx, - layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { - return u.brandMark(gtx, 196, 56) - }), - layout.Rigid(u.headerActions), - ) - }) -} - -func (u *ui) headerActions(gtx layout.Context) layout.Dimensions { - if u.shouldShowLifecycleSetup() { - return layout.Dimensions{} - } - if u.isVaultLocked() { - return layout.Dimensions{} - } - if u.shouldShowDesktopWorkingHeader() { - return layout.Dimensions{} - } - spacing := gtx.Dp(unit.Dp(8)) - 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 { - 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") - 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 { - metrics.MainDims = u.mainMenuButtonGroup(gtx) - return metrics.MainDims - }), - ) - } - - rowOps := op.Record(gtx.Ops) - metrics.RowDims = row(gtx) - rowCall := rowOps.Stop() - - if u.mode == "phone" { - metrics.RowOriginX = max(0, gtx.Constraints.Max.X-metrics.RowDims.Size.X) - } - - surface := dropdownSurface{ContainerWidth: gtx.Constraints.Max.X, LeftInset: 0, TopInset: 0} - - 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 = metrics.syncAnchor().point() - } - if u.mainMenuOpen { - u.phoneMainMenuVisible = true - u.phoneMainMenuAnchor = metrics.mainAnchor().point() - } - width := gtx.Constraints.Max.X - return layout.Dimensions{Size: image.Pt(width, metrics.RowDims.Size.Y)} - } - - if u.syncMenuOpen { - surface.draw(gtx, metrics.syncAnchor(), u.syncMenu) - } - if u.mainMenuOpen { - surface.draw(gtx, metrics.mainAnchor(), u.mainMenu) - } - - 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 { - rows := []layout.Widget{ - func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.showEntries, "Entries") - }, - func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.showRecycle, "Recycle Bin") - }, - func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.showAPITokens, "API Tokens") - }, - func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.showAPIAudit, "API Audit") - }, - func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.showAbout, "About") }, - func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.openSecuritySettings, "Settings") - }, - } - rowWidth := menuActionWidth(gtx, rows) - return intrinsicCompactCard(gtx, func(gtx layout.Context) layout.Dimensions { - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return rightAlignedMenuAction(gtx, rowWidth, rows[0]) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return rightAlignedMenuAction(gtx, rowWidth, rows[1]) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return rightAlignedMenuAction(gtx, rowWidth, rows[2]) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return rightAlignedMenuAction(gtx, rowWidth, rows[3]) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return rightAlignedMenuAction(gtx, rowWidth, rows[4]) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return rightAlignedMenuAction(gtx, rowWidth, rows[5]) - }), - ) - }) -} - func (u *ui) aboutDetailPanel(gtx layout.Context) layout.Dimensions { rows := []layout.Widget{ func(gtx layout.Context) layout.Dimensions { @@ -5483,290 +5343,6 @@ func aboutFact(theme *material.Theme, title, primary, secondary string) layout.W } } -func (u *ui) syncButtonGroup(gtx layout.Context) layout.Dimensions { - label := "Sync" - spacing := unit.Dp(4) - if u.mode == "phone" { - spacing = unit.Dp(3) - } - row := func(gtx layout.Context) layout.Dimensions { - return layout.Flex{Alignment: layout.Middle}.Layout(gtx, - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return syncPrimaryButton(gtx, u.theme, &u.synchronizeVault, label, u.mode == "phone") - }), - layout.Rigid(layout.Spacer{Width: spacing}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return u.syncMenuToggle(gtx) - }), - ) - } - return row(gtx) -} - -func (u *ui) syncMenuToggle(gtx layout.Context) layout.Dimensions { - btn := material.IconButton(u.theme, &u.toggleSyncMenu, u.chevronDownIcon, "More synchronize actions") - if u.syncMenuOpen { - btn.Background = accentColor - btn.Color = color.NRGBA{R: 255, G: 252, B: 247, A: 255} - } else { - btn.Background = color.NRGBA{R: 231, G: 236, B: 232, A: 255} - btn.Color = accentColor - } - btn.Size = unit.Dp(18) - btn.Inset = layout.UniformInset(unit.Dp(8)) - if u.mode == "phone" { - btn.Size = unit.Dp(16) - btn.Inset = layout.UniformInset(unit.Dp(7)) - } - return btn.Layout(gtx) -} - -func (u *ui) syncMenu(gtx layout.Context) layout.Dimensions { - profiles := u.availableRemoteProfiles() - credentials := u.availableRemoteCredentialEntries() - if len(u.vaultRemoteProfileClicks) < len(profiles) { - u.vaultRemoteProfileClicks = make([]widget.Clickable, len(profiles)) - } - if len(u.vaultRemoteCredentialClicks) < len(credentials) { - u.vaultRemoteCredentialClicks = make([]widget.Clickable, len(credentials)) - } - actionRows := []layout.Widget{ - func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.openAdvancedSync, "Open Advanced Sync") - }, - } - if supportsVaultShare(runtime.GOOS) && u.vaultSharer != nil && strings.TrimSpace(u.currentShareableVaultPath()) != "" { - actionRows = append(actionRows, func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.shareCurrentVault, "Share Vault") - }) - } - if u.shouldShowRemoteSyncSetupShortcut() { - actionRows = append(actionRows, func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.useSavedAdvancedSyncRemote, u.remoteSyncSetupShortcutLabel()) - }) - } - if u.shouldShowDirectRemoteSyncShortcut() { - actionRows = append(actionRows, func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.openSelectedVaultRemote, u.directRemoteSyncShortcutLabel()) - }) - } - if u.shouldShowRemoteSyncSettingsShortcut() { - actionRows = append(actionRows, func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.useSavedAdvancedSyncRemote, u.remoteSyncSettingsShortcutLabel()) - }) - } - if u.shouldShowRemoveRemoteSyncShortcut() { - actionRows = append(actionRows, func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.removeSelectedRemoteBinding, u.removeRemoteSyncShortcutLabel()) - }) - } - actionWidth := menuActionWidth(gtx, actionRows) - return intrinsicCompactCard(gtx, func(gtx layout.Context) layout.Dimensions { - 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), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if !supportsVaultShare(runtime.GOOS) || u.vaultSharer == nil || strings.TrimSpace(u.currentShareableVaultPath()) == "" { - return layout.Dimensions{} - } - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return rightAlignedMenuAction(gtx, actionWidth, func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.shareCurrentVault, "Share Vault") - }) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), - ) - }), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return rightAlignedMenuAction(gtx, actionWidth, func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.openAdvancedSync, "Open Advanced Sync") - }) - }), - } - if u.shouldShowRemoteSyncSetupShortcut() { - rows = append(rows, - layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return rightAlignedMenuAction(gtx, actionWidth, func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.useSavedAdvancedSyncRemote, u.remoteSyncSetupShortcutLabel()) - }) - }), - ) - } - if u.shouldShowDirectRemoteSyncShortcut() { - rows = append(rows, - layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return rightAlignedMenuAction(gtx, actionWidth, func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.openSelectedVaultRemote, u.directRemoteSyncShortcutLabel()) - }) - }), - ) - } - if u.shouldShowRemoteSyncSettingsShortcut() { - rows = append(rows, - layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return rightAlignedMenuAction(gtx, actionWidth, func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.useSavedAdvancedSyncRemote, u.remoteSyncSettingsShortcutLabel()) - }) - }), - ) - } - if u.shouldShowRemoveRemoteSyncShortcut() { - rows = append(rows, - layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return rightAlignedMenuAction(gtx, actionWidth, func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.removeSelectedRemoteBinding, u.removeRemoteSyncShortcutLabel()) - }) - }), - ) - } - if u.hasOpenVault() && len(profiles) > 0 && len(credentials) > 0 { - rows = append(rows, - layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - lbl := material.Label(u.theme, unit.Sp(11), u.savedRemoteBindingHeading()) - lbl.Color = mutedColor - return lbl.Layout(gtx) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), - ) - if !u.shouldShowSavedRemoteBindingSelectors() { - rows = append(rows, - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - profileLabel, credentialLabel, syncLabel, ok := u.savedRemoteBindingSummary() - if !ok { - return layout.Dimensions{} - } - return layout.Background{}.Layout(gtx, fill(color.NRGBA{R: 242, G: 245, B: 240, A: 255}), func(gtx layout.Context) layout.Dimensions { - return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions { - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - lbl := material.Label(u.theme, unit.Sp(13), profileLabel) - lbl.Color = accentColor - return lbl.Layout(gtx) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - lbl := material.Label(u.theme, unit.Sp(12), "Credential: "+credentialLabel) - lbl.Color = mutedColor - return lbl.Layout(gtx) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - lbl := material.Label(u.theme, unit.Sp(12), syncLabel) - lbl.Color = mutedColor - return lbl.Layout(gtx) - }), - ) - }) - }) - }), - ) - } else { - for i, profile := range profiles { - i := i - profile := profile - rows = append(rows, - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - selected := strings.TrimSpace(u.selectedVaultRemoteProfileID) == profile.ID - return layout.Inset{Bottom: unit.Dp(4)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions { - return recentSelectionCard(gtx, selected, func(gtx layout.Context) layout.Dimensions { - return u.vaultRemoteProfileClicks[i].Layout(gtx, func(gtx layout.Context) layout.Dimensions { - lbl := material.Label(u.theme, unit.Sp(13), profile.Name) - lbl.Color = accentColor - return lbl.Layout(gtx) - }) - }) - }) - }), - ) - } - rows = append(rows, layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout)) - for i, entry := range credentials { - i := i - entry := entry - rows = append(rows, - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - selected := strings.TrimSpace(u.selectedVaultRemoteCredentialEntryID) == entry.ID - label := entry.Title - if strings.TrimSpace(entry.Username) != "" { - label += " · " + strings.TrimSpace(entry.Username) - } - return layout.Inset{Bottom: unit.Dp(4)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions { - return recentSelectionCard(gtx, selected, func(gtx layout.Context) layout.Dimensions { - return u.vaultRemoteCredentialClicks[i].Layout(gtx, func(gtx layout.Context) layout.Dimensions { - lbl := material.Label(u.theme, unit.Sp(13), label) - lbl.Color = accentColor - return lbl.Layout(gtx) - }) - }) - }) - }), - ) - } - } - if _, ok := u.selectedVaultRemoteProfile(); ok { - if _, ok := u.selectedVaultRemoteCredentialEntry(); ok { - rows = append(rows, - layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.openSelectedVaultRemote, u.openSelectedVaultRemoteButtonLabel()) - }), - ) - } - } - } - if u.hasOpenVault() { - baseURL := strings.TrimSpace(u.remoteBaseURL.Text()) - remotePath := strings.TrimSpace(u.remotePath.Text()) - username := strings.TrimSpace(u.remoteUsername.Text()) - password := u.remotePassword.Text() - if baseURL != "" && remotePath != "" && username != "" && password != "" { - rows = append(rows, - layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - lbl := material.Label(u.theme, unit.Sp(11), u.saveCurrentRemoteBindingHeading()) - lbl.Color = mutedColor - return lbl.Layout(gtx) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.saveCurrentRemoteBinding, u.saveCurrentRemoteBindingButtonLabel()) - }), - ) - } - } - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, rows...) - }) -} - -func intrinsicCompactCard(gtx layout.Context, w layout.Widget) 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() - width := contentDims.Size.X + gtx.Dp(unit.Dp(20)) - maxWidth := gtx.Constraints.Max.X - if maxWidth > 0 && width > maxWidth { - width = maxWidth - } - if width > 0 { - gtx.Constraints.Min.X = width - gtx.Constraints.Max.X = width - } - return compactCard(gtx, w) -} - func (u *ui) sectionSpacing() unit.Dp { if u.mode == "phone" { if u.denseLayout { @@ -7054,101 +6630,6 @@ func (u *ui) groupBarShowsExplicitNavigationButtons() bool { return false } -func (u *ui) topRightActionOrder() []string { - if u.isVaultLocked() { - return nil - } - return []string{"Sync", "Lock", "Menu"} -} - -func (u *ui) mainMenuButtonGroup(gtx layout.Context) layout.Dimensions { - button := func(gtx layout.Context) layout.Dimensions { - icon := u.menuIcon - if icon == nil { - icon = u.settingsIcon - } - btn := material.IconButton(u.theme, &u.toggleMainMenu, icon, "Menu") - if u.mainMenuOpen { - btn.Background = accentColor - btn.Color = color.NRGBA{R: 255, G: 252, B: 247, A: 255} - } else { - btn.Background = selectedColor - btn.Color = accentColor - } - btn.Size = unit.Dp(18) - btn.Inset = layout.UniformInset(unit.Dp(8)) - return btn.Layout(gtx) - } - return button(gtx) -} - -func (u *ui) phoneHeaderMenus(gtx layout.Context) layout.Dimensions { - if u.mode != "phone" { - return layout.Dimensions{} - } - if !u.syncMenuVisibleOnPhone() && !u.mainMenuVisibleOnPhone() { - return layout.Dimensions{} - } - gtx.Constraints.Min = gtx.Constraints.Max - contentInsetPx := gtx.Dp(unit.Dp(16)) - surface := dropdownSurface{ - ContainerWidth: max(0, gtx.Constraints.Max.X-(contentInsetPx*2)), - LeftInset: contentInsetPx, - TopInset: contentInsetPx, - } - - if u.syncMenuVisibleOnPhone() { - surface.draw(gtx, dropdownAnchor{TriggerRightX: u.phoneSyncMenuAnchor.X, TriggerBottomY: u.phoneSyncMenuAnchor.Y}, u.syncMenu) - } - if u.mainMenuVisibleOnPhone() { - surface.draw(gtx, dropdownAnchor{TriggerRightX: u.phoneMainMenuAnchor.X, TriggerBottomY: u.phoneMainMenuAnchor.Y}, u.mainMenu) - } - return layout.Dimensions{Size: gtx.Constraints.Max} -} - -func (u *ui) syncMenuVisibleOnPhone() bool { - return u.mode == "phone" && u.phoneSyncMenuVisible && u.syncMenuOpen -} - -func (u *ui) mainMenuVisibleOnPhone() bool { - return u.mode == "phone" && u.phoneMainMenuVisible && u.mainMenuOpen -} - -func (u *ui) syncMenuDropsBelowTrigger() bool { - return true -} - -func (u *ui) syncMenuRightAlignsToTrigger() bool { - return true -} - -func (u *ui) headerMenusUseOverlayModel() bool { - return true -} - -func (u *ui) mainMenuDropsBelowTrigger() bool { - return true -} - -func (u *ui) mainMenuRightAlignsToTrigger() bool { - return true -} - -func anchoredMenuX(triggerWidth, menuWidth int) int { - return triggerWidth - menuWidth -} - -func anchoredMenuOriginX(containerWidth, rowOriginX, triggerRightX, menuWidth int) int { - x := rowOriginX + triggerRightX - menuWidth - if x < 0 { - return 0 - } - if x+menuWidth > containerWidth { - return max(0, containerWidth-menuWidth) - } - return x -} - func detailLine(th *material.Theme, label, value string) layout.Widget { return func(gtx layout.Context) layout.Dimensions { valueSize := unit.Sp(16) @@ -7351,30 +6832,6 @@ func syncChoiceButton(gtx layout.Context, th *material.Theme, click *widget.Clic return btn.Layout(gtx) } -func menuActionWidth(gtx layout.Context, rows []layout.Widget) int { - width := 0 - for _, row := range rows { - measureGTX := gtx - measureGTX.Constraints.Min = image.Point{} - macro := op.Record(gtx.Ops) - dims := row(measureGTX) - _ = macro.Stop() - if dims.Size.X > width { - width = dims.Size.X - } - } - return width -} - -func rightAlignedMenuAction(gtx layout.Context, width int, child layout.Widget) layout.Dimensions { - if width <= 0 { - return child(gtx) - } - gtx.Constraints.Min.X = width - gtx.Constraints.Max.X = width - return layout.E.Layout(gtx, child) -} - func syncDialogSectionLabel(th *material.Theme, text string) layout.Widget { return func(gtx layout.Context) layout.Dimensions { lbl := material.Label(th, unit.Sp(12), strings.ToUpper(text)) diff --git a/ui_layout_header.go b/ui_layout_header.go new file mode 100644 index 0000000..3b81d7e --- /dev/null +++ b/ui_layout_header.go @@ -0,0 +1,557 @@ +package main + +import ( + "image" + "image/color" + "runtime" + "strings" + + "gioui.org/layout" + "gioui.org/op" + "gioui.org/unit" + "gioui.org/widget" + "gioui.org/widget/material" +) + +func (u *ui) header(gtx layout.Context) layout.Dimensions { + if u.mode == "phone" { + if u.shouldShowLifecycleSetup() || u.isVaultLocked() { + return layout.Dimensions{} + } + gtx.Constraints.Min.X = gtx.Constraints.Max.X + return u.headerActions(gtx) + } + if u.shouldShowDesktopWorkingHeader() { + return layout.Dimensions{} + } + return card(gtx, func(gtx layout.Context) layout.Dimensions { + return layout.Flex{Alignment: layout.Middle}.Layout(gtx, + layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { + return u.brandMark(gtx, 196, 56) + }), + layout.Rigid(u.headerActions), + ) + }) +} + +func (u *ui) headerActions(gtx layout.Context) layout.Dimensions { + if u.shouldShowLifecycleSetup() { + return layout.Dimensions{} + } + if u.isVaultLocked() { + return layout.Dimensions{} + } + if u.shouldShowDesktopWorkingHeader() { + return layout.Dimensions{} + } + spacing := gtx.Dp(unit.Dp(8)) + 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 { + 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") + 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 { + metrics.MainDims = u.mainMenuButtonGroup(gtx) + return metrics.MainDims + }), + ) + } + + rowOps := op.Record(gtx.Ops) + metrics.RowDims = row(gtx) + rowCall := rowOps.Stop() + + if u.mode == "phone" { + metrics.RowOriginX = max(0, gtx.Constraints.Max.X-metrics.RowDims.Size.X) + } + + surface := dropdownSurface{ContainerWidth: gtx.Constraints.Max.X, LeftInset: 0, TopInset: 0} + + 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 = metrics.syncAnchor().point() + } + if u.mainMenuOpen { + u.phoneMainMenuVisible = true + u.phoneMainMenuAnchor = metrics.mainAnchor().point() + } + width := gtx.Constraints.Max.X + return layout.Dimensions{Size: image.Pt(width, metrics.RowDims.Size.Y)} + } + + if u.syncMenuOpen { + surface.draw(gtx, metrics.syncAnchor(), u.syncMenu) + } + if u.mainMenuOpen { + surface.draw(gtx, metrics.mainAnchor(), u.mainMenu) + } + + 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 { + rows := []layout.Widget{ + func(gtx layout.Context) layout.Dimensions { + return tonedButton(gtx, u.theme, &u.showEntries, "Entries") + }, + func(gtx layout.Context) layout.Dimensions { + return tonedButton(gtx, u.theme, &u.showRecycle, "Recycle Bin") + }, + func(gtx layout.Context) layout.Dimensions { + return tonedButton(gtx, u.theme, &u.showAPITokens, "API Tokens") + }, + func(gtx layout.Context) layout.Dimensions { + return tonedButton(gtx, u.theme, &u.showAPIAudit, "API Audit") + }, + func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.showAbout, "About") }, + func(gtx layout.Context) layout.Dimensions { + return tonedButton(gtx, u.theme, &u.openSecuritySettings, "Settings") + }, + } + rowWidth := menuActionWidth(gtx, rows) + return intrinsicCompactCard(gtx, func(gtx layout.Context) layout.Dimensions { + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return rightAlignedMenuAction(gtx, rowWidth, rows[0]) + }), + layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return rightAlignedMenuAction(gtx, rowWidth, rows[1]) + }), + layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return rightAlignedMenuAction(gtx, rowWidth, rows[2]) + }), + layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return rightAlignedMenuAction(gtx, rowWidth, rows[3]) + }), + layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return rightAlignedMenuAction(gtx, rowWidth, rows[4]) + }), + layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return rightAlignedMenuAction(gtx, rowWidth, rows[5]) + }), + ) + }) +} + +func (u *ui) syncButtonGroup(gtx layout.Context) layout.Dimensions { + label := "Sync" + spacing := unit.Dp(4) + if u.mode == "phone" { + spacing = unit.Dp(3) + } + row := func(gtx layout.Context) layout.Dimensions { + return layout.Flex{Alignment: layout.Middle}.Layout(gtx, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return syncPrimaryButton(gtx, u.theme, &u.synchronizeVault, label, u.mode == "phone") + }), + layout.Rigid(layout.Spacer{Width: spacing}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return u.syncMenuToggle(gtx) + }), + ) + } + return row(gtx) +} + +func (u *ui) syncMenuToggle(gtx layout.Context) layout.Dimensions { + btn := material.IconButton(u.theme, &u.toggleSyncMenu, u.chevronDownIcon, "More synchronize actions") + if u.syncMenuOpen { + btn.Background = accentColor + btn.Color = color.NRGBA{R: 255, G: 252, B: 247, A: 255} + } else { + btn.Background = color.NRGBA{R: 231, G: 236, B: 232, A: 255} + btn.Color = accentColor + } + btn.Size = unit.Dp(18) + btn.Inset = layout.UniformInset(unit.Dp(8)) + if u.mode == "phone" { + btn.Size = unit.Dp(16) + btn.Inset = layout.UniformInset(unit.Dp(7)) + } + return btn.Layout(gtx) +} + +func (u *ui) syncMenu(gtx layout.Context) layout.Dimensions { + profiles := u.availableRemoteProfiles() + credentials := u.availableRemoteCredentialEntries() + if len(u.vaultRemoteProfileClicks) < len(profiles) { + u.vaultRemoteProfileClicks = make([]widget.Clickable, len(profiles)) + } + if len(u.vaultRemoteCredentialClicks) < len(credentials) { + u.vaultRemoteCredentialClicks = make([]widget.Clickable, len(credentials)) + } + actionRows := []layout.Widget{ + func(gtx layout.Context) layout.Dimensions { + return tonedButton(gtx, u.theme, &u.openAdvancedSync, "Open Advanced Sync") + }, + } + if supportsVaultShare(runtime.GOOS) && u.vaultSharer != nil && strings.TrimSpace(u.currentShareableVaultPath()) != "" { + actionRows = append(actionRows, func(gtx layout.Context) layout.Dimensions { + return tonedButton(gtx, u.theme, &u.shareCurrentVault, "Share Vault") + }) + } + if u.shouldShowRemoteSyncSetupShortcut() { + actionRows = append(actionRows, func(gtx layout.Context) layout.Dimensions { + return tonedButton(gtx, u.theme, &u.useSavedAdvancedSyncRemote, u.remoteSyncSetupShortcutLabel()) + }) + } + if u.shouldShowDirectRemoteSyncShortcut() { + actionRows = append(actionRows, func(gtx layout.Context) layout.Dimensions { + return tonedButton(gtx, u.theme, &u.openSelectedVaultRemote, u.directRemoteSyncShortcutLabel()) + }) + } + if u.shouldShowRemoteSyncSettingsShortcut() { + actionRows = append(actionRows, func(gtx layout.Context) layout.Dimensions { + return tonedButton(gtx, u.theme, &u.useSavedAdvancedSyncRemote, u.remoteSyncSettingsShortcutLabel()) + }) + } + if u.shouldShowRemoveRemoteSyncShortcut() { + actionRows = append(actionRows, func(gtx layout.Context) layout.Dimensions { + return tonedButton(gtx, u.theme, &u.removeSelectedRemoteBinding, u.removeRemoteSyncShortcutLabel()) + }) + } + actionWidth := menuActionWidth(gtx, actionRows) + return intrinsicCompactCard(gtx, func(gtx layout.Context) layout.Dimensions { + 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), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if !supportsVaultShare(runtime.GOOS) || u.vaultSharer == nil || strings.TrimSpace(u.currentShareableVaultPath()) == "" { + return layout.Dimensions{} + } + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return rightAlignedMenuAction(gtx, actionWidth, func(gtx layout.Context) layout.Dimensions { + return tonedButton(gtx, u.theme, &u.shareCurrentVault, "Share Vault") + }) + }), + layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), + ) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return rightAlignedMenuAction(gtx, actionWidth, func(gtx layout.Context) layout.Dimensions { + return tonedButton(gtx, u.theme, &u.openAdvancedSync, "Open Advanced Sync") + }) + }), + } + if u.shouldShowRemoteSyncSetupShortcut() { + rows = append(rows, + layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return rightAlignedMenuAction(gtx, actionWidth, func(gtx layout.Context) layout.Dimensions { + return tonedButton(gtx, u.theme, &u.useSavedAdvancedSyncRemote, u.remoteSyncSetupShortcutLabel()) + }) + }), + ) + } + if u.shouldShowDirectRemoteSyncShortcut() { + rows = append(rows, + layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return rightAlignedMenuAction(gtx, actionWidth, func(gtx layout.Context) layout.Dimensions { + return tonedButton(gtx, u.theme, &u.openSelectedVaultRemote, u.directRemoteSyncShortcutLabel()) + }) + }), + ) + } + if u.shouldShowRemoteSyncSettingsShortcut() { + rows = append(rows, + layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return rightAlignedMenuAction(gtx, actionWidth, func(gtx layout.Context) layout.Dimensions { + return tonedButton(gtx, u.theme, &u.useSavedAdvancedSyncRemote, u.remoteSyncSettingsShortcutLabel()) + }) + }), + ) + } + if u.shouldShowRemoveRemoteSyncShortcut() { + rows = append(rows, + layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return rightAlignedMenuAction(gtx, actionWidth, func(gtx layout.Context) layout.Dimensions { + return tonedButton(gtx, u.theme, &u.removeSelectedRemoteBinding, u.removeRemoteSyncShortcutLabel()) + }) + }), + ) + } + if u.hasOpenVault() && len(profiles) > 0 && len(credentials) > 0 { + rows = append(rows, + layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + lbl := material.Label(u.theme, unit.Sp(11), u.savedRemoteBindingHeading()) + lbl.Color = mutedColor + return lbl.Layout(gtx) + }), + layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), + ) + if !u.shouldShowSavedRemoteBindingSelectors() { + rows = append(rows, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + profileLabel, credentialLabel, syncLabel, ok := u.savedRemoteBindingSummary() + if !ok { + return layout.Dimensions{} + } + return layout.Background{}.Layout(gtx, fill(color.NRGBA{R: 242, G: 245, B: 240, A: 255}), func(gtx layout.Context) layout.Dimensions { + return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + lbl := material.Label(u.theme, unit.Sp(13), profileLabel) + lbl.Color = accentColor + return lbl.Layout(gtx) + }), + layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + lbl := material.Label(u.theme, unit.Sp(12), "Credential: "+credentialLabel) + lbl.Color = mutedColor + return lbl.Layout(gtx) + }), + layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + lbl := material.Label(u.theme, unit.Sp(12), syncLabel) + lbl.Color = mutedColor + return lbl.Layout(gtx) + }), + ) + }) + }) + }), + ) + } else { + for i, profile := range profiles { + i := i + profile := profile + rows = append(rows, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + selected := strings.TrimSpace(u.selectedVaultRemoteProfileID) == profile.ID + return layout.Inset{Bottom: unit.Dp(4)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return recentSelectionCard(gtx, selected, func(gtx layout.Context) layout.Dimensions { + return u.vaultRemoteProfileClicks[i].Layout(gtx, func(gtx layout.Context) layout.Dimensions { + lbl := material.Label(u.theme, unit.Sp(13), profile.Name) + lbl.Color = accentColor + return lbl.Layout(gtx) + }) + }) + }) + }), + ) + } + rows = append(rows, layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout)) + for i, entry := range credentials { + i := i + entry := entry + rows = append(rows, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + selected := strings.TrimSpace(u.selectedVaultRemoteCredentialEntryID) == entry.ID + label := entry.Title + if strings.TrimSpace(entry.Username) != "" { + label += " · " + strings.TrimSpace(entry.Username) + } + return layout.Inset{Bottom: unit.Dp(4)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return recentSelectionCard(gtx, selected, func(gtx layout.Context) layout.Dimensions { + return u.vaultRemoteCredentialClicks[i].Layout(gtx, func(gtx layout.Context) layout.Dimensions { + lbl := material.Label(u.theme, unit.Sp(13), label) + lbl.Color = accentColor + return lbl.Layout(gtx) + }) + }) + }) + }), + ) + } + } + if _, ok := u.selectedVaultRemoteProfile(); ok { + if _, ok := u.selectedVaultRemoteCredentialEntry(); ok { + rows = append(rows, + layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return tonedButton(gtx, u.theme, &u.openSelectedVaultRemote, u.openSelectedVaultRemoteButtonLabel()) + }), + ) + } + } + } + if u.hasOpenVault() { + baseURL := strings.TrimSpace(u.remoteBaseURL.Text()) + remotePath := strings.TrimSpace(u.remotePath.Text()) + username := strings.TrimSpace(u.remoteUsername.Text()) + password := u.remotePassword.Text() + if baseURL != "" && remotePath != "" && username != "" && password != "" { + rows = append(rows, + layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + lbl := material.Label(u.theme, unit.Sp(11), u.saveCurrentRemoteBindingHeading()) + lbl.Color = mutedColor + return lbl.Layout(gtx) + }), + layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return tonedButton(gtx, u.theme, &u.saveCurrentRemoteBinding, u.saveCurrentRemoteBindingButtonLabel()) + }), + ) + } + } + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, rows...) + }) +} + +func intrinsicCompactCard(gtx layout.Context, w layout.Widget) 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() + width := contentDims.Size.X + gtx.Dp(unit.Dp(20)) + maxWidth := gtx.Constraints.Max.X + if maxWidth > 0 && width > maxWidth { + width = maxWidth + } + if width > 0 { + gtx.Constraints.Min.X = width + gtx.Constraints.Max.X = width + } + return compactCard(gtx, w) +} + +func (u *ui) topRightActionOrder() []string { + if u.isVaultLocked() { + return nil + } + return []string{"Sync", "Lock", "Menu"} +} + +func (u *ui) mainMenuButtonGroup(gtx layout.Context) layout.Dimensions { + button := func(gtx layout.Context) layout.Dimensions { + icon := u.menuIcon + if icon == nil { + icon = u.settingsIcon + } + btn := material.IconButton(u.theme, &u.toggleMainMenu, icon, "Menu") + if u.mainMenuOpen { + btn.Background = accentColor + btn.Color = color.NRGBA{R: 255, G: 252, B: 247, A: 255} + } else { + btn.Background = selectedColor + btn.Color = accentColor + } + btn.Size = unit.Dp(18) + btn.Inset = layout.UniformInset(unit.Dp(8)) + return btn.Layout(gtx) + } + return button(gtx) +} + +func (u *ui) phoneHeaderMenus(gtx layout.Context) layout.Dimensions { + if u.mode != "phone" { + return layout.Dimensions{} + } + if !u.syncMenuVisibleOnPhone() && !u.mainMenuVisibleOnPhone() { + return layout.Dimensions{} + } + gtx.Constraints.Min = gtx.Constraints.Max + contentInsetPx := gtx.Dp(unit.Dp(16)) + surface := dropdownSurface{ + ContainerWidth: max(0, gtx.Constraints.Max.X-(contentInsetPx*2)), + LeftInset: contentInsetPx, + TopInset: contentInsetPx, + } + + if u.syncMenuVisibleOnPhone() { + surface.draw(gtx, dropdownAnchor{TriggerRightX: u.phoneSyncMenuAnchor.X, TriggerBottomY: u.phoneSyncMenuAnchor.Y}, u.syncMenu) + } + if u.mainMenuVisibleOnPhone() { + surface.draw(gtx, dropdownAnchor{TriggerRightX: u.phoneMainMenuAnchor.X, TriggerBottomY: u.phoneMainMenuAnchor.Y}, u.mainMenu) + } + return layout.Dimensions{Size: gtx.Constraints.Max} +} + +func (u *ui) syncMenuVisibleOnPhone() bool { + return u.mode == "phone" && u.phoneSyncMenuVisible && u.syncMenuOpen +} + +func (u *ui) mainMenuVisibleOnPhone() bool { + return u.mode == "phone" && u.phoneMainMenuVisible && u.mainMenuOpen +} + +func (u *ui) syncMenuDropsBelowTrigger() bool { + return true +} + +func (u *ui) syncMenuRightAlignsToTrigger() bool { + return true +} + +func (u *ui) headerMenusUseOverlayModel() bool { + return true +} + +func (u *ui) mainMenuDropsBelowTrigger() bool { + return true +} + +func (u *ui) mainMenuRightAlignsToTrigger() bool { + return true +} + +func anchoredMenuX(triggerWidth, menuWidth int) int { + return triggerWidth - menuWidth +} + +func anchoredMenuOriginX(containerWidth, rowOriginX, triggerRightX, menuWidth int) int { + x := rowOriginX + triggerRightX - menuWidth + if x < 0 { + return 0 + } + if x+menuWidth > containerWidth { + return max(0, containerWidth-menuWidth) + } + return x +} + +func menuActionWidth(gtx layout.Context, rows []layout.Widget) int { + width := 0 + for _, row := range rows { + measureGTX := gtx + measureGTX.Constraints.Min = image.Point{} + macro := op.Record(gtx.Ops) + dims := row(measureGTX) + _ = macro.Stop() + if dims.Size.X > width { + width = dims.Size.X + } + } + return width +} + +func rightAlignedMenuAction(gtx layout.Context, width int, child layout.Widget) layout.Dimensions { + if width <= 0 { + return child(gtx) + } + gtx.Constraints.Min.X = width + gtx.Constraints.Max.X = width + return layout.E.Layout(gtx, child) +}