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() || u.shouldShowDesktopWorkingHeader() { return layout.Dimensions{} } spacing := gtx.Dp(unit.Dp(8)) metrics := headerlayout.ActionMetrics{Spacing: spacing, SyncInnerSpacing: gtx.Dp(unit.Dp(3))} if !u.usesCompactViewport() { metrics.SyncInnerSpacing = gtx.Dp(unit.Dp(4)) } actionCluster := func(gtx layout.Context) layout.Dimensions { return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx, layout.Rigid(func(gtx layout.Context) layout.Dimensions { metrics.SyncDims, metrics.SyncPrimaryDims, metrics.SyncToggleDims = u.syncButtonGroupWithMetrics(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 = actionCluster(gtx) rowCall := rowOps.Stop() metrics.RowOriginX = max(0, gtx.Constraints.Max.X-metrics.RowDims.Size.X) surface := headerlayout.DropdownSurface{ContainerWidth: gtx.Constraints.Max.X, LeftInset: 0, TopInset: 0} rowDims := layout.E.Layout(gtx, func(gtx layout.Context) layout.Dimensions { rowCall.Add(gtx.Ops) return metrics.RowDims }) if u.usesCompactViewport() { u.maybeLogHeaderBounds(newHeaderButtonBounds(image.Pt(u.frameInsetPx, u.frameInsetPx), metrics.Bounds())) } if u.usesCompactViewport() { if u.syncMenuOpen { u.phoneSyncMenuVisible = true u.phoneSyncMenuAnchor = metrics.SyncAnchor().Point() } if u.mainMenuOpen { u.phoneMainMenuVisible = true u.phoneMainMenuAnchor = metrics.MainAnchor().Point() } return layout.Dimensions{Size: image.Pt(gtx.Constraints.Max.X, rowDims.Size.Y)} } if u.syncMenuOpen { surface.Draw(gtx, metrics.SyncAnchor(), u.syncMenu) } if u.mainMenuOpen { surface.Draw(gtx, metrics.MainAnchor(), u.mainMenu) } return rowDims } 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{} } gtx.Constraints.Min = gtx.Constraints.Max contentInsetPx := gtx.Dp(unit.Dp(16)) surface := headerlayout.DropdownSurface{ ContainerWidth: max(0, gtx.Constraints.Max.X-(contentInsetPx*2)), LeftInset: contentInsetPx, TopInset: contentInsetPx, } if u.syncMenuVisibleOnPhone() { surface.Draw(gtx, headerlayout.DropdownAnchor{TriggerRightX: u.phoneSyncMenuAnchor.X, TriggerBottomY: u.phoneSyncMenuAnchor.Y}, u.syncMenu) } if u.mainMenuVisibleOnPhone() { surface.Draw(gtx, headerlayout.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.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) 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) } 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) } 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) }