floem_cosmic_text/
buffer.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3#[cfg(not(feature = "std"))]
4use alloc::{
5    string::{String, ToString},
6    vec::Vec,
7};
8use core::{cmp, fmt};
9use peniko::kurbo::{Point, Size};
10use unicode_segmentation::UnicodeSegmentation;
11
12use crate::{Attrs, AttrsList, LayoutGlyph, LayoutLine, ShapeLine, TextLayoutLine, Wrap};
13#[cfg(feature = "swash")]
14use peniko::Color;
15
16pub struct HitPoint {
17    /// Text line the cursor is on
18    pub line: usize,
19    /// First-byte-index of glyph at cursor (will insert behind this glyph)
20    pub index: usize,
21    /// Whether or not the point was inside the bounds of the layout object.
22    ///
23    /// A click outside the layout object will still resolve to a position in the
24    /// text; for instance a click to the right edge of a line will resolve to the
25    /// end of that line, and a click below the last line will resolve to a
26    /// position in that line.
27    pub is_inside: bool,
28}
29
30pub struct HitPosition {
31    /// Text line the cursor is on
32    pub line: usize,
33    /// Point of the cursor
34    pub point: Point,
35    /// ascent of glyph
36    pub glyph_ascent: f64,
37    /// descent of glyph
38    pub glyph_descent: f64,
39}
40
41/// Current cursor location
42#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Ord, PartialOrd)]
43pub struct Cursor {
44    /// Text line the cursor is on
45    pub line: usize,
46    /// First-byte-index of glyph at cursor (will insert behind this glyph)
47    pub index: usize,
48    /// Whether to associate the cursor with the run before it or the run after it if placed at the
49    /// boundary between two runs
50    pub affinity: Affinity,
51}
52
53impl Cursor {
54    /// Create a new cursor
55    pub const fn new(line: usize, index: usize) -> Self {
56        Self::new_with_affinity(line, index, Affinity::Before)
57    }
58
59    /// Create a new cursor, specifying the affinity
60    pub const fn new_with_affinity(line: usize, index: usize, affinity: Affinity) -> Self {
61        Self {
62            line,
63            index,
64            affinity,
65        }
66    }
67}
68
69/// Whether to associate cursors placed at a boundary between runs with the run before or after it.
70#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
71pub enum Affinity {
72    Before,
73    After,
74}
75
76impl Affinity {
77    pub fn before(&self) -> bool {
78        *self == Self::Before
79    }
80
81    pub fn after(&self) -> bool {
82        *self == Self::After
83    }
84
85    pub fn from_before(before: bool) -> Self {
86        if before {
87            Self::Before
88        } else {
89            Self::After
90        }
91    }
92
93    pub fn from_after(after: bool) -> Self {
94        if after {
95            Self::After
96        } else {
97            Self::Before
98        }
99    }
100}
101
102impl Default for Affinity {
103    fn default() -> Self {
104        Affinity::Before
105    }
106}
107
108/// The position of a cursor within a [`Buffer`].
109pub struct LayoutCursor {
110    pub line: usize,
111    pub layout: usize,
112    pub glyph: usize,
113}
114
115impl LayoutCursor {
116    pub fn new(line: usize, layout: usize, glyph: usize) -> Self {
117        Self {
118            line,
119            layout,
120            glyph,
121        }
122    }
123}
124
125/// A line of visible text for rendering
126pub struct LayoutRun<'a> {
127    /// The index of the original text line
128    pub line_i: usize,
129    /// The original text line
130    pub text: &'a str,
131    /// True if the original paragraph direction is RTL
132    pub rtl: bool,
133    /// The array of layout glyphs to draw
134    pub glyphs: &'a [LayoutGlyph],
135    /// Y offset of line
136    pub line_y: f32,
137    /// width of line
138    pub line_w: f32,
139    /// height of this line
140    pub line_height: f32,
141    /// ascent of glyph
142    pub glyph_ascent: f32,
143    /// descent of glyph
144    pub glyph_descent: f32,
145}
146
147impl<'a> LayoutRun<'a> {
148    /// Return the pixel span `Some((x_left, x_width))` of the highlighted area between `cursor_start`
149    /// and `cursor_end` within this run, or None if the cursor range does not intersect this run.
150    /// This may return widths of zero if `cursor_start == cursor_end`, if the run is empty, or if the
151    /// region's left start boundary is the same as the cursor's end boundary or vice versa.
152    pub fn highlight(&self, cursor_start: Cursor, cursor_end: Cursor) -> Option<(f32, f32)> {
153        let mut x_start = None;
154        let mut x_end = None;
155        let rtl_factor = if self.rtl { 1. } else { 0. };
156        let ltr_factor = 1. - rtl_factor;
157        for glyph in self.glyphs.iter() {
158            let cursor = self.cursor_from_glyph_left(glyph);
159            if cursor >= cursor_start && cursor <= cursor_end {
160                if x_start.is_none() {
161                    x_start = Some(glyph.x + glyph.w * rtl_factor);
162                }
163                x_end = Some(glyph.x + glyph.w * rtl_factor);
164            }
165            let cursor = self.cursor_from_glyph_right(glyph);
166            if cursor >= cursor_start && cursor <= cursor_end {
167                if x_start.is_none() {
168                    x_start = Some(glyph.x + glyph.w * ltr_factor);
169                }
170                x_end = Some(glyph.x + glyph.w * ltr_factor);
171            }
172        }
173        if let Some(x_start) = x_start {
174            let x_end = x_end.expect("end of cursor not found");
175            let (x_start, x_end) = if x_start < x_end {
176                (x_start, x_end)
177            } else {
178                (x_end, x_start)
179            };
180            Some((x_start, x_end - x_start))
181        } else {
182            None
183        }
184    }
185
186    fn cursor_from_glyph_left(&self, glyph: &LayoutGlyph) -> Cursor {
187        if self.rtl {
188            Cursor::new_with_affinity(self.line_i, glyph.end, Affinity::Before)
189        } else {
190            Cursor::new_with_affinity(self.line_i, glyph.start, Affinity::After)
191        }
192    }
193
194    fn cursor_from_glyph_right(&self, glyph: &LayoutGlyph) -> Cursor {
195        if self.rtl {
196            Cursor::new_with_affinity(self.line_i, glyph.start, Affinity::After)
197        } else {
198            Cursor::new_with_affinity(self.line_i, glyph.end, Affinity::Before)
199        }
200    }
201}
202
203/// An iterator of visible text lines, see [`LayoutRun`]
204pub struct LayoutRunIter<'b> {
205    buffer: &'b TextLayout,
206    line_i: usize,
207    layout_i: usize,
208    remaining_len: usize,
209    line_y: f32,
210    total_layout: i32,
211}
212
213impl<'b> LayoutRunIter<'b> {
214    pub fn new(buffer: &'b TextLayout) -> Self {
215        let total_layout_lines: usize = buffer
216            .lines
217            .iter()
218            .map(|line| {
219                line.layout_opt()
220                    .as_ref()
221                    .map(|layout| layout.len())
222                    .unwrap_or_default()
223            })
224            .sum();
225        let top_cropped_layout_lines =
226            total_layout_lines.saturating_sub(buffer.scroll.try_into().unwrap_or_default());
227        let maximum_lines = i32::MAX;
228        let bottom_cropped_layout_lines =
229            if top_cropped_layout_lines > maximum_lines.try_into().unwrap_or_default() {
230                maximum_lines.try_into().unwrap_or_default()
231            } else {
232                top_cropped_layout_lines
233            };
234
235        Self {
236            buffer,
237            line_i: 0,
238            layout_i: 0,
239            remaining_len: bottom_cropped_layout_lines,
240            line_y: 0.0,
241            total_layout: 0,
242        }
243    }
244}
245
246impl<'b> Iterator for LayoutRunIter<'b> {
247    type Item = LayoutRun<'b>;
248
249    fn size_hint(&self) -> (usize, Option<usize>) {
250        (self.remaining_len, Some(self.remaining_len))
251    }
252
253    fn next(&mut self) -> Option<Self::Item> {
254        while let Some(line) = self.buffer.lines.get(self.line_i) {
255            let shape = line.shape_opt().as_ref()?;
256            let layout = line.layout_opt().as_ref()?;
257            while let Some(layout_line) = layout.get(self.layout_i) {
258                self.layout_i += 1;
259
260                let scrolled = self.total_layout < self.buffer.scroll;
261                self.total_layout += 1;
262                if scrolled {
263                    continue;
264                }
265
266                let line_height = layout_line.line_ascent + layout_line.line_descent;
267                self.line_y += line_height;
268                if self.line_y > self.buffer.height {
269                    return None;
270                }
271
272                let offset =
273                    (line_height - (layout_line.glyph_ascent + layout_line.glyph_descent)) / 2.0;
274
275                self.remaining_len -= 1;
276                return Some(LayoutRun {
277                    line_i: self.line_i,
278                    text: line.text(),
279                    rtl: shape.rtl,
280                    glyphs: &layout_line.glyphs,
281                    line_y: self.line_y - offset - layout_line.glyph_descent,
282                    line_w: layout_line.w,
283                    glyph_ascent: layout_line.glyph_ascent,
284                    glyph_descent: layout_line.glyph_descent,
285                    line_height,
286                });
287            }
288            self.line_i += 1;
289            self.layout_i = 0;
290        }
291
292        None
293    }
294}
295
296impl<'b> ExactSizeIterator for LayoutRunIter<'b> {}
297
298/// Metrics of text
299#[derive(Clone, Copy, Debug, Default, PartialEq)]
300pub struct Metrics {
301    /// Font size in pixels
302    pub font_size: f32,
303    /// Line height in pixels
304    pub line_height: f32,
305}
306
307impl Metrics {
308    pub const fn new(font_size: f32, line_height: f32) -> Self {
309        Self {
310            font_size,
311            line_height,
312        }
313    }
314
315    pub fn scale(self, scale: f32) -> Self {
316        Self {
317            font_size: self.font_size * scale,
318            line_height: self.line_height * scale,
319        }
320    }
321}
322
323impl fmt::Display for Metrics {
324    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
325        write!(f, "{}px / {}px", self.font_size, self.line_height)
326    }
327}
328
329/// A buffer of text that is shaped and laid out
330#[derive(Clone)]
331pub struct TextLayout {
332    /// [BufferLine]s (or paragraphs) of text in the buffer
333    pub lines: Vec<TextLayoutLine>,
334    width: f32,
335    height: f32,
336    scroll: i32,
337    /// True if a redraw is requires. Set to false after processing
338    redraw: bool,
339    wrap: Wrap,
340    tab_width: usize,
341}
342
343impl TextLayout {
344    /// Create a new [`Buffer`] with the provided [`FontSystem`] and [`Metrics`]
345    ///
346    /// # Panics
347    ///
348    /// Will panic if `metrics.line_height` is zero.
349    pub fn new() -> Self {
350        let mut buffer = Self {
351            lines: Vec::new(),
352            width: f32::MAX,
353            height: f32::MAX,
354            scroll: 0,
355            redraw: false,
356            wrap: Wrap::Word,
357            tab_width: 8,
358        };
359        buffer.set_text("", AttrsList::new(Attrs::new()));
360        buffer
361    }
362
363    fn relayout(&mut self) {
364        #[cfg(all(feature = "std", not(target_arch = "wasm32")))]
365        let instant = std::time::Instant::now();
366
367        for line in &mut self.lines {
368            if line.shape_opt().is_some() {
369                line.reset_layout();
370                line.layout(self.width, self.wrap);
371            }
372        }
373
374        self.redraw = true;
375
376        #[cfg(all(feature = "std", not(target_arch = "wasm32")))]
377        log::debug!("relayout: {:?}", instant.elapsed());
378    }
379
380    /// Pre-shape lines in the buffer, up to `lines`, return actual number of layout lines
381    pub fn shape_until(&mut self, lines: i32) -> i32 {
382        #[cfg(all(feature = "std", not(target_arch = "wasm32")))]
383        let instant = std::time::Instant::now();
384
385        let mut reshaped = 0;
386        let mut total_layout = 0;
387        for line in &mut self.lines {
388            if total_layout >= lines {
389                break;
390            }
391
392            if line.shape_opt().is_none() {
393                reshaped += 1;
394            }
395            let layout = line.layout(self.width, self.wrap);
396            total_layout += layout.len() as i32;
397        }
398
399        if reshaped > 0 {
400            #[cfg(all(feature = "std", not(target_arch = "wasm32")))]
401            log::debug!("shape_until {}: {:?}", reshaped, instant.elapsed());
402            self.redraw = true;
403        }
404
405        total_layout
406    }
407
408    /// Shape lines until cursor, also scrolling to include cursor in view
409    pub fn shape_until_cursor(&mut self, cursor: Cursor) {
410        #[cfg(all(feature = "std", not(target_arch = "wasm32")))]
411        let instant = std::time::Instant::now();
412
413        let mut reshaped = 0;
414        let mut layout_i = 0;
415        for (line_i, line) in self.lines.iter_mut().enumerate() {
416            if line_i > cursor.line {
417                break;
418            }
419
420            if line.shape_opt().is_none() {
421                reshaped += 1;
422            }
423            let layout = line.layout(self.width, self.wrap);
424            if line_i == cursor.line {
425                let layout_cursor = self.layout_cursor(&cursor);
426                layout_i += layout_cursor.layout as i32;
427                break;
428            } else {
429                layout_i += layout.len() as i32;
430            }
431        }
432
433        if reshaped > 0 {
434            #[cfg(all(feature = "std", not(target_arch = "wasm32")))]
435            log::debug!("shape_until_cursor {}: {:?}", reshaped, instant.elapsed());
436            self.redraw = true;
437        }
438
439        let lines = i32::MAX;
440        if layout_i < self.scroll {
441            self.scroll = layout_i;
442        } else if layout_i >= self.scroll + lines {
443            self.scroll = layout_i - (lines - 1);
444        }
445
446        self.shape_until_scroll();
447    }
448
449    /// Shape lines until scroll
450    pub fn shape_until_scroll(&mut self) {
451        let lines = i32::MAX;
452
453        let scroll_end = self.scroll + lines;
454        let total_layout = self.shape_until(scroll_end);
455
456        self.scroll = cmp::max(0, cmp::min(total_layout - (lines - 1), self.scroll));
457    }
458
459    pub fn layout_cursor(&self, cursor: &Cursor) -> LayoutCursor {
460        let line = &self.lines[cursor.line];
461
462        //TODO: ensure layout is done?
463        let layout = line.layout_opt().as_ref().expect("layout not found");
464        for (layout_i, layout_line) in layout.iter().enumerate() {
465            for (glyph_i, glyph) in layout_line.glyphs.iter().enumerate() {
466                let cursor_end =
467                    Cursor::new_with_affinity(cursor.line, glyph.end, Affinity::Before);
468                let cursor_start =
469                    Cursor::new_with_affinity(cursor.line, glyph.start, Affinity::After);
470                let (cursor_left, cursor_right) = if glyph.level.is_ltr() {
471                    (cursor_start, cursor_end)
472                } else {
473                    (cursor_end, cursor_start)
474                };
475                if *cursor == cursor_left {
476                    return LayoutCursor::new(cursor.line, layout_i, glyph_i);
477                }
478                if *cursor == cursor_right {
479                    return LayoutCursor::new(cursor.line, layout_i, glyph_i + 1);
480                }
481            }
482        }
483
484        // Fall back to start of line
485        //TODO: should this be the end of the line?
486        LayoutCursor::new(cursor.line, 0, 0)
487    }
488
489    /// Shape the provided line index and return the result
490    pub fn line_shape(&mut self, line_i: usize) -> Option<&ShapeLine> {
491        let line = self.lines.get_mut(line_i)?;
492        Some(line.shape())
493    }
494
495    /// Lay out the provided line index and return the result
496    pub fn line_layout(&mut self, line_i: usize) -> Option<&[LayoutLine]> {
497        let line = self.lines.get_mut(line_i)?;
498        Some(line.layout(self.width, self.wrap))
499    }
500
501    /// Get the current [`Wrap`]
502    pub fn wrap(&self) -> Wrap {
503        self.wrap
504    }
505
506    /// Set the current [`Wrap`]
507    pub fn set_wrap(&mut self, wrap: Wrap) {
508        if wrap != self.wrap {
509            self.wrap = wrap;
510            self.relayout();
511            self.shape_until_scroll();
512        }
513    }
514
515    pub fn size(&self) -> Size {
516        self.layout_runs()
517            .fold(Size::new(0.0, 0.0), |mut size, run| {
518                let new_width = run.line_w as f64;
519                if new_width > size.width {
520                    size.width = new_width;
521                }
522
523                size.height += run.line_height as f64;
524
525                size
526            })
527    }
528    /// Set the current buffer dimensions
529    pub fn set_size(&mut self, width: f32, height: f32) {
530        let clamped_width = width.max(0.0);
531        let clamped_height = height.max(0.0);
532
533        if clamped_width != self.width || clamped_height != self.height {
534            self.width = clamped_width;
535            self.height = clamped_height;
536            self.relayout();
537            self.shape_until_scroll();
538        }
539    }
540
541    /// Get the current scroll location
542    pub fn scroll(&self) -> i32 {
543        self.scroll
544    }
545
546    /// Set the current scroll location
547    pub fn set_scroll(&mut self, scroll: i32) {
548        if scroll != self.scroll {
549            self.scroll = scroll;
550            self.redraw = true;
551        }
552    }
553
554    /// Set text of buffer, using provided attributes for each line by default
555    pub fn set_text(&mut self, text: &str, attrs: AttrsList) {
556        self.lines.clear();
557        let mut attrs = attrs;
558        let mut start_index = 0;
559        for line in text.split_terminator('\n') {
560            let l = line.len();
561            let (line, had_r) = if l > 0 && line.as_bytes()[l - 1] == b'\r' {
562                (&line[0..l - 1], true)
563            } else {
564                (line, false)
565            };
566            let new_attrs = attrs.split_off(line.len() + 1 + if had_r { 1 } else { 0 });
567            self.lines.push(TextLayoutLine::new(
568                line.to_string(),
569                attrs.clone(),
570                start_index,
571                self.tab_width,
572            ));
573            attrs = new_attrs;
574
575            start_index += l + 1 + if had_r { 1 } else { 0 };
576        }
577        // Make sure there is always one line
578        if self.lines.is_empty() {
579            self.lines
580                .push(TextLayoutLine::new(String::new(), attrs, 0, self.tab_width));
581        }
582
583        self.scroll = 0;
584
585        self.shape_until_scroll();
586    }
587
588    pub fn set_tab_width(&mut self, tab_width: usize) {
589        self.tab_width = tab_width;
590    }
591
592    /// True if a redraw is needed
593    pub fn redraw(&self) -> bool {
594        self.redraw
595    }
596
597    /// Set redraw needed flag
598    pub fn set_redraw(&mut self, redraw: bool) {
599        self.redraw = redraw;
600    }
601
602    /// Get the visible layout runs for rendering and other tasks
603    pub fn layout_runs(&self) -> LayoutRunIter {
604        LayoutRunIter::new(self)
605    }
606
607    pub fn hit_point(&self, point: Point) -> HitPoint {
608        let x = point.x as f32;
609        let y = point.y as f32;
610        let mut hit_point = HitPoint {
611            index: 0,
612            line: 0,
613            is_inside: false,
614        };
615
616        let mut runs = self.layout_runs().peekable();
617        let mut first_run = true;
618        while let Some(run) = runs.next() {
619            let line_y = run.line_y;
620
621            if first_run && y < line_y - run.line_height {
622                first_run = false;
623                hit_point.line = run.line_i;
624                hit_point.index = 0;
625            } else if y >= line_y - run.line_height && y < line_y {
626                let mut new_cursor_glyph = run.glyphs.len();
627                let mut new_cursor_char = 0;
628
629                let mut first_glyph = true;
630
631                'hit: for (glyph_i, glyph) in run.glyphs.iter().enumerate() {
632                    if first_glyph {
633                        first_glyph = false;
634                        if (run.rtl && x > glyph.x) || (!run.rtl && x < 0.0) {
635                            new_cursor_glyph = 0;
636                            new_cursor_char = 0;
637                        }
638                    }
639                    if x >= glyph.x && x <= glyph.x + glyph.w {
640                        new_cursor_glyph = glyph_i;
641
642                        let cluster = &run.text[glyph.start..glyph.end];
643                        let total = cluster.grapheme_indices(true).count();
644                        let mut egc_x = glyph.x;
645                        let egc_w = glyph.w / (total as f32);
646                        for (egc_i, egc) in cluster.grapheme_indices(true) {
647                            if x >= egc_x && x <= egc_x + egc_w {
648                                new_cursor_char = egc_i;
649
650                                let right_half = x >= egc_x + egc_w / 2.0;
651                                if right_half != glyph.level.is_rtl() {
652                                    // If clicking on last half of glyph, move cursor past glyph
653                                    new_cursor_char += egc.len();
654                                }
655                                break 'hit;
656                            }
657                            egc_x += egc_w;
658                        }
659
660                        let right_half = x >= glyph.x + glyph.w / 2.0;
661                        if right_half != glyph.level.is_rtl() {
662                            // If clicking on last half of glyph, move cursor past glyph
663                            new_cursor_char = cluster.len();
664                        }
665                        break 'hit;
666                    }
667                }
668
669                hit_point.line = run.line_i;
670                hit_point.index = 0;
671
672                match run.glyphs.get(new_cursor_glyph) {
673                    Some(glyph) => {
674                        // Position at glyph
675                        hit_point.index = glyph.start + new_cursor_char;
676                        hit_point.is_inside = true;
677                    }
678                    None => {
679                        if let Some(glyph) = run.glyphs.last() {
680                            // Position at end of line
681                            hit_point.index = glyph.end;
682                        }
683                    }
684                }
685
686                break;
687            } else if runs.peek().is_none() && y > run.line_y {
688                let mut new_cursor = Cursor::new(run.line_i, 0);
689                if let Some(glyph) = run.glyphs.last() {
690                    new_cursor = run.cursor_from_glyph_right(glyph);
691                }
692                hit_point.line = new_cursor.line;
693                hit_point.index = new_cursor.index;
694            }
695        }
696
697        hit_point
698    }
699
700    pub fn line_col_position(&self, line: usize, col: usize) -> HitPosition {
701        let mut last_glyph: Option<&LayoutGlyph> = None;
702        let mut last_line = 0;
703        let mut last_line_y = 0.0;
704        let mut last_glyph_ascent = 0.0;
705        let mut last_glyph_descent = 0.0;
706        for (current_line, run) in self.layout_runs().enumerate() {
707            for glyph in run.glyphs {
708                if line == run.line_i {
709                    if glyph.start > col {
710                        return HitPosition {
711                            line: last_line,
712                            point: Point::new(
713                                last_glyph.map(|g| (g.x + g.w) as f64).unwrap_or(0.0),
714                                last_line_y as f64,
715                            ),
716                            glyph_ascent: last_glyph_ascent as f64,
717                            glyph_descent: last_glyph_descent as f64,
718                        };
719                    }
720                    if (glyph.start..glyph.end).contains(&col) {
721                        return HitPosition {
722                            line: current_line,
723                            point: Point::new(glyph.x as f64, run.line_y as f64),
724                            glyph_ascent: run.glyph_ascent as f64,
725                            glyph_descent: run.glyph_descent as f64,
726                        };
727                    }
728                } else if run.line_i > line {
729                    return HitPosition {
730                        line: last_line,
731                        point: Point::new(
732                            last_glyph.map(|g| (g.x + g.w) as f64).unwrap_or(0.0),
733                            last_line_y as f64,
734                        ),
735                        glyph_ascent: last_glyph_ascent as f64,
736                        glyph_descent: last_glyph_descent as f64,
737                    };
738                }
739                last_glyph = Some(glyph);
740            }
741            last_line = current_line;
742            last_line_y = run.line_y;
743            last_glyph_ascent = run.glyph_ascent;
744            last_glyph_descent = run.glyph_descent;
745        }
746
747        HitPosition {
748            line: last_line,
749            point: Point::new(
750                last_glyph.map(|g| (g.x + g.w) as f64).unwrap_or(0.0),
751                last_line_y as f64,
752            ),
753            glyph_ascent: last_glyph_ascent as f64,
754            glyph_descent: last_glyph_descent as f64,
755        }
756    }
757
758    pub fn hit_position(&self, idx: usize) -> HitPosition {
759        let mut last_line = 0;
760        let mut last_end: usize = 0;
761        let mut offset = 0;
762        let mut last_glyph_width = 0.0;
763        let mut last_position = HitPosition {
764            line: 0,
765            point: Point::ZERO,
766            glyph_ascent: 0.0,
767            glyph_descent: 0.0,
768        };
769        for (line, run) in self.layout_runs().enumerate() {
770            if run.line_i > last_line {
771                last_line = run.line_i;
772                offset += last_end + 1;
773            }
774            for glyph in run.glyphs {
775                if glyph.start + offset > idx {
776                    last_position.point.x += last_glyph_width as f64;
777                    return last_position;
778                }
779                last_end = glyph.end;
780                last_glyph_width = glyph.w;
781                last_position = HitPosition {
782                    line,
783                    point: Point::new(glyph.x as f64, run.line_y as f64),
784                    glyph_ascent: run.glyph_ascent as f64,
785                    glyph_descent: run.glyph_descent as f64,
786                };
787                if (glyph.start + offset..glyph.end + offset).contains(&idx) {
788                    return last_position;
789                }
790            }
791        }
792
793        if idx > 0 {
794            last_position.point.x += last_glyph_width as f64;
795            return last_position;
796        }
797
798        HitPosition {
799            line: 0,
800            point: Point::ZERO,
801            glyph_ascent: 0.0,
802            glyph_descent: 0.0,
803        }
804    }
805
806    /// Convert x, y position to Cursor (hit detection)
807    pub fn hit(&self, x: f32, y: f32) -> Option<Cursor> {
808        #[cfg(all(feature = "std", not(target_arch = "wasm32")))]
809        let instant = std::time::Instant::now();
810
811        let mut new_cursor_opt = None;
812
813        let mut runs = self.layout_runs().peekable();
814        let mut first_run = true;
815        while let Some(run) = runs.next() {
816            let line_y = run.line_y;
817
818            if first_run && y < line_y - run.line_height {
819                first_run = false;
820                let new_cursor = Cursor::new(run.line_i, 0);
821                new_cursor_opt = Some(new_cursor);
822            } else if y >= line_y - run.line_height && y < line_y {
823                let mut new_cursor_glyph = run.glyphs.len();
824                let mut new_cursor_char = 0;
825                let mut new_cursor_affinity = Affinity::After;
826
827                let mut first_glyph = true;
828
829                'hit: for (glyph_i, glyph) in run.glyphs.iter().enumerate() {
830                    if first_glyph {
831                        first_glyph = false;
832                        if (run.rtl && x > glyph.x) || (!run.rtl && x < 0.0) {
833                            new_cursor_glyph = 0;
834                            new_cursor_char = 0;
835                        }
836                    }
837                    if x >= glyph.x && x <= glyph.x + glyph.w {
838                        new_cursor_glyph = glyph_i;
839
840                        let cluster = &run.text[glyph.start..glyph.end];
841                        let total = cluster.grapheme_indices(true).count();
842                        let mut egc_x = glyph.x;
843                        let egc_w = glyph.w / (total as f32);
844                        for (egc_i, egc) in cluster.grapheme_indices(true) {
845                            if x >= egc_x && x <= egc_x + egc_w {
846                                new_cursor_char = egc_i;
847
848                                let right_half = x >= egc_x + egc_w / 2.0;
849                                if right_half != glyph.level.is_rtl() {
850                                    // If clicking on last half of glyph, move cursor past glyph
851                                    new_cursor_char += egc.len();
852                                    new_cursor_affinity = Affinity::Before;
853                                }
854                                break 'hit;
855                            }
856                            egc_x += egc_w;
857                        }
858
859                        let right_half = x >= glyph.x + glyph.w / 2.0;
860                        if right_half != glyph.level.is_rtl() {
861                            // If clicking on last half of glyph, move cursor past glyph
862                            new_cursor_char = cluster.len();
863                            new_cursor_affinity = Affinity::Before;
864                        }
865                        break 'hit;
866                    }
867                }
868
869                let mut new_cursor = Cursor::new(run.line_i, 0);
870
871                match run.glyphs.get(new_cursor_glyph) {
872                    Some(glyph) => {
873                        // Position at glyph
874                        new_cursor.index = glyph.start + new_cursor_char;
875                        new_cursor.affinity = new_cursor_affinity;
876                    }
877                    None => {
878                        if let Some(glyph) = run.glyphs.last() {
879                            // Position at end of line
880                            new_cursor.index = glyph.end;
881                            new_cursor.affinity = Affinity::Before;
882                        }
883                    }
884                }
885
886                new_cursor_opt = Some(new_cursor);
887
888                break;
889            } else if runs.peek().is_none() && y > run.line_y {
890                let mut new_cursor = Cursor::new(run.line_i, 0);
891                if let Some(glyph) = run.glyphs.last() {
892                    new_cursor = run.cursor_from_glyph_right(glyph);
893                }
894                new_cursor_opt = Some(new_cursor);
895            }
896        }
897
898        #[cfg(all(feature = "std", not(target_arch = "wasm32")))]
899        log::trace!("click({}, {}): {:?}", x, y, instant.elapsed());
900
901        new_cursor_opt
902    }
903
904    /// Draw the buffer
905    #[cfg(feature = "swash")]
906    pub fn draw<F>(&self, cache: &mut crate::SwashCache, color: Color, mut f: F)
907    where
908        F: FnMut(i32, i32, u32, u32, Color),
909    {
910        for run in self.layout_runs() {
911            for glyph in run.glyphs.iter() {
912                let (cache_key, x_int, y_int) = (glyph.cache_key, glyph.x_int, glyph.y_int);
913
914                let glyph_color = glyph.color;
915
916                cache.with_pixels(cache_key, glyph_color, |x, y, color| {
917                    f(x_int + x, run.line_y as i32 + y_int + y, 1, 1, color);
918                });
919            }
920        }
921    }
922}