diff --git a/crates/text/src/lib.rs b/crates/text/src/lib.rs index 74ba787..3df5a74 100644 --- a/crates/text/src/lib.rs +++ b/crates/text/src/lib.rs @@ -1112,7 +1112,12 @@ fn break_opportunities(text: &str, latin_break: bool) -> (Vec, Vec) let chars: Vec = text.chars().collect(); for (i, w) in chars.windows(2).enumerate() { let (prev, next) = (w[0], w[1]); - let keep = if is_hangul(prev) && is_hangul(next) { false } else { latin_break || !matches!(prev, '/' | '\\') || next.is_whitespace() }; + // PowerPoint never breaks after a zero-width space, nor before an em dash (only after it). + let keep = if (is_hangul(prev) && is_hangul(next)) || prev == '\u{200B}' || next == '\u{2014}' { + false + } else { + latin_break || !matches!(prev, '/' | '\\') || next.is_whitespace() + }; if !keep && !mandatory.get(i + 1).copied().unwrap_or(false) && let Some(a) = allowed.get_mut(i + 1) @@ -1461,8 +1466,12 @@ fn layout_scaled(ctx: &Ctx, shape: &Shape, body: &TextBody, bp: &BodyProps, opts // Logical advances; tabs measure from the start edge. let mut adv = Vec::with_capacity(n); let mut x = 0.0; + // A line that breaks at a soft hyphen ends with a visible hyphen. + let shy_end = li + 1 < raw.len() && line_cells.last().is_some_and(|c| c.ch == '\u{AD}'); for (k, c) in line_cells.iter().enumerate() { - let a = if c.ch == '\t' { + let a = if shy_end && k + 1 == n { + p.faces.get(c.face).zip(p.styles.get(c.style)).map(|(f, st)| measure_str(f, st.size, "-").0).unwrap_or(0.0) + } else if c.ch == '\t' { tab_advance(x0 + x, &tabs, props.default_tab.unwrap_or(72.0), tab_segment(&p.cells, rl.start + k)) } else { c.adv @@ -1694,7 +1703,13 @@ fn layout_scaled(ctx: &Ctx, shape: &Shape, body: &TextBody, bp: &BodyProps, opts } if let Some(r) = cur.as_mut() { r.chars = (r.chars.0.min(ci), r.chars.1.max(ci + 1)); - if cell.head == ci && !cell.glyphs.is_empty() { + if cell.ch == '\u{AD}' && cx1 - cx > 0.01 { + // The soft hyphen a line breaks at is drawn as a hyphen. + if let Some(f) = p.faces.get(cell.face) { + r.glyphs.push((f.glyph_for('-'), cx, base_y)); + r.cells.push((ci, cell.ch)); + } + } else if cell.head == ci && !cell.glyphs.is_empty() { // The cluster's glyphs start at its leftmost cell; kashida (right-to-left // only) fills the gap on the cluster's left with tatweel glyphs. let cells_of = diff --git a/crates/text/src/tests.rs b/crates/text/src/tests.rs index 1dc955e..8613e02 100644 --- a/crates/text/src/tests.rs +++ b/crates/text/src/tests.rs @@ -978,3 +978,38 @@ fn underlines_sit_at_the_font_underline_in_their_style() { let (_, words) = underline_decos("words"); assert_eq!(words.len(), 1); } + +#[test] +fn no_break_after_a_zero_width_space_or_before_an_em_dash() { + let (allowed, _) = break_opportunities("ab\u{200B}cd", false); + assert!(!allowed[3], "PowerPoint does not break after a zero-width space"); + let (allowed, _) = break_opportunities("ab\u{2014}cd", false); + assert!(!allowed[2] && allowed[3], "an em dash ends a line, never starts one"); +} + +#[test] +fn a_line_broken_at_a_soft_hyphen_shows_a_hyphen() { + let (p, mut sh) = setup("Soft\u{AD}hyphenated\u{AD}words", 150.0, 300.0); + let para = &mut sh.text.as_mut().unwrap().paragraphs[0]; + para.props.bullet = Some(Bullet::None); + para.runs[0].props.size = Some(24.0); + let l = lay(&p, &sh, 150.0, 300.0); + assert!(l.lines.len() >= 2); + let first = &l.lines[0]; + let shy = first.end - 1; + assert_eq!(first.end, 5, "breaks after the soft hyphen: {:?}", l.lines.iter().map(|x| (x.start, x.end)).collect::>()); + let (x0, x1) = first.edges[shy - first.start]; + assert!(x1 - x0 > 2.0, "the hyphen takes room"); + let r = l.runs.iter().find(|r| r.cells.iter().any(|c| c.0 == shy)).expect("a glyph for the soft hyphen"); + assert_eq!(r.glyphs[r.cells.iter().position(|c| c.0 == shy).unwrap()].0, r.face.glyph_for('-')); + // Soft hyphens draw only where a line ends; in a wide box, nowhere. + let ends: Vec = l.lines.iter().map(|x| x.end.saturating_sub(1)).collect(); + for r in &l.runs { + for (g, c) in r.glyphs.iter().zip(&r.cells) { + assert!(c.1 != '\u{AD}' || ends.contains(&c.0) || g.0 != r.face.glyph_for('-'), "mid-line soft hyphen drawn"); + } + } + let wide = lay(&p, &sh, 2000.0, 300.0); + assert_eq!(wide.lines.len(), 1); + assert!(wide.runs.iter().all(|r| r.cells.iter().all(|c| c.1 != '\u{AD}' || r.glyphs.iter().all(|g| g.0 != r.face.glyph_for('-'))))); +}