floem_renderer/text/
layout.rs

1use std::{ops::Range, sync::LazyLock};
2
3use crate::text::AttrsList;
4use cosmic_text::{
5    Affinity, 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().as_ref()?;
131            let layout = line.layout_opt().as_ref()?;
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) -> Self {
225        let mut layout = Self::new();
226        layout.set_text(text, attrs_list);
227        layout
228    }
229
230    pub fn set_text(&mut self, text: &str, attrs_list: AttrsList) {
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            self.buffer.lines.push(BufferLine::new(
241                line_text,
242                ending,
243                attrs_list.clone(),
244                Shaping::Advanced,
245            ));
246            attrs_list = new_attrs;
247        }
248        if self.buffer.lines.is_empty() {
249            self.buffer.lines.push(BufferLine::new(
250                "",
251                LineEnding::default(),
252                attrs_list,
253                Shaping::Advanced,
254            ));
255            self.lines_range.push(0..0)
256        }
257        self.buffer.set_scroll(Scroll::default());
258        let mut font_system = FONT_SYSTEM.lock();
259        self.buffer.shape_until_scroll(&mut font_system, false);
260    }
261
262    pub fn set_wrap(&mut self, wrap: Wrap) {
263        let mut font_system = FONT_SYSTEM.lock();
264        self.buffer.set_wrap(&mut font_system, wrap);
265    }
266
267    pub fn set_tab_width(&mut self, tab_width: usize) {
268        let mut font_system = FONT_SYSTEM.lock();
269        self.buffer
270            .set_tab_width(&mut font_system, tab_width as u16);
271    }
272
273    pub fn set_size(&mut self, width: f32, height: f32) {
274        let mut font_system = FONT_SYSTEM.lock();
275        self.width_opt = Some(width);
276        self.height_opt = Some(height);
277        self.buffer
278            .set_size(&mut font_system, Some(width), Some(height));
279    }
280
281    pub fn metrics(&self) -> Metrics {
282        self.buffer.metrics()
283    }
284
285    pub fn lines(&self) -> &[BufferLine] {
286        &self.buffer.lines
287    }
288
289    pub fn lines_range(&self) -> &[Range<usize>] {
290        &self.lines_range
291    }
292
293    pub fn layout_runs(&self) -> LayoutRunIter {
294        LayoutRunIter::new(self)
295    }
296
297    pub fn layout_cursor(&mut self, cursor: Cursor) -> LayoutCursor {
298        let line = cursor.line;
299        let mut font_system = FONT_SYSTEM.lock();
300        self.buffer
301            .layout_cursor(&mut font_system, cursor)
302            .unwrap_or_else(|| LayoutCursor::new(line, 0, 0))
303    }
304
305    pub fn hit_position(&self, idx: usize) -> HitPosition {
306        let mut last_line = 0;
307        let mut last_end: usize = 0;
308        let mut offset = 0;
309        let mut last_glyph_width = 0.0;
310        let mut last_position = HitPosition {
311            line: 0,
312            point: Point::ZERO,
313            glyph_ascent: 0.0,
314            glyph_descent: 0.0,
315        };
316        for (line, run) in self.layout_runs().enumerate() {
317            if run.line_i > last_line {
318                last_line = run.line_i;
319                offset += last_end + 1;
320            }
321            for glyph in run.glyphs {
322                if glyph.start + offset > idx {
323                    last_position.point.x += last_glyph_width as f64;
324                    return last_position;
325                }
326                last_end = glyph.end;
327                last_glyph_width = glyph.w;
328                last_position = HitPosition {
329                    line,
330                    point: Point::new(glyph.x as f64, run.line_y as f64),
331                    glyph_ascent: run.max_ascent as f64,
332                    glyph_descent: run.max_descent as f64,
333                };
334                if (glyph.start + offset..glyph.end + offset).contains(&idx) {
335                    return last_position;
336                }
337            }
338        }
339
340        if idx > 0 {
341            last_position.point.x += last_glyph_width as f64;
342            return last_position;
343        }
344
345        HitPosition {
346            line: 0,
347            point: Point::ZERO,
348            glyph_ascent: 0.0,
349            glyph_descent: 0.0,
350        }
351    }
352
353    pub fn hit_point(&self, point: Point) -> HitPoint {
354        if let Some(cursor) = self.hit(point.x as f32, point.y as f32) {
355            let size = self.size();
356            let is_inside = point.x <= size.width && point.y <= size.height;
357            HitPoint {
358                line: cursor.line,
359                index: cursor.index,
360                is_inside,
361            }
362        } else {
363            HitPoint {
364                line: 0,
365                index: 0,
366                is_inside: false,
367            }
368        }
369    }
370
371    /// Convert x, y position to Cursor (hit detection)
372    pub fn hit(&self, x: f32, y: f32) -> Option<Cursor> {
373        self.buffer.hit(x, y)
374    }
375
376    pub fn line_col_position(&self, line: usize, col: usize) -> HitPosition {
377        let mut last_glyph: Option<&LayoutGlyph> = None;
378        let mut last_line = 0;
379        let mut last_line_y = 0.0;
380        let mut last_glyph_ascent = 0.0;
381        let mut last_glyph_descent = 0.0;
382        for (current_line, run) in self.layout_runs().enumerate() {
383            for glyph in run.glyphs {
384                match run.line_i.cmp(&line) {
385                    std::cmp::Ordering::Equal => {
386                        if glyph.start > col {
387                            return HitPosition {
388                                line: last_line,
389                                point: Point::new(
390                                    last_glyph.map(|g| (g.x + g.w) as f64).unwrap_or(0.0),
391                                    last_line_y as f64,
392                                ),
393                                glyph_ascent: last_glyph_ascent as f64,
394                                glyph_descent: last_glyph_descent as f64,
395                            };
396                        }
397                        if (glyph.start..glyph.end).contains(&col) {
398                            return HitPosition {
399                                line: current_line,
400                                point: Point::new(glyph.x as f64, run.line_y as f64),
401                                glyph_ascent: run.max_ascent as f64,
402                                glyph_descent: run.max_descent as f64,
403                            };
404                        }
405                    }
406                    std::cmp::Ordering::Greater => {
407                        return HitPosition {
408                            line: last_line,
409                            point: Point::new(
410                                last_glyph.map(|g| (g.x + g.w) as f64).unwrap_or(0.0),
411                                last_line_y as f64,
412                            ),
413                            glyph_ascent: last_glyph_ascent as f64,
414                            glyph_descent: last_glyph_descent as f64,
415                        };
416                    }
417                    std::cmp::Ordering::Less => {}
418                };
419                last_glyph = Some(glyph);
420            }
421            last_line = current_line;
422            last_line_y = run.line_y;
423            last_glyph_ascent = run.max_ascent;
424            last_glyph_descent = run.max_descent;
425        }
426
427        HitPosition {
428            line: last_line,
429            point: Point::new(
430                last_glyph.map(|g| (g.x + g.w) as f64).unwrap_or(0.0),
431                last_line_y as f64,
432            ),
433            glyph_ascent: last_glyph_ascent as f64,
434            glyph_descent: last_glyph_descent as f64,
435        }
436    }
437
438    pub fn size(&self) -> Size {
439        self.buffer
440            .layout_runs()
441            .fold(Size::new(0.0, 0.0), |mut size, run| {
442                let new_width = run.line_w as f64;
443                if new_width > size.width {
444                    size.width = new_width;
445                }
446
447                size.height += run.line_height as f64;
448
449                size
450            })
451    }
452}