diff --git a/ROADMAP.md b/ROADMAP.md index 8ff8fbb9a..4b6788ad6 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 | 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) | | 2026-10-11 | WordArt (#416): text fill/outline/shadow/glow/reflection, Transform warps, our own styles; straight/elbow/curved connectors glued to shapes that re-route; DOCX round trip | @@ -171,6 +172,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm | Date | Change | Summary | |---|---|---| +| 2026-10-11 | trivial | Progress log: Paragraph dialog options, mirror indents (#512) | | 2026-10-11 | trivial | Progress log: content controls (#381) | | 2026-10-11 | trivial | Progress log: Insert › Chart (#409); catalog 408/439 (92.9%) | | 2026-10-11 | trivial | Progress log: Line Numbers, Hyphenation, Language, Envelopes and Labels dialogs (#407) | diff --git a/crates/doc/src/props.rs b/crates/doc/src/props.rs index 510f5e04f..d1f9d2cf7 100644 --- a/crates/doc/src/props.rs +++ b/crates/doc/src/props.rs @@ -664,6 +664,10 @@ pub struct ParaProps { /// Ignore space before/after between paragraphs of the same style. #[serde(skip_serializing_if = "Option::is_none")] pub contextual_spacing: Option, + /// `w:mirrorIndents` (ECMA-376 §17.3.1.18): the left and right indents are the inside and + /// outside ones, so they swap sides on even (left-hand) pages. + #[serde(skip_serializing_if = "Option::is_none")] + pub mirror_indents: Option, #[serde(skip_serializing_if = "Option::is_none")] pub keep_next: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -728,7 +732,7 @@ pub struct ParaProps { impl ParaProps { pub fn overlay(&mut self, patch: &ParaProps) { overlay_fields!(self, patch; style, align, indent_left, indent_right, indent_first, space_before, space_after, line_spacing, - contextual_spacing, keep_next, keep_lines, page_break_before, widow_control, outline_level, numbering, tabs, shading, borders, + contextual_spacing, mirror_indents, keep_next, keep_lines, page_break_before, widow_control, outline_level, numbering, tabs, shading, borders, suppress_hyphens, suppress_line_numbers, bidi, drop_cap, kinsoku, word_wrap, overflow_punct, top_line_punct, auto_space_de, auto_space_dn, snap_to_grid); } diff --git a/crates/doc/src/resolve.rs b/crates/doc/src/resolve.rs index 619106676..10dbebbed 100644 --- a/crates/doc/src/resolve.rs +++ b/crates/doc/src/resolve.rs @@ -140,6 +140,8 @@ pub struct ResolvedPara { pub space_after: f32, pub line_spacing: LineSpacing, pub contextual_spacing: bool, + /// Left/right indents are inside/outside: they swap on even pages. + pub mirror_indents: bool, pub keep_next: bool, pub keep_lines: bool, pub page_break_before: bool, @@ -152,6 +154,7 @@ pub struct ResolvedPara { pub bidi: bool, pub drop_cap: u8, pub suppress_hyphens: bool, + pub suppress_line_numbers: bool, /// Asian typography (see `ParaProps`): East Asian line-breaking rules. pub kinsoku: bool, /// Latin words wrap whole (`false`: they may break at any character). @@ -195,6 +198,7 @@ impl ResolvedPara { LineSpacing::Exactly(v) => LineSpacing::Exactly(if v.is_finite() { v.clamp(0.7, 1584.0) } else { 12.0 }), }, contextual_spacing: p.contextual_spacing.unwrap_or(false), + mirror_indents: p.mirror_indents.unwrap_or(false), keep_next: p.keep_next.unwrap_or(false), keep_lines: p.keep_lines.unwrap_or(false), page_break_before: p.page_break_before.unwrap_or(false), @@ -207,6 +211,7 @@ impl ResolvedPara { bidi: p.bidi.unwrap_or(false), drop_cap: p.drop_cap.unwrap_or(0).min(10), suppress_hyphens: p.suppress_hyphens.unwrap_or(false), + suppress_line_numbers: p.suppress_line_numbers.unwrap_or(false), kinsoku: p.kinsoku.unwrap_or(true), word_wrap: p.word_wrap.unwrap_or(true), overflow_punct: p.overflow_punct.unwrap_or(true), diff --git a/crates/docx/src/read/props.rs b/crates/docx/src/read/props.rs index 6e73840a0..5b67dd1e4 100644 --- a/crates/docx/src/read/props.rs +++ b/crates/docx/src/read/props.rs @@ -155,6 +155,7 @@ impl PropCtx { "w:pageBreakBefore" => p.page_break_before = Some(on_off(k)), "w:widowControl" => p.widow_control = Some(on_off(k)), "w:contextualSpacing" => p.contextual_spacing = Some(on_off(k)), + "w:mirrorIndents" => p.mirror_indents = Some(on_off(k)), "w:suppressAutoHyphens" => p.suppress_hyphens = Some(on_off(k)), "w:suppressLineNumbers" => p.suppress_line_numbers = Some(on_off(k)), "w:bidi" => p.bidi = Some(on_off(k)), diff --git a/crates/docx/src/write/props.rs b/crates/docx/src/write/props.rs index c2a22617a..26636ab4b 100644 --- a/crates/docx/src/write/props.rs +++ b/crates/docx/src/write/props.rs @@ -306,6 +306,7 @@ pub fn ppr_inner(w: &mut W, p: &ParaProps, framed: bool, num_change: Option<&Cha w.empty("w:ind", &a); } toggle(w, "w:contextualSpacing", p.contextual_spacing); + toggle(w, "w:mirrorIndents", p.mirror_indents); if let Some(a) = p.align { w.val("w:jc", align_val(a)); } diff --git a/crates/docx/tests/roundtrip.rs b/crates/docx/tests/roundtrip.rs index 701f8bcb8..5da8a57e9 100644 --- a/crates/docx/tests/roundtrip.rs +++ b/crates/docx/tests/roundtrip.rs @@ -140,6 +140,7 @@ fn every_para_prop_round_trips() { space_after: Some(12.0), line_spacing: Some(LineSpacing::Multiple(1.5)), contextual_spacing: Some(true), + mirror_indents: Some(true), keep_next: Some(true), keep_lines: Some(false), page_break_before: Some(true), diff --git a/crates/layout/src/lib.rs b/crates/layout/src/lib.rs index 3642a385e..8aa66c43c 100644 --- a/crates/layout/src/lib.rs +++ b/crates/layout/src/lib.rs @@ -2449,7 +2449,9 @@ fn place_para(ctx: &mut Ctx, pb: &mut PageBuilder, p: &Paragraph, block: usize, // Relative, so a bottom moved down for notes continuing on the next page stays put. pb.bottom -= pb.notes_h() - before; } - let x = pb.col_x(); + // Mirror indents: on an even (left-hand) page the inside indent is on the right, so the + // paragraph's lines (broken for the same width) move over by the indents' difference. + let x = pb.col_x() + mirror_shift(&pl.rp, pb.number, pb.web); let y = pb.y + lead; // Notes that no longer fit below the text (a long one, or the rest of one continued from // the page before) get what's left under it and continue on the next page. @@ -2472,7 +2474,7 @@ fn place_para(ctx: &mut Ctx, pb: &mut PageBuilder, p: &Paragraph, block: usize, let (behind, front) = place_objects(ctx, StoryRef::Body, &[block as u32], p, &pl, (l0, l1), (x, y), &at, &float_rects, 0); items.extend(front); // Line numbers in the left margin. - if let Some(ln) = pb.sect.line_numbers.clone().filter(|_| !pb.web && pl.rp.style != "Header" && p.props.suppress_line_numbers != Some(true)) + if let Some(ln) = pb.sect.line_numbers.clone().filter(|_| !pb.web && pl.rp.style != "Header" && !pl.rp.suppress_line_numbers) && let Some(first) = pl.lines.get(l0) { for li in l0..l1 { @@ -2527,6 +2529,16 @@ fn place_para(ctx: &mut Ctx, pb: &mut PageBuilder, p: &Paragraph, block: usize, pb.prev = Some((pl.rp.style.clone(), pl.rp.contextual_spacing, pl.rp.space_after)); } +/// How far a paragraph with mirrored indents (`w:mirrorIndents`) moves right on page `number`: +/// its left and right indents are the inside and outside ones, which swap sides on even pages. +fn mirror_shift(rp: &wordcraft_doc::resolve::ResolvedPara, number: u32, web: bool) -> f32 { + if !rp.mirror_indents || web || !number.is_multiple_of(2) { + return 0.0; + } + let (left, right) = rp.visual_indents(); + right - left +} + /// Height of the first line of the block after `block` (for keep-with-next). fn next_first_line(ctx: &mut Ctx, block: usize, width: f32) -> f32 { match ctx.doc.body.get(block + 1).map(|b| &**b) { diff --git a/crates/layout/src/tests.rs b/crates/layout/src/tests.rs index a4b61b3e7..d38e3574b 100644 --- a/crates/layout/src/tests.rs +++ b/crates/layout/src/tests.rs @@ -3397,6 +3397,26 @@ fn rotated_canvas_draws_its_clipped_members_inside_the_turn() { } } +#[test] +fn mirror_indents_swap_sides_on_even_pages() { + // #512: with mirrored indents the left indent is the inside one, on the right of an even page. + let mut d = Document::from_text("odd page\neven page"); + let last = Pos::body(1, 0); + d.format_paragraphs(&Pos::body(0, 0), &last, &|p| { + p.indent_left = Some(72.0); + p.mirror_indents = Some(true); + }) + .unwrap(); + d.format_paragraphs(&last, &last, &|p| p.page_break_before = Some(true)).unwrap(); + let l = lay(&d); + assert_eq!(l.pages.len(), 2); + let odd = l.caret(&Pos::body(0, 0)).unwrap(); + let even = l.caret(&last).unwrap(); + assert_eq!(even.page, 1); + assert!((odd.x - 144.0).abs() < 0.5, "odd page: inside indent on the left, {odd:?}"); + assert!((even.x - 72.0).abs() < 0.5, "even page: the left side has the outside indent (0), {even:?}"); +} + /// Ten short paragraphs in a two-column section (`widths`: unequal columns), then one paragraph /// in a one-column section starting `start`. fn two_column_section(start: wordcraft_doc::section::SectionStart, widths: Vec<(f32, f32)>) -> Document { diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 503b14043..6a5c067d8 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -63,6 +63,12 @@ pub enum Dialog { /// `asian` when the dialog opened: OK sets only the flags that changed. #[serde(skip)] asian_was: [bool; 6], + /// Mirror indents, don't add space between paragraphs of the same style, suppress line + /// numbers, don't hyphenate (#512). + opts: [bool; 4], + /// `opts` when the dialog opened: OK sets only the ones that changed. + #[serde(skip)] + opts_was: [bool; 4], }, /// Tabs (#320). Tabs { @@ -834,6 +840,8 @@ impl Dialog { tab: if name == "asianTypography" { 2 } else { 0 }, asian: [rp.kinsoku, rp.word_wrap, rp.overflow_punct, rp.top_line_punct, rp.auto_space_de, rp.auto_space_dn], asian_was: [rp.kinsoku, rp.word_wrap, rp.overflow_punct, rp.top_line_punct, rp.auto_space_de, rp.auto_space_dn], + opts: [rp.mirror_indents, rp.contextual_spacing, rp.suppress_line_numbers, rp.suppress_hyphens], + opts_was: [rp.mirror_indents, rp.contextual_spacing, rp.suppress_line_numbers, rp.suppress_hyphens], } } "tabs" => Dialog::Tabs { form: Box::new(crate::dialogs_para::TabsForm::read(app)?) }, @@ -1316,6 +1324,8 @@ fn body(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool { tab, asian, asian_was, + opts, + opts_was, } => { ui.horizontal(|ui| { for (k, name) in ["Indents and Spacing", "Line and Page Breaks", "Asian Typography"].into_iter().enumerate() { @@ -1344,16 +1354,19 @@ fn body(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool { }); }); ui.label(egui::RichText::new(tl!("Indentation")).font(semibold(12.5))); + // Mirrored indents are the inside and outside ones (they swap on even pages). + let (l_label, r_label) = if opts[0] { ("Inside:", "Outside:") } else { ("Left:", "Right:") }; egui::Grid::new("ind").num_columns(4).show(ui, |ui| { - ui.label(tl!("Left:")); + ui.label(tl!(l_label)); ui.add(egui::DragValue::new(left).speed(0.05).suffix("\"").max_decimals(2)); - ui.label(tl!("Right:")); + ui.label(tl!(r_label)); ui.add(egui::DragValue::new(right).speed(0.05).suffix("\"").max_decimals(2)); ui.end_row(); ui.label(tl!("First line:")); ui.add(egui::DragValue::new(first).speed(0.05).suffix("\"").max_decimals(2)); ui.end_row(); }); + ui.checkbox(&mut opts[0], tl!("Mirror indents")); ui.label(egui::RichText::new(tl!("Spacing")).font(semibold(12.5))); egui::Grid::new("sp").num_columns(4).show(ui, |ui| { ui.label(tl!("Before:")); @@ -1365,6 +1378,7 @@ fn body(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool { ui.add(egui::DragValue::new(after).speed(1.0).range(0.0..=1584.0).suffix(format!(" {}", tl!("pt")))); ui.end_row(); }); + ui.checkbox(&mut opts[1], tl!("Don't add space between paragraphs of the same style")); } 1 => { ui.label(egui::RichText::new(tl!("Line and Page Breaks")).font(semibold(12.5))); @@ -1372,6 +1386,9 @@ fn body(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool { ui.checkbox(keep_next, tl!("Keep with next")); ui.checkbox(keep_lines, tl!("Keep lines together")); ui.checkbox(page_break, tl!("Page break before")); + ui.label(egui::RichText::new(tl!("Formatting exceptions")).font(semibold(12.5))); + ui.checkbox(&mut opts[2], tl!("Suppress line numbers")); + ui.checkbox(&mut opts[3], tl!("Don't hyphenate")); } _ => { let [kinsoku, word_wrap, overflow, top_line, de, dn] = asian; @@ -1393,7 +1410,19 @@ fn body(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool { let tabs = ui.button(tl!("Tabs…")).clicked(); let (ok, cancel) = buttons(ui, tl!("OK")); if ok || tabs { - apply_paragraph(app, *rtl, align, *left, *right, *first, *before, *after, *line, [*keep_next, *keep_lines, *page_break, *widow]); + apply_paragraph( + app, + *rtl, + align, + *left, + *right, + *first, + *before, + *after, + *line, + [*keep_next, *keep_lines, *page_break, *widow], + (*opts, *opts_was), + ); apply_asian(app, *asian, *asian_was); } // Tabs… keeps what was set here and moves on to the Tabs dialog. @@ -2388,9 +2417,6 @@ fn recipient_list(app: &mut WordApp, ui: &mut Ui, fields: &mut Vec, rows } } -/// The Paragraph dialog's OK. `align`, `left` and `right` are as seen on the page; `flags` are -/// keep with next, keep lines together, page break before, widow/orphan control. -#[allow(clippy::too_many_arguments)] /// The Table Properties dialog's fields: Table, Row, Column and Cell sections. fn table_properties(ui: &mut Ui, f: &mut TableForm) { let unit = wordcraft_geom::Unit::default(); @@ -2460,6 +2486,11 @@ fn table_properties(ui: &mut Ui, f: &mut TableForm) { }); } +/// The Paragraph dialog's OK. `align`, `left` and `right` are as seen on the page; `flags` are +/// keep with next, keep lines together, page break before, widow/orphan control; `opts` (now, +/// when opened) are mirror indents, contextual spacing, suppress line numbers and don't +/// hyphenate, of which only the changed ones are set. +#[allow(clippy::too_many_arguments)] fn apply_paragraph( app: &mut WordApp, rtl: bool, @@ -2471,6 +2502,7 @@ fn apply_paragraph( after: f32, line: f32, flags: [bool; 4], + opts: ([bool; 4], [bool; 4]), ) { let [keep_next, keep_lines, page_break, widow] = flags; // Direction first: the alignment and indents are as seen on the page, and their logical @@ -2478,14 +2510,21 @@ fn apply_paragraph( let _ = app.run(if rtl { "para.rtl" } else { "para.ltr" }, json!({})); let _ = app.run("para.align", json!({"value": align})); let (start, end) = if rtl { (right, left) } else { (left, right) }; - let _ = app.run( - "para.set", - json!({"props": { - "indentLeft": start * 72.0, "indentRight": end * 72.0, "indentFirst": first * 72.0, - "spaceBefore": before, "spaceAfter": after, "lineSpacing": {"rule": "multiple", "value": line}, - "keepNext": keep_next, "keepLines": keep_lines, "pageBreakBefore": page_break, "widowControl": widow, - }}), - ); + let mut props = json!({ + "indentLeft": start * 72.0, "indentRight": end * 72.0, "indentFirst": first * 72.0, + "spaceBefore": before, "spaceAfter": after, "lineSpacing": {"rule": "multiple", "value": line}, + "keepNext": keep_next, "keepLines": keep_lines, "pageBreakBefore": page_break, "widowControl": widow, + }); + let (now, was) = opts; + for (key, (on, was)) in ["mirrorIndents", "contextualSpacing", "suppressLineNumbers", "suppressHyphens"].into_iter().zip(now.into_iter().zip(was)) + { + if on != was + && let Some(m) = props.as_object_mut() + { + m.insert(key.into(), json!(on)); + } + } + let _ = app.run("para.set", json!({ "props": props })); } /// The Paragraph dialog's Asian Typography: sets the flags that changed since it opened, so an @@ -2827,12 +2866,40 @@ mod tests { /// Open the Paragraph dialog and press OK without changing anything. fn ok_unchanged(app: &mut WordApp) { - let Some(Dialog::Paragraph { rtl, align, left, right, first, before, after, line, keep_next, keep_lines, page_break, widow, .. }) = - Dialog::open("paragraph", app) + ok_with(app, |_| {}); + } + + /// Open the Paragraph dialog, change its options (`opts`) with `edit` and press OK. + fn ok_with(app: &mut WordApp, edit: impl FnOnce(&mut [bool; 4])) { + let Some(Dialog::Paragraph { + rtl, align, left, right, first, before, after, line, keep_next, keep_lines, page_break, widow, mut opts, .. + }) = Dialog::open("paragraph", app) else { panic!("no paragraph dialog") }; - apply_paragraph(app, rtl, &align, left, right, first, before, after, line, [keep_next, keep_lines, page_break, widow]); + let was = opts; + edit(&mut opts); + apply_paragraph(app, rtl, &align, left, right, first, before, after, line, [keep_next, keep_lines, page_break, widow], (opts, was)); + } + + #[test] + fn paragraph_dialog_sets_only_the_options_that_changed() { + // #512: mirror indents, same-style spacing, suppress line numbers, don't hyphenate. + let mut app = app_with("Hello"); + ok_unchanged(&mut app); + let p = props(&app); + assert_eq!((p.mirror_indents, p.contextual_spacing, p.suppress_line_numbers, p.suppress_hyphens), (None, None, None, None)); + ok_with(&mut app, |o| *o = [true; 4]); + let p = props(&app); + assert_eq!( + (p.mirror_indents, p.contextual_spacing, p.suppress_line_numbers, p.suppress_hyphens), + (Some(true), Some(true), Some(true), Some(true)) + ); + let Some(Dialog::Paragraph { opts, .. }) = Dialog::open("paragraph", &mut app) else { panic!() }; + assert_eq!(opts, [true; 4], "the dialog shows what is set"); + ok_with(&mut app, |o| o[3] = false); + assert_eq!(props(&app).suppress_hyphens, Some(false)); + assert_eq!(props(&app).mirror_indents, Some(true)); } #[test] @@ -2870,7 +2937,19 @@ mod tests { panic!() }; // Right to left, aligned right on the page, 1" from the right edge. - apply_paragraph(&mut app, true, "right", left, right + 1.0, first, before, after, line, [keep_next, keep_lines, page_break, widow]); + apply_paragraph( + &mut app, + true, + "right", + left, + right + 1.0, + first, + before, + after, + line, + [keep_next, keep_lines, page_break, widow], + ([false; 4], [false; 4]), + ); let _ = align; let p = props(&app); assert_eq!(p.bidi, Some(true)); diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index e565cdc20..2fca07236 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -84,19 +84,23 @@ Enter formatting for number: Formato del número: Follow number with: Después del número: Formatting Formato + Formatting exceptions Excepciones de formato Include level number from: Incluir número de nivel de: Ink Entrada de lápiz Insertions and Deletions Inserciones y eliminaciones Insertions: Inserciones: + Inside: Interior: Left border Borde izquierdo Legal style numbering Numeración de estilo legal Link level to style: Vincular nivel con estilo: List Definition Definición de lista + Mirror indents Sangrías simétricas Nothing Nada Number alignment: Alineación del número: Number format Formato de número Number style for this level: Estilo de número para este nivel: Outside border Borde exterior + Outside: Exterior: Restart list after: Reiniciar lista después de: Right border Borde derecho Save the document to turn on AutoSave Guarda el documento para activar el autoguardado diff --git a/crates/ui-egui/src/i18n/et.tsv b/crates/ui-egui/src/i18n/et.tsv index d87436bce..73a1494ea 100644 --- a/crates/ui-egui/src/i18n/et.tsv +++ b/crates/ui-egui/src/i18n/et.tsv @@ -276,6 +276,7 @@ Format Painter Vormingupintsel Formatted Text Vormindatud tekst Formatting at Selection Valiku vormindus + Formatting exceptions Vormindamise erandid Formula Valem General Üldine Go To Mine @@ -347,6 +348,7 @@ Insert\nCitation Lisa\nviide Insert\nFootnote Lisa\nallmärkus Inside Borders Siseäärised + Inside: Sisemine: Inspect Document Kontrolli dokumenti Inspect Styles at Selection Kontrolli valiku stiile Interface language: Kasutajaliidese keel: @@ -390,6 +392,7 @@ Link to Previous Seo eelmisega Links Lingid Location: {path} Asukoht: {path} + Mirror indents Peegelda taanded MIT OR Apache-2.0. Copyright (c) 2026 ArtCraft Team and the WordCraft contributors. MIT OR Apache-2.0. Autoriõigus (c) 2026 ArtCrafti meeskond ja WordCrafti kaasautorid. Macros Makrod Mailings Postitused @@ -469,6 +472,7 @@ Outline Liigendus Outline Level Liigendustase Outside Borders Välisäärised + Outside: Välimine: Oval Ovaal PDF document (*.pdf) PDF-dokument (*.pdf) Page Leht diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 1166cd8b7..06b1f688e 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -81,10 +81,12 @@ Enter formatting for number: 番号の書式: Follow number with: 番号に続く文字: Formatting 書式 + Formatting exceptions 書式の例外 Include level number from: 含めるレベルの番号: Ink インク Insertions and Deletions 挿入と削除 Insertions: 挿入された箇所: + Inside: 内側: Left border 左罫線 Left Margin 左余白 Indents and Spacing インデントと間隔 @@ -92,6 +94,7 @@ Line breaking 行の分割 Link level to style: レベルに対応付けるスタイル: List Definition リストの定義 + Mirror indents 左右対称のインデント More templates → その他のテンプレート → No templates match your search. 検索に一致するテンプレートはありません。 Nothing なし @@ -99,6 +102,7 @@ Number format 番号書式 Number style for this level: このレベルの番号の種類: Outside border 外側の罫線 + Outside: 外側: Reopen 再度開く Restart list after: リストを振り直すレベル: Right border 右罫線 diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 8d6091219..179df1ca2 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -85,19 +85,23 @@ Enter formatting for number: Formatação do número: Follow number with: Após o número: Formatting Formatação + Formatting exceptions Exceções de formatação Include level number from: Incluir número do nível: Ink Tinta Insertions and Deletions Inserções e exclusões Insertions: Inserções: + Inside: Interno: Left border Borda esquerda Legal style numbering Numeração no estilo legal Link level to style: Vincular nível ao estilo: List Definition Definição da lista + Mirror indents Recuos espelhados Nothing Nada Number alignment: Alinhamento do número: Number format Formato do número Number style for this level: Estilo de número deste nível: Outside border Borda externa + Outside: Externo: Restart list after: Reiniciar lista após: Right border Borda direita Save the document to turn on AutoSave Salve o documento para ativar o salvamento automático diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index a42b8720e..b027924ff 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -2100,6 +2100,11 @@ preview Title Пример Apply Применить Convert Notes Преобразовать сноски +# Paragraph dialog options (#512). + Formatting exceptions Исключения форматирования + Inside: Внутри: + Mirror indents Зеркальные отступы + Outside: Снаружи: # WordArt and connectors (#416). WordArt Styles Стили WordArt Text Fill Заливка текста diff --git a/crates/ui-egui/src/i18n/sr-latn.tsv b/crates/ui-egui/src/i18n/sr-latn.tsv index 289108876..664637da8 100644 --- a/crates/ui-egui/src/i18n/sr-latn.tsv +++ b/crates/ui-egui/src/i18n/sr-latn.tsv @@ -108,20 +108,24 @@ Enter formatting for number: Oblik broja: Follow number with: Posle broja: Formatting Oblikovanje + Formatting exceptions Izuzeci formatiranja Include level number from: Uključi broj nivoa: Ink Mastilo Insertions and Deletions Umetanja i brisanja Insertions: Umetanja: + Inside: Unutra: Left border Leva ivica Left Margin Leva margina Legal style numbering Pravno numerisanje Link level to style: Poveži nivo sa stilom: List Definition Definicija liste + Mirror indents Preslikaj uvlačenja Nothing Ništa Number alignment: Poravnanje broja: Number format Format broja Number style for this level: Stil broja za ovaj nivo: Outside border Spoljna ivica + Outside: Spolja: Reopen Ponovo otvori Restart list after: Ponovo počni listu posle: Right border Desna ivica diff --git a/crates/ui-egui/src/i18n/sr.tsv b/crates/ui-egui/src/i18n/sr.tsv index 1b1e2181b..ffe850f1d 100644 --- a/crates/ui-egui/src/i18n/sr.tsv +++ b/crates/ui-egui/src/i18n/sr.tsv @@ -112,20 +112,24 @@ Enter formatting for number: Облик броја: Follow number with: После броја: Formatting Обликовање + Formatting exceptions Изузеци форматирања Include level number from: Укључи број нивоа: Ink Мастило Insertions and Deletions Уметања и брисања Insertions: Уметања: + Inside: Унутра: Left border Лева ивица Left Margin Лева маргина Legal style numbering Правно нумерисање Link level to style: Повежи ниво са стилом: List Definition Дефиниција листе + Mirror indents Пресликај увлачења Nothing Ништа Number alignment: Поравнање броја: Number format Формат броја Number style for this level: Стил броја за овај ниво: Outside border Спољна ивица + Outside: Споља: Reopen Поново отвори Restart list after: Поново почни листу после: Right border Десна ивица diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index 977773cd5..f8e0aae73 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -1722,6 +1722,11 @@ Apply Uygula Convert Notes Notları Dönüştür +# Paragraph dialog options (#512). + Formatting exceptions Biçimlendirme özel durumları + Inside: İç: + Mirror indents Ayna girintiler + Outside: Dış: # WordArt and connectors (#416). WordArt Styles WordArt Stilleri Text Fill Metin Dolgusu diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index ad7e1cf8b..628a6d8fc 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -87,19 +87,23 @@ Enter formatting for number: Вигляд номера: Follow number with: Після номера: Formatting Форматування + Formatting exceptions Винятки форматування Include level number from: Додати номер рівня: Ink Рукописні примітки Insertions and Deletions Вставлення й видалення Insertions: Вставлення: + Inside: Усередині: Left border Ліва межа Legal style numbering Юридична нумерація Link level to style: Зв'язати рівень зі стилем: List Definition Визначення списку + Mirror indents Дзеркальні відступи Nothing Нічого Number alignment: Вирівнювання номера: Number format Формат номера Number style for this level: Стиль номера для цього рівня: Outside border Зовнішня межа + Outside: Зовні: Restart list after: Почати список знову після: Right border Права межа Save the document to turn on AutoSave Збережіть документ, щоб увімкнути автозбереження diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index c3f6daed7..755b1080f 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -93,10 +93,12 @@ Enter formatting for number: 输入编号的格式: Follow number with: 编号之后: Formatting 格式 + Formatting exceptions 格式例外 Include level number from: 包含的级别编号来自: Ink 墨迹 Insertions and Deletions 插入和删除 Insertions: 插入内容: + Inside: 内侧: Left border 左框线 Left Margin 左边距 Indents and Spacing 缩进和间距 @@ -104,6 +106,7 @@ Line breaking 断行 Link level to style: 将级别链接到样式: List Definition 列表定义 + Mirror indents 对称缩进 More templates → 更多模板 → No templates match your search. 没有与搜索匹配的模板。 Nothing 无 @@ -111,6 +114,7 @@ Number format 编号格式 Number style for this level: 此级别的编号样式: Outside border 外侧框线 + Outside: 外侧: Reopen 重新打开 Restart list after: 重新开始列表的级别: Right border 右框线 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index b84fac387..fdcea9f3f 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -82,10 +82,12 @@ Enter formatting for number: 輸入編號的格式: Follow number with: 編號之後: Formatting 格式設定 + Formatting exceptions 格式例外 Include level number from: 包含的層級編號來自: Ink 筆跡 Insertions and Deletions 插入和刪除 Insertions: 插入內容: + Inside: 內側: Left border 左框線 Left Margin 左邊界 Indents and Spacing 縮排和間距 @@ -93,6 +95,7 @@ Line breaking 斷行 Link level to style: 將層級連結到樣式: List Definition 清單定義 + Mirror indents 對稱縮排 More templates → 更多範本 → No templates match your search. 沒有符合搜尋的範本。 Nothing 無 @@ -100,6 +103,7 @@ Number format 編號格式 Number style for this level: 此層級的編號樣式: Outside border 外側框線 + Outside: 外側: Reopen 重新開啟 Restart list after: 重新開始清單的層級: Right border 右框線 diff --git a/crates/ui-egui/src/panes.rs b/crates/ui-egui/src/panes.rs index b82a0b8c5..33d2a72f5 100644 --- a/crates/ui-egui/src/panes.rs +++ b/crates/ui-egui/src/panes.rs @@ -594,6 +594,7 @@ fn describe(prop: &str, v: &Value) -> String { "spaceAfter" => "Space after", "lineSpacing" => "Line spacing", "contextualSpacing" => "Don't add space between paragraphs of the same style", + "mirrorIndents" => "Mirror indents", "keepNext" => "Keep with next", "keepLines" => "Keep lines together", "pageBreakBefore" => "Page break before", diff --git a/docs/layout-parity.md b/docs/layout-parity.md index 569244dce..c7bcc0742 100644 --- a/docs/layout-parity.md +++ b/docs/layout-parity.md @@ -72,6 +72,7 @@ font can't match Word's metrics. | Date | Change | Summary | |---|---|---| +| 2026-10-11 | trivial | Mirror indents (`w:mirrorIndents`): left/right indents swap on even pages; suppress line numbers inherited from styles (#512) | | 2026-10-11 | trivial | NUMPAGES/SECTIONPAGES in the body show the real totals (the body is laid out again once the page count is known, at most three extra passes, #451); footnotes referenced in table cells go to the page bottom (#450); PDF links to a bookmark go to the page its start is on, not its paragraph's first page (#455) | | 2026-10-11 | trivial | Mirrored margins on even pages, tall and linked footers/headers push the body, long endnotes continue, caret in continued notes (#452, #453, #454, #456, #457) | | 2026-10-11 | trivial | Hyphenation zone, words in capitals and consecutive-hyphen limit follow the document settings; Manual Hyphenation (#407) | diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 9af2620a1..b97430489 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -37,6 +37,7 @@ own (`AGENTS.md`: no Microsoft iconography, ever). | Date | Change | Summary | |---|---|---| +| 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) | | 2026-10-11 | trivial | Line Numbers, Hyphenation (with Manual Hyphenation), Language, Envelopes and Labels dialogs (#407); the three duplicated modal-dialog rows merged into one |