From 5d261ed3d81a41ef2aa896fd2984d605eeda2c42 Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Thu, 13 Aug 2026 12:57:13 +0200 Subject: [PATCH] =?UTF-8?q?Men=C3=BCleiste:=20Breite=20f=C3=BCr=20die=20Ge?= =?UTF-8?q?stalt,=20nicht=20f=C3=BCr=20den=20H=C3=B6chstwert?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nachgemessen statt geraten. Das Netzwerkelement war 69 Punkte breit, meine Rechnung ergab 47 — und die anderen Module lagen konstant 16 darüber. Sechs Punkte hatte allein das Netzwerk zusätzlich. Zwei Ursachen. Die Hysterese hielt die Breite auf dem Höchstwert der letzten zwanzig Sekunden; bei einem Wert, der im Sekundentakt schwankt, ist der fast immer größer als der aktuelle. Und der Text wurde linksbündig gezeichnet, die Differenz stand also komplett rechts als Lücke. Jetzt wird die Breite für die **Gestalt** reserviert: alle Ziffern auf 9 gesetzt, das ergibt die breiteste Zeichenfolge derselben Form. Von 14 auf 37 KB/s bewegt sich damit nichts, und breiter wird es nur, wenn eine Stelle dazukommt. Gezeichnet wird mittig, damit ein Rest sich auf beide Seiten verteilt statt rechts zu stehen. Ergebnis: 69 → 57 Punkte. Die verbleibenden rund 16 Punkte je Element legt macOS selbst um jedes Statuselement; die Lücken zwischen benachbarten Elementen sind gemessen 0. Neue Menüleistenmodule starten jetzt mit „Symbol + Wert" statt „Wert" — bestehende Einstellungen bleiben, wie sie sind. Co-Authored-By: Claude Opus 5 --- .../MetricsProvider/MetricMenuBarModule.swift | 17 +++++--- .../NetworkMenuBarModule.swift | 15 +++++-- .../Sources/OnyxMenuBar/MenuBarModule.swift | 2 +- .../Sources/OnyxMenuBar/MenuBarText.swift | 11 +++++ .../OnyxMenuBarTests/MenuBarTextTests.swift | 42 +++++++++++++++++++ 5 files changed, 76 insertions(+), 11 deletions(-) diff --git a/Packages/OnyxKit/Sources/MetricsProvider/MetricMenuBarModule.swift b/Packages/OnyxKit/Sources/MetricsProvider/MetricMenuBarModule.swift index 325fb71..c138ed6 100644 --- a/Packages/OnyxKit/Sources/MetricsProvider/MetricMenuBarModule.swift +++ b/Packages/OnyxKit/Sources/MetricsProvider/MetricMenuBarModule.swift @@ -121,9 +121,14 @@ final class MetricStatusView: NSView, WidthReporting { private var fadeTimer: Timer? private var fadePhase: Double = 0 - private var adaptive = AdaptiveWidth(minimum: MenuBarText.minimumWidth) private var shownWidth: CGFloat = MenuBarText.minimumWidth + /// Die Breite für die Gestalt des Werts, nicht für seinen Höchstwert — + /// zwischen „34 %" und „87 %" darf sich nichts bewegen. + private func shape(_ text: String) -> String { + MenuBarText.widestOfSameShape(text) + } + override var intrinsicContentSize: NSSize { NSSize(width: shownWidth, height: 22) } @@ -139,11 +144,11 @@ final class MetricStatusView: NSView, WidthReporting { case .symbol: max(MenuBarText.minimumWidth, symbolSlot + 4) case .graph: 28 case .bars: barsWidth - case .value: MenuBarText.width(for: displayValue) - case .valueAndGraph: MenuBarText.width(for: displayValue) + 2 + 28 - case .labelAndValue: MenuBarText.width(for: "\(label) \(displayValue)") + case .value: MenuBarText.width(for: shape(displayValue)) + case .valueAndGraph: MenuBarText.width(for: shape(displayValue)) + 2 + 28 + case .labelAndValue: MenuBarText.width(for: shape("\(label) \(displayValue)")) case .symbolAndValue: - MenuBarText.width(for: displayValue) + symbolSlot + MenuBarText.innerGap + MenuBarText.width(for: shape(displayValue)) + symbolSlot + MenuBarText.innerGap } } @@ -180,7 +185,7 @@ final class MetricStatusView: NSView, WidthReporting { updateFadeTimer() - let next = adaptive.update(needed: width) + let next = max(width, MenuBarText.minimumWidth) guard next != shownWidth else { return } shownWidth = next invalidateIntrinsicContentSize() diff --git a/Packages/OnyxKit/Sources/NetworkProvider/NetworkMenuBarModule.swift b/Packages/OnyxKit/Sources/NetworkProvider/NetworkMenuBarModule.swift index 54efee7..f82a5a9 100644 --- a/Packages/OnyxKit/Sources/NetworkProvider/NetworkMenuBarModule.swift +++ b/Packages/OnyxKit/Sources/NetworkProvider/NetworkMenuBarModule.swift @@ -83,8 +83,13 @@ final class NetworkStatusView: NSView, WidthReporting { /// Leiste dabei nicht zappelt. private static func ratesWidth(_ snapshot: NetworkSnapshot) -> CGFloat { let font = NSFont.monospacedDigitSystemFont(ofSize: 9, weight: .regular) + // Die **Gestalt** reservieren, nicht den Höchstwert der letzten + // Messungen: der Durchsatz schwankt im Sekundentakt, und der Höchstwert + // ist fast immer größer als das, was gerade dasteht. Die Differenz + // stand als Lücke daneben. let lines = ["↓ " + Throughput.formatted(snapshot.downloadRate), "↑ " + Throughput.formatted(snapshot.uploadRate)] + .map(MenuBarText.widestOfSameShape) let widest = lines .map { NSAttributedString(string: $0, attributes: [.font: font]).size().width } .max() ?? 0 @@ -111,7 +116,6 @@ final class NetworkStatusView: NSView, WidthReporting { MenuBarText.symbolWidth(snapshot.interfaceKind.symbolName, height: 13) } - private var adaptive = AdaptiveWidth(minimum: MenuBarText.minimumWidth) private var width: CGFloat = MenuBarText.minimumWidth var onWidthChange: ((CGFloat) -> Void)? @@ -124,7 +128,7 @@ final class NetworkStatusView: NSView, WidthReporting { self.history = history needsDisplay = true - let next = adaptive.update(needed: neededWidth()) + let next = max(neededWidth(), MenuBarText.minimumWidth) guard next != width else { return } width = next invalidateIntrinsicContentSize() @@ -175,8 +179,11 @@ final class NetworkStatusView: NSView, WidthReporting { attributes: attributes) let up = NSAttributedString(string: "↑ " + Throughput.formatted(snapshot.uploadRate), attributes: attributes) - down.draw(at: NSPoint(x: rect.minX + 2, y: rect.midY - 0.5)) - up.draw(at: NSPoint(x: rect.minX + 2, y: rect.midY - 10.5)) + // Mittig statt links: bleibt doch einmal Platz übrig — etwa weil die + // zweite Zeile schmaler ist —, verteilt er sich auf beide Seiten statt + // als Lücke rechts zu stehen. + down.draw(at: NSPoint(x: rect.midX - down.size().width / 2, y: rect.midY - 0.5)) + up.draw(at: NSPoint(x: rect.midX - up.size().width / 2, y: rect.midY - 10.5)) } private func drawGraph(color: NSColor, in rect: NSRect) { diff --git a/Packages/OnyxKit/Sources/OnyxMenuBar/MenuBarModule.swift b/Packages/OnyxKit/Sources/OnyxMenuBar/MenuBarModule.swift index dc4d299..1835a96 100644 --- a/Packages/OnyxKit/Sources/OnyxMenuBar/MenuBarModule.swift +++ b/Packages/OnyxKit/Sources/OnyxMenuBar/MenuBarModule.swift @@ -57,7 +57,7 @@ public struct MenuBarModuleSettings: Codable, Sendable, Equatable { public var usesThresholdColors: Bool public init(isEnabled: Bool = false, - presentation: MenuBarPresentation = .value, + presentation: MenuBarPresentation = .symbolAndValue, refreshInterval: TimeInterval = 2, usesThresholdColors: Bool = true) { self.isEnabled = isEnabled diff --git a/Packages/OnyxKit/Sources/OnyxMenuBar/MenuBarText.swift b/Packages/OnyxKit/Sources/OnyxMenuBar/MenuBarText.swift index 79eda0b..1c21718 100644 --- a/Packages/OnyxKit/Sources/OnyxMenuBar/MenuBarText.swift +++ b/Packages/OnyxKit/Sources/OnyxMenuBar/MenuBarText.swift @@ -39,6 +39,17 @@ public enum MenuBarText { return max(minimumWidth, content + symbol) } + /// Dieselbe Zeichenfolge mit allen Ziffern auf 9 gesetzt. + /// + /// Damit lässt sich die Breite für die **Gestalt** eines Werts reservieren + /// statt für seinen Höchstwert. Bei ständig schwankenden Zahlen — dem + /// Netzwerkdurchsatz etwa — ist der Höchstwert der letzten Messungen fast + /// immer größer als der aktuelle, und die Differenz steht als Lücke daneben. + /// So bewegt sich nur noch etwas, wenn eine Stelle dazukommt. + public static func widestOfSameShape(_ text: String) -> String { + String(text.map { $0.isNumber ? "9" : $0 }) + } + public static func width(of text: String) -> CGFloat { guard !text.isEmpty else { return 0 } return NSAttributedString(string: text, attributes: [.font: font]).size().width diff --git a/Packages/OnyxKit/Tests/OnyxMenuBarTests/MenuBarTextTests.swift b/Packages/OnyxKit/Tests/OnyxMenuBarTests/MenuBarTextTests.swift index 3edee00..0d9c2f5 100644 --- a/Packages/OnyxKit/Tests/OnyxMenuBarTests/MenuBarTextTests.swift +++ b/Packages/OnyxKit/Tests/OnyxMenuBarTests/MenuBarTextTests.swift @@ -49,3 +49,45 @@ struct MenuBarTextTests { #expect(MenuBarText.width(of: "111") == MenuBarText.width(of: "888")) } } + +// Eine Breite, die dem Höchstwert der letzten Messungen folgt, ist bei einem +// Wert, der ständig schwankt, fast immer zu groß: der Netzwerkdurchsatz +// wechselt im Sekundentakt, und das Element blieb dauerhaft auf der Breite +// eines vergangenen Ausschlags stehen. Die Differenz stand als Lücke daneben. +// +// Stattdessen: die Ziffern durch Neunen ersetzen. Das ergibt die breiteste +// Zeichenfolge **derselben Gestalt** — die Breite ändert sich dann nur noch, +// wenn eine Stelle dazukommt oder die Einheit wechselt. + +@Suite("Breite gleicher Gestalt") +struct DigitShapeTests { + + @Test("Ziffern werden zu Neunen") + func digitsBecomeNines() { + #expect(MenuBarText.widestOfSameShape("↓ 14 KB/s") == "↓ 99 KB/s") + #expect(MenuBarText.widestOfSameShape("34 %") == "99 %") + } + + @Test("Alles außer Ziffern bleibt stehen") + func nonDigitsSurvive() { + #expect(MenuBarText.widestOfSameShape("↑ 2,4 MB/s") == "↑ 9,9 MB/s") + } + + @Test("Gleich lange Zahlen ergeben dieselbe Gestalt") + func sameLengthIsStable() { + // Genau darum geht es: von 14 auf 37 KB/s darf sich nichts bewegen. + #expect(MenuBarText.widestOfSameShape("↓ 14 KB/s") + == MenuBarText.widestOfSameShape("↓ 37 KB/s")) + } + + @Test("Eine Stelle mehr ist eine andere Gestalt") + func moreDigitsIsWider() { + #expect(MenuBarText.widestOfSameShape("↓ 999 KB/s") + != MenuBarText.widestOfSameShape("↓ 99 KB/s")) + } + + @Test("Ohne Ziffern bleibt der Text, wie er ist") + func textWithoutDigitsIsUnchanged() { + #expect(MenuBarText.widestOfSameShape("–") == "–") + } +}