Diagramme rechnen mit der vollen Länge, eigene Symbole für CPU und GPU
Frisch gestartet hat der Verlauf fünf Messwerte statt sechzig. Gerechnet wurde aber „Breite geteilt durch Anzahl" — also war jeder Balken ein Fünftel breit, und mit jedem weiteren wurden sie schmaler. Das Diagramm sah nach einem Ausschlag aus und bedeutete nichts. Jetzt kommt die Schrittweite aus der vollen Länge. Fehlende Werte bleiben als freie Fläche links stehen; der Verlauf wächst nach links weg, wie bei jedem Zeitdiagramm. Das steckte an **drei** Stellen, alle mit derselben Rechnung: dem Balkendiagramm im Netzwerk-Popover, der Sparkline in den Kacheln und der Linie in den Menüleisten-Modulen. Die gemeinsame Rechnung liegt jetzt in `GraphLayout` und ist geprüft, statt dreimal danebenzugehen. Nebenbei: die Fläche unter der Sparkline begann am linken Rand statt am ersten Messwert — ein Keil über die ganze Breite, wo noch nichts gemessen war. CPU und GPU bekommen eigene Symbole aus chip.svg und graphic-card.svg. SF Symbols hat für beides nur denselben Chip, und „cpu" neben „cpu.fill" ist kein Unterschied, den man in der Menüleiste erkennt. Die Beschriftung unter den Ringen klebte am Rand und sah aus, als gehörte sie noch hinein — sechs Punkte Abstand statt zwei. Und die Popover für CPU und GPU tragen jetzt einen Titel: bei drei gleich aussehenden Ringen sieht man sonst nicht, vor welchem man steht. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@
|
|||||||
1E2D654EFDDF55AEA75DBB7C /* WeatherProvider in Frameworks */ = {isa = PBXBuildFile; productRef = D4578EB64F7A3372B49BC50C /* WeatherProvider */; };
|
1E2D654EFDDF55AEA75DBB7C /* WeatherProvider in Frameworks */ = {isa = PBXBuildFile; productRef = D4578EB64F7A3372B49BC50C /* WeatherProvider */; };
|
||||||
2BB6AFD10DFB13FC4467BE6B /* SMCAccess.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D6DE2FA3022423D80A867C9 /* SMCAccess.swift */; };
|
2BB6AFD10DFB13FC4467BE6B /* SMCAccess.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D6DE2FA3022423D80A867C9 /* SMCAccess.swift */; };
|
||||||
378A5A417112CCB31EF3F774 /* OnyxNotch in Frameworks */ = {isa = PBXBuildFile; productRef = 3A32CAE6CC4D01D660F0016F /* OnyxNotch */; };
|
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 */; };
|
39E29ADC71D4860543AD25EB /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45BD2DF1B775C733C8F75635 /* SettingsView.swift */; };
|
||||||
4E729C7A6498B4679C8D2C42 /* OnyxCore in Frameworks */ = {isa = PBXBuildFile; productRef = C64A111F449D0F9D36ED46FB /* OnyxCore */; };
|
4E729C7A6498B4679C8D2C42 /* OnyxCore in Frameworks */ = {isa = PBXBuildFile; productRef = C64A111F449D0F9D36ED46FB /* OnyxCore */; };
|
||||||
5BE38CAFCDA4B9CBE787CABA /* CalendarProvider in Frameworks */ = {isa = PBXBuildFile; productRef = A3E3949D664131D593CBEEDC /* CalendarProvider */; };
|
5BE38CAFCDA4B9CBE787CABA /* CalendarProvider in Frameworks */ = {isa = PBXBuildFile; productRef = A3E3949D664131D593CBEEDC /* CalendarProvider */; };
|
||||||
@@ -37,6 +38,7 @@
|
|||||||
CB75EE7C8826AC1CCAD35686 /* FanControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC3FDFD387188D36AF8287DE /* FanControl.swift */; };
|
CB75EE7C8826AC1CCAD35686 /* FanControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC3FDFD387188D36AF8287DE /* FanControl.swift */; };
|
||||||
CD501EBAE39807F5D354C5B2 /* OnyxMenuBar in Frameworks */ = {isa = PBXBuildFile; productRef = A7FD7A4864E5ED4B95C211AF /* OnyxMenuBar */; };
|
CD501EBAE39807F5D354C5B2 /* OnyxMenuBar in Frameworks */ = {isa = PBXBuildFile; productRef = A7FD7A4864E5ED4B95C211AF /* OnyxMenuBar */; };
|
||||||
EA4D6312E8A96E177F10797F /* OnyxDesign in Frameworks */ = {isa = PBXBuildFile; productRef = 3D680DD645941493A7D3559E /* OnyxDesign */; };
|
EA4D6312E8A96E177F10797F /* OnyxDesign in Frameworks */ = {isa = PBXBuildFile; productRef = 3D680DD645941493A7D3559E /* OnyxDesign */; };
|
||||||
|
F2A3AD184864422EA14A8C64 /* graphic-card.svg in Resources */ = {isa = PBXBuildFile; fileRef = 1E6A1C208C7EA56971A055E1 /* graphic-card.svg */; };
|
||||||
FF97F56CE1C734E59F1B40F4 /* AppIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = 055263008CB3306A8D09E6D4 /* AppIcon.icon */; };
|
FF97F56CE1C734E59F1B40F4 /* AppIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = 055263008CB3306A8D09E6D4 /* AppIcon.icon */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
@@ -53,6 +55,8 @@
|
|||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
055263008CB3306A8D09E6D4 /* AppIcon.icon */ = {isa = PBXFileReference; lastKnownFileType = wrapper.icon; path = AppIcon.icon; sourceTree = "<group>"; };
|
055263008CB3306A8D09E6D4 /* AppIcon.icon */ = {isa = PBXFileReference; lastKnownFileType = wrapper.icon; path = AppIcon.icon; sourceTree = "<group>"; };
|
||||||
0804288DC4A8146DD9F3FC3E /* Onyx.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Onyx.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
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 = "<group>"; };
|
||||||
|
1E6A1C208C7EA56971A055E1 /* graphic-card.svg */ = {isa = PBXFileReference; path = "graphic-card.svg"; sourceTree = "<group>"; };
|
||||||
24ADD22F5CE30B27020B479B /* MenuBarIcon.svg */ = {isa = PBXFileReference; path = MenuBarIcon.svg; sourceTree = "<group>"; };
|
24ADD22F5CE30B27020B479B /* MenuBarIcon.svg */ = {isa = PBXFileReference; path = MenuBarIcon.svg; sourceTree = "<group>"; };
|
||||||
41C778876BEF3D8B04F7C3C9 /* LaunchAtLogin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchAtLogin.swift; sourceTree = "<group>"; };
|
41C778876BEF3D8B04F7C3C9 /* LaunchAtLogin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchAtLogin.swift; sourceTree = "<group>"; };
|
||||||
45BD2DF1B775C733C8F75635 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
45BD2DF1B775C733C8F75635 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
||||||
@@ -156,9 +160,11 @@
|
|||||||
0FBA13B97C53F1D0D740A633 /* Resources */,
|
0FBA13B97C53F1D0D740A633 /* Resources */,
|
||||||
055263008CB3306A8D09E6D4 /* AppIcon.icon */,
|
055263008CB3306A8D09E6D4 /* AppIcon.icon */,
|
||||||
B83C4E3CB821F903F3977E88 /* AppModel.swift */,
|
B83C4E3CB821F903F3977E88 /* AppModel.swift */,
|
||||||
|
085D860E654E09E4CD6E535A /* chip.svg */,
|
||||||
FC3FDFD387188D36AF8287DE /* FanControl.swift */,
|
FC3FDFD387188D36AF8287DE /* FanControl.swift */,
|
||||||
F12041E330231599C6540C21 /* FanSettingsView.swift */,
|
F12041E330231599C6540C21 /* FanSettingsView.swift */,
|
||||||
89309D9F5722FA1B91CF2905 /* FanWidget.swift */,
|
89309D9F5722FA1B91CF2905 /* FanWidget.swift */,
|
||||||
|
1E6A1C208C7EA56971A055E1 /* graphic-card.svg */,
|
||||||
50A018673EE6BD9C50F1EFA0 /* Info.plist */,
|
50A018673EE6BD9C50F1EFA0 /* Info.plist */,
|
||||||
41C778876BEF3D8B04F7C3C9 /* LaunchAtLogin.swift */,
|
41C778876BEF3D8B04F7C3C9 /* LaunchAtLogin.swift */,
|
||||||
508DECB5C05264E6130C373E /* Localizable.xcstrings */,
|
508DECB5C05264E6130C373E /* Localizable.xcstrings */,
|
||||||
@@ -283,6 +289,8 @@
|
|||||||
FF97F56CE1C734E59F1B40F4 /* AppIcon.icon in Resources */,
|
FF97F56CE1C734E59F1B40F4 /* AppIcon.icon in Resources */,
|
||||||
A529046C33E6B00C0E9508FF /* Localizable.xcstrings in Resources */,
|
A529046C33E6B00C0E9508FF /* Localizable.xcstrings in Resources */,
|
||||||
7CE2EDC6BC58678269BF27AD /* MenuBarIcon.svg in Resources */,
|
7CE2EDC6BC58678269BF27AD /* MenuBarIcon.svg in Resources */,
|
||||||
|
3991CB2BF41C6C014EF4528F /* chip.svg in Resources */,
|
||||||
|
F2A3AD184864422EA14A8C64 /* graphic-card.svg in Resources */,
|
||||||
B0FCCD92AF22F1636482EACD /* sound-waves.svg in Resources */,
|
B0FCCD92AF22F1636482EACD /* sound-waves.svg in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
|||||||
1
Onyx/chip.svg
Normal file
1
Onyx/chip.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg height="512pt" viewBox="0 0 512 512" width="512pt" xmlns="http://www.w3.org/2000/svg"><path d="m150 150h212v212h-212zm0 0"/><path d="m497 211c8.285156 0 15-6.714844 15-15s-6.714844-15-15-15h-45v-30h45c8.285156 0 15-6.714844 15-15s-6.714844-15-15-15h-45v-26c0-19.300781-15.699219-35-35-35h-26v-45c0-8.285156-6.714844-15-15-15s-15 6.714844-15 15v45h-30v-45c0-8.285156-6.714844-15-15-15s-15 6.714844-15 15v45h-30v-45c0-8.285156-6.714844-15-15-15s-15 6.714844-15 15v45h-30v-45c0-8.285156-6.714844-15-15-15s-15 6.714844-15 15v45h-30v-45c0-8.285156-6.714844-15-15-15s-15 6.714844-15 15v45h-26c-19.300781 0-35 15.699219-35 35v26h-45c-8.285156 0-15 6.714844-15 15s6.714844 15 15 15h45v30h-45c-8.285156 0-15 6.714844-15 15s6.714844 15 15 15h45v30h-45c-8.285156 0-15 6.714844-15 15s6.714844 15 15 15h45v30h-45c-8.285156 0-15 6.714844-15 15s6.714844 15 15 15h45v30h-45c-8.285156 0-15 6.714844-15 15s6.714844 15 15 15h45v26c0 19.300781 15.699219 35 35 35h26v45c0 8.285156 6.714844 15 15 15s15-6.714844 15-15v-45h30v45c0 8.285156 6.714844 15 15 15s15-6.714844 15-15v-45h30v45c0 8.285156 6.714844 15 15 15s15-6.714844 15-15v-45h30v45c0 8.285156 6.714844 15 15 15s15-6.714844 15-15v-45h30v45c0 8.285156 6.714844 15 15 15s15-6.714844 15-15v-45h26c19.300781 0 35-15.699219 35-35v-26h45c8.285156 0 15-6.714844 15-15s-6.714844-15-15-15h-45v-30h45c8.285156 0 15-6.714844 15-15s-6.714844-15-15-15h-45v-30h45c8.285156 0 15-6.714844 15-15s-6.714844-15-15-15h-45v-30zm-105 166c0 8.285156-6.714844 15-15 15h-242c-8.285156 0-15-6.714844-15-15v-242c0-8.285156 6.714844-15 15-15h242c8.285156 0 15 6.714844 15 15zm0 0"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.6 KiB |
1
Onyx/graphic-card.svg
Normal file
1
Onyx/graphic-card.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg id="Layer_1" enable-background="new 0 0 512 512" height="512" viewBox="0 0 512 512" width="512" xmlns="http://www.w3.org/2000/svg"><path d="m482.807 132.033h-416.747v-31.976c0-4.418-3.582-8-8-8h-50.06c-4.418 0-8 3.582-8 8s3.582 8 8 8h42.06v56.593h-18.713c-4.418 0-8 3.582-8 8v63.691c0 4.418 3.582 8 8 8h18.713v19.21h-18.713c-4.418 0-8 3.582-8 8v63.691c0 4.418 3.582 8 8 8h18.713v68.699c0 4.418 3.582 8 8 8s8-3.582 8-8v-36.081h30.802v32.508c0 4.418 3.582 8 8 8h111.592c4.418 0 8-3.582 8-8v-32.508h26.387v32.508c0 4.418 3.582 8 8 8h198.358c4.418 0 8-3.582 8-8v-32.508h17.608c16.097 0 29.193-13.096 29.193-29.193v-185.44c0-16.098-13.096-29.194-29.193-29.194zm-443.46 96.309v-47.692h10.713v47.691h-10.713zm0 98.901v-47.691h10.713v47.691zm169.107 73.125h-21.198v-10.103c0-4.418-3.582-8-8-8s-8 3.582-8 8v10.103h-21.197v-10.103c0-4.418-3.582-8-8-8s-8 3.582-8 8v10.103h-21.197v-24.508h95.592zm240.745 0h-17.062v-10.103c0-4.418-3.582-8-8-8s-8 3.582-8 8v10.103h-17.059v-10.103c0-4.418-3.582-8-8-8s-8 3.582-8 8v10.103h-17.06v-10.103c0-4.418-3.582-8-8-8s-8 3.582-8 8v10.103h-17.059v-10.103c0-4.418-3.582-8-8-8s-8 3.582-8 8v10.103h-17.059v-10.103c0-4.418-3.582-8-8-8s-8 3.582-8 8v10.103h-17.06v-24.508h182.358v24.508zm46.801-53.701c0 7.274-5.918 13.193-13.193 13.193h-416.747v-24.607c0-.003 0-.007 0-.01s0-.006 0-.01v-63.672c0-.003 0-.007 0-.01s0-.006 0-.01v-35.19c0-.003 0-.007 0-.01s0-.006 0-.01v-63.671c0-.003 0-.007 0-.01s0-.006 0-.01v-24.607h416.747c7.275 0 13.193 5.919 13.193 13.193zm-110.99-185.502c-51.16 0-92.782 41.621-92.782 92.781s41.622 92.781 92.782 92.781 92.781-41.621 92.781-92.781-41.621-92.781-92.781-92.781zm-41.818 157.126c17.407-5.223 32.416-17.168 41.284-33.275.178.003.355.014.534.014 2.446 0 4.823-.293 7.107-.83 3.553 16.244-.643 33.591-11.516 46.392-13.744-.782-26.524-5.203-37.409-12.301zm-33.3-48.457c10.281 5.547 21.821 8.447 33.451 8.447 6.489 0 13.003-.909 19.337-2.746 1.781 1.841 3.783 3.464 5.969 4.823-8.966 13.989-24.18 23.285-40.903 24.663-8.757-9.807-15.039-21.862-17.854-35.187zm116.937-80.231c-17.408 5.223-32.417 17.168-41.285 33.275-.178-.003-.355-.014-.534-.014-2.446 0-4.823.293-7.107.829-3.553-16.245.644-33.592 11.517-46.393 13.745.783 26.524 5.204 37.409 12.303zm-41.819 79.426c-8.317 0-15.083-6.767-15.083-15.083s6.766-15.082 15.083-15.082c8.316 0 15.083 6.766 15.083 15.082s-6.766 15.083-15.083 15.083zm-21.588-37.414c-1.841 1.781-3.465 3.784-4.824 5.969-13.989-8.966-23.285-24.18-24.663-40.904 9.803-8.752 21.853-15.032 35.171-17.849-8.638 16.017-10.813 35.102-5.684 52.784zm-9.481 21.799c-.003.178-.014.354-.014.533 0 2.446.293 4.823.83 7.108-16.245 3.552-33.592-.645-46.393-11.517.782-13.745 5.203-26.524 12.302-37.41 5.223 17.407 17.168 32.417 33.275 41.286zm52.657 22.864c1.841-1.781 3.465-3.783 4.824-5.969 13.989 8.966 23.285 24.18 24.662 40.904-9.803 8.752-21.853 15.032-35.17 17.849 8.638-16.017 10.813-35.103 5.684-52.784zm9.481-21.798c.003-.178.014-.355.014-.534 0-2.445-.293-4.822-.829-7.106 16.245-3.552 33.592.645 46.392 11.517-.782 13.744-5.203 26.524-12.302 37.409-5.223-17.408-17.168-32.417-33.275-41.286zm-8.737-22.12c-1.781-1.842-3.784-3.465-5.969-4.824 8.966-13.989 24.18-23.286 40.904-24.663 8.752 9.803 15.032 21.853 17.849 35.171-16.017-8.64-35.103-10.814-52.784-5.684zm-230.291-71.195c-51.16 0-92.781 41.621-92.781 92.781s41.622 92.781 92.781 92.781 92.781-41.621 92.781-92.781-41.622-92.781-92.781-92.781zm-41.818 157.125c17.407-5.223 32.416-17.168 41.284-33.275.178.003.355.014.534.014 2.446 0 4.823-.293 7.107-.829 3.553 16.244-.643 33.591-11.516 46.392-13.745-.782-26.524-5.203-37.409-12.302zm-33.3-48.455c10.281 5.547 21.821 8.446 33.45 8.446 6.489 0 13.003-.909 19.337-2.746 1.78 1.841 3.783 3.464 5.968 4.823-8.966 13.989-24.18 23.285-40.903 24.663-8.755-9.807-15.037-21.862-17.852-35.186zm116.937-80.232c-17.408 5.223-32.417 17.168-41.286 33.275-.178-.003-.355-.014-.534-.014-2.445 0-4.823.293-7.106.829-3.553-16.245.644-33.592 11.517-46.393 13.744.783 26.523 5.204 37.409 12.303zm-41.82 79.426c-8.316 0-15.083-6.767-15.083-15.083s6.766-15.082 15.083-15.082 15.083 6.766 15.083 15.082-6.766 15.083-15.083 15.083zm-21.587-37.414c-1.842 1.781-3.465 3.784-4.824 5.97-13.988-8.966-23.285-24.18-24.662-40.904 9.803-8.752 21.853-15.032 35.171-17.849-8.639 16.016-10.814 35.101-5.685 52.783zm-9.482 21.799c-.003.178-.013.354-.013.533 0 2.446.293 4.823.83 7.108-16.245 3.552-33.591-.645-46.392-11.516.782-13.745 5.203-26.525 12.302-37.41 5.222 17.407 17.166 32.416 33.273 41.285zm52.657 22.864c1.841-1.781 3.465-3.784 4.824-5.969 13.989 8.966 23.285 24.18 24.663 40.904-9.803 8.752-21.853 15.032-35.171 17.849 8.639-16.017 10.813-35.103 5.684-52.784zm9.481-21.798c.003-.178.014-.354.014-.533 0-2.445-.293-4.822-.829-7.106 16.245-3.552 33.592.645 46.393 11.517-.783 13.744-5.203 26.524-12.302 37.409-5.224-17.409-17.169-32.419-33.276-41.287zm-8.736-22.12c-1.781-1.842-3.784-3.465-5.97-4.824 8.966-13.989 24.181-23.286 40.905-24.663 8.752 9.803 15.032 21.853 17.848 35.172-16.016-8.64-35.101-10.814-52.783-5.685z"/></svg>
|
||||||
|
After Width: | Height: | Size: 4.9 KiB |
@@ -222,6 +222,10 @@ final class MetricStatusView: NSView, WidthReporting {
|
|||||||
if metric == .battery {
|
if metric == .battery {
|
||||||
drawBattery(color: color,
|
drawBattery(color: color,
|
||||||
at: NSPoint(x: 0, y: bounds.midY - BatteryGlyph.size.height / 2))
|
at: NSPoint(x: 0, y: bounds.midY - BatteryGlyph.size.height / 2))
|
||||||
|
} else if let image = Self.customGlyphs[metric] {
|
||||||
|
MenuBarText.drawTemplate(image, color: color,
|
||||||
|
in: NSRect(x: 0, y: bounds.midY - 6.5,
|
||||||
|
width: glyph, height: 13))
|
||||||
} else {
|
} else {
|
||||||
MenuBarText.drawSymbol(currentSymbol, color: color,
|
MenuBarText.drawSymbol(currentSymbol, color: color,
|
||||||
in: NSRect(x: 0, y: bounds.midY - 6.5,
|
in: NSRect(x: 0, y: bounds.midY - 6.5,
|
||||||
@@ -246,6 +250,19 @@ final class MetricStatusView: NSView, WidthReporting {
|
|||||||
MenuBarText.draw(text, color: color, in: rect ?? bounds)
|
MenuBarText.draw(text, color: color, in: rect ?? bounds)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Eigene Vorlagen für CPU und GPU — SF Symbols hat für beides nur den
|
||||||
|
/// gleichen Chip, und „cpu" neben „cpu.fill" ist kein Unterschied, den man
|
||||||
|
/// in der Menüleiste erkennt.
|
||||||
|
private static let customGlyphs: [MetricKind: NSImage] = {
|
||||||
|
var glyphs: [MetricKind: NSImage] = [:]
|
||||||
|
for (metric, name) in [(MetricKind.cpu, "chip"), (.gpu, "graphic-card")] {
|
||||||
|
guard let url = Bundle.main.url(forResource: name, withExtension: "svg"),
|
||||||
|
let image = NSImage(contentsOf: url) else { continue }
|
||||||
|
glyphs[metric] = image
|
||||||
|
}
|
||||||
|
return glyphs
|
||||||
|
}()
|
||||||
|
|
||||||
private func drawSymbol(color: NSColor) {
|
private func drawSymbol(color: NSColor) {
|
||||||
if metric == .battery {
|
if metric == .battery {
|
||||||
drawBattery(color: color,
|
drawBattery(color: color,
|
||||||
@@ -254,6 +271,12 @@ final class MetricStatusView: NSView, WidthReporting {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
let side: CGFloat = 15
|
let side: CGFloat = 15
|
||||||
|
let rect = NSRect(x: bounds.midX - side / 2, y: bounds.midY - side / 2,
|
||||||
|
width: side, height: side)
|
||||||
|
if let glyph = Self.customGlyphs[metric] {
|
||||||
|
MenuBarText.drawTemplate(glyph, color: color, in: rect)
|
||||||
|
return
|
||||||
|
}
|
||||||
let width = MenuBarText.symbolWidth(currentSymbol, height: side)
|
let width = MenuBarText.symbolWidth(currentSymbol, height: side)
|
||||||
MenuBarText.drawSymbol(currentSymbol, color: color,
|
MenuBarText.drawSymbol(currentSymbol, color: color,
|
||||||
in: NSRect(x: bounds.midX - width / 2,
|
in: NSRect(x: bounds.midX - width / 2,
|
||||||
@@ -299,8 +322,10 @@ final class MetricStatusView: NSView, WidthReporting {
|
|||||||
/// Wie breit das Glyph ist. Der Akku wird selbst gezeichnet und hat eigene
|
/// Wie breit das Glyph ist. Der Akku wird selbst gezeichnet und hat eigene
|
||||||
/// Maße; alles andere ist ein SF-Symbol.
|
/// Maße; alles andere ist ein SF-Symbol.
|
||||||
private var symbolSlot: CGFloat {
|
private var symbolSlot: CGFloat {
|
||||||
metric == .battery ? BatteryGlyph.size.width
|
if metric == .battery { return BatteryGlyph.size.width }
|
||||||
: MenuBarText.symbolWidth(currentSymbol, height: 13)
|
// Eigene Vorlagen sind quadratisch; ihre Breite folgt der Höhe.
|
||||||
|
if Self.customGlyphs[metric] != nil { return 13 }
|
||||||
|
return MenuBarText.symbolWidth(currentSymbol, height: 13)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Das Symbol zum **Zustand**, nicht zur Metrik.
|
/// Das Symbol zum **Zustand**, nicht zur Metrik.
|
||||||
@@ -316,14 +341,21 @@ final class MetricStatusView: NSView, WidthReporting {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func drawGraph(color: NSColor, in rect: NSRect) {
|
private func drawGraph(color: NSColor, in rect: NSRect) {
|
||||||
guard history.count > 1, let context = NSGraphicsContext.current?.cgContext else { return }
|
guard let context = NSGraphicsContext.current?.cgContext else { return }
|
||||||
let step = rect.width / CGFloat(history.count - 1)
|
// Die Schrittweite kommt aus der vollen Länge des Verlaufs, nicht aus
|
||||||
|
// der Anzahl vorhandener Werte: frisch gestartet sind es zwei statt
|
||||||
|
// sechzig, und daraus gerechnet spannte sich der Weg über die ganze
|
||||||
|
// Breite.
|
||||||
|
let xs = GraphLayout.positions(count: history.count,
|
||||||
|
capacity: MetricsModel.historyLength, in: rect)
|
||||||
|
guard !xs.isEmpty else { return }
|
||||||
|
|
||||||
context.setStrokeColor(color.withAlphaComponent(0.85).cgColor)
|
context.setStrokeColor(color.withAlphaComponent(0.85).cgColor)
|
||||||
context.setLineWidth(1)
|
context.setLineWidth(1)
|
||||||
context.setLineJoin(.round)
|
context.setLineJoin(.round)
|
||||||
for (index, value) in history.enumerated() {
|
for (index, x) in xs.enumerated() {
|
||||||
let point = CGPoint(x: rect.minX + CGFloat(index) * step,
|
let value = history[history.count - xs.count + index]
|
||||||
|
let point = CGPoint(x: x,
|
||||||
y: rect.minY + rect.height * CGFloat(min(max(value, 0), 1)))
|
y: rect.minY + rect.height * CGFloat(min(max(value, 0), 1)))
|
||||||
index == 0 ? context.move(to: point) : context.addLine(to: point)
|
index == 0 ? context.move(to: point) : context.addLine(to: point)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ struct MetricPopoverContent: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
|
// Ein Titel oben: bei drei gleich aussehenden Ringen sieht man
|
||||||
|
// sonst nicht, ob man vor dem CPU- oder dem GPU-Popover steht.
|
||||||
|
Text(String(localized: .init(metric.localizationKey), bundle: .module))
|
||||||
|
.font(.headline)
|
||||||
|
|
||||||
switch metric {
|
switch metric {
|
||||||
case .cpu, .gpu: ProcessorDetail(model: model, metric: metric)
|
case .cpu, .gpu: ProcessorDetail(model: model, metric: metric)
|
||||||
case .memory: MemoryDetail(snapshot: model.snapshot)
|
case .memory: MemoryDetail(snapshot: model.snapshot)
|
||||||
@@ -64,7 +69,9 @@ private struct Dial: View {
|
|||||||
var tint: Color = .accentColor
|
var tint: Color = .accentColor
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 2) {
|
// Sechs Punkte Abstand statt zwei: die Beschriftung klebte am Ring und
|
||||||
|
// sah aus, als gehörte sie noch hinein.
|
||||||
|
VStack(spacing: 6) {
|
||||||
ZStack {
|
ZStack {
|
||||||
Circle().stroke(.quaternary, lineWidth: 5)
|
Circle().stroke(.quaternary, lineWidth: 5)
|
||||||
Circle()
|
Circle()
|
||||||
@@ -76,6 +83,7 @@ private struct Dial: View {
|
|||||||
.frame(width: 62, height: 62)
|
.frame(width: 62, height: 62)
|
||||||
Text(caption).font(.caption2).foregroundStyle(.secondary)
|
Text(caption).font(.caption2).foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
|
.padding(.bottom, 2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import OnyxDesign
|
import OnyxDesign
|
||||||
import OnyxWidgetKit
|
import OnyxWidgetKit
|
||||||
|
import OnyxMenuBar
|
||||||
|
|
||||||
/// Ein Widget je Hardwaregröße. Alle teilen sich dasselbe Modell und damit
|
/// Ein Widget je Hardwaregröße. Alle teilen sich dasselbe Modell und damit
|
||||||
/// dieselbe Messschleife.
|
/// dieselbe Messschleife.
|
||||||
@@ -137,21 +138,31 @@ private struct WideMetric: View {
|
|||||||
public struct Sparkline: View {
|
public struct Sparkline: View {
|
||||||
let values: [Double]
|
let values: [Double]
|
||||||
let tint: Color
|
let tint: Color
|
||||||
|
/// Wie viele Werte der Verlauf fasst — die Schrittweite hängt daran, nicht
|
||||||
|
/// an der Anzahl der bisher vorhandenen.
|
||||||
|
let capacity: Int
|
||||||
|
|
||||||
public init(values: [Double], tint: Color) {
|
public init(values: [Double], tint: Color, capacity: Int = MetricsModel.historyLength) {
|
||||||
self.values = values
|
self.values = values
|
||||||
self.tint = tint
|
self.tint = tint
|
||||||
|
self.capacity = capacity
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
GeometryReader { geometry in
|
GeometryReader { geometry in
|
||||||
let points = Array(values.suffix(MetricsModel.historyLength))
|
let points = Array(values.suffix(capacity))
|
||||||
if points.count > 1 {
|
if points.count > 1 {
|
||||||
let size = geometry.size
|
let size = geometry.size
|
||||||
let step = size.width / CGFloat(points.count - 1)
|
// Die Schrittweite kommt aus der **vollen** Länge des Verlaufs,
|
||||||
let coordinates = points.enumerated().map { index, value in
|
// nicht aus der Anzahl vorhandener Werte. Frisch gestartet sind
|
||||||
CGPoint(x: CGFloat(index) * step,
|
// es zwei statt sechzig; daraus gerechnet spannte sich der Weg
|
||||||
y: size.height * (1 - CGFloat(min(max(value, 0), 1))))
|
// zwischen ihnen über die ganze Breite — ein riesiger Zickzack,
|
||||||
|
// der nach viel Bewegung aussah und nichts bedeutete.
|
||||||
|
let xs = GraphLayout.positions(count: points.count,
|
||||||
|
capacity: capacity,
|
||||||
|
in: CGRect(origin: .zero, size: size))
|
||||||
|
let coordinates = zip(xs, points).map { x, value in
|
||||||
|
CGPoint(x: x, y: size.height * (1 - CGFloat(min(max(value, 0), 1))))
|
||||||
}
|
}
|
||||||
|
|
||||||
let line = Path { path in
|
let line = Path { path in
|
||||||
@@ -162,10 +173,15 @@ public struct Sparkline: View {
|
|||||||
|
|
||||||
// Dieselbe Kurve, unten geschlossen — die Fläche darunter gibt
|
// Dieselbe Kurve, unten geschlossen — die Fläche darunter gibt
|
||||||
// dem Verlauf Gewicht, ohne dass eine zweite Linie nötig wäre.
|
// dem Verlauf Gewicht, ohne dass eine zweite Linie nötig wäre.
|
||||||
|
// Die Fläche beginnt am ersten Messwert, nicht am linken Rand:
|
||||||
|
// sonst zieht sich bei wenigen Werten ein Keil über die ganze
|
||||||
|
// Breite, obwohl dort noch nichts gemessen wurde.
|
||||||
let area = Path { path in
|
let area = Path { path in
|
||||||
path.move(to: CGPoint(x: 0, y: size.height))
|
guard let first = coordinates.first, let last = coordinates.last
|
||||||
|
else { return }
|
||||||
|
path.move(to: CGPoint(x: first.x, y: size.height))
|
||||||
coordinates.forEach { path.addLine(to: $0) }
|
coordinates.forEach { path.addLine(to: $0) }
|
||||||
path.addLine(to: CGPoint(x: size.width, y: size.height))
|
path.addLine(to: CGPoint(x: last.x, y: size.height))
|
||||||
path.closeSubpath()
|
path.closeSubpath()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -184,12 +184,19 @@ final class NetworkStatusView: NSView, WidthReporting {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func drawGraph(color: NSColor, in rect: NSRect) {
|
private func drawGraph(color: NSColor, in rect: NSRect) {
|
||||||
guard history.count > 1, let context = NSGraphicsContext.current?.cgContext else { return }
|
guard let context = NSGraphicsContext.current?.cgContext else { return }
|
||||||
let step = rect.width / CGFloat(history.count - 1)
|
// Die Schrittweite kommt aus der vollen Länge des Verlaufs, nicht aus
|
||||||
|
// der Anzahl vorhandener Werte: frisch gestartet sind es zwei statt
|
||||||
|
// sechzig, und daraus gerechnet spannte sich der Weg über die ganze
|
||||||
|
// Breite.
|
||||||
|
let xs = GraphLayout.positions(count: history.count,
|
||||||
|
capacity: NetworkModel.historyLength, in: rect)
|
||||||
|
guard !xs.isEmpty else { return }
|
||||||
context.setStrokeColor(color.withAlphaComponent(0.85).cgColor)
|
context.setStrokeColor(color.withAlphaComponent(0.85).cgColor)
|
||||||
context.setLineWidth(1)
|
context.setLineWidth(1)
|
||||||
for (index, value) in history.enumerated() {
|
for (index, x) in xs.enumerated() {
|
||||||
let point = CGPoint(x: rect.minX + CGFloat(index) * step,
|
let value = history[history.count - xs.count + index]
|
||||||
|
let point = CGPoint(x: x,
|
||||||
y: rect.minY + rect.height * CGFloat(min(max(value, 0), 1)))
|
y: rect.minY + rect.height * CGFloat(min(max(value, 0), 1)))
|
||||||
index == 0 ? context.move(to: point) : context.addLine(to: point)
|
index == 0 ? context.move(to: point) : context.addLine(to: point)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import MetricsProvider
|
import MetricsProvider
|
||||||
|
import OnyxMenuBar
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
struct NetworkPopoverContent: View {
|
struct NetworkPopoverContent: View {
|
||||||
@@ -164,12 +165,21 @@ private struct TrafficChart: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
GeometryReader { geometry in
|
GeometryReader { geometry in
|
||||||
let points = history.suffix(60)
|
let points = history.suffix(NetworkModel.historyLength)
|
||||||
let peak = max(points.map { max($0.downloadRate, $0.uploadRate) }.max() ?? 1, 1)
|
let peak = max(points.map { max($0.downloadRate, $0.uploadRate) }.max() ?? 1, 1)
|
||||||
let width = geometry.size.width / CGFloat(max(points.count, 1))
|
// Die Balkenbreite kommt aus der **vollen** Länge des Verlaufs.
|
||||||
|
// Vorher war sie `Breite / Anzahl`: bei fünf Messwerten also ein
|
||||||
|
// Fünftel je Balken, und mit jedem weiteren wurden sie schmaler.
|
||||||
|
// Frisch gestartet sah das Diagramm dadurch aus wie ein Ausschlag.
|
||||||
|
let width = GraphLayout.barWidth(capacity: NetworkModel.historyLength,
|
||||||
|
totalWidth: geometry.size.width, spacing: 1)
|
||||||
let half = geometry.size.height / 2
|
let half = geometry.size.height / 2
|
||||||
|
|
||||||
HStack(alignment: .center, spacing: 1) {
|
HStack(alignment: .center, spacing: 1) {
|
||||||
|
// Fehlende Messwerte bleiben als freie Fläche links stehen; der
|
||||||
|
// Verlauf wächst nach links weg, wie bei jedem Zeitdiagramm.
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
|
||||||
ForEach(Array(points.enumerated()), id: \.offset) { _, snapshot in
|
ForEach(Array(points.enumerated()), id: \.offset) { _, snapshot in
|
||||||
VStack(spacing: 1) {
|
VStack(spacing: 1) {
|
||||||
Rectangle()
|
Rectangle()
|
||||||
@@ -181,7 +191,7 @@ private struct TrafficChart: View {
|
|||||||
.frame(height: max(half * CGFloat(snapshot.downloadRate / peak), 0.5))
|
.frame(height: max(half * CGFloat(snapshot.downloadRate / peak), 0.5))
|
||||||
.frame(maxHeight: .infinity, alignment: .top)
|
.frame(maxHeight: .infinity, alignment: .top)
|
||||||
}
|
}
|
||||||
.frame(width: max(width - 1, 1))
|
.frame(width: width)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(width: geometry.size.width, height: geometry.size.height)
|
.frame(width: geometry.size.width, height: geometry.size.height)
|
||||||
|
|||||||
@@ -154,7 +154,8 @@ private struct NetworkWidgetView: View {
|
|||||||
|
|
||||||
Rates(snapshot: model.snapshot, compact: false)
|
Rates(snapshot: model.snapshot, compact: false)
|
||||||
|
|
||||||
Sparkline(values: model.series(download: true), tint: Onyx.Color.accent)
|
Sparkline(values: model.series(download: true), tint: Onyx.Color.accent,
|
||||||
|
capacity: NetworkModel.historyLength)
|
||||||
.frame(height: 18)
|
.frame(height: 18)
|
||||||
|
|
||||||
Divider().overlay(Onyx.Color.hairline)
|
Divider().overlay(Onyx.Color.hairline)
|
||||||
|
|||||||
41
Packages/OnyxKit/Sources/OnyxMenuBar/GraphLayout.swift
Normal file
41
Packages/OnyxKit/Sources/OnyxMenuBar/GraphLayout.swift
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import CoreGraphics
|
||||||
|
|
||||||
|
/// Wo die Punkte eines Verlaufsdiagramms liegen.
|
||||||
|
///
|
||||||
|
/// Die Schrittweite kommt aus der **vollen** Länge des Verlaufs, nicht aus der
|
||||||
|
/// Anzahl der bisher vorhandenen Werte. Frisch gestartet hat der Verlauf zwei
|
||||||
|
/// Messwerte statt sechzig — wurde daraus gerechnet, spannte sich der Weg
|
||||||
|
/// zwischen ihnen über die ganze Breite. Das sah nach viel Bewegung aus und
|
||||||
|
/// bedeutete nichts.
|
||||||
|
public enum GraphLayout {
|
||||||
|
|
||||||
|
/// - Parameters:
|
||||||
|
/// - count: wie viele Werte vorliegen.
|
||||||
|
/// - capacity: wie viele der Verlauf fasst.
|
||||||
|
public static func positions(count: Int, capacity: Int, in rect: CGRect) -> [CGFloat] {
|
||||||
|
// Eine Linie braucht zwei Punkte. Einen zu zeichnen ergäbe einen Strich
|
||||||
|
// am Rand, der wie ein Messwert aussieht.
|
||||||
|
guard count >= 2, capacity >= 2 else { return [] }
|
||||||
|
|
||||||
|
let shown = min(count, capacity)
|
||||||
|
let step = rect.width / CGFloat(capacity - 1)
|
||||||
|
// Neue Werte rechts: der Verlauf wächst nach links weg.
|
||||||
|
let start = rect.maxX - CGFloat(shown - 1) * step
|
||||||
|
return (0..<shown).map { start + CGFloat($0) * step }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Die Breite eines Balkens.
|
||||||
|
///
|
||||||
|
/// Aus der **vollen** Länge gerechnet, nicht aus den vorhandenen Werten:
|
||||||
|
/// sonst ist bei fünf Messwerten jeder Balken ein Fünftel breit, und mit
|
||||||
|
/// jedem weiteren werden sie schmaler. Fehlende Werte bleiben stattdessen
|
||||||
|
/// als freie Fläche links stehen.
|
||||||
|
public static func barWidth(capacity: Int, totalWidth: CGFloat,
|
||||||
|
spacing: CGFloat) -> CGFloat {
|
||||||
|
guard capacity > 0 else { return totalWidth }
|
||||||
|
let gaps = spacing * CGFloat(capacity - 1)
|
||||||
|
// Sichtbar bleiben, auch wenn es eng wird — sonst verschwindet der
|
||||||
|
// Verlauf bei schmalem Fenster ganz.
|
||||||
|
return max((totalWidth - gaps) / CGFloat(capacity), 0.5)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import Testing
|
||||||
|
import CoreGraphics
|
||||||
|
@testable import OnyxMenuBar
|
||||||
|
|
||||||
|
// Frisch gestartet hat der Verlauf zwei Messwerte statt sechzig. Wurde die
|
||||||
|
// Schrittweite aus der vorhandenen Anzahl gerechnet, spannte sich der Weg
|
||||||
|
// zwischen ihnen über die ganze Breite — ein riesiger Zickzack, der nach viel
|
||||||
|
// Bewegung aussah und nichts bedeutete.
|
||||||
|
|
||||||
|
@Suite("Verlaufsdiagramm")
|
||||||
|
struct GraphLayoutTests {
|
||||||
|
|
||||||
|
private let rect = CGRect(x: 0, y: 0, width: 60, height: 10)
|
||||||
|
|
||||||
|
@Test("Bei vollem Verlauf reicht er von Rand zu Rand")
|
||||||
|
func fullHistorySpansTheWidth() {
|
||||||
|
let xs = GraphLayout.positions(count: 60, capacity: 60, in: rect)
|
||||||
|
#expect(xs.first == rect.minX)
|
||||||
|
#expect(abs(xs.last! - rect.maxX) < 0.001)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Wenige Werte behalten dieselbe Schrittweite")
|
||||||
|
func fewValuesKeepTheStep() {
|
||||||
|
// Der Kern: zwei Messwerte stehen so eng wie zwei von sechzig, nicht
|
||||||
|
// über die volle Breite verteilt.
|
||||||
|
let full = GraphLayout.positions(count: 60, capacity: 60, in: rect)
|
||||||
|
let few = GraphLayout.positions(count: 2, capacity: 60, in: rect)
|
||||||
|
let fullStep = full[1] - full[0]
|
||||||
|
#expect(abs((few[1] - few[0]) - fullStep) < 0.001)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Neue Werte stehen rechts")
|
||||||
|
func newValuesAreOnTheRight() {
|
||||||
|
// Der Verlauf wächst nach links weg, wie bei jedem Zeitdiagramm.
|
||||||
|
let xs = GraphLayout.positions(count: 3, capacity: 60, in: rect)
|
||||||
|
#expect(abs(xs.last! - rect.maxX) < 0.001)
|
||||||
|
#expect(xs.first! > rect.midX)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Unter zwei Werten gibt es nichts zu zeichnen")
|
||||||
|
func lessThanTwoDrawsNothing() {
|
||||||
|
// Eine Linie braucht zwei Punkte. Einen zu zeichnen ergäbe einen
|
||||||
|
// Strich am Rand, der wie ein Messwert aussieht.
|
||||||
|
#expect(GraphLayout.positions(count: 1, capacity: 60, in: rect).isEmpty)
|
||||||
|
#expect(GraphLayout.positions(count: 0, capacity: 60, in: rect).isEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Mehr Werte als Platz laufen nicht über")
|
||||||
|
func overflowIsClamped() {
|
||||||
|
let xs = GraphLayout.positions(count: 200, capacity: 60, in: rect)
|
||||||
|
#expect(xs.allSatisfy { $0 >= rect.minX - 0.001 && $0 <= rect.maxX + 0.001 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suite("Balkendiagramm")
|
||||||
|
struct BarLayoutTests {
|
||||||
|
|
||||||
|
@Test("Die Balkenbreite hängt nicht an der Anzahl der Werte")
|
||||||
|
func barWidthIsIndependentOfCount() {
|
||||||
|
// Der Fehler, um den es geht: bei fünf Messwerten war jeder Balken ein
|
||||||
|
// Fünftel breit, und mit jedem weiteren wurden sie schmaler.
|
||||||
|
let a = GraphLayout.barWidth(capacity: 60, totalWidth: 300, spacing: 1)
|
||||||
|
#expect(a == GraphLayout.barWidth(capacity: 60, totalWidth: 300, spacing: 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Alle Balken samt Abstand füllen die Breite")
|
||||||
|
func barsFillTheWidth() {
|
||||||
|
let width = GraphLayout.barWidth(capacity: 60, totalWidth: 300, spacing: 1)
|
||||||
|
let total = 60 * width + 59 * 1
|
||||||
|
#expect(abs(total - 300) < 0.001)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Ein Balken bleibt sichtbar, auch wenn es eng wird")
|
||||||
|
func barsStayVisible() {
|
||||||
|
// Sonst verschwindet der Verlauf bei schmalem Popover ganz.
|
||||||
|
#expect(GraphLayout.barWidth(capacity: 500, totalWidth: 100, spacing: 1) >= 0.5)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user