From c990ea558bd0c69d97ec719ddffb964fd58b6625 Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Thu, 13 Aug 2026 14:20:06 +0200 Subject: [PATCH] =?UTF-8?q?Blitz=20blendet=20zwischen=20zwei=20fertigen=20?= =?UTF-8?q?Fassungen=20=C3=BCber?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deine Idee war die bessere. Eine helle Fläche mit dunklem Rand ließ sich aus einem einzelnen Symbol nur nachbauen, indem dasselbe Zeichen zweimal in verschiedener Größe übereinander gezeichnet wurde — und die Größenverhältnisse sahen nie stimmig aus, wie man an den drei Anläufen sieht. Zwei fertige Fassungen ineinander zu blenden ist der geradere Weg: gezeichnet wird genau das, was in der Datei steht. Erst die dunkle voll deckend, dann die helle mit steigender Deckkraft darüber. Zeichnete man beide anteilig, schiene dazwischen der Hintergrund durch und der Blitz wirkte blass. Ohne eigenen Rand bekommt der Blitz den vollen Innenraum des Akkus statt Innenraum minus Rand — er ist damit wieder etwas größer, bleibt aber innerhalb des Rahmens. Der Test hält die neue Grenze fest. Co-Authored-By: Claude Opus 5 --- Onyx.xcodeproj/project.pbxproj | 12 ++- Onyx/bolt-dark.svg | 40 ++++++++++ Onyx/bolt-light.svg | 1 + Onyx/thunder.svg | 27 ------- .../MetricsProvider/BatteryGlyph.swift | 80 +++++++++---------- .../BatteryGlyphTests.swift | 11 +-- 6 files changed, 94 insertions(+), 77 deletions(-) create mode 100644 Onyx/bolt-dark.svg create mode 100644 Onyx/bolt-light.svg delete mode 100644 Onyx/thunder.svg diff --git a/Onyx.xcodeproj/project.pbxproj b/Onyx.xcodeproj/project.pbxproj index 9ff6bed..14c43ab 100644 --- a/Onyx.xcodeproj/project.pbxproj +++ b/Onyx.xcodeproj/project.pbxproj @@ -16,7 +16,9 @@ 378A5A417112CCB31EF3F774 /* OnyxNotch in Frameworks */ = {isa = PBXBuildFile; productRef = 3A32CAE6CC4D01D660F0016F /* OnyxNotch */; }; 3991CB2BF41C6C014EF4528F /* chip.svg in Resources */ = {isa = PBXBuildFile; fileRef = 085D860E654E09E4CD6E535A /* chip.svg */; }; 39E29ADC71D4860543AD25EB /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45BD2DF1B775C733C8F75635 /* SettingsView.swift */; }; + 3C7D32A39B906BD7385B1231 /* bolt-light.svg in Resources */ = {isa = PBXBuildFile; fileRef = B8214C5599D1857E832447EC /* bolt-light.svg */; }; 4E729C7A6498B4679C8D2C42 /* OnyxCore in Frameworks */ = {isa = PBXBuildFile; productRef = C64A111F449D0F9D36ED46FB /* OnyxCore */; }; + 574FDD46FEBAC96E38CDFC7A /* bolt-dark.svg in Resources */ = {isa = PBXBuildFile; fileRef = 7A866E72010907DF4BEF025C /* bolt-dark.svg */; }; 5BE38CAFCDA4B9CBE787CABA /* CalendarProvider in Frameworks */ = {isa = PBXBuildFile; productRef = A3E3949D664131D593CBEEDC /* CalendarProvider */; }; 68EE5495435A82E20248EBC2 /* OnyxHelperProtocol in Frameworks */ = {isa = PBXBuildFile; productRef = 91C048471C023EC8A82B1BD3 /* OnyxHelperProtocol */; }; 6A972372414D6EFDC81E2661 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = B442E8A39C37C8FD92B4CA83 /* main.swift */; }; @@ -27,7 +29,6 @@ 9C2E419014B7E04630FDF151 /* OnyxApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89CAC2A7DA89B8707D45D65 /* OnyxApp.swift */; }; 9EC9D57A96489F5578000822 /* NetworkProvider in Frameworks */ = {isa = PBXBuildFile; productRef = FF4E99C8BF0D8944C314A8AF /* NetworkProvider */; }; A529046C33E6B00C0E9508FF /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 508DECB5C05264E6130C373E /* Localizable.xcstrings */; }; - A89903046D47175EE68392FA /* thunder.svg in Resources */ = {isa = PBXBuildFile; fileRef = 19F96370F33A525437F6E691 /* thunder.svg */; }; A9617230F602C5D52F63A7E2 /* ShelfProvider in Frameworks */ = {isa = PBXBuildFile; productRef = FA6C9F90DF47DC0DFB9ED55D /* ShelfProvider */; }; B0FCCD92AF22F1636482EACD /* sound-waves.svg in Resources */ = {isa = PBXBuildFile; fileRef = 8BCDFD427ACD83B8AA7BA39E /* sound-waves.svg */; }; B309DD8966B5348250CEC6BD /* LaunchAtLogin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41C778876BEF3D8B04F7C3C9 /* LaunchAtLogin.swift */; }; @@ -59,7 +60,6 @@ 0804288DC4A8146DD9F3FC3E /* Onyx.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Onyx.app; sourceTree = BUILT_PRODUCTS_DIR; }; 085D860E654E09E4CD6E535A /* chip.svg */ = {isa = PBXFileReference; path = chip.svg; sourceTree = ""; }; 16F2B1B9A74594526F34F5C9 /* WidgetOptionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetOptionsView.swift; sourceTree = ""; }; - 19F96370F33A525437F6E691 /* thunder.svg */ = {isa = PBXFileReference; path = thunder.svg; sourceTree = ""; }; 1E6A1C208C7EA56971A055E1 /* graphic-card.svg */ = {isa = PBXFileReference; path = "graphic-card.svg"; sourceTree = ""; }; 24ADD22F5CE30B27020B479B /* MenuBarIcon.svg */ = {isa = PBXFileReference; path = MenuBarIcon.svg; sourceTree = ""; }; 41C778876BEF3D8B04F7C3C9 /* LaunchAtLogin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchAtLogin.swift; sourceTree = ""; }; @@ -70,6 +70,7 @@ 5FCD3B382484AE4E9EF2F4B6 /* OnboardingWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingWindowController.swift; sourceTree = ""; }; 6115888EE7738877C9304138 /* MixerMenuBarModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MixerMenuBarModule.swift; sourceTree = ""; }; 63A5510538846C8427AEB831 /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = ""; }; + 7A866E72010907DF4BEF025C /* bolt-dark.svg */ = {isa = PBXFileReference; path = "bolt-dark.svg"; sourceTree = ""; }; 821B742C211265914B6CC44B /* OnyxKit */ = {isa = PBXFileReference; lastKnownFileType = folder; name = OnyxKit; path = Packages/OnyxKit; sourceTree = SOURCE_ROOT; }; 87597F09E971C14CF1E475DB /* Onyx.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Onyx.entitlements; sourceTree = ""; }; 89309D9F5722FA1B91CF2905 /* FanWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FanWidget.swift; sourceTree = ""; }; @@ -77,6 +78,7 @@ 8BCDFD427ACD83B8AA7BA39E /* sound-waves.svg */ = {isa = PBXFileReference; path = "sound-waves.svg"; sourceTree = ""; }; 8E43384BA603C866D148D9D8 /* SettingsWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsWindowController.swift; sourceTree = ""; }; B442E8A39C37C8FD92B4CA83 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; + B8214C5599D1857E832447EC /* bolt-light.svg */ = {isa = PBXFileReference; path = "bolt-light.svg"; sourceTree = ""; }; B83C4E3CB821F903F3977E88 /* AppModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppModel.swift; sourceTree = ""; }; F12041E330231599C6540C21 /* FanSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FanSettingsView.swift; sourceTree = ""; }; F89CAC2A7DA89B8707D45D65 /* OnyxApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnyxApp.swift; sourceTree = ""; }; @@ -164,6 +166,8 @@ 0FBA13B97C53F1D0D740A633 /* Resources */, 055263008CB3306A8D09E6D4 /* AppIcon.icon */, B83C4E3CB821F903F3977E88 /* AppModel.swift */, + 7A866E72010907DF4BEF025C /* bolt-dark.svg */, + B8214C5599D1857E832447EC /* bolt-light.svg */, 085D860E654E09E4CD6E535A /* chip.svg */, FC3FDFD387188D36AF8287DE /* FanControl.swift */, F12041E330231599C6540C21 /* FanSettingsView.swift */, @@ -181,7 +185,6 @@ 45BD2DF1B775C733C8F75635 /* SettingsView.swift */, 8E43384BA603C866D148D9D8 /* SettingsWindowController.swift */, 8BCDFD427ACD83B8AA7BA39E /* sound-waves.svg */, - 19F96370F33A525437F6E691 /* thunder.svg */, 16F2B1B9A74594526F34F5C9 /* WidgetOptionsView.swift */, ); path = Onyx; @@ -295,10 +298,11 @@ FF97F56CE1C734E59F1B40F4 /* AppIcon.icon in Resources */, A529046C33E6B00C0E9508FF /* Localizable.xcstrings in Resources */, 7CE2EDC6BC58678269BF27AD /* MenuBarIcon.svg in Resources */, + 574FDD46FEBAC96E38CDFC7A /* bolt-dark.svg in Resources */, + 3C7D32A39B906BD7385B1231 /* bolt-light.svg in Resources */, 3991CB2BF41C6C014EF4528F /* chip.svg in Resources */, F2A3AD184864422EA14A8C64 /* graphic-card.svg in Resources */, B0FCCD92AF22F1636482EACD /* sound-waves.svg in Resources */, - A89903046D47175EE68392FA /* thunder.svg in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Onyx/bolt-dark.svg b/Onyx/bolt-dark.svg new file mode 100644 index 0000000..6738438 --- /dev/null +++ b/Onyx/bolt-dark.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Onyx/bolt-light.svg b/Onyx/bolt-light.svg new file mode 100644 index 0000000..4979b81 --- /dev/null +++ b/Onyx/bolt-light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Onyx/thunder.svg b/Onyx/thunder.svg deleted file mode 100644 index 669523e..0000000 --- a/Onyx/thunder.svg +++ /dev/null @@ -1,27 +0,0 @@ - - - - diff --git a/Packages/OnyxKit/Sources/MetricsProvider/BatteryGlyph.swift b/Packages/OnyxKit/Sources/MetricsProvider/BatteryGlyph.swift index e529483..680d40e 100644 --- a/Packages/OnyxKit/Sources/MetricsProvider/BatteryGlyph.swift +++ b/Packages/OnyxKit/Sources/MetricsProvider/BatteryGlyph.swift @@ -68,16 +68,22 @@ public enum BatteryGlyph { return (filled, empty) } - /// Der Blitz als eigene Vorlage. + /// Der Blitz in zwei Fassungen, hell und dunkel. /// - /// SF Symbols' `bolt.fill` ist eine massive Fläche; die mitgelieferte - /// Kontur trifft die Form besser und bringt schon einen kräftigen Strich - /// mit. Fehlt die Datei, bleibt das Systemsymbol. - public static let chargingGlyph: NSImage? = { - guard let url = Bundle.main.url(forResource: "thunder", withExtension: "svg") + /// **Warum zwei Dateien statt einer eingefärbten:** eine helle Fläche mit + /// dunklem Rand ließ sich aus einem einzelnen Symbol nur nachbauen, indem + /// dasselbe Zeichen zweimal in verschiedener Größe übereinander gezeichnet + /// wurde — und die Größenverhältnisse sahen nie stimmig aus. Zwei fertige + /// Fassungen ineinander zu blenden ist der geradere Weg: gezeichnet wird + /// genau das, was in der Datei steht. + public static let boltLight: NSImage? = load("bolt-light") + public static let boltDark: NSImage? = load("bolt-dark") + + private static func load(_ name: String) -> NSImage? { + guard let url = Bundle.main.url(forResource: name, withExtension: "svg") else { return nil } return NSImage(contentsOf: url) - }() + } /// Wie dick der Rand um das Zeichen ist. public static let outlineWidth: CGFloat = 1 @@ -95,12 +101,11 @@ public enum BatteryGlyph { /// der Außenhöhe ausgeht, landet bei einem Blitz, der den ganzen Akku /// ausfüllt und an den Rahmen stößt. public static func glyphHeight(for indicator: Indicator, in height: CGFloat) -> CGFloat { - let inner = height - 2 * fillInset - let usable = max(inner - 2 * outlineWidth, 1) + let inner = max(height - 2 * fillInset, 1) switch indicator { // Das Pausenzeichen ist von Haus aus höher gebaut als der Blitz. - case .paused: return usable * 0.8 - default: return usable + case .paused: return inner * 0.8 + default: return inner } } @@ -206,25 +211,20 @@ public enum BatteryGlyph { return } - // Der Blitz: mit dunklem Rand, und fadend. + // Der Blitz blendet zwischen den beiden Fassungen über. // - // Der Rand ist immer die Gegenfarbe zum Blitz selbst. Damit bleibt er - // in **jedem** Moment des Fadens lesbar: über der hellen Füllung trägt - // ihn der Rand, über dem leeren Teil der Blitz. + // Erst die dunkle voll deckend, dann die helle mit steigender + // Deckkraft darüber. So ist das Zeichen in jedem Moment vollständig + // gedeckt — zeichnete man beide anteilig, schiene dazwischen der + // Hintergrund durch und der Blitz wirkte blass. let whiteness = boltWhiteness(phase: fadePhase) - let inverseColor = NSColor(white: 1 - whiteness, alpha: 1) - let boltColor = NSColor(white: whiteness, alpha: 1) - - if let custom = chargingGlyph { - MenuBarText.drawTemplate(custom, color: inverseColor, in: outlineRect(glyphRect)) - MenuBarText.drawTemplate(custom, color: boltColor, in: glyphRect) + if let dark = boltDark, let light = boltLight { + dark.draw(in: glyphRect, from: .zero, operation: .sourceOver, fraction: 1) + light.draw(in: glyphRect, from: .zero, operation: .sourceOver, + fraction: whiteness) return } - if let outline = tint(glyph, with: inverseColor) { - outline.draw(in: outlineRect(glyphRect), from: .zero, - operation: .sourceOver, fraction: 1) - } - if let filled = tint(glyph, with: boltColor) { + if let filled = tint(glyph, with: NSColor(white: whiteness, alpha: 1)) { filled.draw(in: glyphRect, from: .zero, operation: .sourceOver, fraction: 1) } } @@ -285,12 +285,7 @@ public struct BatteryGlyphView: View { let phase = context.date.timeIntervalSinceReferenceDate / BatteryGlyph.fadeDuration let whiteness = BatteryGlyph.boltWhiteness(phase: phase) - ZStack { - bolt(name: name, side: size + 2 * BatteryGlyph.outlineWidth) - .foregroundStyle(Color(white: 1 - whiteness)) - bolt(name: name, side: size) - .foregroundStyle(Color(white: whiteness)) - } + bolt(side: size, whiteness: whiteness) } } else { Image(systemName: name) @@ -308,17 +303,20 @@ public struct BatteryGlyphView: View { .frame(width: width, height: height) } - /// Dieselbe Vorlage wie in der Menüleiste, sonst das Systemsymbol. + /// Dieselben zwei Fassungen wie in der Menüleiste, ineinander geblendet. @ViewBuilder - private func bolt(name: String, side: CGFloat) -> some View { - if let custom = BatteryGlyph.chargingGlyph { - Image(nsImage: custom) - .renderingMode(.template) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: side, height: side) + private func bolt(side: CGFloat, whiteness: Double) -> some View { + if let dark = BatteryGlyph.boltDark, let light = BatteryGlyph.boltLight { + ZStack { + Image(nsImage: dark).resizable().aspectRatio(contentMode: .fit) + Image(nsImage: light).resizable().aspectRatio(contentMode: .fit) + .opacity(whiteness) + } + .frame(width: side, height: side) } else { - Image(systemName: name).font(.system(size: side)) + Image(systemName: "bolt.fill") + .font(.system(size: side)) + .foregroundStyle(Color(white: whiteness)) } } diff --git a/Packages/OnyxKit/Tests/MetricsProviderTests/BatteryGlyphTests.swift b/Packages/OnyxKit/Tests/MetricsProviderTests/BatteryGlyphTests.swift index 7e890e5..5742132 100644 --- a/Packages/OnyxKit/Tests/MetricsProviderTests/BatteryGlyphTests.swift +++ b/Packages/OnyxKit/Tests/MetricsProviderTests/BatteryGlyphTests.swift @@ -237,14 +237,15 @@ struct BatteryGlyphSizeTests { private let height = BatteryGlyph.size.height - @Test("Das Zeichen passt samt Rand in den Innenraum") - func glyphAndOutlineFitInside() { + @Test("Das Zeichen passt in den Innenraum") + func glyphFitsInside() { // Der Fehler, um den es ging: von der Außenhöhe ausgegangen, füllte der - // Blitz den ganzen Akku und stieß an den Rahmen. + // Blitz den ganzen Akku und stieß an den Rahmen. Bezugsgröße ist der + // Innenraum — der Rahmen nimmt oben und unten Platz, die Füllung sitzt + // eingerückt. let inner = height - 2 * BatteryGlyph.fillInset for indicator in BatteryGlyph.Indicator.allCases { - let glyph = BatteryGlyph.glyphHeight(for: indicator, in: height) - #expect(glyph + 2 * BatteryGlyph.outlineWidth <= inner + 0.001) + #expect(BatteryGlyph.glyphHeight(for: indicator, in: height) <= inner + 0.001) } }