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

179 lines
7.4 KiB
Swift

import AppKit
import SwiftUI
extension StatusItemController {
func refreshMenuCardHeights(in menu: NSMenu) {
let width = self.renderedMenuWidth(for: menu)
for item in menu.items {
if let view = item.view as? PersistentRefreshMenuView {
guard abs(view.frame.width - width) > 0.5 else { continue }
view.applySize(width: width, height: PersistentRefreshRowMetrics.defaults.rowHeight)
continue
}
guard let view = item.view, view is any MenuCardMeasuring else { continue }
guard abs(view.frame.width - width) > 0.5 else { continue }
let id = item.representedObject as? String ?? "menuCard"
let scope = self.menuProvider(for: menu)?.rawValue ?? id
let height = self.cachedMenuCardHeight(for: id, scope: scope, width: width) {
self.menuCardHeight(for: view, width: width)
}
view.frame = NSRect(
origin: .zero,
size: NSSize(width: width, height: height))
}
}
func makeMenuCardItem<CardContent: View>(
_ view: CardContent,
id: String,
width: CGFloat,
heightCacheScope: String? = nil,
heightCacheFingerprint: String? = nil,
submenu: NSMenu? = nil,
submenuIndicatorAlignment: Alignment = .topTrailing,
submenuIndicatorTopPadding: CGFloat = 8,
containsInteractiveControls: Bool = false,
usesGPUSelection: Bool = false,
onClick: (() -> Void)? = nil) -> NSMenuItem
{
let allowsMenuHighlight = submenu != nil || onClick != nil
if !self.menuCardRenderingEnabledForController {
let item = NSMenuItem()
item.isEnabled = allowsMenuHighlight
item.representedObject = id
item.submenu = submenu
if submenu != nil {
item.target = self
item.action = #selector(self.menuCardNoOp(_:))
}
return item
}
if usesGPUSelection {
// Selection is painted by AppKit/GPU, so the SwiftUI content is pinned to its normal
// appearance via a `highlightState` that is never flipped; these rows skip hosting-view
// recycling because the recycler is typed to `MenuCardItemHostingView`.
let interactiveRegionStore = MenuCardInteractiveRegionStore()
let wrapped = MenuCardSectionContainerView(
highlightState: MenuCardHighlightState(),
showsSubmenuIndicator: submenu != nil,
submenuIndicatorAlignment: submenuIndicatorAlignment,
submenuIndicatorTopPadding: submenuIndicatorTopPadding,
refreshMonitor: self.menuCardRefreshMonitor,
interactiveRegionStore: interactiveRegionStore)
{
view
}
let gpuHosting = GPUSelectionHostingView(
rootView: wrapped,
allowsMenuHighlight: allowsMenuHighlight,
containsInteractiveControls: containsInteractiveControls,
interactiveRegionStore: interactiveRegionStore,
onClick: onClick)
let gpuHeight = self.cachedMenuCardHeight(
for: id,
scope: heightCacheScope ?? id,
width: width,
fingerprint: heightCacheFingerprint)
{
self.menuCardHeight(for: gpuHosting, width: width)
}
gpuHosting.frame = NSRect(origin: .zero, size: NSSize(width: width, height: gpuHeight))
return self.makeMenuCardNSMenuItem(
hosting: gpuHosting,
id: id,
submenu: submenu,
isEnabled: allowsMenuHighlight || containsInteractiveControls)
}
let hosting: MenuCardItemHostingView<MenuCardSectionContainerView<CardContent>>
if let recycled = self.takeRecyclableMenuCardView(
for: id,
as: MenuCardItemHostingView<MenuCardSectionContainerView<CardContent>>.self)
{
let wrapped = MenuCardSectionContainerView(
highlightState: recycled.highlightState,
showsSubmenuIndicator: submenu != nil,
submenuIndicatorAlignment: submenuIndicatorAlignment,
submenuIndicatorTopPadding: submenuIndicatorTopPadding,
refreshMonitor: self.menuCardRefreshMonitor,
interactiveRegionStore: recycled.interactiveRegionStore)
{
view
}
recycled.prepareForReuse(
rootView: wrapped,
allowsMenuHighlight: allowsMenuHighlight,
containsInteractiveControls: containsInteractiveControls,
onClick: onClick)
hosting = recycled
} else {
let highlightState = MenuCardHighlightState()
let interactiveRegionStore = MenuCardInteractiveRegionStore()
let wrapped = MenuCardSectionContainerView(
highlightState: highlightState,
showsSubmenuIndicator: submenu != nil,
submenuIndicatorAlignment: submenuIndicatorAlignment,
submenuIndicatorTopPadding: submenuIndicatorTopPadding,
refreshMonitor: self.menuCardRefreshMonitor,
interactiveRegionStore: interactiveRegionStore)
{
view
}
hosting = MenuCardItemHostingView(
rootView: wrapped,
highlightState: highlightState,
allowsMenuHighlight: allowsMenuHighlight,
containsInteractiveControls: containsInteractiveControls,
interactiveRegionStore: interactiveRegionStore,
onClick: onClick)
}
let height = self.cachedMenuCardHeight(
for: id,
scope: heightCacheScope ?? id,
width: width,
fingerprint: heightCacheFingerprint)
{
self.menuCardHeight(for: hosting, width: width)
}
hosting.frame = NSRect(origin: .zero, size: NSSize(width: width, height: height))
return self.makeMenuCardNSMenuItem(
hosting: hosting,
id: id,
submenu: submenu,
isEnabled: allowsMenuHighlight || containsInteractiveControls)
}
/// Wraps a measured hosting view in the `NSMenuItem` the menu installs, wiring submenu routing.
private func makeMenuCardNSMenuItem(
hosting: NSView,
id: String,
submenu: NSMenu?,
isEnabled: Bool) -> NSMenuItem
{
let item = NSMenuItem()
item.view = hosting
item.isEnabled = isEnabled
item.representedObject = id
item.submenu = submenu
if submenu != nil {
item.target = self
item.action = #selector(self.menuCardNoOp(_:))
}
return item
}
private func menuCardHeight(for view: NSView, width: CGFloat) -> CGFloat {
let basePadding: CGFloat = 6
let descenderSafety: CGFloat = 1
if let measured = view as? MenuCardMeasuring {
return max(1, ceil(measured.measuredHeight(width: width) + basePadding + descenderSafety))
}
view.frame = NSRect(origin: .zero, size: NSSize(width: width, height: 1))
let fitted = view.fittingSize
return max(1, ceil(fitted.height + basePadding + descenderSafety))
}
}