mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-24 09:57:08 -06:00
Appearance modes: sync with system, separate dark and light themes (#1981)
* Appearance modes: sync with system, separate dark and light themes * Appearance button cycles Auto, Light, Dark * Appearance: default stays Dark, follow the portal signal instead of polling, split out the Preferences resize * Preferences: size both appearance cards for the five dark themes --------- Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
co-authored by
Brandon Thomas
parent
782bcb67fa
commit
8c3b6f8e5f
+2
-2
@@ -20,8 +20,8 @@ next to them.
|
||||
| `crates/ui-egui/src/i18n/uk.tsv` | Ukrainian UI translations | PhotoCraft contributors | Original translations of PhotoCraft's English labels | MIT OR Apache-2.0, [`LICENSE-translations.txt`](crates/ui-egui/src/i18n/LICENSE-translations.txt) |
|
||||
| `assets/fonts/Inter-Regular.ttf`, `Inter-Medium.ttf`, `Inter-SemiBold.ttf` | Inter 4.001 (UI font) | The Inter Project Authors (Rasmus Andersson) | <https://github.com/rsms/inter> | SIL OFL 1.1, [`assets/fonts/OFL-Inter.txt`](assets/fonts/OFL-Inter.txt) |
|
||||
| `assets/fonts/JetBrainsMono-Regular.ttf` | JetBrains Mono 2.305 (numeric font) | The JetBrains Mono Project Authors | <https://github.com/JetBrains/JetBrainsMono> | SIL OFL 1.1, [`assets/fonts/OFL-JetBrainsMono.txt`](assets/fonts/OFL-JetBrainsMono.txt) |
|
||||
| `assets/icons/*.svg` except `slice-knife.svg`, `clip-below.svg`, `clip-release.svg`, `layer-mask.svg`, `adjustment-layer.svg`, `eraser-background.svg`, `eraser-magic.svg`, `lasso-magnetic.svg`, `direct-select.svg`, `painting-symmetry.svg` and `symmetry-*.svg` (109 files) | Lucide icons | Lucide Icons and Contributors | <https://github.com/lucide-icons/lucide> (`icons/<name>.svg`; `align-*` are now named `text-align-*` upstream) | ISC, [`assets/icons/LICENSE-lucide.txt`](assets/icons/LICENSE-lucide.txt) |
|
||||
| `assets/icons/{check,chevron-down,chevron-right,chevron-up,chevrons-left,chevrons-right,circle,clock,compass,info,link,link-2,lock,minus,moon,move,navigation,plus,search,square,trash,triangle,type,x,zoom-in}.svg` | Lucide icons derived from Feather (subset of the row above) | Cole Bemis (Feather), Lucide Contributors | <https://github.com/feathericons/feather> via Lucide | MIT (Feather) and ISC (Lucide), [`assets/icons/LICENSE-lucide.txt`](assets/icons/LICENSE-lucide.txt) |
|
||||
| `assets/icons/*.svg` except `slice-knife.svg`, `clip-below.svg`, `clip-release.svg`, `layer-mask.svg`, `adjustment-layer.svg`, `eraser-background.svg`, `eraser-magic.svg`, `lasso-magnetic.svg`, `direct-select.svg`, `painting-symmetry.svg` and `symmetry-*.svg` (110 files) | Lucide icons | Lucide Icons and Contributors | <https://github.com/lucide-icons/lucide> (`icons/<name>.svg`; `align-*` are now named `text-align-*` upstream) | ISC, [`assets/icons/LICENSE-lucide.txt`](assets/icons/LICENSE-lucide.txt) |
|
||||
| `assets/icons/{check,chevron-down,chevron-right,chevron-up,chevrons-left,chevrons-right,circle,clock,compass,info,link,link-2,lock,minus,monitor,moon,move,navigation,plus,search,square,trash,triangle,type,x,zoom-in}.svg` | Lucide icons derived from Feather (subset of the row above) | Cole Bemis (Feather), Lucide Contributors | <https://github.com/feathericons/feather> via Lucide | MIT (Feather) and ISC (Lucide), [`assets/icons/LICENSE-lucide.txt`](assets/icons/LICENSE-lucide.txt) |
|
||||
| `assets/icons/slice-knife.svg` | Slice tool glyph | PhotoCraft contributors | Original work | MIT OR Apache-2.0 |
|
||||
| `assets/icons/clip-below.svg`, `assets/icons/clip-release.svg` | Clipping-mask cursors (⌥ over the line between two layers): a hooked arrow onto the layer below; the same with a small "no" badge to release | PhotoCraft contributors | Original work | MIT OR Apache-2.0 |
|
||||
| `assets/icons/layer-mask.svg`, `assets/icons/adjustment-layer.svg` | Layers panel footer glyphs: Add a mask (a frame around a solid disc) and New Fill or Adjustment Layer (a circle, one half solid), drawn on Lucide's 24 px grid and stroke | PhotoCraft contributors | Original work | MIT OR Apache-2.0 |
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
//! Linux desktop colour scheme, including Wayland compositors that do not report a winit theme.
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
const PORTAL_NAMESPACE: &str = "org.freedesktop.appearance";
|
||||
#[cfg(target_os = "linux")]
|
||||
const PORTAL_KEY: &str = "color-scheme";
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn portal_proxy() -> Option<zbus::blocking::Proxy<'static>> {
|
||||
let conn = zbus::blocking::connection::Builder::session().ok()?.method_timeout(std::time::Duration::from_secs(2)).build().ok()?;
|
||||
zbus::blocking::Proxy::new(&conn, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop", "org.freedesktop.portal.Settings").ok()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn portal_theme(proxy: &zbus::blocking::Proxy<'_>) -> Option<egui::Theme> {
|
||||
let value: zbus::zvariant::OwnedValue = proxy.call("Read", &(PORTAL_NAMESPACE, PORTAL_KEY)).ok()?;
|
||||
decode(portal_code(value)?)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn portal_code(value: zbus::zvariant::OwnedValue) -> Option<u32> {
|
||||
let mut value: zbus::zvariant::Value<'_> = value.into();
|
||||
// The Settings.Read result is a variant; some portals nest another variant inside it.
|
||||
for _ in 0..4 {
|
||||
match value {
|
||||
zbus::zvariant::Value::Value(inner) => value = *inner,
|
||||
other => return u32::try_from(other).ok(),
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn decode(code: u32) -> Option<egui::Theme> {
|
||||
match code {
|
||||
1 => Some(egui::Theme::Dark),
|
||||
2 => Some(egui::Theme::Light),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// One-shot fallback for desktops without the settings portal. It runs once at start-up, by
|
||||
/// absolute path, and is never repeated.
|
||||
#[cfg(target_os = "linux")]
|
||||
fn gtk_theme() -> Option<egui::Theme> {
|
||||
let exe = ["/usr/bin/gsettings", "/bin/gsettings", "/usr/local/bin/gsettings"].into_iter().find(|p| std::path::Path::new(p).is_file())?;
|
||||
let output = std::process::Command::new(exe).args(["get", "org.gnome.desktop.interface", "color-scheme"]).output().ok()?;
|
||||
if !output.status.success() {
|
||||
return None;
|
||||
}
|
||||
parse_gtk_scheme(std::str::from_utf8(&output.stdout).ok()?)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn parse_gtk_scheme(s: &str) -> Option<egui::Theme> {
|
||||
match s.trim().trim_matches('\'') {
|
||||
"prefer-dark" => Some(egui::Theme::Dark),
|
||||
"prefer-light" => Some(egui::Theme::Light),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn code_of(theme: Option<egui::Theme>) -> u8 {
|
||||
match theme {
|
||||
Some(egui::Theme::Dark) => 1,
|
||||
Some(egui::Theme::Light) => 2,
|
||||
None => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads the colour scheme once, then follows the portal's `SettingChanged` signal. There is no
|
||||
/// polling: the worker sleeps in the signal iterator and wakes the UI only when the value changes.
|
||||
pub fn service() -> Option<photocraft_ui_egui::SystemThemeFn> {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
use std::sync::{
|
||||
Arc, OnceLock,
|
||||
atomic::{AtomicU8, Ordering},
|
||||
};
|
||||
let value = Arc::new(AtomicU8::new(0));
|
||||
let wake: Arc<OnceLock<egui::Context>> = Arc::new(OnceLock::new());
|
||||
let (worker_value, worker_wake) = (Arc::clone(&value), Arc::clone(&wake));
|
||||
let (ready, wait) = std::sync::mpsc::channel();
|
||||
if std::thread::Builder::new()
|
||||
.name("appearance-portal".into())
|
||||
.spawn(move || {
|
||||
let proxy = portal_proxy();
|
||||
let initial = proxy.as_ref().and_then(portal_theme).or_else(gtk_theme);
|
||||
worker_value.store(code_of(initial), Ordering::Relaxed);
|
||||
let _ = ready.send(());
|
||||
let Some(proxy) = proxy else { return };
|
||||
let Ok(signals) = proxy.receive_signal("SettingChanged") else { return };
|
||||
for message in signals {
|
||||
let Ok((namespace, key, changed)) = message.body().deserialize::<(String, String, zbus::zvariant::OwnedValue)>() else {
|
||||
continue;
|
||||
};
|
||||
if namespace != PORTAL_NAMESPACE || key != PORTAL_KEY {
|
||||
continue;
|
||||
}
|
||||
let code = code_of(portal_code(changed).and_then(decode));
|
||||
if worker_value.swap(code, Ordering::Relaxed) != code
|
||||
&& let Some(ctx) = worker_wake.get()
|
||||
{
|
||||
ctx.request_repaint();
|
||||
}
|
||||
}
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let _ = wait.recv_timeout(std::time::Duration::from_millis(250));
|
||||
Some(Box::new(move |ctx| {
|
||||
let _ = wake.set(ctx.clone());
|
||||
decode(u32::from(value.load(Ordering::Relaxed)))
|
||||
}))
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn portal_scheme_values() {
|
||||
assert_eq!(super::decode(0), None);
|
||||
assert_eq!(super::decode(1), Some(egui::Theme::Dark));
|
||||
assert_eq!(super::decode(2), Some(egui::Theme::Light));
|
||||
assert_eq!(super::decode(9), None);
|
||||
let nested = zbus::zvariant::Value::Value(Box::new(zbus::zvariant::Value::Value(Box::new(zbus::zvariant::Value::U32(2)))));
|
||||
assert_eq!(super::portal_code(zbus::zvariant::OwnedValue::try_from(nested).unwrap()), Some(2));
|
||||
assert_eq!(super::parse_gtk_scheme("'prefer-light'\n"), Some(egui::Theme::Light));
|
||||
assert_eq!(super::parse_gtk_scheme("'prefer-dark'\n"), Some(egui::Theme::Dark));
|
||||
assert_eq!(super::parse_gtk_scheme("'default'\n"), None);
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
mod app_dirs;
|
||||
mod app_icon;
|
||||
mod appearance;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod apple_events;
|
||||
mod caps_lock;
|
||||
|
||||
@@ -359,6 +359,7 @@ pub fn native(automation: Option<photocraft_automation::AuthorizedWorkspace>) ->
|
||||
}),
|
||||
load_prefs: Some(Box::new(|| std::fs::read_to_string(prefs_file()?).ok())),
|
||||
save_prefs: Some(Box::new(|text: &str| write_atomic(&prefs_file().ok_or("no config directory")?, text.as_bytes()))),
|
||||
system_theme: crate::appearance::service(),
|
||||
append_text: Some(Box::new(|path: &str, text: &str| {
|
||||
use std::io::Write;
|
||||
let mut f = std::fs::OpenOptions::new().create(true).append(true).open(path).map_err(|e| e.to_string())?;
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect width="20" height="14" x="2" y="3" rx="2" />
|
||||
<line x1="8" x2="16" y1="21" y2="21" />
|
||||
<line x1="12" x2="12" y1="17" y2="21" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 346 B |
@@ -56,6 +56,9 @@ choice!(PointSize { PostScript = "postScript", Traditional = "traditional" } def
|
||||
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!(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!(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", P100 = "100", P125 = "125", P150 = "150", P175 = "175", P200 = "200", P250 = "250", P300 = "300" } default Auto);
|
||||
@@ -207,7 +210,11 @@ impl Default for General {
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct Interface {
|
||||
/// Legacy single-theme preference, retained for old automation and saved settings.
|
||||
pub theme: Theme,
|
||||
pub appearance_mode: AppearanceMode,
|
||||
pub dark_theme: DarkTheme,
|
||||
pub light_theme: LightTheme,
|
||||
/// Pasteboard colour in standard screen mode (`canvasCustomColor` when "custom").
|
||||
pub canvas_color: CanvasColor,
|
||||
pub canvas_custom_color: String,
|
||||
@@ -237,6 +244,9 @@ impl Default for Interface {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
theme: Theme::ProMedium,
|
||||
appearance_mode: AppearanceMode::Dark,
|
||||
dark_theme: DarkTheme::ProMedium,
|
||||
light_theme: LightTheme::StudioLight,
|
||||
canvas_color: CanvasColor::Default,
|
||||
canvas_custom_color: "#282828".into(),
|
||||
canvas_border: CanvasBorder::DropShadow,
|
||||
@@ -896,6 +906,9 @@ pub fn choices(path: &str) -> Option<&'static [&'static str]> {
|
||||
"general.colorPicker" => ColorPicker::NAMES,
|
||||
"general.imageInterpolation" => Interpolation::NAMES,
|
||||
"interface.theme" => Theme::NAMES,
|
||||
"interface.appearanceMode" => AppearanceMode::NAMES,
|
||||
"interface.darkTheme" => DarkTheme::NAMES,
|
||||
"interface.lightTheme" => LightTheme::NAMES,
|
||||
"interface.canvasColor" => CanvasColor::NAMES,
|
||||
"interface.canvasBorder" => CanvasBorder::NAMES,
|
||||
"interface.uiScale" => UiScale::NAMES,
|
||||
@@ -1301,6 +1314,19 @@ impl Session {
|
||||
/// defaults and unknown keys are ignored, so files from older and newer versions load.
|
||||
pub fn load_prefs_json(&mut self, s: &str) -> std::result::Result<(), String> {
|
||||
let mut v: Value = serde_json::from_str(s).map_err(|e| format!("preferences: {e}"))?;
|
||||
// Saved preferences before appearance modes had one concrete theme. Preserve its
|
||||
// appearance instead of silently switching established users to Auto.
|
||||
if let Some(interface) = v.get_mut("interface").and_then(Value::as_object_mut)
|
||||
&& !interface.contains_key("appearanceMode")
|
||||
&& 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"),
|
||||
};
|
||||
interface.insert("appearanceMode".into(), json!(mode));
|
||||
interface.insert(slot.into(), json!(theme.name()));
|
||||
}
|
||||
let color = v.as_object_mut().and_then(|m| m.remove("colorSettings"));
|
||||
let presets = v.as_object_mut().and_then(|m| m.remove("presets"));
|
||||
let prefs: Preferences = serde_json::from_value(v).map_err(|e| format!("preferences: {e}"))?;
|
||||
@@ -1361,6 +1387,44 @@ impl Session {
|
||||
} else {
|
||||
next.set(path, value)?;
|
||||
}
|
||||
// Old `prefs.set interface.theme` clients (by path or inside a section object) still
|
||||
// select a visible theme.
|
||||
if path == "interface.theme" || next.interface.theme != self.prefs().interface.theme {
|
||||
match next.interface.theme {
|
||||
Theme::Pro => {
|
||||
next.interface.dark_theme = DarkTheme::Pro;
|
||||
next.interface.appearance_mode = AppearanceMode::Dark;
|
||||
}
|
||||
Theme::ProMedium => {
|
||||
next.interface.dark_theme = DarkTheme::ProMedium;
|
||||
next.interface.appearance_mode = AppearanceMode::Dark;
|
||||
}
|
||||
Theme::Studio => {
|
||||
next.interface.dark_theme = DarkTheme::Studio;
|
||||
next.interface.appearance_mode = AppearanceMode::Dark;
|
||||
}
|
||||
Theme::StudioLight => {
|
||||
next.interface.light_theme = LightTheme::StudioLight;
|
||||
next.interface.appearance_mode = AppearanceMode::Light;
|
||||
}
|
||||
Theme::Classic => {
|
||||
next.interface.light_theme = LightTheme::Classic;
|
||||
next.interface.appearance_mode = AppearanceMode::Light;
|
||||
}
|
||||
Theme::Adwaita => {
|
||||
next.interface.light_theme = LightTheme::Adwaita;
|
||||
next.interface.appearance_mode = AppearanceMode::Light;
|
||||
}
|
||||
Theme::SolarizedDark => {
|
||||
next.interface.dark_theme = DarkTheme::SolarizedDark;
|
||||
next.interface.appearance_mode = AppearanceMode::Dark;
|
||||
}
|
||||
Theme::AdwaitaDark => {
|
||||
next.interface.dark_theme = DarkTheme::AdwaitaDark;
|
||||
next.interface.appearance_mode = AppearanceMode::Dark;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.prefs.edit(|p| *p = next);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -22,6 +22,46 @@ fn defaults_match_photoshop() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn appearance_defaults_and_legacy_theme_migrate() {
|
||||
let p = Preferences::default();
|
||||
// New users keep Photoshop's default; following the system is opt-in.
|
||||
assert_eq!(p.interface.appearance_mode, AppearanceMode::Dark);
|
||||
assert_eq!(p.interface.dark_theme, DarkTheme::ProMedium);
|
||||
assert_eq!(p.interface.light_theme, LightTheme::StudioLight);
|
||||
for (theme, mode, dark, light) in
|
||||
[("pro", AppearanceMode::Dark, DarkTheme::Pro, LightTheme::StudioLight), ("classic", AppearanceMode::Light, DarkTheme::ProMedium, LightTheme::Classic)]
|
||||
{
|
||||
let mut s = Session::new();
|
||||
s.load_prefs_json(&json!({"interface": {"theme": theme}}).to_string()).unwrap();
|
||||
assert_eq!(s.prefs().interface.appearance_mode, mode);
|
||||
assert_eq!(s.prefs().interface.dark_theme, dark);
|
||||
assert_eq!(s.prefs().interface.light_theme, light);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn appearance_choices_validate_and_legacy_theme_still_selects() {
|
||||
let mut s = Session::new();
|
||||
s.execute("prefs.set", json!({"values": {"interface.appearanceMode": "auto", "interface.darkTheme": "studio", "interface.lightTheme": "classic"}}))
|
||||
.unwrap();
|
||||
assert_eq!(s.prefs().interface.appearance_mode, AppearanceMode::Auto);
|
||||
assert_eq!(s.prefs().interface.dark_theme, DarkTheme::Studio);
|
||||
assert_eq!(s.prefs().interface.light_theme, LightTheme::Classic);
|
||||
let mut restarted = Session::new();
|
||||
restarted.load_prefs_json(&s.prefs_to_json()).unwrap();
|
||||
assert_eq!(restarted.prefs().interface, s.prefs().interface);
|
||||
assert!(s.execute("prefs.set", json!({"path": "interface.darkTheme", "value": "classic"})).is_err());
|
||||
s.execute("prefs.set", json!({"path": "interface.theme", "value": "pro"})).unwrap();
|
||||
assert_eq!(s.prefs().interface.appearance_mode, AppearanceMode::Dark);
|
||||
assert_eq!(s.prefs().interface.dark_theme, DarkTheme::Pro);
|
||||
assert_eq!(s.prefs().interface.light_theme, LightTheme::Classic);
|
||||
s.execute("prefs.set", json!({"path": "interface", "value": {"theme": "studioLight"}})).unwrap();
|
||||
assert_eq!(s.prefs().interface.appearance_mode, AppearanceMode::Light);
|
||||
assert_eq!(s.prefs().interface.light_theme, LightTheme::StudioLight);
|
||||
assert_eq!(s.prefs().interface.dark_theme, DarkTheme::Pro);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_set_reset_by_path() {
|
||||
let mut s = session();
|
||||
|
||||
@@ -2309,6 +2309,14 @@ fontWeight Italic Kurzíva
|
||||
Cycle Orientation Přepnout orientaci
|
||||
Set the overlay options for the Crop tool Nastavit volby překryvu pro nástroj Oříznutí
|
||||
Alt-click went to the desktop, which moves windows with Alt-drag. Set its window-move key to Super, or latch Alt in Window › Modifier Keys. Alt+klik převzalo pracovní prostředí, které pomocí Alt+tažení přesouvá okna. Nastavte jeho klávesu pro přesun oken na Super, nebo zamkněte Alt v nabídce Okno › Modifikační klávesy.
|
||||
Appearance Mode Režim vzhledu
|
||||
Dark Theme Tmavý motiv
|
||||
Light Theme Světlý motiv
|
||||
Pro Medium Pro Medium
|
||||
Studio Light Studio Light
|
||||
Next Appearance Mode Další režim vzhledu
|
||||
Sync with system Synchronizovat se systémem
|
||||
Active Aktivní
|
||||
Color Picker (Solid Color) Výběr barvy (plná barva)
|
||||
Straighten the image by drawing a line on it Narovnat obraz nakreslením čáry
|
||||
Set additional Crop options Nastavit další volby oříznutí
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -2348,6 +2348,14 @@ fontWeight Italic Kursiv
|
||||
Cycle Orientation Ausrichtung wechseln
|
||||
Set the overlay options for the Crop tool Überlagerungsoptionen für das Freistellungswerkzeug festlegen
|
||||
Alt-click went to the desktop, which moves windows with Alt-drag. Set its window-move key to Super, or latch Alt in Window › Modifier Keys. Der Alt-Klick ging an die Arbeitsumgebung, die Fenster mit Alt-Ziehen verschiebt. Stelle dort die Taste zum Verschieben von Fenstern auf Super oder raste Alt unter Fenster › Modifikatortasten ein.
|
||||
Appearance Mode Erscheinungsmodus
|
||||
Dark Theme Dunkles Design
|
||||
Light Theme Helles Design
|
||||
Pro Medium Pro Medium
|
||||
Studio Light Studio Light
|
||||
Next Appearance Mode Nächster Erscheinungsmodus
|
||||
Sync with system Mit System synchronisieren
|
||||
Active Aktiv
|
||||
Color Picker (Solid Color) Farbwähler (Volltonfarbe)
|
||||
Straighten the image by drawing a line on it Bild durch Ziehen einer Linie gerade ausrichten
|
||||
Set additional Crop options Zusätzliche Freistellungsoptionen festlegen
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -3419,6 +3419,12 @@ fontWeight Thin Πολύ λεπτά
|
||||
Cycle Orientation Εναλλαγή προσανατολισμού
|
||||
Set the overlay options for the Crop tool Ορισμός επιλογών επικάλυψης για το εργαλείο Περικοπή
|
||||
Alt-click went to the desktop, which moves windows with Alt-drag. Set its window-move key to Super, or latch Alt in Window › Modifier Keys. Το Alt-κλικ πήγε στο περιβάλλον εργασίας, που μετακινεί παράθυρα με Alt-σύρσιμο. Ορίστε το πλήκτρο μετακίνησης παραθύρων του σε Super ή κλειδώστε το Alt στο Παράθυρο › Πλήκτρα τροποποίησης.
|
||||
Appearance Mode Λειτουργία εμφάνισης
|
||||
Pro Medium Pro Medium
|
||||
Studio Light Studio Light
|
||||
Next Appearance Mode Επόμενη λειτουργία εμφάνισης
|
||||
Sync with system Συγχρονισμός με το σύστημα
|
||||
Active Ενεργό
|
||||
Color Picker (Solid Color) Επιλογέας χρώματος (συμπαγές χρώμα)
|
||||
Straighten the image by drawing a line on it Ευθυγράμμιση της εικόνας σχεδιάζοντας μια γραμμή πάνω της
|
||||
Set additional Crop options Ορισμός πρόσθετων επιλογών περικοπής
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 965 and column 18.
|
@@ -2377,6 +2377,14 @@ fontWeight Italic Cursiva
|
||||
Cycle Orientation Alternar orientación
|
||||
Set the overlay options for the Crop tool Definir las opciones de superposición de la herramienta Recortar
|
||||
Alt-click went to the desktop, which moves windows with Alt-drag. Set its window-move key to Super, or latch Alt in Window › Modifier Keys. El Alt+clic lo recibió el escritorio, que mueve ventanas con Alt+arrastrar. Cambia su tecla para mover ventanas a Super o bloquea Alt en Ventana › Teclas modificadoras.
|
||||
Appearance Mode Modo de apariencia
|
||||
Dark Theme Tema oscuro
|
||||
Light Theme Tema claro
|
||||
Pro Medium Pro Medium
|
||||
Studio Light Studio Light
|
||||
Next Appearance Mode Siguiente modo de apariencia
|
||||
Sync with system Sincronizar con el sistema
|
||||
Active Activo
|
||||
Color Picker (Solid Color) Selector de color (color uniforme)
|
||||
Straighten the image by drawing a line on it Enderezar la imagen dibujando una línea en ella
|
||||
Set additional Crop options Definir opciones de recorte adicionales
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 4 and column 47.
|
@@ -2730,6 +2730,14 @@ fontWeight Italic Italique
|
||||
Cycle Orientation Changer d’orientation
|
||||
Set the overlay options for the Crop tool Définir les options de superposition de l’outil Recadrage
|
||||
Alt-click went to the desktop, which moves windows with Alt-drag. Set its window-move key to Super, or latch Alt in Window › Modifier Keys. L’Alt-clic a été intercepté par le bureau, qui déplace les fenêtres avec Alt-glisser. Réglez sa touche de déplacement des fenêtres sur Super, ou verrouillez Alt dans Fenêtre › Touches de modification.
|
||||
Appearance Mode Mode d’apparence
|
||||
Dark Theme Thème sombre
|
||||
Light Theme Thème clair
|
||||
Pro Medium Pro Medium
|
||||
Studio Light Studio Light
|
||||
Next Appearance Mode Mode d’apparence suivant
|
||||
Sync with system Synchroniser avec le système
|
||||
Active Actif
|
||||
Color Picker (Solid Color) Sélecteur de couleurs (couleur unie)
|
||||
Straighten the image by drawing a line on it Redresser l’image en traçant une ligne dessus
|
||||
Set additional Crop options Définir d’autres options de recadrage
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -2717,6 +2717,14 @@ fontWeight Italic Miring
|
||||
Cycle Orientation Ganti Orientasi
|
||||
Set the overlay options for the Crop tool Atur opsi overlay untuk alat Crop
|
||||
Alt-click went to the desktop, which moves windows with Alt-drag. Set its window-move key to Super, or latch Alt in Window › Modifier Keys. Alt-klik diambil oleh desktop, yang memindahkan jendela dengan Alt-seret. Atur tombol pemindah jendelanya ke Super, atau kunci Alt di Jendela › Tombol Pengubah.
|
||||
Appearance Mode Mode tampilan
|
||||
Dark Theme Tema gelap
|
||||
Light Theme Tema terang
|
||||
Pro Medium Pro Medium
|
||||
Studio Light Studio Light
|
||||
Next Appearance Mode Mode tampilan berikutnya
|
||||
Sync with system Sinkronkan dengan sistem
|
||||
Active Aktif
|
||||
Color Picker (Solid Color) Pemilih Warna (Warna Solid)
|
||||
Straighten the image by drawing a line on it Luruskan gambar dengan menggambar garis di atasnya
|
||||
Set additional Crop options Atur opsi Crop tambahan
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 4 and column 47.
|
@@ -2379,6 +2379,14 @@ fontWeight Italic Corsivo
|
||||
Cycle Orientation Cambia orientamento
|
||||
Set the overlay options for the Crop tool Imposta le opzioni di sovrapposizione dello strumento Taglierina
|
||||
Alt-click went to the desktop, which moves windows with Alt-drag. Set its window-move key to Super, or latch Alt in Window › Modifier Keys. L’Alt-click è andato al desktop, che sposta le finestre con Alt-trascina. Imposta il suo tasto per spostare le finestre su Super, oppure blocca Alt in Finestra › Tasti modificatori.
|
||||
Appearance Mode Modalità aspetto
|
||||
Dark Theme Tema scuro
|
||||
Light Theme Tema chiaro
|
||||
Pro Medium Pro Medium
|
||||
Studio Light Studio Light
|
||||
Next Appearance Mode Modalità aspetto successiva
|
||||
Sync with system Sincronizza con il sistema
|
||||
Active Attivo
|
||||
Color Picker (Solid Color) Selettore colore (tinta unita)
|
||||
Straighten the image by drawing a line on it Raddrizza l’immagine tracciando una linea
|
||||
Set additional Crop options Imposta opzioni di ritaglio aggiuntive
|
||||
|
||||
|
@@ -3333,6 +3333,14 @@ fontWeight Italic 斜体
|
||||
Cycle Orientation 方向を切り替え
|
||||
Set the overlay options for the Crop tool 切り抜きツールのオーバーレイオプションを設定
|
||||
Alt-click went to the desktop, which moves windows with Alt-drag. Set its window-move key to Super, or latch Alt in Window › Modifier Keys. Alt+クリックはデスクトップに取られました(Alt+ドラッグでウィンドウを移動する設定です)。デスクトップのウィンドウ移動キーを Super に変更するか、ウィンドウ › 修飾キーで Alt を固定してください。
|
||||
Appearance Mode 外観モード
|
||||
Dark Theme ダークテーマ
|
||||
Light Theme ライトテーマ
|
||||
Pro Medium Pro Medium
|
||||
Studio Light Studio Light
|
||||
Next Appearance Mode 次の外観モード
|
||||
Sync with system システムと同期
|
||||
Active 適用中
|
||||
Color Picker (Solid Color) カラーピッカー(べた塗り)
|
||||
Straighten the image by drawing a line on it 画像上に線を描いて角度を補正
|
||||
Set additional Crop options 切り抜きの追加オプションを設定
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -2888,6 +2888,14 @@ fontWeight Italic 기울임
|
||||
Cycle Orientation 방향 순환
|
||||
Set the overlay options for the Crop tool 자르기 도구의 오버레이 옵션 설정
|
||||
Alt-click went to the desktop, which moves windows with Alt-drag. Set its window-move key to Super, or latch Alt in Window › Modifier Keys. Alt 클릭이 데스크톱으로 전달되었습니다. 데스크톱이 Alt 드래그로 창을 이동합니다. 창 이동 키를 Super로 바꾸거나 창 › 보조 키에서 Alt를 고정하세요.
|
||||
Appearance Mode 화면 모드
|
||||
Dark Theme 어두운 테마
|
||||
Light Theme 밝은 테마
|
||||
Pro Medium Pro Medium
|
||||
Studio Light Studio Light
|
||||
Next Appearance Mode 다음 화면 모드
|
||||
Sync with system 시스템과 동기화
|
||||
Active 활성
|
||||
Color Picker (Solid Color) 색상 피커 (단색)
|
||||
Straighten the image by drawing a line on it 이미지에 선을 그려 수평 맞추기
|
||||
Set additional Crop options 추가 자르기 옵션 설정
|
||||
|
||||
|
@@ -263,7 +263,7 @@ fn preferences_preview_does_not_commit_until_applied_and_survives_reload() {
|
||||
dialog.fields.get_mut("values").expect("values")["interface"]["language"] = json!("fr");
|
||||
h.run_steps(3);
|
||||
let text = drawn_text(&h);
|
||||
assert!(text.iter().any(|text| text == "Langue"), "the draft language must translate the actual dialog");
|
||||
assert!(text.iter().any(|text| text == "Apparence"), "the draft language must translate the visible dialog");
|
||||
assert!(text.iter().any(|text| text == "Annuler"), "the dialog buttons must preview the language too");
|
||||
assert!(text.iter().any(|text| text == "File"), "the main window keeps its committed language");
|
||||
assert_eq!(h.state().session.prefs().interface.language, "auto");
|
||||
|
||||
@@ -3494,6 +3494,11 @@ cameraRaw Whites Witten
|
||||
Cycle Orientation Oriëntatie wisselen
|
||||
Set the overlay options for the Crop tool Overlay-opties voor het gereedschap Uitsnijden instellen
|
||||
Alt-click went to the desktop, which moves windows with Alt-drag. Set its window-move key to Super, or latch Alt in Window › Modifier Keys. De Alt-klik ging naar het bureaublad, dat vensters verplaatst met Alt-slepen. Stel daar de toets voor het verplaatsen van vensters in op Super, of vergrendel Alt via Venster › Wijzigingstoetsen.
|
||||
|
||||
Appearance Mode Weergavemodus
|
||||
Next Appearance Mode Volgende weergavemodus
|
||||
Sync with system Synchroniseren met systeem
|
||||
Active Actief
|
||||
Color Picker (Solid Color) Kleurkiezer (effen kleur)
|
||||
Straighten the image by drawing a line on it Recht de afbeelding uit door er een lijn op te tekenen
|
||||
Set additional Crop options Aanvullende uitsnijopties instellen
|
||||
@@ -3522,3 +3527,5 @@ cameraRaw Whites Witten
|
||||
XOR XOR
|
||||
Color Burn (Paint.NET) Kleur doordrukken (Paint.NET)
|
||||
Color Dodge (Paint.NET) Kleur tegenhouden (Paint.NET)
|
||||
Pro Medium Pro Medium
|
||||
Studio Light Studio Light
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 8 and column 34.
|
@@ -3332,6 +3332,14 @@ cameraRaw Roughness Chropowatość
|
||||
Cycle Orientation Przełącz orientację
|
||||
Set the overlay options for the Crop tool Ustaw opcje nakładki narzędzia Kadrowanie
|
||||
Alt-click went to the desktop, which moves windows with Alt-drag. Set its window-move key to Super, or latch Alt in Window › Modifier Keys. Alt+kliknięcie przejęło środowisko pulpitu, które przesuwa okna przez Alt+przeciąganie. Ustaw w nim klawisz przesuwania okien na Super lub zablokuj Alt w Okno › Klawisze modyfikujące.
|
||||
Appearance Mode Tryb wyglądu
|
||||
Dark Theme Ciemny motyw
|
||||
Light Theme Jasny motyw
|
||||
Pro Medium Pro Medium
|
||||
Studio Light Studio Light
|
||||
Next Appearance Mode Następny tryb wyglądu
|
||||
Sync with system Synchronizuj z systemem
|
||||
Active Aktywny
|
||||
Color Picker (Solid Color) Próbnik kolorów (kolor jednolity)
|
||||
Straighten the image by drawing a line on it Wyprostuj obraz, rysując na nim linię
|
||||
Set additional Crop options Ustaw dodatkowe opcje kadrowania
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -2738,6 +2738,14 @@ fontWeight Italic Itálico
|
||||
Cycle Orientation Alternar orientação
|
||||
Set the overlay options for the Crop tool Definir as opções de sobreposição da ferramenta Corte demarcado
|
||||
Alt-click went to the desktop, which moves windows with Alt-drag. Set its window-move key to Super, or latch Alt in Window › Modifier Keys. O Alt-clique foi para o desktop, que move janelas com Alt-arrastar. Mude a tecla de mover janelas dele para Super ou trave o Alt em Janela › Teclas modificadoras.
|
||||
Appearance Mode Modo de aparência
|
||||
Dark Theme Tema escuro
|
||||
Light Theme Tema claro
|
||||
Pro Medium Pro Medium
|
||||
Studio Light Studio Light
|
||||
Next Appearance Mode Próximo modo de aparência
|
||||
Sync with system Sincronizar com o sistema
|
||||
Active Ativo
|
||||
Color Picker (Solid Color) Seletor de Cores (Cor sólida)
|
||||
Straighten the image by drawing a line on it Endireitar a imagem desenhando uma linha sobre ela
|
||||
Set additional Crop options Definir opções adicionais de corte
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -2584,6 +2584,14 @@ fontWeight Italic Курсив
|
||||
Cycle Orientation Сменить ориентацию
|
||||
Set the overlay options for the Crop tool Задать параметры перекрытия для инструмента «Рамка»
|
||||
Alt-click went to the desktop, which moves windows with Alt-drag. Set its window-move key to Super, or latch Alt in Window › Modifier Keys. Alt+щелчок перехватило окружение рабочего стола: оно перемещает окна через Alt+перетаскивание. Назначьте для перемещения окон клавишу Super или зафиксируйте Alt в меню Окно › Клавиши-модификаторы.
|
||||
Appearance Mode Режим оформления
|
||||
Dark Theme Тёмная тема
|
||||
Light Theme Светлая тема
|
||||
Pro Medium Pro Medium
|
||||
Studio Light Studio Light
|
||||
Next Appearance Mode Следующий режим оформления
|
||||
Sync with system Синхронизировать с системой
|
||||
Active Активна
|
||||
Color Picker (Solid Color) Выбор цвета (сплошной цвет)
|
||||
Straighten the image by drawing a line on it Выровнять изображение, нарисовав на нем линию
|
||||
Set additional Crop options Задать дополнительные параметры кадрирования
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -3419,6 +3419,13 @@ cameraRaw Masking Маскування
|
||||
Cycle Orientation Змінити орієнтацію
|
||||
Set the overlay options for the Crop tool Задати параметри накладання для інструмента «Рамка»
|
||||
Alt-click went to the desktop, which moves windows with Alt-drag. Set its window-move key to Super, or latch Alt in Window › Modifier Keys. Alt-клік перехопило робоче середовище, яке переміщує вікна через Alt-перетягування. Призначте для переміщення вікон клавішу Super або зафіксуйте Alt у меню Вікно › Клавіші-модифікатори.
|
||||
|
||||
Appearance Mode Режим вигляду
|
||||
Dark Theme Темна тема
|
||||
Light Theme Світла тема
|
||||
Next Appearance Mode Наступний режим вигляду
|
||||
Sync with system Синхронізувати із системою
|
||||
Active Активна
|
||||
Color Picker (Solid Color) Вибір кольору (суцільний колір)
|
||||
Straighten the image by drawing a line on it Вирівняти зображення, намалювавши на ньому лінію
|
||||
Set additional Crop options Задати додаткові параметри кадрування
|
||||
@@ -3447,3 +3454,5 @@ cameraRaw Masking Маскування
|
||||
XOR XOR
|
||||
Color Burn (Paint.NET) Затемнення основи (Paint.NET)
|
||||
Color Dodge (Paint.NET) Освітлення основи (Paint.NET)
|
||||
Pro Medium Pro Medium
|
||||
Studio Light Studio Light
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 1306 and column 18.
|
@@ -2317,6 +2317,14 @@ fontWeight Italic 斜体
|
||||
Cycle Orientation 循环切换取向
|
||||
Set the overlay options for the Crop tool 设置裁剪工具的叠加选项
|
||||
Alt-click went to the desktop, which moves windows with Alt-drag. Set its window-move key to Super, or latch Alt in Window › Modifier Keys. Alt 单击被桌面环境截获了,它用 Alt 拖动来移动窗口。请把桌面的窗口移动键改为 Super,或在 窗口 › 修饰键 中锁定 Alt。
|
||||
Appearance Mode 外观模式
|
||||
Dark Theme 深色主题
|
||||
Light Theme 浅色主题
|
||||
Pro Medium Pro Medium
|
||||
Studio Light Studio Light
|
||||
Next Appearance Mode 下一个外观模式
|
||||
Sync with system 与系统同步
|
||||
Active 当前使用
|
||||
Color Picker (Solid Color) 拾色器(纯色)
|
||||
Straighten the image by drawing a line on it 通过在图像上画一条线来拉直图像
|
||||
Set additional Crop options 设置其他裁剪选项
|
||||
|
||||
|
@@ -2286,6 +2286,14 @@ fontWeight Italic 斜體
|
||||
Cycle Orientation 循環切換方向
|
||||
Set the overlay options for the Crop tool 設定裁切工具的覆蓋選項
|
||||
Alt-click went to the desktop, which moves windows with Alt-drag. Set its window-move key to Super, or latch Alt in Window › Modifier Keys. Alt 按一下被桌面環境攔截了,它用 Alt 拖曳來移動視窗。請把桌面的視窗移動鍵改為 Super,或在 視窗 › 輔助鍵 中鎖定 Alt。
|
||||
Appearance Mode 外觀模式
|
||||
Dark Theme 深色主題
|
||||
Light Theme 淺色主題
|
||||
Pro Medium Pro Medium
|
||||
Studio Light Studio Light
|
||||
Next Appearance Mode 下一個外觀模式
|
||||
Sync with system 與系統同步
|
||||
Active 目前使用
|
||||
Color Picker (Solid Color) 檢色器(純色)
|
||||
Straighten the image by drawing a line on it 在影像上繪製一條線以拉直影像
|
||||
Set additional Crop options 設定其他裁切選項
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -67,6 +67,7 @@ pub static ICONS: &[(&str, &[u8])] = &[
|
||||
("message-square", include_bytes!("../../../assets/icons/message-square.svg")),
|
||||
("minus", include_bytes!("../../../assets/icons/minus.svg")),
|
||||
("moon", include_bytes!("../../../assets/icons/moon.svg")),
|
||||
("monitor", include_bytes!("../../../assets/icons/monitor.svg")),
|
||||
("mouse-pointer-2", include_bytes!("../../../assets/icons/mouse-pointer-2.svg")),
|
||||
("move", include_bytes!("../../../assets/icons/move.svg")),
|
||||
("navigation", include_bytes!("../../../assets/icons/navigation.svg")),
|
||||
|
||||
@@ -227,6 +227,8 @@ pub type Inbox = std::sync::Arc<std::sync::Mutex<Vec<(String, Vec<u8>)>>>;
|
||||
pub type LoadTextFn = Box<dyn FnMut() -> Option<String>>;
|
||||
/// Persist the preferences text.
|
||||
pub type SaveTextFn = Box<dyn FnMut(&str) -> Result<(), String>>;
|
||||
/// Platform appearance when egui cannot detect it (for example, Wayland without a theme event).
|
||||
pub type SystemThemeFn = Box<dyn Fn(&egui::Context) -> Option<egui::Theme>>;
|
||||
/// Autosave a document snapshot for crash recovery: (snapshot, revision, original path).
|
||||
pub type AutosaveFn = Box<dyn FnMut(&std::sync::Arc<Document>, u64, Option<&str>) -> Result<(), String>>;
|
||||
/// Poll successful or failed background writes: (document id, revision, result).
|
||||
@@ -300,6 +302,7 @@ pub struct Services {
|
||||
/// the web (see `prefs_ui`).
|
||||
pub load_prefs: Option<LoadTextFn>,
|
||||
pub save_prefs: Option<SaveTextFn>,
|
||||
pub system_theme: Option<SystemThemeFn>,
|
||||
/// The native window is connected directly to a Wayland compositor.
|
||||
pub is_wayland: bool,
|
||||
/// On Wayland, the shell command that starts this install under XWayland, where native file
|
||||
@@ -1414,6 +1417,14 @@ impl PhotocraftApp {
|
||||
}
|
||||
|
||||
pub fn set_theme(&mut self, ctx: &egui::Context, kind: theme::ThemeKind) {
|
||||
if let Err(e) = self.run("prefs.set", serde_json::json!({"path": "interface.theme", "value": kind.id()})) {
|
||||
self.ui.status = e;
|
||||
} else {
|
||||
self.apply_theme(ctx, kind);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn apply_theme(&mut self, ctx: &egui::Context, kind: theme::ThemeKind) {
|
||||
self.ui.theme = kind;
|
||||
theme::apply(ctx, kind);
|
||||
self.checker = None;
|
||||
|
||||
@@ -63,7 +63,7 @@ pub const UI_COMMANDS: &[(&str, &str, &[&str], Option<&str>)] = &[
|
||||
("window.togglePanels", "Show/Hide All Panels", &[], Some("Tab")),
|
||||
("window.toggle.dock", "Show/Hide Panels", &[], Some("Shift+Tab")),
|
||||
("window.toggle.options", "Options", &["Window"], None),
|
||||
("window.theme.toggle", "Next Theme", &["Window"], None),
|
||||
("window.theme.toggle", "Next Appearance Mode", &["Window"], None),
|
||||
("window.theme.pro", "Pro Theme", &["Window", "Theme"], None),
|
||||
("window.theme.proMedium", "Pro Medium Gray Theme", &["Window", "Theme"], None),
|
||||
("window.theme.studio", "Studio Theme", &["Window", "Theme"], None),
|
||||
@@ -304,8 +304,7 @@ pub(crate) fn invoke_unguarded(app: &mut PhotocraftApp, ctx: &egui::Context, id:
|
||||
Ok(json!({"window": wid}))
|
||||
}
|
||||
"window.theme.toggle" => {
|
||||
let next = app.ui.theme.next();
|
||||
app.set_theme(ctx, next);
|
||||
crate::prefs_ui::cycle_appearance(app, ctx);
|
||||
Ok(Value::Null)
|
||||
}
|
||||
"window.theme.pro"
|
||||
|
||||
@@ -814,7 +814,7 @@ mod tests {
|
||||
assert!(!file.children.contains(&Node::Standard(Standard::CloseWindow)), "⌘W stays File › Close");
|
||||
}
|
||||
|
||||
/// Themes and Next Theme move from Window to the app menu's Appearance, checked by the theme.
|
||||
/// Themes and the appearance-mode cycle move from Window to the app menu's Appearance.
|
||||
#[test]
|
||||
fn themes_move_to_the_app_menus_appearance() {
|
||||
let mut a = app(false);
|
||||
@@ -829,7 +829,7 @@ mod tests {
|
||||
assert!(themes.contains(&"window.theme.pro".to_string()) && themes.contains(&"window.theme.classic".to_string()));
|
||||
assert_eq!(l.bar.find("window.theme.classic").unwrap().checked, Some(true));
|
||||
assert_eq!(l.bar.find("window.theme.pro").unwrap().checked, Some(false));
|
||||
assert_eq!(l.bar.find("window.theme.toggle").unwrap().label, "Next Theme");
|
||||
assert_eq!(l.bar.find("window.theme.toggle").unwrap().label, "Next Appearance Mode");
|
||||
let window = ids(&l.bar.menu(MenuRole::Window).unwrap().children);
|
||||
assert!(!window.iter().any(|id| id.starts_with("window.theme.") || id == "[Theme]"), "{window:?}");
|
||||
}
|
||||
|
||||
@@ -566,10 +566,14 @@ pub fn title_bar(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
|
||||
{
|
||||
app.ui.palette_open = !app.ui.palette_open;
|
||||
}
|
||||
let theme_icon = if t.dark() { "sun" } else { "moon" };
|
||||
if icons_shown >= 2 && icons::button(ui, theme_icon, 28.0, false, tl!("Switch theme")).clicked() {
|
||||
let next = app.ui.theme.next();
|
||||
app.set_theme(ui.ctx(), next);
|
||||
let (theme_icon, appearance_label) = match app.session.prefs().interface.appearance_mode {
|
||||
photocraft_engine::prefs::AppearanceMode::Auto => ("monitor", tl!("Auto")),
|
||||
photocraft_engine::prefs::AppearanceMode::Dark => ("moon", tl!("Dark")),
|
||||
photocraft_engine::prefs::AppearanceMode::Light => ("sun", tl!("Light")),
|
||||
};
|
||||
let appearance_tip = format!("{}: {}", tl!("Appearance Mode"), appearance_label);
|
||||
if icons_shown >= 2 && icons::button(ui, theme_icon, 28.0, false, &appearance_tip).clicked() {
|
||||
crate::prefs_ui::cycle_appearance(app, ui.ctx());
|
||||
}
|
||||
// The community Discord, one click away while the bar has room for it
|
||||
// (narrow windows drop it first; it is also Help › Discord).
|
||||
|
||||
+217
-42
@@ -11,8 +11,10 @@ use std::collections::{BTreeMap, HashMap, VecDeque};
|
||||
|
||||
use egui::{Color32, RichText, Sense, vec2};
|
||||
use photocraft_doc::DocId;
|
||||
#[cfg(test)]
|
||||
use photocraft_engine::prefs::Theme;
|
||||
use photocraft_engine::jobs::{JobEvent, JobId, JobOutcome, Started};
|
||||
use photocraft_engine::prefs::{self, SECTIONS, Theme};
|
||||
use photocraft_engine::prefs::{self, AppearanceMode, DarkTheme, LightTheme, SECTIONS};
|
||||
use photocraft_engine::snap::{SnapLine, SnapTargets};
|
||||
use serde_json::{Map, Value, json};
|
||||
|
||||
@@ -29,7 +31,6 @@ pub struct Runtime {
|
||||
/// This instance's preferences as of `saved_rev`: the base of the three-way merge on save.
|
||||
saved_value: Option<Value>,
|
||||
save_retry: SaveRetry,
|
||||
theme_pref: Option<Theme>,
|
||||
next_autosave_ms: f64,
|
||||
/// Last revision whose recovery file was actually written successfully.
|
||||
autosaved: HashMap<DocId, u64>,
|
||||
@@ -78,30 +79,47 @@ pub fn pasteboard_color(app: &PhotocraftApp) -> Option<Color32> {
|
||||
})
|
||||
}
|
||||
|
||||
fn theme_kind(t: Theme) -> ThemeKind {
|
||||
match t {
|
||||
Theme::Pro => ThemeKind::Pro,
|
||||
Theme::ProMedium => ThemeKind::ProMedium,
|
||||
Theme::Studio => ThemeKind::Studio,
|
||||
Theme::StudioLight => ThemeKind::StudioLight,
|
||||
Theme::Classic => ThemeKind::Classic,
|
||||
Theme::Adwaita => ThemeKind::Adwaita,
|
||||
Theme::AdwaitaDark => ThemeKind::AdwaitaDark,
|
||||
Theme::SolarizedDark => ThemeKind::SolarizedDark,
|
||||
fn selected_theme(interface: &prefs::Interface, system: Option<egui::Theme>) -> ThemeKind {
|
||||
let light = match interface.appearance_mode {
|
||||
AppearanceMode::Light => true,
|
||||
AppearanceMode::Dark => false,
|
||||
AppearanceMode::Auto => system == Some(egui::Theme::Light),
|
||||
};
|
||||
if light {
|
||||
match interface.light_theme {
|
||||
LightTheme::StudioLight => ThemeKind::StudioLight,
|
||||
LightTheme::Classic => ThemeKind::Classic,
|
||||
LightTheme::Adwaita => ThemeKind::Adwaita,
|
||||
}
|
||||
} else {
|
||||
match interface.dark_theme {
|
||||
DarkTheme::Pro => ThemeKind::Pro,
|
||||
DarkTheme::ProMedium => ThemeKind::ProMedium,
|
||||
DarkTheme::Studio => ThemeKind::Studio,
|
||||
DarkTheme::SolarizedDark => ThemeKind::SolarizedDark,
|
||||
DarkTheme::AdwaitaDark => ThemeKind::AdwaitaDark,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn theme_pref(k: ThemeKind) -> Theme {
|
||||
match k {
|
||||
ThemeKind::Pro => Theme::Pro,
|
||||
ThemeKind::ProMedium => Theme::ProMedium,
|
||||
ThemeKind::Studio => Theme::Studio,
|
||||
ThemeKind::StudioLight => Theme::StudioLight,
|
||||
ThemeKind::Classic => Theme::Classic,
|
||||
ThemeKind::Adwaita => Theme::Adwaita,
|
||||
ThemeKind::AdwaitaDark => Theme::AdwaitaDark,
|
||||
ThemeKind::SolarizedDark => Theme::SolarizedDark,
|
||||
pub(crate) fn cycle_appearance(app: &mut PhotocraftApp, ctx: &egui::Context) {
|
||||
let next = match app.session.prefs().interface.appearance_mode {
|
||||
AppearanceMode::Auto => AppearanceMode::Light,
|
||||
AppearanceMode::Light => AppearanceMode::Dark,
|
||||
AppearanceMode::Dark => AppearanceMode::Auto,
|
||||
};
|
||||
if let Err(e) = app.run("prefs.set", json!({"path": "interface.appearanceMode", "value": next.name()})) {
|
||||
app.ui.status = e;
|
||||
return;
|
||||
}
|
||||
let selected = selected_theme(&app.session.prefs().interface, system_theme(app, ctx));
|
||||
if app.ui.theme != selected {
|
||||
app.apply_theme(ctx, selected);
|
||||
}
|
||||
}
|
||||
|
||||
fn system_theme(app: &PhotocraftApp, ctx: &egui::Context) -> Option<egui::Theme> {
|
||||
app.services.system_theme.as_ref().and_then(|read| read(ctx)).or_else(|| ctx.system_theme())
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ lifecycle
|
||||
@@ -287,18 +305,10 @@ pub fn tick(app: &mut PhotocraftApp, ctx: &egui::Context) {
|
||||
}
|
||||
recovery(app);
|
||||
sync_display_scale(app, ctx);
|
||||
// Interface theme: a preference change applies to the UI; a theme picked from the Window
|
||||
// menu is stored as the preference.
|
||||
let pref = app.session.prefs().interface.theme;
|
||||
if app.prefs_rt.theme_pref != Some(pref) {
|
||||
app.prefs_rt.theme_pref = Some(pref);
|
||||
if app.ui.theme != theme_kind(pref) {
|
||||
app.set_theme(ctx, theme_kind(pref));
|
||||
}
|
||||
} else if theme_pref(app.ui.theme) != pref {
|
||||
let t = theme_pref(app.ui.theme);
|
||||
app.session.prefs.edit(|p| p.interface.theme = t);
|
||||
app.prefs_rt.theme_pref = Some(t);
|
||||
// The native service also covers Wayland sessions where winit has no system theme.
|
||||
let selected = selected_theme(&app.session.prefs().interface, system_theme(app, ctx));
|
||||
if app.ui.theme != selected {
|
||||
app.apply_theme(ctx, selected);
|
||||
}
|
||||
crate::theme::set_ui_font_size(ctx, app.session.prefs().interface.ui_font_size);
|
||||
presets_store(app);
|
||||
@@ -865,7 +875,8 @@ pub fn body(app: &mut PhotocraftApp, ui: &mut egui::Ui, f: &mut Map<String, Valu
|
||||
match f.get("__prefsui").and_then(Value::as_str).unwrap_or("") {
|
||||
"prefs" => {
|
||||
f.insert("__gpuInfo".into(), json!(app.perf.gpu_info.lines()));
|
||||
prefs_body(ui, f);
|
||||
let system = system_theme(app, ui.ctx());
|
||||
prefs_body(ui, f, system);
|
||||
}
|
||||
"shortcuts" => shortcuts_body(app, ui, f),
|
||||
"presets" => presets_body(app, ui, f),
|
||||
@@ -876,6 +887,12 @@ pub fn body(app: &mut PhotocraftApp, ui: &mut egui::Ui, f: &mut Map<String, Valu
|
||||
}
|
||||
|
||||
fn humanize(key: &str) -> String {
|
||||
match key {
|
||||
"appearanceMode" => return "Appearance Mode".into(),
|
||||
"darkTheme" => return "Dark Theme".into(),
|
||||
"lightTheme" => return "Light Theme".into(),
|
||||
_ => {}
|
||||
}
|
||||
// These controls appear only for WebP, so reuse the existing translated labels.
|
||||
if key == "webpLossless" {
|
||||
return "Lossless".into();
|
||||
@@ -939,7 +956,7 @@ fn color_of(s: &str) -> Color32 {
|
||||
}
|
||||
|
||||
/// Preferences: section list on the left, the section's settings on the right.
|
||||
fn prefs_body(ui: &mut egui::Ui, f: &mut Map<String, Value>) {
|
||||
fn prefs_body(ui: &mut egui::Ui, f: &mut Map<String, Value>, system: Option<egui::Theme>) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let mut section = f.get("section").and_then(Value::as_str).unwrap_or("general").to_string();
|
||||
let mut values = f.get("values").cloned().unwrap_or(Value::Null);
|
||||
@@ -991,7 +1008,7 @@ fn prefs_body(ui: &mut egui::Ui, f: &mut Map<String, Value>) {
|
||||
ui.add_space(4.0);
|
||||
ui.label(RichText::new(tl!("These settings aren't available in PhotoCraft yet.")).color(t.text_faint));
|
||||
} else if let Some(obj) = values.get_mut(§ion).and_then(Value::as_object_mut) {
|
||||
section_fields(ui, §ion, obj, &order, lang);
|
||||
section_fields(ui, §ion, obj, &order, lang, system);
|
||||
if section == "performance" {
|
||||
gpu_status_rows(ui, f.get("__gpuInfo"), obj);
|
||||
}
|
||||
@@ -1078,10 +1095,123 @@ fn export_field_visible(obj: &Map<String, Value>, key: &str) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
fn theme_preview(ui: &mut egui::Ui, kind: ThemeKind, width: f32) {
|
||||
let p = Tokens::for_kind(kind);
|
||||
let (rect, _) = ui.allocate_exact_size(vec2(width, 108.0), Sense::hover());
|
||||
let painter = ui.painter();
|
||||
painter.rect_filled(rect, 3.0, p.dock);
|
||||
let bar = egui::Rect::from_min_size(rect.min, vec2(rect.width(), 16.0));
|
||||
painter.rect_filled(bar, 0.0, p.chrome);
|
||||
for (x, w) in [(8.0, 18.0), (31.0, 13.0), (49.0, 16.0), (70.0, 12.0)] {
|
||||
painter.rect_filled(egui::Rect::from_min_size(bar.min + vec2(x, 6.0), vec2(w, 3.0)), 1.0, p.text_dim);
|
||||
}
|
||||
let tools = egui::Rect::from_min_max(bar.left_bottom(), egui::pos2(rect.left() + 23.0, rect.bottom()));
|
||||
painter.rect_filled(tools, 0.0, p.chrome);
|
||||
for y in [24.0, 39.0, 54.0, 69.0, 84.0] {
|
||||
// The first tool is the active one.
|
||||
let color = if y == 24.0 { p.accent } else { p.icon };
|
||||
painter.rect_filled(egui::Rect::from_min_size(rect.min + vec2(7.0, y), vec2(9.0, 9.0)), 2.0, color);
|
||||
}
|
||||
let dock = egui::Rect::from_min_max(egui::pos2(rect.right() - 75.0, bar.bottom()), rect.max);
|
||||
painter.rect_filled(dock, 0.0, p.dock);
|
||||
painter.rect_filled(egui::Rect::from_min_size(dock.min, vec2(dock.width(), 12.0)), 0.0, p.tab_strip);
|
||||
painter.rect_filled(egui::Rect::from_min_size(dock.min + vec2(6.0, 4.0), vec2(31.0, 3.0)), 1.0, p.text_dim);
|
||||
for y in [36.0, 55.0, 74.0] {
|
||||
let row = egui::Rect::from_min_size(rect.min + vec2(rect.width() - 71.0, y), vec2(67.0, 16.0));
|
||||
// The middle row is the selected layer.
|
||||
let selected = y == 55.0;
|
||||
painter.rect_filled(row, 1.0, if selected { p.accent } else { p.card });
|
||||
painter.rect_filled(egui::Rect::from_min_size(row.min + vec2(4.0, 3.0), vec2(13.0, 10.0)), 1.0, p.canvas);
|
||||
let label = if selected { egui::Color32::from_white_alpha(210) } else { p.text_dim };
|
||||
painter.rect_filled(egui::Rect::from_min_size(row.min + vec2(22.0, 6.0), vec2(35.0, 3.0)), 1.0, label);
|
||||
}
|
||||
let canvas = egui::Rect::from_min_max(egui::pos2(tools.right(), bar.bottom()), egui::pos2(dock.left(), rect.bottom()));
|
||||
painter.rect_filled(canvas, 0.0, p.canvas);
|
||||
let art = canvas.shrink2(vec2(15.0, 13.0));
|
||||
painter.rect_filled(art, 0.0, p.card);
|
||||
painter.rect_stroke(art, 0.0, egui::Stroke::new(1.0, p.card_border), egui::StrokeKind::Inside);
|
||||
}
|
||||
|
||||
fn theme_card(ui: &mut egui::Ui, title: &str, active: bool, selected: &mut String, options: &[(&str, ThemeKind)], width: f32) {
|
||||
let Some((_, first_kind)) = options.first() else { return };
|
||||
let t = Tokens::get(ui.ctx());
|
||||
egui::Frame::new()
|
||||
.fill(t.card)
|
||||
.stroke(egui::Stroke::new(1.0, if active { t.accent } else { t.card_border }))
|
||||
.corner_radius(t.radius)
|
||||
.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.
|
||||
ui.set_min_height(298.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(RichText::new(tl!(title)).strong().color(t.text));
|
||||
if active {
|
||||
ui.label(RichText::new(tl!("Active")).color(t.accent_text).small());
|
||||
}
|
||||
});
|
||||
ui.add_space(5.0);
|
||||
let preview = options.iter().find(|(id, _)| *id == selected.as_str()).map_or(*first_kind, |(_, kind)| *kind);
|
||||
theme_preview(ui, preview, width - 20.0);
|
||||
ui.add_space(6.0);
|
||||
for (id, _) in options {
|
||||
let label = choice_label(id);
|
||||
ui.radio_value(selected, (*id).to_string(), tl!(&label));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn appearance_rows(ui: &mut egui::Ui, obj: &mut Map<String, Value>, system: Option<egui::Theme>) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
ui.label(RichText::new(tl!("Appearance")).strong().color(t.text));
|
||||
let mut mode = obj.get("appearanceMode").and_then(Value::as_str).unwrap_or("auto").to_string();
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(RichText::new(tl!("Appearance Mode")).color(t.text_dim));
|
||||
let choices: [(String, &str); 3] = [("auto".into(), tl!("Sync with system")), ("dark".into(), tl!("Dark")), ("light".into(), tl!("Light"))];
|
||||
let pairs: Vec<(String, &str)> = choices.iter().map(|(id, label)| (id.clone(), *label)).collect();
|
||||
crate::widgets::dropdown(ui, "appearance-mode", &mut mode, &pairs, 190.0);
|
||||
});
|
||||
obj.insert("appearanceMode".into(), json!(mode));
|
||||
ui.add_space(8.0);
|
||||
let light_active = mode == "light" || (mode == "auto" && system == Some(egui::Theme::Light));
|
||||
let width = ((ui.available_width() - 10.0) / 2.0).max(190.0);
|
||||
let mut light = obj.get("lightTheme").and_then(Value::as_str).unwrap_or("studioLight").to_string();
|
||||
let mut dark = obj.get("darkTheme").and_then(Value::as_str).unwrap_or("proMedium").to_string();
|
||||
ui.horizontal_top(|ui| {
|
||||
ui.set_min_width(width * 2.0 + 10.0);
|
||||
ui.allocate_ui_with_layout(vec2(width, 300.0), egui::Layout::top_down(egui::Align::Min), |ui| {
|
||||
theme_card(ui, "Light Theme", light_active, &mut light, &[("studioLight", ThemeKind::StudioLight), ("classic", ThemeKind::Classic), ("adwaita", ThemeKind::Adwaita)], width);
|
||||
});
|
||||
ui.add_space(10.0);
|
||||
ui.allocate_ui_with_layout(vec2(width, 300.0), egui::Layout::top_down(egui::Align::Min), |ui| {
|
||||
theme_card(
|
||||
ui,
|
||||
"Dark Theme",
|
||||
!light_active,
|
||||
&mut dark,
|
||||
&[
|
||||
("proMedium", ThemeKind::ProMedium),
|
||||
("pro", ThemeKind::Pro),
|
||||
("studio", ThemeKind::Studio),
|
||||
("solarizedDark", ThemeKind::SolarizedDark),
|
||||
("adwaitaDark", ThemeKind::AdwaitaDark),
|
||||
],
|
||||
width,
|
||||
);
|
||||
});
|
||||
});
|
||||
obj.insert("lightTheme".into(), json!(light));
|
||||
obj.insert("darkTheme".into(), json!(dark));
|
||||
ui.add_space(12.0);
|
||||
}
|
||||
|
||||
/// Generic editor for a section's fields: checkboxes, dropdowns for choices, colour swatches,
|
||||
/// number fields with the preference's range, text fields.
|
||||
fn section_fields(ui: &mut egui::Ui, section: &str, obj: &mut Map<String, Value>, order: &[String], lang: crate::i18n::Lang) {
|
||||
fn section_fields(ui: &mut egui::Ui, section: &str, obj: &mut Map<String, Value>, order: &[String], lang: crate::i18n::Lang, system: Option<egui::Theme>) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
if section == "interface" {
|
||||
appearance_rows(ui, obj, system);
|
||||
}
|
||||
if section == "performance" {
|
||||
rendering_mode_row(ui, obj);
|
||||
}
|
||||
@@ -1092,7 +1222,8 @@ fn section_fields(ui: &mut egui::Ui, section: &str, obj: &mut Map<String, Value>
|
||||
let path = format!("{section}.{k}");
|
||||
// Settings nothing reads yet stay out of the dialog (issue #204); their stored values
|
||||
// pass through untouched.
|
||||
if prefs::is_hidden(&path)
|
||||
if (section == "interface" && matches!(k.as_str(), "theme" | "appearanceMode" | "darkTheme" | "lightTheme"))
|
||||
|| prefs::is_hidden(&path)
|
||||
|| (section == "performance" && matches!(k.as_str(), "useGpu" | "gpuBackend" | "renderingMode"))
|
||||
|| (section == "export" && !export_field_visible(obj, &k))
|
||||
{
|
||||
@@ -1640,6 +1771,48 @@ mod tests {
|
||||
use super::*;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[test]
|
||||
fn appearance_resolves_both_saved_themes_and_follows_system() {
|
||||
let mut interface = prefs::Interface { appearance_mode: AppearanceMode::Auto, ..prefs::Interface::default() };
|
||||
assert_eq!(selected_theme(&interface, Some(egui::Theme::Dark)), ThemeKind::ProMedium);
|
||||
assert_eq!(selected_theme(&interface, Some(egui::Theme::Light)), ThemeKind::StudioLight);
|
||||
interface.dark_theme = DarkTheme::Studio;
|
||||
interface.light_theme = LightTheme::Classic;
|
||||
assert_eq!(selected_theme(&interface, Some(egui::Theme::Dark)), ThemeKind::Studio);
|
||||
assert_eq!(selected_theme(&interface, Some(egui::Theme::Light)), ThemeKind::Classic);
|
||||
interface.appearance_mode = AppearanceMode::Dark;
|
||||
assert_eq!(selected_theme(&interface, Some(egui::Theme::Light)), ThemeKind::Studio);
|
||||
interface.appearance_mode = AppearanceMode::Light;
|
||||
assert_eq!(selected_theme(&interface, Some(egui::Theme::Dark)), ThemeKind::Classic);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_uses_native_system_theme_when_egui_does_not_report_one() {
|
||||
let (mut app, _) = app_with_store();
|
||||
app.services.system_theme = Some(Box::new(|_| Some(egui::Theme::Light)));
|
||||
app.run("prefs.set", json!({"path": "interface.appearanceMode", "value": "auto"})).unwrap();
|
||||
app.run("prefs.set", json!({"path": "interface.appearanceMode", "value": "auto"})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
tick(&mut app, &ctx);
|
||||
assert_eq!(app.ui.theme, ThemeKind::StudioLight);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn appearance_button_cycles_modes_without_losing_theme_choices() {
|
||||
let ctx = egui::Context::default();
|
||||
let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
|
||||
app.run("prefs.set", json!({"values": {"interface.darkTheme": "studio", "interface.lightTheme": "classic"}})).unwrap();
|
||||
for (mode, visible) in
|
||||
[(AppearanceMode::Auto, ThemeKind::Studio), (AppearanceMode::Light, ThemeKind::Classic), (AppearanceMode::Dark, ThemeKind::Studio)]
|
||||
{
|
||||
cycle_appearance(&mut app, &ctx);
|
||||
assert_eq!(app.session.prefs().interface.appearance_mode, mode);
|
||||
assert_eq!(app.ui.theme, visible);
|
||||
assert_eq!(app.session.prefs().interface.dark_theme, DarkTheme::Studio);
|
||||
assert_eq!(app.session.prefs().interface.light_theme, LightTheme::Classic);
|
||||
}
|
||||
}
|
||||
|
||||
/// Every generated preference label, section title and choice label has an entry in each
|
||||
/// language that claims complete menus (Japanese, Traditional Chinese, ...).
|
||||
#[test]
|
||||
@@ -2050,7 +2223,8 @@ mod tests {
|
||||
assert!(h.get_by_label("Apply").accesskit_node().is_disabled());
|
||||
|
||||
let values = h.state_mut().ui.dialog_mut(id).unwrap().fields.get_mut("values").unwrap();
|
||||
values["interface"]["theme"] = json!("studioLight");
|
||||
values["interface"]["appearanceMode"] = json!("light");
|
||||
values["interface"]["lightTheme"] = json!("studioLight");
|
||||
values["interface"]["uiFontSize"] = json!("large");
|
||||
values["performance"]["historyStates"] = json!(12);
|
||||
h.run_steps(2);
|
||||
@@ -2068,7 +2242,8 @@ mod tests {
|
||||
assert!(h.get_by_label("Apply").accesskit_node().is_disabled());
|
||||
let saved: Value = serde_json::from_str(store.lock().unwrap().as_ref().unwrap()).unwrap();
|
||||
assert_eq!(saved["performance"]["historyStates"], 12);
|
||||
assert_eq!(saved["interface"]["theme"], "studioLight");
|
||||
assert_eq!(saved["interface"]["appearanceMode"], "light");
|
||||
assert_eq!(saved["interface"]["lightTheme"], "studioLight");
|
||||
assert_eq!(saved["interface"]["uiFontSize"], "large");
|
||||
|
||||
// Repeated Apply starts from the validated values, not the dialog's original snapshot.
|
||||
@@ -2140,7 +2315,7 @@ mod tests {
|
||||
use egui_kittest::{Harness, kittest::Queryable};
|
||||
let obj = json!({"showTooltips": true}).as_object().unwrap().clone();
|
||||
let mut h =
|
||||
Harness::new_ui_state(|ui, obj: &mut Map<String, Value>| section_fields(ui, "interface", obj, &[], crate::i18n::Lang::from_pref("en")), obj);
|
||||
Harness::new_ui_state(|ui, obj: &mut Map<String, Value>| section_fields(ui, "interface", obj, &[], crate::i18n::Lang::from_pref("en"), None), obj);
|
||||
h.run_steps(4);
|
||||
assert!(h.query_by_label("Applies at next launch.").is_none());
|
||||
h.get_by_label("Show tooltips").click();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Design system: themes, colour tokens, radii, typography.
|
||||
//!
|
||||
//! - **Studio** (default): near-black surfaces, rounded cards, Inter + JetBrains Mono, soft violet
|
||||
//! - **Studio**: near-black surfaces, rounded cards, Inter + JetBrains Mono, soft violet
|
||||
//! accent. Modelled on the look of modern pro editors (such as Photoshop 2025).
|
||||
//! - **Studio Light**: the same system on light surfaces.
|
||||
//! - **Classic**: a deliberately Windows-2000-era look (grey bevels, square corners, navy selection)
|
||||
|
||||
+2
-2
@@ -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, 15 not run, 7 not measurable yet (baseline 2026-10-05) |
|
||||
| [File compatibility](#file-compatibility) | 4 | 12 | 11 | io corpus: oracle ≥ 146/170, round trip ≥ 169/170; psd-tools corpus: oracle ≥ 236/309, round trip ≥ 307/309; photoshop oracles: oracle ≥ 133/256, round trip ≥ 258/256 |
|
||||
| [Tools](#tools) | 17 | 14 | 8 | |
|
||||
| [Workspace and UI](#workspace-and-ui) | 5 | 7 | 11 | settings that do nothing: 48 of 141 (target 0) |
|
||||
| [Workspace and UI](#workspace-and-ui) | 5 | 7 | 11 | settings that do nothing: 48 of 144 (target 0) |
|
||||
| [Type](#type) | 0 | 3 | 6 | |
|
||||
| [Automation](#automation) | 3 | 2 | 6 | 627 of 627 menu items live (100.0%) |
|
||||
| [Reliability](#reliability) | 5 | 0 | 2 | never-crash lints on 28 of 28 crates; panic_hunt present |
|
||||
@@ -172,7 +172,7 @@ Corpus floors (`crates/io/tests/corpus.rs`): the enforced minimums; the corpus t
|
||||
|
||||
## Workspace and UI
|
||||
|
||||
**Settings that do nothing: 48 of 141** (target 0, #204). Counted by the prefs audit (`xtask/src/scorecard.rs`): a `Preferences` field counts as read when code outside the prefs model (`prefs.rs` except its `impl Session` blocks; tests excluded; the schema-driven dialog never names a field) accesses `section.field`, or `.section` and `.field` in the same file, or calls a section method that reads `self.field`. It is a heuristic lower bound: a generic name can look read when it isn't.
|
||||
**Settings that do nothing: 48 of 144** (target 0, #204). Counted by the prefs audit (`xtask/src/scorecard.rs`): a `Preferences` field counts as read when code outside the prefs model (`prefs.rs` except its `impl Session` blocks; tests excluded; the schema-driven dialog never names a field) accesses `section.field`, or `.section` and `.field` in the same file, or calls a section method that reads `self.field`. It is a heuristic lower bound: a generic name can look read when it isn't.
|
||||
|
||||
Unread: `general.color_picker`, `general.beep_when_done`, `general.resize_image_during_place`, `general.always_create_smart_objects_when_placing`, `general.animated_zoom`, `general.zoom_resizes_windows`, `interface.dynamic_color_sliders`, `workspace.auto_collapse_icon_panels`, `workspace.auto_show_hidden_panels`, `workspace.open_documents_as_tabs`, `workspace.enable_floating_document_window_docking`, `workspace.enable_narrow_options_bar`, `tools.enable_flick_panning`, `tools.vary_round_brush_hardness_on_hud`, `tools.show_transformation_values`, `tools.double_click_layer_mask_launches_select_and_mask`, `file_handling.image_previews`, `file_handling.save_in_background`, `file_handling.ignore_exif_profile_tag`, `file_handling.maximize_psd_compatibility`, `performance.cache_levels`, `performance.legacy_compositing`, `scratch_disks.disks`, `cursors.brush_preview_color`, `units_and_rulers.type_units`, `units_and_rulers.column_width`, `units_and_rulers.gutter`, `units_and_rulers.print_resolution`, `plug_ins.show_extension_panels`, `plug_ins.allow_scripts_to_connect`, `plug_ins.generator_enabled`, `type_.smart_quotes`, `type_.missing_glyph_protection`, `type_.show_font_names_in_english`, `type_.text_engine`, `type_.font_preview`, `type_.recent_fonts`, `enhanced_controls.scrubby_slider_acceleration`, `enhanced_controls.touch_gestures`, `enhanced_controls.zoom_with_trackpad_pinch`, `raw_defaults.color_space`, `raw_defaults.bit_depth`, `raw_defaults.resolution`, `raw_defaults.sharpen_for`, `raw_defaults.open_as_smart_object`, `raw_defaults.apply_auto_tone`, `integrations.allow_agent_control`, `integrations.control_port`.
|
||||
|
||||
|
||||
+3
-2
@@ -4,7 +4,8 @@
|
||||
|
||||
| Theme | Intent |
|
||||
|---|---|
|
||||
| **Pro** (default) | Photoshop-style Spectrum dark: flat charcoal panels (#323232), dark tab strips, Spectrum blue accent (#378ef0), pill buttons, checkboxes, compact 12 px type |
|
||||
| Pro | Photoshop-style Spectrum dark: flat charcoal panels (#323232), dark tab strips, Spectrum blue accent (#378ef0), pill buttons, checkboxes, compact 12 px type |
|
||||
| Pro Medium | Photoshop-style medium-gray panels and a dark canvas |
|
||||
| Studio | Dark studio style: near-black, rounded cards, pill tabs, violet accent, toggles |
|
||||
| Studio Light | Studio on light surfaces |
|
||||
| Classic | Windows-2000 bevels, square corners, navy selection |
|
||||
@@ -12,7 +13,7 @@
|
||||
| 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 |
|
||||
|
||||
Switch themes with the sun icon, Window → Theme, or `ui.set {"theme":"classic"}` over the control channel.
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user