Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
Swift
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327// MenuBandPopoverPanel.swift//// Custom popover-styled NSPanel. NSPopover places its arrow at the// anchor's screen-x and auto-fits the content to the visible screen,// which means the arrow always lands roughly where the anchor sits// — when the status item is near the screen edge, the popover content// shifts inward and the arrow ends up on the inward side of the// content rather than flush at one corner. This panel decouples the// two: the window's frame and the arrow tip's screen-x are set// independently, so the content can sit far from the arrow and the// arrow can land at any horizontal position on the panel's top edge.//// Visually it mimics NSPopover: rounded body + small triangular arrow// rendered through a single NSVisualEffectView with a CAShapeLayer// mask, so the liquid-glass material flows continuously from the// arrow into the body.
import AppKit
final class MenuBandPopoverPanel: NSPanel { static let arrowHeight: CGFloat = 11 static let arrowWidth: CGFloat = 22 static let cornerRadius: CGFloat = 10
let chrome: MenuBandPopoverChrome
init(content: NSView, contentSize: NSSize) { let totalSize = NSSize( width: contentSize.width, height: contentSize.height + Self.arrowHeight ) chrome = MenuBandPopoverChrome(content: content) super.init( contentRect: NSRect(origin: .zero, size: totalSize), styleMask: [.borderless, .nonactivatingPanel], backing: .buffered, defer: false ) isOpaque = false backgroundColor = .clear hasShadow = true level = .popUpMenu animationBehavior = .none collectionBehavior = [.transient, .ignoresCycle] hidesOnDeactivate = false canHide = false isMovableByWindowBackground = false acceptsMouseMovedEvents = true titleVisibility = .hidden titlebarAppearsTransparent = true isReleasedWhenClosed = false contentView = chrome }
override var canBecomeKey: Bool { true } override var canBecomeMain: Bool { false }
/// Routes ⌘/⌥+letter chords to the note path WHEN the popover is the key /// window (quiet-focus with the popover open). Mirrors `KeyCapturePanel`'s /// override so a simultaneous ⌘+letter chords consistently and shadows the /// matching system command — but we give focused controls (any text field) /// first crack via `super`, so editing shortcuts there still work. var keyEquivalentHandler: ((NSEvent) -> Bool)? override func performKeyEquivalent(with event: NSEvent) -> Bool { if super.performKeyEquivalent(with: event) { return true } if let handler = keyEquivalentHandler, handler(event) { return true } return false }
/// Position the panel so that: /// • the panel's top edge sits at `topScreenY` (the menubar bottom), /// • the panel's left edge sits at `leftScreenX`, /// • the arrow tip points at `arrowScreenX`. /// `arrowScreenX` may be inside or outside the panel's horizontal /// extent; the chrome clamps the rendered arrow to a small inset so /// it never falls off the rounded corner radii. func position(leftScreenX: CGFloat, topScreenY: CGFloat, arrowScreenX: CGFloat) { let frameSize = frame.size let panelFrame = NSRect( x: leftScreenX, y: topScreenY - frameSize.height, width: frameSize.width, height: frameSize.height ) setFrame(panelFrame, display: true) // Arrow position is in chrome-local coords (origin at panel // bottom-left). Convert from screen-x. let arrowLocalX = arrowScreenX - leftScreenX chrome.setArrowOffsetFromLeft(arrowLocalX) }
/// Resize the whole panel to host `contentSize` (+ the arrow strip), /// keeping the TOP edge pinned to the menubar so the popover grows / /// shrinks downward — used when the notation staff slides in/out and /// the entire popover height changes, not just an inner row. func resizeContent(to contentSize: NSSize, animated: Bool) { let newH = contentSize.height + Self.arrowHeight let newW = max(contentSize.width, frame.width) NSLog("MenuBand panel resize: %.0f×%.0f → %.0f×%.0f", frame.width, frame.height, newW, newH) let top = frame.maxY // pin top edge let originX = frame.minX // keep left edge let newFrame = NSRect(x: originX, y: top - newH, width: newW, height: newH) if animated { let timing = CAMediaTimingFunction(name: .easeInEaseOut) // Animate the mask silhouette in lockstep with the frame so the // glass body grows downward (top edge pinned) and the picker // slides into view, rather than the mask snapping to full size // while the window frame catches up. chrome.animateMask(toPanelSize: newFrame.size, duration: 0.2, timing: timing) NSAnimationContext.runAnimationGroup { ctx in ctx.duration = 0.2 ctx.timingFunction = timing animator().setFrame(newFrame, display: true) } } else { setFrame(newFrame, display: true) } }}
final class MenuBandPopoverChrome: NSView { /// The single backdrop surface for the whole popover (body + arrow). /// On macOS 26 this is a real liquid-glass `NSGlassEffectView`; on /// older systems it falls back to the `.popover` `NSVisualEffectView`. /// Either way a `CAShapeLayer` mask carves the popover silhouette so /// the surface flows continuously from the arrow tip into the body. private let backdrop: NSView /// Non-nil only on the legacy (pre-26) path, for appearance re-tints. private let legacyVisualEffect: NSVisualEffectView? private let content: NSView private let maskLayer = CAShapeLayer() /// Rounds the content view's own corners to match the backdrop /// silhouette. Without it the masked glass backdrop is rounded but /// the content (a plain rectangle on top) shows sharp corners. private let contentMaskLayer = CAShapeLayer() private var arrowOffsetFromLeft: CGFloat = MenuBandPopoverPanel.cornerRadius + MenuBandPopoverPanel.arrowWidth / 2
init(content: NSView) { self.content = content // Prefer the dedicated liquid-glass view when the platform (and // the user's style override) allow it; otherwise the classic // vibrancy material. if PianoWaveformWindowStyle.shouldUseLiquidGlass, #available(macOS 26.0, *) { let glass = NSGlassEffectView() glass.style = .clear self.backdrop = glass self.legacyVisualEffect = nil } else { let ve = NSVisualEffectView() ve.material = .popover ve.blendingMode = .behindWindow ve.state = .active self.backdrop = ve self.legacyVisualEffect = ve } super.init(frame: .zero) wantsLayer = true layer?.masksToBounds = false
backdrop.wantsLayer = true backdrop.translatesAutoresizingMaskIntoConstraints = false addSubview(backdrop)
content.translatesAutoresizingMaskIntoConstraints = false content.wantsLayer = true addSubview(content)
NSLayoutConstraint.activate([ backdrop.leadingAnchor.constraint(equalTo: leadingAnchor), backdrop.trailingAnchor.constraint(equalTo: trailingAnchor), backdrop.topAnchor.constraint(equalTo: topAnchor), backdrop.bottomAnchor.constraint(equalTo: bottomAnchor), // Content sits in the body region (below the arrow strip). content.leadingAnchor.constraint(equalTo: leadingAnchor), content.trailingAnchor.constraint(equalTo: trailingAnchor), content.topAnchor.constraint( equalTo: topAnchor, constant: MenuBandPopoverPanel.arrowHeight), content.bottomAnchor.constraint(equalTo: bottomAnchor), ])
backdrop.layer?.mask = maskLayer content.layer?.mask = contentMaskLayer }
/// Force the legacy NSVisualEffectView to re-tint against the /// current effective appearance. AppDelegate calls this from /// `systemAppearanceChanged()` because NSVisualEffectView can latch /// on to the appearance present at construction time — without this /// nudge, the popover's "popover" material keeps showing the old /// tint after a system flip. The liquid-glass view re-tints itself, /// so this is a no-op on the macOS 26 path. func refreshAppearance() { guard let ve = legacyVisualEffect else { needsDisplay = true; return } ve.appearance = nil // Toggling the state forces AppKit to rebuild the material's // CALayer backing, picking up the new effectiveAppearance. ve.state = .inactive ve.state = .active ve.needsDisplay = true needsDisplay = true }
override func viewDidChangeEffectiveAppearance() { super.viewDidChangeEffectiveAppearance() refreshAppearance() }
required init?(coder: NSCoder) { fatalError() }
override var isFlipped: Bool { false }
func setArrowOffsetFromLeft(_ offset: CGFloat) { // Clamp so the arrow fits between the rounded body corners. let minX = MenuBandPopoverPanel.cornerRadius + MenuBandPopoverPanel.arrowWidth / 2 + 2 let maxX = bounds.width - MenuBandPopoverPanel.cornerRadius - MenuBandPopoverPanel.arrowWidth / 2 - 2 arrowOffsetFromLeft = max(minX, min(maxX, offset)) rebuildMask() }
override func layout() { super.layout() rebuildMask() }
private func rebuildMask() { let size = bounds.size guard size.width > 0, size.height > 0 else { return } maskLayer.path = bodyArrowPath(for: size) maskLayer.frame = bounds
// Clip the content view to the same rounded body rect so its // corners match the backdrop instead of showing sharp edges. let cb = content.bounds if cb.width > 0, cb.height > 0 { contentMaskLayer.path = CGPath( roundedRect: cb, cornerWidth: MenuBandPopoverPanel.cornerRadius, cornerHeight: MenuBandPopoverPanel.cornerRadius, transform: nil) contentMaskLayer.frame = cb } }
/// Build the continuous body+arrow silhouette path for a given panel /// `size` (chrome-local coords, origin bottom-left). Extracted from /// `rebuildMask` so the mask can be *animated* to a new size (see /// `animateMask(toPanelSize:duration:timing:)`) rather than only /// rebuilt synchronously in `layout()` — which is what made the /// instrument-picker open snap instead of slide. private func bodyArrowPath(for size: CGSize) -> CGPath { let arrowH = MenuBandPopoverPanel.arrowHeight let arrowW = MenuBandPopoverPanel.arrowWidth let radius = MenuBandPopoverPanel.cornerRadius
// Body rect: the rounded rectangle below the arrow strip. // The arrow is drawn as a small triangle attached to the body's // top edge, so the mask is one continuous path. let bodyTop = size.height - arrowH let arrowTipX = max(arrowOffsetFromLeft, radius + arrowW / 2 + 2)
let path = CGMutablePath() // Bottom-left → bottom-right with rounded corners path.move(to: CGPoint(x: 0, y: radius)) path.addArc(tangent1End: CGPoint(x: 0, y: 0), tangent2End: CGPoint(x: radius, y: 0), radius: radius) path.addLine(to: CGPoint(x: size.width - radius, y: 0)) path.addArc(tangent1End: CGPoint(x: size.width, y: 0), tangent2End: CGPoint(x: size.width, y: radius), radius: radius) // Right side up to body top path.addLine(to: CGPoint(x: size.width, y: bodyTop - radius)) path.addArc(tangent1End: CGPoint(x: size.width, y: bodyTop), tangent2End: CGPoint(x: size.width - radius, y: bodyTop), radius: radius) // Body top edge → arrow base right path.addLine(to: CGPoint(x: arrowTipX + arrowW / 2, y: bodyTop)) // Arrow tip path.addLine(to: CGPoint(x: arrowTipX, y: size.height)) // Arrow base left path.addLine(to: CGPoint(x: arrowTipX - arrowW / 2, y: bodyTop)) // Continue body top edge to top-left corner radius path.addLine(to: CGPoint(x: radius, y: bodyTop)) path.addArc(tangent1End: CGPoint(x: 0, y: bodyTop), tangent2End: CGPoint(x: 0, y: bodyTop - radius), radius: radius) path.closeSubpath() return path }
/// Animate the liquid-glass silhouette + content clip to a new panel /// size in lockstep with the window's frame animation, so the body /// grows/shrinks fluidly (instrument-picker slide) instead of the /// mask snapping to the final shape while the frame catches up — the /// desync that read as "the whole popover moves." func animateMask(toPanelSize size: CGSize, duration: TimeInterval, timing: CAMediaTimingFunction) { let radius = MenuBandPopoverPanel.cornerRadius let bodyTarget = bodyArrowPath(for: size) let contentRect = CGRect( x: 0, y: 0, width: size.width, height: max(0, size.height - MenuBandPopoverPanel.arrowHeight)) let contentTarget = CGPath(roundedRect: contentRect, cornerWidth: radius, cornerHeight: radius, transform: nil)
func animate(_ layer: CAShapeLayer, to target: CGPath, frame: CGRect) { let anim = CABasicAnimation(keyPath: "path") anim.fromValue = layer.presentation()?.path ?? layer.path anim.toValue = target anim.duration = duration anim.timingFunction = timing layer.path = target layer.frame = frame layer.add(anim, forKey: "maskResize") } animate(maskLayer, to: bodyTarget, frame: CGRect(origin: .zero, size: size)) animate(contentMaskLayer, to: contentTarget, frame: contentRect) }}