Files
gridcraft/crates/ui-egui/tests/page_layout_view.rs
T
vangel76andClaude Opus 5.5 cdd71b4d5d Headers and footers: first-page and even-page variants, Insert › Header & Footer edits in place (#344)
XLSX headerFooter kept only oddHeader/oddFooter: a workbook whose header is on a
different first page (differentFirst + firstHeader) or on even pages showed no
header, and saving dropped it. The model now keeps the first/even headers and
footers and the differentFirst / differentOddEven / scaleWithDoc / alignWithMargins
flags; XLSX reads and writes them; the PDF and the Page Layout view use the right
pair per page; in-place editing edits the page's own pair, and the Header & Footer
tab has Different First Page and Different Odd & Even Pages. pageLayout.headerFooter
takes the variants and a page: odd|even|first for one-section edits.

Insert › Header & Footer now switches to Page Layout view and edits the first
page's centre header, as in Excel, instead of opening the codes dialog.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 21:44:49 +02:00

165 lines
6.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.
//! Page Layout view through real pointer and keyboard input: cells are hit through the page
//! transform, and headers are edited in place, committed on a click elsewhere, and undoable.
use egui::{Event, Key, Modifiers, PointerButton, Pos2, pos2, vec2};
use gridcraft_engine::Session;
use gridcraft_engine::core::CellRef;
use gridcraft_engine::model::SheetViewMode;
use gridcraft_ui_egui::SheetApp;
use gridcraft_ui_egui::grid::Geo;
use serde_json::json;
type Harness = egui_kittest::Harness<'static, SheetApp>;
fn fixture() -> Harness {
let mut session = Session::new();
session.new_workbook();
for r in 1..=60 {
session.execute("cell.set", json!({"cell": format!("A{r}"), "input": r.to_string()})).unwrap();
}
session.execute("pageLayout.headerFooter", json!({"header": "&LLeft"})).unwrap();
session.execute("view.pageLayout", json!({})).unwrap();
let mut h = egui_kittest::Harness::builder()
.with_size(vec2(1200.0, 900.0))
.with_step_dt(1.0 / 60.0)
.with_os(egui::os::OperatingSystem::Mac)
.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 geo(h: &Harness) -> Geo {
h.state().grid.geo.clone().unwrap()
}
fn click(h: &mut Harness, p: Pos2) {
// Keep clicks apart from egui's double-click window.
h.run_steps(40);
h.input_mut().events.push(Event::PointerMoved(p));
h.input_mut().events.push(Event::PointerButton { pos: p, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE });
h.step();
h.input_mut().events.push(Event::PointerButton { pos: p, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE });
h.run_steps(3);
}
fn header(h: &Harness) -> String {
h.state().session.doc().unwrap().wb.active().unwrap().print.header.clone()
}
/// Screen point of a layout point.
fn screen(g: &Geo, x: f64, y: f64) -> Pos2 {
pos2(g.cells.left() + (x - g.scroll.x as f64) as f32 * g.z, g.cells.top() + (y - g.scroll.y as f64) as f32 * g.z)
}
#[test]
fn cells_are_hit_through_the_page_transform() {
let mut h = fixture();
let g = geo(&h);
let pg = g.paper.clone().expect("Page Layout view lays out pages");
let app = h.state();
let sh = app.session.doc().unwrap().wb.active().unwrap();
assert_eq!(sh.view, SheetViewMode::PageLayout);
// Cells start inside the first page's margins, not at the grid's corner.
let a1 = g.cell_rect(sh, CellRef::new(0, 0));
let content = screen(&g, pg.page_x(0) + pg.margins[0], pg.page_y(0) + pg.margins[2]);
assert!((a1.left() - content.x).abs() < 0.5 && (a1.top() - content.y).abs() < 0.5, "{a1:?} vs {content:?}");
let c5 = g.cell_rect(sh, CellRef::parse("C5").unwrap()).center();
assert_eq!(g.cell_at(sh, c5), CellRef::parse("C5").unwrap());
click(&mut h, c5);
assert_eq!(h.state().session.doc().unwrap().selection.active, CellRef::parse("C5").unwrap());
// A click in the gap left of the page selects nothing new.
let gap = screen(&g, pg.gap / 2.0, pg.page_y(0) + pg.margins[2] + 30.0);
click(&mut h, gap);
assert_eq!(h.state().session.doc().unwrap().selection.active, CellRef::parse("C5").unwrap());
}
#[test]
fn header_is_edited_in_place_committed_by_a_click_elsewhere_and_undoable() {
let mut h = fixture();
let g = geo(&h);
let pg = g.paper.clone().unwrap();
// The centre third of the first page's top margin.
let (px, py, w, _) = pg.page_rect(0, 0);
let centre = screen(&g, px + w / 2.0, py + pg.margins[2] / 2.0);
click(&mut h, centre);
{
let app = h.state();
let e = app.grid.hf_edit.as_ref().expect("clicking the top margin edits the header");
assert_eq!((e.footer, e.section), (false, 1));
assert_eq!(app.ui.ribbon_tab, "Header & Footer");
}
h.input_mut().events.push(Event::Text("Report ".into()));
h.run_steps(2);
gridcraft_ui_egui::paper_view::insert_field(h.state_mut(), "&[Page]");
h.run_steps(2);
assert_eq!(h.state().grid.hf_edit.as_ref().unwrap().texts[1], "Report &[Page]");
assert_eq!(header(&h), "&LLeft", "nothing is written while editing");
// Clicking a cell commits (one undo step) and selects the cell.
let sh_geo = geo(&h);
let b3 = {
let app = h.state();
let sh = app.session.doc().unwrap().wb.active().unwrap();
sh_geo.cell_rect(sh, CellRef::parse("B3").unwrap()).center()
};
click(&mut h, b3);
assert!(h.state().grid.hf_edit.is_none());
assert_eq!(header(&h), "&LLeft&CReport &P");
assert_eq!(h.state().session.doc().unwrap().selection.active, CellRef::parse("B3").unwrap());
assert_ne!(h.state().ui.ribbon_tab, "Header & Footer");
h.state_mut().run("edit.undo", json!({})).unwrap();
assert_eq!(header(&h), "&LLeft");
// Escape cancels an edit without writing.
click(&mut h, centre);
h.input_mut().events.push(Event::Text("x".into()));
h.run_steps(2);
h.input_mut().events.push(Event::Key { key: Key::Escape, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::NONE });
h.run_steps(2);
assert!(h.state().grid.hf_edit.is_none());
assert_eq!(header(&h), "&LLeft");
}
#[test]
fn the_first_page_edits_its_own_header_when_different_first_page_is_on() {
let mut h = fixture();
h.state_mut().run("pageLayout.headerFooter", json!({"differentFirst": true, "firstHeader": "&CCover"})).unwrap();
h.run_steps(3);
// Insert › Header & Footer edits the first page's centre header, here the first-page one.
gridcraft_ui_egui::paper_view::start_editing(h.state_mut());
h.run_steps(3);
{
let e = h.state().grid.hf_edit.as_ref().expect("Insert › Header & Footer starts editing");
assert_eq!((e.footer, e.section, e.kind), (false, 1, gridcraft_engine::model::HfPage::First));
assert_eq!(e.texts[1], "Cover");
}
h.input_mut().events.push(Event::Text(" page".into()));
h.run_steps(2);
let g = geo(&h);
let b3 = {
let app = h.state();
g.cell_rect(app.session.doc().unwrap().wb.active().unwrap(), CellRef::parse("B3").unwrap()).center()
};
click(&mut h, b3);
let pr = h.state().session.doc().unwrap().wb.active().unwrap().print.clone();
assert_eq!(pr.first_header, "&CCover page");
assert_eq!(pr.header, "&LLeft", "the other pages keep their header");
}
#[test]
fn insert_header_and_footer_switches_to_page_layout_view() {
let mut h = fixture();
h.state_mut().run("view.normal", json!({})).unwrap();
h.run_steps(3);
gridcraft_ui_egui::paper_view::start_editing(h.state_mut());
h.run_steps(4);
assert_eq!(h.state().session.doc().unwrap().wb.active().unwrap().view, SheetViewMode::PageLayout);
let e = h.state().grid.hf_edit.as_ref().expect("the header is being edited");
assert_eq!((e.footer, e.section, e.texts[0].as_str()), (false, 1, "Left"));
}