diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 2cbf0510f..f549aec2b 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -209,9 +209,31 @@ pub struct Perf { last: f64, } +/// Which parts of the app's own window chrome are drawn. Everything is on by default; a host +/// that embeds the sheet in its own window (and owns files itself) can turn off the title bar +/// and the File tab, whose Backstage view then never opens. Not persisted: it describes the +/// host, not a user preference. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Chrome { + /// The title bar: AutoSave, Home, Save, Undo/Redo, the file name, Comments, Share, Search and + /// the theme switch. + pub title_bar: bool, + /// The ribbon's File tab and the Backstage view it opens (also reached by keyboard and + /// commands such as `ui.backstage`). + pub file_tab: bool, +} + +impl Default for Chrome { + fn default() -> Self { + Chrome { title_bar: true, file_tab: true } + } +} + pub struct SheetApp { pub session: Session, pub ui: UiState, + /// The window chrome to draw (see [`Chrome`]). + pub chrome: Chrome, pub views: HashMap<(u64, usize), SheetView>, pub editor: Option, pub text_box_editor: Option, @@ -270,6 +292,7 @@ impl SheetApp { SheetApp { session, ui: UiState::default(), + chrome: Chrome::default(), views: HashMap::new(), editor: None, text_box_editor: None, @@ -752,6 +775,9 @@ impl SheetApp { /// Opens the File tab's Backstage view on `page`, committing a cell still being edited first /// (File actions work on the whole workbook). A draft that can't be committed keeps the sheet. pub fn open_backstage(&mut self, page: backstage::Page) { + if !self.chrome.file_tab { + return; + } if !self.commit_edit(0, 0, false, false) { return; } @@ -1115,10 +1141,16 @@ impl SheetApp { // The language the widgets translate with this frame (see `i18n::current`). i18n::set_current(&ctx, self.ui.language); let t = theme::Tokens::get(&ctx); + // A host can turn the File tab off while Backstage is already open: close it. + if !self.chrome.file_tab { + self.backstage = None; + } if self.backstage.is_none() { self.ribbon_keys(&ctx); } - ribbon::title_bar(self, ui); + if self.chrome.title_bar { + ribbon::title_bar(self, ui); + } if self.backstage.is_some() { // File tab: the Backstage view replaces everything below the title bar. backstage::show(self, ui); diff --git a/crates/ui-egui/src/ribbon.rs b/crates/ui-egui/src/ribbon.rs index 662c87ef1..a0ab5a339 100644 --- a/crates/ui-egui/src/ribbon.rs +++ b/crates/ui-egui/src/ribbon.rs @@ -176,7 +176,9 @@ pub fn show(app: &mut SheetApp, ui: &mut Ui) { ui.horizontal(|ui| { ui.spacing_mut().item_spacing.x = 2.0; ui.add_space(8.0); - file_menu(app, ui); + if app.chrome.file_tab { + file_menu(app, ui); + } let mut tabs: Vec<&str> = TABS.to_vec(); let ctx_tabs = contextual_tabs(app); tabs.extend(ctx_tabs.iter()); diff --git a/crates/ui-egui/tests/host_chrome.rs b/crates/ui-egui/tests/host_chrome.rs new file mode 100644 index 000000000..627e24025 --- /dev/null +++ b/crates/ui-egui/tests/host_chrome.rs @@ -0,0 +1,65 @@ +//! A host that embeds the sheet in its own window can turn off the app's title bar and File tab. + +use egui_kittest::kittest::Queryable as _; +use gridcraft_engine::Session; +use gridcraft_ui_egui::{Chrome, Services, SheetApp, backstage::Page}; + +fn harness(chrome: Chrome) -> egui_kittest::Harness<'static, SheetApp> { + let mut session = Session::new(); + session.new_workbook(); + let mut app = SheetApp::new(session, Services::default()); + app.chrome = chrome; + let mut h = egui_kittest::Harness::builder().with_size(egui::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 the_title_bar_and_file_tab_are_drawn_by_default() { + let h = harness(Chrome::default()); + assert!(h.query_by_label("AutoSave").is_some(), "the title bar is drawn"); + assert!(h.query_by_label("File").is_some(), "the File tab is drawn"); +} + +#[test] +fn a_host_can_leave_out_the_title_bar_and_the_file_tab() { + let full = harness(Chrome::default()); + let h = harness(Chrome { title_bar: false, file_tab: false }); + assert!(h.query_by_label("AutoSave").is_none(), "no title bar"); + assert!(h.query_by_label("File").is_none(), "no File tab"); + // The ribbon tabs are painted text, so measure the move with the first ribbon control. + let (with_bar, without) = (full.get_by_label("Paste").rect(), h.get_by_label("Paste").rect()); + let moved = with_bar.min.y - without.min.y; + assert!((25.0..=40.0).contains(&moved), "the ribbon moves up by the title bar's height (28 on macOS, 38 elsewhere), moved {moved}"); + assert_eq!(with_bar.min.x, without.min.x, "the ribbon's groups keep their place across"); +} + +#[test] +fn without_the_file_tab_the_backstage_view_never_opens() { + let mut h = harness(Chrome { title_bar: true, file_tab: false }); + h.state_mut().open_backstage(Page::Home); + h.run_steps(3); + assert!(h.state().backstage.is_none(), "opening Backstage directly is ignored"); + h.state_mut().open_backstage(Page::Print); + h.run_steps(3); + assert!(h.state().backstage.is_none(), "so is every page, Print and Save As included"); + assert!(h.query_by_label("AutoSave").is_some(), "the title bar can stay on its own"); +} + +#[test] +fn turning_the_file_tab_off_closes_an_open_backstage() { + let mut h = harness(Chrome::default()); + h.state_mut().open_backstage(Page::Home); + h.run_steps(3); + assert!(h.state().backstage.is_some(), "Backstage opens with the File tab on"); + h.state_mut().chrome.file_tab = false; + h.run_steps(3); + assert!(h.state().backstage.is_none(), "and closes when the host turns the File tab off"); +}