From c5ebf4d1cdc6c14cea34fab34ef1f2253885e555 Mon Sep 17 00:00:00 2001 From: MarekSpodBiedry <73122968+MarekSpodBiedry@users.noreply.github.com> Date: Mon, 12 Oct 2026 01:52:51 +0200 Subject: [PATCH] Keep inter-word spaces in stroke-font MTEXT (#628) (#644) Squash of PR #644 by @MarekSpodBiedry. Co-Authored-By: Claude Opus 5.5 --- crates/fonts/src/lib.rs | 11 +++++++++++ crates/fonts/src/mtext.rs | 8 +++++++- crates/fonts/src/mtext/para.rs | 2 +- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/crates/fonts/src/lib.rs b/crates/fonts/src/lib.rs index 0217fa9..73e7794 100644 --- a/crates/fonts/src/lib.rs +++ b/crates/fonts/src/lib.rs @@ -213,6 +213,17 @@ pub fn line_width(s: &str, height: f64, width_factor: f64) -> f64 { (n - stroke::GAP / stroke::CAP * height * width_factor).max(0.0) } +/// Width of the gap MTEXT leaves between two words. Words are measured without their trailing +/// letter gap, so the stroke font's gap is added on both sides of the space, as TEXT lays it out. +pub fn space_width(font: &TextFont, height: f64, width_factor: f64) -> f64 { + if let TextFont::Outline(face) = font + && let Some(w) = ttf::width(face, " ", height, width_factor) + { + return w; + } + stroke_advance(' ', height, width_factor) + stroke::GAP / stroke::CAP * height * width_factor +} + /// The font a piece of text is set in: the built-in stroke font or an installed TrueType / /// OpenType face (outlines). Either way, characters the font lacks come from a fallback font. #[derive(Clone, Debug, Default)] diff --git a/crates/fonts/src/mtext.rs b/crates/fonts/src/mtext.rs index 8c08a61..7129591 100644 --- a/crates/fonts/src/mtext.rs +++ b/crates/fonts/src/mtext.rs @@ -571,7 +571,7 @@ pub fn layout_mtext_with(contents: &str, p: &MTextParams) -> MTextLayout { } Atom::Word(segs) => { let ww: f64 = segs.iter().map(|(sg, f)| seg_width(sg, fmt(*f))).sum(); - let sw = pending_space.map(|f| text_width(&fmt(f).font, " ", fmt(f).height, fmt(f).wf)).unwrap_or(0.0); + let sw = pending_space.map(|f| crate::space_width(&fmt(f).font, fmt(f).height, fmt(f).wf)).unwrap_or(0.0); if width > 0.0 && !line.items.is_empty() && line.width + sw + ww > width + 1e-9 { finish(&mut lines, &mut line, fmt(last_fmt), para); } else if !line.items.is_empty() || pending_space.is_some() { @@ -693,6 +693,12 @@ mod tests { assert!((c.x - 5.0).abs() < 1e-9 && (c.y - 5.0).abs() < 1e-9); } + #[test] + fn stroke_word_space_matches_text() { + let l = layout_mtext("A B", Vec2::ZERO, 1.0, 0.0, 1, 0.0, 1.0); + assert!((l.bounds.width() - crate::line_width("A B", 1.0, 1.0)).abs() < 1e-9); + } + fn lay(s: &str, width: f64) -> MTextLayout { layout_mtext(s, Vec2::ZERO, 1.0, width, 1, 0.0, 1.0) } diff --git a/crates/fonts/src/mtext/para.rs b/crates/fonts/src/mtext/para.rs index 6680369..80d7618 100644 --- a/crates/fonts/src/mtext/para.rs +++ b/crates/fonts/src/mtext/para.rs @@ -426,7 +426,7 @@ pub(super) fn layout(contents: &str, p: &MTextParams, base: &Fmt) -> MTextLayout let paras = Paragraphs::scan(contents); let (atoms, fmts) = parse(contents, base.clone()); let fmt = |i: usize| fmts.get(i).unwrap_or(base); - let space_w = |f: usize| crate::text_width(&fmt(f).font, " ", fmt(f).height, fmt(f).wf); + let space_w = |f: usize| crate::space_width(&fmt(f).font, fmt(f).height, fmt(f).wf); let mut lines: Vec = Vec::new(); let mut k = 0usize;