From 4265c2f30166f90b52dd3a8a74a3ad66b18ceb34 Mon Sep 17 00:00:00 2001 From: imachus <51533227+imachus@users.noreply.github.com> Date: Wed, 14 Oct 2026 15:43:14 +0900 Subject: [PATCH] Type: whitespace the primary font doesn't map takes the fallback face too (#1844) An ideographic space in a face without U+3000 was shaped with that face and could set at zero width, removing an indent. Only what the layout handles itself (tab, line and paragraph breaks) stays with the primary. Co-Authored-By: Claude Opus 5.5 --- crates/text/src/shape.rs | 5 ++++- crates/text/src/tests.rs | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/crates/text/src/shape.rs b/crates/text/src/shape.rs index 0447b03c4..650c3fc3c 100644 --- a/crates/text/src/shape.rs +++ b/crates/text/src/shape.rs @@ -374,7 +374,10 @@ pub(crate) fn shape_range( // Digits are drawn in the style's numeral system: the font must cover that digit. let c = st.digits.map(c); let level = level_at(i); - let covered = c.is_whitespace() || c.is_control() || pmap.as_ref().is_none_or(|m| m.map(c).is_some()); + // Whitespace the primary font doesn't map goes to the fallback face like any other + // character (#1844): an ideographic space set at zero width would remove an indent. + // What the layout handles itself (tab, line and paragraph breaks) stays. + let covered = c.is_control() || pmap.as_ref().is_none_or(|m| m.map(c).is_some()); let face = if covered { primary.clone() } else if let Some((_, f)) = cache.iter().find(|(k, _)| *k == c) { diff --git a/crates/text/src/tests.rs b/crates/text/src/tests.rs index 8796791db..d7c52af75 100644 --- a/crates/text/src/tests.rs +++ b/crates/text/src/tests.rs @@ -405,6 +405,26 @@ fn fallback_font_per_character() { assert_eq!(l.glyphs[2].font_id, primary.id()); } +/// A space the primary font doesn't map goes to the fallback face like any other character +/// (#1844): an ideographic space in a face without U+3000 keeps its em, so an indent made with it +/// isn't lost. A tab, which the layout handles itself, stays with the primary face. Needs a bundled +/// face with U+3000 and あ (craft-fonts; skipped without it). +#[test] +fn whitespace_the_primary_font_lacks_takes_the_fallback_face() { + let primary = db().face("Source Sans 3", "Regular").unwrap(); + assert!(!primary.covers('\u{3000}')); + if ['\u{3000}', 'あ'].iter().any(|&c| db().fallback_for(c, primary.id()).is_none()) { + eprintln!("skipped: built without craft-fonts (set CRAFT_FONTS_DIR to a craft-fonts checkout to run it)"); + return; + } + let l = layout(db(), &point("あ\u{3000}あ", style(20.0))); + let width: f64 = l.glyphs.iter().map(|g| g.advance).sum(); + assert!((width - 60.0).abs() < 0.01, "あ あ sets at 3 em: {:?}", l.glyphs.iter().map(|g| g.advance).collect::>()); + assert_ne!(l.glyphs[1].font_id, primary.id()); + let l = layout(db(), &point("a\tb", style(20.0))); + assert!(l.glyphs.iter().all(|g| g.font_id == primary.id())); +} + #[test] fn multiple_runs_and_styles() { let mut t = point("", style(10.0));