Skip to main content

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}