text_typeset/layout/line.rs
1use std::ops::Range;
2
3use crate::shaping::run::ShapedRun;
4use crate::shaping::shaper::TextDirection;
5
6#[derive(Clone)]
7pub struct LayoutLine {
8 pub runs: Vec<PositionedRun>,
9 /// Baseline y relative to block top (set by block layout).
10 pub y: f32,
11 pub ascent: f32,
12 pub descent: f32,
13 pub leading: f32,
14 /// Total line height: ascent + descent + leading.
15 pub line_height: f32,
16 /// Actual content width (sum of run advances).
17 pub width: f32,
18 /// Character range in the block's text.
19 pub char_range: Range<usize>,
20}
21
22impl LayoutLine {
23 /// End (exclusive) of the cluster starting at char offset `cluster`:
24 /// the smallest distinct glyph cluster in the line strictly greater
25 /// than `cluster`, or the line's `char_range.end` if none is larger.
26 ///
27 /// Clusters across the whole line are the complete set of logical
28 /// char-offset boundaries, so the next-larger one is exactly where
29 /// `cluster`'s char span ends — true for LTR, RTL, and multi-char
30 /// (ligature) clusters alike.
31 pub(crate) fn cluster_end(&self, cluster: usize) -> usize {
32 let mut best: Option<usize> = None;
33 for run in &self.runs {
34 for g in &run.shaped_run.glyphs {
35 let c = g.cluster as usize;
36 if c > cluster {
37 best = Some(best.map_or(c, |b| b.min(c)));
38 }
39 }
40 }
41 best.unwrap_or(self.char_range.end)
42 }
43
44 /// Find the x coordinate for a char offset within this line.
45 ///
46 /// Builds the line's caret stops `(logical_offset, x)` in visual
47 /// order — direction-aware, so an RTL run's caret for its lowest
48 /// offset sits at its rightmost edge — then returns the x of the stop
49 /// matching `offset` (nearest by offset if there's no exact match).
50 pub fn x_for_offset(&self, offset: usize) -> f32 {
51 let stops = self.caret_stops();
52 if stops.is_empty() {
53 return 0.0;
54 }
55 // Exact match first (earliest/leftmost stop wins on ties).
56 if let Some((_, x)) = stops.iter().find(|(o, _)| *o == offset) {
57 return *x;
58 }
59 // No exact stop (e.g. offset inside a multi-char cluster): snap to
60 // the nearest stop by logical distance.
61 stops
62 .iter()
63 .min_by_key(|(o, _)| o.abs_diff(offset))
64 .map(|(_, x)| *x)
65 .unwrap_or(0.0)
66 }
67
68 /// Caret stops `(logical char offset, x)` across the line in visual
69 /// (left-to-right) order. For an LTR run each glyph contributes a stop
70 /// at its left edge (the glyph's own cluster) plus a trailing stop at
71 /// the run's right edge; for an RTL run the leftmost edge is the
72 /// trailing (highest) offset and each glyph's right edge is its own
73 /// (leading) offset.
74 fn caret_stops(&self) -> Vec<(usize, f32)> {
75 let mut stops: Vec<(usize, f32)> = Vec::new();
76 for run in &self.runs {
77 let glyphs = &run.shaped_run.glyphs;
78 if glyphs.is_empty() {
79 continue;
80 }
81 let mut gx = run.x;
82 if run.shaped_run.direction == TextDirection::RightToLeft {
83 // Leftmost edge: caret after the last logical char in the run.
84 stops.push((self.cluster_end(glyphs[0].cluster as usize), gx));
85 for g in glyphs {
86 gx += g.x_advance;
87 stops.push((g.cluster as usize, gx));
88 }
89 } else {
90 for g in glyphs {
91 stops.push((g.cluster as usize, gx));
92 gx += g.x_advance;
93 }
94 // Rightmost edge: caret after the last logical char in the run.
95 let last = glyphs.last().map(|g| g.cluster as usize).unwrap_or(0);
96 stops.push((self.cluster_end(last), gx));
97 }
98 }
99 stops
100 }
101}
102
103#[derive(Clone)]
104pub struct PositionedRun {
105 pub shaped_run: ShapedRun,
106 /// X offset from the left edge of the content area.
107 pub x: f32,
108 /// Decoration flags for this run.
109 pub decorations: RunDecorations,
110}
111
112/// Text decoration flags and metadata carried from the source TextFormat.
113#[derive(Clone, Debug, Default)]
114pub struct RunDecorations {
115 pub underline_style: crate::types::UnderlineStyle,
116 pub overline: bool,
117 pub strikeout: bool,
118 pub is_link: bool,
119 /// Text foreground color (RGBA). None means default (black).
120 pub foreground_color: Option<[f32; 4]>,
121 /// Underline color (RGBA). None means use foreground_color.
122 pub underline_color: Option<[f32; 4]>,
123 /// Text-level background highlight color (RGBA). None means transparent.
124 pub background_color: Option<[f32; 4]>,
125 /// Hyperlink destination URL.
126 pub anchor_href: Option<String>,
127 /// Tooltip text.
128 pub tooltip: Option<String>,
129 /// Vertical alignment (normal, superscript, subscript).
130 pub vertical_alignment: crate::types::VerticalAlignment,
131}