Files
steipete--codexbar/Sources/CodexBar/StatusItemController+MenuSmartUpdate.swift
T
2026-07-13 12:22:33 +08:00

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)
}
}