mirror of
https://github.com/storytold/gridcraft.git
synced 2026-11-10 12:29:58 -06:00
Addresses review comment 4453674677: after a frame drew both controls, turning off the horizontal scroll bar or the sheet tabs left app.grid.hscroll / app.grid.tab_split holding the old rectangles. sheet_tabs now clears both before any visibility return. Test: tab_split::hiding_the_controls_clears_their_cached_rectangles toggles both options on one live harness. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
221 lines
10 KiB
Rust
221 lines
10 KiB
Rust
//! 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");
|
||
}
|
||
|
||
#[test]
|
||
fn hiding_the_controls_clears_their_cached_rectangles() {
|
||
// Turned off on a live window, the controls drop the rectangles of the frame that drew them.
|
||
let mut h = harness(900.0, 1);
|
||
assert!(h.state().grid.tab_split.is_some() && h.state().grid.hscroll.is_some(), "both controls drawn first");
|
||
|
||
h.state_mut().run("view.showSheetTabs", json!({"on": false})).unwrap();
|
||
h.run_steps(5);
|
||
assert!(h.state().grid.tab_split.is_none(), "no split handle once the tabs are hidden");
|
||
assert!(h.state().grid.hscroll.is_some(), "the scroll bar stays");
|
||
|
||
h.state_mut().run("view.showSheetTabs", json!({"on": true})).unwrap();
|
||
h.state_mut().run("view.showHorizontalScrollBar", json!({"on": false})).unwrap();
|
||
h.run_steps(5);
|
||
assert!(h.state().grid.hscroll.is_none() && h.state().grid.tab_split.is_none(), "no scroll bar and no split handle");
|
||
|
||
h.state_mut().run("view.showSheetTabs", json!({"on": false})).unwrap();
|
||
h.run_steps(5);
|
||
assert!(h.state().grid.hscroll.is_none() && h.state().grid.tab_split.is_none(), "no bar at all");
|
||
}
|