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) } }