From afe4c324a72b916527ea8497260733ae1c6ff38e Mon Sep 17 00:00:00 2001 From: Zach Berry <1112351+zberry@users.noreply.github.com> Date: Sun, 18 Oct 2026 23:24:08 -0500 Subject: [PATCH] [II5] A paragraph with its own numbering start begins a new run at that number Layout added the paragraph's start to the running count, so start k set paragraph by paragraph read 1, 3, 5. A change of start_at between neighbouring numbered paragraphs now restarts the count, as PowerPoint's Start at does; the same start on every paragraph still counts on. Co-Authored-By: Claude Sonnet 5.5 --- crates/engine/src/cmd/format.rs | 2 +- crates/text/src/layout.rs | 62 ++++++++++++++++++++++----------- crates/text/src/tests.rs | 25 +++++++++++++ 3 files changed, 68 insertions(+), 21 deletions(-) diff --git a/crates/engine/src/cmd/format.rs b/crates/engine/src/cmd/format.rs index 6e9e75ce..0f81cfaf 100644 --- a/crates/engine/src/cmd/format.rs +++ b/crates/engine/src/cmd/format.rs @@ -179,7 +179,7 @@ pub fn specs() -> Vec { "Numbering", ["Home", "Paragraph"], None, - "{on?: bool, scheme?: arabicPeriod|arabicParenR|romanUcPeriod|romanLcPeriod|alphaUcPeriod|alphaLcParenR|alphaLcPeriod, start?: n, color?, size?: % of text}", + "{on?: bool, scheme?: arabicPeriod|arabicParenR|romanUcPeriod|romanLcPeriod|alphaUcPeriod|alphaLcParenR|alphaLcPeriod, start?: n (a numbered paragraph whose start differs from that of the last numbered paragraph at its level begins a new run of numbers at n, as PowerPoint's Start at does), color?, size?: % of text}", has_text_or_shapes, numbering ), diff --git a/crates/text/src/layout.rs b/crates/text/src/layout.rs index f764cf52..05cc4beb 100644 --- a/crates/text/src/layout.rs +++ b/crates/text/src/layout.rs @@ -132,38 +132,60 @@ impl BoxGeom { } } -/// Number shown for a paragraph at list level `lvl`, advancing the per-level counters. -fn next_number(numbering: &mut [u32], lvl: usize, auto_num: bool, empty: bool) -> Option { - if auto_num && !empty { - if let Some(n) = numbering.get_mut(lvl) { - *n += 1; - } - for deeper in numbering.iter_mut().skip(lvl + 1) { - *deeper = 0; - } - numbering.get(lvl).copied() - } else if auto_num { - // Preview the next number without consuming it: empty paragraphs do not advance - // numbering in the saved document or affect later populated paragraphs. - numbering.get(lvl).map(|n| n.saturating_add(1)) - } else { - if !empty { - for n in numbering.iter_mut().skip(lvl) { +/// Number shown for a paragraph at list level `lvl`, advancing the per-level counters. `start_at` +/// is the paragraph's numbering start (`None`: not numbered); `run_start` holds the start each +/// level's current run of numbers began with, and a paragraph that changes it starts a new run +/// (PowerPoint's "Start at"). +fn next_number(numbering: &mut [u32], run_start: &mut [u32], lvl: usize, start_at: Option, empty: bool) -> Option { + match start_at { + Some(start) if !empty => { + if let (Some(rs), Some(n)) = (run_start.get_mut(lvl), numbering.get_mut(lvl)) + && *rs != start + { + *rs = start; *n = 0; } + if let Some(n) = numbering.get_mut(lvl) { + *n += 1; + } + for deeper in numbering.iter_mut().skip(lvl + 1) { + *deeper = 0; + } + numbering.get(lvl).copied() + } + // Preview the next number without consuming it: empty paragraphs do not advance + // numbering in the saved document or affect later populated paragraphs. A paragraph + // with its own start would begin a new run, so it previews that run's first number. + Some(start) => { + if run_start.get(lvl).is_some_and(|rs| *rs != start) { + Some(1) + } else { + numbering.get(lvl).map(|n| n.saturating_add(1)) + } + } + None => { + if !empty { + for n in numbering.iter_mut().skip(lvl) { + *n = 0; + } + } + None } - None } } fn shape_paragraphs(ctx: &Ctx, shape: &Shape, body: &TextBody, opts: &Opts, scale: f64, upright: Upright) -> Vec { let mut numbering: Vec = vec![0; 9]; + let mut run_start: Vec = vec![0; 9]; let mut shaped_paras: Vec = Vec::with_capacity(body.paragraphs.len()); for (pi, para) in body.paragraphs.iter().enumerate() { let lvl = para.level.min(8) as usize; let pp = resolve::para(ctx, shape, para); - let auto_num = matches!(pp.bullet, Some(Bullet::AutoNum { .. })); - let number = next_number(&mut numbering, lvl, auto_num, para.is_empty()); + let start_at = match &pp.bullet { + Some(Bullet::AutoNum { start_at, .. }) => Some(*start_at), + _ => None, + }; + let number = next_number(&mut numbering, &mut run_start, lvl, start_at, para.is_empty()); shaped_paras.push(shape_para(ctx, shape, body, pi, scale, opts, number, upright)); } shaped_paras diff --git a/crates/text/src/tests.rs b/crates/text/src/tests.rs index fd02e821..c24a49f3 100644 --- a/crates/text/src/tests.rs +++ b/crates/text/src/tests.rs @@ -1845,3 +1845,28 @@ fn an_empty_paragraph_keeps_its_paragraph_mark_height() { let h = l.lines[1].bottom - l.lines[1].top; assert!((h - h10).abs() < 0.5, "a 10pt empty paragraph should give {h10}, got {h}"); } + +#[test] +fn a_paragraph_with_its_own_start_at_restarts_the_count_there() { + // PowerPoint's "Start at" on a paragraph begins a new run of numbers at that value, so setting + // start k on paragraph k reads 1, 2, 3, not 1, 3, 5. + // The bullets' glyph ids next to those of the expected labels, as pairs (drawn, expected). + let check = |starts: &[u32], expect: &[u32]| { + let (p, mut sh) = setup("a\nb\nc\nd", 600.0, 300.0); + for (para, st) in sh.text.as_mut().unwrap().paragraphs.iter_mut().zip(starts) { + para.props.bullet = Some(Bullet::AutoNum { scheme: "arabicPeriod".into(), start_at: *st }); + } + let l = lay(&p, &sh, 600.0, 300.0); + let bullets: Vec<_> = l.runs.iter().filter(|r| r.cells.is_empty()).collect(); + assert_eq!(bullets.len(), expect.len()); + for (b, n) in bullets.iter().zip(expect) { + let want = measure_str(&b.face, b.size, &format!("{n}.")).1.iter().map(|g| g.0).collect::>(); + assert_eq!(b.glyphs.iter().map(|g| g.0).collect::>(), want, "expected {n}. for starts {starts:?}"); + } + }; + check(&[1, 2, 3, 4], &[1, 2, 3, 4]); + // The same start on every paragraph still counts on from it (a whole box numbered at once). + check(&[3, 3, 3, 3], &[3, 4, 5, 6]); + // A later paragraph with a new start begins a fresh run. + check(&[1, 1, 7, 7], &[1, 2, 7, 8]); +}