diff --git a/crates/text/src/tests/harakat.rs b/crates/text/src/tests/harakat.rs index c097fc48b..e590f103c 100644 --- a/crates/text/src/tests/harakat.rs +++ b/crates/text/src/tests/harakat.rs @@ -512,6 +512,43 @@ fn assert_authored_ownership(t: &TextObject, text: &str, l: &TextLayout, what: & } } +/// Repeated glyphs in one cluster can belong to differently painted runs. Match each authored +/// occurrence in order, without needing an installed font, and still reject a changed owner. +#[test] +fn duplicate_authored_glyphs_keep_occurrence_order() { + let mut text = TextObject::point(Point::ZERO, "\u{0651}", style(FAMILY)); + text.runs.push(vectorcraft_doc::TextRun::new("\u{0651}", CharStyle { fill: red(), ..style(FAMILY) })); + let glyph = |run| PositionedGlyph { + outline: Default::default(), + run, + byte: 0, + origin: Point::ZERO, + advance: 0.0, + len: 4, + angle: 0.0, + line: 0, + font_id: 0, + gid: 7, + xf: Affine::IDENTITY, + rtl: true, + scale: 1.0, + image: None, + colour: None, + bend: None, + unturned: Affine::IDENTITY, + pivot: Point::ZERO, + carets: Vec::new(), + width: 0.0, + width_start: 0.0, + }; + let before = TextLayout { glyphs: vec![glyph(0), glyph(1)], ..TextLayout::default() }; + assert_eq!(assert_kashida_ownership(&text, &before, &before, &red(), "duplicate glyphs", false), 0); + + let mut wrong = before.clone(); + wrong.glyphs[0].run = 1; + assert!(std::panic::catch_unwind(|| assert_kashida_ownership(&text, &before, &wrong, &red(), "changed owner", false)).is_err()); +} + /// Compare authored glyph run indices before/after Insert Kashidas (matched by byte and gid; /// layout order can differ between point and justified area), and assert every inserted tatweel /// uses the base letter's paint at the cluster start—not `mark_fill`. When `require_mark_last`, @@ -525,16 +562,17 @@ fn assert_kashida_ownership( what: &str, require_mark_last: bool, ) -> usize { - let mut before_runs: std::collections::HashMap<(usize, u32), Vec> = std::collections::HashMap::new(); + let mut before_runs: std::collections::HashMap<(usize, u32), std::collections::VecDeque> = std::collections::HashMap::new(); for g in before.glyphs.iter().filter(|g| g.len > 0) { - before_runs.entry((g.byte, g.gid)).or_default().push(g.run); + before_runs.entry((g.byte, g.gid)).or_default().push_back(g.run); } let mut after_n = 0; for g in with_kashida.glyphs.iter().filter(|g| g.len > 0) { after_n += 1; let runs = before_runs.get_mut(&(g.byte, g.gid)).unwrap_or_else(|| panic!("{what}: authored glyph {} at {} missing before kashida", g.gid, g.byte)); - let want_run = runs.pop().unwrap_or_else(|| panic!("{what}: no remaining before-run for {} at {}", g.gid, g.byte)); + // Duplicate glyphs at the same cluster byte retain their occurrence order. + let want_run = runs.pop_front().unwrap_or_else(|| panic!("{what}: no remaining before-run for {} at {}", g.gid, g.byte)); assert_eq!(g.run, want_run, "{what}: authored run at {} gid {}", g.byte, g.gid); let want_fill = coloured.runs.get(want_run).map(|r| &r.style.fill).unwrap_or_else(|| panic!("{what}: missing run {want_run}")); assert_eq!(&run_of(coloured, g).style.fill, want_fill, "{what}: authored fill at {}", g.byte);