Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add menubar waveform strip that slides below piano on note activity

CVDisplayLink-driven slide animation (NSAnimationContext animator proxy
does not reliably move borderless non-activating panels). Panel level set
below mainMenu so the menubar occludes it during slide. Suppressed when
settings popover or floating palette is visible. Auto-hides 2s after
last note release.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

authored by

Esteban Uribe
Claude Opus 4.6
and committed by
prompt.ac/@jeffrey
c738bee1 8f1ae496

+295
+22
slab/menuband/Sources/MenuBand/AppDelegate.swift
··· 14 14 private let popover = NSPopover() 15 15 private var popoverVC: MenuBandPopoverViewController? 16 16 private lazy var floatingPlayPalette = FloatingPlayPaletteController(menuBand: menuBand) 17 + private lazy var waveformStrip = MenuBarWaveformStrip(menuBand: menuBand) 17 18 private var appBeforePopover: NSRunningApplication? 18 19 private var appBeforeFocusCapture: NSRunningApplication? 19 20 private var focusCaptureArmedByShortcut = false ··· 135 136 self.updateIcon() 136 137 self.popoverVC?.refreshHeldNotes() 137 138 self.floatingPlayPalette.refresh() 139 + self.updateWaveformStrip() 138 140 } 139 141 menuBand.bootstrap() 140 142 floatingPlayPalette.onDismiss = { [weak self] in 141 143 self?.updateIcon() 142 144 self?.popoverVC?.syncFromController() 145 + self?.updateWaveformStripSuppression() 143 146 } 144 147 floatingPlayPalette.isPianoFocusActive = { [weak self] in 145 148 self?.localCapture.isArmed ?? false ··· 423 426 if menuBand.typeMode { 424 427 menuBand.disableTypeModeForFocusCapture() 425 428 } 429 + updateWaveformStripSuppression() 426 430 } 427 431 428 432 private func toggleFocusCaptureFromShortcut() { ··· 568 572 } 569 573 570 574 func applicationWillTerminate(_ notification: Notification) { 575 + waveformStrip.dismiss() 571 576 floatingPlayPalette.dismiss(reason: .programmatic) 572 577 menuBand.shutdown() 573 578 } ··· 992 997 if let m = clickAwayMonitor { NSEvent.removeMonitor(m); clickAwayMonitor = nil } 993 998 if let m = popoverEscMonitor { NSEvent.removeMonitor(m); popoverEscMonitor = nil } 994 999 appBeforePopover = nil 1000 + updateWaveformStripSuppression() 995 1001 } 996 1002 997 1003 /// Distributed-notification entry point for the dev affordance ··· 1036 1042 : frontmost 1037 1043 NSApp.activate(ignoringOtherApps: true) 1038 1044 popover.show(relativeTo: anchor, of: button, preferredEdge: .minY) 1045 + updateWaveformStripSuppression() 1039 1046 DispatchQueue.main.async { 1040 1047 self.popover.contentViewController?.view.window?.makeKey() 1041 1048 } ··· 1064 1071 } 1065 1072 } 1066 1073 } 1074 + } 1075 + 1076 + // MARK: - Menubar waveform strip 1077 + 1078 + private func updateWaveformStrip() { 1079 + if !menuBand.litNotes.isEmpty { 1080 + waveformStrip.reposition(statusItemButton: statusItem.button) 1081 + waveformStrip.showIfNeeded() 1082 + } else { 1083 + waveformStrip.scheduleHide() 1084 + } 1085 + } 1086 + 1087 + private func updateWaveformStripSuppression() { 1088 + waveformStrip.suppressed = popover.isShown || floatingPlayPalette.isShown 1067 1089 } 1068 1090 }
+273
slab/menuband/Sources/MenuBand/MenuBarWaveformStrip.swift
··· 1 + import AppKit 2 + 3 + /// Thin floating waveform strip that slides down from beneath the menubar 4 + /// piano when notes are played, and slides back up after a configurable 5 + /// idle delay. Hidden whenever the settings popover or floating piano 6 + /// palette is visible to avoid visual clutter. 7 + /// 8 + /// The panel sits one level below `NSWindow.Level.mainMenu` so the menubar 9 + /// itself occludes it. The slide animation moves the panel from behind the 10 + /// menubar downward into view, and reverses to hide. 11 + final class MenuBarWaveformStrip { 12 + private let menuBand: MenuBandController 13 + private let waveformView = WaveformView() 14 + private var panel: NSPanel? 15 + private weak var statusButton: NSStatusBarButton? 16 + 17 + /// How long to keep the strip visible after the last note ends. 18 + private let hideDelay: TimeInterval = 2.0 19 + private var hideWorkItem: DispatchWorkItem? 20 + 21 + /// Strip height in points. 22 + private static let stripHeight: CGFloat = 36 23 + 24 + /// Animation duration in seconds. 25 + private static let slideDuration: TimeInterval = 0.22 26 + 27 + /// Window level just below the menubar so the menubar occludes the 28 + /// strip while it's tucked behind. 29 + private static let stripLevel = NSWindow.Level(rawValue: NSWindow.Level.mainMenu.rawValue - 1) 30 + 31 + /// CVDisplayLink-driven slide animation. NSAnimationContext + animator() 32 + /// proxy doesn't reliably move borderless non-activating panels, so we 33 + /// drive the frame ourselves at display refresh rate. 34 + private var slideLink: CVDisplayLink? 35 + private var slideStartTime: CFTimeInterval = 0 36 + private var slideFromY: CGFloat = 0 37 + private var slideToY: CGFloat = 0 38 + private var slideCompletion: (() -> Void)? 39 + private var isSliding = false 40 + 41 + /// True while the strip panel is on screen (visible or animating in). 42 + var isShown: Bool { panel?.isVisible == true } 43 + 44 + /// External suppression — callers set this when the popover or floating 45 + /// palette opens. While suppressed, `showIfNeeded` is a no-op and any 46 + /// visible strip is immediately hidden. 47 + var suppressed: Bool = false { 48 + didSet { 49 + guard suppressed != oldValue else { return } 50 + if suppressed { hide(animated: false) } 51 + } 52 + } 53 + 54 + init(menuBand: MenuBandController) { 55 + self.menuBand = menuBand 56 + waveformView.menuBand = menuBand 57 + } 58 + 59 + deinit { 60 + stopSlide() 61 + } 62 + 63 + // MARK: - Show / hide 64 + 65 + /// Call whenever `litNotes` becomes non-empty (a note starts). 66 + func showIfNeeded() { 67 + guard !suppressed else { return } 68 + cancelPendingHide() 69 + if !isShown && !isSliding { show() } 70 + } 71 + 72 + /// Call whenever `litNotes` becomes empty (all notes released). 73 + func scheduleHide() { 74 + cancelPendingHide() 75 + let work = DispatchWorkItem { [weak self] in 76 + self?.hide(animated: true) 77 + } 78 + hideWorkItem = work 79 + DispatchQueue.main.asyncAfter(deadline: .now() + hideDelay, execute: work) 80 + } 81 + 82 + /// Immediately tear down (app termination, etc.). 83 + func dismiss() { 84 + cancelPendingHide() 85 + stopSlide() 86 + waveformView.isLive = false 87 + panel?.orderOut(nil) 88 + } 89 + 90 + /// Store the button reference so positioning works from inside show(). 91 + func reposition(statusItemButton: NSStatusBarButton?) { 92 + statusButton = statusItemButton 93 + guard let panel = panel, panel.isVisible, !isSliding else { return } 94 + positionPanel(panel) 95 + } 96 + 97 + // MARK: - Geometry 98 + 99 + /// Compute the target frame for the strip: directly below the menubar, 100 + /// aligned to the piano key region of the status item. 101 + private func targetFrame() -> NSRect? { 102 + guard let button = statusButton, 103 + let buttonWindow = button.window else { return nil } 104 + 105 + let imgSize = KeyboardIconRenderer.imageSize 106 + let bb = button.bounds 107 + let xOff = (bb.width - imgSize.width) / 2.0 108 + 109 + let pianoOriginX = xOff + KeyboardIconRenderer.pad 110 + let pianoWidth = imgSize.width - KeyboardIconRenderer.settingsW 111 + - KeyboardIconRenderer.settingsGap - KeyboardIconRenderer.pad * 2 112 + 113 + let localRect = NSRect(x: pianoOriginX, y: 0, width: pianoWidth, height: bb.height) 114 + let windowRect = button.convert(localRect, to: nil) 115 + let screenRect = buttonWindow.convertToScreen(windowRect) 116 + 117 + return NSRect( 118 + x: screenRect.origin.x, 119 + y: screenRect.origin.y - Self.stripHeight, 120 + width: screenRect.width, 121 + height: Self.stripHeight 122 + ) 123 + } 124 + 125 + // MARK: - Slide animation 126 + 127 + private func show() { 128 + if panel == nil { buildPanel() } 129 + guard let panel = panel, let target = targetFrame() else { return } 130 + 131 + // Start tucked behind the menubar (origin shifted up by strip height). 132 + let hiddenY = target.origin.y + target.height 133 + panel.setFrame(NSRect(x: target.origin.x, y: hiddenY, 134 + width: target.width, height: target.height), 135 + display: true) 136 + waveformView.isLive = true 137 + panel.orderFrontRegardless() 138 + 139 + // Slide down to the target position. 140 + startSlide(fromY: hiddenY, toY: target.origin.y) { [weak self] in 141 + // Ensure we land exactly at the target. 142 + panel.setFrameOrigin(target.origin) 143 + self?.isSliding = false 144 + } 145 + } 146 + 147 + private func hide(animated: Bool) { 148 + cancelPendingHide() 149 + stopSlide() 150 + guard let panel = panel, panel.isVisible else { 151 + waveformView.isLive = false 152 + return 153 + } 154 + if !animated { 155 + waveformView.isLive = false 156 + panel.orderOut(nil) 157 + return 158 + } 159 + // Slide back up behind the menubar. 160 + let currentY = panel.frame.origin.y 161 + let hiddenY = currentY + panel.frame.height 162 + startSlide(fromY: currentY, toY: hiddenY) { [weak self] in 163 + self?.waveformView.isLive = false 164 + panel.orderOut(nil) 165 + self?.isSliding = false 166 + } 167 + } 168 + 169 + private func startSlide(fromY: CGFloat, toY: CGFloat, completion: @escaping () -> Void) { 170 + stopSlide() 171 + isSliding = true 172 + slideFromY = fromY 173 + slideToY = toY 174 + slideStartTime = CACurrentMediaTime() 175 + slideCompletion = completion 176 + 177 + var link: CVDisplayLink? 178 + CVDisplayLinkCreateWithActiveCGDisplays(&link) 179 + guard let link = link else { 180 + // Fallback: jump immediately if CVDisplayLink fails. 181 + panel?.setFrameOrigin(NSPoint(x: panel?.frame.origin.x ?? 0, y: toY)) 182 + completion() 183 + return 184 + } 185 + let opaque = Unmanaged.passUnretained(self).toOpaque() 186 + CVDisplayLinkSetOutputCallback(link, { _, _, _, _, _, ctx -> CVReturn in 187 + guard let ctx = ctx else { return kCVReturnSuccess } 188 + let strip = Unmanaged<MenuBarWaveformStrip>.fromOpaque(ctx).takeUnretainedValue() 189 + DispatchQueue.main.async { strip.tickSlide() } 190 + return kCVReturnSuccess 191 + }, opaque) 192 + CVDisplayLinkStart(link) 193 + slideLink = link 194 + } 195 + 196 + private func tickSlide() { 197 + guard isSliding, let panel = panel else { 198 + stopSlide() 199 + return 200 + } 201 + let elapsed = CACurrentMediaTime() - slideStartTime 202 + let t = min(1.0, elapsed / Self.slideDuration) 203 + // Ease-out cubic for show, ease-in cubic for hide. 204 + let eased: CGFloat 205 + if slideToY < slideFromY { 206 + // Sliding down (show): ease-out 207 + let f = 1.0 - t 208 + eased = CGFloat(1.0 - f * f * f) 209 + } else { 210 + // Sliding up (hide): ease-in 211 + eased = CGFloat(t * t * t) 212 + } 213 + let currentY = slideFromY + (slideToY - slideFromY) * eased 214 + panel.setFrameOrigin(NSPoint(x: panel.frame.origin.x, y: currentY)) 215 + 216 + if t >= 1.0 { 217 + let completion = slideCompletion 218 + stopSlide() 219 + completion?() 220 + } 221 + } 222 + 223 + private func stopSlide() { 224 + if let link = slideLink { 225 + CVDisplayLinkStop(link) 226 + slideLink = nil 227 + } 228 + slideCompletion = nil 229 + } 230 + 231 + private func cancelPendingHide() { 232 + hideWorkItem?.cancel() 233 + hideWorkItem = nil 234 + } 235 + 236 + // MARK: - Panel construction 237 + 238 + private func buildPanel() { 239 + let p = NSPanel( 240 + contentRect: NSRect(origin: .zero, size: NSSize(width: 200, height: Self.stripHeight)), 241 + styleMask: [.borderless, .nonactivatingPanel], 242 + backing: .buffered, 243 + defer: false 244 + ) 245 + p.isOpaque = true 246 + p.backgroundColor = .black 247 + p.hasShadow = true 248 + p.level = Self.stripLevel 249 + p.collectionBehavior = [.transient, .ignoresCycle] 250 + p.hidesOnDeactivate = false 251 + p.canHide = false 252 + p.isMovable = false 253 + p.animationBehavior = .none 254 + p.acceptsMouseMovedEvents = false 255 + 256 + waveformView.translatesAutoresizingMaskIntoConstraints = false 257 + p.contentView = NSView() 258 + p.contentView!.addSubview(waveformView) 259 + NSLayoutConstraint.activate([ 260 + waveformView.leadingAnchor.constraint(equalTo: p.contentView!.leadingAnchor), 261 + waveformView.trailingAnchor.constraint(equalTo: p.contentView!.trailingAnchor), 262 + waveformView.topAnchor.constraint(equalTo: p.contentView!.topAnchor), 263 + waveformView.bottomAnchor.constraint(equalTo: p.contentView!.bottomAnchor), 264 + ]) 265 + 266 + panel = p 267 + } 268 + 269 + private func positionPanel(_ panel: NSPanel) { 270 + guard let target = targetFrame() else { return } 271 + panel.setFrame(target, display: true) 272 + } 273 + }