From 57ff14d3f357a827bf144644aa42bcca993e67bc Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Sun, 19 Jul 2026 17:16:41 +0300 Subject: [PATCH] fix(mtext): render font italic flag and non-breaking space MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Complete the editor/layout support for two MTEXT inline codes that were parsed but not rendered: - `\f…|i1;` set the font italic flag but layout only fed oblique from `\Q`, so imported italic showed upright. Map font.italic to the same 15° slant the editor's Italic button uses (explicit `\Q` still wins), and make that button recognise + clear the flag so imported italic can be toggled off. - `\~` (non-breaking space) was a normal Space atom, so lines could wrap at it. Fold it into the word — it still renders as a space but no longer breaks. Co-Authored-By: Claude Opus 4.8 --- src/app/mtext_editor.rs | 17 +++++++++++++-- src/entities/text_support.rs | 41 +++++++++++++++++++++++++++++++----- 2 files changed, 51 insertions(+), 7 deletions(-) diff --git a/src/app/mtext_editor.rs b/src/app/mtext_editor.rs index a4df4a7bd..11076c6df 100644 --- a/src/app/mtext_editor.rs +++ b/src/app/mtext_editor.rs @@ -707,8 +707,21 @@ impl super::OpenCADStudio { each(&mut cells[a..b], |p| p.set_strikethrough(on)); } MTextFmt::Italic => { - let on = !all_have(&cells[a..b], |p| p.oblique_angle == Some(15.0)); - each(&mut cells[a..b], |p| p.oblique_angle = on.then_some(15.0)); + // Italic is the editor's canonical 15° oblique. An imported + // run may instead carry the font italic flag (`\f…|i1;`); + // recognise that as italic too, and clear it on toggle so + // the one canonical form is the oblique — otherwise an + // imported italic run could never be switched off. + let on = !all_have(&cells[a..b], |p| { + p.oblique_angle == Some(15.0) + || p.font.as_ref().is_some_and(|f| f.italic) + }); + each(&mut cells[a..b], |p| { + p.oblique_angle = on.then_some(15.0); + if let Some(f) = p.font.as_mut() { + f.italic = false; + } + }); } MTextFmt::Bold => { // Real bold: set the font's bold flag (the SDF renderer bakes diff --git a/src/entities/text_support.rs b/src/entities/text_support.rs index 376eaa5b6..ca6cea10d 100644 --- a/src/entities/text_support.rs +++ b/src/entities/text_support.rs @@ -527,10 +527,16 @@ pub fn adapt_mtext_paragraphs( None => 1.0, }, width_mul: p.width_factor.map(|w| w as f32).unwrap_or(1.0), - oblique_rad: p - .oblique_angle - .map(|q| (q as f32).to_radians()) - .unwrap_or(0.0), + // Explicit `\Q` oblique wins; otherwise the font italic flag + // (`\f…|i1;`) renders as the same ~15° slant the editor's Italic + // button applies, so imported italic text isn't shown upright. + oblique_rad: match p.oblique_angle { + Some(q) => (q as f32).to_radians(), + None if p.font.as_ref().is_some_and(|f| f.italic) => { + 15.0_f32.to_radians() + } + None => 0.0, + }, tracking: p.tracking.map(|t| t as f32).unwrap_or(1.0), valign: match p.line_align { Some(MTextLineAlignment::Middle) => 1, @@ -1071,7 +1077,13 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout { MTextRunKind::Glyphs(text) => { let mut word = String::new(); for ch in text.chars() { - if ch == ' ' || ch == '\u{00A0}' || ch == '\t' { + if ch == '\u{00A0}' { + // Non-breaking space (`\~`): keep it inside the word + // so the line can't wrap here. It still renders as a + // space — the font advances over the ' ' — but the + // words it joins stay together. + word.push(' '); + } else if ch == ' ' || ch == '\t' { if !word.is_empty() { atoms.push(LayoutAtom { kind: AtomKind::Word(std::mem::take(&mut word)), @@ -2009,6 +2021,25 @@ mod adapter_tests { assert!(d.underline); } + #[test] + fn font_italic_flag_renders_as_oblique() { + // A `\f…|i1;` italic font has no true-italic glyphs in the stroke/SDF + // faces, so it must render as the same slant the editor's Italic button + // applies (~15°) — not upright. + let (_, it) = first_run(&adapt_mtext_paragraphs("\\fArial|i1;x", 2.5, true)); + assert!( + (it.oblique_rad - 15f32.to_radians()).abs() < 1e-4, + "italic flag should slant, got {}", + it.oblique_rad + ); + // A non-italic font stays upright. + let (_, up) = first_run(&adapt_mtext_paragraphs("\\fArial|i0;x", 2.5, true)); + assert!(up.oblique_rad.abs() < 1e-4, "got {}", up.oblique_rad); + // An explicit `\Q` wins over the italic flag (no double slant). + let (_, q) = first_run(&adapt_mtext_paragraphs("\\Q30;\\fArial|i1;x", 2.5, true)); + assert!((q.oblique_rad - 30f32.to_radians()).abs() < 1e-4, "got {}", q.oblique_rad); + } + #[test] fn stacking_keeps_its_halves_apart() { // `\S` reaches layout as a stack, not as flattened text: the halves