diff --git a/crates/svg/tests/it/text_layout.rs b/crates/svg/tests/it/text_layout.rs index 534d24ebcc..25d4d27d68 100644 --- a/crates/svg/tests/it/text_layout.rs +++ b/crates/svg/tests/it/text_layout.rs @@ -164,14 +164,19 @@ fn the_space_before_the_first_character_on_a_path_is_its_dx() { } /// Insert Aki: each character the layout moves by its Aki starts a `` where the character is -/// drawn, so a reader draws the text where the canvas does, with fewer ``s asked for too. -/// On a path the type is outlined. +/// drawn, so a reader draws the text where the canvas does, on left-aligned, centered and +/// right-aligned lines and with fewer ``s asked for too. On a path the type is outlined. #[test] fn insert_aki_places_each_moved_character() { // 250/1000 em of 12 pt before the O (3 pt) and 500/1000 after it (6 pt), then a run with Aki // after every character, and a second line. let spaced = CharStyle { aki_left: 250.0, aki_right: 500.0, ..style(12.0) }; let runs = vec![run("H", style(12.0)), run("O", spaced), run("H ", style(12.0)), run("OHO\nHO", CharStyle { aki_right: 500.0, ..style(12.0) })]; + // A centered or right-aligned line whose characters the Aki moves is placed segment by + // segment, not anchored. + for justify in [Justify::Center, Justify::Right] { + assert_drawn_in_place(&area_doc(runs.clone(), 200.0, justify)); + } let d = area_doc(runs, 200.0, Justify::Left); assert_drawn_in_place(&d); let plain = tspans(&export(&area_doc(vec![run("HOH OHO\nHO", style(12.0))], 200.0, Justify::Left), &ExportOptions::default())).len(); diff --git a/crates/text/src/layout.rs b/crates/text/src/layout.rs index 208871b300..cbe7aaf394 100644 --- a/crates/text/src/layout.rs +++ b/crates/text/src/layout.rs @@ -337,6 +337,19 @@ impl Ctx<'_> { n } + /// Does glyph `g`'s own width start where its placement sets it ([`PositionedGlyph::width_start`])? + /// Not for an inline graphic, nor in vertical type for an upright or tate-chu-yoko glyph, which + /// start their cell at the start of their advance. + fn places_own_width(&self, g: &SGlyph) -> bool { + g.inline.is_none() && !(self.vertical && (g.tcy.is_some() || (takes_cell(g) && stands_upright(g)))) + } + + /// How far glyph `g`'s own width moves when the glyph is drawn `shift` further along its + /// advance: `shift` when its placement sets where that width starts, otherwise 0. + fn own_width_shift(&self, g: &SGlyph, shift: f64) -> f64 { + if self.places_own_width(g) { shift } else { 0.0 } + } + fn emit(&mut self, g: &SGlyph, pre: Affine, origin: Point, angle: f64, advance: f64, line: usize) { // Only the glyph right after an upright glyph can continue its cluster. let cluster = self.upright_cluster.take(); @@ -540,7 +553,7 @@ impl Ctx<'_> { let [a, b, ..] = pre.as_coeffs(); a.hypot(b) }; - let width_start = if g.inline.is_none() && !upright && !(self.vertical && g.tcy.is_some()) { + let width_start = if self.places_own_width(g) { Some((g.kern_shift + g.dx - g.lead) * along).filter(|s| s.is_finite()).unwrap_or(0.0) } else if upright { Some(g.kern_shift * along).filter(|s| s.is_finite()).unwrap_or(0.0) diff --git a/crates/text/src/layout/flow.rs b/crates/text/src/layout/flow.rs index d72dd246ef..21ab10c6e6 100644 --- a/crates/text/src/layout/flow.rs +++ b/crates/text/src/layout/flow.rs @@ -1031,12 +1031,9 @@ fn emit_flow_line(cx: &mut Ctx<'_>, pen: &mut Pen<'_>, paragraph: &ParagraphData advance += kashida.unwrap_or(0.0); let y = line.position.baseline - align_shift(glyph, cx.style_at(glyph.byte).char_align, line_em, cx.vertical); // A glyph scaled to fill the line keeps its Insert Aki before it at its width. - let transform = if scale != 1.0 { - let aki = if glyph.aki_shift.is_finite() { glyph.aki_shift * (1.0 - scale) } else { 0.0 }; - Affine::translate((x + aki, y)) * Affine::scale_non_uniform(scale, 1.0) - } else { - Affine::translate((x, y)) - }; + let aki = if scale != 1.0 && glyph.aki_shift.is_finite() { glyph.aki_shift * (1.0 - scale) } else { 0.0 }; + let transform = + if scale != 1.0 { Affine::translate((x + aki, y)) * Affine::scale_non_uniform(scale, 1.0) } else { Affine::translate((x, y)) }; if glyph_index < paragraph.marker { let at = cx.out.glyphs.len(); marker_glyphs = Some(marker_glyphs.map_or(at..at + 1, |range: Range| range.start.min(at)..range.end.max(at + 1))); @@ -1045,8 +1042,11 @@ fn emit_flow_line(cx: &mut Ctx<'_>, pen: &mut Pen<'_>, paragraph: &ParagraphData } } cx.emit(glyph, transform, Point::new(x, y), 0.0, advance, line_index); + // Its own width starts where the glyph is drawn, with the Aki kept at its width. + let own_shift = cx.own_width_shift(glyph, aki); if let Some(out) = cx.out.glyphs.last_mut() { out.scale = paragraph.flex.get(glyph_index).map_or(1.0, |flex| flex.scale) * scale; + out.width_start += own_shift; } if !leader.is_empty() { // The leader belongs to the tab's glyph: the gap stays one character for the diff --git a/crates/text/src/shape.rs b/crates/text/src/shape.rs index 83ff986bd3..c8c460aa3b 100644 --- a/crates/text/src/shape.rs +++ b/crates/text/src/shape.rs @@ -807,7 +807,8 @@ fn optical_pairs(db: &FontDb, text: &str, glyphs: &mut [SGlyph], runs: &[(Range< let mut cache = HashMap::new(); let mut corrections: HashMap<[u64; 15], Option> = HashMap::new(); let mut references: HashMap> = HashMap::new(); - let shaped_advances: Vec = glyphs.iter().map(|g| g.adv).collect(); + // The Insert Aki a cluster's last glyph holds does not make a mark its base. + let shaped_advances: Vec = glyphs.iter().map(|g| g.adv - g.aki).collect(); for i in 0..glyphs.len().saturating_sub(1) { let Some((a, b)) = glyphs.get(i).zip(glyphs.get(i + 1)) else { continue }; if a.level != b.level { diff --git a/crates/text/src/tests/aki.rs b/crates/text/src/tests/aki.rs index 9eac47e560..eab94cb2b1 100644 --- a/crates/text/src/tests/aki.rs +++ b/crates/text/src/tests/aki.rs @@ -66,6 +66,12 @@ fn optical_kerning_spaces_the_pair_as_without_the_aki() { assert!(near(&moved, &[0.0, 25.0, 25.0]), "{moved:?}"); let moved = moves(&optical, &with(&optical, 0, |s| s.aki_right = 250.0), &[0, 1, 2]); assert!(near(&moved, &[0.0, 25.0, 25.0]), "{moved:?}"); + // A letter with a combining mark: the pair is measured from the letter, not from the mark + // whose advance holds the cluster's Aki. + let marked = point("H\u{301}V", CharStyle { optical_kerning: true, ..style(100.0) }); + let after = "H\u{301}".len(); + let moved = moves(&marked, &with(&marked, 0, |s| s.aki_right = 1000.0), &[0, after]); + assert!(near(&moved, &[0.0, 100.0]), "{moved:?}"); } #[test] @@ -112,6 +118,15 @@ fn justification_does_not_stretch_or_narrow_the_aki() { assert!(near(&moved, &[0.0, 25.0, 25.0]), "{moved:?}"); let moved = moves(&scaled, &with(&scaled, 1, |s| s.aki_left = 250.0), &[1, 2]); assert!(near(&moved, &[25.0, 25.0]), "{moved:?}"); + // A glyph that justification widens to fill the line keeps the Aki before it at its width, + // and its own width, which ligature carets stand in, starts where the glyph is drawn. + let mut filled = area("HOH", style(100.0), Rect::new(0.0, 0.0, 600.0, 400.0), Justify::JustifyAll); + filled.para.justification.glyph_scaling = [100.0, 100.0, 300.0]; + let l = layout(db(), &with(&filled, 1, |s| s.aki_left = 250.0)); + let g = l.glyphs.iter().find(|g| g.byte == 1).unwrap(); + assert!(g.scale > 1.0 + 1e-6, "the glyph is widened: {}", g.scale); + let drawn = g.xf.as_coeffs()[4]; + assert!((g.origin.x + g.width_start - drawn).abs() < 1e-6, "{} {} {drawn}", g.origin.x, g.width_start); let mut spaced = point("H H", style(100.0)); spaced.para.justification.word_spacing = [200.0, 200.0, 200.0]; let moved = moves(&spaced, &with(&spaced, 1, |s| s.aki_right = 250.0), &[0, 2]);