mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-13 23:17:49 +00:00
* #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:
28 files changed
+335
-10
No files matched your search
+4
-2
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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,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 |
|
||||
|
||||
@@ -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
@@ -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 |
|
||||
|
||||
Reference in new issue
Block a user