floem_renderer/text/
layout.rs

1use std::{ops::Range, sync::LazyLock};
2
3use crate::text::AttrsList;
4use cosmic_text::{
5    Affinity, Align, Buffer, BufferLine, Cursor, FontSystem, LayoutCursor, LayoutGlyph, LineEnding,
6    LineIter, Metrics, Scroll, Shaping, Wrap,
7};
8use parking_lot::Mutex;
9use peniko::kurbo::{Point, Size};
10use unicode_segmentation::UnicodeSegmentation;
11
12pub static FONT_SYSTEM: LazyLock<Mutex<FontSystem>> = LazyLock::new(|| {
13    let mut font_system = FontSystem::new();
14    #[cfg(target_os = "macos")]
15    font_system.db_mut().set_sans_serif_family("Helvetica Neue");
16    #[cfg(target_os = "windows")]
17    font_system.db_mut().set_sans_serif_family("Segoe UI");
18    #[cfg(not(any(target_os = "macos", target_os = "windows")))]
19    font_system.db_mut().set_sans_serif_family("Noto Sans");
20    Mutex::new(font_system)
21});
22
23/// A line of visible text for rendering
24#[derive(Debug)]
25pub struct LayoutRun<'a> {
26    /// The index of the original text line
27    pub line_i: usize,
28    /// The original text line
29    pub text: &'a str,
30    /// True if the original paragraph direction is RTL
31    pub rtl: bool,
32    /// The array of layout glyphs to draw
33    pub glyphs: &'a [LayoutGlyph],
34    /// Maximum ascent of the glyphs in line
35    pub max_ascent: f32,
36    /// Maximum descent of the glyphs in line
37    pub max_descent: f32,
38    /// Y offset to baseline of line
39    pub line_y: f32,
40    /// Y offset to top of line
41    pub line_top: f32,
42    /// Y offset to next line
43    pub line_height: f32,
44    /// Width of line
45    pub line_w: f32,
46}
47
48impl LayoutRun<'_> {
49    /// Return the pixel span `Some((x_left, x_width))` of the highlighted area between `cursor_start`
50    /// and `cursor_end` within this run, or None if the cursor range does not intersect this run.
51    /// This may return widths of zero if `cursor_start == cursor_end`, if the run is empty, or if the
52    /// region's left start boundary is the same as the cursor's end boundary or vice versa.
53    pub fn highlight(&self, cursor_start: Cursor, cursor_end: Cursor) -> Option<(f32, f32)> {
54        let mut x_start = None;
55        let mut x_end = None;
56        let rtl_factor = if self.rtl { 1. } else { 0. };
57        let ltr_factor = 1. - rtl_factor;
58        for glyph in self.glyphs.iter() {
59            let cursor = self.cursor_from_glyph_left(glyph);
60            if cursor >= cursor_start && cursor <= cursor_end {
61                if x_start.is_none() {
62                    x_start = Some(glyph.x + glyph.w * rtl_factor);
63                }
64                x_end = Some(glyph.x + glyph.w * rtl_factor);
65            }
66            let cursor = self.cursor_from_glyph_right(glyph);
67            if cursor >= cursor_start && cursor <= cursor_end {
68                if x_start.is_none() {
69                    x_start = Some(glyph.x + glyph.w * ltr_factor);
70                }
71                x_end = Some(glyph.x + glyph.w * ltr_factor);
72            }
73        }
74        if let Some(x_start) = x_start {
75            let x_end = x_end.expect("end of cursor not found");
76            let (x_start, x_end) = if x_start < x_end {
77                (x_start, x_end)
78            } else {
79                (x_end, x_start)
80            };
81            Some((x_start, x_end - x_start))
82        } else {
83            None
84        }
85    }
86
87    fn cursor_from_glyph_left(&self, glyph: &LayoutGlyph) -> Cursor {
88        if self.rtl {
89            Cursor::new_with_affinity(self.line_i, glyph.end, Affinity::Before)
90        } else {
91            Cursor::new_with_affinity(self.line_i, glyph.start, Affinity::After)
92        }
93    }
94
95    pub fn cursor_from_glyph_right(&self, glyph: &LayoutGlyph) -> Cursor {
96        if self.rtl {
97            Cursor::new_with_affinity(self.line_i, glyph.start, Affinity::After)
98        } else {
99            Cursor::new_with_affinity(self.line_i, glyph.end, Affinity::Before)
100        }
101    }
102}
103
104/// An iterator of visible text lines, see [`LayoutRun`]
105#[derive(Debug)]
106pub struct LayoutRunIter<'b> {
107    text_layout: &'b TextLayout,
108    line_i: usize,
109    layout_i: usize,
110    total_height: f32,
111    line_top: f32,
112}
113
114impl<'b> LayoutRunIter<'b> {
115    pub fn new(text_layout: &'b TextLayout) -> Self {
116        Self {
117            text_layout,
118            line_i: text_layout.buffer.scroll().line,
119            layout_i: 0,
120            total_height: 0.0,
121            line_top: 0.0,
122        }
123    }
124}
125
126impl<'b> Iterator for LayoutRunIter<'b> {
127    type Item = LayoutRun<'b>;
128
129    fn next(&mut self) -> Option<Self::Item> {
130        while let Some(line) = self.text_layout.buffer.lines.get(self.line_i) {
131            let shape = line.shape_opt()?;
132            let layout = line.layout_opt()?;
133            while let Some(layout_line) = layout.get(self.layout_i) {
134                self.layout_i += 1;
135
136                let line_height = layout_line
137                    .line_height_opt
138                    .unwrap_or(self.text_layout.buffer.metrics().line_height);
139                self.total_height += line_height;
140
141                let line_top = self.line_top - self.text_layout.buffer.scroll().vertical;
142                let glyph_height = layout_line.max_ascent + layout_line.max_descent;
143                let centering_offset = (line_height - glyph_height) / 2.0;
144                let line_y = line_top + centering_offset + layout_line.max_ascent;
145                if let Some(height) = self.text_layout.height_opt {
146                    if line_y > height {
147                        return None;
148                    }
149                }
150                self.line_top += line_height;
151                if line_y < 0.0 {
152                    continue;
153                }
154
155                return Some(LayoutRun {
156                    line_i: self.line_i,
157                    text: line.text(),
158                    rtl: shape.rtl,
159                    glyphs: &layout_line.glyphs,
160                    max_ascent: layout_line.max_ascent,
161                    max_descent: layout_line.max_descent,
162                    line_y,
163                    line_top,
164                    line_height,
165                    line_w: layout_line.w,
166                });
167            }
168            self.line_i += 1;
169            self.layout_i = 0;
170        }
171
172        None
173    }
174}
175
176pub struct HitPosition {
177    /// Text line the cursor is on
178    pub line: usize,
179    /// Point of the cursor
180    pub point: Point,
181    /// ascent of glyph
182    pub glyph_ascent: f64,
183    /// descent of glyph
184    pub glyph_descent: f64,
185}
186
187pub struct HitPoint {
188    /// Text line the cursor is on
189    pub line: usize,
190    /// First-byte-index of glyph at cursor (will insert behind this glyph)
191    pub index: usize,
192    /// Whether or not the point was inside the bounds of the layout object.
193    ///
194    /// A click outside the layout object will still resolve to a position in the
195    /// text; for instance a click to the right edge of a line will resolve to the
196    /// end of that line, and a click below the last line will resolve to a
197    /// position in that line.
198    pub is_inside: bool,
199    pub affinity: Affinity,
200}
201
202#[derive(Clone, Debug)]
203pub struct TextLayout {
204    buffer: Buffer,
205    lines_range: Vec<Range<usize>>,
206    width_opt: Option<f32>,
207    height_opt: Option<f32>,
208}
209
210impl Default for TextLayout {
211    fn default() -> Self {
212        Self::new()
213    }
214}
215
216impl TextLayout {
217    pub fn new() -> Self {
218        TextLayout {
219            buffer: Buffer::new_empty(Metrics::new(16.0, 16.0)),
220            lines_range: Vec::new(),
221            width_opt: None,
222            height_opt: None,
223        }
224    }
225
226    pub fn new_with_text(text: &str, attrs_list: AttrsList, align: Option<Align>) -> Self {
227        let mut layout = Self::new();
228        layout.set_text(text, attrs_list, align);
229        layout
230    }
231
232    pub fn set_text(&mut self, text: &str, attrs_list: AttrsList, align: Option<Align>) {
233        self.buffer.lines.clear();
234        self.lines_range.clear();
235        let mut attrs_list = attrs_list.0;
236        for (range, ending) in LineIter::new(text) {
237            self.lines_range.push(range.clone());
238            let line_text = &text[range];
239            let new_attrs = attrs_list
240                .clone()
241                .split_off(line_text.len() + ending.as_str().len());
242            let mut line =
243                BufferLine::new(line_text, ending, attrs_list.clone(), Shaping::Advanced);
244            line.set_align(align);
245            self.buffer.lines.push(line);
246            attrs_list = new_attrs;
247        }
248        if self.buffer.lines.is_empty() {
249            let mut line =
250                BufferLine::new("", LineEnding::default(), attrs_list, Shaping::Advanced);
251            line.set_align(align);
252            self.buffer.lines.push(line);
253            self.lines_range.push(0..0)
254        }
255        self.buffer.set_scroll(Scroll::default());
256
257        let mut font_system = FONT_SYSTEM.lock();
258
259        // two-pass layout for alignment to work properly
260        let needs_two_pass =
261            align.is_some() && align != Some(Align::Left) && self.width_opt.is_none();
262        if needs_two_pass {
263            // first pass: shape and layout without width constraint to measure natural width
264            self.buffer.shape_until_scroll(&mut font_system, false);
265
266            // measure the actual width
267            let measured_width = self
268                .buffer
269                .layout_runs()
270                .fold(0.0f32, |width, run| width.max(run.line_w));
271
272            // second pass: set the measured width and layout again
273            if measured_width > 0.0 {
274                self.buffer
275                    .set_size(&mut font_system, Some(measured_width), self.height_opt);
276                // shape again after size change
277                self.buffer.shape_until_scroll(&mut font_system, false);
278            }
279        } else {
280            // For left-aligned text, single pass is sufficient
281            self.buffer.shape_until_scroll(&mut font_system, false);
282        }
283    }
284
285    pub fn set_wrap(&mut self, wrap: Wrap) {
286        let mut font_system = FONT_SYSTEM.lock();
287        self.buffer.set_wrap(&mut font_system, wrap);
288    }
289
290    pub fn set_tab_width(&mut self, tab_width: usize) {
291        let mut font_system = FONT_SYSTEM.lock();
292        self.buffer
293            .set_tab_width(&mut font_system, tab_width as u16);
294    }
295
296    pub fn set_size(&mut self, width: f32, height: f32) {
297        let mut font_system = FONT_SYSTEM.lock();
298        self.width_opt = Some(width);
299        self.height_opt = Some(height);
300        self.buffer
301            .set_size(&mut font_system, Some(width), Some(height));
302    }
303
304    pub fn metrics(&self) -> Metrics {
305        self.buffer.metrics()
306    }
307
308    pub fn lines(&self) -> &[BufferLine] {
309        &self.buffer.lines
310    }
311
312    pub fn lines_range(&self) -> &[Range<usize>] {
313        &self.lines_range
314    }
315
316    pub fn layout_runs(&self) -> LayoutRunIter<'_> {
317        LayoutRunIter::new(self)
318    }
319
320    pub fn layout_cursor(&mut self, cursor: Cursor) -> LayoutCursor {
321        let line = cursor.line;
322        let mut font_system = FONT_SYSTEM.lock();
323        self.buffer
324            .layout_cursor(&mut font_system, cursor)
325            .unwrap_or_else(|| LayoutCursor::new(line, 0, 0))
326    }
327
328    pub fn hit_position(&self, idx: usize) -> HitPosition {
329        let mut last_line = 0;
330        let mut last_end: usize = 0;
331        let mut offset = 0;
332        let mut last_glyph_width = 0.0;
333        let mut last_position = HitPosition {
334            line: 0,
335            point: Point::ZERO,
336            glyph_ascent: 0.0,
337            glyph_descent: 0.0,
338        };
339        for (line, run) in self.layout_runs().enumerate() {
340            if run.line_i > last_line {
341                last_line = run.line_i;
342                offset += last_end + 1;
343            }
344            for glyph in run.glyphs {
345                last_end = glyph.end;
346                last_glyph_width = glyph.w;
347                last_position = HitPosition {
348                    line,
349                    point: Point::new(glyph.x as f64, run.line_y as f64),
350                    glyph_ascent: run.max_ascent as f64,
351                    glyph_descent: run.max_descent as f64,
352                };
353                if (glyph.start + offset..=glyph.end + offset).contains(&idx) {
354                    // possibly inside ligature, need to resolve glyph internal offset
355
356                    let glyph_str = &run.text[glyph.start..glyph.end];
357                    let relative_idx = idx - offset - glyph.start;
358                    let mut total_graphemes = 0;
359                    let mut grapheme_i = 0;
360
361                    for (i, _) in glyph_str.grapheme_indices(true) {
362                        if relative_idx > i {
363                            grapheme_i += 1;
364                        }
365
366                        total_graphemes += 1;
367                    }
368
369                    if glyph.level.is_rtl() {
370                        grapheme_i = total_graphemes - grapheme_i;
371                    }
372
373                    last_position.point.x +=
374                        (grapheme_i as f64 / total_graphemes as f64) * glyph.w as f64;
375
376                    return last_position;
377                }
378            }
379        }
380
381        if idx > 0 {
382            last_position.point.x += last_glyph_width as f64;
383            return last_position;
384        }
385
386        HitPosition {
387            line: 0,
388            point: Point::ZERO,
389            glyph_ascent: 0.0,
390            glyph_descent: 0.0,
391        }
392    }
393
394    pub fn hit_point(&self, point: Point) -> HitPoint {
395        if let Some(cursor) = self.hit(point.x as f32, point.y as f32) {
396            let size = self.size();
397            let is_inside = point.x <= size.width && point.y <= size.height;
398            HitPoint {
399                line: cursor.line,
400                index: cursor.index,
401                is_inside,
402                affinity: cursor.affinity,
403            }
404        } else {
405            HitPoint {
406                line: 0,
407                index: 0,
408                is_inside: false,
409                affinity: Affinity::Before,
410            }
411        }
412    }
413
414    /// Convert x, y position to Cursor (hit detection)
415    pub fn hit(&self, x: f32, y: f32) -> Option<Cursor> {
416        self.buffer.hit(x, y)
417    }
418
419    pub fn line_col_position(&self, line: usize, col: usize) -> HitPosition {
420        let mut last_glyph: Option<&LayoutGlyph> = None;
421        let mut last_line = 0;
422        let mut last_line_y = 0.0;
423        let mut last_glyph_ascent = 0.0;
424        let mut last_glyph_descent = 0.0;
425        for (current_line, run) in self.layout_runs().enumerate() {
426            for glyph in run.glyphs {
427                match run.line_i.cmp(&line) {
428                    std::cmp::Ordering::Equal => {
429                        if glyph.start > col {
430                            return HitPosition {
431                                line: last_line,
432                                point: Point::new(
433                                    last_glyph.map(|g| (g.x + g.w) as f64).unwrap_or(0.0),
434                                    last_line_y as f64,
435                                ),
436                                glyph_ascent: last_glyph_ascent as f64,
437                                glyph_descent: last_glyph_descent as f64,
438                            };
439                        }
440                        if (glyph.start..glyph.end).contains(&col) {
441                            return HitPosition {
442                                line: current_line,
443                                point: Point::new(glyph.x as f64, run.line_y as f64),
444                                glyph_ascent: run.max_ascent as f64,
445                                glyph_descent: run.max_descent as f64,
446                            };
447                        }
448                    }
449                    std::cmp::Ordering::Greater => {
450                        return HitPosition {
451                            line: last_line,
452                            point: Point::new(
453                                last_glyph.map(|g| (g.x + g.w) as f64).unwrap_or(0.0),
454                                last_line_y as f64,
455                            ),
456                            glyph_ascent: last_glyph_ascent as f64,
457                            glyph_descent: last_glyph_descent as f64,
458                        };
459                    }
460                    std::cmp::Ordering::Less => {}
461                };
462                last_glyph = Some(glyph);
463            }
464            last_line = current_line;
465            last_line_y = run.line_y;
466            last_glyph_ascent = run.max_ascent;
467            last_glyph_descent = run.max_descent;
468        }
469
470        HitPosition {
471            line: last_line,
472            point: Point::new(
473                last_glyph.map(|g| (g.x + g.w) as f64).unwrap_or(0.0),
474                last_line_y as f64,
475            ),
476            glyph_ascent: last_glyph_ascent as f64,
477            glyph_descent: last_glyph_descent as f64,
478        }
479    }
480
481    pub fn size(&self) -> Size {
482        self.buffer
483            .layout_runs()
484            .fold(Size::new(0.0, 0.0), |mut size, run| {
485                let new_width = run.line_w as f64;
486                if new_width > size.width {
487                    size.width = new_width;
488                }
489
490                size.height += run.line_height as f64;
491
492                size
493            })
494    }
495}