Undo history list beside the Quick Access Toolbar's Undo (#701) (#855)

* #701: Undo history list beside the Quick Access Toolbar's Undo

A drop-down arrow next to Undo lists recent steps, newest first; hovering a
row highlights it and every newer step, the footer reads "Undo N Actions"
(Cancel when none is highlighted), and a click undoes them all at once.

Engine: Session::undo_history (newest first, capped at UNDO_LIST_MAX = 100)
and Session::undo_steps; edit.undoTo {"count": n} (0, negative and non-number
counts rejected, counts past the depth clamped, stops at protection) and the
read-only edit.undoHistory for CLI and MCP. Catalog: Undo List.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* i18n: Hungarian and French strings for this change

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* #701: Bulgarian for the Undo history strings; drop a duplicate Turkish entry

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-10-12 10:05:06 -04:00
1 parent 5383d80f75
commit 55bfc33418
28 files changed
+335 -10

No files matched your search

+4 -2
View File
@@ -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 |
+1
View File
@@ -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
+28
View File
@@ -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<CommandSpec> {
})
.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()));
+3 -1
View File
@@ -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;
+19 -1
View File
@@ -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<String> {
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();
+62
View File
@@ -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();
+139
View File
@@ -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<String> {
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<Rect> = (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<egui::Event>, 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<String> = 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.
+4
View File
@@ -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 се отваря на Файл › Начало с празен документ, шаблоните и последните документи. Изкл.: отваря се празен документ.
Can't render this file because it contains an unexpected character in line 7 and column 25.
+4
View File
@@ -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.
Can't render this file because it contains an unexpected character in line 6 and column 25.
+4
View File
@@ -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
Can't render this file because it contains an unexpected character in line 6 and column 25.
+4
View File
@@ -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
Can't render this file because it contains an unexpected character in line 6 and column 25.
+4
View File
@@ -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
Can't render this file because it contains an unexpected character in line 458 and column 13.
+4
View File
@@ -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.
Can't render this file because it contains an unexpected character in line 7 and column 25.
+4
View File
@@ -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.
Can't render this file because it contains an unexpected character in line 6 and column 25.
+4
View File
@@ -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
Can't render this file because it contains an unexpected character in line 6 and column 25.
+4
View File
@@ -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 フィールドの更新
Can't render this file because it contains an unexpected character in line 560 and column 13.
+4
View File
@@ -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.
Can't render this file because it contains an unexpected character in line 6 and column 25.
+4
View File
@@ -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
Can't render this file because it contains an unexpected character in line 6 and column 25.
+4
View File
@@ -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 Обновить указатель
Can't render this file because it contains an unexpected character in line 7 and column 25.
+4
View File
@@ -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
Can't render this file because it contains an unexpected character in line 6 and column 25.
+4
View File
@@ -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 Ажурирај поље
Can't render this file because it contains an unexpected character in line 11 and column 25.
+5 -1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 438 and column 13.
+4
View File
@@ -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 Угору
Can't render this file because it contains an unexpected character in line 6 and column 31.
+4
View File
@@ -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 更新域
Can't render this file because it contains an unexpected character in line 7 and column 25.
+4
View File
@@ -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 更新功能變數
Can't render this file because it contains an unexpected character in line 561 and column 13.
+2 -2
View File
@@ -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 |
+1 -1
View File
@@ -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)) |
+3 -2
View File
@@ -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 |