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