diff --git a/apps/gridcraft/src/main.rs b/apps/gridcraft/src/main.rs index 4d6faaa2c..961a352e2 100644 --- a/apps/gridcraft/src/main.rs +++ b/apps/gridcraft/src/main.rs @@ -97,6 +97,8 @@ fn load_prefs(app: &mut SheetApp) { } // First run (or a ui.json without a usable language): follow the desktop's language. app.ui.language = gridcraft_ui_egui::i18n::Language::system(); + // ...and its light or dark appearance (#239), until a theme is chosen and saved in ui.json. + app.ui.system_theme = true; let Some(dir) = config_dir() else { return }; if let Ok(b) = std::fs::read(dir.join("ui.json")) && let Ok(ui) = serde_json::from_slice::(&b) diff --git a/crates/ui-egui/src/icons.rs b/crates/ui-egui/src/icons.rs index b2079c821..87bde2d52 100644 --- a/crates/ui-egui/src/icons.rs +++ b/crates/ui-egui/src/icons.rs @@ -108,6 +108,8 @@ pub enum Icon { Share, Search, Settings, + /// Light/dark appearance: a circle, its right half filled. + Appearance, Sheet, Book, Folder, @@ -596,6 +598,16 @@ pub fn paint(p: &Painter, rect: Rect, icon: Icon, c: Color32) { g.circle(8.0, 8.0, 5.0, None, Some(c)); g.circle(8.0, 8.0, 2.0, Some(c), None); } + Icon::Appearance => { + g.circle(8.0, 8.0, 5.5, None, Some(c)); + let half: Vec<(f32, f32)> = (0..=12) + .map(|i| { + let a = std::f32::consts::PI * (i as f32 / 12.0 - 0.5); + (8.0 + 5.5 * a.cos(), 8.0 + 5.5 * a.sin()) + }) + .collect(); + g.poly(&half, c); + } Icon::Book => { g.rect(2.0, 2.0, 12.0, 12.0, Some(GREEN), None); g.rect(4.0, 4.0, 8.0, 8.0, Some(Color32::WHITE), None); diff --git a/crates/ui-egui/src/ribbon.rs b/crates/ui-egui/src/ribbon.rs index d7566f397..209bf766a 100644 --- a/crates/ui-egui/src/ribbon.rs +++ b/crates/ui-egui/src/ribbon.rs @@ -135,6 +135,16 @@ pub fn title_bar(app: &mut SheetApp, ui: &mut Ui) { if icon_button(ui, Icon::Comment, t.text_dim, "Comments", vec2(28.0, ch)).clicked() { app.open_dialog("comments", json!({})); } + // Quick light/dark switch (#194); right-click offers System as well. The same + // choice lives in File › Display theme and the View tab. + let lang = app.ui.language; + let theme = icon_button(ui, Icon::Appearance, t.text_dim, "Dark Mode", vec2(28.0, 26.0)); + let dark = ui.ctx().theme() == egui::Theme::Dark; + theme.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::Checkbox, true, dark, lang.tr("Dark Mode"))); + if theme.clicked() { + act(app, "view.darkMode", json!({})); + } + theme.context_menu(|ui| theme_choices(app, ui)); }); }); }); @@ -252,9 +262,24 @@ fn file_menu(app: &mut SheetApp, ui: &mut Ui) { ui.close(); } } + // Excel keeps the theme under File › Account › Office Theme (#194). + ui.separator(); + ui.menu_button(app.ui.language.tr("Display theme"), |ui| theme_choices(app, ui)); }); } +/// System / Light / Dark, as in the View tab's Display theme box (`view.theme`). +fn theme_choices(app: &mut SheetApp, ui: &mut Ui) { + let lang = app.ui.language; + let mode = app.ui.theme_mode(); + for (value, label) in [("system", "System"), ("light", "Light"), ("dark", "Dark")] { + if ui.radio(mode == value, lang.tr(label)).clicked() { + act(app, "view.theme", json!({"mode": value})); + ui.close(); + } + } +} + fn contextual_tabs(app: &SheetApp) -> Vec<&'static str> { let mut v = Vec::new(); if let Some(d) = app.session.active() diff --git a/crates/ui-egui/tests/system_theme.rs b/crates/ui-egui/tests/system_theme.rs index 0d2cdea32..a0a6b007d 100644 --- a/crates/ui-egui/tests/system_theme.rs +++ b/crates/ui-egui/tests/system_theme.rs @@ -162,3 +162,29 @@ fn invalid_mode_preserves_preferences_and_legacy_overrides_leave_system_mode() { system_theme(&mut h, Some(Theme::Dark)); assert_appearance(&h, false); } + +#[test] +fn title_bar_switch_and_file_menu_choose_the_theme() { + // #194: one click in the title bar switches light/dark; File › Display theme offers all three. + let mut h = harness(UiState::default()); + system_theme(&mut h, Some(Theme::Light)); + assert_appearance(&h, false); + h.get_by_role_and_label(egui::accesskit::Role::CheckBox, "Dark Mode").click(); + h.run_steps(2); + assert_eq!(preferences(&h)["dark"], true); + assert_eq!(preferences(&h)["systemTheme"], false); + assert_appearance(&h, true); + h.get_by_role_and_label(egui::accesskit::Role::CheckBox, "Dark Mode").click(); + h.run_steps(2); + assert_appearance(&h, false); + + h.get_by_label("File").click(); + h.run_steps(3); + h.get_by_label("Display theme ⏵").click(); + h.run_steps(3); + h.get_by_label("System").click(); + h.run_steps(3); + assert_eq!(preferences(&h)["systemTheme"], true); + system_theme(&mut h, Some(Theme::Dark)); + assert_appearance(&h, true); +} diff --git a/docs/ui-parity.md b/docs/ui-parity.md index ed4cd1f1e..f06c8c526 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -23,7 +23,7 @@ Sources: `crates/ui-egui/src` (ribbon, grid, editor, formula bar, dialogs, panes | Status bar: Sum/Count/Average and more, view buttons, zoom | yes | yes | customizable statistics partial | | Task panes (Format Chart, Comments, Watch, Selection, PivotTable fields) | yes | yes | element-level Format pane for shapes and pictures missing | | Backstage (File) view: Info, New with templates, Open recent, Export, Options | yes | partial | Options dialog and template gallery missing | -| Dark mode, system appearance | yes | yes | done (10-09) | +| Dark mode, system appearance | yes | yes | done (10-09); follows the system on first run, title-bar switch and File › Display theme (#194, #239) | | Page Layout view (paper view with rulers and header editing) | yes | **no** (Page Break Preview only) | 8–12 h | | Multiple windows per workbook | yes | no (#162) | see [hardware-parity.md](hardware-parity.md) | @@ -94,5 +94,6 @@ scrollbar bugs 2–4 · pixel/feel pass against observed Excel 7–12. |---|---|---| | 2026-10-11 | minor | Delete and Backspace now remove the selected chart, picture or shape (#213) | | 2026-10-10 | minor | Keep formula helpers inside the worksheet when scrolling (#200) | +| 2026-10-11 | trivial | Theme: title-bar light/dark switch, File › Display theme, system appearance on first run (#194, #239) | | 2026-10-10 | minor | Chart resize bug (#190) | | 2026-10-10 | major | Created: chrome, grid, keyboard, dialogs, accessibility; key tips, Page Layout view and accessibility tree flagged |