From 9135c7f6f013de8d1608c39e5d65f1c96815255a Mon Sep 17 00:00:00 2001 From: Marco Arnulfo Date: Fri, 23 Oct 2026 17:07:42 +0200 Subject: [PATCH 1/3] Ribbon: fall back to Home when the active contextual tab is gone (fixes #2329) --- crates/ui-egui/src/ribbon.rs | 7 ++ crates/ui-egui/tests/contextual_ribbon_tab.rs | 74 +++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 crates/ui-egui/tests/contextual_ribbon_tab.rs diff --git a/crates/ui-egui/src/ribbon.rs b/crates/ui-egui/src/ribbon.rs index 356381d66..45856565f 100644 --- a/crates/ui-egui/src/ribbon.rs +++ b/crates/ui-egui/src/ribbon.rs @@ -204,6 +204,11 @@ pub fn show(app: &mut SheetApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); let collapsed = app.ui.ribbon_collapsed; let h = if collapsed { 34.0 } else { 34.0 + 92.0 }; + // A contextual tab exists only while its object is selected (Excel hides Table Design once the + // cell leaves the table); a remembered one that is gone would draw an empty ribbon. + if CONTEXTUAL_TABS.contains(&app.ui.ribbon_tab.as_str()) && !contextual_tabs(app).contains(&app.ui.ribbon_tab.as_str()) { + app.ui.ribbon_tab = "Home".to_string(); + } egui::Panel::top("ribbon") .exact_size(h) .frame(egui::Frame::NONE.fill(t.window).inner_margin(egui::Margin { left: 10, right: 10, top: 0, bottom: 6 })) @@ -322,6 +327,8 @@ fn theme_choices(app: &mut SheetApp, ui: &mut Ui) { } } +const CONTEXTUAL_TABS: [&str; 3] = ["Table Design", "Chart Design", crate::paper_view::TAB]; + 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/contextual_ribbon_tab.rs b/crates/ui-egui/tests/contextual_ribbon_tab.rs new file mode 100644 index 000000000..6e1626dd5 --- /dev/null +++ b/crates/ui-egui/tests/contextual_ribbon_tab.rs @@ -0,0 +1,74 @@ +//! A contextual ribbon tab (Table Design, Chart Design) only exists while its object is selected. +//! The tab remembered in the saved UI state, or left behind when the selection moves away, must +//! not leave the ribbon blank: it falls back to Home. + +use egui::vec2; +use gridcraft_engine::Session; +use gridcraft_ui_egui::SheetApp; +use serde_json::json; + +type Harness = egui_kittest::Harness<'static, SheetApp>; + +fn harness(session: Session) -> Harness { + let mut h = egui_kittest::Harness::builder().with_size(vec2(1200.0, 800.0)).build_ui_state( + |ui, app: &mut SheetApp| { + let ctx = ui.ctx().clone(); + app.logic(&ctx); + app.ui(ui); + }, + SheetApp::new(session, Default::default()), + ); + h.run_steps(4); + h +} + +fn session_with_table() -> Session { + let mut session = Session::new(); + session.new_workbook(); + session.execute("range.setValues", json!({"range": "A1", "values": [["Name", "Qty"], ["a", 1], ["b", 2]]})).unwrap(); + session.execute("insert.table", json!({"range": "A1:B3", "header": true})).unwrap(); + session.execute("selection.set", json!({"range": "A2"})).unwrap(); + session +} + +fn app_restored_on(tab: &str, session: Session) -> Harness { + let mut app = SheetApp::new(session, Default::default()); + app.ui.ribbon_tab = tab.into(); + let mut h = egui_kittest::Harness::builder().with_size(vec2(1200.0, 800.0)).build_ui_state( + |ui, app: &mut SheetApp| { + let ctx = ui.ctx().clone(); + app.logic(&ctx); + app.ui(ui); + }, + app, + ); + h.run_steps(4); + h +} + +#[test] +fn a_saved_table_design_tab_without_a_table_opens_on_home() { + let mut session = Session::new(); + session.new_workbook(); + let h = app_restored_on("Table Design", session); + assert_eq!(h.state().ui.ribbon_tab, "Home"); +} + +#[test] +fn a_saved_chart_design_tab_without_a_chart_opens_on_home() { + let mut session = Session::new(); + session.new_workbook(); + let h = app_restored_on("Chart Design", session); + assert_eq!(h.state().ui.ribbon_tab, "Home"); +} + +#[test] +fn leaving_the_table_while_on_table_design_returns_to_home() { + let mut h = harness(session_with_table()); + h.state_mut().ui.ribbon_tab = "Table Design".into(); + h.run_steps(2); + assert_eq!(h.state().ui.ribbon_tab, "Table Design", "the tab stays while the cell is in the table"); + h.state_mut().session.execute("selection.set", json!({"range": "D10"})).unwrap(); + h.run_steps(2); + assert_eq!(h.state().ui.ribbon_tab, "Home"); +} From a0ceea1468a72baf4654950cf9d84611e649dd91 Mon Sep 17 00:00:00 2001 From: Marco Arnulfo Date: Fri, 23 Oct 2026 17:16:30 +0200 Subject: [PATCH 2/3] Open on the Home tab every launch, as in Excel (fixes #2329) --- apps/gridcraft-web/src/prefs.rs | 2 +- apps/gridcraft/src/main.rs | 2 +- crates/ui-egui/src/lib.rs | 8 +++++++ crates/ui-egui/tests/contextual_ribbon_tab.rs | 22 +++++++++++++++++++ 4 files changed, 32 insertions(+), 2 deletions(-) diff --git a/apps/gridcraft-web/src/prefs.rs b/apps/gridcraft-web/src/prefs.rs index 7d8c0abf8..987343c56 100644 --- a/apps/gridcraft-web/src/prefs.rs +++ b/apps/gridcraft-web/src/prefs.rs @@ -22,7 +22,7 @@ pub fn restore(app: &mut SheetApp, ui: Option<&str>, prefs: Option<&str>) { { // No usable saved language: keep the browser's, as the desktop keeps the system's. let language = gridcraft_ui_egui::i18n::saved_language(&json).unwrap_or(app.ui.language); - app.ui = state; + app.ui = state.at_startup(); app.ui.language = language; } if let Some(s) = prefs.filter(readable) diff --git a/apps/gridcraft/src/main.rs b/apps/gridcraft/src/main.rs index 7c0eb04e6..6c0d2f676 100644 --- a/apps/gridcraft/src/main.rs +++ b/apps/gridcraft/src/main.rs @@ -111,7 +111,7 @@ fn load_prefs(app: &mut SheetApp) { { let saved = serde_json::from_slice(&b).ok().and_then(|v| gridcraft_ui_egui::i18n::saved_language(&v)); let language = saved.unwrap_or(app.ui.language); - app.ui = ui; + app.ui = ui.at_startup(); app.ui.language = language; } if let Ok(b) = std::fs::read(dir.join("prefs.json")) diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index faae9bfae..676359355 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -117,6 +117,14 @@ impl Default for UiState { } impl UiState { + /// The state saved by an earlier run, as the app starts with it. The ribbon tab is written to + /// the preferences but not restored: Excel opens on Home every launch, so a contextual tab + /// (Table Design) cannot come back with nothing selected to justify it. + pub fn at_startup(mut self) -> Self { + self.ribbon_tab = "Home".into(); + self + } + pub fn theme_preference(&self) -> egui::ThemePreference { if self.system_theme { egui::ThemePreference::System diff --git a/crates/ui-egui/tests/contextual_ribbon_tab.rs b/crates/ui-egui/tests/contextual_ribbon_tab.rs index 6e1626dd5..bdd8b5c93 100644 --- a/crates/ui-egui/tests/contextual_ribbon_tab.rs +++ b/crates/ui-egui/tests/contextual_ribbon_tab.rs @@ -72,3 +72,25 @@ fn leaving_the_table_while_on_table_design_returns_to_home() { h.run_steps(2); assert_eq!(h.state().ui.ribbon_tab, "Home"); } + +#[test] +fn startup_opens_on_home_not_the_saved_tab() { + let mut saved = gridcraft_ui_egui::UiState::default(); + saved.ribbon_tab = "Table Design".into(); + saved.dark = true; + let json = serde_json::to_string(&saved).unwrap(); + assert!(json.contains(r#""ribbonTab":"Table Design""#), "{json}"); + let restored = serde_json::from_str::(&json).unwrap().at_startup(); + assert_eq!(restored.ribbon_tab, "Home", "the saved tab is not restored"); + assert!(restored.dark, "the other preferences are"); +} + +#[test] +fn an_ordinary_tab_stays_put_in_a_session() { + let mut session = Session::new(); + session.new_workbook(); + let mut h = harness(session); + h.state_mut().ui.ribbon_tab = "Data".into(); + h.run_steps(3); + assert_eq!(h.state().ui.ribbon_tab, "Data"); +} From 45ec9dfbfe6b1a0a82f4c6ddf92fbe68a3e6e608 Mon Sep 17 00:00:00 2001 From: Marco Arnulfo Date: Fri, 23 Oct 2026 18:44:23 +0200 Subject: [PATCH 3/3] Build the saved UiState in the test with struct update syntax (clippy field_reassign_with_default) --- crates/ui-egui/tests/contextual_ribbon_tab.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/ui-egui/tests/contextual_ribbon_tab.rs b/crates/ui-egui/tests/contextual_ribbon_tab.rs index bdd8b5c93..5221f2d60 100644 --- a/crates/ui-egui/tests/contextual_ribbon_tab.rs +++ b/crates/ui-egui/tests/contextual_ribbon_tab.rs @@ -75,9 +75,7 @@ fn leaving_the_table_while_on_table_design_returns_to_home() { #[test] fn startup_opens_on_home_not_the_saved_tab() { - let mut saved = gridcraft_ui_egui::UiState::default(); - saved.ribbon_tab = "Table Design".into(); - saved.dark = true; + let saved = gridcraft_ui_egui::UiState { ribbon_tab: "Table Design".into(), dark: true, ..Default::default() }; let json = serde_json::to_string(&saved).unwrap(); assert!(json.contains(r#""ribbonTab":"Table Design""#), "{json}"); let restored = serde_json::from_str::(&json).unwrap().at_startup();