142 lines
6.8 KiB
Swift
142 lines
6.8 KiB
Swift
import AppKit
|
|
import CodexBarCore
|
|
import SwiftUI
|
|
|
|
extension StatusItemController {
|
|
/// Smart update: rebuild everything below the provider switcher while keeping the switcher view intact.
|
|
struct MenuUpdateContext {
|
|
let provider: UsageProvider?
|
|
let currentProvider: UsageProvider
|
|
let switcherSelection: ProviderSwitcherSelection
|
|
let menuWidth: CGFloat
|
|
let codexAccountDisplay: CodexAccountMenuDisplay?
|
|
let tokenAccountDisplay: TokenAccountMenuDisplay?
|
|
let openAIContext: OpenAIWebContext
|
|
let descriptor: MenuDescriptor
|
|
}
|
|
|
|
/// Smart update: rebuild everything below the provider switcher while keeping the switcher view intact.
|
|
func updateMenuContentPreservingSwitcher(
|
|
_ menu: NSMenu,
|
|
context: MenuUpdateContext)
|
|
{
|
|
self.performMenuMutationWithoutAnimation {
|
|
let contentStartIndex = self.providerSwitcherContentStartIndex(in: menu)
|
|
if let switcherView = menu.items.first?.view as? ProviderSwitcherView {
|
|
switcherView.updateSelection(context.switcherSelection)
|
|
switcherView.updateQuotaIndicators()
|
|
}
|
|
let outgoingSelection = self.lastMergedMenuContentSelection
|
|
let isSelectionSwitch = outgoingSelection != nil && outgoingSelection != context.switcherSelection
|
|
let enabledProviders = self.store.enabledProvidersForDisplay()
|
|
|
|
if isSelectionSwitch,
|
|
let outgoingSelection,
|
|
let cachedItems = self.reusableMergedSwitcherContent(
|
|
for: context.switcherSelection,
|
|
in: menu,
|
|
menuWidth: context.menuWidth,
|
|
codexAccountDisplay: context.codexAccountDisplay,
|
|
tokenAccountDisplay: context.tokenAccountDisplay)
|
|
{
|
|
// Park the outgoing payloads for an equally instant switch-back. Compatible
|
|
// menu-item shells stay attached, avoiding the empty intermediate layout that
|
|
// AppKit can visibly render when the whole content block is removed first.
|
|
let outgoingCodexAccountDisplay = self.lastCodexAccountMenuDisplay
|
|
let outgoingTokenAccountDisplay = self.lastTokenAccountMenuDisplay
|
|
self.rememberMergedSwitcherState(enabledProviders, context.switcherSelection)
|
|
let displacedItems = self.replaceMenuContentKeepingRowsVisible(
|
|
menu,
|
|
fromIndex: contentStartIndex,
|
|
with: cachedItems)
|
|
// Cached items may have changed refresh state while detached from a menu.
|
|
self.updatePersistentRefreshItemsEnabled()
|
|
self.refreshMenuCardHeights(in: menu)
|
|
self.cacheMergedSwitcherContent(
|
|
displacedItems,
|
|
in: menu,
|
|
selection: outgoingSelection,
|
|
context: MergedSwitcherContentCacheContext(
|
|
menuWidth: context.menuWidth,
|
|
codexAccountDisplay: outgoingCodexAccountDisplay,
|
|
tokenAccountDisplay: outgoingTokenAccountDisplay,
|
|
contentVersion: nil))
|
|
self.lastCodexAccountMenuDisplay = context.codexAccountDisplay
|
|
self.lastTokenAccountMenuDisplay = context.tokenAccountDisplay
|
|
self.cacheVisibleMergedSwitcherContent(
|
|
in: menu,
|
|
selection: context.switcherSelection,
|
|
contentStartIndex: contentStartIndex,
|
|
menuWidth: context.menuWidth,
|
|
contentVersion: self.menuSession.contentVersion)
|
|
return
|
|
}
|
|
|
|
// Rebuild path (data tick, or switch whose incoming tab must be built): recycle
|
|
// the outgoing hosting views and reconcile in place when the row skeleton is
|
|
// unchanged, so an open tracked menu sees content mutations instead of item
|
|
// churn. The fresh content is built into a detached scratch menu while its
|
|
// interaction closures capture the live menu they will serve.
|
|
let shapes = self.menuContentShapes(in: menu, fromIndex: contentStartIndex)
|
|
self.harvestRecyclableMenuCardViews(
|
|
in: menu,
|
|
fromIndex: contentStartIndex,
|
|
displacedSelection: outgoingSelection,
|
|
preserveHighlightedItem: true)
|
|
defer { self.clearMenuCardViewRecyclePool() }
|
|
self.rememberMergedSwitcherState(enabledProviders, context.switcherSelection)
|
|
let scratch = NSMenu()
|
|
scratch.autoenablesItems = false
|
|
self.addSwitcherScopedMenuContent(into: scratch, captureMenu: menu, context: context)
|
|
self.reconcileMenuContent(menu, fromIndex: contentStartIndex, shapes: shapes, with: scratch)
|
|
self.refreshMenuCardHeights(in: menu)
|
|
self.cacheVisibleMergedSwitcherContent(
|
|
in: menu,
|
|
selection: context.switcherSelection,
|
|
contentStartIndex: contentStartIndex,
|
|
menuWidth: context.menuWidth,
|
|
contentVersion: self.menuSession.contentVersion)
|
|
}
|
|
}
|
|
|
|
/// Adds everything below the provider switcher (account switchers, card content, and
|
|
/// actionable sections) to `target`, which may be a detached scratch menu; interaction
|
|
/// closures always capture `captureMenu`, the live menu the rows will serve.
|
|
private func addSwitcherScopedMenuContent(
|
|
into target: NSMenu,
|
|
captureMenu: NSMenu,
|
|
context: MenuUpdateContext)
|
|
{
|
|
self.addCodexAccountSwitcherIfNeeded(
|
|
to: target,
|
|
display: context.codexAccountDisplay,
|
|
width: context.menuWidth,
|
|
captureMenu: captureMenu)
|
|
self.lastCodexAccountMenuDisplay = context.codexAccountDisplay
|
|
self.addTokenAccountSwitcherIfNeeded(
|
|
to: target,
|
|
display: context.tokenAccountDisplay,
|
|
width: context.menuWidth,
|
|
captureMenu: captureMenu)
|
|
self.lastTokenAccountMenuDisplay = context.tokenAccountDisplay
|
|
|
|
let menuContext = MenuCardContext(
|
|
currentProvider: context.currentProvider,
|
|
selectedProvider: context.provider,
|
|
menuWidth: context.menuWidth,
|
|
codexAccountDisplay: context.codexAccountDisplay,
|
|
tokenAccountDisplay: context.tokenAccountDisplay,
|
|
openAIContext: context.openAIContext)
|
|
self.addPrimaryMenuContent(
|
|
to: target,
|
|
context: menuContext,
|
|
switcherSelection: context.switcherSelection,
|
|
captureMenu: captureMenu)
|
|
self.addActionableSections(
|
|
context.descriptor.sections,
|
|
to: target,
|
|
width: context.menuWidth,
|
|
captureMenu: captureMenu)
|
|
}
|
|
}
|