diff --git a/ROADMAP.md b/ROADMAP.md index 4b6788a..5198e85 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -131,6 +131,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm | Date | What landed | |---|---| +| 2026-10-11 | Right-click Wrap Text submenu on pictures, shapes and text boxes: wrapping styles, Move with Text / Fix Position on Page (#513) | | 2026-10-11 | Paragraph dialog options (#512): mirror indents (`w:mirrorIndents`, laid out on even pages), same-style spacing, suppress line numbers, don't hyphenate | | 2026-10-11 | Screen readers can read the document canvas (#488): paragraphs, headings, lists, tables, links, pictures and the caret exposed through AccessKit | | 2026-10-11 | Compare / Combine dialog with comparison settings, Restrict Editing pane (exceptions, SHA-512 password, locked styles; DOCX round trip), AutoCorrect Options dialog saved per user (#412) | diff --git a/crates/engine/src/cmd/objects.rs b/crates/engine/src/cmd/objects.rs index 209a23b..7f0116e 100644 --- a/crates/engine/src/cmd/objects.rs +++ b/crates/engine/src/cmd/objects.rs @@ -265,6 +265,9 @@ pub fn specs() -> Vec { }) .params(r#"{"side": "bothSides|left|right|largest"} (the sides of the selected Square, Tight or Through object text flows on; largest: the wider side, line by line)"#) .when(has_movable), + CommandSpec::new("arrange.moveWithText", "Move with Text", "Layout › Arrange › Wrap Text", |s, v| move_with_text(s, p::bool(v, "value").unwrap_or(true))) + .params(r#"{"value": bool} (true: Move with Text, the floating object's vertical position follows its paragraph; false: Fix Position on Page. It stays where it is)"#) + .when(has_floating), CommandSpec::new("arrange.position", "Position", "Layout › Arrange", |s, v| { let preset = p::str(v, "preset").unwrap_or("middleCenter").to_string(); let (x, y) = (p::f32(v, "x"), p::f32(v, "y")); @@ -766,6 +769,34 @@ fn unalign(s: &mut Session) -> Result<(), CmdError> { Ok(()) } +/// Whether a floating object moves with its text (Word's "Move with Text"): its vertical position +/// follows its paragraph (as layout places any anchor but the page and its margins), rather than +/// being fixed on the page ("Fix Position on Page"). +pub fn moves_with_text(f: &Float) -> bool { + use wordcraft_doc::para::Anchor; + !matches!(f.v_rel, Anchor::Page | Anchor::Margin | Anchor::TopMargin | Anchor::BottomMargin | Anchor::InsideMargin | Anchor::OutsideMargin) +} + +/// Arrange › Wrap Text › Move with Text / Fix Position on Page: anchor the selected floating +/// object's vertical position to its paragraph or to the page, keeping it where it is now. +fn move_with_text(s: &mut Session, with_text: bool) -> CmdResult { + use wordcraft_doc::para::Anchor; + let (pos, obj) = selected(s).ok_or_else(|| CmdError::Disabled("no picture or shape selected".into()))?; + let Some((_, _, float)) = obj.frame() else { return Err(CmdError::Disabled("select a floating picture or shape first".into())) }; + if moves_with_text(float) == with_text { + return Ok(json!({"object": serde_json::to_value(&obj).unwrap_or(Value::Null)})); + } + let hit = s.layout().object(&pos, s.page_hint).ok_or_else(|| CmdError::Failed("the object isn't laid out".into()))?; + // Paragraph offsets start at the paragraph's top (the hit's origin), page offsets at the page's. + let y = if with_text { hit.rect.y - hit.origin.y } else { hit.rect.y }; + let o = edit_float(s, &pos, |f| { + f.v_rel = if with_text { Anchor::Paragraph } else { Anchor::Page }; + f.v_align = None; + f.y = y.clamp(-MAX_OFFSET, MAX_OFFSET); + })?; + Ok(json!({"object": serde_json::to_value(o).unwrap_or(Value::Null)})) +} + /// The top-level body paragraph under `y` on `page`: the last one starting at or above it (else /// the first starting on the page). pub(crate) fn anchor_paragraph(layout: &wordcraft_layout::DocLayout, page: usize, y: f32) -> Option { @@ -1409,6 +1440,35 @@ mod tests { assert!(s.run("picture.crop", &json!({"left": 0.1})).is_err()); } + /// #513: Wrap Text › Fix Position on Page / Move with Text re-anchor a floating picture + /// vertically without moving it; one undo step each; an inline picture can't. + #[test] + fn move_with_text_and_fix_position_keep_the_picture_in_place() { + use wordcraft_doc::para::Anchor; + let mut s = Session::new(wordcraft_doc::Document::new()); + s.run("text.insert", &json!({"text": "First paragraph\nSecond"})).unwrap(); + let data = super::super::insert::base64_encode(&png(20, 10)); + s.run("insert.picture", &json!({"data": data, "width": 100})).unwrap(); + assert!(s.run("arrange.moveWithText", &json!({"value": false})).is_err(), "inline: no anchoring"); + s.run("arrange.wrap", &json!({"wrap": "square"})).unwrap(); + let v_rel = |s: &Session| selected(s).unwrap().1.frame().unwrap().2.v_rel; + let with_text = |s: &Session| moves_with_text(selected(s).unwrap().1.frame().unwrap().2); + let rect = |s: &mut Session| { + let pos = selected(s).unwrap().0; + s.layout().object(&pos, 0).unwrap().rect + }; + let before = rect(&mut s); + s.run("arrange.moveWithText", &json!({"value": false})).unwrap(); + assert_eq!(v_rel(&s), Anchor::Page); + assert!(!with_text(&s)); + assert!((rect(&mut s).y - before.y).abs() < 0.01, "it stays put"); + s.run("arrange.moveWithText", &json!({"value": true})).unwrap(); + assert!(with_text(&s)); + assert!((rect(&mut s).y - before.y).abs() < 0.01, "it stays put"); + s.run("edit.undo", &json!({})).unwrap(); + assert_eq!(v_rel(&s), Anchor::Page); + } + /// #332: Rotate and Flip turn the frame (an angle and mirroring), never the pixels; an exact /// angle is normalised; each is one undo step; the rotated frame takes its rotated room. #[test] diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 6240239..f8d4053 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -728,18 +728,23 @@ pub fn show(app: &mut WordApp, ui: &mut Ui) { None => {} } } + // Right-click on a picture, shape or text box selects it (its menu offers Wrap Text). if resp.secondary_clicked() && app.session.math.is_none() && let Some(p) = resp.interact_pointer_pos() && let Some((page, x, y)) = page_at(&rects, &layout, geo.scale, p) - && let Some(pos) = layout.hit(page, x, y, app.session.sel.focus.story) { - let (a, b) = app.session.sel.ordered(); - if !(a <= pos && pos <= b) || app.session.sel.is_collapsed() { - app.session.sel = wordcraft_engine::Selection::caret(pos); + if crate::objects::select_at(app, &layout, &rects, geo.scale, p) { + app.canvas.context_issue = None; + app.canvas.context_synonyms = None; + } else if let Some(pos) = layout.hit(page, x, y, app.session.sel.focus.story) { + let (a, b) = app.session.sel.ordered(); + if !(a <= pos && pos <= b) || app.session.sel.is_collapsed() { + app.session.sel = wordcraft_engine::Selection::caret(pos); + } + app.canvas.context_issue = app.session.run("review.suggestions", &json!({})).ok().filter(|v| !v.is_null()); + app.canvas.context_synonyms = app.session.run("review.thesaurus", &json!({})).ok().and_then(|v| v.get("synonyms").cloned()); } - app.canvas.context_issue = app.session.run("review.suggestions", &json!({})).ok().filter(|v| !v.is_null()); - app.canvas.context_synonyms = app.session.run("review.thesaurus", &json!({})).ok().and_then(|v| v.get("synonyms").cloned()); } resp.context_menu(|ui| context_menu(app, ui)); // Stand down while the context menu owns the pointer so the two never double up; @@ -1665,16 +1670,24 @@ fn context_menu(app: &mut WordApp, ui: &mut Ui) { item(ui, app, tl!("Edit Field…"), "fields.setCode", json!({})); item(ui, app, tl!("Toggle Field Codes"), "fields.toggleCode", json!({})); } - let object = wordcraft_engine::cmd::objects::selected(&app.session).is_some(); - if object || app.session.sel.focus.path.cell().is_some() { + let selected_object = wordcraft_engine::cmd::objects::selected(&app.session).is_some(); + if selected_object || app.session.sel.focus.path.cell().is_some() { item(ui, app, "View Alt Text…", "view.altTextPane", json!({"value": true})); } - if crate::ribbon::has_picture_selected(&app.session) { + // A selected picture, shape, text box or chart: its own items, then Wrap Text (#513). + let picture = crate::ribbon::has_picture_selected(&app.session); + let object = wordcraft_engine::cmd::objects::object_selection(&app.session).is_some(); + if picture || object { ui.separator(); + } + if picture { item(ui, app, "Change Picture…", "ui.changePicture", json!({})); item(ui, app, "Reset Picture", "picture.reset", json!({})); item(ui, app, "Size and Crop…", "ui.tab", json!({"tab": "Picture Format"})); } + if object { + ui.menu_button(tl!("Wrap Text"), |ui| crate::ribbon::wrap_text_items(ui, app)); + } if app.session.sel.focus.path.cell().is_some() { ui.separator(); ui.menu_button(tl!("Insert"), |ui| { @@ -1821,6 +1834,53 @@ mod tests { assert_eq!(h.state().session.doc.plain_text(StoryRef::Part(part)), "Original"); } + /// #513: right-clicking a picture selects it and offers Wrap Text, as in Word; choosing + /// Square there wraps text around it. + #[test] + fn right_click_on_a_picture_offers_wrap_text() { + use egui_kittest::kittest::Queryable; + let mut a = WordApp::new(wordcraft_engine::Session::new(wordcraft_doc::Document::new()), Default::default()); + a.run("text.insert", json!({"text": "Text before the picture "})).unwrap(); + let mut png = Vec::new(); + let img = image::RgbaImage::from_pixel(40, 20, image::Rgba([200, 30, 30, 255])); + image::DynamicImage::ImageRgba8(img).write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).unwrap(); + a.run("insert.picture", json!({"data": wordcraft_engine::cmd::insert::base64_encode(&png), "width": 160})).unwrap(); + let pos = wordcraft_engine::cmd::objects::selected(&a.session).unwrap().0; + a.session.sel = wordcraft_engine::Selection::caret(Pos::body(0, 0)); + let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(1440.0, 900.0)).build_ui_state( + |ui, app: &mut WordApp| { + let ctx = ui.ctx().clone(); + app.logic(&ctx); + app.ui(ui); + }, + a, + ); + for _ in 0..6 { + h.step(); + } + let rect = h.state_mut().session.layout().object(&pos, 0).expect("the picture is laid out").rect; + let at = crate::frame::to_screen(h.state().canvas.page_rects[0], h.state().canvas.scale, rect).center(); + h.event(egui::Event::PointerMoved(at)); + for pressed in [true, false] { + h.event(egui::Event::PointerButton { pos: at, button: egui::PointerButton::Secondary, pressed, modifiers: Default::default() }); + h.step(); + } + h.step(); + let picked = wordcraft_engine::cmd::objects::object_selection(&h.state().session).map(|(p, _)| p); + assert_eq!(picked, Some(pos.clone()), "the right-click selected the picture"); + h.get_by_label_contains("Wrap Text").hover(); + for _ in 0..3 { + h.step(); + } + assert!(h.query_by_label_contains("Fix Position on Page").is_some(), "the anchoring choices are offered"); + h.get_by_label_contains("Square").click(); + for _ in 0..3 { + h.step(); + } + let o = wordcraft_engine::cmd::objects::selected(&h.state().session).unwrap().1; + assert_eq!(o.frame().unwrap().2.wrap, wordcraft_doc::para::Wrap::Square); + } + #[test] fn only_web_and_email_links_are_followed() { for ok in ["https://example.com/a?b#c", "http://example.com", "HTTPS://EXAMPLE.COM", "mailto:someone@example.com"] { diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 2fca072..2d0ad40 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -1734,6 +1734,8 @@ Rewind Rebobinar Forward Avanzar Stroke {n} of {total} Trazo {n} de {total} + Move with Text Mover con el texto + Fix Position on Page Fijar posición en la página # WordArt and connectors (#416). WordArt Styles Estilos de WordArt diff --git a/crates/ui-egui/src/i18n/et.tsv b/crates/ui-egui/src/i18n/et.tsv index 73a1494..c110719 100644 --- a/crates/ui-egui/src/i18n/et.tsv +++ b/crates/ui-egui/src/i18n/et.tsv @@ -1697,6 +1697,8 @@ Rewind Keri algusesse Forward Edasi Stroke {n} of {total} Joon {n}/{total} + Move with Text Liigu tekstiga + Fix Position on Page Fikseeri asukoht lehel # WordArt and connectors (#416). WordArt Styles WordArti laadid diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 06b1f68..ed60338 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -1718,6 +1718,8 @@ Rewind 巻き戻し Forward 早送り Stroke {n} of {total} ストローク {n}/{total} + Move with Text 文字列と一緒に移動する + Fix Position on Page ページ上で位置を固定する # WordArt and connectors (#416). WordArt Styles ワードアートのスタイル diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 179df1c..edee558 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -1730,6 +1730,8 @@ Rewind Retroceder Forward Avançar Stroke {n} of {total} Traço {n} de {total} + Move with Text Mover com o texto + Fix Position on Page Fixar posição na página # WordArt and connectors (#416). WordArt Styles Estilos de WordArt diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index b027924..41c4950 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -2100,6 +2100,9 @@ preview Title Пример Apply Применить Convert Notes Преобразовать сноски +# Wrap Text context menu (#513). + Move with Text Перемещать вместе с текстом + Fix Position on Page Зафиксировать положение на странице # Paragraph dialog options (#512). Formatting exceptions Исключения форматирования Inside: Внутри: diff --git a/crates/ui-egui/src/i18n/sr-latn.tsv b/crates/ui-egui/src/i18n/sr-latn.tsv index 664637d..4f819b3 100644 --- a/crates/ui-egui/src/i18n/sr-latn.tsv +++ b/crates/ui-egui/src/i18n/sr-latn.tsv @@ -1728,6 +1728,8 @@ Rewind Na početak Forward Napred Stroke {n} of {total} Potez {n} od {total} + Move with Text Pomeraj sa tekstom + Fix Position on Page Fiksiraj položaj na stranici # WordArt and connectors (#416). WordArt Styles WordArt stilovi diff --git a/crates/ui-egui/src/i18n/sr.tsv b/crates/ui-egui/src/i18n/sr.tsv index ffe850f..eef5b20 100644 --- a/crates/ui-egui/src/i18n/sr.tsv +++ b/crates/ui-egui/src/i18n/sr.tsv @@ -1732,6 +1732,8 @@ Rewind На почетак Forward Напред Stroke {n} of {total} Потез {n} од {total} + Move with Text Померај са текстом + Fix Position on Page Фиксирај положај на страници # WordArt and connectors (#416). WordArt Styles WordArt стилови diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index f8e0aae..42d7bc0 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -1722,6 +1722,9 @@ Apply Uygula Convert Notes Notları Dönüştür +# Wrap Text context menu (#513). + Move with Text Metinle birlikte taşı + Fix Position on Page Sayfadaki konumu sabitle # Paragraph dialog options (#512). Formatting exceptions Biçimlendirme özel durumları Inside: İç: diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 628a6d8..9405c5e 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -1730,6 +1730,8 @@ Rewind На початок Forward Вперед Stroke {n} of {total} Штрих {n} з {total} + Move with Text Переміщувати з текстом + Fix Position on Page Зафіксувати положення на сторінці # WordArt and connectors (#416). WordArt Styles Стилі WordArt diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 755b108..8cf870c 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -1730,6 +1730,8 @@ Rewind 倒回 Forward 前进 Stroke {n} of {total} 第 {n} 笔,共 {total} 笔 + Move with Text 随文字移动 + Fix Position on Page 在页面上的位置固定 # WordArt and connectors (#416). WordArt Styles 艺术字样式 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index fdcea9f..a7c94b0 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -1719,6 +1719,8 @@ Rewind 倒轉 Forward 快轉 Stroke {n} of {total} 第 {n} 筆,共 {total} 筆 + Move with Text 隨文字移動 + Fix Position on Page 固定於頁面上的位置 # WordArt and connectors (#416). WordArt Styles 文字藝術師樣式 diff --git a/crates/ui-egui/src/objects.rs b/crates/ui-egui/src/objects.rs index 046b521..14c8111 100644 --- a/crates/ui-egui/src/objects.rs +++ b/crates/ui-egui/src/objects.rs @@ -205,6 +205,22 @@ fn link_click(app: &mut WordApp, layout: &DocLayout, pages: &[Rect], scale: f32, } } +/// Right-click at `at`: the picture, shape or text box under it becomes the selection (as a click +/// would, without a drag), so the context menu acts on it. Returns whether there was one. +pub fn select_at(app: &mut WordApp, layout: &DocLayout, pages: &[Rect], scale: f32, at: Pos2) -> bool { + if crate::canvas::editing_header_footer(app, layout) { + return false; + } + let Some((object, _)) = grab_at(app, layout, pages, scale, at) else { return false }; + let pos = object.pos(); + // Already selected (alone or among several picked for Group): keep the selection. + if !wordcraft_engine::cmd::objects::picked_objects(&app.session).contains(&pos) { + let end = Pos { off: pos.off + OBJ.len_utf8(), ..pos.clone() }; + let _ = app.run("select.range", json!({"anchor": pos, "focus": end})); + } + true +} + /// What a press at `at` would grab: a handle of the shown frame, else an object. fn grab_at(app: &WordApp, layout: &DocLayout, pages: &[Rect], scale: f32, at: Pos2) -> Option<(ObjectHit, Grab)> { let handle = active(app, layout).filter(|(o, _)| o.story == StoryRef::Body && !fixed(app, o)).and_then(|(o, _)| { diff --git a/crates/ui-egui/src/ribbon.rs b/crates/ui-egui/src/ribbon.rs index 03e5e5a..9134d52 100644 --- a/crates/ui-egui/src/ribbon.rs +++ b/crates/ui-egui/src/ribbon.rs @@ -327,6 +327,37 @@ fn mi_check(ui: &mut Ui, app: &mut WordApp, label: &str, checked: bool, id: &str } } +/// Word's text wrapping styles in menu order: label, model value, `arrange.wrap` parameter. +pub(crate) const WRAP_STYLES: [(&str, wordcraft_doc::para::Wrap, &str); 7] = { + use wordcraft_doc::para::Wrap; + [ + ("In Line with Text", Wrap::Inline, "inline"), + ("Square", Wrap::Square, "square"), + ("Tight", Wrap::Tight, "tight"), + ("Through", Wrap::Through, "through"), + ("Top and Bottom", Wrap::TopAndBottom, "topAndBottom"), + ("Behind Text", Wrap::BehindText, "behindText"), + ("In Front of Text", Wrap::InFrontOfText, "inFrontOfText"), + ] +}; + +/// The Wrap Text menu for the selected picture, shape or text box: the seven styles (the +/// current one checked), then Move with Text / Fix Position on Page for a floating one. The +/// canvas context menu shows it; the ribbon's Wrap Text button can share it. +pub(crate) fn wrap_text_items(ui: &mut Ui, app: &mut WordApp) { + use wordcraft_engine::cmd::objects::{moves_with_text, selected}; + let state = selected(&app.session).and_then(|(_, o)| o.frame().map(|(_, _, f)| (f.wrap, moves_with_text(f)))); + let wrap = state.map(|(w, _)| w); + for (label, w, v) in WRAP_STYLES { + mi_check(ui, app, label, wrap == Some(w), "arrange.wrap", json!({"wrap": v})); + } + ui.separator(); + // An inline object is neither: it's part of the text. + let with_text = state.filter(|(w, _)| *w != wordcraft_doc::para::Wrap::Inline).map(|(_, m)| m); + mi_check(ui, app, "Move with Text", with_text == Some(true), "arrange.moveWithText", json!({"value": true})); + mi_check(ui, app, "Fix Position on Page", with_text == Some(false), "arrange.moveWithText", json!({"value": false})); +} + fn home(app: &mut WordApp, ui: &mut Ui) { let st = app.session.run("format.state", &json!({})).unwrap_or_default(); let flag = |k: &str| st.get(k).and_then(Value::as_bool).unwrap_or(false); diff --git a/docs/ui-parity.md b/docs/ui-parity.md index b974304..664bfab 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -17,8 +17,8 @@ own (`AGENTS.md`: no Microsoft iconography, ever). | 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 | **125** commands bound in `CommandSpec::key`, 144 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). Missing: en dash on the numeric keypad | ~250 default shortcuts | ~70% | 2–4 | | Modal dialogs | **52** (`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, 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 | +| Context menus (right-click) | text, spelling suggestions, fields (Update Field, Edit Field…, Toggle Field Codes, #339); right-click selects a picture, shape or text box and offers Wrap Text (styles, Move with Text / Fix Position on Page, #513); tables partial | rich per context | 60% | 3–5 | | Task panes (Navigation, Styles, Comments, Reviewing, Format Picture/Shape, Thesaurus, Accessibility, Clipboard, Selection) | Navigation, Styles, Comments, Thesaurus, Restrict Editing, Accessibility (grouped findings, jump-to, fixes) and Alt Text (#386), Read Aloud, Zotero; Style Inspector (#236); Clipboard pane (#264) open | all | 70% | 4–6 | -| Context menus (right-click) | text, spelling suggestions, fields (Update Field, Edit Field…, Toggle Field Codes, #339); objects and tables partial | rich per context | 55% | 3–5 | | Backstage (Home, New, Open, Info, Save As, Print, Share, Export, Options) | Home and New separate (#254), Info properties kept (#268) | | 68% | 4–6 | | Rulers: indents, tabs, margins, table columns | indents draggable (one undo, #62); tabs; table column drag pending (#49) | ✅ | 70% | 2–3 | | On-canvas objects: select, drag, resize handles, nudge | ✅ tested (#228) | ✅ | 75% | — | @@ -37,6 +37,7 @@ own (`AGENTS.md`: no Microsoft iconography, ever). | Date | Change | Summary | |---|---|---| +| 2026-10-11 | trivial | Right-click on a picture, shape or text box selects it and offers Wrap Text: the seven wrapping styles (current one checked), Move with Text and Fix Position on Page (`arrange.moveWithText`) (#513) | | 2026-10-11 | trivial | Paragraph dialog: Mirror indents (Inside/Outside labels), Don't add space between paragraphs of the same style, Formatting exceptions (Suppress line numbers, Don't hyphenate) (#512) | | 2026-10-11 | trivial | Screen readers: the document canvas exposes paragraphs, headings, lists, tables, links, pictures and the caret through AccessKit (#488); 25% → 50% | | 2026-10-11 | trivial | Compare / Combine Documents (original and revised document, label changes with, comparison settings, word or character level, show changes in) and AutoCorrect Options (replacement list, capitalization, exceptions, AutoFormat As You Type, Math AutoCorrect) dialogs; Restrict Editing pane (formatting and editing restrictions, exceptions for everyone, password); modal dialog rows merged into one (#412) |