diff --git a/crates/compose/src/lib.rs b/crates/compose/src/lib.rs index 5fe8f6d51..ebf1e377b 100644 --- a/crates/compose/src/lib.rs +++ b/crates/compose/src/lib.rs @@ -165,6 +165,20 @@ pub struct PlacedGlyph { } impl PlacedGlyph { + /// Whether an underline or a strikethrough runs across this glyph. + /// + /// A tab is not drawn, but it is a character with an advance, and the rule runs across its + /// whole width as InDesign draws it: forms rule their fill-in lines with underlined tabs, and + /// without this they lost every writing line (#1466). The spaces a line break trimmed off the + /// end, the break characters themselves and the markers' carriers are not part of the line's + /// text and are not spanned. + pub fn spans_rules(&self) -> bool { + if self.visible { + return true; + } + self.adv > 0.0 && matches!(self.rendered_char, '\t' | designcraft_doc::story::RIGHT_INDENT_TAB) + } + /// In a vertical frame, the turn that sets this glyph upright, applied after drawing it at `x` /// on `baseline`: it hangs from its vertical origin ([`designcraft_fonts::FontFace::v_origin`], /// centred across) at `x` on the line's centre, the middle of the font's em box; a tate-chu-yoko diff --git a/crates/pdf/src/text.rs b/crates/pdf/src/text.rs index 300a47d93..1aaf435d9 100644 --- a/crates/pdf/src/text.rs +++ b/crates/pdf/src/text.rs @@ -160,7 +160,7 @@ fn cluster_chars(members: &[PlacedGlyph], cluster: &str) -> Option Vec<(designcraft_compose::Rule, Rect)> { let mut bars: Vec<(designcraft_compose::Rule, Rect)> = Vec::new(); for line in &ft.lines { - for g in line.glyphs.iter().filter(|g| g.visible) { + for g in line.glyphs.iter().filter(|g| g.spans_rules()) { let style = &cs.styles[g.style as usize]; let (on, rule) = if underline { (style.underline, &style.underline_rule) } else { (style.strikethrough, &style.strike_rule) }; if !on { @@ -638,3 +638,39 @@ fn glyph_outlines(glyphs: &[PlacedGlyph], size: f64, tan: f64) -> BezPath { } bp } + +#[cfg(test)] +mod rule_tests { + use super::*; + use designcraft_compose::Cache; + use designcraft_doc::{Document, ParaFormat, SpreadRef}; + use designcraft_geom::Rect; + + /// A tab draws nothing but advances to its stop, and the underline runs across it, as it does + /// on screen: a form's fill-in lines are underlined tabs (#1466). + #[test] + fn an_underline_runs_across_a_tab() { + let mut doc = Document::new(&Default::default()); + for style in &mut doc.styles_mut().paragraph { + style.chars.underline = Some(true); + } + let layer = doc.default_layer(); + let (_, sid) = + doc.add_text_frame(SpreadRef::Doc(0), Rect::new(36.0, 36.0, 400.0, 300.0), layer, "Name\tCity", ParaFormat::default()).unwrap(); + let cs = Cache::new().get(&doc, sid, None); + let line = &cs.frames[0].lines[0]; + let tab = line.glyphs.iter().find(|g| g.rendered_char == '\t').expect("the tab is placed on the line"); + let bars = text_rules(&cs, &cs.frames[0], true); + assert_eq!(bars.len(), 1, "one bar across the line, not one before the tab and one after: {bars:?}"); + let bar = bars[0].1; + assert!(bar.x0 <= tab.x + 0.5 && bar.x1 >= tab.x + tab.adv - 0.5, "the bar covers the tab: {bar:?}, tab {} + {}", tab.x, tab.adv); + // The strikethrough of the same text behaves the same way. + for style in &mut doc.styles_mut().paragraph { + style.chars.underline = None; + style.chars.strikethrough = Some(true); + } + let cs = Cache::new().get(&doc, sid, None); + assert!(text_rules(&cs, &cs.frames[0], true).is_empty(), "no underline now"); + assert_eq!(text_rules(&cs, &cs.frames[0], false).len(), 1, "one strike across the line"); + } +} diff --git a/crates/render/src/text.rs b/crates/render/src/text.rs index 70f31b4f6..a4fa80ad0 100644 --- a/crates/render/src/text.rs +++ b/crates/render/src/text.rs @@ -162,7 +162,7 @@ fn build_line(db: &FontDb, cs: &ComposedStory, l: &Line, vertical: bool) -> Line let mut strikes: Vec<(String, f32, Rect)> = Vec::new(); let mut glyphs = 0; for g in &l.glyphs { - if !g.visible { + if !g.spans_rules() { continue; } let style = &cs.styles[g.style as usize]; @@ -185,6 +185,10 @@ fn build_line(db: &FontDb, cs: &ComposedStory, l: &Line, vertical: bool) -> Line } } } + // A tab carries the rule across its width but draws nothing itself. + if !g.visible { + continue; + } let outline = db.outline(&g.face, g.gid); if outline.elements().is_empty() { continue; @@ -725,3 +729,45 @@ mod cache_identity_tests { assert_eq!(glyphs.lines[0].runs, runs); } } + +#[cfg(test)] +mod rule_tests { + use super::*; + + /// A tab is not drawn, but it is a character with an advance, and InDesign underlines it: + /// forms rule their fill-in lines with underlined tabs (`Name ⇥⇥⇥⇥, city ⇥⇥⇥⇥`). The bars + /// stopped at the last letter before each tab, so an imported form lost every writing line + /// (#1466). + #[test] + fn an_underline_runs_across_a_tab() { + let mut doc = designcraft_doc::Document::new(&Default::default()); + for style in &mut doc.styles_mut().paragraph { + style.chars.underline = Some(true); + } + let layer = doc.default_layer(); + let (_, sid) = doc + .add_text_frame( + designcraft_doc::SpreadRef::Doc(0), + Rect::new(36.0, 36.0, 400.0, 300.0), + layer, + "Name\tCity", + designcraft_doc::ParaFormat::default(), + ) + .unwrap(); + let owner = crate::Cache::new(); + let cs = owner.get(&doc, sid, None); + let line = &cs.frames[0].lines[0]; + let tab = line.glyphs.iter().find(|g| g.rendered_char == '\t').expect("the tab is placed on the line"); + assert!(!tab.visible, "a tab draws nothing itself"); + assert!(tab.adv > 1.0, "it advances to its stop: {}", tab.adv); + + let mut cache = GlyphCache::default(); + cache.tick(); + let glyphs = cache.get(&cs, &cs.frames[0]); + let bars = &glyphs.lines[0].underlines; + assert_eq!(bars.len(), 1, "one bar across the line, not one before the tab and one after: {bars:?}"); + let bar = bars[0].2; + assert!(bar.x0 <= tab.x + 0.5, "the bar starts at the text, not at the tab: {bar:?}"); + assert!(bar.x1 >= tab.x + tab.adv - 0.5, "the bar covers the tab's width: {bar:?}, tab {} + {}", tab.x, tab.adv); + } +}