Two Studio-layout themes for KDE Plasma desktops: original colour values
chosen to sit close to Breeze's cool blue-grey surfaces and blue accent
(and its 5 px radius) so the app blends into a Plasma desktop, without
copying Breeze's colour schemes. Text readability comes first.
- theme.rs: ThemeKind::BreezeLight / BreezeDark. Every text colour (text,
secondary, faint, accent, danger, warning) holds 4.5:1 on every surface
and on hovered, pressed and selected states; the primary button is a
deeper blue that takes white text and stays above 4.5:1 while it
darkens on hover and press. Breeze's own inactive text and link blue
are only about 2.8:1 on its light surfaces, so ours are darker. The
close button goes red on hover.
- prefs: Theme/LightTheme::BreezeLight ("breezeLight") and
Theme/DarkTheme::BreezeDark ("breezeDark"): Preferences > Interface,
`prefs.set`.
- Window > Theme > Breeze Light/Dark Theme (`window.theme.breezeLight`,
`window.theme.breezeDark`) and `ui.set {"theme": ...}` (aliases:
breeze, kde, plasma for the light one; ...dark for the dark one).
- i18n: the four new labels in all 16 catalogs.
- docs/ui-design.md and ATTRIBUTION.md (original work, credits Breeze as
the inspiration; Breeze's schemes are LGPL-2.0-or-later and none of
their values, files or code are copied).
- Tests: names/aliases, surface order and blue-grey character, the
contrast matrix for both themes, and the preference/mode selection.
This commit is contained in:
parent
0c4016a4db
commit
67e34a1d4c
|
|
@ -36,6 +36,7 @@ next to them.
|
|||
| `assets/app-icon/` (all files) | PhotoCraft app icon (nine-tailed kitsune) | The project owner (drawn in ArtCraft, vectorised) | Original work, see [`assets/app-icon/README.md`](assets/app-icon/README.md) | MIT OR Apache-2.0, [`assets/app-icon/LICENSE.txt`](assets/app-icon/LICENSE.txt) |
|
||||
| `packaging/macos/dmg/` (all files) | PhotoCraft DMG window background and layout | @XusBadia, from the PhotoCraft app icon | Original work, see [`packaging/macos/dmg/README.md`](packaging/macos/dmg/README.md) | MIT OR Apache-2.0, like the code |
|
||||
| `crates/ui-egui/src/theme.rs` (Solarized Dark palette) | Solarized colour scheme used by the Solarized Dark theme | Ethan Schoonover | <https://ethanschoonover.com/solarized/> | MIT, [`LICENSE-solarized.txt`](crates/ui-egui/src/LICENSE-solarized.txt) |
|
||||
| `crates/ui-egui/src/theme.rs` (Breeze Light and Breeze Dark themes) | The Breeze Light and Breeze Dark themes are original colour values on PhotoCraft's Studio layout, chosen to look close to KDE Plasma's Breeze (cool blue-greys, blue accent, 5 px frame radius) so the app blends into a Plasma desktop. None of Breeze's colour values, files or code are copied; the values are a few levels away from its schemes and tuned for text contrast | PhotoCraft contributors | Inspired by the look of KDE Breeze, <https://invent.kde.org/plasma/breeze> (its colour schemes are LGPL-2.0-or-later) | Original work |
|
||||
|
||||
### Optional build input: craft-fonts (not files in this repo)
|
||||
|
||||
|
|
|
|||
|
|
@ -229,7 +229,7 @@ pub struct UiSetParams {
|
|||
/// Fields for the control method `ui.set`: tool, panels, dock, dockTabs, dockWidth, colorPanel
|
||||
/// ({background: bool} picks which swatch the Color panel edits), maskTarget,
|
||||
/// vectorMaskTarget, selectionMode, zoom, center, rotation (view angle in degrees), fit, theme (pro, proMedium, studio,
|
||||
/// studioLight, classic, solarizedDark, adwaita, adwaitaDark), brushSection, brushTab,
|
||||
/// studioLight, classic, solarizedDark, adwaita, adwaitaDark, breezeLight, breezeDark), brushSection, brushTab,
|
||||
/// brushesView, brushPicker ([x, y] opens the Brush Preset picker there, null closes it),
|
||||
/// brushPickerName, brushPickerStroke, brushPickerTip (the picker card's parts, at least one
|
||||
/// on), brushPickerScale (the picker footer's card-scale slider, 0.15 to 2, 1 the standard),
|
||||
|
|
|
|||
|
|
@ -55,10 +55,10 @@ choice!(TypeUnit { Points = "points", Pixels = "pixels", Millimeters = "mm" } de
|
|||
choice!(PointSize { PostScript = "postScript", Traditional = "traditional" } default PostScript);
|
||||
choice!(Interpolation { BicubicAutomatic = "bicubicAutomatic", Nearest = "nearestNeighbor", Bilinear = "bilinear", Bicubic = "bicubic", BicubicSmoother = "bicubicSmoother", BicubicSharper = "bicubicSharper", PreserveDetails = "preserveDetails" } default BicubicAutomatic);
|
||||
choice!(ColorPicker { Adobe = "adobe", System = "system" } default Adobe);
|
||||
choice!(Theme { Pro = "pro", ProMedium = "proMedium", Studio = "studio", StudioLight = "studioLight", Classic = "classic", SolarizedDark = "solarizedDark", Adwaita = "adwaita", AdwaitaDark = "adwaitaDark" } default ProMedium);
|
||||
choice!(Theme { Pro = "pro", ProMedium = "proMedium", Studio = "studio", StudioLight = "studioLight", Classic = "classic", SolarizedDark = "solarizedDark", Adwaita = "adwaita", AdwaitaDark = "adwaitaDark", BreezeLight = "breezeLight", BreezeDark = "breezeDark" } default ProMedium);
|
||||
choice!(AppearanceMode { Auto = "auto", Dark = "dark", Light = "light" } default Dark);
|
||||
choice!(DarkTheme { Pro = "pro", ProMedium = "proMedium", Studio = "studio", SolarizedDark = "solarizedDark", AdwaitaDark = "adwaitaDark" } default ProMedium);
|
||||
choice!(LightTheme { StudioLight = "studioLight", Classic = "classic", Adwaita = "adwaita" } default StudioLight);
|
||||
choice!(DarkTheme { Pro = "pro", ProMedium = "proMedium", Studio = "studio", SolarizedDark = "solarizedDark", AdwaitaDark = "adwaitaDark", BreezeDark = "breezeDark" } default ProMedium);
|
||||
choice!(LightTheme { StudioLight = "studioLight", Classic = "classic", Adwaita = "adwaita", BreezeLight = "breezeLight" } default StudioLight);
|
||||
choice!(CanvasColor { Default = "default", Black = "black", DarkGray = "darkGray", MediumGray = "mediumGray", LightGray = "lightGray", Custom = "custom" } default Default);
|
||||
choice!(CanvasBorder { DropShadow = "dropShadow", Line = "line", None = "none" } default DropShadow);
|
||||
choice!(UiScale { Auto = "auto", P75 = "75", P80 = "80", P85 = "85", P90 = "90", P95 = "95", P100 = "100", P125 = "125", P150 = "150", P175 = "175", P200 = "200", P250 = "250", P300 = "300" } default Auto);
|
||||
|
|
@ -1462,8 +1462,8 @@ impl Session {
|
|||
&& let Some(theme) = interface.get("theme").and_then(Value::as_str).and_then(Theme::parse)
|
||||
{
|
||||
let (mode, slot) = match theme {
|
||||
Theme::Pro | Theme::ProMedium | Theme::Studio | Theme::SolarizedDark | Theme::AdwaitaDark => ("dark", "darkTheme"),
|
||||
Theme::StudioLight | Theme::Classic | Theme::Adwaita => ("light", "lightTheme"),
|
||||
Theme::Pro | Theme::ProMedium | Theme::Studio | Theme::SolarizedDark | Theme::AdwaitaDark | Theme::BreezeDark => ("dark", "darkTheme"),
|
||||
Theme::StudioLight | Theme::Classic | Theme::Adwaita | Theme::BreezeLight => ("light", "lightTheme"),
|
||||
};
|
||||
interface.insert("appearanceMode".into(), json!(mode));
|
||||
interface.insert(slot.into(), json!(theme.name()));
|
||||
|
|
@ -1564,6 +1564,14 @@ impl Session {
|
|||
next.interface.dark_theme = DarkTheme::AdwaitaDark;
|
||||
next.interface.appearance_mode = AppearanceMode::Dark;
|
||||
}
|
||||
Theme::BreezeDark => {
|
||||
next.interface.dark_theme = DarkTheme::BreezeDark;
|
||||
next.interface.appearance_mode = AppearanceMode::Dark;
|
||||
}
|
||||
Theme::BreezeLight => {
|
||||
next.interface.light_theme = LightTheme::BreezeLight;
|
||||
next.interface.appearance_mode = AppearanceMode::Light;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.prefs.edit(|p| *p = next);
|
||||
|
|
|
|||
|
|
@ -696,6 +696,10 @@
|
|||
Classic Theme Klasický motiv
|
||||
Solarized dark Solarized tmavý
|
||||
Solarized Dark Theme Tmavý motiv Solarized
|
||||
Breeze dark Breeze tmavý
|
||||
Breeze Dark Theme Tmavý motiv Breeze
|
||||
Breeze light Breeze světlý
|
||||
Breeze Light Theme Světlý motiv Breeze
|
||||
Next Theme Další motiv
|
||||
Actions Akce
|
||||
Brush Settings Nastavení štětce
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -682,6 +682,10 @@
|
|||
Classic Theme Klassisches Design
|
||||
Solarized dark Solarized dunkel
|
||||
Solarized Dark Theme Solarized-Design (Dunkel)
|
||||
Breeze dark Breeze dunkel
|
||||
Breeze Dark Theme Breeze-Design (Dunkel)
|
||||
Breeze light Breeze hell
|
||||
Breeze Light Theme Breeze-Design (Hell)
|
||||
Next Theme Nächstes Design
|
||||
Actions Aktionen
|
||||
Brush Settings Pinseleinstellungen
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -2070,6 +2070,10 @@
|
|||
Classic Theme Κλασικό θέμα
|
||||
Solarized dark Solarized σκούρο
|
||||
Solarized Dark Theme Σκούρο θέμα Solarized
|
||||
Breeze dark Breeze σκούρο
|
||||
Breeze Dark Theme Σκούρο θέμα Breeze
|
||||
Breeze light Breeze φωτεινό
|
||||
Breeze Light Theme Φωτεινό θέμα Breeze
|
||||
Clear Canvas Guides Απαλοιφή οδηγών καμβά
|
||||
Clear Override Απαλοιφή παράκαμψης
|
||||
Clear Samples Απαλοιφή δειγμάτων
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 5.
|
|
|
@ -688,6 +688,10 @@
|
|||
Classic Theme Tema Clásico
|
||||
Solarized dark Solarized oscuro
|
||||
Solarized Dark Theme Tema Solarized oscuro
|
||||
Breeze dark Breeze oscuro
|
||||
Breeze Dark Theme Tema Breeze oscuro
|
||||
Breeze light Breeze claro
|
||||
Breeze Light Theme Tema Breeze claro
|
||||
Next Theme Tema siguiente
|
||||
Actions Acciones
|
||||
Brush Settings Ajustes del pincel
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 4 and column 47.
|
|
|
@ -680,6 +680,10 @@
|
|||
Classic Theme Thème Classique
|
||||
Solarized dark Solarized sombre
|
||||
Solarized Dark Theme Thème Solarized sombre
|
||||
Breeze dark Breeze sombre
|
||||
Breeze Dark Theme Thème Breeze sombre
|
||||
Breeze light Breeze clair
|
||||
Breeze Light Theme Thème Breeze clair
|
||||
Next Theme Thème suivant
|
||||
Actions Actions
|
||||
Brush Settings Paramètres du pinceau
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -673,6 +673,10 @@
|
|||
Classic Theme Tema Klasik
|
||||
Solarized dark Solarized gelap
|
||||
Solarized Dark Theme Tema Solarized Gelap
|
||||
Breeze dark Breeze gelap
|
||||
Breeze Dark Theme Tema Breeze Gelap
|
||||
Breeze light Breeze terang
|
||||
Breeze Light Theme Tema Breeze Terang
|
||||
Next Theme Tema Berikutnya
|
||||
Actions Action
|
||||
Brush Settings Pengaturan Kuas
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 4 and column 47.
|
|
|
@ -686,6 +686,10 @@
|
|||
Classic Theme Tema classico
|
||||
Solarized dark Solarized scuro
|
||||
Solarized Dark Theme Tema Solarized scuro
|
||||
Breeze dark Breeze scuro
|
||||
Breeze Dark Theme Tema Breeze scuro
|
||||
Breeze light Breeze chiaro
|
||||
Breeze Light Theme Tema Breeze chiaro
|
||||
Next Theme Tema successivo
|
||||
Actions Azioni
|
||||
Brush Settings Impostazioni pennello
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 14.
|
|
|
@ -692,6 +692,10 @@
|
|||
Classic Theme クラシックテーマ
|
||||
Solarized dark Solarized ダーク
|
||||
Solarized Dark Theme Solarized ダークテーマ
|
||||
Breeze dark Breeze ダーク
|
||||
Breeze Dark Theme Breeze ダークテーマ
|
||||
Breeze light Breeze ライト
|
||||
Breeze Light Theme Breeze ライトテーマ
|
||||
Next Theme 次のテーマ
|
||||
Actions アクション
|
||||
Brush Settings ブラシ設定
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -664,6 +664,10 @@
|
|||
Classic Theme 클래식 테마
|
||||
Solarized dark Solarized 어두움
|
||||
Solarized Dark Theme Solarized 어두운 테마
|
||||
Breeze dark Breeze 어두움
|
||||
Breeze Dark Theme Breeze 어두운 테마
|
||||
Breeze light Breeze 밝게
|
||||
Breeze Light Theme Breeze 밝은 테마
|
||||
Next Theme 다음 테마
|
||||
Actions 액션
|
||||
Brush Settings 브러시 설정
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 10.
|
|
|
@ -672,6 +672,10 @@
|
|||
Classic Theme Klassiek thema
|
||||
Solarized dark Solarized donker
|
||||
Solarized Dark Theme Solarized-thema donker
|
||||
Breeze dark Breeze donker
|
||||
Breeze Dark Theme Breeze-thema donker
|
||||
Breeze light Breeze licht
|
||||
Breeze Light Theme Breeze-thema licht
|
||||
Adwaita Light Theme Adwaita-thema licht
|
||||
Adwaita Dark Theme Adwaita-thema donker
|
||||
Adwaita Adwaita
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 8 and column 34.
|
|
|
@ -690,6 +690,10 @@
|
|||
Classic Theme Motyw klasyczny
|
||||
Solarized dark Solarized ciemny
|
||||
Solarized Dark Theme Ciemny motyw Solarized
|
||||
Breeze dark Breeze ciemny
|
||||
Breeze Dark Theme Ciemny motyw Breeze
|
||||
Breeze light Breeze jasny
|
||||
Breeze Light Theme Jasny motyw Breeze
|
||||
Next Theme Następny motyw
|
||||
Actions Akcje
|
||||
Brush Settings Ustawienia pędzla
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -677,6 +677,10 @@
|
|||
Classic Theme Tema clássico
|
||||
Solarized dark Solarized escuro
|
||||
Solarized Dark Theme Tema Solarized Escuro
|
||||
Breeze dark Breeze escuro
|
||||
Breeze Dark Theme Tema Breeze Escuro
|
||||
Breeze light Breeze claro
|
||||
Breeze Light Theme Tema Breeze Claro
|
||||
Next Theme Próximo tema
|
||||
Actions Ações
|
||||
Brush Settings Configurações do pincel
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -1393,6 +1393,10 @@
|
|||
Classic Theme Классическая тема
|
||||
Solarized dark Solarized тёмная
|
||||
Solarized Dark Theme Тёмная тема Solarized
|
||||
Breeze dark Breeze тёмная
|
||||
Breeze Dark Theme Тёмная тема Breeze
|
||||
Breeze light Breeze светлая
|
||||
Breeze Light Theme Светлая тема Breeze
|
||||
Studio Theme Тема Studio
|
||||
Studio Light Theme Светлая тема Studio
|
||||
Adwaita Light Theme Светлая тема Adwaita
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -1374,6 +1374,10 @@
|
|||
Classic Theme Класична тема
|
||||
Solarized dark Solarized темна
|
||||
Solarized Dark Theme Темна тема Solarized
|
||||
Breeze dark Breeze темна
|
||||
Breeze Dark Theme Темна тема Breeze
|
||||
Breeze light Breeze світла
|
||||
Breeze Light Theme Світла тема Breeze
|
||||
Adwaita Light Theme Світла тема Adwaita
|
||||
Adwaita Dark Theme Темна тема Adwaita
|
||||
Adwaita Adwaita
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 12.
|
|
|
@ -692,6 +692,10 @@
|
|||
Classic Theme 经典主题
|
||||
Solarized dark Solarized 深色
|
||||
Solarized Dark Theme Solarized 深色主题
|
||||
Breeze dark Breeze 深色
|
||||
Breeze Dark Theme Breeze 深色主题
|
||||
Breeze light Breeze 浅色
|
||||
Breeze Light Theme Breeze 浅色主题
|
||||
Next Theme 下一代主题
|
||||
Actions 动作
|
||||
Brush Settings 画笔设置
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 14.
|
|
|
@ -696,6 +696,10 @@
|
|||
Classic Theme 經典主題
|
||||
Solarized dark Solarized 深色
|
||||
Solarized Dark Theme Solarized 深色主題
|
||||
Breeze dark Breeze 深色
|
||||
Breeze Dark Theme Breeze 深色主題
|
||||
Breeze light Breeze 淺色
|
||||
Breeze Light Theme Breeze 淺色主題
|
||||
Next Theme 下一個主題
|
||||
Actions 動作
|
||||
Brush Settings 筆刷設定
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -75,6 +75,8 @@ pub const UI_COMMANDS: &[(&str, &str, &[&str], Option<&str>)] = &[
|
|||
("window.theme.solarizedDark", "Solarized Dark Theme", &["Window", "Theme"], None),
|
||||
("window.theme.adwaita", "Adwaita Light Theme", &["Window", "Theme"], None),
|
||||
("window.theme.adwaitaDark", "Adwaita Dark Theme", &["Window", "Theme"], None),
|
||||
("window.theme.breezeLight", "Breeze Light Theme", &["Window", "Theme"], None),
|
||||
("window.theme.breezeDark", "Breeze Dark Theme", &["Window", "Theme"], None),
|
||||
("window.theme.system", "Sync with system", &["Window", "Theme"], None),
|
||||
("edit.search", "Search…", &["Edit"], Some("Cmd+F")),
|
||||
("help.discord", "Join the ArtCraft Discord…", &["Help"], None),
|
||||
|
|
@ -329,7 +331,9 @@ pub(crate) fn invoke_unguarded(app: &mut PhotocraftApp, ctx: &egui::Context, id:
|
|||
| "window.theme.classic"
|
||||
| "window.theme.solarizedDark"
|
||||
| "window.theme.adwaita"
|
||||
| "window.theme.adwaitaDark" => {
|
||||
| "window.theme.adwaitaDark"
|
||||
| "window.theme.breezeLight"
|
||||
| "window.theme.breezeDark" => {
|
||||
let k = crate::theme::ThemeKind::from_name(&id["window.theme.".len()..]).unwrap_or_default();
|
||||
app.set_theme(ctx, k);
|
||||
Ok(Value::Null)
|
||||
|
|
|
|||
|
|
@ -90,6 +90,7 @@ fn selected_theme(interface: &prefs::Interface, system: Option<egui::Theme>) ->
|
|||
LightTheme::StudioLight => ThemeKind::StudioLight,
|
||||
LightTheme::Classic => ThemeKind::Classic,
|
||||
LightTheme::Adwaita => ThemeKind::Adwaita,
|
||||
LightTheme::BreezeLight => ThemeKind::BreezeLight,
|
||||
}
|
||||
} else {
|
||||
match interface.dark_theme {
|
||||
|
|
@ -98,6 +99,7 @@ fn selected_theme(interface: &prefs::Interface, system: Option<egui::Theme>) ->
|
|||
DarkTheme::Studio => ThemeKind::Studio,
|
||||
DarkTheme::SolarizedDark => ThemeKind::SolarizedDark,
|
||||
DarkTheme::AdwaitaDark => ThemeKind::AdwaitaDark,
|
||||
DarkTheme::BreezeDark => ThemeKind::BreezeDark,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1209,7 +1211,7 @@ fn theme_card(ui: &mut egui::Ui, title: &str, active: bool, selected: &mut Strin
|
|||
.inner_margin(10.0)
|
||||
.show(ui, |ui| {
|
||||
ui.set_width(width - 20.0);
|
||||
// Room for the longer list (five dark themes), so both cards line up.
|
||||
// Room for the longer list (six dark themes), so both cards line up.
|
||||
ui.set_min_height(298.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(RichText::new(tl!(title)).strong().color(t.text));
|
||||
|
|
@ -1252,7 +1254,12 @@ fn appearance_rows(ui: &mut egui::Ui, obj: &mut Map<String, Value>, system: Opti
|
|||
"Light Theme",
|
||||
light_active,
|
||||
&mut light,
|
||||
&[("studioLight", ThemeKind::StudioLight), ("classic", ThemeKind::Classic), ("adwaita", ThemeKind::Adwaita)],
|
||||
&[
|
||||
("studioLight", ThemeKind::StudioLight),
|
||||
("classic", ThemeKind::Classic),
|
||||
("adwaita", ThemeKind::Adwaita),
|
||||
("breezeLight", ThemeKind::BreezeLight),
|
||||
],
|
||||
width,
|
||||
);
|
||||
});
|
||||
|
|
@ -1269,6 +1276,7 @@ fn appearance_rows(ui: &mut egui::Ui, obj: &mut Map<String, Value>, system: Opti
|
|||
("studio", ThemeKind::Studio),
|
||||
("solarizedDark", ThemeKind::SolarizedDark),
|
||||
("adwaitaDark", ThemeKind::AdwaitaDark),
|
||||
("breezeDark", ThemeKind::BreezeDark),
|
||||
],
|
||||
width,
|
||||
);
|
||||
|
|
@ -1921,6 +1929,23 @@ mod tests {
|
|||
assert_eq!(selected_theme(&interface, Some(egui::Theme::Dark)), ThemeKind::Classic);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn breeze_themes_are_chosen_by_preference_and_fix_the_matching_mode() {
|
||||
let ctx = egui::Context::default();
|
||||
let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
|
||||
for (id, kind, mode) in [("breezeLight", ThemeKind::BreezeLight, AppearanceMode::Light), ("breezeDark", ThemeKind::BreezeDark, AppearanceMode::Dark)] {
|
||||
app.run("prefs.set", json!({"path": "interface.theme", "value": id})).unwrap();
|
||||
tick(&mut app, &ctx);
|
||||
let interface = app.session.prefs().interface.clone();
|
||||
assert_eq!(interface.appearance_mode, mode, "{id}");
|
||||
assert_eq!(selected_theme(&interface, None), kind, "{id}");
|
||||
assert_eq!(app.ui.theme, kind, "{id}");
|
||||
}
|
||||
// The two slots are independent: choosing the dark one left the light choice alone.
|
||||
assert_eq!(app.session.prefs().interface.light_theme, LightTheme::BreezeLight);
|
||||
assert_eq!(app.session.prefs().interface.dark_theme, DarkTheme::BreezeDark);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_uses_native_system_theme_when_egui_does_not_report_one() {
|
||||
let (mut app, _) = app_with_store();
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@
|
|||
//! with the rest of a GNOME desktop.
|
||||
//! - **Solarized Dark**: Ethan Schoonover's Solarized palette (base03/base02 surfaces, base0 text,
|
||||
//! Solarized blue accent) with the Studio layout.
|
||||
//! - **Breeze** (Light and Dark): the Studio layout adapted to the look of KDE Plasma's Breeze
|
||||
//! (blue-grey surfaces, Breeze blue accent, 5 px corners), so the app sits with the rest of a
|
||||
//! Plasma desktop.
|
||||
//!
|
||||
//! Widgets read [`Tokens::get`] instead of hard-coding colours, so every theme applies everywhere.
|
||||
|
||||
|
|
@ -34,10 +37,14 @@ pub enum ThemeKind {
|
|||
/// Solarized Dark (Ethan Schoonover's palette): the base03 background, base02 panels and
|
||||
/// base0 body text, with the Solarized blue accent.
|
||||
SolarizedDark,
|
||||
/// An adaptation of KDE Plasma's Breeze Light look.
|
||||
BreezeLight,
|
||||
/// An adaptation of KDE Plasma's Breeze Dark look.
|
||||
BreezeDark,
|
||||
}
|
||||
|
||||
impl ThemeKind {
|
||||
pub const ALL: [ThemeKind; 8] = [
|
||||
pub const ALL: [ThemeKind; 10] = [
|
||||
ThemeKind::Pro,
|
||||
ThemeKind::ProMedium,
|
||||
ThemeKind::Studio,
|
||||
|
|
@ -46,6 +53,8 @@ impl ThemeKind {
|
|||
ThemeKind::SolarizedDark,
|
||||
ThemeKind::Adwaita,
|
||||
ThemeKind::AdwaitaDark,
|
||||
ThemeKind::BreezeLight,
|
||||
ThemeKind::BreezeDark,
|
||||
];
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
|
|
@ -57,6 +66,8 @@ impl ThemeKind {
|
|||
ThemeKind::Adwaita => "Adwaita (Light)",
|
||||
ThemeKind::AdwaitaDark => "Adwaita (Dark)",
|
||||
ThemeKind::SolarizedDark => "Solarized Dark",
|
||||
ThemeKind::BreezeLight => "Breeze Light",
|
||||
ThemeKind::BreezeDark => "Breeze Dark",
|
||||
}
|
||||
}
|
||||
/// The canonical name: `ui.set {theme}` accepts it and the Window › Theme commands are
|
||||
|
|
@ -71,6 +82,8 @@ impl ThemeKind {
|
|||
ThemeKind::Adwaita => "adwaita",
|
||||
ThemeKind::AdwaitaDark => "adwaitaDark",
|
||||
ThemeKind::SolarizedDark => "solarizedDark",
|
||||
ThemeKind::BreezeLight => "breezeLight",
|
||||
ThemeKind::BreezeDark => "breezeDark",
|
||||
}
|
||||
}
|
||||
pub fn next(self) -> Self {
|
||||
|
|
@ -87,6 +100,8 @@ impl ThemeKind {
|
|||
"adwaita" | "adwaitalight" | "gnome" | "gnomelight" => Some(ThemeKind::Adwaita),
|
||||
"adwaitadark" | "gnomedark" => Some(ThemeKind::AdwaitaDark),
|
||||
"solarizeddark" | "solarized" => Some(ThemeKind::SolarizedDark),
|
||||
"breezelight" | "breeze" | "kde" | "kdelight" | "plasma" | "plasmalight" => Some(ThemeKind::BreezeLight),
|
||||
"breezedark" | "kdedark" | "plasmadark" => Some(ThemeKind::BreezeDark),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
@ -422,6 +437,102 @@ impl Tokens {
|
|||
caption_close_text: Color32::WHITE,
|
||||
scrim: Color32::from_black_alpha(110),
|
||||
},
|
||||
// The light counterpart, adapted the same way: our own values, chosen a few levels
|
||||
// from Breeze Light's surfaces so the app blends into a Plasma desktop without being a
|
||||
// copy. Breeze's own inactive text and link blue are only about 2.8:1 on such
|
||||
// surfaces, so the secondary and faint text, the accent and danger text and the status
|
||||
// hues are darker to hold 4.5:1 (checked by the tests), and the primary button is a
|
||||
// deeper blue that takes white text.
|
||||
ThemeKind::BreezeLight => Tokens {
|
||||
kind,
|
||||
chrome: Color32::from_rgb(219, 222, 226),
|
||||
canvas: Color32::from_rgb(219, 222, 226),
|
||||
canvas_dot: Color32::from_rgb(219, 222, 226),
|
||||
dock: Color32::from_rgb(224, 227, 231),
|
||||
card: Color32::from_rgb(236, 238, 241),
|
||||
card_border: Color32::from_rgb(203, 208, 213),
|
||||
field: Color32::from_rgb(249, 250, 252),
|
||||
field_border: Color32::from_rgb(184, 191, 198),
|
||||
hover: Color32::from_rgb(226, 230, 235),
|
||||
pressed: Color32::from_rgb(208, 214, 221),
|
||||
text: Color32::from_rgb(33, 37, 42),
|
||||
text_dim: Color32::from_rgb(54, 63, 72),
|
||||
text_faint: Color32::from_rgb(81, 90, 100),
|
||||
icon: Color32::from_rgb(52, 58, 64),
|
||||
accent: Color32::from_rgb(58, 172, 232),
|
||||
accent_soft: Color32::from_rgba_unmultiplied(58, 172, 232, 48),
|
||||
accent_border: Color32::from_rgba_unmultiplied(58, 172, 232, 170),
|
||||
accent_text: Color32::from_rgb(30, 94, 136),
|
||||
separator: Color32::from_rgb(214, 218, 222),
|
||||
shadow: Color32::from_black_alpha(45),
|
||||
primary_bg: Color32::from_rgb(35, 122, 180),
|
||||
primary_text: Color32::WHITE,
|
||||
danger: Color32::from_rgb(170, 50, 64),
|
||||
warning: Color32::from_rgb(153, 72, 0),
|
||||
radius_sm: 5.0,
|
||||
radius: 5.0,
|
||||
radius_lg: 6.0,
|
||||
bevel: false,
|
||||
pro: false,
|
||||
round_chips: true,
|
||||
tab_strip: Color32::TRANSPARENT,
|
||||
row_selected: Color32::TRANSPARENT,
|
||||
histogram_bg: Color32::from_rgb(33, 37, 42),
|
||||
histogram_level: 240,
|
||||
caption_close: Color32::from_rgb(213, 51, 60),
|
||||
caption_close_text: Color32::WHITE,
|
||||
scrim: Color32::from_black_alpha(110),
|
||||
},
|
||||
// An adaptation of KDE Plasma's Breeze Dark look on the Studio layout, not a copy: our
|
||||
// own values, chosen a few levels from Breeze Dark's surfaces (the darkest canvas, then
|
||||
// the dock, the cards, and the chrome and controls) so the app blends into a Plasma
|
||||
// desktop. Near-white text, the blue accent and the orange warning hue sit close to
|
||||
// Breeze's, with its 5 px frame radius.
|
||||
// Readability wins over fidelity: every text colour holds 4.5:1 on every surface and on
|
||||
// the hover, pressed and selected states (checked by the tests), so the secondary and
|
||||
// faint text, the accent and danger text and the status hues are lighter than Breeze's,
|
||||
// hover and pressed are small steps, and the primary button is a deeper blue that takes
|
||||
// white text. Outlines and those steps are our own tints.
|
||||
ThemeKind::BreezeDark => Tokens {
|
||||
kind,
|
||||
chrome: Color32::from_rgb(38, 42, 48),
|
||||
canvas: Color32::from_rgb(17, 20, 24),
|
||||
canvas_dot: Color32::from_rgb(17, 20, 24),
|
||||
dock: Color32::from_rgb(26, 29, 34),
|
||||
card: Color32::from_rgb(29, 33, 38),
|
||||
card_border: Color32::from_rgb(52, 56, 61),
|
||||
field: Color32::from_rgb(38, 42, 48),
|
||||
field_border: Color32::from_rgb(68, 73, 79),
|
||||
hover: Color32::from_rgb(50, 54, 59),
|
||||
pressed: Color32::from_rgb(58, 63, 69),
|
||||
text: Color32::from_rgb(250, 251, 253),
|
||||
text_dim: Color32::from_rgb(200, 206, 213),
|
||||
text_faint: Color32::from_rgb(164, 170, 177),
|
||||
icon: Color32::from_rgb(236, 238, 240),
|
||||
accent: Color32::from_rgb(58, 172, 232),
|
||||
accent_soft: Color32::from_rgba_unmultiplied(58, 172, 232, 40),
|
||||
accent_border: Color32::from_rgba_unmultiplied(58, 172, 232, 160),
|
||||
accent_text: Color32::from_rgb(80, 184, 238),
|
||||
separator: Color32::from_rgb(46, 50, 54),
|
||||
shadow: Color32::from_black_alpha(150),
|
||||
primary_bg: Color32::from_rgb(30, 118, 176),
|
||||
primary_text: Color32::WHITE,
|
||||
danger: Color32::from_rgb(227, 111, 123),
|
||||
warning: Color32::from_rgb(244, 120, 6),
|
||||
radius_sm: 5.0,
|
||||
radius: 5.0,
|
||||
radius_lg: 6.0,
|
||||
bevel: false,
|
||||
pro: false,
|
||||
round_chips: true,
|
||||
tab_strip: Color32::TRANSPARENT,
|
||||
row_selected: Color32::TRANSPARENT,
|
||||
histogram_bg: Color32::from_rgb(17, 20, 24),
|
||||
histogram_level: 225,
|
||||
caption_close: Color32::from_rgb(213, 51, 60),
|
||||
caption_close_text: Color32::WHITE,
|
||||
scrim: Color32::from_black_alpha(110),
|
||||
},
|
||||
// Ethan Schoonover's Solarized Dark palette: base03 canvas, base02 surfaces, base0
|
||||
// body text and the Solarized blue accent. https://ethanschoonover.com/solarized/
|
||||
// (MIT; see LICENSE-solarized.txt)
|
||||
|
|
@ -523,7 +634,10 @@ impl Tokens {
|
|||
}
|
||||
|
||||
pub fn dark(&self) -> bool {
|
||||
matches!(self.kind, ThemeKind::Studio | ThemeKind::Pro | ThemeKind::ProMedium | ThemeKind::SolarizedDark | ThemeKind::AdwaitaDark)
|
||||
matches!(
|
||||
self.kind,
|
||||
ThemeKind::Studio | ThemeKind::Pro | ThemeKind::ProMedium | ThemeKind::SolarizedDark | ThemeKind::AdwaitaDark | ThemeKind::BreezeDark
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -934,6 +1048,68 @@ mod tests {
|
|||
}
|
||||
assert_eq!(ThemeKind::from_name("gnome dark"), Some(ThemeKind::AdwaitaDark));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn breeze_themes_parse_and_meet_text_contrast() {
|
||||
for (kind, aliases) in [
|
||||
(ThemeKind::BreezeLight, ["breeze", "Breeze Light", "breeze_light", "kde", "plasma"]),
|
||||
(ThemeKind::BreezeDark, ["breeze dark", "Breeze Dark", "breeze_dark", "kde dark", "plasma-dark"]),
|
||||
] {
|
||||
assert_eq!(ThemeKind::from_name(kind.id()), Some(kind));
|
||||
assert_eq!(ThemeKind::from_name(kind.label()), Some(kind));
|
||||
for alias in aliases {
|
||||
assert_eq!(ThemeKind::from_name(alias), Some(kind), "{alias}");
|
||||
}
|
||||
assert!(ThemeKind::ALL.contains(&kind));
|
||||
let t = Tokens::for_kind(kind);
|
||||
assert_eq!(t.kind, kind);
|
||||
assert_eq!(t.dark(), kind == ThemeKind::BreezeDark);
|
||||
assert!(!t.pro && !t.bevel, "{kind:?} uses the Studio layout");
|
||||
// The design intent, not exact values (the palette is an adaptation and may be tuned):
|
||||
// the canvas is the most recessed surface, then the dock and the cards, and controls
|
||||
// sit above the cards they are on; the accent is a blue.
|
||||
let lum = |c: Color32| u32::from(c.r()) + u32::from(c.g()) + u32::from(c.b());
|
||||
assert!(lum(t.canvas) < lum(t.dock) && lum(t.dock) < lum(t.card) && lum(t.card) < lum(t.field), "{kind:?}: surface order");
|
||||
assert!(t.accent.b() > t.accent.g() && t.accent.g() > t.accent.r(), "blue accent: {:?}", t.accent);
|
||||
// Plasma's surfaces are cool blue-greys: dark ones stay below 64 per channel and light
|
||||
// ones above 205, all with a little more blue than red (our own values, close to it).
|
||||
for c in [t.chrome, t.canvas, t.dock, t.card, t.field] {
|
||||
assert!(if t.dark() { c.r().max(c.g()).max(c.b()) < 64 } else { c.r().min(c.g()).min(c.b()) > 205 }, "{kind:?}: {c:?}");
|
||||
assert!(c.b() > c.r(), "{kind:?}: blue-grey {c:?}");
|
||||
}
|
||||
// All text must read: AA (4.5:1) for every text colour on every surface it can sit on,
|
||||
// including a hovered or pressed control and a selected (accent-tinted) row.
|
||||
let tint = |under: Color32| {
|
||||
let k = f32::from(t.accent_soft.a()) / 255.0;
|
||||
let ch = |over: u8, under: u8| (f32::from(over) * k + f32::from(under) * (1.0 - k)).round() as u8;
|
||||
Color32::from_rgb(ch(t.accent.r(), under.r()), ch(t.accent.g(), under.g()), ch(t.accent.b(), under.b()))
|
||||
};
|
||||
let resting = [t.chrome, t.canvas, t.dock, t.card, t.field];
|
||||
let active = [t.hover, t.pressed, tint(t.chrome), tint(t.dock), tint(t.card), tint(t.field)];
|
||||
for surface in resting.into_iter().chain(active) {
|
||||
for (name, colour) in [("text", t.text), ("dim text", t.text_dim), ("faint text", t.text_faint), ("accent text", t.accent_text)] {
|
||||
assert!(contrast(colour, surface) >= 4.5, "{kind:?}: {name} on {surface:?}");
|
||||
}
|
||||
}
|
||||
// The text must also keep a hierarchy, not collapse into one grey.
|
||||
assert!(contrast(t.text, t.card) > contrast(t.text_dim, t.card) && contrast(t.text_dim, t.card) > contrast(t.text_faint, t.card), "{kind:?}");
|
||||
// Danger and warning colours carry text on the resting surfaces; on hovered, pressed and
|
||||
// selected ones they are status marks, held to the 3:1 of graphical objects.
|
||||
for surface in resting {
|
||||
assert!(contrast(t.danger, surface) >= 4.5, "{kind:?}: danger on {surface:?}");
|
||||
assert!(contrast(t.warning, surface) >= 4.5, "{kind:?}: warning on {surface:?}");
|
||||
}
|
||||
for surface in active {
|
||||
assert!(contrast(t.danger, surface) >= 3.0, "{kind:?}: danger on {surface:?}");
|
||||
assert!(contrast(t.warning, surface) >= 3.0, "{kind:?}: warning on {surface:?}");
|
||||
}
|
||||
// A primary button darkens while hovered and pressed (button_impl), so check all three.
|
||||
for k in [1.0, 0.93, 0.85] {
|
||||
assert!(contrast(t.primary_text, t.primary_bg.gamma_multiply(k)) >= 4.5, "{kind:?}: primary button label at {k}");
|
||||
}
|
||||
assert!(contrast(t.caption_close_text, t.caption_close) >= 4.5, "{kind:?}: close button glyph");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Development feature: live design-token overrides.
|
||||
|
|
|
|||
|
|
@ -12,12 +12,16 @@
|
|||
| Adwaita | GNOME's libadwaita light palette on the Studio layout: white header bar and cards on a grey sidebar, GNOME blue accent (#3584e4), 6 / 9 / 12 px radii, grey (not red) close button |
|
||||
| Adwaita Dark | The libadwaita dark palette on the same layout |
|
||||
| Solarized Dark | Ethan Schoonover's Solarized palette: base03 canvas, base02 panels, base0 text, Solarized blue accent |
|
||||
| Breeze Light | An adaptation of KDE Plasma's Breeze Light on the Studio layout: cool light blue-grey surfaces a few levels from Breeze's so the app blends into a Plasma desktop, Breeze-style blue accent, 5 px radii, red close button on hover; text and status colours are darker than Breeze's for contrast |
|
||||
| Breeze Dark | The same for Breeze Dark: cool dark blue-grey surfaces (darkest canvas, then dock, cards, chrome and controls) near Breeze's, blue accent, 5 px radii; secondary text and status colours are lighter than Breeze's for contrast |
|
||||
|
||||
Both Breeze themes are our own values close to Plasma's (not a copy of its colour schemes) and put text readability first: every text colour holds 4.5:1 on every surface and on hovered, pressed and selected states (`theme.rs` tests), so their secondary text, accent and status colours are darker (Light) or lighter (Dark) than Breeze's own, and the primary button is a deeper blue with white text.
|
||||
|
||||
Edit → Preferences → Interface shows an Appearance Mode selector (Sync with system, Dark, Light) above separate light and dark theme cards with PhotoCraft editor previews and radio choices. Auto is opt-in and follows the operating system while the app is running. On Linux it reads the desktop portal once and then listens for its `SettingChanged` signal (no polling, and the UI repaints only when the value changes); `gsettings` runs at most once at start-up, by absolute path, when the portal gives no answer. A missing system appearance falls back to Dark. New installs keep Photoshop's default: Dark mode with Pro Medium (Studio Light is the saved light theme). Existing saved single-theme preferences migrate to a fixed Dark or Light mode with their chosen theme. The header appearance button cycles Auto → Light → Dark; its icon shows a monitor, moon or sun for the selected mode. Window → Theme and `ui.set {"theme":"classic"}` select a theme and fix the mode to its light or dark family.
|
||||
|
||||
Window → Theme and macOS PhotoCraft → Appearance also offer Sync with system. Its checkmark follows the saved Auto mode rather than the displayed palette; choosing a manual theme selects its fixed appearance mode.
|
||||
|
||||
The toolbar's foreground and background colour chips follow the `Tokens::round_chips` flag. Pro and Classic leave it off and draw Photoshop's overlapping squares with Default Colors and Switch Colors above them. Studio, Studio Light, Solarized Dark, Adwaita and Adwaita Dark (the Studio-layout themes) turn it on and draw large round chips with a curved Switch Colors arrow centred right under them, its heads touching the chips: stacked vertically in one tool column, with Default Colors at their top-right, and side by side in two tool columns, with Default Colors at the toolbar's left edge. Another non-Pro theme can opt in by setting the flag.
|
||||
The toolbar's foreground and background colour chips follow the `Tokens::round_chips` flag. Pro and Classic leave it off and draw Photoshop's overlapping squares with Default Colors and Switch Colors above them. Studio, Studio Light, Solarized Dark, Adwaita, Adwaita Dark, Breeze Light and Breeze Dark (the Studio-layout themes) turn it on and draw large round chips with a curved Switch Colors arrow centred right under them, its heads touching the chips: stacked vertically in one tool column, with Default Colors at their top-right, and side by side in two tool columns, with Default Colors at the toolbar's left edge. Another non-Pro theme can opt in by setting the flag.
|
||||
|
||||
## Rules
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue