From b745c136b22d7fd4c7dbea0f2c1c9b9f855d9e23 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 24 Oct 2026 12:54:07 -0700 Subject: [PATCH] Headers and footers: printing follows the Scale with Document and Align with Page Margins check boxes The Header & Footer tab now has the Scale with Document and Align with Page Margins check boxes, and pageLayout.headerFooter takes scaleWithDoc and alignWithMargins. XLSX already kept both settings, but nothing else read them. With Scale with Document on, header and footer text prints at the sheet's print scale. With it off, the text keeps its point size. With Align with Page Margins on, the left and right sections line up with the page's side margins. With it off, they start 0.7 inch from the paper's edges, or a quarter of the page width on paper narrower than 2.8 inches. The Page Layout view draws headers and footers and takes clicks on them the same way. Both settings are on by default, so a sheet printed at a scale other than 100% now prints its header and footer text at that scale. --- crates/engine/src/cmd/print.rs | 58 ++++++++++++++++++++- crates/engine/src/cmd/view.rs | 10 +++- crates/engine/src/paper.rs | 40 +++++++++++++- crates/ui-egui/src/i18n/cs.rs | 2 + crates/ui-egui/src/i18n/de.rs | 2 + crates/ui-egui/src/i18n/el.rs | 2 + crates/ui-egui/src/i18n/es.rs | 2 + crates/ui-egui/src/i18n/fr.rs | 2 + crates/ui-egui/src/i18n/it.rs | 2 + crates/ui-egui/src/i18n/ja.rs | 2 + crates/ui-egui/src/i18n/ko.rs | 2 + crates/ui-egui/src/i18n/nl.rs | 2 + crates/ui-egui/src/i18n/pl.rs | 2 + crates/ui-egui/src/i18n/pt.rs | 2 + crates/ui-egui/src/i18n/ro.rs | 2 + crates/ui-egui/src/i18n/ru.rs | 2 + crates/ui-egui/src/i18n/sr.rs | 2 + crates/ui-egui/src/i18n/tr.rs | 2 + crates/ui-egui/src/i18n/uk.rs | 2 + crates/ui-egui/src/i18n/vi.rs | 2 + crates/ui-egui/src/i18n/zh.rs | 2 + crates/ui-egui/src/paper_view.rs | 36 ++++++++----- crates/ui-egui/tests/it/page_layout_view.rs | 22 ++++++++ docs/ui-parity.md | 2 +- 24 files changed, 185 insertions(+), 19 deletions(-) diff --git a/crates/engine/src/cmd/print.rs b/crates/engine/src/cmd/print.rs index 1c91518e2..52c975688 100644 --- a/crates/engine/src/cmd/print.rs +++ b/crates/engine/src/cmd/print.rs @@ -139,8 +139,26 @@ impl SheetLayout { let [l, r, t, b, ..] = self.margins; ((self.page_w - l - r).max(36.0), (self.page_h - t - b).max(36.0)) } + /// Left and right margins, in points, that header and footer sections line up with: the + /// page's side margins with Align with Page Margins on, otherwise 0.7 inch on each side, at + /// most a quarter of the page width. + pub(crate) fn hf_sides(&self) -> (f32, f32) { + if self.settings.hf_align_with_margins { + (self.margins[0], self.margins[1]) + } else { + let side = HF_FIXED_SIDE_MARGIN.min(self.page_w / 4.0); + (side, side) + } + } + /// Scale of header and footer text: the print scale with Scale with Document on, otherwise 1. + pub(crate) fn hf_text_scale(&self) -> f32 { + if self.settings.hf_scale_with_doc { self.scale } else { 1.0 } + } } +/// Side margin of headers and footers, in points, when Align with Page Margins is off. +const HF_FIXED_SIDE_MARGIN: f32 = 0.7 * 72.0; + /// Options shared by the print commands. #[derive(Clone, Debug, Default)] pub struct PrintOptions { @@ -1476,7 +1494,9 @@ fn draw_headings(pg: &mut Page, b: &Block, col_heads: bool, row_heads: bool, at: } fn draw_header_footer(pg: &mut Page, lay: &SheetLayout, which: gridcraft_model::HfPage, cx: &HfContext) { - let [l, r, _, _, hm, fm] = lay.margins; + let [_, _, _, _, hm, fm] = lay.margins; + let (l, r) = lay.hf_sides(); + let text_scale = lay.hf_text_scale(); let pw = lay.page_w; let ph = lay.page_h; for (spec, header) in [(lay.settings.hf(which, false), true), (lay.settings.hf(which, true), false)] { @@ -1488,7 +1508,7 @@ fn draw_header_footer(pg: &mut Page, lay: &SheetLayout, which: gridcraft_model:: if sec.text.trim().is_empty() { continue; } - let size = sec.size.unwrap_or(10.0).clamp(1.0, 72.0); + let size = sec.size.unwrap_or(10.0).clamp(1.0, 72.0) * text_scale; let font = Font::styled(gridcraft_pdf::Family::Sans, sec.bold, sec.italic); let lines: Vec<&str> = sec.text.split('\n').take(20).collect(); let n = lines.len() as f32; @@ -2021,6 +2041,40 @@ mod tests { assert!(contains(&pdf, "(Odd 1) Tj") && contains(&pdf, "(Odd 2) Tj") && !contains(&pdf, "(Cover) Tj")); } + /// The x position and font size of the first `(text) Tj` in a PDF. + fn text_at(pdf: &[u8], text: &str) -> Option<(f32, f32)> { + let s = String::from_utf8_lossy(pdf); + let before = &s[..s.find(&format!("({text}) Tj"))?]; + let tf = before.rfind(" Tf")?; + let size = before[..tf].rsplit(' ').next()?.parse().ok()?; + let tm = before.rfind(" Tm")?; + let mut nums = before[..tm].rsplit([' ', '\n']); + let _y = nums.next()?; + let x = nums.next()?.parse().ok()?; + Some((x, size)) + } + + #[test] + fn headers_scale_with_the_sheet_and_align_with_its_margins_unless_turned_off() { + let mut s = s(); + s.execute("cell.set", json!({"cell": "A1", "input": "x"})).unwrap(); + s.execute("pageLayout.pageSetup", json!({"scale": 50, "left": 1.5, "right": 1.5})).unwrap(); + s.execute("pageLayout.headerFooter", json!({"header": "&LLeft&C&14Middle"})).unwrap(); + // Both on (the default): the text prints at half size, from the 1.5-inch left margin. + let (pdf, _) = pdf_of(&mut s, json!({})); + assert_eq!(text_at(&pdf, "Left"), Some((108.0, 5.0))); + assert_eq!(text_at(&pdf, "Middle").map(|t| t.1), Some(7.0)); + let r = s.execute("pageLayout.headerFooter", json!({"scaleWithDoc": false, "alignWithMargins": false})).unwrap(); + assert_eq!((&r["scaleWithDoc"], &r["alignWithMargins"]), (&json!(false), &json!(false))); + // Both off: full size, 0.7 inch from the paper's left edge. + let (pdf, _) = pdf_of(&mut s, json!({})); + assert_eq!(text_at(&pdf, "Left"), Some((50.4, 10.0))); + assert_eq!(text_at(&pdf, "Middle").map(|t| t.1), Some(14.0)); + s.execute("edit.undo", json!({})).unwrap(); + let pr = &s.doc().unwrap().wb.sheet(0).unwrap().print; + assert!(pr.hf_scale_with_doc && pr.hf_align_with_margins); + } + #[test] fn pdf_prints_cjk_text_instead_of_question_marks() { let mut s = s(); diff --git a/crates/engine/src/cmd/view.rs b/crates/engine/src/cmd/view.rs index b42aec9a6..60d907f40 100644 --- a/crates/engine/src/cmd/view.rs +++ b/crates/engine/src/cmd/view.rs @@ -115,7 +115,7 @@ pub fn specs() -> Vec { "Header & Footer", ["Insert", "Text"], None, - "{sheet?, header?: \"&C&P\", footer?: \"&CPage &P of &N\", differentFirst?: bool, differentOddEven?: bool, firstHeader?, firstFooter?, evenHeader?, evenFooter?} or one section: {sheet?, page?: odd|even|first, part: header|footer, section: left|center|right, text: \"Page &[Page] of &[Pages]\"} (field names or codes) → {header, footer, differentFirst, differentOddEven, firstHeader, firstFooter, evenHeader, evenFooter}", + "{sheet?, header?: \"&C&P\", footer?: \"&CPage &P of &N\", differentFirst?: bool, differentOddEven?: bool, scaleWithDoc?: bool, alignWithMargins?: bool, firstHeader?, firstFooter?, evenHeader?, evenFooter?} or one section: {sheet?, page?: odd|even|first, part: header|footer, section: left|center|right, text: \"Page &[Page] of &[Pages]\"} (field names or codes) → {header, footer, differentFirst, differentOddEven, scaleWithDoc, alignWithMargins, firstHeader, firstFooter, evenHeader, evenFooter}", has_doc, header_footer ), @@ -392,6 +392,12 @@ fn header_footer(s: &mut Session, p: &Json) -> Result { if let Some(on) = bool_param(p, "differentOddEven") { pr.different_odd_even = on; } + if let Some(on) = bool_param(p, "scaleWithDoc") { + pr.hf_scale_with_doc = on; + } + if let Some(on) = bool_param(p, "alignWithMargins") { + pr.hf_align_with_margins = on; + } if let Some((page, footer, idx, text)) = §ion { let spec = pr.hf_mut(*page, *footer); let mut parts = super::print::split_header_footer(spec); @@ -405,6 +411,8 @@ fn header_footer(s: &mut Session, p: &Json) -> Result { "footer": pr.footer, "differentFirst": pr.different_first, "differentOddEven": pr.different_odd_even, + "scaleWithDoc": pr.hf_scale_with_doc, + "alignWithMargins": pr.hf_align_with_margins, "firstHeader": pr.first_header, "firstFooter": pr.first_footer, "evenHeader": pr.even_header, diff --git a/crates/engine/src/paper.rs b/crates/engine/src/paper.rs index 01065c09e..0800f4fc7 100644 --- a/crates/engine/src/paper.rs +++ b/crates/engine/src/paper.rs @@ -29,6 +29,12 @@ pub struct PageGrid { pub gap: f64, /// Print scale (1.0 = 100%): screen sizes of cells are `zoom × scale`. pub scale: f32, + /// Left and right margins of headers and footers in layout units (the side margins, or a + /// fixed 0.7 inch, at most a quarter of the page width, with Align with Page Margins off). + pub hf_sides: [f64; 2], + /// Layout units per sheet pixel of header and footer text: 1 with Scale with Document on, + /// otherwise `1 / scale`, so the text keeps its point size on paper. + pub hf_text_k: f32, /// Paper size in points (after orientation), for the rulers. pub paper_pt: (f32, f32), pub row_bands: Vec<(u32, u32)>, @@ -65,6 +71,8 @@ impl PageGrid { let s = f64::from(lay.scale.clamp(0.1, 4.0)); let to_layout = |pt: f32| f64::from(pt) / f64::from(PX_TO_PT) / s; let margins = lay.margins.map(to_layout); + let (hf_left, hf_right) = lay.hf_sides(); + let hf_text_k = lay.hf_text_scale() / s as f32; let band_range = |bands: &[(u32, u32)], lo: u32, hi: u32| { let first = bands.iter().position(|b| b.1 >= lo).unwrap_or(0); let last = bands.iter().rposition(|b| b.0 <= hi).unwrap_or(first).max(first); @@ -79,6 +87,8 @@ impl PageGrid { margins, gap: f64::from(PAGE_GAP) / s, scale: lay.scale, + hf_sides: [to_layout(hf_left), to_layout(hf_right)], + hf_text_k, paper_pt: (lay.page_w, lay.page_h), rows_complete: row_bands.last().is_some_and(|b| b.1 >= MAX_ROWS - 1), cols_complete: col_bands.last().is_some_and(|b| b.1 >= MAX_COLS - 1), @@ -174,11 +184,12 @@ impl PageGrid { } /// The header or footer section under the point: the top or bottom margin of a page, split - /// into thirds between the side margins. + /// into thirds between the header and footer side margins. pub fn hf_zone_at(&self, x: f64, y: f64) -> Option<((usize, usize), HfZone)> { let (i, j) = self.page_at(x, y)?; let (px, py, _, _) = self.page_rect(i, j); - let [l, r, t, b, ..] = self.margins; + let [_, _, t, b, ..] = self.margins; + let [l, r] = self.hf_sides; let footer = if y < py + t { false } else if y >= py + self.page_h - b { @@ -301,6 +312,31 @@ mod tests { assert!(g.is_data_page(0, 0) && !g.is_data_page(1, 0)); } + #[test] + fn header_zones_and_text_follow_scale_with_document_and_align_with_page_margins() { + let mut wb = wb_with(10, 3); + if let Some(sh) = wb.sheet_mut(0) { + sh.print.scale = 50; + sh.print.margins = [2.0, 2.0, 0.75, 0.75, 0.3, 0.3]; + } + let g = PageGrid::build(&wb, 0, 50, 20).unwrap(); + let (px, py, _, _) = g.page_rect(0, 0); + // Both on: the sections sit between the 2-inch side margins and the text scales with the cells. + assert_eq!(g.hf_sides, [g.margins[0], g.margins[1]]); + assert!((g.hf_text_k - 1.0).abs() < 1e-6); + assert_eq!(g.hf_zone_at(px + 1000.0, py + 3.0).map(|z| z.1.section), Some(2)); + if let Some(sh) = wb.sheet_mut(0) { + sh.print.hf_scale_with_doc = false; + sh.print.hf_align_with_margins = false; + } + let g = PageGrid::build(&wb, 0, 50, 20).unwrap(); + // Both off: 0.7 inch (67.2 sheet pixels, 134.4 layout units at 50%) on each side, and the + // text keeps its point size on paper. + assert!(g.hf_sides.iter().all(|s| (s - 134.4).abs() < 1e-3), "{:?}", g.hf_sides); + assert!((g.hf_text_k - 2.0).abs() < 1e-6); + assert_eq!(g.hf_zone_at(px + 1000.0, py + 3.0).map(|z| z.1.section), Some(1)); + } + #[test] fn print_scale_and_hostile_settings_stay_bounded() { let mut wb = wb_with(3, 3); diff --git a/crates/ui-egui/src/i18n/cs.rs b/crates/ui-egui/src/i18n/cs.rs index fc9181feb..c622a1831 100644 --- a/crates/ui-egui/src/i18n/cs.rs +++ b/crates/ui-egui/src/i18n/cs.rs @@ -206,6 +206,8 @@ pub(super) fn translate(text: &str) -> Option<&'static str> { "Footer" => "Zápatí", "Different First Page" => "Jiná první stránka", "Different Odd & Even Pages" => "Jiné liché a sudé stránky", + "Scale with Document" => "Měnit velikost s dokumentem", + "Align with Page Margins" => "Zarovnat k okrajům stránky", "Go to Footer" => "Přejít na zápatí", "Go to Header" => "Přejít na záhlaví", "Go to\nFooter" => "Přejít\nna zápatí", diff --git a/crates/ui-egui/src/i18n/de.rs b/crates/ui-egui/src/i18n/de.rs index 61c2292f4..8296909c8 100644 --- a/crates/ui-egui/src/i18n/de.rs +++ b/crates/ui-egui/src/i18n/de.rs @@ -210,6 +210,8 @@ pub(super) fn translate(text: &str) -> Option<&'static str> { "Footer" => "Fußzeile", "Different First Page" => "Erste Seite anders", "Different Odd & Even Pages" => "Gerade und ungerade Seiten anders", + "Scale with Document" => "Mit Dokument skalieren", + "Align with Page Margins" => "An Seitenrändern ausrichten", "Go to Footer" => "Zur Fußzeile wechseln", "Go to Header" => "Zur Kopfzeile wechseln", "Go to\nFooter" => "Zur\nFußzeile", diff --git a/crates/ui-egui/src/i18n/el.rs b/crates/ui-egui/src/i18n/el.rs index e54416a6f..bd972471d 100644 --- a/crates/ui-egui/src/i18n/el.rs +++ b/crates/ui-egui/src/i18n/el.rs @@ -202,6 +202,8 @@ pub(super) fn translate(text: &str) -> Option<&'static str> { "Footer" => "Υποσέλιδο", "Different First Page" => "Διαφορετική πρώτη σελίδα", "Different Odd & Even Pages" => "Διαφορετικές μονές και ζυγές σελίδες", + "Scale with Document" => "Κλιμάκωση με το έγγραφο", + "Align with Page Margins" => "Στοίχιση με τα περιθώρια σελίδας", "Go to Footer" => "Μετάβαση στο υποσέλιδο", "Go to Header" => "Μετάβαση στην κεφαλίδα", "Go to\nFooter" => "Μετάβαση στο\nυποσέλιδο", diff --git a/crates/ui-egui/src/i18n/es.rs b/crates/ui-egui/src/i18n/es.rs index 36898c098..5a1770438 100644 --- a/crates/ui-egui/src/i18n/es.rs +++ b/crates/ui-egui/src/i18n/es.rs @@ -209,6 +209,8 @@ pub(super) fn translate(text: &str) -> Option<&'static str> { "Footer" => "Pie de página", "Different First Page" => "Primera página diferente", "Different Odd & Even Pages" => "Páginas pares e impares diferentes", + "Scale with Document" => "Escalar con el documento", + "Align with Page Margins" => "Alinear con los márgenes de página", "Go to Footer" => "Ir al pie de página", "Go to Header" => "Ir al encabezado", "Go to\nFooter" => "Ir al pie\nde página", diff --git a/crates/ui-egui/src/i18n/fr.rs b/crates/ui-egui/src/i18n/fr.rs index 317647c2f..9927e060f 100644 --- a/crates/ui-egui/src/i18n/fr.rs +++ b/crates/ui-egui/src/i18n/fr.rs @@ -4,6 +4,8 @@ pub(super) fn translate(text: &str) -> Option<&'static str> { Some(match text { "Different First Page" => "Première page différente", "Different Odd & Even Pages" => "Pages paires et impaires différentes", + "Scale with Document" => "Mettre à l'échelle avec le document", + "Align with Page Margins" => "Aligner sur les marges de la page", "Paste" => "Coller", "Paste Values" => "Coller les valeurs", "Paste Formulas" => "Coller les formules", diff --git a/crates/ui-egui/src/i18n/it.rs b/crates/ui-egui/src/i18n/it.rs index 65f356669..d0a06adfa 100644 --- a/crates/ui-egui/src/i18n/it.rs +++ b/crates/ui-egui/src/i18n/it.rs @@ -327,6 +327,8 @@ pub(super) fn translate(text: &str) -> Option<&'static str> { "Go to Header" => "Vai a intestazione", "Different First Page" => "Prima pagina diversa", "Different Odd & Even Pages" => "Pagine pari e dispari diverse", + "Scale with Document" => "Ridimensiona con il documento", + "Align with Page Margins" => "Allinea ai margini della pagina", "Go to Footer" => "Vai a piè di pagina", "Ruler" => "Righello", // File › Backstage view. diff --git a/crates/ui-egui/src/i18n/ja.rs b/crates/ui-egui/src/i18n/ja.rs index 2df4cfc11..c78a111f5 100644 --- a/crates/ui-egui/src/i18n/ja.rs +++ b/crates/ui-egui/src/i18n/ja.rs @@ -331,6 +331,8 @@ pub(super) const CORE: &[(&str, &str)] = &[ ("Go to Header", "ヘッダーに移動"), ("Different First Page", "先頭ページのみ別指定"), ("Different Odd & Even Pages", "奇数/偶数ページ別指定"), + ("Scale with Document", "文書に合わせて拡大/縮小"), + ("Align with Page Margins", "ページ余白に合わせる"), ("Go to Footer", "フッターに移動"), ("Ruler", "ルーラー"), ("File", "ファイル"), diff --git a/crates/ui-egui/src/i18n/ko.rs b/crates/ui-egui/src/i18n/ko.rs index d3c440cf3..35195e2ae 100644 --- a/crates/ui-egui/src/i18n/ko.rs +++ b/crates/ui-egui/src/i18n/ko.rs @@ -332,6 +332,8 @@ pub(super) fn translate(text: &str) -> Option<&'static str> { "Go to Header" => "머리글로 이동", "Different First Page" => "첫 페이지를 다르게 지정", "Different Odd & Even Pages" => "짝수와 홀수 페이지를 다르게 지정", + "Scale with Document" => "문서에 맞게 배율 조정", + "Align with Page Margins" => "페이지 여백에 맞추기", "Go to Footer" => "바닥글로 이동", "Ruler" => "눈금자", // File › Backstage view. diff --git a/crates/ui-egui/src/i18n/nl.rs b/crates/ui-egui/src/i18n/nl.rs index c1f952357..b8c995057 100644 --- a/crates/ui-egui/src/i18n/nl.rs +++ b/crates/ui-egui/src/i18n/nl.rs @@ -326,6 +326,8 @@ pub(super) fn translate(text: &str) -> Option<&'static str> { "Go to Header" => "Naar koptekst", "Different First Page" => "Eerste pagina afwijkend", "Different Odd & Even Pages" => "Even en oneven pagina's afwijkend", + "Scale with Document" => "Meeschalen met document", + "Align with Page Margins" => "Uitlijnen op paginamarges", "Go to Footer" => "Naar voettekst", "Ruler" => "Liniaal", "File" => "Bestand", diff --git a/crates/ui-egui/src/i18n/pl.rs b/crates/ui-egui/src/i18n/pl.rs index e3185d55a..9b4c8b90a 100644 --- a/crates/ui-egui/src/i18n/pl.rs +++ b/crates/ui-egui/src/i18n/pl.rs @@ -322,6 +322,8 @@ pub(super) fn translate(text: &str) -> Option<&'static str> { "Go to Header" => "Przejdź do nagłówka", "Different First Page" => "Inna pierwsza strona", "Different Odd & Even Pages" => "Inne strony nieparzyste i parzyste", + "Scale with Document" => "Skaluj z dokumentem", + "Align with Page Margins" => "Wyrównaj do marginesów strony", "Go to Footer" => "Przejdź do stopki", "Ruler" => "Linijka", "File" => "Plik", diff --git a/crates/ui-egui/src/i18n/pt.rs b/crates/ui-egui/src/i18n/pt.rs index 1b56ff504..6e6349657 100644 --- a/crates/ui-egui/src/i18n/pt.rs +++ b/crates/ui-egui/src/i18n/pt.rs @@ -328,6 +328,8 @@ pub(super) fn translate(text: &str) -> Option<&'static str> { "Go to Header" => "Ir para o Cabeçalho", "Different First Page" => "Primeira Página Diferente", "Different Odd & Even Pages" => "Páginas Pares e Ímpares Diferentes", + "Scale with Document" => "Dimensionar com o Documento", + "Align with Page Margins" => "Alinhar com as Margens da Página", "Go to Footer" => "Ir para o Rodapé", "Ruler" => "Régua", // File › Backstage view. diff --git a/crates/ui-egui/src/i18n/ro.rs b/crates/ui-egui/src/i18n/ro.rs index 9e0d92516..abd3faa4c 100644 --- a/crates/ui-egui/src/i18n/ro.rs +++ b/crates/ui-egui/src/i18n/ro.rs @@ -329,6 +329,8 @@ pub(super) fn translate(text: &str) -> Option<&'static str> { "Go to Header" => "Salt la antet", "Different First Page" => "Prima pagină diferită", "Different Odd & Even Pages" => "Pagini pare și impare diferite", + "Scale with Document" => "Scalare cu documentul", + "Align with Page Margins" => "Aliniere la marginile paginii", "Go to Footer" => "Salt la subsol", "Ruler" => "Riglă", "File" => "Fișier", diff --git a/crates/ui-egui/src/i18n/ru.rs b/crates/ui-egui/src/i18n/ru.rs index ce675701e..dfbc945c4 100644 --- a/crates/ui-egui/src/i18n/ru.rs +++ b/crates/ui-egui/src/i18n/ru.rs @@ -332,6 +332,8 @@ pub(super) fn translate(text: &str) -> Option<&'static str> { "Go to Header" => "Перейти к верхнему колонтитулу", "Different First Page" => "Особый колонтитул для первой страницы", "Different Odd & Even Pages" => "Разные колонтитулы для четных и нечетных страниц", + "Scale with Document" => "Масштабировать вместе с документом", + "Align with Page Margins" => "Выровнять по полям страницы", "Go to Footer" => "Перейти к нижнему колонтитулу", "Ruler" => "Линейка", // File › Backstage view. diff --git a/crates/ui-egui/src/i18n/sr.rs b/crates/ui-egui/src/i18n/sr.rs index c71ce53a8..71c600feb 100644 --- a/crates/ui-egui/src/i18n/sr.rs +++ b/crates/ui-egui/src/i18n/sr.rs @@ -325,6 +325,8 @@ pub(super) fn translate(text: &str) -> Option<&'static str> { "Go to Header" => "Idi na zaglavlje", "Different First Page" => "Drugačija prva stranica", "Different Odd & Even Pages" => "Drugačije neparne i parne stranice", + "Scale with Document" => "Skaliraj sa dokumentom", + "Align with Page Margins" => "Poravnaj sa marginama stranice", "Go to Footer" => "Idi na podnožje", "Ruler" => "Lenjir", "File" => "Datoteka", diff --git a/crates/ui-egui/src/i18n/tr.rs b/crates/ui-egui/src/i18n/tr.rs index ce2bc8e1e..86f950375 100644 --- a/crates/ui-egui/src/i18n/tr.rs +++ b/crates/ui-egui/src/i18n/tr.rs @@ -205,6 +205,8 @@ pub(super) fn translate(text: &str) -> Option<&'static str> { "Footer" => "Altbilgi", "Different First Page" => "İlk Sayfada Farklı", "Different Odd & Even Pages" => "Tek ve Çift Sayfalarda Farklı", + "Scale with Document" => "Belgeyle Ölçeklendir", + "Align with Page Margins" => "Sayfa Kenar Boşluklarıyla Hizala", "Go to Footer" => "Altbilgiye Git", "Go to Header" => "Üstbilgiye Git", "Go to\nFooter" => "Altbilgiye\nGit", diff --git a/crates/ui-egui/src/i18n/uk.rs b/crates/ui-egui/src/i18n/uk.rs index 97659cc12..24a3c0756 100644 --- a/crates/ui-egui/src/i18n/uk.rs +++ b/crates/ui-egui/src/i18n/uk.rs @@ -325,6 +325,8 @@ pub(super) fn translate(text: &str) -> Option<&'static str> { "Go to Header" => "Перейти до верхнього колонтитула", "Different First Page" => "Окремий колонтитул для першої сторінки", "Different Odd & Even Pages" => "Різні колонтитули для парних і непарних сторінок", + "Scale with Document" => "Масштабувати разом із документом", + "Align with Page Margins" => "Вирівняти за полями сторінки", "Go to Footer" => "Перейти до нижнього колонтитула", "Ruler" => "Лінійка", // File › Backstage view. diff --git a/crates/ui-egui/src/i18n/vi.rs b/crates/ui-egui/src/i18n/vi.rs index 94c32b3a5..11fe14fcf 100644 --- a/crates/ui-egui/src/i18n/vi.rs +++ b/crates/ui-egui/src/i18n/vi.rs @@ -330,6 +330,8 @@ pub(super) fn translate(text: &str) -> Option<&'static str> { "Go to Header" => "Đi tới đầu trang", "Different First Page" => "Trang đầu khác", "Different Odd & Even Pages" => "Trang chẵn lẻ khác nhau", + "Scale with Document" => "Co giãn theo tài liệu", + "Align with Page Margins" => "Căn theo lề trang", "Go to Footer" => "Đi tới chân trang", "Ruler" => "Thước", // File › Backstage view. diff --git a/crates/ui-egui/src/i18n/zh.rs b/crates/ui-egui/src/i18n/zh.rs index c9d3ecb7d..c03dfb384 100644 --- a/crates/ui-egui/src/i18n/zh.rs +++ b/crates/ui-egui/src/i18n/zh.rs @@ -330,6 +330,8 @@ pub(super) fn translate(text: &str) -> Option<&'static str> { "Go to Header" => "转至页眉", "Different First Page" => "首页不同", "Different Odd & Even Pages" => "奇偶页不同", + "Scale with Document" => "随文档缩放", + "Align with Page Margins" => "与页边距对齐", "Go to Footer" => "转至页脚", "Ruler" => "标尺", // File › Backstage view. diff --git a/crates/ui-egui/src/paper_view.rs b/crates/ui-egui/src/paper_view.rs index 621a97903..d0005d76d 100644 --- a/crates/ui-egui/src/paper_view.rs +++ b/crates/ui-egui/src/paper_view.rs @@ -182,7 +182,8 @@ fn tr<'a>(app: &SheetApp, s: &'a str) -> &'a str { /// Section rectangles (left, centre, right) of a page's header or footer on screen. fn section_rects(geo: &Geo, pg: &PageGrid, (i, j): (usize, usize), footer: bool) -> [Rect; 3] { let (px, py, w, h) = pg.page_rect(i, j); - let [l, r, t, b, ..] = pg.margins; + let [_, _, t, b, ..] = pg.margins; + let [l, r] = pg.hf_sides; let (y0, y1) = if footer { (py + h - b, py + h) } else { (py, py + t) }; let inner = (w - l - r).max(3.0); let third = inner / 3.0; @@ -195,8 +196,8 @@ fn section_rects(geo: &Geo, pg: &PageGrid, (i, j): (usize, usize), footer: bool) out } -fn hf_font(geo: &Geo, size_pt: Option, bold: bool) -> FontId { - let px = size_pt.unwrap_or(10.0).clamp(1.0, 72.0) * 96.0 / 72.0 * geo.z; +fn hf_font(geo: &Geo, pg: &PageGrid, size_pt: Option, bold: bool) -> FontId { + let px = size_pt.unwrap_or(10.0).clamp(1.0, 72.0) * 96.0 / 72.0 * pg.hf_text_k * geo.z; let px = px.clamp(4.0, 120.0); if bold { theme::ui_bold(px) } else { theme::ui_font(px) } } @@ -235,7 +236,7 @@ pub(crate) fn paint_headers_footers(app: &SheetApp, ui: &egui::Ui, p: &Painter, if spec.is_empty() { if hovered { let label = if footer { "Add footer" } else { "Add header" }; - p.text(rects[1].center(), Align2::CENTER_CENTER, tr(app, label), hf_font(geo, Some(10.0), false), Color32::from_gray(120)); + p.text(rects[1].center(), Align2::CENTER_CENTER, tr(app, label), hf_font(geo, &pg, Some(10.0), false), Color32::from_gray(120)); } continue; } @@ -255,7 +256,7 @@ pub(crate) fn paint_headers_footers(app: &SheetApp, ui: &egui::Ui, p: &Painter, if sec.text.trim().is_empty() { continue; } - let font = hf_font(geo, sec.size, sec.bold); + let font = hf_font(geo, &pg, sec.size, sec.bold); let r = rects[k]; let (anchor, x) = match k { 0 => (Align2::LEFT_TOP, r.left()), @@ -326,8 +327,9 @@ pub fn start_editing(app: &mut SheetApp) { app.grid.hf_start = true; } -/// Turns Different First Page / Different Odd & Even Pages on or off, keeping the edit on the -/// same page (which may now show another header). +/// Turns a Header & Footer option (Different First Page, Different Odd & Even Pages, Scale with +/// Document, Align with Page Margins) on or off, keeping the edit on the same page (which may +/// now show another header). fn set_option(app: &mut SheetApp, key: &str, on: bool) { let Some(e) = app.grid.hf_edit.as_ref() else { return }; let (sheet, page, zone) = (e.sheet, e.page, HfZone { footer: e.footer, section: e.section }); @@ -446,7 +448,7 @@ fn editor(app: &mut SheetApp, ui: &mut egui::Ui, geo: &Geo, pg: &PageGrid) { let rects = section_rects(geo, pg, e.page, e.footer); let p = ui.painter().with_clip_rect(geo.cells); let t = Tokens::get(ui.ctx()); - let font = hf_font(geo, Some(10.0), false); + let font = hf_font(geo, pg, Some(10.0), false); let mut switch_to = None; for (k, r) in rects.iter().enumerate() { if !r.intersects(geo.cells) { @@ -893,11 +895,10 @@ pub fn ribbon_tab(app: &mut SheetApp, ui: &mut egui::Ui) { go_to(app, true); } sep(ui); - let (first, odd_even) = app - .session - .active() - .and_then(|d| d.wb.sheet(app.grid.hf_edit.as_ref().map_or(0, |e| e.sheet)).map(|s| (s.print.different_first, s.print.different_odd_even))) - .unwrap_or_default(); + let (first, odd_even, scale_doc, align) = + app.session.active().and_then(|d| d.wb.sheet(app.grid.hf_edit.as_ref().map_or(0, |e| e.sheet))).map_or((false, false, true, true), |s| { + (s.print.different_first, s.print.different_odd_even, s.print.hf_scale_with_doc, s.print.hf_align_with_margins) + }); ui.vertical(|ui| { let (mut f, mut o) = (first, odd_even); if ui.checkbox(&mut f, tr(app, "Different First Page")).changed() { @@ -907,6 +908,15 @@ pub fn ribbon_tab(app: &mut SheetApp, ui: &mut egui::Ui) { set_option(app, "differentOddEven", o); } }); + ui.vertical(|ui| { + let (mut sd, mut am) = (scale_doc, align); + if ui.checkbox(&mut sd, tr(app, "Scale with Document")).changed() { + set_option(app, "scaleWithDoc", sd); + } + if ui.checkbox(&mut am, tr(app, "Align with Page Margins")).changed() { + set_option(app, "alignWithMargins", am); + } + }); } fn sep(ui: &mut egui::Ui) { diff --git a/crates/ui-egui/tests/it/page_layout_view.rs b/crates/ui-egui/tests/it/page_layout_view.rs index b2e80d0d9..863ad2b08 100644 --- a/crates/ui-egui/tests/it/page_layout_view.rs +++ b/crates/ui-egui/tests/it/page_layout_view.rs @@ -2,6 +2,7 @@ //! transform, and headers are edited in place, committed on a click elsewhere, and undoable. use egui::{Event, Key, Modifiers, PointerButton, Pos2, pos2, vec2}; +use egui_kittest::kittest::Queryable; use gridcraft_engine::Session; use gridcraft_engine::core::CellRef; use gridcraft_engine::model::SheetViewMode; @@ -163,6 +164,27 @@ fn the_first_page_edits_its_own_header_when_different_first_page_is_on() { assert_eq!(pr.header, "&LLeft", "the other pages keep their header"); } +#[test] +fn scale_with_document_and_align_with_page_margins_are_check_boxes_on_the_header_and_footer_tab() { + let mut h = fixture(); + gridcraft_ui_egui::paper_view::start_editing(h.state_mut()); + h.run_steps(4); + let options = |h: &Harness| { + let pr = &h.state().session.doc().unwrap().wb.active().unwrap().print; + (pr.hf_scale_with_doc, pr.hf_align_with_margins) + }; + assert_eq!(options(&h), (true, true)); + h.get_by_label("Scale with Document").click(); + h.run_steps(3); + assert_eq!(options(&h), (false, true)); + h.get_by_label("Align with Page Margins").click(); + h.run_steps(3); + assert_eq!(options(&h), (false, false)); + assert!(h.state().grid.hf_edit.is_some(), "the header stays in edit"); + h.state_mut().run("edit.undo", json!({})).unwrap(); + assert_eq!(options(&h), (false, true)); +} + #[test] fn insert_header_and_footer_switches_to_page_layout_view() { let mut h = fixture(); diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 5d5f1efee..8cb3ce5ac 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -32,7 +32,7 @@ Sources: `crates/ui-egui/src` (ribbon, grid, editor, formula bar, dialogs, panes | Dark mode, system appearance | yes | yes | done (10-09); follows the system on first run, title-bar switch and File › Display theme (#194, #239) | | Backstage (File) view: Info, New with templates, Open recent, Export, Options | yes | yes (#174, 10-11): the File tab opens a full-window view (Esc or the back arrow return) with Home, New (blank + the three samples), Open (recent + Browse; the recent list tells when each workbook was last changed, 10-12), Info (location, properties, Protect Workbook), Save, Save As, Print (page preview, active sheet or workbook, fit to page, Page Setup), Export (PDF, CSV, web page), Close and Options (`file.options`: theme, language, formula bar, AutoSave, calculation) | no Account, Share or template gallery beyond the samples; Options applies at once (no OK/Cancel dialog) and holds only the settings GridCraft has | | Dark mode, system appearance | yes | yes | done (10-09) | -| Page Layout view (paper view with rulers and header editing) | yes | yes (#329, 10-11): per-sheet view saved in XLSX, pages with margins and gaps on the print pagination, "Click to add data" pages, rulers (inches or centimetres), headers and footers drawn and edited in place with a Header & Footer tab | margins drag on the rulers (#792, with header and footer margins); header/footer pictures, Different First Page / Odd & Even missing (1–3 h) | +| Page Layout view (paper view with rulers and header editing) | yes | yes (#329, 10-11): per-sheet view saved in XLSX, pages with margins and gaps on the print pagination, "Click to add data" pages, rulers (inches or centimetres), headers and footers drawn and edited in place with a Header & Footer tab, whose Scale with Document and Align with Page Margins set whether header and footer text follows the print scale and the side margins, on paper and in the view | margins drag on the rulers (#792, with header and footer margins); header/footer pictures, Different First Page / Odd & Even missing (1–3 h) | | Multiple windows per workbook | yes | no (#162) | see [hardware-parity.md](hardware-parity.md) | ## Grid interaction