From 42c1f71b52f361475ef4df9291d46ccce8926dcb Mon Sep 17 00:00:00 2001 From: Dmitrii Khlapov Date: Wed, 14 Oct 2026 19:58:33 +0200 Subject: [PATCH] =?UTF-8?q?Custom=20page=20sizes:=20Size=20=E2=80=BA=20Mor?= =?UTF-8?q?e=20Paper=20Sizes=20and=20Page=20Setup=20width/height=20(#1433)?= =?UTF-8?q?=20(#1496)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * #1433: WIP * Co-Authored-By: Claude Opus 5.5 * Co-Authored-By: Claude Opus 5.5 * #1433: one OK is one undo step; errors stay in the dialog Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 Co-authored-by: Brandon Thomas --- ROADMAP.md | 4 +- crates/engine/src/cmd/page.rs | 54 +++- crates/engine/src/tests.rs | 66 +++++ crates/ui-egui/src/dialogs.rs | 444 ++++++++++++++++++++++++++-- crates/ui-egui/src/i18n/bg.tsv | 5 + crates/ui-egui/src/i18n/cs.tsv | 5 + crates/ui-egui/src/i18n/de.tsv | 5 + crates/ui-egui/src/i18n/es.tsv | 5 + crates/ui-egui/src/i18n/et.tsv | 5 + crates/ui-egui/src/i18n/fr.tsv | 5 + crates/ui-egui/src/i18n/hu.tsv | 5 + crates/ui-egui/src/i18n/it.tsv | 5 + crates/ui-egui/src/i18n/ja.tsv | 5 + crates/ui-egui/src/i18n/pl.tsv | 5 + crates/ui-egui/src/i18n/pt-br.tsv | 5 + crates/ui-egui/src/i18n/ru.tsv | 5 + crates/ui-egui/src/i18n/sr-latn.tsv | 5 + crates/ui-egui/src/i18n/sr.tsv | 5 + crates/ui-egui/src/i18n/tr.tsv | 5 + crates/ui-egui/src/i18n/uk.tsv | 5 + crates/ui-egui/src/i18n/vi.tsv | 7 + crates/ui-egui/src/i18n/zh-hans.tsv | 5 + crates/ui-egui/src/i18n/zh-hant.tsv | 5 + crates/ui-egui/src/ribbon.rs | 28 +- docs/ui-parity.md | 5 +- 25 files changed, 653 insertions(+), 45 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 9c896ed00..6fdcdba2d 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,7 +2,7 @@ **Stage: alpha** · next: beta, ~15 points (60% → 75% ready for real work) and ~150–230 h away -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** trivial (progress log: HTML table cells and region paper for opened HTML, #1381; previously trivial: progress log: rounded rectangle corners, #1476; previously trivial: progress log: Save As type list, #1399; previously trivial: progress log: privacy options on save, #1062; previously trivial: progress log: equation pictures in theme colours on Windows, #1002; previously trivial: symbol fonts, #776; previously trivial: the save prompt answers from the keyboard, #861; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously trivial: formatting revisions landed, #41; previously trivial: Draw tab ink, #307, and Draw Table and Eraser, #303, landed: catalog 402/434; previously minor: readiness table with hours per audience; full number back to the additive weighted sum, 60%; mainstream and essentials numbers added; alpha gate checked; previously major: full re-measure against Word for Mac 16.113.4; restructured to the craftrules progress-docs standard) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** trivial (progress log: HTML table cells and region paper for opened HTML, #1381; previously trivial: progress log: custom page size, #1433; previously trivial: progress log: rounded rectangle corners, #1476; previously trivial: progress log: Save As type list, #1399; previously trivial: progress log: privacy options on save, #1062; previously trivial: progress log: equation pictures in theme colours on Windows, #1002; previously trivial: symbol fonts, #776; previously trivial: the save prompt answers from the keyboard, #861; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously trivial: formatting revisions landed, #41; previously trivial: Draw tab ink, #307, and Draw Table and Eraser, #303, landed: catalog 402/434; previously minor: readiness table with hours per audience; full number back to the additive weighted sum, 60%; mainstream and essentials numbers added; alpha gate checked; previously major: full re-measure against Word for Mac 16.113.4; restructured to the craftrules progress-docs standard) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 > **Printing update:** 2026-10-12 · direct desktop printing through craft-print; details below. @@ -153,6 +153,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm | 2026-10-13 | Copying an equation also puts MathML in HTML on the clipboard beside the linear text like `y=2x_s` (#816; Word/LibreOffice paste not yet verified; OMML is #1297). HTML export writes equations as MathML too, and opening or pasting HTML with MathML gives equations back | | 2026-10-13 | The Equation tab's galleries, structure buttons and symbols keep the theme's colours on Windows: their pictures share a few textures written in place (no wipe of pictures already on screen, room for the built-ins at high display scales), instead of hundreds of small textures freed in bulk, which on DirectX 12 showed inverted, red or teal tiles (#1002) | | 2026-10-14 | HTML import (#1381): header cells (``) are bold unless their own CSS says otherwise, and `text-align` on a cell, its row, row group or table aligns the cell's paragraphs (a paragraph's own alignment wins), so `wordcraft-cli convert page.html out.docx` writes the bold header and `w:jc` right-aligned cells. HTML, Markdown and plain text, which give no page size, open on the region's paper (A4 in Italy, Letter in the US) like a new blank document, in the app and in `wordcraft-cli`; documents with their own page size keep it | +| 2026-10-14 | Custom page sizes (#1433): Layout › Size ends with More Paper Sizes…, which opens Page Setup's new Paper group — a Paper size list (the named sizes, or Custom size) and Width and Height in the measurement unit; OK sets the size only when it changed, and a wider-than-tall size comes out landscape. `layout.size` now clamps each side to Word's 0.1"–22" and refuses a page the margins don't fit (the margins rule: 36 pt of text room), and Page Setup checks the size and margins together before applying either | | 2026-10-13 | Full screen (#980): F11, or View › Window › Full Screen, fills the screen with the window (⌃⌘F on macOS); F11 again or Escape gives back the window as it was. Next Field, which Word puts on F11, keeps Word's Alt+F1 and Alt+Shift+F1 (and Shift+F11). The web app leaves full screen to the browser's own F11 | | 2026-10-13 | Double-clicking the top or bottom margin of a page creates its header or footer when it has none and puts the caret in it, as in Word (#960): the one that page shows (different first page, odd and even pages), only in Print Layout; one left empty is taken away again when you leave it, so nothing empty is saved. `insert.editHeader`/`insert.editFooter` take `{"page"}` for agents | | 2026-10-13 | Alt+numpad character codes, as in Word on Windows (#961): hold Alt, type a decimal code, release Alt — `0233` → é (Windows-1252), `233` → Θ (OEM code page 437; 850 in Western European locales and English outside the US), codes above 255 are Unicode (`8364` → €). Not on macOS; `text.typeCode` for agents | @@ -319,6 +320,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm | 2026-10-13 | trivial | Progress log: clicks on ribbon chrome keep typing in the document (#1093) | | 2026-10-13 | trivial | Progress log: dragged shapes wrap in their new column (#884) | | 2026-10-14 | trivial | Progress log: HTML header cells, cell alignment and region paper for opened HTML/Markdown/text (#1381) | +| 2026-10-14 | trivial | Progress log: custom page size from More Paper Sizes… and Page Setup (#1433) | | 2026-10-13 | trivial | Progress log: full screen on F11 (#980) | | 2026-10-13 | trivial | Progress log: interface audit back in CI (#1128) | | 2026-10-13 | trivial | Progress log: double-click the margin to create and edit a header or footer (#960) | diff --git a/crates/engine/src/cmd/page.rs b/crates/engine/src/cmd/page.rs index 2e9b7f4fa..b3cbcd6f2 100644 --- a/crates/engine/src/cmd/page.rs +++ b/crates/engine/src/cmd/page.rs @@ -23,7 +23,7 @@ pub fn specs() -> Vec { with_sect(s, |x| x.set_landscape(land)) }) .params(r#"{"value": "portrait|landscape"}"#), - CommandSpec::new("layout.size", "Size", "Layout › Page Setup", size).params(r#"{"name"?: "Letter|Legal|A4|…", "width"?: pt, "height"?: pt}"#), + CommandSpec::new("layout.size", "Size", "Layout › Page Setup", size).params(r#"{"name"?: "Letter|Legal|A4|…", "width"?: pt, "height"?: pt} (each side clamped to 7.2–1584 pt, 0.1–22 in; the section keeps its orientation)"#), CommandSpec::new("layout.columns", "Columns", "Layout › Page Setup", columns) .params( r#"{"count": 1-12, "space"?: pt, "separator"?: bool, "preset"?: "left|right", "widths"?: [[width pt, space after pt], …] (unequal columns; sets the count), "apply"?: "section|document|forward"}"#, @@ -225,15 +225,19 @@ pub fn sect(s: &Session) -> SectionProps { s.doc.sections().get(i).map(|(_, x)| (*x).clone()).unwrap_or_default() } -/// Apply to the section(s) of the selection. -pub(crate) fn with_sect(s: &mut Session, f: impl Fn(&mut SectionProps)) -> CmdResult { +/// The last block of each section of the selection. +fn selected_section_ends(s: &Session) -> Vec { let (a, b) = s.sel.ordered(); let (i0, i1) = (a.path.0.first().copied().unwrap_or(0) as usize, b.path.0.first().copied().unwrap_or(0) as usize); let ends: Vec = s.doc.sections().iter().map(|(e, _)| *e).collect(); let first = ends.iter().position(|e| i0 <= *e).unwrap_or(0); let last = ends.iter().position(|e| i1 <= *e).unwrap_or(first); - for k in first..=last { - let block = ends.get(k).copied().unwrap_or(0); + (first..=last).map(|k| ends.get(k).copied().unwrap_or(0)).collect() +} + +/// Apply to the section(s) of the selection. +pub(crate) fn with_sect(s: &mut Session, f: impl Fn(&mut SectionProps)) -> CmdResult { + for block in selected_section_ends(s) { f(s.doc.section_mut(block)); } Ok(serde_json::to_value(sect(s)).unwrap_or(Value::Null)) @@ -355,7 +359,7 @@ fn margins(s: &mut Session, v: &Value) -> CmdResult { }; let gutter = p::f32(v, "gutter"); let cur = sect(s); - if l + r + gutter.unwrap_or(cur.gutter) > cur.page_w - 36.0 || t + b > cur.page_h - 36.0 { + if !margins_fit((cur.page_w, cur.page_h), (t, b, l, r), gutter.unwrap_or(cur.gutter)) { return Err(CmdError::Params("margins leave no room for text".into())); } // A preset picks mirrored or not; plain numbers (a ruler drag, an agent) keep the setting. @@ -373,6 +377,23 @@ fn margins(s: &mut Session, v: &Value) -> CmdResult { }) } +/// Why `layout.size` refuses a page its margins don't fit. +pub const MARGINS_DONT_FIT: &str = "The margins don't fit on a page this size; make the margins smaller first."; + +/// Text room the margins must leave on a page, each way, points. +pub const MIN_TEXT_ROOM: f32 = 36.0; + +/// Whether margins (top, bottom, left, right) and a gutter leave text room on a page (width, +/// height), all in points: the rule `layout.margins` and `layout.size` both apply, and Page +/// Setup checks before it runs either. +pub fn margins_fit(page: (f32, f32), (t, b, l, r): (f32, f32, f32, f32), gutter: f32) -> bool { + l + r + gutter <= page.0 - MIN_TEXT_ROOM && t + b <= page.1 - MIN_TEXT_ROOM +} + +/// The smallest and largest page side, points (0.1" and 22", as Word allows). +pub const MIN_PAGE_SIDE: f32 = 7.2; +pub const MAX_PAGE_SIDE: f32 = 1584.0; + fn size(s: &mut Session, v: &Value) -> CmdResult { let (w, h) = if let Some(n) = p::str(v, "name") { let (_, w, h) = wordcraft_geom::PAPER_SIZES @@ -383,14 +404,21 @@ fn size(s: &mut Session, v: &Value) -> CmdResult { } else { (p::req_f32(v, "width")?, p::req_f32(v, "height")?) }; - if !(72.0..=1584.0).contains(&w) || !(72.0..=1584.0).contains(&h) { - return Err(CmdError::Params("page size must be 1\"–22\"".into())); + // Word's range per side is 0.1"–22" (#1433); `p::f32` already refused NaN and infinities. + let (w, h) = (w.clamp(MIN_PAGE_SIDE, MAX_PAGE_SIDE), h.clamp(MIN_PAGE_SIDE, MAX_PAGE_SIDE)); + // Each section keeps its orientation: the long side across in landscape, down in portrait. + let oriented = |x: &SectionProps| if x.landscape { (w.max(h), w.min(h)) } else { (w.min(h), w.max(h)) }; + let sections = s.doc.sections(); + let fits = |block: usize| { + sections + .iter() + .find(|(e, _)| *e == block) + .is_none_or(|(_, x)| margins_fit(oriented(x), (x.margin_top, x.margin_bottom, x.margin_left, x.margin_right), x.gutter)) + }; + if !selected_section_ends(s).into_iter().all(fits) { + return Err(CmdError::Params(MARGINS_DONT_FIT.into())); } - with_sect(s, |x| { - let (pw, ph) = if x.landscape { (w.max(h), w.min(h)) } else { (w.min(h), w.max(h)) }; - x.page_w = pw; - x.page_h = ph; - }) + with_sect(s, |x| (x.page_w, x.page_h) = oriented(x)) } fn columns(s: &mut Session, v: &Value) -> CmdResult { diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 9b3eb63d7..eec8ffffa 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -1512,6 +1512,71 @@ fn page_setup_and_breaks() { assert!(s.run("layout.margins", &json!({"left": 1000.0})).is_err()); } +/// #1433: a custom page size (More Paper Sizes…) sets the section in its orientation, and +/// out-of-range sizes clamp to Word's 0.1"–22" instead of failing or reaching the layout. +#[test] +fn layout_size_takes_a_custom_size_and_clamps_it() { + let mut s = s(); + let size = |s: &Session| (s.doc.last_section.page_w, s.doc.last_section.page_h); + run(&mut s, "layout.size", json!({"width": 300.0, "height": 500.0})); + assert_eq!(size(&s), (300.0, 500.0)); + run(&mut s, "layout.margins", json!({"top": 0.0, "bottom": 0.0, "left": 0.0, "right": 0.0})); + run(&mut s, "layout.size", json!({"width": 40.0, "height": 5000.0})); + assert_eq!(size(&s), (40.0, 1584.0), "clamped to 22\""); + for junk in [ + json!({"width": 1e300, "height": 400.0}), + json!({"width": "NaN", "height": 400.0}), + json!({"width": null}), + json!({}), + // Clamped to 0.1", still less than the 36 pt of text room the margins need. + json!({"width": -50.0, "height": 400.0}), + json!({"width": 1.0, "height": 400.0}), + ] { + assert!(s.run("layout.size", &junk).is_err(), "{junk}"); + assert_eq!(size(&s), (40.0, 1584.0), "{junk} leaves the size alone"); + } + // A landscape section keeps its orientation: the long side goes across. + run(&mut s, "layout.orientation", json!({"value": "landscape"})); + run(&mut s, "layout.size", json!({"width": 300.0, "height": 500.0})); + assert_eq!(size(&s), (500.0, 300.0)); + assert!(s.doc.last_section.landscape); +} + +/// #1433: `layout.size` refuses a page its margins don't fit, by the rule `layout.margins` uses +/// (36 pt of text room each way), and changes nothing. +#[test] +fn layout_size_refuses_a_page_its_margins_dont_fit() { + let mut s = s(); + run(&mut s, "layout.margins", json!({"preset": "normal"})); + let before = s.doc.last_section.clone(); + for (w, h) in [(100.0, 500.0), (500.0, 100.0), (179.0, 500.0)] { + let e = s.run("layout.size", &json!({"width": w, "height": h})).unwrap_err().to_string(); + assert!(e.contains("margins don't fit"), "{w}×{h}: {e}"); + assert_eq!(s.doc.last_section, before, "{w}×{h} changes nothing"); + } + run(&mut s, "layout.size", json!({"width": 180.0, "height": 180.0})); + assert_eq!((s.doc.last_section.page_w, s.doc.last_section.page_h), (180.0, 180.0), "exactly 36 pt of room is enough"); +} + +/// #1433: the smallest page there can be (0.1" wide, no margins, set straight through Page +/// Setup's section) still lays out and exports text, a table and two columns. +#[test] +fn a_tiny_page_lays_out_and_exports() { + let mut s = s(); + run(&mut s, "text.insert", json!({"text": "A paragraph of text that cannot fit on so small a page."})); + run(&mut s, "insert.table", json!({"rows": 2, "cols": 3})); + run(&mut s, "text.insert", json!({"text": "cell"})); + run(&mut s, "layout.columns", json!({"count": 2})); + let mut sect = s.doc.last_section.clone(); + (sect.page_w, sect.page_h) = (7.2, 40.0); + (sect.margin_top, sect.margin_bottom, sect.margin_left, sect.margin_right, sect.gutter) = (0.0, 0.0, 0.0, 0.0, 0.0); + run(&mut s, "layout.pageSetup", json!({"section": serde_json::to_value(§).unwrap()})); + assert_eq!((s.doc.last_section.page_w, s.doc.last_section.page_h), (7.2, 40.0)); + assert!(!s.layout().pages.is_empty()); + let pdf = crate::io::save_bytes("tiny.pdf", &s.doc).unwrap(); + assert!(pdf.starts_with(b"%PDF")); +} + #[test] fn track_changes_and_accept() { let mut s = s(); @@ -2141,6 +2206,7 @@ fn hostile_params_never_panic() { json!({}), json!({"text": 5, "size": "x", "path": [], "pos": {"block": 9999, "off": 99999}}), json!({"value": -1e308, "rows": 1e9}), + json!({"width": 1e-9, "height": 1e30, "top": -1e9, "left": 1e9}), ]; for spec in reg.all() { // These reach outside the session (files). Read Aloud (`review.readAloud`, `readAloud.*`) diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 4537d5ec4..dddf40d01 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -246,6 +246,14 @@ pub enum Dialog { grid_was: (u8, u32, u32), /// Section direction: right to left (`layout.sectionRtl`). rtl: bool, + /// Paper width and height in the measurement unit as shown (landscape: the wide side + /// first); `paper_was` as opened (`layout.size` runs only if it changed, #1433). + paper: (f32, f32), + paper_was: (f32, f32), + /// Custom size chosen in Paper size: shown even while the numbers match a named size. + paper_custom: bool, + /// Why OK didn't apply (the margins don't fit the paper). + message: String, }, Link { url: String, @@ -1074,7 +1082,12 @@ impl Dialog { }; let size = app.session.doc.styles.resolve_char(Some("Normal"), &Default::default()).size; let grid = (kind, sp.grid_lines_per_page().min(1000), sp.grid_chars_per_line(size).min(1000)); + let paper = (sp.page_w / crate::options::unit().pt_per_unit(), sp.page_h / crate::options::unit().pt_per_unit()); Dialog::PageSetup { + paper, + paper_was: paper, + paper_custom: false, + message: String::new(), top: sp.margin_top / crate::options::unit().pt_per_unit(), bottom: sp.margin_bottom / crate::options::unit().pt_per_unit(), left: sp.margin_left / crate::options::unit().pt_per_unit(), @@ -1387,6 +1400,126 @@ pub fn show(app: &mut WordApp, ctx: &egui::Context) { } } +/// Labels of Page Setup's paper size (#1433), in one place in case Word's wording differs. +pub(crate) const PAPER_LABELS: PaperLabels = + PaperLabels { group: "Paper", size: "Paper size:", custom: "Custom size", width: "Width:", height: "Height:", more: "More Paper Sizes…" }; + +pub(crate) struct PaperLabels { + pub group: &'static str, + pub size: &'static str, + pub custom: &'static str, + pub width: &'static str, + pub height: &'static str, + /// The Size menu's last item, opening Page Setup. + pub more: &'static str, +} + +/// The named paper size `paper` (in the measurement unit, either orientation) is, or None for a +/// custom size. +pub(crate) fn paper_entry(paper: (f32, f32)) -> Option<&'static str> { + let ppu = crate::options::unit().pt_per_unit(); + wordcraft_geom::paper_name(paper.0 * ppu, paper.1 * ppu) +} + +/// The commands Page Setup's OK runs for the paper, orientation and margins (#1433), all in the +/// measurement unit as the dialog shows them. Refuses when the margins don't fit the paper. +/// Otherwise it orders them so that none is refused on the way: the size first +/// when the section's margins fit it, else the margins first, else margins of 0 in between. +/// `layout.size` keeps the section's orientation; `layout.orientation` then turns the page and +/// its margins to what the dialog shows. A size equal to the opened one (either way round, +/// within 0.01 pt) doesn't run `layout.size`. +fn page_layout_commands( + app: &WordApp, + (paper, was): ((f32, f32), (f32, f32)), + landscape: bool, + (top, bottom, left, right): (f32, f32, f32, f32), +) -> Result, &'static str> { + use wordcraft_engine::cmd::page::{MARGINS_DONT_FIT, margins_fit, sect}; + let ppu = crate::options::unit().pt_per_unit(); + let (paper, was) = ((paper.0 * ppu, paper.1 * ppu), (was.0 * ppu, was.1 * ppu)); + let m = (top * ppu, bottom * ppu, left * ppu, right * ppu); + let cur = sect(&app.session); + if !margins_fit(paper, m, cur.gutter) { + return Err(MARGINS_DONT_FIT); + } + let turned = |on: bool, (a, b): (f32, f32)| if on { (a.max(b), a.min(b)) } else { (a.min(b), a.max(b)) }; + let ((w0, h0), (w1, h1)) = (turned(false, paper), turned(false, was)); + let new_size = (w0 - w1).abs() >= 0.01 || (h0 - h1).abs() >= 0.01; + let size = ("layout.size", json!({"width": paper.0, "height": paper.1})); + let orientation = ("layout.orientation", json!({"value": if landscape { "landscape" } else { "portrait" }})); + let margins = ("layout.margins", json!({"top": m.0, "bottom": m.1, "left": m.2, "right": m.3})); + let old_margins = (cur.margin_top, cur.margin_bottom, cur.margin_left, cur.margin_right); + Ok(if !new_size || margins_fit(turned(cur.landscape, paper), old_margins, cur.gutter) { + let mut v = if new_size { vec![size] } else { Vec::new() }; + v.extend([orientation, margins]); + v + } else if margins_fit(turned(landscape, (cur.page_w, cur.page_h)), m, cur.gutter) { + vec![orientation, margins, size] + } else { + vec![orientation, ("layout.margins", json!({"top": 0.0, "bottom": 0.0, "left": 0.0, "right": 0.0})), size, margins] + }) +} + +/// Run a dialog's commands as one undo step. If one fails, put the document and the undo stack +/// back as they were (nothing applied) and return its error. +fn run_as_one_step(app: &mut WordApp, commands: Vec<(&str, Value)>) -> Result<(), String> { + let snapshot = app.session.edit_snapshot(); + for (i, (id, params)) in commands.into_iter().enumerate() { + if i > 0 { + app.session.join_next_undo(); + } + if let Err(e) = app.run(id, params) { + app.session.restore(snapshot); + return Err(e); + } + } + Ok(()) +} + +/// Page Setup's Paper group (#1433): a Paper size list (the named sizes and Custom size) and +/// the width and height. The fields follow the orientation (the wide side is the width in +/// landscape), and a typed size that is wider than tall turns the orientation to landscape. +fn page_paper_group(ui: &mut Ui, paper: &mut (f32, f32), landscape: &mut bool, custom: &mut bool) { + if (*landscape && paper.0 < paper.1) || (!*landscape && paper.0 > paper.1) { + *paper = (paper.1, paper.0); + } + let unit = crate::options::unit(); + let ppu = unit.pt_per_unit(); + let l = &PAPER_LABELS; + ui.label(egui::RichText::new(tl!(l.group)).font(semibold(12.5))); + ui.horizontal(|ui| { + let label = ui.label(tl!(l.size)); + let entry = paper_entry(*paper).filter(|_| !*custom); + let shown = tl!(entry.unwrap_or(l.custom)); + let combo = egui::ComboBox::from_id_salt("ps_paper_size").width(180.0).height(400.0).selected_text(shown).show_ui(ui, |ui| { + for (n, w, h) in wordcraft_geom::PAPER_SIZES { + if ui.selectable_label(entry == Some(*n), tl!(n)).clicked() { + let (w, h) = (w / ppu, h / ppu); + *paper = if *landscape { (w.max(h), w.min(h)) } else { (w.min(h), w.max(h)) }; + *custom = false; + } + } + // Custom size keeps the numbers to edit, and stays chosen while they match a name. + if ui.selectable_label(entry.is_none(), tl!(l.custom)).clicked() { + *custom = true; + } + }); + combo.response.labelled_by(label.id); + }); + egui::Grid::new("ps_paper").num_columns(4).show(ui, |ui| { + use wordcraft_engine::cmd::page::{MAX_PAGE_SIDE, MIN_PAGE_SIDE}; + let range = MIN_PAGE_SIDE / ppu..=MAX_PAGE_SIDE / ppu; + ui.label(tl!(l.width)); + let w = ui.add(egui::DragValue::new(&mut paper.0).speed(3.6 / ppu).range(range.clone()).measure(unit).max_decimals(2)); + ui.label(tl!(l.height)); + let h = ui.add(egui::DragValue::new(&mut paper.1).speed(3.6 / ppu).range(range).measure(unit).max_decimals(2)); + if (w.changed() || h.changed()) && paper.0 != paper.1 { + *landscape = paper.0 > paper.1; + } + ui.end_row(); + }); +} + pub(crate) fn buttons(ui: &mut Ui, ok: &str) -> (bool, bool) { let mut r = (false, false); ui.add_space(8.0); @@ -1868,7 +2001,7 @@ fn body(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool { } ok || cancel } - Dialog::PageSetup { top, bottom, left, right, landscape, rtl, grid, grid_was } => { + Dialog::PageSetup { top, bottom, left, right, landscape, rtl, grid, grid_was, paper, paper_was, paper_custom, message } => { ui.label(egui::RichText::new(tl!("Margins")).font(semibold(12.5))); egui::Grid::new("ps").num_columns(4).show(ui, |ui| { ui.label(tl!("Top:")); @@ -1889,6 +2022,7 @@ fn body(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool { ui.radio_value(landscape, false, tl!("Portrait")); ui.radio_value(landscape, true, tl!("Landscape")); }); + page_paper_group(ui, paper, landscape, paper_custom); ui.label(egui::RichText::new(tl!("Document Grid")).font(semibold(12.5))); ui.horizontal_wrapped(|ui| { ui.radio_value(&mut grid.0, 0, tl!("No grid")); @@ -1908,24 +2042,34 @@ fn body(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool { ui.radio_value(rtl, false, tl!("Left-to-right")); ui.radio_value(rtl, true, tl!("Right-to-left")); }); - let (ok, cancel) = buttons(ui, tl!("OK")); + if !message.is_empty() { + ui.label(egui::RichText::new(message.as_str()).small().color(ui.visuals().error_fg_color)); + } + let (mut ok, cancel) = buttons(ui, tl!("OK")); if ok { - let _ = app.run("layout.orientation", json!({"value": if *landscape { "landscape" } else { "portrait" }})); - let _ = - app.run("layout.margins", json!({"top": *top * crate::options::unit().pt_per_unit(), "bottom": *bottom * crate::options::unit().pt_per_unit(), "left": *left * crate::options::unit().pt_per_unit(), "right": *right * crate::options::unit().pt_per_unit()})); - if grid != grid_was { - let kind = ["none", "lines", "linesAndChars", "snapToChars"].get(grid.0 as usize).copied().unwrap_or("none"); - let mut v = json!({"type": kind}); - if grid.1 != grid_was.1 { - v["linesPerPage"] = json!(grid.1); - } - if grid.2 != grid_was.2 { - v["charsPerLine"] = json!(grid.2); - } - let _ = app.run("layout.documentGrid", v); - } - if *rtl != wordcraft_engine::cmd::page::sect(&app.session).rtl { - let _ = app.run("layout.sectionRtl", json!({"value": *rtl})); + // Everything OK changes is one undo step; a refused command applies nothing. + let result = page_layout_commands(app, (*paper, *paper_was), *landscape, (*top, *bottom, *left, *right)) + .map_err(|e| tl!(e).to_string()) + .and_then(|mut commands| { + if grid != grid_was { + let kind = ["none", "lines", "linesAndChars", "snapToChars"].get(grid.0 as usize).copied().unwrap_or("none"); + let mut v = json!({"type": kind}); + if grid.1 != grid_was.1 { + v["linesPerPage"] = json!(grid.1); + } + if grid.2 != grid_was.2 { + v["charsPerLine"] = json!(grid.2); + } + commands.push(("layout.documentGrid", v)); + } + if *rtl != wordcraft_engine::cmd::page::sect(&app.session).rtl { + commands.push(("layout.sectionRtl", json!({"value": *rtl}))); + } + run_as_one_step(app, commands) + }); + if let Err(e) = result { + *message = e; + ok = false; } } ok || cancel @@ -3705,4 +3849,268 @@ mod tests { assert!((after.min.x - (before.min.x - 200.0)).abs() <= 1.0, "moved left: {before:?} -> {after:?}"); assert!((after.min.y - (before.min.y + 120.0)).abs() <= 1.0, "moved down: {before:?} -> {after:?}"); } + + fn app_harness(app: WordApp) -> egui_kittest::Harness<'static, WordApp> { + egui_kittest::Harness::builder().with_size(egui::vec2(1440.0, 900.0)).build_ui_state( + |ui, app: &mut WordApp| { + let ctx = ui.ctx().clone(); + app.logic(&ctx); + app.ui(ui); + }, + app, + ) + } + + /// The Page Setup paper size as opened, in points, and the Paper size entry it shows. + fn opened_paper(app: &mut WordApp) -> ((f32, f32), Option<&'static str>) { + let Some(Dialog::PageSetup { paper, .. }) = Dialog::open("pageSetup", app) else { panic!("no page setup dialog") }; + let ppu = crate::options::unit().pt_per_unit(); + ((paper.0 * ppu, paper.1 * ppu), paper_entry(paper)) + } + + /// #1433: Page Setup's Paper size shows the named size the section has, in either + /// orientation, and Custom size (None) for anything else. + #[test] + fn page_setup_reads_the_paper_size() { + let mut app = app_with("Page"); + let sect = |app: &WordApp| wordcraft_engine::cmd::page::sect(&app.session); + let ((w, h), entry) = opened_paper(&mut app); + assert!((w - sect(&app).page_w).abs() < 0.01 && (h - sect(&app).page_h).abs() < 0.01, "{w}×{h}"); + assert_eq!(entry, wordcraft_geom::paper_name(w, h)); + assert!(entry.is_some(), "a new document has a named size"); + let _ = app.run("layout.size", json!({"name": "A4"})); + let _ = app.run("layout.orientation", json!({"value": "landscape"})); + let ((w, h), entry) = opened_paper(&mut app); + assert!(w > h, "landscape shows the wide side as the width: {w}×{h}"); + assert_eq!(entry, Some("A4")); + let _ = app.run("layout.size", json!({"width": 300.0, "height": 500.0})); + assert_eq!(opened_paper(&mut app).1, None, "Custom size"); + } + + /// #1433: a custom width and height applied with OK set the section's size (before the + /// orientation, so a wide custom size ends up landscape); an unchanged size doesn't run + /// `layout.size`, and switching orientation alone only turns the page. + #[test] + fn page_setup_applies_a_custom_paper_size() { + use egui_kittest::kittest::Queryable; + let ppu = crate::options::unit().pt_per_unit(); + let size_steps = |app: &WordApp| app.session.undo_labels().iter().filter(|l| l.as_str() == "Size").count(); + let sect = |app: &WordApp| { + let s = wordcraft_engine::cmd::page::sect(&app.session); + (s.page_w.round(), s.page_h.round(), s.landscape) + }; + // OK on an unchanged dialog leaves the size alone. + let mut h = app_harness(app_with("Page")); + let before = sect(h.state()); + h.state_mut().run("ui.dialog", json!({"name": "pageSetup"})).unwrap(); + h.run_steps(3); + h.get_by_label("OK").click(); + h.run_steps(3); + assert!(h.state().dialog.is_none()); + assert_eq!((sect(h.state()), size_steps(h.state())), (before, 0), "no layout.size for an unchanged size"); + + for ((w, ht), want) in [((400.0, 600.0), (400.0, 600.0, false)), ((800.0, 400.0), (800.0, 400.0, true))] { + h.state_mut().run("ui.dialog", json!({"name": "pageSetup"})).unwrap(); + h.run_steps(2); + if let Some(Dialog::PageSetup { paper, landscape, .. }) = h.state_mut().dialog.as_mut() { + *paper = (w / ppu, ht / ppu); + *landscape = w > ht; + } + h.run_steps(2); + h.get_by_label("OK").click(); + h.run_steps(3); + assert_eq!(sect(h.state()), want, "{w}×{ht}"); + } + assert_eq!(size_steps(h.state()), 2); + + // Portrait again with the same paper: the dialog swaps width and height, which is no new size. + h.state_mut().run("ui.dialog", json!({"name": "pageSetup"})).unwrap(); + h.run_steps(2); + if let Some(Dialog::PageSetup { paper, landscape, .. }) = h.state_mut().dialog.as_mut() { + *paper = (paper.1, paper.0); + *landscape = false; + } + h.run_steps(2); + h.get_by_label("OK").click(); + h.run_steps(3); + assert_eq!(sect(h.state()), (400.0, 800.0, false)); + assert_eq!(size_steps(h.state()), 2, "turning the page isn't a new size"); + } + + /// #1433: the Paper group's own widgets: choosing A4 in Paper size fills Width and Height, + /// Landscape swaps them, OK gives an A4 landscape section; typing a width wider than the + /// height turns the orientation to Landscape and shows Custom size; choosing Custom size + /// keeps the numbers and stays chosen. + #[test] + fn page_setup_paper_widgets_work_together() { + use egui::accesskit::Role; + use egui_kittest::kittest::Queryable; + let ppu = crate::options::unit().pt_per_unit(); + let mut h = app_harness(app_with("Page")); + h.state_mut().run("ui.dialog", json!({"name": "pageSetup"})).unwrap(); + h.run_steps(3); + let state = |h: &egui_kittest::Harness<'_, WordApp>| match &h.state().dialog { + Some(Dialog::PageSetup { paper, landscape, .. }) => { + ((paper.0 * ppu * 100.0).round() / 100.0, (paper.1 * ppu * 100.0).round() / 100.0, *landscape) + } + _ => panic!("no page setup dialog"), + }; + let combo = |h: &egui_kittest::Harness<'_, WordApp>| h.get_by_role_and_label(Role::ComboBox, "Paper size:").value(); + assert_eq!(combo(&h).as_deref(), Some("Letter")); + h.get_by_role_and_label(Role::ComboBox, "Paper size:").click(); + h.run_steps(3); + h.get_by_label("A4").click(); + h.run_steps(3); + assert_eq!(state(&h), (595.28, 841.89, false), "A4 fills the fields"); + assert_eq!(combo(&h).as_deref(), Some("A4")); + h.get_by_label("Landscape").click(); + h.run_steps(3); + assert_eq!(state(&h), (841.89, 595.28, true), "Landscape swaps them"); + assert_eq!(combo(&h).as_deref(), Some("A4")); + h.get_by_label("OK").click(); + h.run_steps(3); + let sect = wordcraft_engine::cmd::page::sect(&h.state().session); + assert_eq!(((sect.page_w * 100.0).round() / 100.0, (sect.page_h * 100.0).round() / 100.0, sect.landscape), (841.89, 595.28, true)); + + // Portrait A4 again; a width wider than the height turns it to Landscape, Custom size. + h.state_mut().run("layout.orientation", json!({"value": "portrait"})).unwrap(); + h.state_mut().run("ui.dialog", json!({"name": "pageSetup"})).unwrap(); + h.run_steps(3); + h.get_by_value("8.27\"").focus(); + h.run_steps(2); + h.key_press_modifiers(egui::Modifiers::COMMAND, egui::Key::A); + h.event(egui::Event::Text("15".into())); + h.key_press(egui::Key::Tab); + h.run_steps(3); + assert_eq!(state(&h), (1080.0, 841.89, true), "wider than tall: Landscape"); + assert_eq!(combo(&h).as_deref(), Some("Custom size")); + + // Back to A4, then Custom size: the numbers stay and so does the choice. + h.get_by_role_and_label(Role::ComboBox, "Paper size:").click(); + h.run_steps(3); + h.get_by_label("A4").click(); + h.run_steps(3); + h.get_by_role_and_label(Role::ComboBox, "Paper size:").click(); + h.run_steps(3); + h.get_by_label("Custom size").click(); + h.run_steps(3); + assert_eq!(state(&h), (841.89, 595.28, true)); + assert_eq!(combo(&h).as_deref(), Some("Custom size")); + } + + /// #1433: Page Setup checks the new size and margins together: a page its margins don't fit + /// keeps the dialog open with a message and applies nothing; a smaller page with smaller + /// margins applies (margins first, so no step is refused on the way). + #[test] + fn page_setup_refuses_margins_that_dont_fit_and_applies_smaller_ones_first() { + use egui_kittest::kittest::Queryable; + let ppu = crate::options::unit().pt_per_unit(); + let mut h = app_harness(app_with("Page")); + let before = wordcraft_engine::cmd::page::sect(&h.state().session); + let undo = h.state().session.undo_depth(); + h.state_mut().run("ui.dialog", json!({"name": "pageSetup"})).unwrap(); + h.run_steps(2); + if let Some(Dialog::PageSetup { paper, .. }) = h.state_mut().dialog.as_mut() { + *paper = (100.0 / ppu, 500.0 / ppu); + } + h.run_steps(2); + h.get_by_label("OK").click(); + h.run_steps(3); + assert!( + matches!(&h.state().dialog, Some(Dialog::PageSetup { message, .. }) if message.contains("margins don't fit")), + "stays open with a message" + ); + assert_eq!((wordcraft_engine::cmd::page::sect(&h.state().session), h.state().session.undo_depth()), (before, undo), "nothing applied"); + + if let Some(Dialog::PageSetup { top, bottom, left, right, .. }) = h.state_mut().dialog.as_mut() { + (*top, *bottom, *left, *right) = (18.0 / ppu, 18.0 / ppu, 18.0 / ppu, 18.0 / ppu); + } + h.run_steps(2); + h.get_by_label("OK").click(); + h.run_steps(3); + assert!(h.state().dialog.is_none()); + let s = wordcraft_engine::cmd::page::sect(&h.state().session); + assert_eq!((s.page_w.round(), s.page_h.round(), s.margin_left.round(), s.margin_top.round()), (100.0, 500.0, 18.0, 18.0)); + } + + /// Open Page Setup in `h`, set its paper and margins (points) and press OK. + fn page_setup_ok(h: &mut egui_kittest::Harness<'_, WordApp>, (w, ht): (f32, f32), (t, b, l, r): (f32, f32, f32, f32)) { + use egui_kittest::kittest::Queryable; + let ppu = crate::options::unit().pt_per_unit(); + h.state_mut().run("ui.dialog", json!({"name": "pageSetup"})).unwrap(); + h.run_steps(2); + if let Some(Dialog::PageSetup { paper, landscape, top, bottom, left, right, .. }) = h.state_mut().dialog.as_mut() { + *paper = (w / ppu, ht / ppu); + *landscape = w > ht; + (*top, *bottom, *left, *right) = (t / ppu, b / ppu, l / ppu, r / ppu); + } + h.run_steps(2); + h.get_by_label("OK").click(); + h.run_steps(3); + } + + /// #1433: one OK in Page Setup is one undo step, whichever order its commands run in (the + /// size first; the margins first; margins of 0 in between), and Undo restores the section. + #[test] + fn page_setup_ok_is_one_undo_step() { + let sect = |h: &egui_kittest::Harness<'_, WordApp>| wordcraft_engine::cmd::page::sect(&h.state().session); + // Size first, then orientation and margins. + let mut h = app_harness(app_with("Page")); + let (before, depth) = (sect(&h), h.state().session.undo_depth()); + page_setup_ok(&mut h, (400.0, 600.0), (36.0, 36.0, 36.0, 36.0)); + assert!(h.state().dialog.is_none()); + assert_eq!((sect(&h).page_w, sect(&h).margin_left), (400.0, 36.0)); + assert_eq!(h.state().session.undo_depth(), depth + 1, "one step"); + h.state_mut().run("edit.undo", json!({})).unwrap(); + assert_eq!(sect(&h), before, "one undo restores the section"); + + // Neither size first nor margins first fits on the way: margins of 0 in between. + h.state_mut().run("layout.margins", json!({"top": 72.0, "bottom": 72.0, "left": 250.0, "right": 250.0})).unwrap(); + let (before, depth) = (sect(&h), h.state().session.undo_depth()); + page_setup_ok(&mut h, (300.0, 900.0), (400.0, 400.0, 36.0, 36.0)); + assert!(h.state().dialog.is_none()); + let s = sect(&h); + assert_eq!((s.page_w, s.page_h, s.margin_top, s.margin_left), (300.0, 900.0, 400.0, 36.0)); + assert_eq!(h.state().session.undo_depth(), depth + 1, "one step"); + h.state_mut().run("edit.undo", json!({})).unwrap(); + assert_eq!(sect(&h), before, "one undo restores the section"); + } + + /// #1433: a command the dialog runs that fails (here `layout.size`, refused by the second + /// selected section's margins) shows its error, keeps the dialog open and leaves the + /// document and the undo stack as they were. + #[test] + fn page_setup_shows_a_refused_command_and_applies_nothing() { + let mut app = app_with("one"); + app.run("layout.break", json!({"kind": "nextPage"})).unwrap(); + app.run("text.insert", json!({"text": "two"})).unwrap(); + app.run("layout.margins", json!({"left": 150.0, "right": 150.0})).unwrap(); + // Both sections selected, the first one's settings in the dialog. + app.run("select.all", json!({})).unwrap(); + let sel = &mut app.session.sel; + std::mem::swap(&mut sel.anchor, &mut sel.focus); + let doc = app.session.doc.sections().iter().map(|(_, x)| (*x).clone()).collect::>(); + let depth = app.session.undo_depth(); + let mut h = app_harness(app); + page_setup_ok(&mut h, (300.0, 500.0), (72.0, 72.0, 72.0, 72.0)); + assert!( + matches!(&h.state().dialog, Some(Dialog::PageSetup { message, .. }) if message.contains("margins don't fit")), + "stays open with the error" + ); + let after = h.state().session.doc.sections().iter().map(|(_, x)| (*x).clone()).collect::>(); + assert_eq!((after, h.state().session.undo_depth()), (doc, depth), "nothing applied"); + } + + /// #1433: Layout › Size › More Paper Sizes… opens Page Setup. + #[test] + fn more_paper_sizes_opens_page_setup() { + use egui_kittest::kittest::Queryable; + let mut h = egui_kittest::Harness::builder() + .with_size(egui::vec2(400.0, 600.0)) + .build_ui_state(|ui, app: &mut WordApp| crate::ribbon::size_menu(ui, app), app_with("Page")); + h.run_steps(2); + h.get(egui_kittest::kittest::By::new().label_contains("More Paper Sizes…")).click(); + h.run_steps(2); + assert_eq!(h.state().dialog.as_ref().map(|d| d.name()), Some("pageSetup")); + } } diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index faef7ef61..1cf83e0bb 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -2467,6 +2467,11 @@ Leave out the date and time of comments and tracked changes when saving Без дата и час на коментарите и проследените промени при запазване The document you are editing keeps the names and dates; only the saved file leaves them out. Документът, който редактирате, запазва имената и датите; само записаният файл ги пропуска. Privacy Settings Текущи настройки за поверителност + Paper Хартия + Paper size: Размер на хартията: + Custom size Потребителски размер + More Paper Sizes… Още размери на хартията… + The margins don't fit on a page this size; make the margins smaller first. Полетата не се побират на страница с този размер; първо намалете полетата. Full Screen Цял екран Full\nScreen Цял\nекран Use the browser's full screen (F11) Използвайте режима на цял екран на браузъра (F11) diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 4c9f6235b..e58338a8a 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -2092,6 +2092,11 @@ Leave out the date and time of comments and tracked changes when saving Při ukládání vynechat datum a čas komentářů a sledovaných změn The document you are editing keeps the names and dates; only the saved file leaves them out. Upravovaný dokument si jména a data ponechá; vynechá je jen uložený soubor. Privacy Settings Aktuální nastavení soukromí + Paper Papír + Paper size: Velikost papíru: + Custom size Vlastní velikost + More Paper Sizes… Další velikosti papíru… + The margins don't fit on a page this size; make the margins smaller first. Okraje se na stránku této velikosti nevejdou; nejprve okraje zmenšete. Full Screen Celá obrazovka Full\nScreen Celá\nobrazovka Use the browser's full screen (F11) Použijte celou obrazovku prohlížeče (F11) diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index da8dfc74a..1da97e88b 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -2086,6 +2086,11 @@ Leave out the date and time of comments and tracked changes when saving Beim Speichern Datum und Uhrzeit von Kommentaren und nachverfolgten Änderungen weglassen The document you are editing keeps the names and dates; only the saved file leaves them out. Das bearbeitete Dokument behält Namen und Daten; nur die gespeicherte Datei lässt sie weg. Privacy Settings Aktuelle Datenschutzeinstellungen + Paper Papier + Paper size: Papierformat: + Custom size Benutzerdefiniertes Format + More Paper Sizes… Weitere Papierformate… + The margins don't fit on a page this size; make the margins smaller first. Die Ränder passen nicht auf eine Seite dieser Größe; verkleinern Sie zuerst die Ränder. Full Screen Vollbild Full\nScreen Voll-\nbild Use the browser's full screen (F11) Verwenden Sie den Vollbildmodus des Browsers (F11) diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index ebd4b24e5..8e4970d93 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -2453,6 +2453,11 @@ preview Title Título Leave out the date and time of comments and tracked changes when saving Omitir la fecha y la hora de los comentarios y los cambios controlados al guardar The document you are editing keeps the names and dates; only the saved file leaves them out. El documento que editas conserva los nombres y las fechas; solo el archivo guardado los omite. Privacy Settings Configuración de privacidad actual + Paper Papel + Paper size: Tamaño del papel: + Custom size Tamaño personalizado + More Paper Sizes… Más tamaños de papel… + The margins don't fit on a page this size; make the margins smaller first. Los márgenes no caben en una página de este tamaño; reduzca primero los márgenes. Full Screen Pantalla completa Full\nScreen Pantalla\ncompleta Use the browser's full screen (F11) Use la pantalla completa del navegador (F11) diff --git a/crates/ui-egui/src/i18n/et.tsv b/crates/ui-egui/src/i18n/et.tsv index 8ca557d77..f5a44f431 100644 --- a/crates/ui-egui/src/i18n/et.tsv +++ b/crates/ui-egui/src/i18n/et.tsv @@ -2121,6 +2121,11 @@ Leave out the date and time of comments and tracked changes when saving Jäta salvestamisel välja kommentaaride ja jälitatud muudatuste kuupäev ja kellaaeg The document you are editing keeps the names and dates; only the saved file leaves them out. Redigeeritav dokument säilitab nimed ja kuupäevad; need jäetakse välja ainult salvestatud failist. Privacy Settings Praegused privaatsussätted + Paper Paber + Paper size: Paberi suurus: + Custom size Kohandatud suurus + More Paper Sizes… Rohkem paberisuurusi… + The margins don't fit on a page this size; make the margins smaller first. Veerised ei mahu selle suurusega lehele; vähendage esmalt veeriseid. Full Screen Täisekraan Full\nScreen Täis-\nekraan Use the browser's full screen (F11) Kasutage brauseri täisekraani (F11) diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 75b1ffed5..edb6b4f9e 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -2095,6 +2095,11 @@ Leave out the date and time of comments and tracked changes when saving Omettre la date et l’heure des commentaires et des modifications suivies à l’enregistrement The document you are editing keeps the names and dates; only the saved file leaves them out. Le document en cours de modification garde les noms et les dates ; seul le fichier enregistré les omet. Privacy Settings Paramètres de confidentialité actuels + Paper Papier + Paper size: Taille du papier : + Custom size Taille personnalisée + More Paper Sizes… Autres tailles de papier… + The margins don't fit on a page this size; make the margins smaller first. Les marges ne tiennent pas sur une page de cette taille ; réduisez d’abord les marges. Full Screen Plein écran Full\nScreen Plein\nécran Use the browser's full screen (F11) Utilisez le plein écran du navigateur (F11) diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index b57039465..c7ebd6893 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -2475,6 +2475,11 @@ Leave out the date and time of comments and tracked changes when saving Mentéskor a megjegyzések és a követett módosítások dátumának és idejének elhagyása The document you are editing keeps the names and dates; only the saved file leaves them out. A szerkesztett dokumentum megtartja a neveket és a dátumokat; csak a mentett fájlból maradnak ki. Privacy Settings Jelenlegi adatvédelmi beállítások + Paper Papír + Paper size: Papírméret: + Custom size Egyéni méret + More Paper Sizes… További papírméretek… + The margins don't fit on a page this size; make the margins smaller first. A margók nem férnek el ekkora oldalon; először csökkentse a margókat. Full Screen Teljes képernyő Full\nScreen Teljes\nképernyő Use the browser's full screen (F11) Használja a böngésző teljes képernyős módját (F11) diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 46fad2c93..1d78bb46d 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -2462,6 +2462,11 @@ preview Title Esempio Leave out the date and time of comments and tracked changes when saving Ometti data e ora di commenti e revisioni al salvataggio The document you are editing keeps the names and dates; only the saved file leaves them out. Il documento che stai modificando conserva nomi e date; solo il file salvato li omette. Privacy Settings Impostazioni di privacy attuali + Paper Carta + Paper size: Dimensioni carta: + Custom size Dimensioni personalizzate + More Paper Sizes… Altre dimensioni carta… + The margins don't fit on a page this size; make the margins smaller first. I margini non entrano in una pagina di queste dimensioni; riduci prima i margini. Full Screen Schermo intero Full\nScreen Schermo\nintero Use the browser's full screen (F11) Usa lo schermo intero del browser (F11) diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index daaddbfcb..8896a814b 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -2087,6 +2087,11 @@ Leave out the date and time of comments and tracked changes when saving 保存時にコメントと変更履歴の日時を含めない The document you are editing keeps the names and dates; only the saved file leaves them out. 編集中のドキュメントには名前と日時が残ります。保存したファイルにだけ含まれません。 Privacy Settings 現在のプライバシー設定 + Paper 用紙 + Paper size: 用紙サイズ: + Custom size ユーザー定義サイズ + More Paper Sizes… その他の用紙サイズ… + The margins don't fit on a page this size; make the margins smaller first. この用紙サイズには余白が収まりません。先に余白を小さくしてください。 Full Screen 全画面表示 Full\nScreen 全画面\n表示 Use the browser's full screen (F11) ブラウザーの全画面表示を使用してください (F11) diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 4be2c5507..4c6e4f088 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -2471,6 +2471,11 @@ preview Title Tytuł Leave out the date and time of comments and tracked changes when saving Pomiń datę i godzinę komentarzy i śledzonych zmian przy zapisywaniu The document you are editing keeps the names and dates; only the saved file leaves them out. Edytowany dokument zachowuje nazwiska i daty; pomija je tylko zapisany plik. Privacy Settings Bieżące ustawienia prywatności + Paper Papier + Paper size: Rozmiar papieru: + Custom size Rozmiar niestandardowy + More Paper Sizes… Więcej rozmiarów papieru… + The margins don't fit on a page this size; make the margins smaller first. Marginesy nie mieszczą się na stronie tego rozmiaru; najpierw zmniejsz marginesy. Full Screen Pełny ekran Full\nScreen Pełny\nekran Use the browser's full screen (F11) Użyj trybu pełnoekranowego przeglądarki (F11) diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 86ff9aa02..1a0c9a4b7 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -2099,6 +2099,11 @@ Leave out the date and time of comments and tracked changes when saving Omitir a data e a hora dos comentários e das alterações controladas ao salvar The document you are editing keeps the names and dates; only the saved file leaves them out. O documento que você está editando mantém os nomes e as datas; só o arquivo salvo os omite. Privacy Settings Configurações de privacidade atuais + Paper Papel + Paper size: Tamanho do papel: + Custom size Tamanho personalizado + More Paper Sizes… Mais tamanhos de papel… + The margins don't fit on a page this size; make the margins smaller first. As margens não cabem em uma página deste tamanho; reduza as margens primeiro. Full Screen Tela inteira Full\nScreen Tela\ninteira Use the browser's full screen (F11) Use a tela inteira do navegador (F11) diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 050f874cd..993b27c14 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -2477,6 +2477,11 @@ preview Title Пример Leave out the date and time of comments and tracked changes when saving Не сохранять дату и время примечаний и отслеживаемых изменений The document you are editing keeps the names and dates; only the saved file leaves them out. Редактируемый документ сохраняет имена и даты; их не будет только в сохранённом файле. Privacy Settings Текущие параметры конфиденциальности + Paper Бумага + Paper size: Размер бумаги: + Custom size Другой размер + More Paper Sizes… Другие размеры бумаги… + The margins don't fit on a page this size; make the margins smaller first. Поля не помещаются на странице такого размера; сначала уменьшите поля. Full Screen Во весь экран Full\nScreen Во весь\nэкран Use the browser's full screen (F11) Используйте полноэкранный режим браузера (F11) diff --git a/crates/ui-egui/src/i18n/sr-latn.tsv b/crates/ui-egui/src/i18n/sr-latn.tsv index d9aca608c..211af3972 100644 --- a/crates/ui-egui/src/i18n/sr-latn.tsv +++ b/crates/ui-egui/src/i18n/sr-latn.tsv @@ -2097,6 +2097,11 @@ Leave out the date and time of comments and tracked changes when saving Izostavi datum i vreme komentara i praćenih izmena pri čuvanju The document you are editing keeps the names and dates; only the saved file leaves them out. Dokument koji uređujete zadržava imena i datume; izostavlja ih samo sačuvana datoteka. Privacy Settings Trenutna podešavanja privatnosti + Paper Papir + Paper size: Veličina papira: + Custom size Prilagođena veličina + More Paper Sizes… Još veličina papira… + The margins don't fit on a page this size; make the margins smaller first. Margine ne staju na stranu ove veličine; prvo smanjite margine. Full Screen Ceo ekran Full\nScreen Ceo\nekran Use the browser's full screen (F11) Koristite prikaz preko celog ekrana u pregledaču (F11) diff --git a/crates/ui-egui/src/i18n/sr.tsv b/crates/ui-egui/src/i18n/sr.tsv index d5ce60a5a..b89e800a0 100644 --- a/crates/ui-egui/src/i18n/sr.tsv +++ b/crates/ui-egui/src/i18n/sr.tsv @@ -2101,6 +2101,11 @@ Leave out the date and time of comments and tracked changes when saving Изостави датум и време коментара и праћених измена при чувању The document you are editing keeps the names and dates; only the saved file leaves them out. Документ који уређујете задржава имена и датуме; изоставља их само сачувана датотека. Privacy Settings Тренутна подешавања приватности + Paper Папир + Paper size: Величина папира: + Custom size Прилагођена величина + More Paper Sizes… Још величина папира… + The margins don't fit on a page this size; make the margins smaller first. Маргине не стају на страну ове величине; прво смањите маргине. Full Screen Цео екран Full\nScreen Цео\nекран Use the browser's full screen (F11) Користите приказ преко целог екрана у прегледачу (F11) diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index ab261e1f3..d63c09cee 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -2476,6 +2476,11 @@ preview Title Örnek Leave out the date and time of comments and tracked changes when saving Kaydederken açıklamaların ve izlenen değişikliklerin tarih ve saatini dahil etme The document you are editing keeps the names and dates; only the saved file leaves them out. Düzenlediğiniz belge adları ve tarihleri korur; yalnızca kaydedilen dosya bunları içermez. Privacy Settings Geçerli gizlilik ayarları + Paper Kağıt + Paper size: Kağıt boyutu: + Custom size Özel boyut + More Paper Sizes… Diğer Kağıt Boyutları… + The margins don't fit on a page this size; make the margins smaller first. Kenar boşlukları bu boyuttaki bir sayfaya sığmıyor; önce kenar boşluklarını küçültün. Full Screen Tam ekran Full\nScreen Tam\nekran Use the browser's full screen (F11) Tarayıcının tam ekran modunu kullanın (F11) diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index b0c38d67c..20d4c5bed 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -2099,6 +2099,11 @@ Leave out the date and time of comments and tracked changes when saving Не зберігати дату й час приміток і відстежуваних змін The document you are editing keeps the names and dates; only the saved file leaves them out. Документ, який ви редагуєте, зберігає імена й дати; їх не буде лише у збереженому файлі. Privacy Settings Поточні налаштування конфіденційності + Paper Папір + Paper size: Розмір паперу: + Custom size Власний розмір + More Paper Sizes… Інші розміри паперу… + The margins don't fit on a page this size; make the margins smaller first. Поля не вміщуються на сторінці такого розміру; спершу зменште поля. Full Screen На весь екран Full\nScreen На весь\nекран Use the browser's full screen (F11) Скористайтеся повноекранним режимом браузера (F11) diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 71a057825..e1848ba3f 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -2517,3 +2517,10 @@ {name} {height} × {width} in {name} {height} × {width} in {name} {height} × {width} mm {name} {height} × {width} mm {width} × {height} mm {width} × {height} mm + +# Custom paper sizes (#1496) + Custom size Kích thước tùy chỉnh + More Paper Sizes… Thêm khổ giấy… + Paper Giấy + Paper size: Khổ giấy: + The margins don't fit on a page this size; make the margins smaller first. Lề không vừa với trang có kích thước này; hãy thu nhỏ lề trước. diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index bc68ed579..7751d040b 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -2099,6 +2099,11 @@ Leave out the date and time of comments and tracked changes when saving 保存时不包含批注和修订的日期与时间 The document you are editing keeps the names and dates; only the saved file leaves them out. 正在编辑的文档保留姓名和日期;只有保存的文件中不包含它们。 Privacy Settings 当前隐私设置 + Paper 纸张 + Paper size: 纸张大小: + Custom size 自定义大小 + More Paper Sizes… 其他纸张大小… + The margins don't fit on a page this size; make the margins smaller first. 页边距放不进此大小的页面;请先减小页边距。 Full Screen 全屏 Full\nScreen 全屏 Use the browser's full screen (F11) 请使用浏览器的全屏模式 (F11) diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index fde965f1b..e350e75db 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -2088,6 +2088,11 @@ Leave out the date and time of comments and tracked changes when saving 儲存時不包含註解和追蹤修訂的日期與時間 The document you are editing keeps the names and dates; only the saved file leaves them out. 正在編輯的文件會保留名稱和日期;只有儲存的檔案不包含它們。 Privacy Settings 目前的隱私設定 + Paper 紙張 + Paper size: 紙張大小: + Custom size 自訂大小 + More Paper Sizes… 其他紙張大小… + The margins don't fit on a page this size; make the margins smaller first. 邊界放不進此大小的頁面;請先縮小邊界。 Full Screen 全螢幕 Full\nScreen 全螢幕 Use the browser's full screen (F11) 請使用瀏覽器的全螢幕模式 (F11) diff --git a/crates/ui-egui/src/ribbon.rs b/crates/ui-egui/src/ribbon.rs index dae35aed8..6f180135d 100644 --- a/crates/ui-egui/src/ribbon.rs +++ b/crates/ui-egui/src/ribbon.rs @@ -403,6 +403,22 @@ fn stack(ui: &mut Ui, add: impl FnOnce(&mut Ui)) { }); } +/// Layout › Size: the named paper sizes, then More Paper Sizes… for a custom size in Page +/// Setup (#1433). +pub(crate) fn size_menu(ui: &mut Ui, app: &mut WordApp) { + for (n, w, h) in wordcraft_geom::PAPER_SIZES { + // Paper names are the industry's (Letter, A4, B5 (JIS)) and stay as they are; the + // envelopes say what they are. Sizes in the measurement unit, written the region's + // way (`21 cm × 29,7 cm`, #838). + let name = if n.starts_with("Envelope ") { tl!(n) } else { n }; + let (width, height) = (crate::options::length(*w), crate::options::length(*h)); + let label = crate::i18n::fmt(tl!("{paper} {width} × {height}"), &[("paper", name), ("width", &width), ("height", &height)]); + mi_text(ui, app, &label, "layout.size", json!({"name": n})); + } + ui.separator(); + mi(ui, app, crate::dialogs::PAPER_LABELS.more, "ui.dialog", json!({"name": "pageSetup"})); +} + pub(crate) fn mi(ui: &mut Ui, app: &mut WordApp, label: &str, id: &str, params: Value) { mi_text(ui, app, tl!(label), id, params); } @@ -1286,17 +1302,7 @@ fn layout(app: &mut WordApp, ui: &mut Ui) { mi(ui, app, "Portrait", "layout.orientation", json!({"value": "portrait"})); mi(ui, app, "Landscape", "layout.orientation", json!({"value": "landscape"})); }); - menu_button(ui, app, "size", Some("Size"), "Size", true, "layout.size", |ui, app| { - for (n, w, h) in wordcraft_geom::PAPER_SIZES { - // Paper names are the industry's (Letter, A4, B5 (JIS)) and stay as they are; the - // envelopes say what they are. Sizes in the measurement unit, written the region's - // way (`21 cm × 29,7 cm`, #838). - let name = if n.starts_with("Envelope ") { tl!(n) } else { n }; - let (width, height) = (crate::options::length(*w), crate::options::length(*h)); - let label = crate::i18n::fmt(tl!("{paper} {width} × {height}"), &[("paper", name), ("width", &width), ("height", &height)]); - mi_text(ui, app, &label, "layout.size", json!({"name": n})); - } - }); + menu_button(ui, app, "size", Some("Size"), "Size", true, "layout.size", size_menu); menu_button(ui, app, "columns", Some("Columns"), "Columns", true, "layout.columns", |ui, app| { mi(ui, app, "One", "layout.columns", json!({"count": 1})); mi(ui, app, "Two", "layout.columns", json!({"count": 2})); diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 776e3828c..70ac8068e 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,6 +1,6 @@ # UI parity with Microsoft Word -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** trivial (full screen on F11, #980; previously trivial: File dialogs row: Save As lists each format by name, #1399; previously trivial: no document behind the Start screen, #1129; previously trivial: equation pictures share a few textures, #1002; previously trivial: selection bar in the left margin, #856; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously major: first version; ribbon, dialogs, shortcuts and on-canvas interaction measured from `crates/ui-egui` and the command registry) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** trivial (Page Setup paper size and Layout › Size › More Paper Sizes…, #1433; previously trivial: full screen on F11, #980; previously trivial: File dialogs row: Save As lists each format by name, #1399; previously trivial: no document behind the Start screen, #1129; previously trivial: equation pictures share a few textures, #1002; previously trivial: selection bar in the left margin, #856; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously major: first version; ribbon, dialogs, shortcuts and on-canvas interaction measured from `crates/ui-egui` and the command registry) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 How WordCraft looks and feels next to Word: ribbon, dialogs, panes, keyboard, mouse and touch. Typing behaviour has its own checklist ([`typing-parity.md`](typing-parity.md)). All assets are our @@ -17,8 +17,8 @@ own (`AGENTS.md`: no Microsoft iconography, ever). | Colour drop-downs: theme/standard colours, Recent Colors, More Colors… → Colors dialog (Standard honeycomb, Custom hue/saturation field with lightness, RGB/HSL and hex) | ✅ Font Color (ribbon, mini toolbar, Font dialog), Shading, Page Color, Shape Fill/Outline/Glow, pen colours, dialog colour fields (#606); no Gradient or eyedropper | ✅ | 90% | 1 | | Quick Access Toolbar | AutoSave, then the user's commands (Save, Undo, Redo by default); add, remove and reorder from its … menu (common commands, More Commands…), right-click on any ribbon button, or Customize Ribbon; saved per user (#379, #376). Undo's ▾ list: recent steps newest first, hover highlights the steps a click undoes ("Undo 3 Actions"), one click undoes them all (#701; `edit.undoTo`, `edit.undoHistory`). Not shown below the ribbon | customizable | 90% | 0.5 | | Customize Ribbon / Customize Keyboard | Customize Keyboard (#368): categories, a command's keys, press a combination to see which command uses it, Assign / Remove / Reset All; custom keys override built-ins, removed built-ins do nothing, saved per user; `tools.customizeKeyboard` for agents. Customize Ribbon (#379): show, hide and reorder tabs, custom tabs and custom groups (on any tab) holding any commands, rename, remove, Reset; right-click on the ribbon; saved per user; `tools.customizeRibbon` for agents. Built-in tabs can't be renamed; no import/export file | ✅ | 85% | 1–2 | +| Modal dialogs | **54** (`Dialog` enum: Font, Enclose Characters, Paragraph, Tabs, Borders and Shading, Find, Go To, Insert Table, Table Properties, Page Setup, Link, Bookmark, Word Count, Zoom, Watermark, New Style, Modify Style, Manage Styles, Table Style, Commands, Icons, Templates and Add-ins, Organizer, Compare and Combine, AutoCorrect Options, Paste Special, About, Save Changes, Password, Encrypt with Password, Define New Multilevel List, Track Changes Options; Columns, Symbol with Special Characters, Field (#321); Field code (#339); mail merge Recipients, Insert Merge Field, Find Recipient, Rules, Match Fields, Check for Errors; Caption with New Label and Numbering, Index, Mark Index Entry, Custom Table of Contents with Options and TOC styles, Source Manager, Create/Edit Source (#402); Line Numbers, Hyphenation with Manual Hyphenation, Language, Envelopes, Labels (#407); Page Setup has a Paper size list and custom width and height, opened from Layout › Size › More Paper Sizes… (#1433)) plus panes and menus for the rest | ~100 (Options panes…) | ~45% | 11–19 | | Keyboard shortcuts | **152** commands bound in `CommandSpec::key`, 172 key combinations (counted from the registry on Linux, 2026-10-13); styles, spacing, symbols, fields, views, mail merge (#330); field keys Alt+F9, Shift+F9, Ctrl+Shift+F9, Ctrl+F11, Ctrl+Shift+F11, Shift+F11 (#339), Next Field on Alt+F1 and Alt+Shift+F1 (Word's other keys for it) since F11 went to full screen (#980; GNOME and KDE may keep Alt+F1 for their own overview or application menu, and then Next Field has no key there; Previous Field keeps Shift+F11); Alt+Shift+arrows (move paragraphs, promote/demote), Shift+F5 Go Back and Alt+X (#343); Outline view: Alt+Shift+Plus/Minus expand/collapse, Alt+Shift+1–9 and Alt+Shift+A Show Level (#493); Alt+Q to "Tell me" command search (#819, not on macOS). Missing: en dash on the numeric keypad | ~250 default shortcuts | ~70% | 2–4 | -| Modal dialogs | **54** (`Dialog` enum: Font, Enclose Characters, Paragraph, Tabs, Borders and Shading, Find, Go To, Insert Table, Table Properties, Page Setup, Link, Bookmark, Word Count, Zoom, Watermark, New Style, Modify Style, Manage Styles, Table Style, Commands, Icons, Templates and Add-ins, Organizer, Compare and Combine, AutoCorrect Options, Paste Special, About, Save Changes, Password, Encrypt with Password, Define New Multilevel List, Track Changes Options; Columns, Symbol with Special Characters, Field (#321); Field code (#339); mail merge Recipients, Insert Merge Field, Find Recipient, Rules, Match Fields, Check for Errors; Caption with New Label and Numbering, Index, Mark Index Entry, Custom Table of Contents with Options and TOC styles, Source Manager, Create/Edit Source (#402); Line Numbers, Hyphenation with Manual Hyphenation, Language, Envelopes, Labels (#407)) plus panes and menus for the rest | ~100 (Options panes…) | ~45% | 11–19 | | Task panes (Navigation, Styles, Comments, Reviewing, Format Picture/Shape, Thesaurus, Accessibility, Clipboard, Selection) | Navigation (long headings truncate with an ellipsis instead of expanding the pane, #759), Styles, Comments, Thesaurus, Restrict Editing, Accessibility (grouped findings, jump-to, fixes) and Alt Text (#386), Read Aloud, Zotero; Style Inspector (#236); Clipboard pane (#264) open | all | 70% | 4–6 | | Context menus (right-click) | text, spelling suggestions, fields (Update Field, Edit Field…, Toggle Field Codes, #339); right-click selects a picture, shape or text box and offers Wrap Text (styles, Move with Text / Fix Position on Page, #513); tables, the items following the selection as in Word (Insert, Delete Cells, Rows, Columns or Table, Merge and Split Cells, Distribute, AutoFit, Text Direction, Insert Caption…, Table Properties…; no Border Styles yet) | rich per context | 65% | 2–4 | | Backstage (Home, New, Open, Info, Save As, Print, Share, Export, Options) | Home and New separate (#254), Info properties kept (#268), Options in Word's panes with working settings (#485), Print page with preview, page range and copies (#641); launched with nothing to open, the app starts on Home (the Start screen: Blank document, templates, Recent; no document is open until one is picked or opened, so Esc or Back shows an empty window with the ribbon greyed out but for Help, #1129), with File › Options › General › Start-up to turn it off (#766) | | 68% | 4–6 | @@ -52,6 +52,7 @@ own (`AGENTS.md`: no Microsoft iconography, ever). | 2026-10-13 | trivial | Clicks on ribbon and status-bar chrome that run no command button (Styles and style-set gallery tiles, tab names, empty ribbon space, View check boxes, the status bar's view buttons and language menu) no longer take keyboard focus from the page: the caret stays and typing continues, as in Word; text fields such as the font size box still take focus. Tabs, gallery tiles and the status bar's view buttons report their name and state to screen readers (#1093) | | 2026-10-13 | trivial | Interface size: a chosen size is absolute ("100%" is one screen pixel per point, even where the system reports 200 % under a bare window manager) and the new default, Auto, keeps the display scale and shows the size it draws at; 75–300 %; an old saved size becomes the size it drew at; on the web a chosen size cancels browser zoom, Auto follows it (#822) | | 2026-10-13 | trivial | Copy and Cut of a selection holding an equation put HTML with the equation as MathML on the system clipboard beside the text (desktop, through arboard), as MathML in HTML on the clipboard (Word/LibreOffice paste not yet verified; OMML is #1297) (#816). Only copies containing an equation get HTML. Web: text only | +| 2026-10-14 | trivial | Page Setup: a Paper group with Paper size (the named sizes, the one the section has in either orientation, or Custom size) and Width and Height in the measurement unit; a wider-than-tall size turns the orientation to landscape; a size the margins don't fit keeps the dialog open with a message and applies nothing. Layout › Size ends with More Paper Sizes…, which opens it (#1433) | | 2026-10-13 | trivial | Full screen (#980): View › Window › Full Screen and F11 (⌃⌘F on macOS), Escape leaves it once nothing else takes it (a selection collapses first); Next Field moves to Alt+F1; keyboard counts re-measured (152 commands, 172 combinations) | | 2026-10-14 | trivial | File dialogs: the system Save As type list names each format, the document's own first (#1399) | | 2026-10-11 | trivial | Tables on the page: Ctrl shares a column border's change among the columns right of it, the left edge moves a left-aligned table's indent (a centred or right-aligned one keeps its alignment), a border of cells selected across moves only in their rows, Alt shows the column widths on the ruler (#711); 80% → 90% |