From a2831d44568424b9bb4ac73ca9636c56330f6b4a Mon Sep 17 00:00:00 2001 From: Inzimam Date: Fri, 26 Jun 2026 08:40:06 +0000 Subject: [PATCH] Redraw menu bar icon to match the new Fluent brand The menu bar status item was never derived from AppIcon. The idle glyph was the SF Symbol "waveform" (release) or a hand-drawn rounded-square-with-bars (dev), so updating AppIcon-Source.png / AppIcon.icns had no effect on it. Replace StampedMenuBarIcon with FluentMenuBarIcon: a monochrome template glyph that mirrors the new app icon (a speech-bubble ring with a flowing waveform). It is used for the idle state in both release and dev builds, with a small marker dot distinguishing the dev build. Recording/transcribing keep their record.circle / ellipsis.circle status icons. Drawn as a template image so macOS tints it for light/dark menu bars; a full-color icon is not appropriate for a menu bar item. --- Sources/App.swift | 97 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 66 insertions(+), 31 deletions(-) diff --git a/Sources/App.swift b/Sources/App.swift index 4f7982d6..69a8d194 100644 --- a/Sources/App.swift +++ b/Sources/App.swift @@ -23,53 +23,88 @@ struct MenuBarLabel: View { @EnvironmentObject var appState: AppState @ObservedObject var notificationManager = VocabularyNotificationManager.shared - private var iconName: String { - if appState.isRecording { return "record.circle" } - if appState.isTranscribing { return "ellipsis.circle" } - return "waveform" - } - var body: some View { HStack(spacing: 4) { if notificationManager.showCheckmark { Image(systemName: "checkmark") } - if AppBuild.isDevBundle && !appState.isRecording && !appState.isTranscribing { - Image(nsImage: StampedMenuBarIcon.templateImage) - .renderingMode(.template) + if appState.isRecording { + Image(systemName: "record.circle") + } else if appState.isTranscribing { + Image(systemName: "ellipsis.circle") } else { - Image(systemName: iconName) + // Idle: the Fluent brand glyph (speech bubble + waveform). + // Dev builds get a small marker dot so they're easy to tell + // apart from a release build running side by side. + Image(nsImage: AppBuild.isDevBundle + ? FluentMenuBarIcon.devTemplateImage + : FluentMenuBarIcon.templateImage) + .renderingMode(.template) } } .animation(.easeInOut(duration: 0.2), value: notificationManager.showCheckmark) } } -enum StampedMenuBarIcon { - static let templateImage: NSImage = { +/// The menu bar status icon, drawn as a monochrome template so macOS can tint +/// it correctly for light and dark menu bars. It is a line-art interpretation +/// of the Fluent app icon: a speech-bubble ring with a flowing waveform inside. +/// The app icon (`AppIcon.icns`) is the full-color dock/Finder icon and is not +/// used here, because menu bar items should be simple monochrome glyphs. +enum FluentMenuBarIcon { + static let templateImage: NSImage = makeImage(markDev: false) + static let devTemplateImage: NSImage = makeImage(markDev: true) + + private static func makeImage(markDev: Bool) -> NSImage { let size = NSSize(width: 18, height: 16) - let image = NSImage(size: size, flipped: false) { rect in - let path = NSBezierPath() - path.windingRule = .evenOdd - path.append(NSBezierPath(roundedRect: rect, xRadius: 3, yRadius: 3)) - let bars: [(x: CGFloat, y: CGFloat, h: CGFloat)] = [ - (3.0, 7.0, 2.0), - (5.5, 5.0, 6.0), - (8.0, 3.0, 10.0), - (10.5, 4.0, 8.0), - (13.0, 6.0, 4.0), - ] - for bar in bars { - path.append(NSBezierPath( - roundedRect: NSRect(x: bar.x, y: bar.y, width: 1.5, height: bar.h), - xRadius: 0.75, yRadius: 0.75 - )) - } + let image = NSImage(size: size, flipped: false) { _ in + NSColor.black.setStroke() NSColor.black.setFill() - path.fill() + + let center = NSPoint(x: 8.8, y: 8.7) + let radius: CGFloat = 6.0 + + // Speech-bubble ring, left slightly open at the lower-left so the + // tail can flow out of it like the logo. + let ring = NSBezierPath() + ring.appendArc(withCenter: center, radius: radius, + startAngle: 232, endAngle: 200, clockwise: false) + ring.lineWidth = 1.5 + ring.lineCapStyle = .round + ring.stroke() + + // Tail: a short flick out of the lower-left of the bubble. + let tail = NSBezierPath() + tail.move(to: NSPoint(x: 4.7, y: 4.3)) + tail.curve(to: NSPoint(x: 2.7, y: 2.4), + controlPoint1: NSPoint(x: 4.0, y: 3.4), + controlPoint2: NSPoint(x: 3.5, y: 2.8)) + tail.lineWidth = 1.5 + tail.lineCapStyle = .round + tail.stroke() + + // Waveform squiggle across the middle, echoing the logo's flowing + // line: a low start, rising to a tall central peak, then easing down. + let wave = NSBezierPath() + wave.move(to: NSPoint(x: 5.0, y: 7.9)) + wave.line(to: NSPoint(x: 6.6, y: 10.4)) + wave.line(to: NSPoint(x: 7.9, y: 8.0)) + wave.line(to: NSPoint(x: 9.2, y: 11.1)) + wave.line(to: NSPoint(x: 10.7, y: 8.0)) + wave.line(to: NSPoint(x: 12.6, y: 9.6)) + wave.lineWidth = 1.5 + wave.lineJoinStyle = .round + wave.lineCapStyle = .round + wave.stroke() + + if markDev { + // Small filled dot at the top-right to denote the dev build. + NSBezierPath(ovalIn: NSRect(x: 14.0, y: 12.0, width: 3.2, height: 3.2)).fill() + } + return true } image.isTemplate = true return image - }() + } }