Right-clicking a picture, shape, text box or chart selects it (as a click would) and its context menu offers Wrap Text: In Line with Text, Square, Tight, Through, Top and Bottom, Behind Text, In Front of Text (current one checked), then Move with Text / Fix Position on Page. The latter two are a new engine command, arrange.moveWithText, which re-anchors a floating object's vertical position to its paragraph or the page without moving it. The menu items live in one shared function (ribbon::wrap_text_items, WRAP_STYLES) for the ribbon to reuse. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
parent
b0431eadc2
commit
88a45329b2
|
|
@ -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) |
|
||||
|
|
|
|||
|
|
@ -265,6 +265,9 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
})
|
||||
.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<wordcraft_doc::Path> {
|
||||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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"] {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 25.
|
|
|
@ -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
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 12.
|
|
|
@ -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 ワードアートのスタイル
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 3.
|
|
|
@ -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
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 25.
|
|
|
@ -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: Внутри:
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 7 and column 25.
|
|
|
@ -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
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 25.
|
|
|
@ -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 стилови
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 11 and column 25.
|
|
|
@ -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: İç:
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 18.
|
|
|
@ -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
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 31.
|
|
|
@ -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 艺术字样式
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 7 and column 25.
|
|
|
@ -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 文字藝術師樣式
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 4.
|
|
|
@ -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, _)| {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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) |
|
||||
|
|
|
|||
Loading…
Reference in New Issue