69 lines · 3.6 KB · Swift
RawDownload
import AppKit extension StatusItemController {    /// Collects the card hosting views of items the current populate pass is about to discard    /// so `makeMenuCardItem` can reuse them for cards with the same identifier (or, failing    /// that, the same content type) instead of building fresh hosting views.    ///    /// Safety: live menu items can alias one merged-switcher cache entry — the one for the    /// selection currently displayed, re-cached at the end of every populate. Consuming that    /// entry up front (`displacedSelection`) guarantees no cache entry can still reference a    /// harvested view; entries for other selections only hold items already detached from the    /// menu. Harvested views are detached from their outgoing items; whatever the pass does    /// not consume is released by `clearMenuCardViewRecyclePool`.    func harvestRecyclableMenuCardViews(        in menu: NSMenu,        fromIndex: Int,        displacedSelection: ProviderSwitcherSelection?,        preserveHighlightedItem: Bool = false)    {        self.menuCardViewRecyclePool.removeAll(keepingCapacity: true)        let menuKey = ObjectIdentifier(menu)        if let displacedSelection {            self.mergedSwitcherContentCaches[menuKey]?.removeValue(forKey: displacedSelection)        }        guard self.menuCardRenderingEnabledForController else { return }        guard fromIndex >= 0, fromIndex < menu.items.count else { return }        for item in menu.items[fromIndex...] {            guard let id = item.representedObject as? String else { continue }            guard let view = item.view, view is any MenuCardMeasuring else { continue }            guard self.menuCardViewRecyclePool[id] == nil else { continue }            // Unhighlight before detaching: the highlight tracker unwinds through the            // outgoing item's `view`, which is about to become nil, so a recycled view            // would otherwise re-attach visibly highlighted with no path to clear it.            if self.highlightedMenuItems[menuKey] === item {                if !preserveHighlightedItem {                    self.highlightedMenuItems.removeValue(forKey: menuKey)                }            }            (view as? MenuCardHighlighting)?.setHighlighted(false)            item.view = nil            self.menuCardViewRecyclePool[id] = view        }    }     /// Pops a pool entry adoptable as `ViewType`: the same card identifier when its view    /// matches, otherwise the first type-compatible leftover. The fallback is what makes    /// provider switches cheap — a different provider's card with a different identifier but    /// the same SwiftUI content type (for example two providers' usage cards) is repainted    /// in place instead of being rebuilt.    func takeRecyclableMenuCardView<ViewType: NSView>(for id: String, as type: ViewType.Type) -> ViewType? {        if let candidate = self.menuCardViewRecyclePool.removeValue(forKey: id) {            if let adopted = candidate as? ViewType {                return adopted            }            // A same-id view of an incompatible shape can never be adopted later in this            // pass; dropping it restores the build-fresh behavior.            return nil        }        guard let match = self.menuCardViewRecyclePool.first(where: { $0.value is ViewType }) else {            return nil        }        self.menuCardViewRecyclePool.removeValue(forKey: match.key)        return match.value as? ViewType    }     func clearMenuCardViewRecyclePool() {        self.menuCardViewRecyclePool.removeAll(keepingCapacity: true)    }}