diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index 28bd17c7c..8a2fefbbd 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -4,7 +4,7 @@ Every asset PdfCraft includes, bundles or uses to build its published material, with its author, source and licence. The policy is in [AGENTS.md](AGENTS.md) §1. The machine-readable list, with SHA-256 hashes, is [ATTRIBUTION.toml](ATTRIBUTION.toml). Licence texts are kept beside the assets and summarised in [NOTICE](NOTICE). -## In this repository (264) +## In this repository (265) | Asset | Title | Author | Licence | Source | Used for | |---|---|---|---|---|---| @@ -272,6 +272,7 @@ Every asset PdfCraft includes, bundles or uses to build its published material, | `packaging/macos/dmg/background.svg` | PdfCraft macOS DMG window background | @XusBadia | MIT OR Apache-2.0 | Original work for this repository, derived from the PdfCraft app icon (assets/app-icon/pdfcraft.svg, referenced via an SVG image element, not copied) | Finder window of the macOS DMG; rendered to background.tiff by packaging/macos/dmg/generate.py | | `packaging/macos/dmg/background.tiff` | PdfCraft macOS DMG window background (rendered) | @XusBadia | MIT OR Apache-2.0 | Rendered from packaging/macos/dmg/background.svg by packaging/macos/dmg/generate.py | 1x + 2x HiDPI 16-colour palette TIFF copied into the macOS DMG by packaging/macos/package.sh; includes the app icon and the free LittleCMS built-in sRGB profile | | `crates/sign/tests/data/x509-rsa-sha1.pdf` | One-page PDF with a legacy adbe.x509.rsa_sha1 signature by the RSA test key | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: hand-written PDF objects; signature made with `openssl dgst -sha1 -sign` over its byte ranges (see crates/sign/tests/data/README.md) | Signature validation tests (crates/sign/tests/pdf.rs): the legacy adbe.x509.rsa_sha1 format | +| `assets/icons/monitor.svg` | Lucide icon "monitor" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/monitor.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | ## Compiled in through dependencies (6) diff --git a/ATTRIBUTION.toml b/ATTRIBUTION.toml index 723a36e21..055d858cb 100644 --- a/ATTRIBUTION.toml +++ b/ATTRIBUTION.toml @@ -19,7 +19,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Simplified Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "1cc32f906351568117e0bf64d67cc4aac364772b1fb1cb884bff4b552f7c545d" +sha256 = "eb5bd161938e18015a635d1079fd2b6e4abb77b2f50de2a89ee402cd7ed4020c" [[asset]] path = "crates/ui-egui/src/i18n/zh-hant.tsv" @@ -30,7 +30,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Traditional Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "472f3ebfa5e9831a92aabf469acab5cdb6ae8af6add95611f50f3b74c420575c" +sha256 = "e1a04a188a6b1a9bff4301916bd202bcd46718e22206f86598e923b791364c79" [[asset]] path = "crates/ui-egui/src/i18n/cs.tsv" @@ -52,7 +52,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Brazilian Portuguese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "e0ac6eaa707d1e52505d55f401a442af5674336596f27a4a271b48b91337551e" +sha256 = "d944c1ac98f8cc50de8878686c9145c7995945a567c7f7ebce3fe365fc37037c" [[asset]] path = "crates/ui-egui/src/i18n/es.tsv" @@ -63,7 +63,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Spanish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "db87ab4ba9b7cd2e8547b24cc334ba01af6dbb2a5b1fbb33dd51a8fb44157198" +sha256 = "ed63bb07b8cf1eea833572066a0dcb0cfc5ae2a54eac206b09661fb8743bedfe" [[asset]] path = "crates/ui-egui/src/i18n/fr.tsv" @@ -74,7 +74,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "French UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "6f1cd0b54493510fb22566b0659476f98f5937bffe9742de91092edc3053f68f" +sha256 = "7ae1cb58826e2eb7c2fd381173d232d7f4e5017f6ed557ddd65d4bacf2d69624" [[asset]] path = "crates/ui-egui/src/i18n/de.tsv" @@ -85,7 +85,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "German UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "d95feff5318f449d7a438a3100f4190d0d570afc1ba40b55ecebd0e7edfc3288" +sha256 = "d8b5578203cb43a175cc24bbfe1ae931717f01499a05c381823891f8b88a21e7" [[asset]] path = "crates/ui-egui/src/i18n/te.tsv" @@ -96,7 +96,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Telugu UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "41c9cc2664abb82149d88460b35494022dd72f8f10b074c7972154c556cf5e16" +sha256 = "926c6c6ca1c376b159779588d77c5d033e2ddab105ad97d097417c1b7c598690" [[asset]] path = "crates/ui-egui/src/i18n/it.tsv" @@ -107,7 +107,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Italian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "15736e8b0b22c34d151d3998fb342f0dc1e3d0ebda6baefd56c27581018ad07e" +sha256 = "3831aaeda30436a69ac35cc279726d4f1ba006013cf38e4d94304a7b492d1245" [[asset]] path = "crates/ui-egui/src/i18n/pl.tsv" @@ -118,7 +118,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Polish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "2e36c61bb681d1d357604254d7b38c86564f8b3b52d72382f050529ac224285e" +sha256 = "fefa6bc66dd019254fdd9aa9ad85a861d58f1e4c4ab55e01bf7dbacd7fd054e8" [[asset]] path = "crates/ui-egui/src/i18n/ru.tsv" @@ -129,7 +129,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Russian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "4b253cfe16540a7f2a24f50be28edf3ceef8a4e2db092ebecb97cc325be5a50a" +sha256 = "d82b74ebc99a82963d9f056643aa08b640115bbab0daf76569704b2984e28800" [[asset]] path = "crates/ui-egui/src/i18n/hu.tsv" @@ -140,7 +140,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Hungarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "1bebc7f5441412596124e46f304f446419e55cb8468e18f7ab52dc5562950484" +sha256 = "feb5ae147ab9647d5233a80d42c1e9a2e6c486813db9989969da75ea093b2c1b" [[asset]] path = "crates/ui-egui/src/i18n/bg.tsv" @@ -151,7 +151,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Bulgarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "3f8908a3577fa118300e47cbfe25b3d792775365cbfae0aa07e6178137d4beee" +sha256 = "706c0a4dfbbc6eda15ddde838ccf2ebac3f9af00d8a402c4a59450c660172e10" [[asset]] path = "crates/ui-egui/src/i18n/ja.tsv" @@ -162,7 +162,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Japanese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "0c522c8292bc55c2ba8065378202feda081f02f03029dfe06a9b65aba625c677" +sha256 = "27aa500c5b9daef63a0fdf3cbff60cc31242195f28100b5b20fd96c88b838765" [[asset]] path = "crates/ui-egui/src/i18n/uk.tsv" @@ -173,7 +173,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Ukrainian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "9cc716897f6d3e6a4da91eaadb0aff82117d5c6145575c5f33fb943b557fffe2" +sha256 = "2ded225e2bc1cb210c90a1e76ca4ec4a5613b0f4ac1ed3a3c1cbe1fafeccb34d" [[asset]] path = "crates/ui-egui/src/i18n/ar.tsv" @@ -184,7 +184,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Arabic UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "c6c85ed20d1135f0ff605d28e2fe0c9b0ecf03bde84753d184f2401965bc9bdb" +sha256 = "65ba0e77aec38b0717ff146a6aff1a6b40f14b4de1f3b216ba0babceb9075376" [[asset]] path = "assets/icons/accessibility.svg" @@ -3285,3 +3285,14 @@ licence_file = "LICENSE-MIT" kind = "document" usage = "Signature validation tests (crates/sign/tests/pdf.rs): the legacy adbe.x509.rsa_sha1 format" sha256 = "b77bfb21166f4bd043597e45a1ea28b9d9f4ba6df0142b3a0fc8b7f8e0fc111b" + +[[asset]] +path = "assets/icons/monitor.svg" +title = "Lucide icon \"monitor\"" +author = "Lucide Contributors (portions Cole Bemis, Feather)" +source = "https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/monitor.svg)" +licence = "ISC" +licence_file = "assets/icons/LICENSE-lucide.txt" +kind = "icon" +usage = "UI icon, embedded via crates/ui-egui/src/icon_data.rs" +sha256 = "d1a443233345724859de8e4fb48968ba91e885e12ddbd710701548bd91b39428" diff --git a/assets/icons/monitor.svg b/assets/icons/monitor.svg new file mode 100644 index 000000000..e869f2f27 --- /dev/null +++ b/assets/icons/monitor.svg @@ -0,0 +1,15 @@ + + + + + diff --git a/crates/ui-egui/src/appearance.rs b/crates/ui-egui/src/appearance.rs new file mode 100644 index 000000000..bb804314a --- /dev/null +++ b/crates/ui-egui/src/appearance.rs @@ -0,0 +1,181 @@ +//! Preferences › Appearance: the appearance mode (follow the system, light or dark) above a light +//! and a dark theme card, each with a small picture of PdfCraft in that theme. + +use egui::{Rect, RichText, pos2, vec2}; + +use crate::PdfCraftApp; +use crate::theme::{self, ThemeKind, ThemePreference, Tokens}; + +/// The themes each card offers. One of each today; a new theme is one more row here. +pub const LIGHT_THEMES: &[(ThemeKind, &str)] = &[(ThemeKind::Light, "Light gray")]; +pub const DARK_THEMES: &[(ThemeKind, &str)] = &[(ThemeKind::Dark, "Dark gray")]; + +fn mode_label(preference: ThemePreference) -> &'static str { + match preference { + ThemePreference::System => "Use system setting", + ThemePreference::Light => "Light", + ThemePreference::Dark => "Dark", + } +} + +fn mode_command(preference: ThemePreference) -> &'static str { + match preference { + ThemePreference::System => "view.theme.system", + ThemePreference::Light => "view.theme.light", + ThemePreference::Dark => "view.theme.dark", + } +} + +/// A miniature PdfCraft window in `kind`'s colours: title bar and tabs, the tool bar, the page +/// thumbnails, a page on the pasteboard and the right-hand panel. +fn preview(ui: &mut egui::Ui, kind: ThemeKind, width: f32) { + let p = Tokens::for_kind(kind); + let (rect, _) = ui.allocate_exact_size(vec2(width, 118.0), egui::Sense::hover()); + let painter = ui.painter_at(rect); + painter.rect_filled(rect, 3.0, p.chrome); + let title = Rect::from_min_size(rect.min, vec2(rect.width(), 12.0)); + painter.rect_filled(title, 0.0, p.titlebar); + // The active tab and the theme button. + painter.rect_filled(Rect::from_min_size(title.min + vec2(6.0, 3.0), vec2(36.0, 9.0)), 2.0, p.panel); + painter.rect_filled(Rect::from_min_size(title.min + vec2(9.0, 6.0), vec2(20.0, 2.0)), 1.0, p.text); + painter.circle_filled(pos2(title.right() - 7.0, title.center().y), 2.5, p.icon); + // The tool bar, with its first tool active. + let tools = Rect::from_min_size(pos2(rect.left(), title.bottom()), vec2(rect.width(), 12.0)); + painter.rect_filled(tools, 0.0, p.panel); + for i in 0..8 { + let c = if i == 0 { p.accent } else { p.icon }; + painter.rect_filled(Rect::from_min_size(tools.min + vec2(8.0 + i as f32 * 9.0, 3.5), vec2(5.0, 5.0)), 1.0, c); + } + let body = Rect::from_min_max(pos2(rect.left(), tools.bottom() + 1.0), rect.max); + // Page thumbnails on the left, the second one selected. + let rail = Rect::from_min_size(body.min, vec2((body.width() * 0.16).round(), body.height())); + painter.rect_filled(rail, 0.0, p.panel); + for i in 0..3 { + let thumb = Rect::from_min_size(rail.min + vec2(rail.width() * 0.25, 6.0 + i as f32 * 26.0), vec2(rail.width() * 0.5, 20.0)); + if i == 1 { + painter.rect_filled(thumb.expand(2.0), 2.0, p.accent_soft); + } + painter.rect_filled(thumb, 1.0, egui::Color32::WHITE); + painter.rect_stroke(thumb, 1.0, egui::Stroke::new(0.5, p.border), egui::StrokeKind::Outside); + } + // The right-hand panel: a heading and a few rows. + let side = Rect::from_min_max(pos2(body.right() - (body.width() * 0.22).round(), body.top()), body.max); + painter.rect_filled(side, 0.0, p.panel); + painter.rect_filled(Rect::from_min_size(side.min + vec2(5.0, 5.0), vec2(side.width() * 0.5, 2.5)), 1.0, p.text); + for i in 0..4 { + let y = side.top() + 14.0 + i as f32 * 8.0; + painter.rect_filled(Rect::from_min_size(pos2(side.left() + 5.0, y), vec2(side.width() * 0.7, 2.0)), 1.0, p.text_muted); + } + // The page, white in both themes, with a heading and lines of text. + let paste = Rect::from_min_max(pos2(rail.right(), body.top()), pos2(side.left(), body.bottom())); + painter.rect_filled(paste, 0.0, p.pasteboard); + let page_h = paste.height() - 10.0; + let page = Rect::from_center_size(paste.center(), vec2(page_h * 0.77, page_h)); + painter.rect_filled(page.translate(vec2(1.0, 1.0)), 1.0, p.page_shadow); + painter.rect_filled(page, 1.0, egui::Color32::WHITE); + let ink = egui::Color32::from_gray(0x60); + painter.rect_filled(Rect::from_min_size(page.min + vec2(6.0, 7.0), vec2(page.width() * 0.5, 3.0)), 1.0, egui::Color32::from_gray(0x30)); + for i in 0..8 { + let w = if i % 4 == 3 { 0.45 } else { 0.8 }; + painter.rect_filled(Rect::from_min_size(page.min + vec2(6.0, 15.0 + i as f32 * 6.0), vec2(page.width() * w - 6.0, 1.5)), 0.5, ink); + } +} + +/// One theme card: its title, Active when it is what shows now, a preview of the chosen theme +/// and a radio row per theme. +fn card(ui: &mut egui::Ui, t: &Tokens, title: &str, active: bool, themes: &[(ThemeKind, &str)]) { + egui::Frame::new() + .fill(t.card) + .stroke(egui::Stroke::new(1.0, if active { t.accent } else { t.border })) + .corner_radius(6.0) + .inner_margin(egui::Margin::same(8)) + .show(ui, |ui| { + ui.set_width(ui.available_width()); + ui.horizontal(|ui| { + ui.label(RichText::new(title).font(theme::semibold(12.5)).color(t.text)); + if active { + ui.label(RichText::new(tl!("Active")).color(t.accent_text).small()); + } + }); + ui.add_space(4.0); + let shown = themes.first().map_or(ThemeKind::Light, |(kind, _)| *kind); + preview(ui, shown, ui.available_width()); + ui.add_space(4.0); + for (kind, label) in themes { + // One theme per card today, so it is always the chosen one. + let _ = ui.radio(*kind == shown, tl!(label)); + } + }); +} + +/// The Appearance section of Preferences. +pub fn preferences_section(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) { + ui.label(RichText::new(tl!("Appearance")).font(theme::semibold(13.0))); + ui.horizontal(|ui| { + ui.label(tl!("Appearance mode")); + egui::ComboBox::from_id_salt("appearance-mode").selected_text(tl!(mode_label(app.theme_preference))).show_ui(ui, |ui| { + for preference in [ThemePreference::System, ThemePreference::Light, ThemePreference::Dark] { + if ui.selectable_label(app.theme_preference == preference, tl!(mode_label(preference))).clicked() { + app.execute(mode_command(preference)); + ui.close(); + } + } + }); + }); + ui.add_space(4.0); + let light_active = app.theme == ThemeKind::Light; + ui.columns(2, |cols| { + let [light, dark] = cols else { return }; + card(light, t, tl!("Light theme"), light_active, LIGHT_THEMES); + card(dark, t, tl!("Dark theme"), !light_active, DARK_THEMES); + }); +} + +#[cfg(test)] +mod tests { + use egui_kittest::Harness; + use egui_kittest::kittest::Queryable; + + use super::*; + + /// The mode is chosen in Preferences and both theme cards show, the one on screen marked + /// Active. + #[test] + fn preferences_choose_the_appearance_mode_and_show_both_themes() { + let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); + let mut fonts = false; + let mut h = Harness::builder().with_size(egui::vec2(640.0, 400.0)).build_ui_state( + move |ui, app: &mut PdfCraftApp| { + // The app's fonts (the semibold headings) take effect from the next frame. + if !fonts { + ui.ctx().set_fonts(theme::font_definitions()); + fonts = true; + return; + } + let t = Tokens::get(ui.ctx()); + preferences_section(ui, app, &t); + }, + app, + ); + h.run(); + assert!(h.query_by_label("Light theme").is_some() && h.query_by_label("Dark theme").is_some()); + assert!(h.query_by_label("Light gray").is_some() && h.query_by_label("Dark gray").is_some()); + assert_eq!(h.query_all_by_label("Active").count(), 1); + if let Ok(path) = std::env::var("PDFCRAFT_APPEARANCE_SHOT") { + h.render().unwrap().save(path).unwrap(); + } + + let combo = egui::accesskit::Role::ComboBox; + h.get_by_role(combo).click(); + h.run(); + h.get_by_label("Dark").click(); + h.run(); + assert_eq!(h.state().theme_preference, ThemePreference::Dark); + h.get_by_role(combo).click(); + h.run(); + h.get_by_label("Use system setting").click(); + h.run(); + assert_eq!(h.state().theme_preference, ThemePreference::System); + } +} diff --git a/crates/ui-egui/src/chrome.rs b/crates/ui-egui/src/chrome.rs index 2f9260530..4903b2226 100644 --- a/crates/ui-egui/src/chrome.rs +++ b/crates/ui-egui/src/chrome.rs @@ -172,13 +172,21 @@ pub fn tab_strip(app: &mut PdfCraftApp, ui: &mut egui::Ui) { } ui.with_layout(Layout::right_to_left(Align::Center), |ui| { let (icon, label) = match app.theme_preference { - ThemePreference::System => ("settings", tl!("Use system setting")), + ThemePreference::System => ("monitor", tl!("Use system setting")), ThemePreference::Light => ("sun", tl!("Light gray")), ThemePreference::Dark => ("moon", tl!("Dark gray")), }; let tip = format!("{}: {label}", tl!("Display theme")); let response = icons::button(ui, icon, button, false, &tip); - egui::Popup::menu(&response).show(|ui| theme_menu(app, ui)); + // A click cycles System, Light, Dark; a right-click opens the menu. + if response.clicked() { + app.execute(match app.theme_preference { + ThemePreference::System => "view.theme.light", + ThemePreference::Light => "view.theme.dark", + ThemePreference::Dark => "view.theme.system", + }); + } + egui::Popup::context_menu(&response).id(response.id.with("theme_menu")).show(|ui| appearance_menu(app, ui)); if icons::button(ui, "circle-help", button, false, tl!("Keyboard shortcuts")).clicked() { app.dialog = Some(Dialog::Shortcuts); } @@ -203,6 +211,19 @@ fn theme_menu(app: &mut PdfCraftApp, ui: &mut egui::Ui) { ui.close(); } } + ui.separator(); + if ui.button(tl!("Appearance settings…")).clicked() { + app.dialog = Some(Dialog::Preferences); + ui.close(); + } +} + +/// The right-click menu on the title bar's theme button. +fn appearance_menu(app: &mut PdfCraftApp, ui: &mut egui::Ui) { + if ui.button(tl!("Appearance settings…")).clicked() { + app.dialog = Some(Dialog::Preferences); + ui.close(); + } } /// A tab's chrome around its name: the icon on the left, the close button on the right. diff --git a/crates/ui-egui/src/i18n/ar.tsv b/crates/ui-egui/src/i18n/ar.tsv index bf23b8f7c..af263f9ba 100644 --- a/crates/ui-egui/src/i18n/ar.tsv +++ b/crates/ui-egui/src/i18n/ar.tsv @@ -111,6 +111,12 @@ Open a PDF ({key}) فتح ملف PDF ({key}) Dark gray theme سمة رمادية داكنة Light theme سمة فاتحة + Appearance mode وضع المظهر + Light فاتح + Dark داكن + Active نشط + Dark theme سمة داكنة + Appearance settings… إعدادات المظهر… All tools كل الأدوات Read قراءة Convert تحويل diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index a1501789d..b65e05f19 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -1090,6 +1090,12 @@ Open a PDF ({key}) Отвори PDF файл ({key}) Dark gray theme Тъмносива тема Light theme Светла тема + Appearance mode Режим на облика + Light Светъл + Dark Тъмен + Active Активен + Dark theme Тъмна тема + Appearance settings… Настройки на облика… Print ({key}) Печат ({key}) Save ({key}) Запиши ({key}) Document properties ({key}) Свойства на документа ({key}) diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index cc8b954d9..26dde0296 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -83,6 +83,12 @@ Open a PDF ({key}) PDF öffnen ({key}) Dark gray theme Dunkelgraues Design Light theme Helles Design + Appearance mode Darstellungsmodus + Light Hell + Dark Dunkel + Active Aktiv + Dark theme Dunkles Design + Appearance settings… Darstellungseinstellungen… All tools Alle Werkzeuge Read Lesen Convert Konvertieren diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 74bcde928..c57a9619a 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -104,6 +104,12 @@ Open a PDF ({key}) Abrir un PDF ({key}) Dark gray theme Tema gris oscuro Light theme Tema claro + Appearance mode Modo de apariencia + Light Claro + Dark Oscuro + Active Activo + Dark theme Tema oscuro + Appearance settings… Ajustes de apariencia… All tools Todas las herramientas Read Leer Convert Convertir diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index ab1aa5e12..d41495905 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -83,6 +83,12 @@ Open a PDF ({key}) Ouvrir un PDF ({key}) Dark gray theme Thème gris foncé Light theme Thème clair + Appearance mode Mode d'apparence + Light Clair + Dark Sombre + Active Actif + Dark theme Thème sombre + Appearance settings… Réglages d'apparence… All tools Tous les outils Read Lire Convert Convertir diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index c7f4456aa..308cdb551 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -81,6 +81,12 @@ Open a PDF ({key}) PDF megnyitása ({key}) Dark gray theme Sötétszürke téma Light theme Világos téma + Appearance mode Megjelenési mód + Light Világos + Dark Sötét + Active Aktív + Dark theme Sötét téma + Appearance settings… Megjelenési beállítások… All tools Minden eszköz Read Olvasás Convert Konvertálás diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 8e29b80a8..a85924560 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -107,6 +107,12 @@ Open a PDF ({key}) Apri un PDF ({key}) Dark gray theme Tema grigio scuro Light theme Tema chiaro + Appearance mode Modalità aspetto + Light Chiaro + Dark Scuro + Active Attivo + Dark theme Tema scuro + Appearance settings… Impostazioni aspetto… All tools Tutti gli strumenti Read Lettura Convert Converti diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 31a33d33b..a07b7cbba 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -110,6 +110,12 @@ Open a PDF ({key}) PDF を開く({key}) Dark gray theme ダークグレーのテーマ Light theme ライトテーマ + Appearance mode 外観モード + Light ライト + Dark ダーク + Active 使用中 + Dark theme ダークテーマ + Appearance settings… 外観の設定… All tools すべてのツール Read 閲覧 Convert 変換 diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 407282647..97d0cc68e 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -106,6 +106,12 @@ Open a PDF (⌘O) Otwórz PDF (⌘O) Dark gray theme Ciemnoszary motyw Light theme Jasny motyw + Appearance mode Tryb wyglądu + Light Jasny + Dark Ciemny + Active Aktywny + Dark theme Ciemny motyw + Appearance settings… Ustawienia wyglądu… All tools Wszystkie narzędzia Read Czytaj Convert Konwertuj diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 59290d0a4..aed11c7e1 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -1032,6 +1032,12 @@ Open a PDF ({key}) Abrir um PDF ({key}) Dark gray theme Tema cinza escuro Light theme Tema claro + Appearance mode Modo de aparência + Light Claro + Dark Escuro + Active Ativo + Dark theme Tema escuro + Appearance settings… Configurações de aparência… Print ({key}) Imprimir ({key}) Save ({key}) Salvar ({key}) Document properties ({key}) Propriedades do documento ({key}) diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 448c0f96b..c4423b261 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -104,6 +104,12 @@ Open a PDF ({key}) Открыть PDF ({key}) Dark gray theme Тёмно-серая тема Light theme Светлая тема + Appearance mode Режим оформления + Light Светлое + Dark Тёмное + Active Активно + Dark theme Тёмная тема + Appearance settings… Настройки оформления… All tools Все инструменты Read Чтение Convert Конвертация diff --git a/crates/ui-egui/src/i18n/te.tsv b/crates/ui-egui/src/i18n/te.tsv index 16d1de389..3881fd303 100644 --- a/crates/ui-egui/src/i18n/te.tsv +++ b/crates/ui-egui/src/i18n/te.tsv @@ -87,6 +87,12 @@ comment menu Edit సవరించు Open a PDF ({key}) PDFను తెరువు ({key}) Dark gray theme ముదురు బూడిద రంగు థీమ్ Light theme లేత థీమ్ + Appearance mode రూపం మోడ్ + Light లైట్ + Dark డార్క్ + Active సక్రియం + Dark theme డార్క్ థీమ్ + Appearance settings… రూపం సెట్టింగ్‌లు… All tools అన్ని సాధనాలు Read చదవడం Convert మార్పిడి diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 779fc7327..7105fa265 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -109,6 +109,12 @@ Open a PDF ({key}) Відкрити PDF ({key}) Dark gray theme Темно-сіра тема Light theme Світла тема + Appearance mode Режим вигляду + Light Світлий + Dark Темний + Active Активний + Dark theme Темна тема + Appearance settings… Налаштування вигляду… All tools Усі інструменти Read Читання Convert Перетворення diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 7bc74bee9..cd614f27a 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -1097,6 +1097,12 @@ Open a PDF ({key}) 打开 PDF ({key}) Dark gray theme 深灰色主题 Light theme 浅色主题 + Appearance mode 外观模式 + Light 浅色 + Dark 深色 + Active 当前 + Dark theme 深色主题 + Appearance settings… 外观设置… Print ({key}) 打印 ({key}) Save ({key}) 保存 ({key}) Document properties ({key}) 文档属性 ({key}) diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index d22baa78a..b1487d57e 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -1045,6 +1045,12 @@ Open a PDF ({key}) 開啟 PDF({key}) Dark gray theme 深灰色主題 Light theme 淺色主題 + Appearance mode 外觀模式 + Light 淺色 + Dark 深色 + Active 使用中 + Dark theme 深色主題 + Appearance settings… 外觀設定… Print ({key}) 列印({key}) Save ({key}) 儲存({key}) Document properties ({key}) 文件內容({key}) diff --git a/crates/ui-egui/src/icon_data.rs b/crates/ui-egui/src/icon_data.rs index 7341b5775..6e3f26f57 100644 --- a/crates/ui-egui/src/icon_data.rs +++ b/crates/ui-egui/src/icon_data.rs @@ -118,6 +118,7 @@ pub static ICONS: &[(&str, &[u8])] = &[ ("minimize", include_bytes!("../../../assets/icons/minimize.svg")), ("minimize-2", include_bytes!("../../../assets/icons/minimize-2.svg")), ("minus", include_bytes!("../../../assets/icons/minus.svg")), + ("monitor", include_bytes!("../../../assets/icons/monitor.svg")), ("moon", include_bytes!("../../../assets/icons/moon.svg")), ("mouse-pointer-2", include_bytes!("../../../assets/icons/mouse-pointer-2.svg")), ("move", include_bytes!("../../../assets/icons/move.svg")), diff --git a/crates/ui-egui/src/icons.rs b/crates/ui-egui/src/icons.rs index 9fcafd1c7..7cba05c55 100644 --- a/crates/ui-egui/src/icons.rs +++ b/crates/ui-egui/src/icons.rs @@ -75,7 +75,7 @@ mod tests { #[test] fn shared_catalog_preserves_existing_icons_and_fallback_uri() { - assert_eq!(ICONS.len(), 185); + assert_eq!(ICONS.len(), 186); assert!(exists("minimize-2")); for (name, original) in ICONS { let expected = diff --git a/crates/ui-egui/src/js_ui.rs b/crates/ui-egui/src/js_ui.rs index 545c3b013..0aa5eca60 100644 --- a/crates/ui-egui/src/js_ui.rs +++ b/crates/ui-egui/src/js_ui.rs @@ -230,7 +230,7 @@ pub(crate) fn document_js_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tok close } -/// Preferences: interface language, identity and JavaScript. Returns `true` to close. +/// Preferences: interface language and size, appearance, identity and JavaScript. Returns `true` to close. pub(crate) fn preferences_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> bool { ui.label(egui::RichText::new(tl!("Preferences")).font(theme::semibold(18.0))); ui.horizontal(|ui| { @@ -277,6 +277,8 @@ pub(crate) fn preferences_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tok .color(t.text_muted), ); ui.add_space(8.0); + crate::appearance::preferences_section(ui, app, t); + ui.add_space(8.0); ui.label(egui::RichText::new(tl!("Documents and view")).font(theme::semibold(13.0))); ui.horizontal(|ui| { ui.label(tl!("Default workspace mode")); diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 77145db54..c6886a9f3 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -24,6 +24,7 @@ macro_rules! tl_ctx { mod a11y_ui; mod actions_ui; +mod appearance; pub mod canvas; mod chrome; mod combine_grid; diff --git a/crates/ui-egui/tests/theme.rs b/crates/ui-egui/tests/theme.rs index 2b16796f4..1d738610b 100644 --- a/crates/ui-egui/tests/theme.rs +++ b/crates/ui-egui/tests/theme.rs @@ -24,12 +24,10 @@ fn assert_theme(h: &Harness<'static, PdfCraftApp>, preference: ThemePreference, assert_eq!(h.ctx.theme(), if kind == ThemeKind::Dark { egui::Theme::Dark } else { egui::Theme::Light }); } -fn open_toolbar(h: &mut Harness<'static, PdfCraftApp>, label: &str) { +/// A click on the title bar's theme button cycles Light, Dark, System. +fn click_toolbar(h: &mut Harness<'static, PdfCraftApp>, label: &str) { h.get_by_label(&format!("Display theme: {label}")).click(); h.run_steps(3); - for choice in ["Use system setting", "Light gray", "Dark gray"] { - h.get_by_label(choice); - } } fn open_view_menu(h: &mut Harness<'static, PdfCraftApp>) { @@ -50,18 +48,16 @@ fn toolbar_and_view_menu_share_three_state_preferences() { let mut h = harness(); // Explicit light wins even on a dark desktop, including egui's popup style. assert_theme(&h, ThemePreference::Light, ThemeKind::Light); - open_toolbar(&mut h, "Light gray"); - h.get_by_label("Use system setting").click(); - h.run_steps(3); + click_toolbar(&mut h, "Light gray"); + assert_theme(&h, ThemePreference::Dark, ThemeKind::Dark); + click_toolbar(&mut h, "Dark gray"); assert_theme(&h, ThemePreference::System, ThemeKind::Dark); open_view_menu(&mut h); h.get_by_label("Light gray").click(); h.run_steps(3); assert_theme(&h, ThemePreference::Light, ThemeKind::Light); - open_toolbar(&mut h, "Light gray"); - h.get_by_label("Dark gray").click(); - h.run_steps(3); + click_toolbar(&mut h, "Light gray"); assert_theme(&h, ThemePreference::Dark, ThemeKind::Dark); open_view_menu(&mut h); @@ -71,9 +67,8 @@ fn toolbar_and_view_menu_share_three_state_preferences() { h.input_mut().system_theme = Some(egui::Theme::Light); h.run_steps(3); assert_theme(&h, ThemePreference::System, ThemeKind::Light); - open_toolbar(&mut h, "Use system setting"); - h.get_by_label("Dark gray").click(); - h.run_steps(3); + click_toolbar(&mut h, "Use system setting"); + click_toolbar(&mut h, "Light gray"); // A manual choice exits system mode and survives subsequent OS changes. h.input_mut().system_theme = Some(egui::Theme::Dark); h.run_steps(2); diff --git a/crates/ui-egui/tests/theme_button.rs b/crates/ui-egui/tests/theme_button.rs new file mode 100644 index 000000000..14f34086c --- /dev/null +++ b/crates/ui-egui/tests/theme_button.rs @@ -0,0 +1,41 @@ +//! The title bar's theme button: a click cycles System, Light and Dark; a right-click opens the +//! menu with the appearance settings. + +use egui_kittest::Harness; +use egui_kittest::kittest::Queryable; +use pdfcraft_ui_egui::PdfCraftApp; + +fn harness() -> Harness<'static, PdfCraftApp> { + let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| { + let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); + app + }); + h.run_steps(6); + h +} + +fn preference(h: &Harness<'static, PdfCraftApp>) -> String { + format!("{:?}", h.state().theme_preference) +} + +#[test] +fn a_click_cycles_the_theme_and_a_right_click_opens_the_menu() { + let mut h = harness(); + assert_eq!(preference(&h), "Light", "new installs start Light"); + for (label, next) in + [("Display theme: Light gray", "Dark"), ("Display theme: Dark gray", "System"), ("Display theme: Use system setting", "Light")] + { + h.get_by_label(label).click(); + h.run_steps(4); + assert_eq!(preference(&h), next); + } + assert!(h.query_by_label("Appearance settings…").is_none(), "a plain click opens no menu"); + + h.get_by_label("Display theme: Light gray").click_secondary(); + h.run_steps(4); + h.get_by_label("Appearance settings…").click(); + h.run_steps(4); + h.run_steps(10); + assert!(h.query_by_label("Light theme").is_some(), "Preferences opens on Appearance"); +}