package appui import ( "fmt" "image" "gioui.org/layout" "gioui.org/op" "gioui.org/op/paint" "gioui.org/unit" "gioui.org/widget" "gioui.org/widget/material" headerview "git.julianfamily.org/keepassgo/internal/appui/header" headerlayout "git.julianfamily.org/keepassgo/internal/appui/header/layout" ) func (u *ui) header(gtx layout.Context) layout.Dimensions { if u.usesCompactViewport() { 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.Rigid(func(gtx layout.Context) layout.Dimensions { return u.brandMark(gtx, 196, 56) }), layout.Flexed(1, u.headerActions), ) }) } func (u *ui) headerActions(gtx layout.Context) layout.Dimensions { if u.shouldShowLifecycleSetup() || u.isVaultLocked() { return layout.Dimensions{} } cluster := u.newHeaderActionCluster(gtx) rowDims := cluster.layout(gtx, u) if u.usesCompactViewport() { u.maybeLogHeaderBounds(newHeaderButtonBounds(image.Pt(u.frameInsetPx, u.frameInsetPx), cluster.Metrics.Bounds())) } if u.usesCompactViewport() { cluster.prepareCompactMenus(gtx, u) return layout.Dimensions{Size: image.Pt(gtx.Constraints.Max.X, rowDims.Size.Y)} } return rowDims } type headerActionCluster struct { Metrics headerlayout.ActionMetrics SyncMenu layout.Widget MainMenu layout.Widget } func (c headerActionCluster) ShowSyncMenu() bool { return c.SyncMenu != nil } func (c headerActionCluster) ShowMainMenu() bool { return c.MainMenu != nil } func (u *ui) newHeaderActionCluster(gtx layout.Context) headerActionCluster { cluster := headerActionCluster{ SyncMenu: u.syncMenuWidget(), MainMenu: u.mainMenuWidget(), } spacing := gtx.Dp(unit.Dp(8)) cluster.Metrics = headerlayout.ActionMetrics{Spacing: spacing, SyncInnerSpacing: gtx.Dp(unit.Dp(3))} if !u.usesCompactViewport() { cluster.Metrics.SyncInnerSpacing = gtx.Dp(unit.Dp(4)) } return cluster } func (c *headerActionCluster) layout(gtx layout.Context, u *ui) layout.Dimensions { rowOps := op.Record(gtx.Ops) c.Metrics.RowDims = c.layoutRow(gtx, u) rowCall := rowOps.Stop() c.Metrics.RowOriginX = max(0, gtx.Constraints.Max.X-c.Metrics.RowDims.Size.X) return layout.E.Layout(gtx, func(gtx layout.Context) layout.Dimensions { rowCall.Add(gtx.Ops) return c.Metrics.RowDims }) } 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 { return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx, layout.Rigid(func(gtx layout.Context) layout.Dimensions { c.Metrics.SyncDims, c.Metrics.SyncPrimaryDims, c.Metrics.SyncToggleDims = u.syncButtonGroupWithMetrics(gtx) return c.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") c.Metrics.LockDims = btn.Layout(gtx) return c.Metrics.LockDims }), layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { c.Metrics.MainDims = u.mainMenuButtonGroup(gtx) return c.Metrics.MainDims }), ) } 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 { SyncPrimary image.Rectangle SyncToggle image.Rectangle Lock image.Rectangle MainMenu image.Rectangle } func newHeaderButtonBounds(origin image.Point, bounds headerlayout.ActionBounds) headerButtonBounds { return headerButtonBounds{ SyncPrimary: bounds.SyncPrimary.Add(origin), SyncToggle: bounds.SyncToggle.Add(origin), Lock: bounds.Lock.Add(origin), MainMenu: bounds.MainMenu.Add(origin), } } func (b headerButtonBounds) logLine(mode string) string { return fmt.Sprintf( "keepassgo header-bounds mode=%s sync=%s sync_toggle=%s lock=%s menu=%s", mode, formatHeaderRect(b.SyncPrimary), formatHeaderRect(b.SyncToggle), formatHeaderRect(b.Lock), formatHeaderRect(b.MainMenu), ) } func formatHeaderRect(rect image.Rectangle) string { return fmt.Sprintf("%d,%d-%d,%d", rect.Min.X, rect.Min.Y, rect.Max.X, rect.Max.Y) } func (u *ui) topRightActionOrder() []string { if u.isVaultLocked() { return nil } return []string{"Sync", "Lock", "Menu"} } func (u *ui) phoneHeaderMenus(gtx layout.Context) layout.Dimensions { if !u.usesCompactViewport() || (!u.syncMenuVisibleOnPhone() && !u.mainMenuVisibleOnPhone()) { return layout.Dimensions{} } cluster := u.newHeaderActionCluster(gtx) if u.syncMenuVisibleOnPhone() { 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() dims := cluster.layoutMenuRow(gtx) return layout.Dimensions{Size: image.Pt(gtx.Constraints.Max.X, max(dims.Size.Y, u.phoneSyncMenuOrigin.Y))} }) } if u.mainMenuVisibleOnPhone() { 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() dims := cluster.layoutMenuRow(gtx) return layout.Dimensions{Size: image.Pt(gtx.Constraints.Max.X, max(dims.Size.Y, u.phoneMainMenuOrigin.Y))} }) } 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 { return u.usesCompactViewport() && u.phoneSyncMenuVisible && u.syncMenuOpen } func (u *ui) mainMenuVisibleOnPhone() bool { return u.usesCompactViewport() && 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 (u *ui) lifecycleBranding(gtx layout.Context) layout.Dimensions { if !u.usesCompactViewport() { return layout.Dimensions{} } return layout.Dimensions{} } func (u *ui) brandMark(gtx layout.Context, widthDP, heightDP float32) layout.Dimensions { if u.usesCompactViewport() { return u.brandImage(gtx, u.splashSquare, widthDP, heightDP) } return u.brandImage(gtx, u.logoHorizontal, widthDP, heightDP) } func (u *ui) brandImage(gtx layout.Context, src paint.ImageOp, widthDP, heightDP float32) layout.Dimensions { width := gtx.Dp(unit.Dp(widthDP)) height := gtx.Dp(unit.Dp(heightDP)) if width > gtx.Constraints.Max.X { width = gtx.Constraints.Max.X } if height > gtx.Constraints.Max.Y && gtx.Constraints.Max.Y > 0 { height = gtx.Constraints.Max.Y } img := widget.Image{ Src: src, Fit: widget.Contain, Position: layout.W, Scale: 1.0 / gtx.Metric.PxPerDp, } gtx.Constraints.Min = image.Point{} gtx.Constraints.Max = image.Pt(width, height) return img.Layout(gtx) } func (u *ui) mainMenuWidget() layout.Widget { if !u.mainMenuOpen { return nil } return u.mainMenu } 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") }, } return headerview.MainMenu(gtx, u.theme, rows, compactCard, nil) } func (u *ui) mainMenuButtonGroup(gtx layout.Context) layout.Dimensions { icon := u.menuIcon if icon == nil { icon = u.settingsIcon } return headerview.MainMenuButtonGroup(gtx, u.theme, &u.toggleMainMenu, icon, u.mainMenuOpen, selectedColor, accentColor) } func intrinsicCompactCard(gtx layout.Context, w layout.Widget) layout.Dimensions { return headerlayout.IntrinsicCompactCard(gtx, w, compactCard, nil) } func menuActionWidth(gtx layout.Context, rows []layout.Widget) int { return headerlayout.MenuActionWidth(gtx, rows) } func rightAlignedMenuAction(gtx layout.Context, width int, child layout.Widget) layout.Dimensions { return headerlayout.RightAlignedAction(gtx, width, child) }