Merge pull request #1877 from SocraticsAI:host-chrome

This commit is contained in:
Brandon Thomas
2026-10-19 12:32:51 -04:00
3 changed files with 101 additions and 2 deletions
+33 -1
View File
@@ -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<editor::EditState>,
pub text_box_editor: Option<text_box::EditState>,
@@ -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);
+3 -1
View File
@@ -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());
+65
View File
@@ -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");
}