From 71dba44a0a15e72c5e9df8886fdfd0b7ab151939 Mon Sep 17 00:00:00 2001 From: vangel76 Date: Sat, 31 Oct 2026 17:12:52 +0100 Subject: [PATCH] M8: add persistent screen-only paper color presets (#3398) Co-Authored-By: Codex --- ROADMAP.md | 4 +++ crates/engine/src/cmd/view.rs | 43 +++++++++++++++++++++++++++++ crates/engine/src/prefs.rs | 9 ++++++ crates/engine/src/tests.rs | 34 +++++++++++++++++++++++ crates/ui-egui/src/app/commands.rs | 1 + crates/ui-egui/src/canvas.rs | 29 ++++++------------- crates/ui-egui/src/canvas/pages.rs | 10 +++++-- crates/ui-egui/src/canvas/tests.rs | 36 ++++++++++++++++++++++++ crates/ui-egui/src/options.rs | 15 ++++++++++ crates/ui-egui/src/options/tests.rs | 27 ++++++++++++++++++ docs/roadmap.md | 4 ++- docs/ui-parity.md | 17 ++++++++++++ 12 files changed, 205 insertions(+), 24 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 0535118760..c8fb00101f 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,5 +1,6 @@ # WordCraft roadmap +> **Last reviewed:** 2026-10-31 · **Last updated:** 2026-10-31 · **Change:** minor (#3398: persistent screen paper colors; parity estimates unchanged) > **Highlight update:** 2026-10-25 · Highlight shortcut toggles the last chosen colour off when the whole selection has it; explicit colour commands remain setters (#4085). Readiness estimates unchanged. **Stage: alpha** · next: beta, ~15 points (60% → 75% ready for real work) and ~150–230 h away @@ -141,6 +142,8 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm ## Progress log - 2026-10-31: Keytips reach the Backstage: Alt, F badges its pages with Word's letters, and Alt, F, P (or V) opens Print (#4960). +- **2026-10-31 (#3398):** General Options offers persistent Document background / Soft green / Warm cream / Soft gray screen paper. The view.paperColor command reaches the same setting headlessly. Document page colors and pictures, saves, printing and exports retain their own styling; the screen raster/tile cache and page backing share the selected color. Read Mode None uses the preset; explicit Sepia/Inverse keep priority. Parity counts and readiness estimates unchanged. + - 2026-10-29: Documents with hyperlinks or a table of contents print again on the desktop (#4734). The print job scales each page to the paper, and the spooler refused a page with link annotations ("flatten page annotations before using shrink-to-fit"); the PDF sent to the printer now omits link annotations, which print nothing; the link and table-of-contents text stays on the page. Exported PDFs and Save these pages as PDF (`file.printPdf`) keep the annotations. - 2026-10-28: Word count counts each Chinese and Japanese character as a word, as Word does, in the status bar, the Word Count dialog and NUMWORDS fields. Unspaced Chinese text used to count as one word per run of text; Korean, which is written with spaces, still counts by words. (#4547) - 2026-10-28: Aligned-copy review fixes keep the document axis locked through middle-button scrolling and let Ctrl/Command+Shift start on a secondary selected object. Plain Ctrl and Shift clicks keep their existing behavior (#4764). @@ -678,6 +681,7 @@ Footer page-table height and body wrapping are corrected. Other pagination gaps | Date | Change | Summary | |---|---|---| +| 2026-10-31 | minor | #3398: persistent screen-only green, cream and gray paper presets, with document backgrounds and export colors retained. Read Mode None follows the screen preset; Sepia/Inverse retain priority. | | 2026-10-31 | trivial | Progress log: Backstage keytips, Alt, F, P prints (#4960) | | 2026-10-30 | trivial | Progress log: only the PDF sent to the printer leaves out link annotations; Save these pages as PDF keeps them (#4734) | | 2026-10-29 | trivial | Progress log: documents with links print (#4734) | diff --git a/crates/engine/src/cmd/view.rs b/crates/engine/src/cmd/view.rs index c7f5f922fd..b2848126b0 100644 --- a/crates/engine/src/cmd/view.rs +++ b/crates/engine/src/cmd/view.rs @@ -40,6 +40,15 @@ pub fn specs() -> Vec { CommandSpec::new("view.readPageColor", "Reading Page Color", "Reading › Layout", read_page_color) .params(r#"{"value": "none" | "sepia" | "inverse"}"#) .pure(), + CommandSpec::new("view.paperColor", "Screen Paper Color", "View › Display", |s, v| { + let color: PaperColor = serde_json::from_value(v.get("value").cloned().unwrap_or(Value::Null)) + .map_err(|_| CmdError::Params("screen paper color must be document, green, cream, or gray".into()))?; + s.prefs.paper_color = color; + if color != PaperColor::Document { + s.view.dark_mode = false; + } + Ok(json!({"paperColor": color})) + }).params(r#"{"value": "document" | "green" | "cream" | "gray"}"#).pure(), // Print Layout: hide the white space (margins, headers, footers) between pages. Screen only. CommandSpec::new("view.hideWhiteSpace", "Hide White Space", "View › Show", |s, v| toggle(s, v, |x| &mut x.hide_white_space)) .params(r#"{"value"?: bool}"#) @@ -234,6 +243,40 @@ fn read_line_spacing(s: &mut Session, v: &Value) -> CmdResult { Ok(json!({"spacing": spacing})) } +/// Screen-only paper presets, saved with user preferences, never in the document. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum PaperColor { + #[default] + Document, + Green, + Cream, + Gray, +} + +impl PaperColor { + pub const ALL: [Self; 4] = [Self::Document, Self::Green, Self::Cream, Self::Gray]; + + pub fn label(self) -> &'static str { + match self { + Self::Document => "Document background", + Self::Green => "Soft green", + Self::Cream => "Warm cream", + Self::Gray => "Soft gray", + } + } + + pub fn color(self) -> wordcraft_doc::props::Rgb { + use wordcraft_doc::props::Rgb; + match self { + Self::Document => Rgb::WHITE, + Self::Green => Rgb(0xCC, 0xE8, 0xCF), + Self::Cream => Rgb(0xFA, 0xF0, 0xDC), + Self::Gray => Rgb(0xE8, 0xEA, 0xED), + } + } +} + /// Read Mode › View › Page Color, as in Word. Screen only: the document and its own page color /// are unchanged. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] diff --git a/crates/engine/src/prefs.rs b/crates/engine/src/prefs.rs index 4bafd2ad4d..aa0477dec8 100644 --- a/crates/engine/src/prefs.rs +++ b/crates/engine/src/prefs.rs @@ -7,6 +7,9 @@ use serde::{Deserialize, Serialize}; pub struct Prefs { /// Word Count includes text boxes, footnotes and endnotes (Word's default). pub count_notes: bool, + /// On-screen paper for pages without a document background; excluded from saved documents. + #[serde(deserialize_with = "lenient_paper_color")] + pub paper_color: crate::cmd::view::PaperColor, /// Track Changes Options: what markup shows and how revisions are drawn (per user, as in /// Word). pub markup: wordcraft_layout::display::MarkupOptions, @@ -61,6 +64,7 @@ pub struct Prefs { impl Default for Prefs { fn default() -> Self { Prefs { + paper_color: Default::default(), count_notes: true, markup: Default::default(), print_markup: false, @@ -100,3 +104,8 @@ fn lenient_default_language<'de, D: serde::Deserializer<'de>>(d: D) -> Result>(d: D) -> Result { + let value = serde_json::Value::deserialize(d)?; + Ok(serde_json::from_value(value).unwrap_or_default()) +} diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 95b38df097..3636ac13f2 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -11534,3 +11534,37 @@ fn export_png_rejects_a_page_number_that_does_not_fit() { assert!(!target.exists(), "no file is written for an invalid page"); let _ = std::fs::remove_dir_all(&dir); } + +#[test] +fn screen_paper_color_is_a_persistent_preference_not_document_formatting() { + use crate::cmd::view::PaperColor; + let mut s = s(); + run(&mut s, "text.insert", json!({"text": "Screen paper test", "raw": true})); + let before = serde_json::to_value(&s.doc).unwrap(); + let docx_before = wordcraft_docx::write(&s.doc).unwrap(); + s.dirty = false; + let dirty = s.dirty; + s.view.dark_mode = true; + for (value, expected) in + [("green", PaperColor::Green), ("cream", PaperColor::Cream), ("gray", PaperColor::Gray), ("document", PaperColor::Document)] + { + run(&mut s, "view.paperColor", json!({"value": value})); + assert_eq!(s.prefs.paper_color, expected); + assert_eq!(serde_json::to_value(&s.doc).unwrap(), before); + assert_eq!(s.dirty, dirty); + assert_eq!(wordcraft_docx::write(&s.doc).unwrap(), docx_before); + assert!(!s.view.dark_mode, "choosing a tint turns off dark paper"); + let saved = serde_json::to_value(&s.prefs).unwrap(); + let restored: crate::Prefs = serde_json::from_value(saved).unwrap(); + assert_eq!(restored.paper_color, expected); + } + for v in [json!({}), json!({"value": "red"}), json!({"value": 42})] { + assert!(s.run("view.paperColor", &v).is_err()); + assert_eq!(s.prefs.paper_color, PaperColor::Document); + } + let legacy: crate::Prefs = serde_json::from_value(json!({})).unwrap(); + assert_eq!(legacy.paper_color, PaperColor::Document); + let future: crate::Prefs = serde_json::from_value(json!({"paperColor":"future", "countNotes":false})).unwrap(); + assert_eq!(future.paper_color, PaperColor::Document); + assert!(!future.count_notes); +} diff --git a/crates/ui-egui/src/app/commands.rs b/crates/ui-egui/src/app/commands.rs index 70bb117821..923a031b15 100644 --- a/crates/ui-egui/src/app/commands.rs +++ b/crates/ui-egui/src/app/commands.rs @@ -1017,6 +1017,7 @@ pub(crate) fn works_without_document(id: &str) -> bool { | "tools.customizeKeyboard" | "tools.customizeRibbon" | "view.darkMode" + | "view.paperColor" | "view.marks" | "view.ruler" | "view.numeral" diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index e773a682b7..fc37aefc0d 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -482,8 +482,7 @@ struct CanvasView { area: Rect, hruler: Option, vruler: Option, - dark_page: bool, - sepia_page: bool, + paper_color: wordcraft_doc::props::Rgb, animate: bool, opts: egui::InputOptions, geo: Geometry, @@ -493,10 +492,8 @@ struct CanvasView { } fn prepare_view(app: &mut WordApp, ui: &Ui, full: Rect, layout: &DocLayout) -> CanvasView { - use wordcraft_engine::cmd::view::ReadPageColor; let read_mode = app.session.view.read_mode; - let dark_page = if read_mode { app.session.view.read_page_color == ReadPageColor::Inverse } else { app.session.view.dark_mode }; - let sepia_page = read_mode && app.session.view.read_page_color == ReadPageColor::Sepia; + let paper_color = pages::page_render_options(app, false).paper_of(&app.session.doc); let show_ruler = app.session.view.ruler && app.session.view.mode == wordcraft_layout::ViewMode::Print && !read_mode @@ -564,7 +561,7 @@ fn prepare_view(app: &mut WordApp, ui: &Ui, full: Rect, layout: &DocLayout) -> C { app.session.page_hint = c.page; } - CanvasView { area, hruler, vruler, dark_page, sepia_page, animate, opts, geo, dim_body, grid_story, caret } + CanvasView { area, hruler, vruler, paper_color, animate, opts, geo, dim_body, grid_story, caret } } fn update_caret_scroll(app: &mut WordApp, view: &CanvasView) -> Option { @@ -828,8 +825,7 @@ struct PagePaint<'a> { t: &'a Tokens, painter: &'a egui::Painter, dim_body: bool, - dark_page: bool, - sepia_page: bool, + paper_color: wordcraft_doc::props::Rgb, hidden: Option<(usize, Pos)>, view: u64, content_origin: Pos2, @@ -876,8 +872,7 @@ fn draw_page_gridlines(app: &WordApp, painter: &egui::Painter, t: &Tokens, page: } fn paint_pages(pass: PagePaint<'_>) -> PagePaintOutput { - let PagePaint { app, ui, layout, geo, t, painter, dim_body, dark_page, sepia_page, hidden, view, content_origin, ppp, max_tex, grid_story } = - pass; + let PagePaint { app, ui, layout, geo, t, painter, dim_body, paper_color, hidden, view, content_origin, ppp, max_tex, grid_story } = pass; let mut visible_tile_keys = HashSet::new(); let mut rendered = 0; let mut rects = Vec::with_capacity(geo.rects.len()); @@ -940,14 +935,7 @@ fn paint_pages(pass: PagePaint<'_>) -> PagePaintOutput { if band_top <= 0.0 && band_bot >= sr.height() { painter.clone() } else { painter.with_clip_rect(painter.clip_rect().intersect(band)) }; // Shadow and paper. painter.rect_filled(visual_sr.translate(vec2(0.0, 2.0)).expand(1.5), 1.0, t.page_shadow); - let paper = if dark_page { - Color32::from_gray(wordcraft_render::DARK_PAPER) - } else if sepia_page { - let s = wordcraft_render::SEPIA_PAPER; - Color32::from_rgb(s.0, s.1, s.2) - } else { - Color32::WHITE - }; + let paper = Color32::from_rgb(paper_color.0, paper_color.1, paper_color.2); painter.rect_filled(visual_sr, 0.0, paper); if tiled { // Render before painting to keep the frame sharp immediately after an edit or @@ -1317,7 +1305,7 @@ pub fn show(app: &mut WordApp, ui: &mut Ui) { let scroll_target = update_caret_scroll(app, &view); let motion = scroll_motion(app, ui, full, &view); let scroll_area = configure_scroll_area(app, ui, &view, motion); - let CanvasView { area, hruler, vruler, dark_page, sepia_page, geo, dim_body, grid_story, caret, .. } = view; + let CanvasView { area, hruler, vruler, paper_color, geo, dim_body, grid_story, caret, .. } = view; let mut origin = area.min; let mut ui_area = ui.new_child(egui::UiBuilder::new().max_rect(area)); let mut ime_geometry: Option<(Rect, Rect)> = None; @@ -1344,8 +1332,7 @@ pub fn show(app: &mut WordApp, ui: &mut Ui) { t: &t, painter: &painter, dim_body, - dark_page, - sepia_page, + paper_color, hidden, view: page_view, content_origin, diff --git a/crates/ui-egui/src/canvas/pages.rs b/crates/ui-egui/src/canvas/pages.rs index d4a3483d83..dfd363df12 100644 --- a/crates/ui-egui/src/canvas/pages.rs +++ b/crates/ui-egui/src/canvas/pages.rs @@ -8,7 +8,7 @@ pub(super) fn view_key(app: &WordApp, dim_body: bool, hidden: Option<&Pos>) -> u let mut h = std::collections::hash_map::DefaultHasher::new(); let v = &app.session.view; (v.marks, app.session.prefs.marks, v.show_markup, v.dark_mode, wordcraft_render::DARK_PAPER, v.hide_ink, v.draw.replay).hash(&mut h); - (v.read_mode, v.read_page_color).hash(&mut h); + (v.read_mode, v.read_page_color, app.session.prefs.paper_color).hash(&mut h); app.session.prefs.markup.hash(&mut h); dim_body.hash(&mut h); format!("{:?}{:?}{:?}", app.session.doc.settings.page_color, app.session.doc.settings.page_picture, app.session.doc.settings.watermark) @@ -233,12 +233,18 @@ pub(super) fn page_render_options(app: &WordApp, dim_body: bool) -> wordcraft_re opts.display.revisions = app.session.prefs.markup.clone(); opts.dark = app.session.view.dark_mode; opts.dark_paper = wordcraft_render::DARK_PAPER; + if !opts.dark { + opts.paper = app.session.prefs.paper_color.color(); + } // In Read Mode the Page color sets the paper and the dark page, and Switch Modes is ignored. if app.session.view.read_mode { use wordcraft_engine::cmd::view::ReadPageColor; let color = app.session.view.read_page_color; - opts.read_paper = Some(if color == ReadPageColor::Sepia { wordcraft_render::SEPIA_PAPER } else { wordcraft_doc::props::Rgb::WHITE }); + opts.read_paper = Some(if color == ReadPageColor::Sepia { wordcraft_render::SEPIA_PAPER } else { app.session.prefs.paper_color.color() }); opts.dark = color == ReadPageColor::Inverse; + if opts.dark { + opts.read_paper = Some(wordcraft_doc::props::Rgb::WHITE); + } } opts.display.dim_header = !dim_body; opts.display.dim_body = dim_body; diff --git a/crates/ui-egui/src/canvas/tests.rs b/crates/ui-egui/src/canvas/tests.rs index 6450f1292a..a81ab0c957 100644 --- a/crates/ui-egui/src/canvas/tests.rs +++ b/crates/ui-egui/src/canvas/tests.rs @@ -4841,3 +4841,39 @@ fn the_view_key_changes_when_a_drag_takes_an_object_off_the_page() { assert_ne!(hidden, view_key(&app, false, None), "the page without the dragged object is rendered again"); assert_eq!(hidden, view_key(&app, false, Some(&object)), "the same drag keeps the key: no re-render every frame"); } + +#[test] +fn screen_paper_color_changes_raster_and_cache_without_changing_export_paper() { + use wordcraft_doc::props::Rgb; + let mut app = WordApp::new(wordcraft_engine::Session::new(wordcraft_doc::Document::new()), Default::default()); + let layout = app.session.layout(); + let page = &layout.pages[0]; + let print_before = wordcraft_render::render_page(&app.session.doc, page, 0.1, &Default::default()); + let mut keys = vec![view_key(&app, false, None)]; + for (value, expected) in [("green", Rgb(0xCC, 0xE8, 0xCF)), ("cream", Rgb(0xFA, 0xF0, 0xDC)), ("gray", Rgb(0xE8, 0xEA, 0xED))] { + app.run("view.paperColor", json!({"value":value})).unwrap(); + let key = view_key(&app, false, None); + assert!(!keys.contains(&key), "each preset invalidates page and tile caches"); + keys.push(key); + let opts = page_render_options(&app, false); + assert_eq!(opts.paper_of(&app.session.doc), expected); + let screen = wordcraft_render::render_page(&app.session.doc, page, 0.1, &opts); + assert_eq!(&screen.pixels[..4], &[expected.0, expected.1, expected.2, 255]); + let print = wordcraft_render::render_page(&app.session.doc, page, 0.1, &Default::default()); + assert_eq!(print.pixels, print_before.pixels, "screen tints cannot leak into output rasters"); + } + app.run("view.darkMode", json!({"value":true})).unwrap(); + let d = wordcraft_render::DARK_PAPER; + assert_eq!(page_render_options(&app, false).paper_of(&app.session.doc), Rgb(d, d, d)); + app.run("view.darkMode", json!({"value":false})).unwrap(); + assert_eq!(page_render_options(&app, false).paper_of(&app.session.doc), Rgb(0xE8, 0xEA, 0xED)); + app.run("design.pageColor", json!({"color":"#102040"})).unwrap(); + assert_eq!(page_render_options(&app, false).paper_of(&app.session.doc), Rgb(0x10, 0x20, 0x40)); + app.run("view.readMode", json!({})).unwrap(); + assert_eq!(page_render_options(&app, false).paper_of(&app.session.doc), Rgb(0xE8, 0xEA, 0xED)); + app.run("view.readPageColor", json!({"value":"sepia"})).unwrap(); + assert_eq!(page_render_options(&app, false).paper_of(&app.session.doc), wordcraft_render::SEPIA_PAPER); + app.run("view.readPageColor", json!({"value":"inverse"})).unwrap(); + let d = wordcraft_render::DARK_PAPER; + assert_eq!(page_render_options(&app, false).paper_of(&app.session.doc), Rgb(d, d, d)); +} diff --git a/crates/ui-egui/src/options.rs b/crates/ui-egui/src/options.rs index 83f9d77deb..f48cd5c2f5 100644 --- a/crates/ui-egui/src/options.rs +++ b/crates/ui-egui/src/options.rs @@ -212,6 +212,21 @@ fn general(app: &mut WordApp, ui: &mut Ui) { section(ui, "Interface"); theme_picker(app, ui); interface_size_picker(app, ui); + ui.horizontal(|ui| { + use wordcraft_engine::cmd::view::PaperColor; + let label = ui.label(tl!("Screen paper color:")); + let selected = app.session.prefs.paper_color; + let combo = craft_ui::ComboBox::from_id_salt("screen_paper_color").selected_text(tl!(selected.label())).width(220.0).show_ui(ui, |ui| { + for color in PaperColor::ALL { + if ui.selectable_label(selected == color, tl!(color.label())).clicked() { + let _ = app.run("view.paperColor", json!({"value": color})); + } + } + }); + combo.response.labelled_by(label.id).on_hover_text(tl!( + "Screen only. Document backgrounds, printing and exports keep their colours. Dark page and explicit Read Mode colours take precedence." + )); + }); ui.checkbox(&mut app.ui.mini_toolbar, tl!("Show the mini toolbar when text is selected")); let mut dark_page = app.session.view.dark_mode; if ui diff --git a/crates/ui-egui/src/options/tests.rs b/crates/ui-egui/src/options/tests.rs index 3bb85f05c8..747e4ab89a 100644 --- a/crates/ui-egui/src/options/tests.rs +++ b/crates/ui-egui/src/options/tests.rs @@ -477,3 +477,30 @@ fn length_units_follow_the_interface_language() { h.run_steps(4); assert_eq!(length(72.0), "25,4 mm", "English keeps the English names"); } + +#[test] +fn screen_paper_color_options_round_trip_and_work_without_a_document() { + use wordcraft_engine::cmd::view::PaperColor; + let mut a = app(); + a.start_screen(); + a.run("view.paperColor", json!({"value":"green"})).unwrap(); + let saved = serde_json::to_vec(&a.prefs()).unwrap(); + let mut b = app(); + b.apply_prefs(UiState::from_json(&saved).unwrap()); + assert_eq!(b.session.prefs.paper_color, PaperColor::Green); + b.run("file.options", json!({"pane":"general"})).unwrap(); + let mut h = 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); + }, + b, + ); + h.run_steps(6); + h.get_by_label("Screen paper color:").click(); + h.run_steps(2); + h.get_by_label("Warm cream").click(); + h.run_steps(3); + assert_eq!(h.state().session.prefs.paper_color, PaperColor::Cream); +} diff --git a/docs/roadmap.md b/docs/roadmap.md index efa50e38f0..0e50923839 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,5 +1,6 @@ # WordCraft roadmap: milestones and what's next +> **Last reviewed:** 2026-10-31 · **Last updated:** 2026-10-31 · **Change:** minor (#3398: persistent screen paper colors; parity estimates unchanged) > **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-29 · **Change:** trivial (HTML import keeps preformatted lines with CR, CRLF or LF input newlines (#4810); numeric character references retain their separate meaning; readiness estimates unchanged; previously trivial: egui 0.37 migration: 27 interface languages remain pinned by `craft-i18n.rev` (9205108…), and the input-method handling has synthetic regressions while managed CI passed and native acceptance (#155–#164) remains open; previously trivial: M6: Manage Sources imports Word's `Sources.xml`, #2640; previously minor: Czech proofing review fixes; previously minor: Portuguese proofing; previously minor: Polish interface and Polish proofing; previously trivial: M7: formatting revisions landed, #41; previously trivial: M5: Draw Table and Eraser landed, #303; previously minor: beta distance restated: ~15 points, ~150–230 h; previously: alpha gate checked: six core workflows pass; previously major: milestones moved here from ROADMAP.md; Current focus set from the re-measured gaps) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 Forward-looking plan. The summary is [`ROADMAP.md`](../ROADMAP.md); the ranked work list is @@ -55,7 +56,7 @@ partial rows are fidelity gaps (beta work), not broken workflows. WordCraft stay | M5 | Tables | done (row splitting, floating tables, style formatting); custom table styles (#256); Draw Table and Eraser (#303) | 6–10 h | | M6 | References | first version done (TOC fields, cross-references, Zotero); footnote continuation missing; all 12 bibliography styles (#383); bibliography sources import from Word's `Sources.xml` (#2640) | 15–25 h | | M7 | Review | done (balloons, paragraph-mark revisions, formatting revisions #41); move tracking, multilingual proofing missing | 35–60 h | -| M8 | View | mostly done; window commands missing | 3–5 h | +| M8 | View | mostly done; persistent screen paper presets added (#3398); window commands missing | 3–5 h | | M9 | Mailings | done (first version); Excel/Outlook data sources and email merge missing | 6–10 h | | M10 | File/Backstage | mostly done; native printing, Options depth missing | 12–20 h | | M11 | Draw and objects | text wrap, text boxes, canvas handles, rotation (#332), ink pens and eraser (#307), contour wrap (#354), lasso, Add Pen, Ink to Shape and Ink Replay (#395) done; Ink to Math, grouping missing | 35–55 h | @@ -74,6 +75,7 @@ languages, screen-reader access, ecosystem decisions (VBA, add-ins, cloud), an i | Date | Change | Summary | |---|---|---| +| 2026-10-31 | minor | #3398: persistent screen-only green, cream and gray paper presets, with document backgrounds and export colors retained. Read Mode None follows the screen preset; Sepia/Inverse retain priority. | | 2026-10-29 | trivial | HTML import keeps preformatted lines with CR, CRLF or LF input newlines (#4810); numeric character references retain their separate meaning; readiness estimates unchanged. | | 2026-10-25 | trivial | egui 0.37 migration: 27 interface languages remain pinned by `craft-i18n.rev` (9205108…); input-method surrounding-delete and commit handling has synthetic regressions only, with managed CI passing; native acceptance (#155–#164) and native-speaker review remain open | | 2026-10-18 | trivial | M6: Manage Sources imports Word's `Sources.xml` (#2640) | diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 18ea32347e..d3a80bec1d 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,5 +1,6 @@ # UI parity with Microsoft Word +> **Last reviewed:** 2026-10-31 · **Last updated:** 2026-10-31 · **Change:** minor (#3398: persistent screen paper colors; parity estimates unchanged) > **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-31 · **Change:** trivial (Alt, F badges the Backstage pages; Alt, F, P and V open Print, #4960); previously trivial (View › Macros opens a Macros dialog with Run, Delete and Record, #4327); previously trivial (aligned-copy review fixes retain the document axis through scrolling and include secondary selections (#4764)); previously trivial (Table Properties: row, column and cell settings apply to the selected rows, columns and cells; mixed row check boxes, #4857); previously trivial (Insert › Shapes shows the basic shapes as a grid of drawn icon tiles, #4349); previously trivial (File › Options › Proofing › AutoCorrect Options opens with no document open, #4585); previously trivial (the Shading buttons show the last picked colour, #4412); previously trivial (a blank User Name in File › Options stays blank after a restart, #4368); previously trivial (window size falls back to the screen rectangle when the reported inner size is invalid; equation symbol tooltips no longer repeat a missing glyph's name); previously trivial (window titles and suggested file names omit NUL characters, #3634); previously trivial (Recipient labels continue across product-sized sheets instead of stopping after the first sheet (#4880)); previously trivial (Custom Document Properties dialog from File › Info › Properties › Advanced Properties…, #2982; previously trivial: View › Window › New Window opens another window on the document, each with its own caret and view; previously trivial: mail merge: First Record, Last Record and a record number box in Preview Results; Finish & Merge asks which records to merge or print, #3028; previously trivial: selection bar for right-to-left paragraphs and table rows; previously trivial: read only and comments-only protection grey out edits; previously trivial: the Font dialog previews "NaN" or "inf" in the Size box at 12 pt, #3663; previously trivial: File › Options disables the User name field while Restrict Editing is active; previously trivial: while editing a header or footer, a click in the other one edits it there without scrolling; previously trivial: explicit documents keep the editor when recovery discovery finishes; readable recovery controls; previously trivial: Insert Table of Figures dialog: any caption label, #3013; previously trivial: Home › Underline drop-down shows its lines, #2738; previously trivial: the text context menu includes the formatting mini toolbar, #2959; previously trivial: personal templates in File › New and Save as Word Template, #2852; previously trivial: the scrollbar page tip stays on one line as the page number grows, #3341; previously trivial: File › Home and New: templates and recent documents are buttons named for screen readers, #3176; previously trivial: forms protection greys out the Developer tab's control buttons and Properties; previously trivial: AltGr doesn't open KeyTips, #2388; previously trivial: closing the header or footer puts the caret back where it was in the body, #3331; previously trivial: Format Page Numbers dialog: number format and Continue from previous section / Start at, #1113; previously trivial: inline pictures drag to a dashed insertion cursor and stay inline, #2233, #2234; previously trivial: Save Changes prompt: a text file saved over itself closes, #2376; previously trivial: the equation being edited is framed as in Word: an outline as tall as its line, a grip, an options strip that opens its menu, and its current argument shaded; previously trivial: the table handles show while the caret is in the table, #2428; previously trivial: legacy menu keys, #1697; previously trivial: Shape Fill › Gradient and the Format Shape pane, #1500; previously trivial: Focus fills the screen, and Full Screen View shows the pages alone, #1678; previously trivial: the text pointer over the page is drawn black or white on Windows, #1746; previously trivial: a click in the header/footer band from the body no longer scrolls to the body text, #2103; previously trivial: File › Recover follows the app theme, #3243; previously trivial: long navigation headings wrap onto further lines within the pane, #3015; previously trivial: clearing a no-match font query keeps the recent heading visible, #2068; previously trivial: dragging the vertical scrollbar names the page at the top of the window, #2551; previously trivial: Add Font imports every selected font file, #1789); previously trivial (File › Close keeps the window, #1712; previously trivial: the Spike: Ctrl+F3 and Ctrl+Shift+F3; previously trivial: Ctrl+wheel zoom no longer scrolls the document, #1743; previously trivial: a mouse drag on a table leaves the view where it is, #1738; previously trivial: numbered headings, #1793; previously trivial: Navigation Find exposes Results, #2096; previously trivial: the tab strip never draws tabs over Comments / Editing / Share, #2026; previously trivial: Columns on selected text, #2024; previously trivial: Different First Page headers reachable from page 1, #2826; previously trivial: a copied equation also goes in the clipboard's MathML formats, #1297; previously trivial: Read Mode keeps the keyboard, shows whole columns and keeps text clear of the page-turn bar, #1772); previously trivial: font list entries of tall-ascent fonts sit in their row, not cut off, #2570); previously trivial: Split Cells and Convert Text to Table dialogs follow Word's merged-cell limits and Initial column width, #2592; previously trivial: a new source's Tag is made like Word's, three letters of the author and two digits of the year with a number added when taken, and an edit keeps the Tag, #2094; previously trivial: Web Layout text fills the window width, #1876; previously trivial: Language dialog keeps inherited proofing language and no-proof choices (#2717)); previously trivial: comment balloons readable in the dark theme, #2045; previously trivial: Recently Used Fonts section at the top of the font list, #2068; previously trivial: web preferences kept between visits, #2132; previously trivial: dialog close control: a Windows-style box at the title bar's right end, the whole box clickable, red on hover, #1885); previously trivial (Modify Table Style formats every table style part, #714); previously trivial: a click on the ribbon chrome hands the keyboard to the page in the same frame, #1093 follow-up; previously trivial: Page Setup vertical alignment, #1320; previously trivial: the app opens on the Home tab every launch, as in Word; 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 Recovery discovery no longer interrupts an explicitly created or opened document. Older copies remain in File › Recover, whose buttons use the interface palette. See [local recovery](recovery.md). @@ -68,6 +69,21 @@ Save Changes exposes the document-specific question as an alert-dialog name and The Language dialog reads effective character formatting, including document defaults, paragraph styles and character styles. Accepting it unchanged keeps inherited language and no-proof choices (#2717); explicit direct formatting still takes precedence. Other proofing and Windows-specific reports remain separate. +## Screen paper colors (#3398) + +File › Options › General › Screen paper color selects Document background (the default), +Soft green, Warm cream or Soft gray. The selection persists in user preferences, not in a document. +It changes blank paper in editing views and in Read Mode's None color; a document's explicit +page color or background picture remains visible while editing. Read Mode's explicit Sepia and +Inverse colors override the preference. Choosing a tint turns off Dark page; enabling Dark page +later takes precedence until it is turned off again. Printing, PDF/image exports and saved +DOCX background styling are unaffected. This is a WordCraft extension (`view.paperColor`), not +an increase in the Microsoft Word parity score. + +Evidence: engine `screen_paper_color_is_a_persistent_preference_not_document_formatting`, +canvas `screen_paper_color_changes_raster_and_cache_without_changing_export_paper`, and +options `screen_paper_color_options_round_trip_and_work_without_a_document` tests. + ## Find and Replace: regular expressions (#4177) With **Regular expressions** on (Search Options), Replace with expands capture groups: `$1`…`$n`, `${n}`, `${name}` and `$name` (the longest run of letters, digits and `_`), `$0` / `${0}` (the whole match), and `\1`…`\9` as in wildcard Replace; `$$` is a dollar sign, `\t` a tab, `\\` a backslash, `\$` and `\^` a `$` or `^` as typed, and `\n` a paragraph mark, which splits the paragraph as `^p` does (Replace All stays one undo step). Word's `^&`, `^p`, `^t`, `^c` and the other `^` codes work there too (with Special characters off, only the `^` codes are taken literally). A group that doesn't exist, didn't take part in the match or is out of range expands to nothing; a `$` that starts no group, an unclosed `${`, and other `\` escapes stay as written. Plain and wildcard Replace are unchanged. Word has no regular-expression mode; this follows the Rust `regex` crate's Replace syntax. @@ -86,6 +102,7 @@ Labels → New Document with “One label per recipient” includes all filtered | Date | Change | Summary | |---|---|---| +| 2026-10-31 | minor | #3398: persistent screen-only green, cream and gray paper presets, with document backgrounds and export colors retained. Read Mode None follows the screen preset; Sepia/Inverse retain priority. | | 2026-10-31 | trivial | Keytips: Alt, F badges the Backstage pages with Word's letters and the letter opens the page; Alt, F, P and Alt, F, V open Print; the badges end with the Backstage (#4960) | | 2026-10-31 | trivial | View › Macros opens a Macros dialog (also Alt+F8 and command search): the macros of this session listed with Run, Delete and Record, which reads Stop Recording while a recording runs; the button did nothing before, as `tools.macros` without a macro to run, define or delete only returned the list (#4327) | | 2026-10-28 | trivial | Aligned copies retain their locked document coordinate during middle-button scrolling and can start on secondary selected objects; existing Ctrl and Shift selection behavior stays (#4764) |