Merge pull request #1491 from Hamza0101/fix/underline-under-tabs

An underline runs across a tab, as the rest of the line's text (#1466)
This commit is contained in:
Brandon Thomas authored and GitHub committed 2026-10-23 17:29:54 -04:00
commit 807d9b5723
3 files changed
+98 -2

No files matched your search

+14
View File
@@ -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
+37 -1
View File
@@ -160,7 +160,7 @@ fn cluster_chars(members: &[PlacedGlyph], cluster: &str) -> Option<Vec<(usize, c
fn text_rules(cs: &ComposedStory, ft: &FrameText, underline: bool) -> 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");
}
}
+47 -1
View File
@@ -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);
}
}