diff --git a/ROADMAP.md b/ROADMAP.md index d6bd358..8d95cbf 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-12 · **Change:** trivial (Start screen at launch, #766; previously trivial: Turkish interface catalog completed, #774; previously trivial: Hungarian interface landed; previously trivial: Quick Access Toolbar KeyTips and a saved enable/disable option, #620; previously trivial: new documents follow the system region, #838; previously minor: Polish interface and Polish proofing; 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-12 · **Change:** trivial (Undo history list, #701; previously trivial: Turkish interface catalog completed, #774; previously trivial: Hungarian interface landed; previously trivial: Quick Access Toolbar KeyTips and a saved enable/disable option, #620; previously trivial: new documents follow the system region, #838; previously minor: Polish interface and Polish proofing; 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 WordCraft aims for complete parity with Microsoft Word, then goes further on speed, openness and agent control. This page is the summary; the assessment is @@ -14,7 +14,7 @@ agent control. This page is the summary; the assessment is | Number | Value | Kind | |---|---|---| -| Ribbon/menu catalog coverage | **484 / 499 (97.0%)** | measured (`cargo xtask parity` → [`docs/parity-checklist.md`](docs/parity-checklist.md)) | +| Ribbon/menu catalog coverage | **485 / 500 (97.0%)** | measured (`cargo xtask parity` → [`docs/parity-checklist.md`](docs/parity-checklist.md)) | | **Feature breadth** (weighted, incl. dialog options, styles, languages beyond the ribbon) | **~80%** | estimated | | **Ready for real work** (full target) | **~60%** (55–63%) | estimated, additive weighted sum over the dimensions | | **Mainstream practitioner** | **~55%** | estimated: weekly-work depth × discounts for interaction (×0.93), stability (×0.90), file exchange (×0.90) | @@ -139,6 +139,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm | 2026-10-12 | 32-bit Windows starts on OpenGL. DirectX 12 graphics pipeline creation returns "The parameter is incorrect" and the app exited before the window appeared | | 2026-10-12 | New documents follow the system region (#838): A4 and centimetres outside the US (Letter in the CLDR Letter territories, inches in the US), lengths written with the region's decimal separator (`2,54 cm`) and typed with either; an explicit Measurement units choice wins; `file.options {"region"}` for agents | | 2026-10-12 | Paste from other programs keeps their formatting (#687): Ctrl+V and Home › Paste read the system clipboard's HTML (and RTF on Windows and Wayland); Merge Formatting takes the destination's font and paragraph and keeps bold, italic, underline and strikethrough; Paste Special lists what the system clipboard holds (HTML, RTF, picture, text). Linux: pictures copied in a browser or Spectacle paste on KDE and other Wayland desktops (data-control clipboard), and a paste that finds nothing usable names what the clipboard holds instead of "empty" (#833) | +| 2026-10-12 | Undo history list (#701): a ▾ beside the Quick Access Toolbar's Undo lists recent steps newest first; hovering highlights what a click undoes ("Undo 3 Actions"), and a click undoes them in one go. `edit.undoTo {count}` and `edit.undoHistory` (newest first, up to 100) for CLI and MCP | | 2026-10-12 | Start screen (#766): launched with nothing to open, WordCraft starts on File › Home (Blank document, templates, Recent) like Word, desktop and web; Esc leaves a blank document; File › Options › General › Start-up turns it off | | 2026-10-12 | Word 365 cover pages (#743): a freeform inside a group keeps its shape (it was drawn as a rectangle); text boxes keep their own internal margins and Resize shape to fit text, so a tight title box no longer loses its second line; both survive saving. The missing fills in the report were theme-coloured shapes, fixed by #611 | | 2026-10-12 | Alt+Q moves to "Tell me" command search, as in Word for Windows (#819); not on macOS, where Option+Q types a character | @@ -246,6 +247,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm | Date | Change | Summary | |---|---|---| +| 2026-10-12 | trivial | Progress log: Undo history list (#701) | | 2026-10-12 | trivial | Progress log: Start screen at launch (#766) | | 2026-10-12 | trivial | Progress log: Alt+Q command search (#819); log table header fixed | | 2026-10-12 | trivial | Progress log: ribbon groups fit above their labels | diff --git a/crates/engine/src/catalog.rs b/crates/engine/src/catalog.rs index 47cbfc5..c0cac3e 100644 --- a/crates/engine/src/catalog.rs +++ b/crates/engine/src/catalog.rs @@ -31,6 +31,7 @@ File|Backstage|Recover Unsaved|file.recover File|Backstage|New from Template|file.newFromTemplate File|Backstage|Save as Template|file.saveTemplate Quick Access|Toolbar|Undo|edit.undo +Quick Access|Toolbar|Undo List|edit.undoTo Quick Access|Toolbar|Redo|edit.redo Quick Access|Toolbar|Repeat|edit.repeat Quick Access|Toolbar|AutoSave|file.autosave diff --git a/crates/engine/src/cmd/edit.rs b/crates/engine/src/cmd/edit.rs index eed8113..d8b78a5 100644 --- a/crates/engine/src/cmd/edit.rs +++ b/crates/engine/src/cmd/edit.rs @@ -7,6 +7,26 @@ use wordcraft_doc::{Pos, StoryRef}; use super::{delete_selection, find_pattern, pos_json, sel_result}; use crate::{CmdError, CmdResult, CommandSpec, Selection, Session, p}; +/// Undo several steps at once: the Undo button's list (#701). `count` is validated here and +/// clamped to the undo depth, so hostile numbers never reach the stack. +fn undo_to(s: &mut Session, v: &Value) -> CmdResult { + let n = p::u64(v, "count").ok_or_else(|| CmdError::Params("`count` (whole number, 1 or more) is required".into()))?; + if n == 0 { + return Err(CmdError::Params("`count` must be 1 or more".into())); + } + let n = usize::try_from(n).unwrap_or(usize::MAX).min(s.undo_depth()); + if s.step_lifts_protection(false) { + return Err(CmdError::Disabled("edit.undoTo: use Stop Protection to lift the document's protection".into())); + } + let undone = s.undo_steps(n); + let mut r = sel_result(s)?; + if let Some(m) = r.as_object_mut() { + m.insert("undone".into(), json!(undone)); + m.insert("depth".into(), json!(s.undo_depth())); + } + Ok(r) +} + fn has_sel(s: &Session) -> Option<&'static str> { if s.sel.is_collapsed() { Some("nothing is selected") } else { None } } @@ -22,6 +42,14 @@ pub fn specs() -> Vec { }) .key("Mod+Z") .pure(), + CommandSpec::new("edit.undoTo", "Undo Actions", "Quick Access Toolbar", undo_to) + .params(r#"{"count": n (1 = the newest step, like edit.undo; n undoes the newest n steps at once; at most the undo depth)}"#) + .when(|s| if s.can_undo() { None } else { Some("there is nothing to undo") }) + .pure(), + CommandSpec::new("edit.undoHistory", "Undo History", "Quick Access Toolbar", |s, _| { + Ok(json!({"undo": s.undo_history(), "depth": s.undo_depth(), "redo": s.redo_label()})) + }) + .pure(), CommandSpec::new("edit.redo", "Redo", "Quick Access Toolbar", |s, _| { if s.step_lifts_protection(true) { return Err(CmdError::Disabled("edit.redo: use Stop Protection to lift the document's protection".into())); diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 9c10bef..6f30efd 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -26,7 +26,9 @@ use serde_json::Value; pub use io::Password; pub use keymap::KeyMap; pub use ribbon::RibbonLayout; -pub use session::{ColumnBlock, EditSnapshot, FindState, MathEdit, Prefs, Selection, Session, ViewState, blank_document, set_system_locale}; +pub use session::{ + ColumnBlock, EditSnapshot, FindState, MathEdit, Prefs, Selection, Session, UNDO_LIST_MAX, ViewState, blank_document, set_system_locale, +}; pub use wordcraft_doc as doc; pub use wordcraft_geom::Region; pub use wordcraft_layout as layout; diff --git a/crates/engine/src/session.rs b/crates/engine/src/session.rs index f200f99..20d3532 100644 --- a/crates/engine/src/session.rs +++ b/crates/engine/src/session.rs @@ -490,6 +490,8 @@ fn doc_bib_style(doc: &Document) -> String { /// Maximum undo depth. const MAX_UNDO: usize = 500; +/// How many steps the Undo button's list shows (#701); `edit.undoTo` reaches no further. +pub const UNDO_LIST_MAX: usize = 100; impl Session { /// File name of the persisted custom dictionary inside the settings folder. @@ -799,6 +801,21 @@ impl Session { pub fn undo_labels(&self) -> Vec { self.history.iter().rev().map(|u| u.label.clone()).collect() } + /// The Undo button's list (#701): the undo steps' labels (English, as commands name them), + /// newest first, at most [`UNDO_LIST_MAX`]. + pub fn undo_history(&self) -> Vec<&str> { + self.history.iter().rev().take(UNDO_LIST_MAX).map(|u| u.label.as_str()).collect() + } + /// Undo the newest `count` steps in one go, as picking an entry in the Undo list does (#701). + /// More than the undo depth undoes everything; a step that would take the document's + /// protection away stops it (only Stop Protection lifts it). Returns the steps undone. + pub fn undo_steps(&mut self, count: usize) -> usize { + let mut done = 0; + while done < count && !self.step_lifts_protection(false) && self.undo() { + done += 1; + } + done + } pub fn undo(&mut self) -> bool { self.typing_open = false; let Some(u) = self.history.pop().map(Arc::unwrap_or_clone) else { return false }; @@ -1089,7 +1106,8 @@ impl Session { self.typing_open = false; } // Only the paste right after a tracked cut makes it a move. - if (spec.mutates || matches!(id, "edit.undo" | "edit.redo")) && !matches!(id, "edit.cut" | "edit.paste" | "edit.pasteSpecial") { + if (spec.mutates || matches!(id, "edit.undo" | "edit.undoTo" | "edit.redo")) && !matches!(id, "edit.cut" | "edit.paste" | "edit.pasteSpecial") + { self.pending_move = None; } let sel_before = self.sel.clone(); diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index a630745..de503fb 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -203,6 +203,68 @@ fn typing_enter_undo() { assert_eq!(text(&s), "Hello world"); } +/// The Undo button's list (#701): several steps undone at once, newest first. +#[test] +fn undo_to_takes_several_steps_back_at_once() { + let mut s = s(); + run(&mut s, "text.insert", json!({"text": "Hello"})); + run(&mut s, "select.all", json!({})); + run(&mut s, "format.bold", json!({})); + run(&mut s, "caret.docEnd", json!({})); + run(&mut s, "text.newParagraph", json!({})); + run(&mut s, "text.insert", json!({"text": "World"})); + let h = run(&mut s, "edit.undoHistory", json!({})); + assert_eq!(h["undo"], json!(["Typing", "New Paragraph", "Bold", "Typing"])); + assert_eq!(h["depth"], 4); + assert_eq!(s.undo_history(), ["Typing", "New Paragraph", "Bold", "Typing"]); + // Three steps in one command: World, the paragraph and the bold go; Hello stays. + let r = run(&mut s, "edit.undoTo", json!({"count": 3})); + assert_eq!(r["undone"], 3); + assert_eq!(r["depth"], 1); + assert_eq!(text(&s), "Hello"); + assert_eq!(s.doc.para_at(&s.sel.focus).map(|p| p.props_of_char(1).bold), Some(None)); + assert_eq!(s.undo_history(), ["Typing"]); + // Each undone step is one redo step. + run(&mut s, "edit.redo", json!({})); + run(&mut s, "edit.redo", json!({})); + run(&mut s, "edit.redo", json!({})); + assert_eq!(text(&s), "Hello\nWorld"); + // More than the depth undoes everything. + let r = run(&mut s, "edit.undoTo", json!({"count": 1e300})); + assert_eq!(r["undone"], 4); + assert_eq!(text(&s), ""); + assert!(!s.can_undo()); + assert!(s.run("edit.undoTo", &json!({"count": 1})).is_err(), "nothing to undo"); +} + +#[test] +fn undo_to_rejects_bad_counts_and_leaves_the_document_alone() { + let mut s = s(); + run(&mut s, "text.insert", json!({"text": "Hello"})); + run(&mut s, "text.newParagraph", json!({})); + let before = (s.doc.clone(), s.undo_depth()); + for junk in + [json!({}), json!(null), json!({"count": 0}), json!({"count": -3}), json!({"count": "2"}), json!({"count": [2]}), json!({"count": -1e308})] + { + assert!(s.run("edit.undoTo", &junk).is_err(), "{junk}"); + assert_eq!((s.doc.clone(), s.undo_depth()), before, "{junk}"); + } + // A fraction counts whole steps. + let r = run(&mut s, "edit.undoTo", json!({"count": 1.9})); + assert_eq!(r["undone"], 1); + assert_eq!(text(&s), "Hello"); +} + +#[test] +fn undo_history_is_capped() { + let mut s = s(); + for i in 0..(crate::UNDO_LIST_MAX + 20) { + run(&mut s, "para.indents", json!({"left": i as f64})); + } + assert_eq!(s.undo_history().len(), crate::UNDO_LIST_MAX); + assert_eq!(run(&mut s, "edit.undoHistory", json!({}))["undo"].as_array().map(Vec::len), Some(crate::UNDO_LIST_MAX)); +} + #[test] fn joined_commands_are_one_undo_step() { let mut s = s(); diff --git a/crates/ui-egui/src/chrome.rs b/crates/ui-egui/src/chrome.rs index 9b88ad5..c355f8b 100644 --- a/crates/ui-egui/src/chrome.rs +++ b/crates/ui-egui/src/chrome.rs @@ -58,6 +58,9 @@ fn quick_access(ui: &mut Ui, app: &mut WordApp) { } } crate::widgets::command_menu(&resp, app, id); + if id == "edit.undo" { + undo_list_button(ui, app, enabled); + } } let t = Tokens::get(ui.ctx()); let (r, _) = ui.allocate_exact_size(vec2(26.0, 24.0), Sense::hover()); @@ -73,6 +76,85 @@ fn quick_access(ui: &mut Ui, app: &mut WordApp) { }); } +/// The Undo button's drop-down arrow (#701), which opens [`undo_list`]. +pub(crate) fn undo_list_button_id() -> egui::Id { + egui::Id::new("qat_undo_list") +} + +/// Row `i` of the Undo list (0 = the newest step). +fn undo_list_row_id(i: usize) -> egui::Id { + undo_list_button_id().with(("row", i)) +} + +/// The Undo list's rows, newest first: each step named as the Undo button's tooltip names it. +pub(crate) fn undo_list_rows(app: &WordApp) -> Vec { + app.session.undo_history().into_iter().map(|l| tl!(l).to_string()).collect() +} + +/// The Undo list's footer: how many steps a click undoes, or Cancel while none is highlighted. +pub(crate) fn undo_list_footer(n: usize) -> String { + match n { + 0 => tl!("Cancel").to_string(), + 1 => tl!("Undo 1 Action").to_string(), + n => crate::i18n::fmt(tl!("Undo {n} Actions"), &[("n", &n.to_string())]), + } +} + +/// The small arrow beside Undo: opens the list of steps to undo several at once. +fn undo_list_button(ui: &mut Ui, app: &mut WordApp, enabled: bool) { + let t = Tokens::get(ui.ctx()); + let (r, _) = ui.allocate_exact_size(vec2(12.0, 24.0), Sense::hover()); + let resp = ui.interact(r, undo_list_button_id(), if enabled { Sense::click() } else { Sense::hover() }); + if resp.hovered() && enabled { + ui.painter().rect_filled(r, 4.0, t.hover); + } + let c = if enabled { t.icon } else { t.text_disabled }; + icons::paint(ui.painter(), Rect::from_center_size(r.center(), vec2(9.0, 9.0)), "dropdown", c, c); + let resp = resp.on_hover_text(tl!("Undo History")); + egui::Popup::menu(&resp).show(|ui| undo_list(ui, app)); +} + +/// The Undo list (#701): recent steps, newest first. Hovering a row highlights it and every newer +/// one; clicking it undoes them all in one go (`edit.undoTo`). +fn undo_list(ui: &mut Ui, app: &mut WordApp) { + const W: f32 = 210.0; + const ROW: f32 = 20.0; + let t = Tokens::get(ui.ctx()); + let rows = undo_list_rows(app); + ui.set_width(W); + let (mut hovered, mut clicked) = (None, None); + egui::ScrollArea::vertical().max_height(ROW * 18.0).auto_shrink([false, true]).show(ui, |ui| { + ui.spacing_mut().item_spacing.y = 0.0; + let rects: Vec = (0..rows.len()) + .map(|i| { + let (r, _) = ui.allocate_exact_size(vec2(ui.available_width(), ROW), Sense::hover()); + let resp = ui.interact(r, undo_list_row_id(i), Sense::click()); + if resp.hovered() { + hovered = Some(i); + } + if resp.clicked() { + clicked = Some(i); + } + r + }) + .collect(); + for (i, (r, label)) in rects.iter().zip(&rows).enumerate() { + if hovered.is_some_and(|h| i <= h) { + ui.painter().rect_filled(*r, 0.0, t.selection); + } + // Clipped to its row: a long label never runs into the scroll bar. + ui.painter_at(r.shrink2(vec2(6.0, 0.0))).text(pos2(r.min.x + 6.0, r.center().y), Align2::LEFT_CENTER, label, regular(12.0), t.text); + } + }); + ui.separator(); + let footer = undo_list_footer(hovered.map_or(0, |h| h + 1)); + ui.vertical_centered(|ui| ui.label(egui::RichText::new(footer).font(regular(12.0)).color(t.text))); + if let Some(i) = clicked { + let _ = app.run("edit.undoTo", json!({"count": i + 1})); + ui.close(); + } +} + /// The Quick Access Toolbar's menu button (…), which opens [`qat_menu`]. pub(crate) fn qat_menu_button_id() -> egui::Id { egui::Id::new("qat_menu_button") @@ -426,6 +508,63 @@ impl WordApp { mod tests { use super::*; + fn ctx() -> egui::Context { + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + crate::theme::apply(&ctx, crate::theme::Appearance::Light); + ctx.run_ui(egui::RawInput::default(), |_| {}).drop_without_applying_deltas(); + ctx + } + + fn frame(ctx: &egui::Context, n: &mut u32, events: Vec, a: &mut WordApp) { + *n += 1; + let input = egui::RawInput { time: Some(f64::from(*n) / 60.0), predicted_dt: 1.0 / 60.0, events, ..Default::default() }; + ctx.run_ui(input, |ui| title_bar(a, ui)).drop_without_applying_deltas(); + } + + fn click(ctx: &egui::Context, n: &mut u32, pos: egui::Pos2, a: &mut WordApp) { + let button = |pressed| egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() }; + frame(ctx, n, vec![egui::Event::PointerMoved(pos), button(true)], a); + frame(ctx, n, vec![button(false)], a); + } + + /// #701: the arrow beside Undo lists the session's undo steps, newest first; clicking the third + /// row undoes three steps at once. + #[test] + fn undo_list_matches_the_session_and_undoes_up_to_the_clicked_row() { + let mut a = WordApp::new(wordcraft_engine::Session::new(wordcraft_doc::Document::new()), crate::Services::default()); + a.run("text.insert", json!({"text": "Hello"})).unwrap(); + a.run("select.all", json!({})).unwrap(); + a.run("format.bold", json!({})).unwrap(); + a.run("format.italic", json!({})).unwrap(); + a.run("caret.docEnd", json!({})).unwrap(); + a.run("text.insert", json!({"text": " world"})).unwrap(); + let session: Vec = a.session.undo_history().into_iter().map(str::to_string).collect(); + assert_eq!(session, ["Typing", "Italic", "Bold", "Typing"]); + assert_eq!(undo_list_rows(&a), session); + assert_eq!(undo_list_footer(0), "Cancel"); + assert_eq!(undo_list_footer(1), "Undo 1 Action"); + assert_eq!(undo_list_footer(3), "Undo 3 Actions"); + + let (ctx, mut n) = (ctx(), 0); + frame(&ctx, &mut n, vec![], &mut a); + let arrow = ctx.read_response(undo_list_button_id()).unwrap(); + let popup = egui::Popup::default_response_id(&arrow); + click(&ctx, &mut n, arrow.rect.center(), &mut a); + assert!(egui::Popup::is_id_open(&ctx, popup), "the arrow opens the Undo list"); + frame(&ctx, &mut n, vec![], &mut a); + for i in 0..4 { + assert!(ctx.read_response(undo_list_row_id(i)).is_some(), "row {i} is drawn"); + } + assert!(ctx.read_response(undo_list_row_id(4)).is_none(), "one row per step"); + let third = ctx.read_response(undo_list_row_id(2)).unwrap(); + click(&ctx, &mut n, third.rect.center(), &mut a); + assert_eq!(a.session.undo_history(), ["Typing"], "three steps undone in one go"); + assert_eq!(a.session.doc.plain_text(wordcraft_doc::StoryRef::Body), "Hello"); + frame(&ctx, &mut n, vec![], &mut a); + assert!(!egui::Popup::is_id_open(&ctx, popup), "the list closes"); + } + #[test] fn mac_title_bar_centres_and_clears_the_traffic_lights() { // The lights are centred in the bar, and content keeps the same gap after them. diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index d0a8434..8860e16 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -2441,6 +2441,10 @@ Pastes the clipboard's picture. Поставя картината от клипборда. Region: Регион: Automatic ({region}) Автоматично ({region}) + Undo 1 Action Отмяна на 1 действие + Undo Actions Отмяна на действия + Undo History История на отмяната + Undo {n} Actions Отмяна на {n} действия Start-up Стартиране Show the Start screen when WordCraft starts Показване на началния екран при стартиране на WordCraft On: WordCraft opens on File › Home, with a blank document, the templates and your recent documents. Off: it opens a blank document. Вкл.: WordCraft се отваря на Файл › Начало с празен документ, шаблоните и последните документи. Изкл.: отваря се празен документ. diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 548ccb6..f5f44bc 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -2043,6 +2043,10 @@ Your system has animation effects turned off. Efekty animace jsou v systému vypnuté. Your system doesn't report an animation setting, so animation effects stay on. Systém neuvádí žádné nastavení animací, proto efekty animace zůstávají zapnuté. Do Not Check Spelling or Grammar Nekontrolovat pravopis ani gramatiku + Undo Actions Vrátit více akcí + Undo History Historie vrácení + Undo 1 Action Vrátit 1 akci + Undo {n} Actions Vrátit akce: {n} Start-up Spuštění Show the Start screen when WordCraft starts Při spuštění WordCraftu zobrazit úvodní obrazovku On: WordCraft opens on File › Home, with a blank document, the templates and your recent documents. Off: it opens a blank document. Zapnuto: WordCraft se otevře na kartě Soubor › Domů s prázdným dokumentem, šablonami a nedávnými dokumenty. Vypnuto: otevře prázdný dokument. diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index e33bb59..390af0a 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -840,6 +840,10 @@ Underline Words Only Nur Wörter unterstreichen Undo Rückgängig Undo {action} {action} rückgängig + Undo Actions Mehrere Aktionen rückgängig + Undo History Rückgängig-Verlauf + Undo 1 Action 1 Aktion rückgängig + Undo {n} Actions {n} Aktionen rückgängig Ungroup Gruppierung aufheben Up Nach oben Update Field Feld aktualisieren diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 230a536..18d1b89 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -955,6 +955,10 @@ Underline Words Only Subrayar solo las palabras Undo Deshacer Undo {action} Deshacer {action} + Undo Actions Deshacer acciones + Undo History Historial de deshacer + Undo 1 Action Deshacer 1 acción + Undo {n} Actions Deshacer {n} acciones Unformatted Text Texto sin formato Up Arriba Update Field Actualizar campo diff --git a/crates/ui-egui/src/i18n/et.tsv b/crates/ui-egui/src/i18n/et.tsv index d6d5c64..5eb1f52 100644 --- a/crates/ui-egui/src/i18n/et.tsv +++ b/crates/ui-egui/src/i18n/et.tsv @@ -831,6 +831,10 @@ Underline Words Only Jooni alla ainult sõnad Undo Võta tagasi Undo {action} Võta tagasi: {action} + Undo Actions Võta toimingud tagasi + Undo History Tagasivõtmise ajalugu + Undo 1 Action Võta tagasi 1 toiming + Undo {n} Actions Võta tagasi {n} toimingut Unformatted Text Vormindamata tekst Up Üles Update Field Uuenda välja diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 74cf63b..6a2bdad 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -2061,6 +2061,10 @@ Reading Column Width Largeur de colonne de lecture Reading Line Spacing Interligne de lecture Reading Text Size Taille du texte de lecture + Undo Actions Annuler plusieurs actions + Undo History Historique des annulations + Undo 1 Action Annuler 1 action + Undo {n} Actions Annuler {n} actions Start-up Démarrage Show the Start screen when WordCraft starts Afficher l'écran d'accueil au démarrage de WordCraft On: WordCraft opens on File › Home, with a blank document, the templates and your recent documents. Off: it opens a blank document. Activé : WordCraft s'ouvre sur Fichier › Accueil, avec un document vierge, les modèles et vos documents récents. Désactivé : il ouvre un document vierge. diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index de9387e..c61303e 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -2451,6 +2451,10 @@ Reading Column Width Olvasási oszlopszélesség Reading Line Spacing Olvasási sorköz Reading Text Size Olvasási szövegméret + Undo Actions Több művelet visszavonása + Undo History Visszavonási előzmények + Undo 1 Action 1 művelet visszavonása + Undo {n} Actions {n} művelet visszavonása Start-up Indítás Show the Start screen when WordCraft starts Kezdőképernyő megjelenítése a WordCraft indításakor On: WordCraft opens on File › Home, with a blank document, the templates and your recent documents. Off: it opens a blank document. Be: a WordCraft a Fájl › Kezdőlap oldalon nyílik meg, üres dokumentummal, a sablonokkal és a legutóbbi dokumentumokkal. Ki: üres dokumentumot nyit meg. diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index d06b86a..9306db3 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -857,6 +857,10 @@ Underline Words Only Sottolinea solo le parole Undo Annulla Undo {action} Annulla {action} + Undo Actions Annulla azioni + Undo History Cronologia annullamenti + Undo 1 Action Annulla 1 azione + Undo {n} Actions Annulla {n} azioni Unformatted Text Testo non formattato Up Su Update Field Aggiorna campo diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index c57a8c9..9218fa0 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -907,6 +907,10 @@ Underline Words Only 単語のみ下線 Undo 元に戻す Undo {action} 元に戻す: {action} + Undo Actions 複数の操作を元に戻す + Undo History 元に戻す履歴 + Undo 1 Action 1 件の操作を元に戻す + Undo {n} Actions {n} 件の操作を元に戻す Ungroup グループ解除 Up 上 Update Field フィールドの更新 diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 197ece3..0e660d1 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -2055,6 +2055,10 @@ {name} already exists. Choose Save again to replace it. Plik {name} już istnieje. Wybierz ponownie Zapisz, aby go zastąpić. {name} wasn't found. Nie znaleziono: {name}. “{text}” ({row}, {series}) isn't a number. „{text}” ({row}, {series}) nie jest liczbą. + Undo Actions Cofnij kilka akcji + Undo History Historia cofania + Undo 1 Action Cofnij 1 akcję + Undo {n} Actions Cofnij akcje: {n} Start-up Uruchamianie Show the Start screen when WordCraft starts Pokazuj ekran startowy po uruchomieniu programu WordCraft On: WordCraft opens on File › Home, with a blank document, the templates and your recent documents. Off: it opens a blank document. Włączone: WordCraft otwiera się na karcie Plik › Start z pustym dokumentem, szablonami i ostatnimi dokumentami. Wyłączone: otwiera pusty dokument. diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 53d28f9..065a642 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -907,6 +907,10 @@ Underline Words Only Sublinhar somente palavras Undo Desfazer Undo {action} Desfazer {action} + Undo Actions Desfazer ações + Undo History Histórico de desfazer + Undo 1 Action Desfazer 1 ação + Undo {n} Actions Desfazer {n} ações Ungroup Desagrupar Up Para cima Update Field Atualizar campo diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 159657b..f54937f 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -778,6 +778,10 @@ preview Title Пример Underline Words Only Подчёркивать только слова Undo Отменить Undo {action} Отменить: {action} + Undo Actions Отменить действия + Undo History Журнал отмены + Undo 1 Action Отменить действие + Undo {n} Actions Отменить действий: {n} Up Вверх Update Field Обновить поле Update Index Обновить указатель diff --git a/crates/ui-egui/src/i18n/sr-latn.tsv b/crates/ui-egui/src/i18n/sr-latn.tsv index 49ce84d..3f075e7 100644 --- a/crates/ui-egui/src/i18n/sr-latn.tsv +++ b/crates/ui-egui/src/i18n/sr-latn.tsv @@ -966,6 +966,10 @@ Underline Words Only Podvuci samo reči Undo Opozovi Undo {action} Opozovi {action} + Undo Actions Opozovi radnje + Undo History Istorija opozivanja + Undo 1 Action Opozovi 1 radnju + Undo {n} Actions Opozovi radnje: {n} Ungroup Razgrupisanje Up Gore Update Field Ažuriraj polje diff --git a/crates/ui-egui/src/i18n/sr.tsv b/crates/ui-egui/src/i18n/sr.tsv index e8601d2..4cebbc7 100644 --- a/crates/ui-egui/src/i18n/sr.tsv +++ b/crates/ui-egui/src/i18n/sr.tsv @@ -970,6 +970,10 @@ Underline Words Only Подвуци само речи Undo Опозови Undo {action} Опозови {action} + Undo Actions Опозови радње + Undo History Историја опозивања + Undo 1 Action Опозови 1 радњу + Undo {n} Actions Опозови радње: {n} Ungroup Разгруписање Up Горе Update Field Ажурирај поље diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index 38c62b8..d4b246f 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -784,6 +784,11 @@ preview Title Örnek Underline Words Only Yalnızca Sözcüklerin Altını Çiz Undo Geri Al Undo {action} Geri Al: {action} + Undo Actions Eylemleri Geri Al + Undo History Geri Alma Geçmişi + Undo 1 Action 1 Eylemi Geri Al + Undo {n} Actions {n} Eylemi Geri Al + Unformatted Text Biçimlendirilmemiş Metin Up Yukarı Update Field Alanı Güncelle Update Index Dizini Güncelle @@ -966,7 +971,6 @@ preview Title Örnek Formatted Text Biçimlendirilmiş Metin Rich Text Format (RTF) Zengin Metin Biçimi (RTF) HTML Format HTML Biçimi - Unformatted Text Biçimlendirilmemiş Metin Result Sonuç Drops all formatting and pastes plain text. Tüm biçimlendirmeyi kaldırır ve düz metin yapıştırır. Advanced Find Gelişmiş Bul diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index f347118..1aece16 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -934,6 +934,10 @@ Underline Words Only Підкреслити лише слова Undo Скасувати Undo {action} Скасувати {action} + Undo Actions Скасувати дії + Undo History Журнал скасування + Undo 1 Action Скасувати 1 дію + Undo {n} Actions Скасувати дій: {n} Ungroup Розгрупувати Unformatted Text Неформатований текст Up Угору diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 076a9fb..5128568 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -919,6 +919,10 @@ Underline Words Only 仅给字词加下划线 Undo 撤消 Undo {action} 撤消{action} + Undo Actions 撤消多项操作 + Undo History 撤消历史记录 + Undo 1 Action 撤消 1 项操作 + Undo {n} Actions 撤消 {n} 项操作 Ungroup 取消组合 Up 上 Update Field 更新域 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 0e14726..14fd3e2 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -908,6 +908,10 @@ Underline Words Only 只在文字下加底線 Undo 復原 Undo {action} 復原{action} + Undo Actions 復原多項動作 + Undo History 復原記錄 + Undo 1 Action 復原 1 項動作 + Undo {n} Actions 復原 {n} 項動作 Ungroup 取消群組 Up 上 Update Field 更新功能變數 diff --git a/docs/parity-checklist.md b/docs/parity-checklist.md index db62e14..67451ed 100644 --- a/docs/parity-checklist.md +++ b/docs/parity-checklist.md @@ -2,12 +2,12 @@ Generated by `cargo xtask parity` (`wordcraft-cli parity --markdown`): the word-processor feature catalog (`crates/engine/src/catalog.rs`) compared with the live command registry. -**484 of 499 catalog features have commands (97.0%).** +**485 of 500 catalog features have commands (97.0%).** | Tab | Live | Total | |---|---|---| | File | 22 | 22 | -| Quick Access | 4 | 4 | +| Quick Access | 5 | 5 | | Home | 102 | 103 | | Insert | 47 | 53 | | Draw | 10 | 11 | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 40d81ff..69ef510 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -13,7 +13,7 @@ The generated ribbon checklist is [`parity-checklist.md`](parity-checklist.md). | Number | Value | Kind | |---|---|---| -| Ribbon/menu catalog coverage | **484 / 499 (97.0%)** | **measured**: `cargo xtask parity` (re-derived this pass from `crates/engine/src/catalog.rs` against every `CommandSpec::new` id in the source; identical to the checked-in `parity-checklist.md`) | +| Ribbon/menu catalog coverage | **485 / 500 (97.0%)** | **measured**: `cargo xtask parity` (re-derived this pass from `crates/engine/src/catalog.rs` against every `CommandSpec::new` id in the source; identical to the checked-in `parity-checklist.md`) | | Feature breadth (weighted, beyond the ribbon: dialog options, citation styles, proofing languages, chart types, UI languages) | **~80%** | estimated | | Feature depth (weighted by use, table below) | **~68%** | estimated | | **Ready for real work** (full target) | **~60%** (range 55–63%) | estimated: weighted sum over the dimension table ([By dimension](#by-dimension)) | diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 9b1fc58..074e185 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-12 · **Change:** trivial (start-up opens on File › Home, the Start screen, #766; previously trivial: Navigation headings truncate in the resizable pane, #759; previously trivial: Find and Replace special characters and wildcards, #543; previously trivial: Customize Ribbon and the Quick Access Toolbar, #379; 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-12 · **Change:** trivial (Undo list beside the Quick Access Toolbar's Undo, #701; previously trivial: Navigation headings truncate in the resizable pane, #759; previously trivial: Find and Replace special characters and wildcards, #543; previously trivial: Customize Ribbon and the Quick Access Toolbar, #379; 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 @@ -15,7 +15,7 @@ own (`AGENTS.md`: no Microsoft iconography, ever). | Mini-toolbar on selection | ✅ (#43, two rows #535) | ✅ | 80% | 1–2 | | Mini-toolbar on selection | ✅ (#43) | ✅ | 80% | 1–2 | | 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). Not shown below the ribbon | customizable | 90% | 0.5 | +| 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 | | Keyboard shortcuts | **137** commands bound in `CommandSpec::key`, 156 key combinations (measured on Linux/Windows); styles, spacing, symbols, fields, views, mail merge (#330); field keys Alt+F9, Shift+F9, Ctrl+Shift+F9, Ctrl+F11, Ctrl+Shift+F11, F11, Shift+F11 (#339); 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 | @@ -44,6 +44,7 @@ own (`AGENTS.md`: no Microsoft iconography, ever). | Date | Change | Summary | |---|---|---| +| 2026-10-12 | trivial | Quick Access Toolbar: Undo list to undo several steps at once (#701) | | 2026-10-12 | trivial | Start screen (#766): launched with no file, the app opens on File › Home (Blank document, templates, Recent) as Word does; Esc or the back arrow leaves an empty blank document; File › Options › General › Start-up turns it off, kept between runs; `ui.startScreen` for agents. Not shown with files, `--sample` or `--control` | | 2026-10-12 | trivial | Alt+Q moves to "Tell me" command search, as in Word for Windows (#819) | | 2026-10-12 | trivial | Ribbon groups fit above their labels: View › Show puts Hide White Space (#645) in a second column instead of a fourth row over the "Show" label; Developer › Controls drops the gaps that pushed it into its label; Layout › Paragraph (#654) puts its column gap inside cells (no `add_space` in the grid, a debug assertion) and 1.5 pt between rows so it fits; `ribbon_groups_fit_above_their_labels` checks every tab in every language |