Themes: Soft Dark and Soft Light (#4368) (#4378)

* Themes: Soft Dark and Soft Light (#4368)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: regenerate scorecard after merging main

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-10-12 23:06:37 -04:00
1 parent f5f0f73f25
commit e8af9d592f
28 files changed
+260 -17

No files matched your search

+1
View File
@@ -54,6 +54,7 @@ next to them.
| `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 |
| `crates/ui-egui/src/theme.rs` (Soft Dark and Soft Light themes) | Original colour values and radii for two low-contrast themes on PhotoCraft's Studio layout; no third-party palette, file or code is used | PhotoCraft contributors | Original | Original work |
### Optional build input: craft-fonts (not files in this repo)
+1 -1
View File
@@ -309,7 +309,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, breezeLight, breezeDark), brushSection, brushTab,
/// studioLight, classic, solarizedDark, adwaita, adwaitaDark, breezeLight, breezeDark, softLight, softDark), 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),
+17 -5
View File
@@ -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", ProLightGray = "proLightGray", ProLight = "proLight", Studio = "studio", StudioLight = "studioLight", Classic = "classic", SolarizedDark = "solarizedDark", Adwaita = "adwaita", AdwaitaDark = "adwaitaDark", BreezeLight = "breezeLight", BreezeDark = "breezeDark" } default ProMedium);
choice!(Theme { Pro = "pro", ProMedium = "proMedium", ProLightGray = "proLightGray", ProLight = "proLight", Studio = "studio", StudioLight = "studioLight", Classic = "classic", SolarizedDark = "solarizedDark", Adwaita = "adwaita", AdwaitaDark = "adwaitaDark", BreezeLight = "breezeLight", BreezeDark = "breezeDark", SoftDark = "softDark", SoftLight = "softLight" } default ProMedium);
choice!(AppearanceMode { Auto = "auto", Dark = "dark", Light = "light" } default Dark);
choice!(DarkTheme { Pro = "pro", ProMedium = "proMedium", Studio = "studio", SolarizedDark = "solarizedDark", AdwaitaDark = "adwaitaDark", BreezeDark = "breezeDark" } default ProMedium);
choice!(LightTheme { StudioLight = "studioLight", ProLightGray = "proLightGray", ProLight = "proLight", Classic = "classic", Adwaita = "adwaita", BreezeLight = "breezeLight" } default StudioLight);
choice!(DarkTheme { Pro = "pro", ProMedium = "proMedium", Studio = "studio", SolarizedDark = "solarizedDark", AdwaitaDark = "adwaitaDark", BreezeDark = "breezeDark", SoftDark = "softDark" } default ProMedium);
choice!(LightTheme { StudioLight = "studioLight", ProLightGray = "proLightGray", ProLight = "proLight", Classic = "classic", Adwaita = "adwaita", BreezeLight = "breezeLight", SoftLight = "softLight" } 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);
@@ -1542,8 +1542,12 @@ 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 | Theme::BreezeDark => ("dark", "darkTheme"),
Theme::StudioLight | Theme::ProLightGray | Theme::ProLight | Theme::Classic | Theme::Adwaita | Theme::BreezeLight => ("light", "lightTheme"),
Theme::Pro | Theme::ProMedium | Theme::Studio | Theme::SolarizedDark | Theme::AdwaitaDark | Theme::BreezeDark | Theme::SoftDark => {
("dark", "darkTheme")
}
Theme::StudioLight | Theme::ProLightGray | Theme::ProLight | Theme::Classic | Theme::Adwaita | Theme::BreezeLight | Theme::SoftLight => {
("light", "lightTheme")
}
};
interface.insert("appearanceMode".into(), json!(mode));
interface.insert(slot.into(), json!(theme.name()));
@@ -1666,6 +1670,14 @@ impl Session {
next.interface.light_theme = LightTheme::BreezeLight;
next.interface.appearance_mode = AppearanceMode::Light;
}
Theme::SoftDark => {
next.interface.dark_theme = DarkTheme::SoftDark;
next.interface.appearance_mode = AppearanceMode::Dark;
}
Theme::SoftLight => {
next.interface.light_theme = LightTheme::SoftLight;
next.interface.appearance_mode = AppearanceMode::Light;
}
}
}
self.prefs.edit(|p| *p = next);
+4
View File
@@ -735,6 +735,10 @@
Breeze Dark Theme Tmavý motiv Breeze
Breeze light Breeze světlý
Breeze Light Theme Světlý motiv Breeze
Soft dark Jemný tmavý
Soft light Jemný světlý
Soft Dark Theme Jemné tmavé téma
Soft Light Theme Jemné světlé téma
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.
+4
View File
@@ -721,6 +721,10 @@
Breeze Dark Theme Breeze-Design (Dunkel)
Breeze light Breeze hell
Breeze Light Theme Breeze-Design (Hell)
Soft dark Weich dunkel
Soft light Weich hell
Soft Dark Theme Weiches Design (Dunkel)
Soft Light Theme Weiches 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.
+4
View File
@@ -2132,6 +2132,10 @@
Breeze Dark Theme Σκούρο θέμα Breeze
Breeze light Breeze φωτεινό
Breeze Light Theme Φωτεινό θέμα Breeze
Soft dark Απαλό σκούρο
Soft light Απαλό ανοιχτό
Soft Dark Theme Απαλό σκούρο θέμα
Soft Light Theme Απαλό ανοιχτό θέμα
Clear Canvas Guides Απαλοιφή οδηγών καμβά
Clear Override Απαλοιφή παράκαμψης
Clear Samples Απαλοιφή δειγμάτων
Can't render this file because it contains an unexpected character in line 1005 and column 18.
+4
View File
@@ -728,6 +728,10 @@
Breeze Dark Theme Tema Breeze oscuro
Breeze light Breeze claro
Breeze Light Theme Tema Breeze claro
Soft dark Suave oscuro
Soft light Suave claro
Soft Dark Theme Tema suave oscuro
Soft Light Theme Tema suave 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.
+4
View File
@@ -718,6 +718,10 @@
Breeze Dark Theme Thème Breeze sombre
Breeze light Breeze clair
Breeze Light Theme Thème Breeze clair
Soft dark Doux sombre
Soft light Doux clair
Soft Dark Theme Thème doux sombre
Soft Light Theme Thème doux 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.
+4
View File
@@ -443,6 +443,10 @@
Box Blur… Dobozelmosás…
Breeze Dark Theme Breeze sötét téma
Breeze Light Theme Breeze világos téma
Soft dark Lágy sötét
Soft light Lágy világos
Soft Dark Theme Lágy sötét téma
Soft Light Theme Lágy világos téma
Brush Preview Ecset előnézete
Brush Settings Ecsetbeállítások
Brushes Ecsetek
Can't render this file because it contains an unexpected character in line 6 and column 47.
+4
View File
@@ -711,6 +711,10 @@
Breeze Dark Theme Tema Breeze Gelap
Breeze light Breeze terang
Breeze Light Theme Tema Breeze Terang
Soft dark Lembut gelap
Soft light Lembut terang
Soft Dark Theme Tema lembut gelap
Soft Light Theme Tema lembut 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.
+4
View File
@@ -726,6 +726,10 @@
Breeze Dark Theme Tema Breeze scuro
Breeze light Breeze chiaro
Breeze Light Theme Tema Breeze chiaro
Soft dark Morbido scuro
Soft light Morbido chiaro
Soft Dark Theme Tema morbido scuro
Soft Light Theme Tema morbido chiaro
Next Theme Tema successivo
Actions Azioni
Brush Settings Impostazioni pennello
1 # Italian (it) catalog. Columns, separated by a TAB: context <TAB> source <TAB> translation
726
727
728
729
730
731
732
733
734
735
+4
View File
@@ -728,6 +728,10 @@
Breeze Dark Theme Breeze ダークテーマ
Breeze light Breeze ライト
Breeze Light Theme Breeze ライトテーマ
Soft dark ソフトダーク
Soft light ソフトライト
Soft Dark Theme ソフトダークテーマ
Soft Light Theme ソフトライトテーマ
Next Theme 次のテーマ
Actions アクション
Brush Settings ブラシ設定
Can't render this file because it contains an unexpected character in line 6 and column 47.
+4
View File
@@ -702,6 +702,10 @@
Breeze Dark Theme Breeze 어두운 테마
Breeze light Breeze 밝게
Breeze Light Theme Breeze 밝은 테마
Soft dark 소프트 다크
Soft light 소프트 라이트
Soft Dark Theme 소프트 다크 테마
Soft Light Theme 소프트 라이트 테마
Next Theme 다음 테마
Actions 액션
Brush Settings 브러시 설정
1 # Korean (ko) catalog. UTF-8: context<TAB>English source<TAB>translation.
702
703
704
705
706
707
708
709
710
711
+4
View File
@@ -707,6 +707,10 @@
Breeze Dark Theme Breeze-thema donker
Breeze light Breeze licht
Breeze Light Theme Breeze-thema licht
Soft dark Zacht donker
Soft light Zacht licht
Soft Dark Theme Zacht donker thema
Soft Light Theme Zacht licht thema
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.
+4
View File
@@ -727,6 +727,10 @@
Breeze Dark Theme Ciemny motyw Breeze
Breeze light Breeze jasny
Breeze Light Theme Jasny motyw Breeze
Soft dark Łagodny ciemny
Soft light Łagodny jasny
Soft Dark Theme Łagodny ciemny motyw
Soft Light Theme Łagodny jasny motyw
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.
+4
View File
@@ -713,6 +713,10 @@
Breeze Dark Theme Tema Breeze Escuro
Breeze light Breeze claro
Breeze Light Theme Tema Breeze Claro
Soft dark Suave escuro
Soft light Suave claro
Soft Dark Theme Tema suave escuro
Soft Light Theme Tema suave 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.
+4
View File
@@ -1433,6 +1433,10 @@
Breeze Dark Theme Тёмная тема Breeze
Breeze light Breeze светлая
Breeze Light Theme Светлая тема Breeze
Soft dark Мягкая тёмная
Soft light Мягкая светлая
Soft Dark Theme Мягкая тёмная тема
Soft Light Theme Мягкая светлая тема
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.
+4
View File
@@ -702,6 +702,10 @@
Breeze Dark Theme Esinti Koyu Tema
Breeze light Esinti ışığı
Breeze Light Theme Esinti Işık Teması
Soft dark Yumuşak koyu
Soft light Yumuşak açık
Soft Dark Theme Yumuşak koyu tema
Soft Light Theme Yumuşak açık tema
Adwaita Light Theme Adwaita Işık Teması
Adwaita Dark Theme Adwaita Karanlık Tema
Adwaita Adwaita
Can't render this file because it contains an unexpected character in line 8 and column 34.
+4
View File
@@ -1412,6 +1412,10 @@
Breeze Dark Theme Темна тема Breeze
Breeze light Breeze світла
Breeze Light Theme Світла тема Breeze
Soft dark М'яка темна
Soft light М'яка світла
Soft Dark Theme М'яка темна тема
Soft Light Theme М'яка світла тема
Adwaita Light Theme Світла тема Adwaita
Adwaita Dark Theme Темна тема Adwaita
Adwaita Adwaita
Can't render this file because it contains an unexpected character in line 1362 and column 18.
+4
View File
@@ -2353,6 +2353,10 @@ shape Appearance Diện mạo
Bottom: Dưới:
Breeze Dark Theme Chủ đề Breeze tối
Breeze Light Theme Chủ đề Breeze sáng
Soft dark Dịu tối
Soft light Dịu sáng
Soft Dark Theme Giao diện dịu tối
Soft Light Theme Giao diện dịu sáng
Caps Đầu nét
Center Columns Căn giữa các cột
Change Panel Group Đổi nhóm bảng
Can't render this file because it contains an unexpected character in line 6 and column 47.
+4
View File
@@ -730,6 +730,10 @@
Breeze Dark Theme Breeze 深色主题
Breeze light Breeze 浅色
Breeze Light Theme Breeze 浅色主题
Soft dark 柔和深色
Soft light 柔和浅色
Soft Dark Theme 柔和深色主题
Soft Light Theme 柔和浅色主题
Next Theme 下一代主题
Actions 动作
Brush Settings 画笔设置
1 # Simplified Chinese (zh-hans) catalog. UTF-8, TAB-separated columns:
730
731
732
733
734
735
736
737
738
739
+4
View File
@@ -735,6 +735,10 @@
Breeze Dark Theme Breeze 深色主題
Breeze light Breeze 淺色
Breeze Light Theme Breeze 淺色主題
Soft dark 柔和深色
Soft light 柔和淺色
Soft Dark Theme 柔和深色主題
Soft Light Theme 柔和淺色主題
Next Theme 下一個主題
Actions 動作
Brush Settings 筆刷設定
Can't render this file because it contains an unexpected character in line 6 and column 47.
+5 -1
View File
@@ -89,6 +89,8 @@ pub const UI_COMMANDS: &[(&str, &str, &[&str], Option<&str>)] = &[
("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.softLight", "Soft Light Theme", &["Window", "Theme"], None),
("window.theme.softDark", "Soft 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),
@@ -374,7 +376,9 @@ pub(crate) fn invoke_unguarded(app: &mut PhotocraftApp, ctx: &egui::Context, id:
| "window.theme.adwaita"
| "window.theme.adwaitaDark"
| "window.theme.breezeLight"
| "window.theme.breezeDark" => {
| "window.theme.breezeDark"
| "window.theme.softLight"
| "window.theme.softDark" => {
let k = crate::theme::ThemeKind::from_name(&id["window.theme.".len()..]).unwrap_or_default();
app.set_theme(ctx, k);
Ok(Value::Null)
+12 -3
View File
@@ -97,6 +97,7 @@ fn selected_theme(interface: &prefs::Interface, system: Option<egui::Theme>) ->
LightTheme::Classic => ThemeKind::Classic,
LightTheme::Adwaita => ThemeKind::Adwaita,
LightTheme::BreezeLight => ThemeKind::BreezeLight,
LightTheme::SoftLight => ThemeKind::SoftLight,
}
} else {
match interface.dark_theme {
@@ -106,6 +107,7 @@ fn selected_theme(interface: &prefs::Interface, system: Option<egui::Theme>) ->
DarkTheme::SolarizedDark => ThemeKind::SolarizedDark,
DarkTheme::AdwaitaDark => ThemeKind::AdwaitaDark,
DarkTheme::BreezeDark => ThemeKind::BreezeDark,
DarkTheme::SoftDark => ThemeKind::SoftDark,
}
}
}
@@ -133,6 +135,7 @@ const LIGHT_THEME_OPTIONS: &[(&str, ThemeKind)] = &[
("classic", ThemeKind::Classic),
("adwaita", ThemeKind::Adwaita),
("breezeLight", ThemeKind::BreezeLight),
("softLight", ThemeKind::SoftLight),
];
const DARK_THEME_OPTIONS: &[(&str, ThemeKind)] = &[
("proMedium", ThemeKind::ProMedium),
@@ -141,6 +144,7 @@ const DARK_THEME_OPTIONS: &[(&str, ThemeKind)] = &[
("solarizedDark", ThemeKind::SolarizedDark),
("adwaitaDark", ThemeKind::AdwaitaDark),
("breezeDark", ThemeKind::BreezeDark),
("softDark", ThemeKind::SoftDark),
];
fn favorite_theme_groups(favorites: &[SavedTheme]) -> (Vec<ThemeKind>, Vec<ThemeKind>) {
@@ -2089,7 +2093,12 @@ mod tests {
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)] {
for (id, kind, mode) in [
("breezeLight", ThemeKind::BreezeLight, AppearanceMode::Light),
("breezeDark", ThemeKind::BreezeDark, AppearanceMode::Dark),
("softLight", ThemeKind::SoftLight, AppearanceMode::Light),
("softDark", ThemeKind::SoftDark, AppearanceMode::Dark),
] {
app.run("prefs.set", json!({"path": "interface.theme", "value": id})).unwrap();
tick(&mut app, &ctx);
let interface = app.session.prefs().interface.clone();
@@ -2098,8 +2107,8 @@ mod tests {
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);
assert_eq!(app.session.prefs().interface.light_theme, LightTheme::SoftLight);
assert_eq!(app.session.prefs().interface.dark_theme, DarkTheme::SoftDark);
}
#[test]
+141 -2
View File
@@ -48,10 +48,14 @@ pub enum ThemeKind {
BreezeLight,
/// An adaptation of KDE Plasma's Breeze Dark look.
BreezeDark,
/// A low-contrast dark theme: muted violet-grey surfaces, softened text and a periwinkle accent.
SoftDark,
/// A low-contrast light theme: pale violet-grey surfaces, softened dark text and a periwinkle accent.
SoftLight,
}
impl ThemeKind {
pub const ALL: [ThemeKind; 12] = [
pub const ALL: [ThemeKind; 14] = [
ThemeKind::Pro,
ThemeKind::ProMedium,
ThemeKind::ProLightGray,
@@ -64,6 +68,8 @@ impl ThemeKind {
ThemeKind::AdwaitaDark,
ThemeKind::BreezeLight,
ThemeKind::BreezeDark,
ThemeKind::SoftDark,
ThemeKind::SoftLight,
];
pub fn label(self) -> &'static str {
match self {
@@ -79,6 +85,8 @@ impl ThemeKind {
ThemeKind::SolarizedDark => "Solarized Dark",
ThemeKind::BreezeLight => "Breeze Light",
ThemeKind::BreezeDark => "Breeze Dark",
ThemeKind::SoftDark => "Soft Dark",
ThemeKind::SoftLight => "Soft Light",
}
}
/// The canonical name: `ui.set {theme}` accepts it and the Window › Theme commands are
@@ -97,6 +105,8 @@ impl ThemeKind {
ThemeKind::SolarizedDark => "solarizedDark",
ThemeKind::BreezeLight => "breezeLight",
ThemeKind::BreezeDark => "breezeDark",
ThemeKind::SoftDark => "softDark",
ThemeKind::SoftLight => "softLight",
}
}
/// Photoshop-grammar layout (tab strips, flat panels): the four Pro brightness levels.
@@ -121,6 +131,8 @@ impl ThemeKind {
"solarizeddark" | "solarized" => Some(ThemeKind::SolarizedDark),
"breezelight" | "breeze" | "kde" | "kdelight" | "plasma" | "plasmalight" => Some(ThemeKind::BreezeLight),
"breezedark" | "kdedark" | "plasmadark" => Some(ThemeKind::BreezeDark),
"softdark" => Some(ThemeKind::SoftDark),
"softlight" => Some(ThemeKind::SoftLight),
_ => None,
}
}
@@ -602,6 +614,91 @@ impl Tokens {
caption_close_text: Color32::WHITE,
scrim: Color32::from_black_alpha(110),
},
// Soft Dark: our own low-contrast palette on the Studio layout. Muted violet-grey
// surfaces (never near-black), softened off-white text and a periwinkle accent; the
// text colours still hold 4.5:1 on every surface (checked by the tests).
ThemeKind::SoftDark => Tokens {
kind,
chrome: Color32::from_rgb(58, 58, 68),
canvas: Color32::from_rgb(40, 40, 48),
canvas_dot: Color32::from_rgb(40, 40, 48),
dock: Color32::from_rgb(47, 47, 56),
card: Color32::from_rgb(54, 54, 64),
card_border: Color32::from_rgb(70, 70, 82),
field: Color32::from_rgb(64, 64, 75),
field_border: Color32::from_rgb(84, 84, 98),
hover: Color32::from_rgb(68, 68, 80),
pressed: Color32::from_rgb(74, 74, 88),
text: Color32::from_rgb(238, 238, 246),
text_dim: Color32::from_rgb(212, 212, 224),
text_faint: Color32::from_rgb(190, 190, 206),
icon: Color32::from_rgb(214, 214, 226),
accent: Color32::from_rgb(138, 154, 232),
accent_soft: Color32::from_rgba_unmultiplied(138, 154, 232, 48),
accent_border: Color32::from_rgba_unmultiplied(138, 154, 232, 150),
accent_text: Color32::from_rgb(190, 200, 252),
separator: Color32::from_rgb(36, 36, 44),
shadow: Color32::from_black_alpha(110),
primary_bg: Color32::from_rgb(86, 104, 196),
primary_text: Color32::WHITE,
danger: Color32::from_rgb(250, 160, 172),
warning: Color32::from_rgb(240, 200, 130),
radius_sm: 7.0,
radius: 10.0,
radius_lg: 14.0,
bevel: false,
pro: false,
round_chips: true,
tab_strip: Color32::TRANSPARENT,
row_selected: Color32::TRANSPARENT,
histogram_bg: Color32::from_rgb(40, 40, 48),
histogram_level: 220,
caption_close: Color32::from_rgb(200, 72, 86),
caption_close_text: Color32::WHITE,
scrim: Color32::from_black_alpha(100),
},
// Soft Light: pale violet-grey surfaces, softened dark-violet text (not black) and a
// periwinkle accent, with the same generous radii as Soft Dark.
ThemeKind::SoftLight => Tokens {
kind,
chrome: Color32::from_rgb(232, 230, 236),
canvas: Color32::from_rgb(218, 216, 224),
canvas_dot: Color32::from_rgb(218, 216, 224),
dock: Color32::from_rgb(226, 224, 231),
card: Color32::from_rgb(240, 238, 243),
card_border: Color32::from_rgb(208, 205, 215),
field: Color32::from_rgb(250, 249, 252),
field_border: Color32::from_rgb(190, 187, 201),
hover: Color32::from_rgb(228, 226, 234),
pressed: Color32::from_rgb(212, 209, 221),
text: Color32::from_rgb(44, 42, 56),
text_dim: Color32::from_rgb(62, 60, 76),
text_faint: Color32::from_rgb(80, 78, 96),
icon: Color32::from_rgb(64, 62, 80),
accent: Color32::from_rgb(110, 128, 216),
accent_soft: Color32::from_rgba_unmultiplied(110, 128, 216, 44),
accent_border: Color32::from_rgba_unmultiplied(110, 128, 216, 170),
accent_text: Color32::from_rgb(46, 58, 146),
separator: Color32::from_rgb(210, 207, 218),
shadow: Color32::from_black_alpha(40),
primary_bg: Color32::from_rgb(80, 98, 190),
primary_text: Color32::WHITE,
danger: Color32::from_rgb(166, 44, 62),
warning: Color32::from_rgb(140, 72, 0),
radius_sm: 7.0,
radius: 10.0,
radius_lg: 14.0,
bevel: false,
pro: false,
round_chips: true,
tab_strip: Color32::TRANSPARENT,
row_selected: Color32::TRANSPARENT,
histogram_bg: Color32::from_rgb(44, 42, 56),
histogram_level: 240,
caption_close: Color32::from_rgb(200, 60, 72),
caption_close_text: Color32::WHITE,
scrim: Color32::from_black_alpha(90),
},
// 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)
@@ -714,7 +811,13 @@ impl Tokens {
pub fn dark(&self) -> bool {
matches!(
self.kind,
ThemeKind::Studio | ThemeKind::Pro | ThemeKind::ProMedium | ThemeKind::SolarizedDark | ThemeKind::AdwaitaDark | ThemeKind::BreezeDark
ThemeKind::Studio
| ThemeKind::Pro
| ThemeKind::ProMedium
| ThemeKind::SolarizedDark
| ThemeKind::AdwaitaDark
| ThemeKind::BreezeDark
| ThemeKind::SoftDark
)
}
}
@@ -1168,6 +1271,42 @@ mod tests {
assert_eq!(ThemeKind::from_name("gnome dark"), Some(ThemeKind::AdwaitaDark));
}
#[test]
fn soft_themes_parse_and_hold_text_contrast() {
for (kind, alias) in [(ThemeKind::SoftDark, "soft dark"), (ThemeKind::SoftLight, "Soft_Light")] {
assert_eq!(ThemeKind::from_name(kind.id()), Some(kind));
assert_eq!(ThemeKind::from_name(kind.label()), Some(kind));
assert_eq!(ThemeKind::from_name(alias), Some(kind));
assert!(ThemeKind::ALL.contains(&kind));
let t = Tokens::for_kind(kind);
assert_eq!(t.kind, kind);
assert_eq!(t.dark(), kind == ThemeKind::SoftDark);
assert!(!t.pro && !t.bevel);
// Soft means no extremes: surfaces stay off black and white.
for c in [t.chrome, t.canvas, t.dock, t.card, t.field] {
let (hi, lo) = (c.r().max(c.g()).max(c.b()), c.r().min(c.g()).min(c.b()));
assert!(if t.dark() { (32..96).contains(&hi) } else { hi < 255 && lo > 200 }, "{kind:?}: {c:?}");
}
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 surfaces = [t.chrome, t.canvas, t.dock, t.card, t.field, t.hover, t.pressed, tint(t.dock), tint(t.card)];
for (i, surface) in surfaces.into_iter().enumerate() {
for (name, colour) in [("text", t.text), ("dim", t.text_dim), ("faint", t.text_faint), ("accent text", t.accent_text)] {
assert!(contrast(colour, surface) >= 4.5, "{kind:?}: {name} on {surface:?}");
}
// Status colours are marks on hovered or selected surfaces (3:1), text on resting ones.
let floor = if i < 5 { 4.5 } else { 3.0 };
assert!(contrast(t.danger, surface) >= floor && contrast(t.warning, surface) >= floor, "{kind:?}: status on {surface:?}");
}
for k in [1.0, 0.93, 0.85] {
assert!(contrast(t.primary_text, t.primary_bg.gamma_multiply(k)) >= 4.5, "{kind:?}: primary at {k}");
}
}
}
#[test]
fn breeze_themes_parse_and_meet_text_contrast() {
for (kind, aliases) in [
+2 -2
View File
@@ -9,7 +9,7 @@ Generated by `cargo xtask scorecard` from `perf/budgets.toml`, `perf/baseline.js
| [Performance](#performance) | 3 | 22 | 22 | 56 scenarios: 3 within budget, 22 over budget or failing, 9 tracked only, 17 not run, 5 not measurable yet (baseline 2026-10-05) |
| [File compatibility](#file-compatibility) | 6 | 13 | 8 | io corpus: oracle ≥ 146/170, round trip ≥ 169/170; psd-tools corpus: oracle ≥ 236/309, round trip ≥ 308/309; photoshop oracles: oracle ≥ 133/256, round trip ≥ 258/256 |
| [Tools](#tools) | 28 | 14 | 1 | |
| [Workspace and UI](#workspace-and-ui) | 13 | 5 | 5 | settings that do nothing: 26 of 156 (target 0) |
| [Workspace and UI](#workspace-and-ui) | 13 | 6 | 4 | settings that do nothing: 26 of 156 (target 0) |
| [Type](#type) | 4 | 2 | 3 | |
| [Automation](#automation) | 5 | 2 | 4 | 631 of 631 menu items live (100.0%) |
| [Reliability](#reliability) | 5 | 2 | 0 | never-crash lints on 28 of 28 crates; panic_hunt present |
@@ -192,7 +192,7 @@ Unread: `general.color_picker`, `general.animated_zoom`, `general.zoom_resizes_w
| UI-217-8 | Tool-specific canvas right-click menu; right-click spelling suggestions in text fields | partial | [#217](https://github.com/storytold/photocraft/issues/217) | paint_mouse.rs brush picker and canvas_tool_menu.rs selection actions; text_field_menu.rs gives every text field its right-click Undo/Cut/Copy/Paste/Delete/Select All menu (#2921); other tools missing; the Type tool's edit menu starts with spelling suggestions and Add to Dictionary over a misspelled word (#4370); Ignore / Ignore All, suggestions in the non-editing type menu and in generic text fields are not there yet |
| UI-217-9 | UI scale presets 100/115/125/150/200 % through zoom_factor, remembered | done | [#217](https://github.com/storytold/photocraft/issues/217) | prefs::UiScale offers Auto plus 75 to 300 percent (13 presets, more than the five this row lists); prefs_ui.rs display_scale resolves it (Auto follows the system scale, fractional and unscaled 4K included, #1072/#1278) and sync_display_scale applies it with ctx.set_zoom_factor. It is a preference, so it is remembered. The zoom_factor assert in input_tests.rs is about canvas zoom not scaling the interface, not about this setting |
| UI-217-10 | Appearance: animation toggle, canvas pattern toggle per theme, start-screen recents | partial | [#217](https://github.com/storytold/photocraft/issues/217) | interface.enableAnimations (prefs_ui.rs sync_animations, #3273) zeroes egui animation_time and scroll animation in both styles; the Home screen lists recent files (canvas.rs home_recent, #2691); no canvas pattern toggle yet (#2264) and no option to hide the Home screen recents |
| UI-217-11 | Soft dark and soft light themes with picker previews; outlined knobs in light themes | missing | [#217](https://github.com/storytold/photocraft/issues/217) | theme.rs ThemeKind has twelve themes (the four Pro brightness levels, Studio and Studio Light, Classic, Adwaita and Adwaita Dark, Solarized Dark, Breeze Light and Breeze Dark); no Soft dark or Soft light, and no outlined knobs in the light themes |
| UI-217-11 | Soft dark and soft light themes with picker previews; outlined knobs in light themes | partial | [#217](https://github.com/storytold/photocraft/issues/217) | theme.rs ThemeKind has fourteen themes: the twelve earlier ones plus Soft Dark and Soft Light (low-contrast Studio-layout palettes, text at 4.5:1, selectable in Preferences with the existing theme-card previews, ui.set {theme}, Window > Theme); outlined knobs in the light themes are still missing |
| UI-217-12 | Save custom New Document presets; a real Recent category | done | [#217](https://github.com/storytold/photocraft/issues/217) | document_preset_cmds.rs and new_doc_ui.rs: named presets save/select/delete through preferences; Recent lists Clipboard, the last 20 documents created in the dialog (document.recent.*, persisted with the presets, newest first, deduplicated) and the uncovered defaults (#3131); created_documents_fill_the_recent_category |
| UI-217-13 | Numeric fields accept arithmetic and units; scrubby labels; double-click resets a slider | done | [#217](https://github.com/storytold/photocraft/issues/217) | widgets::value_field: arithmetic plus units (1in, 5mm, 72pt, 50%) converted at the document resolution (#3099); widgets::scrub_label makes options-bar, slider-row and Character labels scrubby (Shift x10, Alt fine); slider rows with a known default reset on double-click |
| UI-217-14 | Layers panel: wheel steps blend modes, drop on New/Trash duplicates/deletes, checker behind art | done | [#217](https://github.com/storytold/photocraft/issues/217) | panels.rs: the wheel steps the blend-mode dropdown (widgets::dropdown_wheel_hovered, #1747); rows dropped on New Layer/Group/Delete duplicate/group/delete them (footer_drop, #736), effect rows dropped on Delete delete that effect or the whole style (fx_trash_drop, #3102); pixel thumbnails draw widgets::checker behind the art; Shift+Plus/Minus step the layer's or the painting tool's blend mode (blend_keys.rs, #3102) |
+3 -1
View File
@@ -14,6 +14,8 @@
| 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 |
| Soft Dark | Low-contrast dark on the Studio layout: muted violet-grey surfaces (never near-black), softened off-white text, periwinkle accent, generous 7/10/14 px radii; text still holds 4.5:1 on every surface (`theme.rs` tests) |
| Soft Light | The light counterpart: pale violet-grey surfaces, softened dark-violet text instead of black, periwinkle accent, the same radii |
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.
@@ -21,7 +23,7 @@ Edit → Preferences → Interface shows an Appearance Mode selector (Sync with
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, 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.
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, Breeze Dark, Soft Dark and Soft Light (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
+2 -2
View File
@@ -78,9 +78,9 @@ note = "interface.enableAnimations (prefs_ui.rs sync_animations, #3273) zeroes e
[[item]]
id = "UI-217-11"
target = "Soft dark and soft light themes with picker previews; outlined knobs in light themes"
status = "missing"
status = "partial"
issue = 217
note = "theme.rs ThemeKind has twelve themes (the four Pro brightness levels, Studio and Studio Light, Classic, Adwaita and Adwaita Dark, Solarized Dark, Breeze Light and Breeze Dark); no Soft dark or Soft light, and no outlined knobs in the light themes"
note = "theme.rs ThemeKind has fourteen themes: the twelve earlier ones plus Soft Dark and Soft Light (low-contrast Studio-layout palettes, text at 4.5:1, selectable in Preferences with the existing theme-card previews, ui.set {theme}, Window > Theme); outlined knobs in the light themes are still missing"
[[item]]
id = "UI-217-12"