Files
gridcraft/crates/ui-egui/tests/it/tab_split.rs
T
Hector VillarrealandClaude Opus 5.5 846cc09d67 #3101: show or hide the sheet tabs and scroll bars of a workbook
Excel can hide the sheet tabs, the horizontal scroll bar and the
vertical scroll bar of a workbook window (File › Options › Advanced ›
Display options for this workbook), saved with the workbook. GridCraft
already read and wrote these settings (#3057) but always showed all
three, and had no command or option for them.

New commands view.showSheetTabs, view.showHorizontalScrollBar and
view.showVerticalScrollBar {on?} (toggling without `on`, undoable,
saved with the workbook), and a "Display options for this workbook"
section in File › Options. The tab bar follows them: without the tabs
the scroll bar takes the bar, without the scroll bar the tabs do, and
with neither the bar isn't shown; without the vertical scroll bar the
grid takes its width.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 01:37:34 -06:00

200 lines
8.9 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! The handle between the sheet tabs and the horizontal scrollbar: dragging it gives the tabs more
//! or less of the tab bar, as in Excel.
use egui::{Event, Modifiers, PointerButton, Pos2, Rect, Shape, pos2, vec2};
use gridcraft_engine::Session;
use gridcraft_ui_egui::SheetApp;
use serde_json::json;
type Harness = egui_kittest::Harness<'static, SheetApp>;
fn harness(width: f32, sheets: usize) -> Harness {
let mut s = Session::new();
s.new_workbook();
s.execute("sheet.rename", json!({"sheet":0,"name":"Tab00"})).unwrap();
for i in 1..sheets {
s.execute("home.insertSheet", json!({"name":format!("Tab{i:02}")})).unwrap();
}
s.execute("sheet.activate", json!({"sheet":0})).unwrap();
let mut h = crate::common::builder().with_step_dt(1.0 / 60.0).with_size(vec2(width, 120.0)).build_ui_state(
move |ui, app: &mut SheetApp| {
app.grid.cells_rect = Some(Rect::from_min_size(pos2(0.0, 0.0), vec2(ui.max_rect().width(), 80.0)));
gridcraft_ui_egui::tabs::sheet_tabs(app, ui);
},
crate::common::app(s),
);
h.run_steps(20);
h
}
fn split(h: &Harness) -> Rect {
h.state().grid.tab_split.expect("the tab bar has a split handle")
}
fn track(h: &Harness) -> Rect {
h.state().grid.hscroll.expect("the tab bar has a horizontal scrollbar")
}
/// The clip rectangle of the tab strip: the viewport the tab names are painted in.
fn strip(h: &Harness) -> Rect {
h.output()
.shapes
.iter()
.find_map(|s| match &s.shape {
Shape::Text(t) if t.galley.job.text == "Tab00" => Some(s.clip_rect),
_ => None,
})
.expect("Tab00 was painted")
}
fn drag(h: &mut Harness, from: Pos2, to: Pos2) {
h.input_mut().events.extend([
Event::PointerMoved(from),
Event::PointerButton { pos: from, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE },
]);
h.step();
for i in 1..=6 {
h.input_mut().events.push(Event::PointerMoved(from + (to - from) * (i as f32 / 6.0)));
h.step();
}
h.input_mut().events.push(Event::PointerButton { pos: to, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE });
h.run_steps(20);
}
#[test]
fn dragging_the_handle_right_gives_overflowing_tabs_more_room() {
let mut h = harness(1200.0, 30);
let (before, handle) = (strip(&h), split(&h));
assert!(before.width() <= 1200.0 * 0.6, "the tabs start at their default share: {before:?}");
drag(&mut h, handle.center(), pos2(1000.0, handle.center().y));
let after = strip(&h);
assert!(after.width() > before.width() + 200.0, "the strip grew: {before:?} -> {after:?}");
assert!((split(&h).center().x - 1000.0).abs() <= 2.0, "the handle follows the pointer: {:?}", split(&h));
assert_eq!(track(&h).left(), split(&h).right(), "the scrollbar starts at the handle");
assert!(track(&h).width() >= 100.0, "the scrollbar keeps a usable width: {:?}", track(&h));
assert_eq!(h.state().session.doc().unwrap().wb.sheets.len(), 30, "dragging the handle does not edit the workbook");
}
#[test]
fn dragging_the_handle_left_gives_the_scrollbar_more_room() {
let mut h = harness(1200.0, 30);
let (before, handle) = (track(&h), split(&h));
drag(&mut h, handle.center(), pos2(handle.center().x - 300.0, handle.center().y));
let after = track(&h);
assert!(after.width() > before.width() + 280.0, "the scrollbar grew: {before:?} -> {after:?}");
assert!(strip(&h).width() < 1200.0 * 0.6 - 280.0, "the strip shrank: {:?}", strip(&h));
}
#[test]
fn the_handle_stops_before_the_scrollbar_gets_too_narrow_and_keeps_new_sheet_shown() {
let mut h = harness(1200.0, 30);
let handle = split(&h);
drag(&mut h, handle.center(), pos2(1195.0, handle.center().y));
assert!(track(&h).width() >= 100.0 - 0.5, "the scrollbar keeps 100 px: {:?}", track(&h));
let (handle, bar_left) = (split(&h), strip(&h).left());
drag(&mut h, handle.center(), pos2(0.0, handle.center().y));
// The tab strip has no width left, and the New sheet button (26 px after a 6 px gap) still
// fits before the handle.
assert!((split(&h).left() - bar_left - 32.0).abs() <= 1.0, "the handle is 32 px after the strip's start {bar_left}: {:?}", split(&h));
}
#[test]
fn a_narrower_window_keeps_the_scrollbar_after_the_handle_was_dragged_right() {
let mut h = harness(1200.0, 30);
let handle = split(&h);
drag(&mut h, handle.center(), pos2(1195.0, handle.center().y));
h.set_size(vec2(700.0, 120.0));
h.run_steps(20);
let track = track(&h);
assert!(track.width() >= 100.0 - 0.5 && track.right() <= 700.5, "the scrollbar keeps 100 px in the bar: {track:?}");
assert_eq!(track.left(), split(&h).right(), "the scrollbar starts at the handle");
assert!(strip(&h).right() <= split(&h).left() - 32.0 + 0.5, "the tabs end before New sheet: {:?}", strip(&h));
}
#[test]
fn room_the_tabs_do_not_fill_stays_empty_and_new_sheet_follows_the_last_tab() {
let mut h = harness(1200.0, 2);
let plus_before = track(&h).left() - 8.0 - 26.0;
let handle = split(&h);
drag(&mut h, handle.center(), pos2(900.0, handle.center().y));
assert!((split(&h).center().x - 900.0).abs() <= 2.0, "the handle stays where it was dragged: {:?}", split(&h));
assert_eq!(track(&h).left(), split(&h).right());
// New sheet (+) is still right after the last tab: clicking there adds a sheet.
let at = pos2(plus_before + 13.0, handle.center().y);
h.input_mut().events.extend([
Event::PointerMoved(at),
Event::PointerButton { pos: at, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE },
]);
h.step();
h.input_mut().events.push(Event::PointerButton { pos: at, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE });
h.run_steps(20);
assert_eq!(h.state().session.doc().unwrap().wb.sheets.len(), 3, "New sheet stayed after the last tab");
}
#[test]
fn the_handle_shows_the_resize_cursor() {
let mut h = harness(1200.0, 30);
let at = split(&h).center();
h.input_mut().events.push(Event::PointerMoved(at));
h.run_steps(2);
assert_eq!(h.output().platform_output.cursor_icon, egui::CursorIcon::ResizeHorizontal);
}
#[test]
fn each_workbook_keeps_its_own_tab_share() {
let mut h = harness(1200.0, 30);
let handle = split(&h);
drag(&mut h, handle.center(), pos2(1000.0, handle.center().y));
assert!(track(&h).left() > 990.0, "the first workbook's scrollbar starts at the handle: {:?}", track(&h));
h.state_mut().session.new_workbook();
h.run_steps(20);
// The new workbook's one tab takes its default room, so its scrollbar starts right after New sheet.
assert!(track(&h).left() < 400.0, "a new workbook starts at the default share: {:?}", track(&h));
assert_eq!(h.state().grid.tab_shares.len(), 1, "only the dragged workbook has a share");
}
/// The tab bar of a workbook whose window has `command` run with `{on: false}` first.
fn harness_without(command: &str) -> Harness {
let mut s = Session::new();
s.new_workbook();
s.execute("sheet.rename", json!({"sheet": 0, "name": "Tab00"})).unwrap();
s.execute(command, json!({"on": false})).unwrap();
let mut h = crate::common::builder().with_step_dt(1.0 / 60.0).with_size(vec2(900.0, 120.0)).build_ui_state(
move |ui, app: &mut SheetApp| {
app.grid.cells_rect = Some(Rect::from_min_size(pos2(0.0, 0.0), vec2(ui.max_rect().width(), 80.0)));
gridcraft_ui_egui::tabs::sheet_tabs(app, ui);
},
crate::common::app(s),
);
h.run_steps(20);
h
}
fn shows_tab_name(h: &Harness) -> bool {
h.output().shapes.iter().any(|s| matches!(&s.shape, Shape::Text(t) if t.galley.job.text == "Tab00"))
}
#[test]
fn display_options_hide_the_sheet_tabs_and_the_horizontal_scroll_bar() {
// #3101: File › Options › Advanced, Display options for this workbook.
let h = harness_without("view.showSheetTabs");
assert!(!shows_tab_name(&h), "no sheet tabs");
assert!(h.state().grid.tab_split.is_none(), "nothing to split without tabs");
let bar = h.state().grid.hscroll.expect("the scroll bar stays");
assert!(bar.left() < 40.0, "the scroll bar takes the bar from its left edge: {bar:?}");
let h = harness_without("view.showHorizontalScrollBar");
assert!(shows_tab_name(&h), "the tabs stay");
assert!(h.state().grid.hscroll.is_none() && h.state().grid.tab_split.is_none(), "no scroll bar and no split handle");
// Both off: no bar at all.
let mut s = Session::new();
s.new_workbook();
s.execute("view.showSheetTabs", json!({"on": false})).unwrap();
s.execute("view.showHorizontalScrollBar", json!({"on": false})).unwrap();
let mut h = crate::common::builder()
.with_size(vec2(900.0, 120.0))
.build_ui_state(|ui, app: &mut SheetApp| gridcraft_ui_egui::tabs::sheet_tabs(app, ui), crate::common::app(s));
h.run_steps(5);
assert!(!shows_tab_name(&h) && h.state().grid.hscroll.is_none() && h.state().grid.tab_split.is_none(), "no bar at all");
}