diff --git a/Sources/CodexBar/PreferencesMenuBarPane.swift b/Sources/CodexBar/PreferencesMenuBarPane.swift index 025f7b5719..0372dc666c 100644 --- a/Sources/CodexBar/PreferencesMenuBarPane.swift +++ b/Sources/CodexBar/PreferencesMenuBarPane.swift @@ -13,6 +13,10 @@ struct MenuBarPane: View { L("overview_choose_providers", String(limit)) } + static func inactiveDisplayContrastAvailable(for style: MenuBarIconStyle) -> Bool { + style == .iconAndPercent + } + var body: some View { Form { Section { @@ -49,6 +53,14 @@ struct MenuBarPane: View { .disabled( self.settings.menuBarIconStyle != .iconAndPercent || self.settings.menuBarDisplayMode == .resetTime) + + Toggle(isOn: self.$settings.menuBarHighContrastOnInactiveDisplays) { + SettingsRowLabel( + L("menu_bar_inactive_display_contrast_title"), + subtitle: "\(MenuBarIconStyle.iconAndPercent.label): " + + L("menu_bar_inactive_display_contrast_subtitle")) + } + .disabled(!Self.inactiveDisplayContrastAvailable(for: self.settings.menuBarIconStyle)) } header: { Text(L("section_icon")) } diff --git a/Sources/CodexBar/Resources/ar.lproj/Localizable.strings b/Sources/CodexBar/Resources/ar.lproj/Localizable.strings index dabdc4e0d0..0d1bfacd7d 100644 --- a/Sources/CodexBar/Resources/ar.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ar.lproj/Localizable.strings @@ -537,6 +537,8 @@ /* Display Pane */ "menu_bar_style_title" = "نمط شريط القوائم"; "menu_bar_style_subtitle" = "كيفية رسم عنصر شريط القوائم."; +"menu_bar_inactive_display_contrast_title" = "تحسين الوضوح على الشاشات غير النشطة"; +"menu_bar_inactive_display_contrast_subtitle" = "استخدم عرضًا عالي التباين لإبقاء الأيقونة والمقياس قابلين للقراءة على الشاشات الأخرى."; "menu_bar_style_critters" = "الكائنات الصغيرة"; "menu_bar_style_bars" = "أشرطة القياس"; "menu_bar_style_icon_percent" = "الأيقونة والنسبة المئوية"; diff --git a/Sources/CodexBar/Resources/ca.lproj/Localizable.strings b/Sources/CodexBar/Resources/ca.lproj/Localizable.strings index 51a1af3c62..f9f988ad46 100644 --- a/Sources/CodexBar/Resources/ca.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ca.lproj/Localizable.strings @@ -515,6 +515,8 @@ /* Display Pane */ "menu_bar_style_title" = "Estil de la barra de menús"; "menu_bar_style_subtitle" = "Com es dibuixa l'element de la barra de menús."; +"menu_bar_inactive_display_contrast_title" = "Millora la visibilitat a les pantalles inactives"; +"menu_bar_inactive_display_contrast_subtitle" = "Utilitza una representació d'alt contrast perquè la icona i la mètrica siguin llegibles a les altres pantalles."; "menu_bar_style_critters" = "Bestioles"; "menu_bar_style_bars" = "Barres de mesura"; "menu_bar_style_icon_percent" = "Icona i percentatge"; diff --git a/Sources/CodexBar/Resources/de.lproj/Localizable.strings b/Sources/CodexBar/Resources/de.lproj/Localizable.strings index 6a7dbff645..3b6e7dc8cf 100644 --- a/Sources/CodexBar/Resources/de.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/de.lproj/Localizable.strings @@ -530,6 +530,8 @@ /* Display Pane */ "menu_bar_style_title" = "Menüleistenstil"; "menu_bar_style_subtitle" = "Legt fest, wie das Menüleistenelement dargestellt wird."; +"menu_bar_inactive_display_contrast_title" = "Sichtbarkeit auf inaktiven Displays verbessern"; +"menu_bar_inactive_display_contrast_subtitle" = "Verwendet eine kontrastreiche Darstellung, damit Symbol und Messwert auf anderen Displays lesbar bleiben."; "menu_bar_style_critters" = "Kreaturen"; "menu_bar_style_bars" = "Messleisten"; "menu_bar_style_icon_percent" = "Symbol und Prozent"; diff --git a/Sources/CodexBar/Resources/en.lproj/Localizable.strings b/Sources/CodexBar/Resources/en.lproj/Localizable.strings index b936d7308a..5cce96aecb 100644 --- a/Sources/CodexBar/Resources/en.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/en.lproj/Localizable.strings @@ -537,6 +537,8 @@ /* Display Pane */ "menu_bar_style_title" = "Menu bar style"; "menu_bar_style_subtitle" = "How the menu bar item is drawn."; +"menu_bar_inactive_display_contrast_title" = "Improve visibility on inactive displays"; +"menu_bar_inactive_display_contrast_subtitle" = "Use high-contrast rendering to keep the icon and metric readable on other displays."; "menu_bar_style_critters" = "Critters"; "menu_bar_style_bars" = "Meter bars"; "menu_bar_style_icon_percent" = "Icon & percent"; diff --git a/Sources/CodexBar/Resources/es.lproj/Localizable.strings b/Sources/CodexBar/Resources/es.lproj/Localizable.strings index c68ab39a88..0120cbd486 100644 --- a/Sources/CodexBar/Resources/es.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/es.lproj/Localizable.strings @@ -524,6 +524,8 @@ /* Display Pane */ "menu_bar_style_title" = "Estilo de la barra de menús"; "menu_bar_style_subtitle" = "Cómo se dibuja el elemento de la barra de menús."; +"menu_bar_inactive_display_contrast_title" = "Mejorar la visibilidad en pantallas inactivas"; +"menu_bar_inactive_display_contrast_subtitle" = "Usa un renderizado de alto contraste para mantener legibles el icono y la métrica en otras pantallas."; "menu_bar_style_critters" = "Bichitos"; "menu_bar_style_bars" = "Barras de medición"; "menu_bar_style_icon_percent" = "Icono y porcentaje"; diff --git a/Sources/CodexBar/Resources/fa.lproj/Localizable.strings b/Sources/CodexBar/Resources/fa.lproj/Localizable.strings index 0cf9c5a89a..7a8650b62f 100644 --- a/Sources/CodexBar/Resources/fa.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/fa.lproj/Localizable.strings @@ -537,6 +537,8 @@ /* Display Pane */ "menu_bar_style_title" = "سبک نوار منو"; "menu_bar_style_subtitle" = "نحوه نمایش آیتم نوار منو."; +"menu_bar_inactive_display_contrast_title" = "بهبود خوانایی در نمایشگرهای غیرفعال"; +"menu_bar_inactive_display_contrast_subtitle" = "از نمایش با کنتراست بالا استفاده می‌کند تا نماد و معیار در نمایشگرهای دیگر خوانا بمانند."; "menu_bar_style_critters" = "موجودات"; "menu_bar_style_bars" = "نوارهای اندازه‌گیری"; "menu_bar_style_icon_percent" = "آیکون و درصد"; diff --git a/Sources/CodexBar/Resources/fr.lproj/Localizable.strings b/Sources/CodexBar/Resources/fr.lproj/Localizable.strings index aeefb35df3..dc0b95041d 100644 --- a/Sources/CodexBar/Resources/fr.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/fr.lproj/Localizable.strings @@ -532,6 +532,8 @@ /* Display Pane */ "menu_bar_style_title" = "Style de la barre de menus"; "menu_bar_style_subtitle" = "Définit l’apparence de l’élément dans la barre de menus."; +"menu_bar_inactive_display_contrast_title" = "Améliorer la visibilité sur les écrans inactifs"; +"menu_bar_inactive_display_contrast_subtitle" = "Utilise un rendu à contraste élevé pour que l’icône et la mesure restent lisibles sur les autres écrans."; "menu_bar_style_critters" = "Créatures"; "menu_bar_style_bars" = "Barres de mesure"; "menu_bar_style_icon_percent" = "Icône et pourcentage"; diff --git a/Sources/CodexBar/Resources/gl.lproj/Localizable.strings b/Sources/CodexBar/Resources/gl.lproj/Localizable.strings index f5c830c7a4..39d1b906c4 100644 --- a/Sources/CodexBar/Resources/gl.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/gl.lproj/Localizable.strings @@ -510,6 +510,8 @@ /* Display Pane */ "menu_bar_style_title" = "Estilo da barra de menús"; "menu_bar_style_subtitle" = "Como se debuxa o elemento da barra de menús."; +"menu_bar_inactive_display_contrast_title" = "Mellorar a visibilidade nas pantallas inactivas"; +"menu_bar_inactive_display_contrast_subtitle" = "Usa unha representación de alto contraste para manter lexibles a icona e a métrica nas outras pantallas."; "menu_bar_style_critters" = "Animaliños"; "menu_bar_style_bars" = "Barras de medición"; "menu_bar_style_icon_percent" = "Icona e porcentaxe"; diff --git a/Sources/CodexBar/Resources/id.lproj/Localizable.strings b/Sources/CodexBar/Resources/id.lproj/Localizable.strings index 2d0cc2c0e8..3e415a9faf 100644 --- a/Sources/CodexBar/Resources/id.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/id.lproj/Localizable.strings @@ -539,6 +539,8 @@ /* Display Pane */ "menu_bar_style_title" = "Gaya menu bar"; "menu_bar_style_subtitle" = "Cara item menu bar digambar."; +"menu_bar_inactive_display_contrast_title" = "Tingkatkan visibilitas di layar tidak aktif"; +"menu_bar_inactive_display_contrast_subtitle" = "Gunakan rendering kontras tinggi agar ikon dan metrik tetap terbaca di layar lain."; "menu_bar_style_critters" = "Critter"; "menu_bar_style_bars" = "Bilah meter"; "menu_bar_style_icon_percent" = "Ikon & persentase"; diff --git a/Sources/CodexBar/Resources/it.lproj/Localizable.strings b/Sources/CodexBar/Resources/it.lproj/Localizable.strings index 4747e332b6..3820c71f39 100644 --- a/Sources/CodexBar/Resources/it.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/it.lproj/Localizable.strings @@ -539,6 +539,8 @@ /* Display Pane */ "menu_bar_style_title" = "Stile barra menu"; "menu_bar_style_subtitle" = "Come viene rappresentato l'elemento della barra menu."; +"menu_bar_inactive_display_contrast_title" = "Migliora la visibilità sugli schermi inattivi"; +"menu_bar_inactive_display_contrast_subtitle" = "Usa un rendering ad alto contrasto per mantenere leggibili icona e metrica sugli altri schermi."; "menu_bar_style_critters" = "Creature"; "menu_bar_style_bars" = "Barre di livello"; "menu_bar_style_icon_percent" = "Icona + percentuale"; diff --git a/Sources/CodexBar/Resources/ja.lproj/Localizable.strings b/Sources/CodexBar/Resources/ja.lproj/Localizable.strings index fbe742250d..0840c5635d 100644 --- a/Sources/CodexBar/Resources/ja.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ja.lproj/Localizable.strings @@ -529,6 +529,8 @@ /* Display Pane */ "menu_bar_style_title" = "メニューバーのスタイル"; "menu_bar_style_subtitle" = "メニューバー項目の表示方法です。"; +"menu_bar_inactive_display_contrast_title" = "非アクティブなディスプレイでの視認性を向上"; +"menu_bar_inactive_display_contrast_subtitle" = "高コントラスト表示を使用し、ほかのディスプレイでもアイコンと指標を読みやすくします。"; "menu_bar_style_critters" = "クリッター"; "menu_bar_style_bars" = "メーターバー"; "menu_bar_style_icon_percent" = "アイコンとパーセント"; diff --git a/Sources/CodexBar/Resources/ko.lproj/Localizable.strings b/Sources/CodexBar/Resources/ko.lproj/Localizable.strings index bf7c10cc5d..2bbd42efad 100644 --- a/Sources/CodexBar/Resources/ko.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ko.lproj/Localizable.strings @@ -521,6 +521,8 @@ "primary_api_key_limit" = "기본 (API 키 한도)"; "menu_bar_style_title" = "메뉴 막대 스타일"; "menu_bar_style_subtitle" = "메뉴 막대 항목의 표시 방식을 선택합니다."; +"menu_bar_inactive_display_contrast_title" = "비활성 디스플레이에서 가시성 향상"; +"menu_bar_inactive_display_contrast_subtitle" = "고대비 렌더링을 사용하여 다른 디스플레이에서도 아이콘과 지표를 읽기 쉽게 유지합니다."; "menu_bar_style_critters" = "크리터"; "menu_bar_style_bars" = "미터 막대"; "menu_bar_style_icon_percent" = "아이콘 및 백분율"; diff --git a/Sources/CodexBar/Resources/nl.lproj/Localizable.strings b/Sources/CodexBar/Resources/nl.lproj/Localizable.strings index 522967312f..6eca77c3b2 100644 --- a/Sources/CodexBar/Resources/nl.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/nl.lproj/Localizable.strings @@ -532,6 +532,8 @@ /* Display Pane */ "menu_bar_style_title" = "Menubalkstijl"; "menu_bar_style_subtitle" = "Hoe het menubalkitem wordt weergegeven."; +"menu_bar_inactive_display_contrast_title" = "Zichtbaarheid op inactieve beeldschermen verbeteren"; +"menu_bar_inactive_display_contrast_subtitle" = "Gebruikt weergave met hoog contrast zodat het pictogram en de metriek leesbaar blijven op andere beeldschermen."; "menu_bar_style_critters" = "Critters"; "menu_bar_style_bars" = "Meterbalken"; "menu_bar_style_icon_percent" = "Pictogram en percentage"; diff --git a/Sources/CodexBar/Resources/pl.lproj/Localizable.strings b/Sources/CodexBar/Resources/pl.lproj/Localizable.strings index 247eb2a71a..b6ed2914ee 100644 --- a/Sources/CodexBar/Resources/pl.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/pl.lproj/Localizable.strings @@ -539,6 +539,8 @@ /* Display Pane */ "menu_bar_style_title" = "Styl paska menu"; "menu_bar_style_subtitle" = "Sposób rysowania elementu paska menu."; +"menu_bar_inactive_display_contrast_title" = "Popraw widoczność na nieaktywnych ekranach"; +"menu_bar_inactive_display_contrast_subtitle" = "Używa renderowania o wysokim kontraście, aby ikona i wskaźnik pozostały czytelne na innych ekranach."; "menu_bar_style_critters" = "Stworki"; "menu_bar_style_bars" = "Paski miernika"; "menu_bar_style_icon_percent" = "Ikona i procent"; diff --git a/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings b/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings index 0a1133d137..c704a461e7 100644 --- a/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings @@ -529,6 +529,8 @@ /* Display Pane */ "menu_bar_style_title" = "Estilo da barra de menus"; "menu_bar_style_subtitle" = "Como o item da barra de menus é desenhado."; +"menu_bar_inactive_display_contrast_title" = "Melhorar a visibilidade em telas inativas"; +"menu_bar_inactive_display_contrast_subtitle" = "Usa renderização de alto contraste para manter o ícone e a métrica legíveis em outras telas."; "menu_bar_style_critters" = "Bichinhos"; "menu_bar_style_bars" = "Barras de medição"; "menu_bar_style_icon_percent" = "Ícone e porcentagem"; diff --git a/Sources/CodexBar/Resources/ru.lproj/Localizable.strings b/Sources/CodexBar/Resources/ru.lproj/Localizable.strings index d55748ec57..c11506f72b 100644 --- a/Sources/CodexBar/Resources/ru.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ru.lproj/Localizable.strings @@ -533,6 +533,8 @@ /* Display Pane */ "menu_bar_style_title" = "Стиль строки меню"; "menu_bar_style_subtitle" = "Как выглядит элемент строки меню."; +"menu_bar_inactive_display_contrast_title" = "Повысить видимость на неактивных дисплеях"; +"menu_bar_inactive_display_contrast_subtitle" = "Использует высококонтрастную отрисовку, чтобы значок и показатель оставались читаемыми на других дисплеях."; "menu_bar_style_critters" = "Декоративные индикаторы"; "menu_bar_style_bars" = "Полосы-индикаторы"; "menu_bar_style_icon_percent" = "Значок и процент"; diff --git a/Sources/CodexBar/Resources/sv.lproj/Localizable.strings b/Sources/CodexBar/Resources/sv.lproj/Localizable.strings index d652a37463..d41357b67f 100644 --- a/Sources/CodexBar/Resources/sv.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/sv.lproj/Localizable.strings @@ -531,6 +531,8 @@ /* Display Pane */ "menu_bar_style_title" = "Menyradsstil"; "menu_bar_style_subtitle" = "Hur menyradsobjektet visas."; +"menu_bar_inactive_display_contrast_title" = "Förbättra synligheten på inaktiva skärmar"; +"menu_bar_inactive_display_contrast_subtitle" = "Använder kontrastrik rendering så att ikonen och mätvärdet förblir läsbara på andra skärmar."; "menu_bar_style_critters" = "Figurer"; "menu_bar_style_bars" = "Mätarstaplar"; "menu_bar_style_icon_percent" = "Ikon och procent"; diff --git a/Sources/CodexBar/Resources/th.lproj/Localizable.strings b/Sources/CodexBar/Resources/th.lproj/Localizable.strings index 2172c83095..cdd97835b3 100644 --- a/Sources/CodexBar/Resources/th.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/th.lproj/Localizable.strings @@ -537,6 +537,8 @@ /* Display Pane */ "menu_bar_style_title" = "รูปแบบแถบเมนู"; "menu_bar_style_subtitle" = "รูปแบบการแสดงรายการในแถบเมนู"; +"menu_bar_inactive_display_contrast_title" = "เพิ่มการมองเห็นบนจอแสดงผลที่ไม่ได้ใช้งาน"; +"menu_bar_inactive_display_contrast_subtitle" = "ใช้การแสดงผลแบบคอนทราสต์สูงเพื่อให้ไอคอนและค่าชี้วัดอ่านได้บนจออื่น"; "menu_bar_style_critters" = "ตัวการ์ตูน"; "menu_bar_style_bars" = "แถบวัด"; "menu_bar_style_icon_percent" = "ไอคอนและเปอร์เซ็นต์"; diff --git a/Sources/CodexBar/Resources/tr.lproj/Localizable.strings b/Sources/CodexBar/Resources/tr.lproj/Localizable.strings index 6a1fa3a57e..977d72c3b9 100644 --- a/Sources/CodexBar/Resources/tr.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/tr.lproj/Localizable.strings @@ -537,6 +537,8 @@ /* Display Pane */ "menu_bar_style_title" = "Menü çubuğu stili"; "menu_bar_style_subtitle" = "Menü çubuğu öğesinin nasıl çizileceğini belirler."; +"menu_bar_inactive_display_contrast_title" = "Etkin olmayan ekranlarda görünürlüğü artır"; +"menu_bar_inactive_display_contrast_subtitle" = "Simge ve ölçümün diğer ekranlarda okunabilir kalması için yüksek kontrastlı işleme kullanır."; "menu_bar_style_critters" = "Canavarlar"; "menu_bar_style_bars" = "Ölçüm çubukları"; "menu_bar_style_icon_percent" = "Simge ve yüzde"; diff --git a/Sources/CodexBar/Resources/uk.lproj/Localizable.strings b/Sources/CodexBar/Resources/uk.lproj/Localizable.strings index c8e1b69861..ff712ac0e7 100644 --- a/Sources/CodexBar/Resources/uk.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/uk.lproj/Localizable.strings @@ -532,6 +532,8 @@ /* Display Pane */ "menu_bar_style_title" = "Стиль рядка меню"; "menu_bar_style_subtitle" = "Визначає вигляд елемента рядка меню."; +"menu_bar_inactive_display_contrast_title" = "Покращити видимість на неактивних дисплеях"; +"menu_bar_inactive_display_contrast_subtitle" = "Використовує висококонтрастне відтворення, щоб піктограма й показник залишалися читабельними на інших дисплеях."; "menu_bar_style_critters" = "Тварини"; "menu_bar_style_bars" = "Смуги-індикатори"; "menu_bar_style_icon_percent" = "Значок і відсоток"; diff --git a/Sources/CodexBar/Resources/vi.lproj/Localizable.strings b/Sources/CodexBar/Resources/vi.lproj/Localizable.strings index 801c55672d..160f4ca13e 100644 --- a/Sources/CodexBar/Resources/vi.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/vi.lproj/Localizable.strings @@ -528,6 +528,8 @@ /* Display Pane */ "menu_bar_style_title" = "Kiểu thanh menu"; "menu_bar_style_subtitle" = "Cách hiển thị mục trên thanh menu."; +"menu_bar_inactive_display_contrast_title" = "Cải thiện khả năng hiển thị trên màn hình không hoạt động"; +"menu_bar_inactive_display_contrast_subtitle" = "Sử dụng hiển thị tương phản cao để biểu tượng và chỉ số vẫn dễ đọc trên các màn hình khác."; "menu_bar_style_critters" = "Sinh vật"; "menu_bar_style_bars" = "Thanh đo"; "menu_bar_style_icon_percent" = "Biểu tượng & phần trăm"; diff --git a/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings b/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings index 5cf4456040..790c6b78c6 100644 --- a/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings @@ -510,6 +510,8 @@ "primary_api_key_limit" = "主要(API 密钥限制)"; "menu_bar_style_title" = "菜单栏样式"; "menu_bar_style_subtitle" = "菜单栏项目的绘制方式。"; +"menu_bar_inactive_display_contrast_title" = "提高非活跃显示器上的可见性"; +"menu_bar_inactive_display_contrast_subtitle" = "使用高对比度绘制,让其他显示器上的图标和指标仍清晰可读。"; "menu_bar_style_critters" = "小动物"; "menu_bar_style_bars" = "进度条"; "menu_bar_style_icon_percent" = "图标和百分比"; diff --git a/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings b/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings index 782b69fa71..7a06e2a064 100644 --- a/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings @@ -531,6 +531,8 @@ "primary_api_key_limit" = "主要(API 金鑰限制)"; "menu_bar_style_title" = "選單列樣式"; "menu_bar_style_subtitle" = "選單列項目的繪製方式。"; +"menu_bar_inactive_display_contrast_title" = "改善非使用中顯示器上的可見度"; +"menu_bar_inactive_display_contrast_subtitle" = "使用高對比顯示,讓其他顯示器上的圖示與指標保持清晰可讀。"; "menu_bar_style_critters" = "小動物"; "menu_bar_style_bars" = "進度條"; "menu_bar_style_icon_percent" = "圖示和百分比"; diff --git a/Sources/CodexBar/SettingsStore+Defaults.swift b/Sources/CodexBar/SettingsStore+Defaults.swift index 6ed94842de..ba408160d6 100644 --- a/Sources/CodexBar/SettingsStore+Defaults.swift +++ b/Sources/CodexBar/SettingsStore+Defaults.swift @@ -282,6 +282,14 @@ extension SettingsStore { } } + var menuBarHighContrastOnInactiveDisplays: Bool { + get { self.defaultsState.menuBarHighContrastOnInactiveDisplays } + set { + self.defaultsState.menuBarHighContrastOnInactiveDisplays = newValue + self.userDefaults.set(newValue, forKey: "menuBarHighContrastOnInactiveDisplays") + } + } + private var menuBarDisplayModeRaw: String? { get { self.defaultsState.menuBarDisplayModeRaw } set { diff --git a/Sources/CodexBar/SettingsStore+MenuObservation.swift b/Sources/CodexBar/SettingsStore+MenuObservation.swift index 6e782fe044..0ca7115ac5 100644 --- a/Sources/CodexBar/SettingsStore+MenuObservation.swift +++ b/Sources/CodexBar/SettingsStore+MenuObservation.swift @@ -27,6 +27,7 @@ extension SettingsStore { _ = self.providerChangelogLinksEnabled _ = self.menuBarShowsBrandIconWithPercent _ = self.menuBarHidesCritters + _ = self.menuBarHighContrastOnInactiveDisplays _ = self.menuBarShowsHighestUsage _ = self.menuBarDisplayMode _ = self.menuBarShowsResetTimeWhenExhausted diff --git a/Sources/CodexBar/SettingsStore.swift b/Sources/CodexBar/SettingsStore.swift index 2689d22576..82a07fb8c1 100644 --- a/Sources/CodexBar/SettingsStore.swift +++ b/Sources/CodexBar/SettingsStore.swift @@ -430,6 +430,8 @@ extension SettingsStore { let menuBarShowsBrandIconWithPercent = userDefaults.object( forKey: "menuBarShowsBrandIconWithPercent") as? Bool ?? false let menuBarHidesCritters = userDefaults.object(forKey: "menuBarHidesCritters") as? Bool ?? false + let menuBarHighContrastOnInactiveDisplays = userDefaults.object( + forKey: "menuBarHighContrastOnInactiveDisplays") as? Bool ?? false let menuBarDisplayModeRaw = userDefaults.string(forKey: "menuBarDisplayMode") ?? MenuBarDisplayMode.percent.rawValue let menuBarShowsResetTimeWhenExhausted = userDefaults.object( @@ -523,6 +525,7 @@ extension SettingsStore { providerChangelogLinksEnabled: providerChangelogLinksEnabled, menuBarShowsBrandIconWithPercent: menuBarShowsBrandIconWithPercent, menuBarHidesCritters: menuBarHidesCritters, + menuBarHighContrastOnInactiveDisplays: menuBarHighContrastOnInactiveDisplays, menuBarDisplayModeRaw: menuBarDisplayModeRaw, menuBarShowsResetTimeWhenExhausted: menuBarShowsResetTimeWhenExhausted, kiroMenuBarDisplayModeRaw: kiroMenuBarDisplayModeRaw, diff --git a/Sources/CodexBar/SettingsStoreState.swift b/Sources/CodexBar/SettingsStoreState.swift index 82c24e08d2..77d9583842 100644 --- a/Sources/CodexBar/SettingsStoreState.swift +++ b/Sources/CodexBar/SettingsStoreState.swift @@ -28,6 +28,7 @@ struct SettingsDefaultsState { var providerChangelogLinksEnabled: Bool var menuBarShowsBrandIconWithPercent: Bool var menuBarHidesCritters: Bool + var menuBarHighContrastOnInactiveDisplays: Bool var menuBarDisplayModeRaw: String? var menuBarShowsResetTimeWhenExhausted: Bool var kiroMenuBarDisplayModeRaw: String? diff --git a/Sources/CodexBar/StatusItemController+Animation.swift b/Sources/CodexBar/StatusItemController+Animation.swift index 88b49161d6..2068150965 100644 --- a/Sources/CodexBar/StatusItemController+Animation.swift +++ b/Sources/CodexBar/StatusItemController+Animation.swift @@ -296,6 +296,7 @@ extension StatusItemController { let brand = ProviderBrandIcon.image(for: primaryProvider) { let displayText = self.menuBarDisplayText(for: primaryProvider, snapshot: snapshot) + let displayedImage = warningFlash ? Self.quotaWarningFlashImage(base: brand) : brand let signature = [ "mode=brandPercent", "provider=\(primaryProvider.rawValue)", @@ -309,22 +310,22 @@ extension StatusItemController { "warningFlash=\(warningFlash ? "1" : "0")", "anim=\(needsAnimation ? "1" : "0")", "hideCritters=\(self.settings.menuBarHidesCritters ? "1" : "0")", + "highContrast=\(self.shouldUseHighContrastStatusItemContent ? "1" : "0")", ].joined(separator: "|") if self.shouldSkipMergedIconRender(signature) { - // AppKit can lose button title/image-position state independently of the cached render signature. - // Keep the cheap title path self-healing even when the icon image itself can be skipped. - self.setButtonTitle(displayText, for: button) + // AppKit can lose button content state independently of the cached render signature. + // Keep this cheap path self-healing even when the provider image itself can be skipped. + self.setButtonContent(image: displayedImage, title: displayText, for: button) self.noteIconPerfRender(skipped: true) return true } - self.setButtonImage( - warningFlash ? Self.quotaWarningFlashImage(base: brand) : brand, for: button) - self.setButtonTitle(displayText, for: button) + self.setButtonContent(image: displayedImage, title: displayText, for: button) self.noteIconPerfRender(skipped: false) return false } - self.setButtonTitle(nil, for: button) + // Brand + percent returns above; remaining paths are image-only apart from the debug marker. + let canSkipCachedRender = self.prepareButtonForImageOnlyCacheHit(button) if let morphProgress { let signature = [ "mode=morph", @@ -335,8 +336,9 @@ extension StatusItemController { "warningFlash=\(warningFlash ? "1" : "0")", "anim=\(needsAnimation ? "1" : "0")", "hideCritters=\(self.settings.menuBarHidesCritters ? "1" : "0")", + "highContrast=\(self.shouldUseHighContrastStatusItemContent ? "1" : "0")", ].joined(separator: "|") - if self.shouldSkipMergedIconRender(signature) { + if self.shouldSkipMergedIconRender(signature), canSkipCachedRender { self.noteIconPerfRender(skipped: true) return true } @@ -344,8 +346,10 @@ extension StatusItemController { progress: morphProgress, style: style, hideCritters: self.settings.menuBarHidesCritters) - self.setButtonImage( - warningFlash ? Self.quotaWarningFlashImage(base: image) : image, for: button) + self.setButtonContent( + image: warningFlash ? Self.quotaWarningFlashImage(base: image) : image, + title: nil, + for: button) } else { let signature = [ "mode=icon", @@ -362,8 +366,9 @@ extension StatusItemController { "warningFlash=\(warningFlash ? "1" : "0")", "anim=\(needsAnimation ? "1" : "0")", "hideCritters=\(self.settings.menuBarHidesCritters ? "1" : "0")", + "highContrast=\(self.shouldUseHighContrastStatusItemContent ? "1" : "0")", ].joined(separator: "|") - if self.shouldSkipMergedIconRender(signature) { + if self.shouldSkipMergedIconRender(signature), canSkipCachedRender { self.noteIconPerfRender(skipped: true) return true } @@ -378,8 +383,10 @@ extension StatusItemController { tilt: tilt, statusIndicator: statusIndicator, hideCritters: self.settings.menuBarHidesCritters) - self.setButtonImage( - warningFlash ? Self.quotaWarningFlashImage(base: image) : image, for: button) + self.setButtonContent( + image: warningFlash ? Self.quotaWarningFlashImage(base: image) : image, + title: nil, + for: button) } self.noteIconPerfRender(skipped: false) return false @@ -439,26 +446,25 @@ extension StatusItemController { let brand = ProviderBrandIcon.image(for: provider) { let displayText = self.menuBarDisplayText(for: provider, snapshot: snapshot) + let displayedImage = warningFlash ? Self.quotaWarningFlashImage(base: brand) : brand let signature = [ "mode=brandPercent", "provider=\(provider.rawValue)", "style=\(String(describing: style))", "text=\(displayText ?? "nil")", "warningFlash=\(warningFlash ? "1" : "0")", + "highContrast=\(self.shouldUseHighContrastStatusItemContent ? "1" : "0")", ].joined(separator: "|") if self.shouldSkipProviderIconRender(provider: provider, signature: signature) { + self.setButtonContent(image: displayedImage, title: displayText, for: button) self.noteIconPerfRender(skipped: true) return true } - self.setButtonImage( - warningFlash ? Self.quotaWarningFlashImage(base: brand) : brand, for: button) - self.setButtonTitle(displayText, for: button) + self.setButtonContent(image: displayedImage, title: displayText, for: button) self.noteIconPerfRender(skipped: false) return false } - self.setButtonTitle(nil, for: button) - // OpenRouter always gets a meter here — the brand-logo fallback was removed on purpose. let resolved = self.resolvedMenuBarIconPercents( provider: provider, @@ -504,6 +510,8 @@ extension StatusItemController { let wiggle = self.wiggleAmount(for: provider) let tilt = self.tiltAmount(for: provider) * .pi / 28 // limit to ~6.4° let statusIndicator = self.store.statusIndicator(for: provider) + // Brand + percent returns above; remaining paths are image-only apart from the debug marker. + let canSkipCachedRender = self.prepareButtonForImageOnlyCacheHit(button) if let morphProgress { let signature = [ "mode=morph", @@ -514,8 +522,9 @@ extension StatusItemController { "warningFlash=\(warningFlash ? "1" : "0")", "loading=\(isLoading ? "1" : "0")", "hideCritters=\(self.settings.menuBarHidesCritters ? "1" : "0")", + "highContrast=\(self.shouldUseHighContrastStatusItemContent ? "1" : "0")", ].joined(separator: "|") - if self.shouldSkipProviderIconRender(provider: provider, signature: signature) { + if self.shouldSkipProviderIconRender(provider: provider, signature: signature), canSkipCachedRender { self.noteIconPerfRender(skipped: true) return true } @@ -523,8 +532,10 @@ extension StatusItemController { progress: morphProgress, style: style, hideCritters: self.settings.menuBarHidesCritters) - self.setButtonImage( - warningFlash ? Self.quotaWarningFlashImage(base: image) : image, for: button) + self.setButtonContent( + image: warningFlash ? Self.quotaWarningFlashImage(base: image) : image, + title: nil, + for: button) } else { let signature = [ "mode=icon", @@ -541,8 +552,9 @@ extension StatusItemController { "warningFlash=\(warningFlash ? "1" : "0")", "loading=\(isLoading ? "1" : "0")", "hideCritters=\(self.settings.menuBarHidesCritters ? "1" : "0")", + "highContrast=\(self.shouldUseHighContrastStatusItemContent ? "1" : "0")", ].joined(separator: "|") - if self.shouldSkipProviderIconRender(provider: provider, signature: signature) { + if self.shouldSkipProviderIconRender(provider: provider, signature: signature), canSkipCachedRender { self.noteIconPerfRender(skipped: true) return true } @@ -557,8 +569,10 @@ extension StatusItemController { tilt: tilt, statusIndicator: statusIndicator, hideCritters: self.settings.menuBarHidesCritters) - self.setButtonImage( - warningFlash ? Self.quotaWarningFlashImage(base: image) : image, for: button) + self.setButtonContent( + image: warningFlash ? Self.quotaWarningFlashImage(base: image) : image, + title: nil, + for: button) } self.noteIconPerfRender(skipped: false) return false @@ -692,17 +706,54 @@ extension StatusItemController { return image } - private func setButtonImage(_ image: NSImage, for button: NSStatusBarButton) { - if button.image === image { return } - button.image = image + private var shouldUseHighContrastStatusItemContent: Bool { + self.settings.menuBarHighContrastOnInactiveDisplays + && self.settings.menuBarIconStyle == .iconAndPercent } - private func setButtonTitle(_ title: String?, for button: NSStatusBarButton) { + func prepareButtonForImageOnlyCacheHit(_ button: NSStatusBarButton) -> Bool { + if self.shouldUseHighContrastStatusItemContent { + guard button.image == nil, + button.imagePosition == .noImage, + button.attributedTitle.length > 0 + else { return false } + return button.attributedTitle.attribute( + .attachment, + at: 0, + effectiveRange: nil) is NSTextAttachment + } + + let value = Self.buttonTitle( + nil, + hasImage: true, + isDebugApp: Self.isDebugApp(bundleIdentifier: Bundle.main.bundleIdentifier)) + if button.title != value { + button.title = value + } + let position: NSControl.ImagePosition = value.isEmpty ? .imageOnly : .imageLeft + if button.imagePosition != position { + button.imagePosition = position + } + return true + } + + private func setButtonContent(image: NSImage, title: String?, for button: NSStatusBarButton) { let isDebugApp = Self.isDebugApp(bundleIdentifier: Bundle.main.bundleIdentifier) let value = Self.buttonTitle( title, - hasImage: button.image != nil || title == nil, + hasImage: true, isDebugApp: isDebugApp) + + if self.shouldUseHighContrastStatusItemContent { + button.image = nil + button.imagePosition = .noImage + button.attributedTitle = Self.highContrastButtonTitle(image: image, title: value) + return + } + + if button.image !== image { + button.image = image + } if button.title != value { button.title = value } @@ -712,6 +763,28 @@ extension StatusItemController { } } + static func highContrastButtonTitle(image: NSImage, title: String) -> NSAttributedString { + let font = NSFont.systemFont(ofSize: NSFont.systemFontSize) + let attachment = NSTextAttachment() + attachment.image = image + attachment.bounds = NSRect( + x: 0, + y: ((font.capHeight - image.size.height) / 2).rounded(), + width: image.size.width, + height: image.size.height) + + let value = NSMutableAttributedString(attachment: attachment) + if !title.isEmpty { + value.append(NSAttributedString( + string: title, + attributes: [ + .font: font, + .foregroundColor: NSColor.labelColor, + ])) + } + return value + } + nonisolated static func buttonTitle(_ title: String?, hasImage: Bool, isDebugApp: Bool = false) -> String { var parts: [String] = [] if let title, !title.isEmpty { diff --git a/Tests/CodexBarTests/PreferencesPaneSmokeTests.swift b/Tests/CodexBarTests/PreferencesPaneSmokeTests.swift index 01198ad337..fee1650b6d 100644 --- a/Tests/CodexBarTests/PreferencesPaneSmokeTests.swift +++ b/Tests/CodexBarTests/PreferencesPaneSmokeTests.swift @@ -28,6 +28,7 @@ struct PreferencesPaneSmokeTests { func `builds preference panes with toggled settings`() { let settings = Self.makeSettingsStore(suite: "PreferencesPaneSmokeTests-toggled") settings.menuBarShowsBrandIconWithPercent = true + settings.menuBarHighContrastOnInactiveDisplays = true settings.menuBarShowsHighestUsage = true settings.multiAccountMenuLayout = .stacked settings.hidePersonalInfo = true @@ -121,6 +122,13 @@ struct PreferencesPaneSmokeTests { #expect(!text.contains("%@")) } + @Test + func `inactive display contrast is available only for icon and percent`() { + #expect(!MenuBarPane.inactiveDisplayContrastAvailable(for: .critters)) + #expect(!MenuBarPane.inactiveDisplayContrastAvailable(for: .bars)) + #expect(MenuBarPane.inactiveDisplayContrastAvailable(for: .iconAndPercent)) + } + @Test func `menu bar icon style maps existing booleans`() { let settings = Self.makeSettingsStore(suite: "PreferencesPaneSmokeTests-menu-bar-icon-style") diff --git a/Tests/CodexBarTests/SettingsStoreTests.swift b/Tests/CodexBarTests/SettingsStoreTests.swift index e2face6bf6..d021c1d4cf 100644 --- a/Tests/CodexBarTests/SettingsStoreTests.swift +++ b/Tests/CodexBarTests/SettingsStoreTests.swift @@ -326,6 +326,32 @@ struct SettingsStoreTests { #expect(storeB.menuBarHidesCritters == true) } + @Test + func `inactive display contrast setting defaults off and persists`() throws { + let suite = "SettingsStoreTests-inactive-display-contrast" + let defaultsA = try #require(UserDefaults(suiteName: suite)) + defaultsA.removePersistentDomain(forName: suite) + let configStore = testConfigStore(suiteName: suite) + let storeA = SettingsStore( + userDefaults: defaultsA, + configStore: configStore, + zaiTokenStore: NoopZaiTokenStore(), + syntheticTokenStore: NoopSyntheticTokenStore()) + + #expect(storeA.menuBarHighContrastOnInactiveDisplays == false) + #expect(defaultsA.bool(forKey: "menuBarHighContrastOnInactiveDisplays") == false) + storeA.menuBarHighContrastOnInactiveDisplays = true + + let defaultsB = try #require(UserDefaults(suiteName: suite)) + let storeB = SettingsStore( + userDefaults: defaultsB, + configStore: configStore, + zaiTokenStore: NoopZaiTokenStore(), + syntheticTokenStore: NoopSyntheticTokenStore()) + + #expect(storeB.menuBarHighContrastOnInactiveDisplays == true) + } + @Test func `persists selected menu provider across instances`() throws { let suite = "SettingsStoreTests-selectedMenuProvider" diff --git a/Tests/CodexBarTests/StatusItemAnimationSignatureTests.swift b/Tests/CodexBarTests/StatusItemAnimationSignatureTests.swift index a0320708ba..a5986037d7 100644 --- a/Tests/CodexBarTests/StatusItemAnimationSignatureTests.swift +++ b/Tests/CodexBarTests/StatusItemAnimationSignatureTests.swift @@ -292,6 +292,139 @@ struct StatusItemAnimationSignatureTests { #expect(button.imagePosition == .imageLeft) } + @Test + func `merged icon only content repairs stale title when cached render is skipped`() throws { + let suite = "StatusItemAnimationSignatureTests-merged-icon-only-title-restore" + let settings = testSettingsStore(suiteName: suite) + settings.statusChecksEnabled = false + settings.refreshFrequency = .manual + settings.mergeIcons = true + settings.selectedMenuProvider = .codex + settings.menuBarShowsBrandIconWithPercent = false + + let registry = ProviderRegistry.shared + if let codexMeta = registry.metadata[.codex] { + settings.setProviderEnabled(provider: .codex, metadata: codexMeta, enabled: true) + } + if let claudeMeta = registry.metadata[.claude] { + settings.setProviderEnabled(provider: .claude, metadata: claudeMeta, enabled: true) + } + + let fetcher = UsageFetcher() + let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings) + let controller = StatusItemController( + store: store, + settings: settings, + account: fetcher.loadAccountInfo(), + updater: DisabledUpdaterController(), + preferencesSelection: PreferencesSelection(), + statusBar: testStatusBar()) + defer { controller.releaseStatusItemsForTesting() } + + store._setSnapshotForTesting( + UsageSnapshot( + primary: RateWindow(usedPercent: 23, windowMinutes: nil, resetsAt: nil, resetDescription: nil), + secondary: nil, + updatedAt: Date()), + provider: .codex) + + controller.applyIcon(phase: nil) + let button = try #require(controller.statusItem.button) + button.title = " stale" + button.imagePosition = .imageLeft + + let skipped = controller.applyIcon(phase: nil) + + #expect(skipped) + #expect(button.title.isEmpty) + #expect(button.imagePosition == .imageOnly) + } + + @Test + func `inactive display contrast embeds the brand and restores standard content when disabled`() throws { + let suite = "StatusItemAnimationSignatureTests-inactive-display-contrast" + let settings = testSettingsStore(suiteName: suite) + settings.statusChecksEnabled = false + settings.refreshFrequency = .manual + settings.mergeIcons = true + settings.selectedMenuProvider = .codex + settings.menuBarShowsBrandIconWithPercent = true + settings.menuBarHighContrastOnInactiveDisplays = true + settings.menuBarDisplayMode = .percent + settings.usageBarsShowUsed = false + + let registry = ProviderRegistry.shared + if let codexMeta = registry.metadata[.codex] { + settings.setProviderEnabled(provider: .codex, metadata: codexMeta, enabled: true) + } + + let fetcher = UsageFetcher() + let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings) + let controller = StatusItemController( + store: store, + settings: settings, + account: fetcher.loadAccountInfo(), + updater: DisabledUpdaterController(), + preferencesSelection: PreferencesSelection(), + statusBar: testStatusBar()) + defer { controller.releaseStatusItemsForTesting() } + + let snapshot = UsageSnapshot( + primary: RateWindow(usedPercent: 23, windowMinutes: nil, resetsAt: nil, resetDescription: nil), + secondary: nil, + updatedAt: Date()) + store._setSnapshotForTesting(snapshot, provider: .codex) + + let displayText = try #require(controller.menuBarDisplayText(for: .codex, snapshot: snapshot)) + let expectedTitle = StatusItemController.buttonTitle(displayText, hasImage: true) + controller.applyIcon(phase: nil) + let button = try #require(controller.statusItem.button) + + #expect(button.image == nil) + #expect(button.imagePosition == .noImage) + #expect(button.attributedTitle.string == "\u{FFFC}\(expectedTitle)") + #expect(button.attributedTitle.attribute(.attachment, at: 0, effectiveRange: nil) is NSTextAttachment) + + #expect(controller.prepareButtonForImageOnlyCacheHit(button)) + #expect(button.image == nil) + #expect(button.imagePosition == .noImage) + #expect(button.attributedTitle.string == "\u{FFFC}\(expectedTitle)") + #expect(button.attributedTitle.attribute(.attachment, at: 0, effectiveRange: nil) is NSTextAttachment) + + button.attributedTitle = NSAttributedString() + #expect(!controller.prepareButtonForImageOnlyCacheHit(button)) + + controller.applyIcon(phase: nil) + #expect(button.attributedTitle.string == "\u{FFFC}\(expectedTitle)") + #expect(button.attributedTitle.attribute(.attachment, at: 0, effectiveRange: nil) is NSTextAttachment) + + settings.menuBarIconStyle = .critters + let critterSkipped = controller.applyIcon(phase: nil) + + #expect(!critterSkipped) + #expect(button.image != nil) + #expect(button.title.isEmpty) + #expect(button.imagePosition == .imageOnly) + #expect(button.attributedTitle.length == 0) + + settings.menuBarIconStyle = .iconAndPercent + controller.applyIcon(phase: nil) + + #expect(button.image == nil) + #expect(button.imagePosition == .noImage) + #expect(button.attributedTitle.string == "\u{FFFC}\(expectedTitle)") + #expect(button.attributedTitle.attribute(.attachment, at: 0, effectiveRange: nil) is NSTextAttachment) + + settings.menuBarHighContrastOnInactiveDisplays = false + let skipped = controller.applyIcon(phase: nil) + + #expect(!skipped) + #expect(button.image != nil) + #expect(button.title == expectedTitle) + #expect(button.imagePosition == .imageLeft) + #expect(button.attributedTitle.attribute(.attachment, at: 0, effectiveRange: nil) == nil) + } + @Test func `merged icon render defers while merged menu is tracking`() async throws { let suite = "StatusItemAnimationSignatureTests-merged-icon-defers-during-tracking" @@ -706,7 +839,7 @@ struct StatusItemAnimationSignatureTests { } @Test - func `split provider icon skips unchanged render signature`() { + func `split provider icon skips unchanged render signature`() throws { let suite = "StatusItemAnimationSignatureTests-split-provider-signature" let settings = testSettingsStore(suiteName: suite) settings.statusChecksEnabled = false @@ -737,7 +870,13 @@ struct StatusItemAnimationSignatureTests { provider: .codex) #expect(controller.applyIcon(for: .codex, phase: nil) == false) + let button = try #require(controller.statusItems[.codex]?.button) + button.title = " stale" + button.imagePosition = .imageLeft + #expect(controller.applyIcon(for: .codex, phase: nil) == true) + #expect(button.title.isEmpty) + #expect(button.imagePosition == .imageOnly) store._setSnapshotForTesting( UsageSnapshot( diff --git a/Tests/CodexBarTests/StatusItemBalanceDisplayTests.swift b/Tests/CodexBarTests/StatusItemBalanceDisplayTests.swift index 3074f52701..bbb141abd6 100644 --- a/Tests/CodexBarTests/StatusItemBalanceDisplayTests.swift +++ b/Tests/CodexBarTests/StatusItemBalanceDisplayTests.swift @@ -663,6 +663,22 @@ struct StatusItemBalanceDisplayTests { #expect(StatusItemController.buttonTitle("42%", hasImage: false, isDebugApp: true) == "42% D") } + @Test + func `high contrast button title embeds image and metric in attributed content`() throws { + let image = NSImage(size: NSSize(width: 18, height: 18)) + image.isTemplate = true + + let title = StatusItemController.highContrastButtonTitle(image: image, title: " 42%") + + #expect(title.string == "\u{FFFC} 42%") + let attachment = try #require(title.attribute(.attachment, at: 0, effectiveRange: nil) as? NSTextAttachment) + #expect(attachment.image === image) + #expect(attachment.bounds.width == 18) + #expect(attachment.bounds.height == 18) + #expect(title.attribute(.font, at: 1, effectiveRange: nil) is NSFont) + #expect(title.attribute(.foregroundColor, at: 1, effectiveRange: nil) as? NSColor == .labelColor) + } + @Test func `debug bundle identity updates status item accessibility`() { #expect(StatusItemController.isDebugApp(bundleIdentifier: "com.steipete.codexbar.debug"))