From 6912b5df76256ba372de50a6218759a20d6cf768 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Tue, 13 Oct 2026 06:33:02 -0400 Subject: [PATCH] Fonts: a letter with combining marks the run's font lacks takes a face that has them all MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "Z̴̡̛͓a̸̧̘l̵̩g̶̞o̸̤" in an Aptos run drew its marks as missing-glyph boxes: no face in the chain covered the whole cluster and the fallback for its base letter neither, so the run's own font was kept. PowerPoint draws the marks stacked. A loaded plain face that covers the whole cluster is now taken before falling back to the run's font. w09-scripts s1 8.40 -> 7.86. Co-Authored-By: Claude Opus 5.5 (1M context) --- crates/fonts/src/fallback.rs | 5 ++++- crates/text/src/tests.rs | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/crates/fonts/src/fallback.rs b/crates/fonts/src/fallback.rs index fa3c830..563e033 100644 --- a/crates/fonts/src/fallback.rs +++ b/crates/fonts/src/fallback.rs @@ -185,7 +185,10 @@ impl FontDb { let f = match self.fallback_for(key, exclude) { Some(f) if covers_cluster(&f, cluster) => f, Some(f) if chain.iter().all(|c| !c.covers(key)) => f, - _ => first(), + // A letter with marks the run's font lacks (stacked combining marks): a loaded plain + // face that draws the whole cluster, as PowerPoint draws the marks, before the run's + // font with missing-glyph boxes. + _ => self.read_faces().iter().find(|f| !f.is_color() && covers_cluster(f, cluster)).cloned().unwrap_or_else(first), }; // A pictograph that defaults to text presentation (⚠ ♥ ⬆ ▶) whose first fallback is the // colour emoji font: PowerPoint draws it from a plain symbol font. diff --git a/crates/text/src/tests.rs b/crates/text/src/tests.rs index 39e4626..1e2be17 100644 --- a/crates/text/src/tests.rs +++ b/crates/text/src/tests.rs @@ -1196,3 +1196,23 @@ fn rtl_columns_start_on_the_right_and_just_anchoring_sits_at_the_bottom() { assert!((lay(&p, &sh, 400.0, 300.0).lines[0].top - b).abs() < 0.01, "{a:?}"); } } + +#[test] +fn stacked_combining_marks_take_a_face_that_draws_them() { + // A letter with marks the run's font lacks draws in a face that has them all (no boxes). + let db = FontDb::global(); + let cluster = ['Z', '\u{0334}', '\u{0321}', '\u{031B}']; + let run_face = db.face("Inter", "Regular"); + let f = db.cascade(&cluster, std::slice::from_ref(&run_face), "Regular"); + if !deckcraft_fonts::covers_cluster(&run_face, &cluster) && ["DejaVu Sans", "Segoe UI"].iter().any(|fam| db.has_family(fam)) { + // Make sure a face with the marks is loaded, then the cluster must take one. + for fam in ["DejaVu Sans", "Segoe UI"] { + if db.has_family(fam) { + let _ = db.face(fam, "Regular"); + } + } + let f = db.cascade(&cluster, std::slice::from_ref(&run_face), "Regular"); + assert!(deckcraft_fonts::covers_cluster(&f, &cluster), "{} lacks the marks", f.family); + } + let _ = f; +}