Die Mediensteuerung stand nebeneinander: Cover links, alles andere in den verbleibenden gut hundert Punkten. Daher „Bernhard H…" statt eines Titels. Jetzt übereinander, wie es zu einer hochkanten Kachel passt. Das Cover ragte zusätzlich über die Kachel hinaus, weil `clipShape` vor dem Rahmen stand — beschnitten wurde damit auf die Eigengröße des Bildes, und die ist bei einem Podcast-Cover ein Vielfaches. Erst Rahmen, dann beschneiden. Menüleistensymbole waren kaum zu sehen. `color.set()` vor `image.draw(in:)` färbt ein Template-Bild nicht: es wird schwarz gezeichnet, auf einer dunklen Menüleiste also unsichtbar. AppKit färbt Template-Bilder nur ein, wenn sie als `image` eines Buttons gesetzt sind — in einer selbst gezeichneten Ansicht muss die Farbe in die Symbolkonfiguration. Betraf alle fünf Zeichenstellen, nicht nur die Lüfter. „fans.auto" stand als roher Schlüssel da: Xcode hatte ihn extrahiert, gefüllt wurde er nie. Beim Nachsehen waren es 35 solche Einträge über alle Kataloge hinweg — die meisten reine Formatschlüssel, die als Durchreicher trotzdem einen Wert brauchen. Der Automatik-Knopf ist jetzt eine gefüllte Pille, wenn sie greift. Blauer Text sieht aus wie ein Link und nicht wie ein Zustand. Und ein Absturz, der noch niemandem passiert ist: meldet der Helfer für einen stehenden Lüfter keine Grenzen, hätte `Slider(in: 0...0)` die App beendet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
86 lines
4.0 KiB
Swift
86 lines
4.0 KiB
Swift
import AppKit
|
|
|
|
/// Beschriftung und Breite eines Menüleisten-Elements.
|
|
///
|
|
/// Die Breite wird **gemessen**, nicht geschätzt. Vorher stand je Darstellungsart
|
|
/// eine feste Zahl im Code, großzügig gewählt, damit nichts abgeschnitten wird —
|
|
/// mit dem Ergebnis, dass neben jedem Wert Platz für ein weiteres Symbol blieb,
|
|
/// in dem nichts stand. In einer Menüleiste ist das der teuerste Platz des
|
|
/// ganzen Bildschirms.
|
|
///
|
|
/// Gemessen wird die **breiteste Zeichenfolge, die vorkommen kann**, nicht die
|
|
/// gerade angezeigte. Sonst ändert das Element bei jeder Messung seine Größe und
|
|
/// schiebt alle Symbole rechts davon hin und her.
|
|
public enum MenuBarText {
|
|
|
|
/// Feste Ziffernbreite: ohne sie wackelt der Text bei jeder Messung.
|
|
///
|
|
/// Als berechnete Eigenschaft, nicht als gespeicherte: `NSFont` ist nicht
|
|
/// `Sendable`, und die Schrift jedes Mal neu zu erfragen kostet nichts —
|
|
/// AppKit gibt dieselbe zwischengespeicherte Instanz zurück.
|
|
public static var font: NSFont {
|
|
NSFont.monospacedDigitSystemFont(ofSize: 11, weight: .regular)
|
|
}
|
|
|
|
public static let sidePadding: CGFloat = 2
|
|
public static let symbolWidth: CGFloat = 13
|
|
/// Abstand zwischen Symbol und Zahl.
|
|
public static let innerGap: CGFloat = 2
|
|
/// Schmaler geht nicht, ohne dass ein Glyph anstößt.
|
|
public static let minimumWidth: CGFloat = 20
|
|
|
|
/// Die Breite eines Elements aus gemessener Textbreite. Rein, damit die
|
|
/// Rechnung prüfbar bleibt.
|
|
public static func itemWidth(textWidth: CGFloat, includesSymbol: Bool) -> CGFloat {
|
|
let symbol = includesSymbol ? symbolWidth + innerGap : 0
|
|
let content = textWidth > 0 ? ceil(textWidth) + 2 * sidePadding : 2 * sidePadding
|
|
return max(minimumWidth, content + symbol)
|
|
}
|
|
|
|
public static func width(of text: String) -> CGFloat {
|
|
guard !text.isEmpty else { return 0 }
|
|
return NSAttributedString(string: text, attributes: [.font: font]).size().width
|
|
}
|
|
|
|
/// Die Breite für die breiteste vorkommende Zeichenfolge.
|
|
public static func width(for reference: String, includesSymbol: Bool = false) -> CGFloat {
|
|
itemWidth(textWidth: width(of: reference), includesSymbol: includesSymbol)
|
|
}
|
|
|
|
/// Zeichnet ein SF-Symbol in der gewünschten Farbe.
|
|
///
|
|
/// `color.set()` vor `image.draw(in:)` reicht **nicht**. Ein Template-Bild
|
|
/// trägt seine Farbe nicht aus dem Grafikzustand, sondern wird schwarz
|
|
/// gezeichnet — auf einer dunklen Menüleiste also unsichtbar. AppKit färbt
|
|
/// Template-Bilder nur ein, wenn sie als `image` eines Buttons gesetzt sind;
|
|
/// in einer selbst gezeichneten Ansicht muss die Farbe in die
|
|
/// Symbolkonfiguration.
|
|
public static func drawSymbol(_ name: String, color: NSColor, in rect: NSRect) {
|
|
guard let image = NSImage(systemSymbolName: name, accessibilityDescription: nil)
|
|
else { return }
|
|
let configuration = NSImage.SymbolConfiguration(pointSize: rect.height,
|
|
weight: .regular)
|
|
.applying(NSImage.SymbolConfiguration(paletteColors: [color]))
|
|
let tinted = image.withSymbolConfiguration(configuration) ?? image
|
|
tinted.draw(in: rect)
|
|
}
|
|
|
|
public static func draw(_ text: String, color: NSColor, in rect: NSRect) {
|
|
let string = NSAttributedString(string: text,
|
|
attributes: [.font: font, .foregroundColor: color])
|
|
let size = string.size()
|
|
string.draw(at: NSPoint(x: rect.midX - size.width / 2,
|
|
y: rect.midY - size.height / 2))
|
|
}
|
|
}
|
|
|
|
/// Eine Statusansicht, deren nötige Breite sich ändern kann.
|
|
///
|
|
/// Der Controller stellt das Statuselement daraufhin neu ein. Ohne diesen Weg
|
|
/// bliebe ein Element in der Breite stehen, die beim allerersten Zeichnen
|
|
/// gebraucht wurde — vor der ersten Messung also.
|
|
@MainActor
|
|
public protocol WidthReporting: AnyObject {
|
|
var onWidthChange: ((CGFloat) -> Void)? { get set }
|
|
}
|