Skip to main content

gpui/text_system/
line.rs

1use crate::{
2    App, Bounds, DevicePixels, Half, Hsla, LineLayout, Pixels, Point, RenderGlyphParams, Result,
3    SharedString, StrikethroughStyle, TextAlign, UnderlineStyle, Window, WrapBoundary,
4    WrappedLineLayout, black, fill, point, px, size, underline_y_offset,
5};
6use derive_more::{Deref, DerefMut};
7use smallvec::SmallVec;
8use std::{ops::Range, sync::Arc};
9
10/// Pre-computed glyph data for efficient painting without per-glyph cache lookups.
11///
12/// This is produced by `ShapedLine::compute_glyph_raster_data` during prepaint
13/// and consumed by `ShapedLine::paint_with_raster_data` during paint.
14#[derive(Clone, Debug)]
15pub struct GlyphRasterData {
16    /// The raster bounds for each glyph, in paint order.
17    pub bounds: Vec<Bounds<DevicePixels>>,
18    /// The render params for each glyph (needed for sprite atlas lookup).
19    pub params: Vec<RenderGlyphParams>,
20}
21
22/// Set the text decoration for a run of text.
23#[derive(Debug, Clone)]
24pub struct DecorationRun {
25    /// The length of the run in utf-8 bytes.
26    pub len: u32,
27
28    /// The color for this run
29    pub color: Hsla,
30
31    /// The background color for this run
32    pub background_color: Option<Hsla>,
33
34    /// The underline style for this run
35    pub underline: Option<UnderlineStyle>,
36
37    /// The strikethrough style for this run
38    pub strikethrough: Option<StrikethroughStyle>,
39}
40
41/// A line of text that has been shaped and decorated.
42#[derive(Clone, Default, Debug, Deref, DerefMut)]
43pub struct ShapedLine {
44    #[deref]
45    #[deref_mut]
46    pub(crate) layout: Arc<LineLayout>,
47    /// The text that was shaped for this line.
48    pub text: SharedString,
49    pub(crate) decoration_runs: SmallVec<[DecorationRun; 32]>,
50}
51
52impl ShapedLine {
53    /// Returns a forward-only cursor for this shaped line.
54    pub fn cursor(&self) -> ShapedLineCursor<'_> {
55        assert_eq!(
56            self.len(),
57            self.text.len(),
58            "cannot split a shaped line with an adjusted length"
59        );
60        let byte_ordered = self
61            .layout
62            .runs
63            .iter()
64            .flat_map(|run| run.glyphs.iter().map(|glyph| glyph.index))
65            .is_sorted();
66        ShapedLineCursor {
67            line: self,
68            unordered_remainder: (!byte_ordered).then(|| self.clone()),
69            byte_index: 0,
70            run_index: 0,
71            glyph_index: 0,
72            decoration_index: 0,
73            decoration_offset: 0,
74            x_offset: px(0.),
75        }
76    }
77
78    /// The length of the line in utf-8 bytes.
79    #[allow(clippy::len_without_is_empty)]
80    pub fn len(&self) -> usize {
81        self.layout.len
82    }
83
84    /// The width of the shaped line in pixels.
85    ///
86    /// This is the glyph advance width computed by the text shaping system and is useful for
87    /// incrementally advancing a "pen" when painting multiple fragments on the same row.
88    pub fn width(&self) -> Pixels {
89        self.layout.width
90    }
91
92    /// Override the len, useful if you're rendering text a
93    /// as text b (e.g. rendering invisibles).
94    pub fn with_len(mut self, len: usize) -> Self {
95        let layout = self.layout.as_ref();
96        self.layout = Arc::new(LineLayout {
97            font_size: layout.font_size,
98            width: layout.width,
99            ascent: layout.ascent,
100            descent: layout.descent,
101            runs: layout.runs.clone(),
102            len,
103        });
104        self
105    }
106
107    /// Paint the line of text to the window.
108    pub fn paint(
109        &self,
110        origin: Point<Pixels>,
111        line_height: Pixels,
112        align: TextAlign,
113        align_width: Option<Pixels>,
114        window: &mut Window,
115        cx: &mut App,
116    ) -> Result<()> {
117        self.paint_with_underline_handler(
118            origin,
119            line_height,
120            align,
121            align_width,
122            window,
123            cx,
124            |_, origin, width, style, window| window.paint_underline(origin, width, style),
125        )
126    }
127
128    /// Paint the line with a handler for each underline.
129    pub fn paint_with_underline_handler(
130        &self,
131        origin: Point<Pixels>,
132        line_height: Pixels,
133        align: TextAlign,
134        align_width: Option<Pixels>,
135        window: &mut Window,
136        cx: &mut App,
137        mut paint_underline: impl FnMut(
138            Range<usize>,
139            Point<Pixels>,
140            Pixels,
141            &UnderlineStyle,
142            &mut Window,
143        ),
144    ) -> Result<()> {
145        paint_line(
146            origin,
147            &self.layout,
148            line_height,
149            align,
150            align_width,
151            &self.decoration_runs,
152            &[],
153            window,
154            cx,
155            &mut paint_underline,
156        )
157    }
158
159    /// Paint the background of the line to the window.
160    pub fn paint_background(
161        &self,
162        origin: Point<Pixels>,
163        line_height: Pixels,
164        align: TextAlign,
165        align_width: Option<Pixels>,
166        window: &mut Window,
167        cx: &mut App,
168    ) -> Result<()> {
169        paint_line_background(
170            origin,
171            &self.layout,
172            line_height,
173            align,
174            align_width,
175            &self.decoration_runs,
176            &[],
177            window,
178            cx,
179        )?;
180
181        Ok(())
182    }
183
184    /// Split this shaped line at a byte index, returning `(prefix, suffix)`.
185    ///
186    /// - `prefix` contains glyphs for bytes `[0, byte_index)` with original positions.
187    ///   Its width equals the x-advance up to the split point.
188    /// - `suffix` contains glyphs for bytes `[byte_index, len)` with positions
189    ///   shifted left so the first glyph starts at x=0, and byte indices rebased to 0.
190    /// - Decoration runs are partitioned at the boundary; a run that straddles it is
191    ///   split into two with adjusted lengths.
192    /// - `font_size`, `ascent`, and `descent` are copied to both halves.
193    pub fn split_at(&self, byte_index: usize) -> (ShapedLine, ShapedLine) {
194        let (left_layout, right_layout) = self.layout.split_at(byte_index);
195
196        // Partition decoration runs. A run straddling the boundary is split into two.
197        let mut left_decorations = SmallVec::new();
198        let mut right_decorations = SmallVec::new();
199        let mut decoration_offset = 0u32;
200        let split_point = byte_index as u32;
201
202        for decoration in &self.decoration_runs {
203            let run_end = decoration_offset + decoration.len;
204
205            if run_end <= split_point {
206                left_decorations.push(decoration.clone());
207            } else if decoration_offset >= split_point {
208                right_decorations.push(decoration.clone());
209            } else {
210                let left_len = split_point - decoration_offset;
211                let right_len = run_end - split_point;
212                left_decorations.push(DecorationRun {
213                    len: left_len,
214                    color: decoration.color,
215                    background_color: decoration.background_color,
216                    underline: decoration.underline,
217                    strikethrough: decoration.strikethrough,
218                });
219                right_decorations.push(DecorationRun {
220                    len: right_len,
221                    color: decoration.color,
222                    background_color: decoration.background_color,
223                    underline: decoration.underline,
224                    strikethrough: decoration.strikethrough,
225                });
226            }
227
228            decoration_offset = run_end;
229        }
230
231        // Split text
232        let left_text = if byte_index == self.text.len() {
233            self.text.clone()
234        } else {
235            SharedString::new(&self.text[..byte_index])
236        };
237        let right_text = if byte_index == 0 {
238            self.text.clone()
239        } else {
240            SharedString::new(&self.text[byte_index..])
241        };
242
243        let left = ShapedLine {
244            layout: Arc::new(left_layout),
245            text: left_text,
246            decoration_runs: left_decorations,
247        };
248
249        let right = ShapedLine {
250            layout: Arc::new(right_layout),
251            text: right_text,
252            decoration_runs: right_decorations,
253        };
254
255        (left, right)
256    }
257}
258
259/// Incrementally splits a [`ShapedLine`] at increasing UTF-8 byte boundaries.
260///
261/// Each piece preserves the original glyphs and decorations, rebased as in
262/// [`ShapedLine::split_at`]. Byte-ordered glyphs are advanced in linear time,
263/// copying each glyph and byte at most once; visually reordered glyphs fall
264/// back to the existing split operation.
265pub struct ShapedLineCursor<'a> {
266    line: &'a ShapedLine,
267    /// Bidirectional shaping can put glyphs out of byte order.
268    unordered_remainder: Option<ShapedLine>,
269    byte_index: usize,
270    run_index: usize,
271    glyph_index: usize,
272    decoration_index: usize,
273    decoration_offset: u32,
274    x_offset: Pixels,
275}
276
277impl<'a> ShapedLineCursor<'a> {
278    /// Takes the bytes since the previous boundary.
279    ///
280    /// Panics if the boundary precedes the previous one, exceeds the line's
281    /// length, or falls inside a UTF-8 character.
282    pub fn take_until(&mut self, byte_index: usize) -> ShapedLine {
283        assert!(
284            byte_index >= self.byte_index,
285            "split boundary moved backwards"
286        );
287        assert!(
288            byte_index <= self.line.len(),
289            "split boundary exceeds line length"
290        );
291        assert!(
292            self.line.text.is_char_boundary(byte_index),
293            "split boundary is not a UTF-8 character boundary"
294        );
295        let previous_index = self.byte_index;
296        let previous_x = self.x_offset;
297        if let Some(remainder) = &mut self.unordered_remainder {
298            let (piece, rest) = remainder.split_at(byte_index - previous_index);
299            *remainder = rest;
300            self.byte_index = byte_index;
301            self.x_offset = self.line.layout.x_for_index(byte_index);
302            return piece;
303        }
304        let mut runs = Vec::new();
305        let mut next_x = self.line.layout.width;
306        while let Some(run) = self.line.layout.runs.get(self.run_index) {
307            let start = self.glyph_index;
308            while let Some(glyph) = run.glyphs.get(self.glyph_index) {
309                if glyph.index >= byte_index {
310                    break;
311                }
312                self.glyph_index += 1;
313            }
314            let end = self.glyph_index;
315            if start < end {
316                runs.push(crate::ShapedRun {
317                    font_id: run.font_id,
318                    glyphs: run.glyphs[start..end]
319                        .iter()
320                        .map(|glyph| crate::ShapedGlyph {
321                            id: glyph.id,
322                            position: point(glyph.position.x - previous_x, glyph.position.y),
323                            index: glyph.index - previous_index,
324                            is_emoji: glyph.is_emoji,
325                        })
326                        .collect(),
327                });
328            }
329            if let Some(glyph) = run.glyphs.get(self.glyph_index) {
330                next_x = glyph.position.x;
331                break;
332            }
333            self.run_index += 1;
334            self.glyph_index = 0;
335        }
336        let mut decorations = SmallVec::new();
337        while let Some(decoration) = self.line.decoration_runs.get(self.decoration_index)
338            && (self.decoration_offset < byte_index as u32
339                || (decoration.len == 0 && self.decoration_offset == byte_index as u32))
340        {
341            let end = self.decoration_offset + decoration.len;
342            let start = self.decoration_offset.max(previous_index as u32);
343            let len = end.min(byte_index as u32) - start;
344            if len > 0 || decoration.len == 0 {
345                decorations.push(DecorationRun {
346                    len,
347                    color: decoration.color,
348                    background_color: decoration.background_color,
349                    underline: decoration.underline,
350                    strikethrough: decoration.strikethrough,
351                });
352            }
353            if end <= byte_index as u32 {
354                self.decoration_index += 1;
355                self.decoration_offset = end;
356            } else {
357                break;
358            }
359        }
360        self.byte_index = byte_index;
361        self.x_offset = next_x;
362        ShapedLine {
363            layout: Arc::new(LineLayout {
364                font_size: self.line.layout.font_size,
365                width: next_x - previous_x,
366                ascent: self.line.layout.ascent,
367                descent: self.line.layout.descent,
368                runs,
369                len: byte_index - previous_index,
370            }),
371            text: SharedString::new(&self.line.text[previous_index..byte_index]),
372            decoration_runs: decorations,
373        }
374    }
375
376    /// Returns the original line's x position at the current boundary.
377    pub fn x_offset(&self) -> Pixels {
378        self.x_offset
379    }
380}
381
382impl LineLayout {
383    /// Paint this layout to the window, using the given decoration runs to color
384    /// glyphs and draw underlines and strikethroughs.
385    ///
386    /// This is a lower-level alternative to [`ShapedLine::paint`] for callers that
387    /// hold a bare layout and track decorations themselves.
388    pub fn paint(
389        &self,
390        origin: Point<Pixels>,
391        line_height: Pixels,
392        align: TextAlign,
393        align_width: Option<Pixels>,
394        decoration_runs: &[DecorationRun],
395        window: &mut Window,
396        cx: &mut App,
397    ) -> Result<()> {
398        paint_line(
399            origin,
400            self,
401            line_height,
402            align,
403            align_width,
404            decoration_runs,
405            &[],
406            window,
407            cx,
408            &mut |_, origin, width, style, window| window.paint_underline(origin, width, style),
409        )
410    }
411
412    /// Paint the background of this layout to the window, using the given
413    /// decoration runs to determine background colors.
414    ///
415    /// This is a lower-level alternative to [`ShapedLine::paint_background`] for
416    /// callers that hold a bare layout and track decorations themselves.
417    pub fn paint_background(
418        &self,
419        origin: Point<Pixels>,
420        line_height: Pixels,
421        align: TextAlign,
422        align_width: Option<Pixels>,
423        decoration_runs: &[DecorationRun],
424        window: &mut Window,
425        cx: &mut App,
426    ) -> Result<()> {
427        paint_line_background(
428            origin,
429            self,
430            line_height,
431            align,
432            align_width,
433            decoration_runs,
434            &[],
435            window,
436            cx,
437        )
438    }
439}
440
441/// A line of text that has been shaped, decorated, and wrapped by the text layout system.
442#[derive(Default, Debug, Deref, DerefMut)]
443pub struct WrappedLine {
444    #[deref]
445    #[deref_mut]
446    pub(crate) layout: Arc<WrappedLineLayout>,
447    /// The text that was shaped for this line.
448    pub text: SharedString,
449    pub(crate) decoration_runs: Vec<DecorationRun>,
450}
451
452impl WrappedLine {
453    /// The length of the underlying, unwrapped layout, in utf-8 bytes.
454    #[allow(clippy::len_without_is_empty)]
455    pub fn len(&self) -> usize {
456        self.layout.len()
457    }
458
459    /// Paint this line of text to the window.
460    pub fn paint(
461        &self,
462        origin: Point<Pixels>,
463        line_height: Pixels,
464        align: TextAlign,
465        bounds: Option<Bounds<Pixels>>,
466        window: &mut Window,
467        cx: &mut App,
468    ) -> Result<()> {
469        let align_width = match bounds {
470            Some(bounds) => Some(bounds.size.width),
471            None => self.layout.wrap_width,
472        };
473
474        paint_line(
475            origin,
476            &self.layout.unwrapped_layout,
477            line_height,
478            align,
479            align_width,
480            &self.decoration_runs,
481            &self.wrap_boundaries,
482            window,
483            cx,
484            &mut |_, origin, width, style, window| window.paint_underline(origin, width, style),
485        )?;
486
487        Ok(())
488    }
489
490    /// Paint the background of line of text to the window.
491    pub fn paint_background(
492        &self,
493        origin: Point<Pixels>,
494        line_height: Pixels,
495        align: TextAlign,
496        bounds: Option<Bounds<Pixels>>,
497        window: &mut Window,
498        cx: &mut App,
499    ) -> Result<()> {
500        let align_width = match bounds {
501            Some(bounds) => Some(bounds.size.width),
502            None => self.layout.wrap_width,
503        };
504
505        paint_line_background(
506            origin,
507            &self.layout.unwrapped_layout,
508            line_height,
509            align,
510            align_width,
511            &self.decoration_runs,
512            &self.wrap_boundaries,
513            window,
514            cx,
515        )?;
516
517        Ok(())
518    }
519}
520
521fn paint_line(
522    origin: Point<Pixels>,
523    layout: &LineLayout,
524    line_height: Pixels,
525    align: TextAlign,
526    align_width: Option<Pixels>,
527    decoration_runs: &[DecorationRun],
528    wrap_boundaries: &[WrapBoundary],
529    window: &mut Window,
530    cx: &mut App,
531    paint_underline: &mut dyn FnMut(
532        Range<usize>,
533        Point<Pixels>,
534        Pixels,
535        &UnderlineStyle,
536        &mut Window,
537    ),
538) -> Result<()> {
539    let line_bounds = line_paint_bounds(
540        origin,
541        layout,
542        line_height,
543        align,
544        align_width,
545        wrap_boundaries,
546    );
547    window.paint_layer(line_bounds, |window| {
548        let padding_top = (line_height - layout.ascent - layout.descent) / 2.;
549        let baseline_offset = point(px(0.), padding_top + layout.ascent);
550        let underline_y_offset = underline_y_offset(line_height, layout.ascent, layout.descent);
551        let mut decoration_runs = decoration_runs.iter();
552        let mut wraps = wrap_boundaries.iter().peekable();
553        let mut run_end = 0;
554        let mut color = black();
555        let mut current_underline: Option<(Point<Pixels>, UnderlineStyle, Range<usize>)> = None;
556        let mut current_strikethrough: Option<(Point<Pixels>, StrikethroughStyle)> = None;
557        let text_system = cx.text_system().clone();
558        let mut glyph_origin = point(
559            aligned_origin_x(
560                origin,
561                align_width.unwrap_or(layout.width),
562                px(0.0),
563                &align,
564                layout,
565                wraps.peek(),
566            ),
567            origin.y,
568        );
569        let mut prev_glyph_position = Point::default();
570        let mut max_glyph_size = size(px(0.), px(0.));
571        let mut first_glyph_x = origin.x;
572        for (run_ix, run) in layout.runs.iter().enumerate() {
573            max_glyph_size = text_system.bounding_box(run.font_id, layout.font_size).size;
574
575            for (glyph_ix, glyph) in run.glyphs.iter().enumerate() {
576                glyph_origin.x += glyph.position.x - prev_glyph_position.x;
577                if glyph_ix == 0 && run_ix == 0 {
578                    first_glyph_x = glyph_origin.x;
579                }
580
581                if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) {
582                    wraps.next();
583                    if let Some((underline_origin, underline_style, underline_range)) =
584                        current_underline.as_mut()
585                    {
586                        if glyph_origin.x == underline_origin.x {
587                            underline_origin.x -= max_glyph_size.width.half();
588                        };
589                        paint_underline(
590                            underline_range.clone(),
591                            *underline_origin,
592                            glyph_origin.x - underline_origin.x,
593                            underline_style,
594                            window,
595                        );
596                        if glyph.index < run_end {
597                            underline_origin.x = origin.x;
598                            underline_origin.y += line_height;
599                        } else {
600                            current_underline = None;
601                        }
602                    }
603                    if let Some((strikethrough_origin, strikethrough_style)) =
604                        current_strikethrough.as_mut()
605                    {
606                        if glyph_origin.x == strikethrough_origin.x {
607                            strikethrough_origin.x -= max_glyph_size.width.half();
608                        };
609                        window.paint_strikethrough(
610                            *strikethrough_origin,
611                            glyph_origin.x - strikethrough_origin.x,
612                            strikethrough_style,
613                        );
614                        if glyph.index < run_end {
615                            strikethrough_origin.x = origin.x;
616                            strikethrough_origin.y += line_height;
617                        } else {
618                            current_strikethrough = None;
619                        }
620                    }
621
622                    glyph_origin.x = aligned_origin_x(
623                        origin,
624                        align_width.unwrap_or(layout.width),
625                        glyph.position.x,
626                        &align,
627                        layout,
628                        wraps.peek(),
629                    );
630                    glyph_origin.y += line_height;
631                }
632                prev_glyph_position = glyph.position;
633
634                let mut finished_underline: Option<(Point<Pixels>, UnderlineStyle, Range<usize>)> =
635                    None;
636                let mut finished_strikethrough: Option<(Point<Pixels>, StrikethroughStyle)> = None;
637                if glyph.index >= run_end {
638                    let mut style_run = decoration_runs.next();
639
640                    // ignore style runs that apply to a partial glyph
641                    while let Some(run) = style_run {
642                        if glyph.index < run_end + (run.len as usize) {
643                            break;
644                        }
645                        run_end += run.len as usize;
646                        style_run = decoration_runs.next();
647                    }
648
649                    if let Some(style_run) = style_run {
650                        let style_run_start = run_end;
651                        if let Some((_, underline_style, underline_range)) = &mut current_underline
652                        {
653                            if style_run.underline.as_ref() != Some(underline_style) {
654                                finished_underline = current_underline.take();
655                            } else {
656                                underline_range.end = style_run_start + style_run.len as usize;
657                            }
658                        }
659                        if let Some(run_underline) = style_run.underline.as_ref() {
660                            current_underline.get_or_insert((
661                                point(glyph_origin.x, glyph_origin.y + underline_y_offset),
662                                UnderlineStyle {
663                                    color: Some(run_underline.color.unwrap_or(style_run.color)),
664                                    thickness: run_underline.thickness,
665                                    wavy: run_underline.wavy,
666                                },
667                                style_run_start..style_run_start + style_run.len as usize,
668                            ));
669                        }
670                        if let Some((_, strikethrough_style)) = &mut current_strikethrough
671                            && style_run.strikethrough.as_ref() != Some(strikethrough_style)
672                        {
673                            finished_strikethrough = current_strikethrough.take();
674                        }
675                        if let Some(run_strikethrough) = style_run.strikethrough.as_ref() {
676                            current_strikethrough.get_or_insert((
677                                point(
678                                    glyph_origin.x,
679                                    glyph_origin.y
680                                        + (((layout.ascent * 0.5) + baseline_offset.y) * 0.5),
681                                ),
682                                StrikethroughStyle {
683                                    color: Some(run_strikethrough.color.unwrap_or(style_run.color)),
684                                    thickness: run_strikethrough.thickness,
685                                },
686                            ));
687                        }
688
689                        run_end += style_run.len as usize;
690                        color = style_run.color;
691                    } else {
692                        run_end = layout.len;
693                        finished_underline = current_underline.take();
694                        finished_strikethrough = current_strikethrough.take();
695                    }
696                }
697
698                if let Some((mut underline_origin, underline_style, underline_range)) =
699                    finished_underline
700                {
701                    if underline_origin.x == glyph_origin.x {
702                        underline_origin.x -= max_glyph_size.width.half();
703                    };
704                    paint_underline(
705                        underline_range,
706                        underline_origin,
707                        glyph_origin.x - underline_origin.x,
708                        &underline_style,
709                        window,
710                    );
711                }
712
713                if let Some((mut strikethrough_origin, strikethrough_style)) =
714                    finished_strikethrough
715                {
716                    if strikethrough_origin.x == glyph_origin.x {
717                        strikethrough_origin.x -= max_glyph_size.width.half();
718                    };
719                    window.paint_strikethrough(
720                        strikethrough_origin,
721                        glyph_origin.x - strikethrough_origin.x,
722                        &strikethrough_style,
723                    );
724                }
725
726                let max_glyph_bounds = Bounds {
727                    origin: glyph_origin,
728                    size: max_glyph_size,
729                };
730
731                let content_mask = window.content_mask();
732                if max_glyph_bounds.intersects(&content_mask.bounds) {
733                    let vertical_offset = point(px(0.0), glyph.position.y);
734                    if glyph.is_emoji {
735                        window.paint_emoji(
736                            glyph_origin + baseline_offset + vertical_offset,
737                            run.font_id,
738                            glyph.id,
739                            layout.font_size,
740                        )?;
741                    } else {
742                        window.paint_glyph(
743                            glyph_origin + baseline_offset + vertical_offset,
744                            run.font_id,
745                            glyph.id,
746                            layout.font_size,
747                            color,
748                        )?;
749                    }
750                }
751            }
752        }
753
754        let mut last_line_end_x = first_glyph_x + layout.width;
755        if let Some(boundary) = wrap_boundaries.last() {
756            let run = &layout.runs[boundary.run_ix];
757            let glyph = &run.glyphs[boundary.glyph_ix];
758            last_line_end_x -= glyph.position.x;
759        }
760
761        if let Some((mut underline_start, underline_style, underline_range)) =
762            current_underline.take()
763        {
764            if last_line_end_x == underline_start.x {
765                underline_start.x -= max_glyph_size.width.half()
766            };
767            paint_underline(
768                underline_range,
769                underline_start,
770                last_line_end_x - underline_start.x,
771                &underline_style,
772                window,
773            );
774        }
775
776        if let Some((mut strikethrough_start, strikethrough_style)) = current_strikethrough.take() {
777            if last_line_end_x == strikethrough_start.x {
778                strikethrough_start.x -= max_glyph_size.width.half()
779            };
780            window.paint_strikethrough(
781                strikethrough_start,
782                last_line_end_x - strikethrough_start.x,
783                &strikethrough_style,
784            );
785        }
786
787        Ok(())
788    })
789}
790
791fn paint_line_background(
792    origin: Point<Pixels>,
793    layout: &LineLayout,
794    line_height: Pixels,
795    align: TextAlign,
796    align_width: Option<Pixels>,
797    decoration_runs: &[DecorationRun],
798    wrap_boundaries: &[WrapBoundary],
799    window: &mut Window,
800    cx: &mut App,
801) -> Result<()> {
802    let line_bounds = line_paint_bounds(
803        origin,
804        layout,
805        line_height,
806        align,
807        align_width,
808        wrap_boundaries,
809    );
810    window.paint_layer(line_bounds, |window| {
811        let mut decoration_runs = decoration_runs.iter();
812        let mut wraps = wrap_boundaries.iter().peekable();
813        let mut run_end = 0;
814        let mut current_background: Option<(Point<Pixels>, Hsla)> = None;
815        let text_system = cx.text_system().clone();
816        let mut glyph_origin = point(
817            aligned_origin_x(
818                origin,
819                align_width.unwrap_or(layout.width),
820                px(0.0),
821                &align,
822                layout,
823                wraps.peek(),
824            ),
825            origin.y,
826        );
827        let mut prev_glyph_position = Point::default();
828        let mut max_glyph_size = size(px(0.), px(0.));
829        for (run_ix, run) in layout.runs.iter().enumerate() {
830            max_glyph_size = text_system.bounding_box(run.font_id, layout.font_size).size;
831
832            for (glyph_ix, glyph) in run.glyphs.iter().enumerate() {
833                glyph_origin.x += glyph.position.x - prev_glyph_position.x;
834
835                if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) {
836                    wraps.next();
837                    if let Some((background_origin, background_color)) = current_background.as_mut()
838                    {
839                        if glyph_origin.x == background_origin.x {
840                            background_origin.x -= max_glyph_size.width.half()
841                        }
842                        window.paint_quad(fill(
843                            Bounds {
844                                origin: *background_origin,
845                                size: size(glyph_origin.x - background_origin.x, line_height),
846                            },
847                            *background_color,
848                        ));
849                        if glyph.index < run_end {
850                            background_origin.x = origin.x;
851                            background_origin.y += line_height;
852                        } else {
853                            current_background = None;
854                        }
855                    }
856
857                    glyph_origin.x = aligned_origin_x(
858                        origin,
859                        align_width.unwrap_or(layout.width),
860                        glyph.position.x,
861                        &align,
862                        layout,
863                        wraps.peek(),
864                    );
865                    glyph_origin.y += line_height;
866                }
867                prev_glyph_position = glyph.position;
868
869                let mut finished_background: Option<(Point<Pixels>, Hsla)> = None;
870                if glyph.index >= run_end {
871                    let mut style_run = decoration_runs.next();
872
873                    // ignore style runs that apply to a partial glyph
874                    while let Some(run) = style_run {
875                        if glyph.index < run_end + (run.len as usize) {
876                            break;
877                        }
878                        run_end += run.len as usize;
879                        style_run = decoration_runs.next();
880                    }
881
882                    if let Some(style_run) = style_run {
883                        if let Some((_, background_color)) = &mut current_background
884                            && style_run.background_color.as_ref() != Some(background_color)
885                        {
886                            finished_background = current_background.take();
887                        }
888                        if let Some(run_background) = style_run.background_color {
889                            current_background.get_or_insert((
890                                point(glyph_origin.x, glyph_origin.y),
891                                run_background,
892                            ));
893                        }
894                        run_end += style_run.len as usize;
895                    } else {
896                        run_end = layout.len;
897                        finished_background = current_background.take();
898                    }
899                }
900
901                if let Some((mut background_origin, background_color)) = finished_background {
902                    let mut width = glyph_origin.x - background_origin.x;
903                    if background_origin.x == glyph_origin.x {
904                        background_origin.x -= max_glyph_size.width.half();
905                    };
906                    window.paint_quad(fill(
907                        Bounds {
908                            origin: background_origin,
909                            size: size(width, line_height),
910                        },
911                        background_color,
912                    ));
913                }
914            }
915        }
916
917        let mut last_line_end_x = origin.x + layout.width;
918        if let Some(boundary) = wrap_boundaries.last() {
919            let run = &layout.runs[boundary.run_ix];
920            let glyph = &run.glyphs[boundary.glyph_ix];
921            last_line_end_x -= glyph.position.x;
922        }
923
924        if let Some((mut background_origin, background_color)) = current_background.take() {
925            if last_line_end_x == background_origin.x {
926                background_origin.x -= max_glyph_size.width.half()
927            };
928            window.paint_quad(fill(
929                Bounds {
930                    origin: background_origin,
931                    size: size(last_line_end_x - background_origin.x, line_height),
932                },
933                background_color,
934            ));
935        }
936
937        Ok(())
938    })
939}
940
941fn line_paint_bounds(
942    origin: Point<Pixels>,
943    layout: &LineLayout,
944    line_height: Pixels,
945    align: TextAlign,
946    align_width: Option<Pixels>,
947    wrap_boundaries: &[WrapBoundary],
948) -> Bounds<Pixels> {
949    let mut bounds = Bounds::new(
950        origin,
951        size(
952            layout.width,
953            line_height * (wrap_boundaries.len() as f32 + 1.),
954        ),
955    );
956    if align == TextAlign::Left || layout.len == 0 {
957        return bounds;
958    }
959
960    let align_width = align_width.unwrap_or(layout.width);
961    let mut row_start = Pixels::ZERO;
962    let row_ends = wrap_boundaries
963        .iter()
964        .map(|boundary| {
965            layout.runs[boundary.run_ix].glyphs[boundary.glyph_ix]
966                .position
967                .x
968        })
969        .chain([layout.width]);
970    for (row, row_end) in row_ends.enumerate() {
971        let width = row_end - row_start;
972        let offset = match align {
973            TextAlign::Left => Pixels::ZERO,
974            TextAlign::Center => (align_width - width) / 2.,
975            TextAlign::Right => align_width - width,
976        };
977        bounds = bounds.union(&Bounds::new(
978            point(origin.x + offset, origin.y + line_height * row as f32),
979            size(width, line_height),
980        ));
981        row_start = row_end;
982    }
983    bounds
984}
985
986fn aligned_origin_x(
987    origin: Point<Pixels>,
988    align_width: Pixels,
989    last_glyph_x: Pixels,
990    align: &TextAlign,
991    layout: &LineLayout,
992    wrap_boundary: Option<&&WrapBoundary>,
993) -> Pixels {
994    let end_of_line = if let Some(WrapBoundary { run_ix, glyph_ix }) = wrap_boundary {
995        layout.runs[*run_ix].glyphs[*glyph_ix].position.x
996    } else {
997        layout.width
998    };
999
1000    let line_width = end_of_line - last_glyph_x;
1001
1002    match align {
1003        TextAlign::Left => origin.x,
1004        TextAlign::Center => (origin.x * 2.0 + align_width - line_width) / 2.0,
1005        TextAlign::Right => origin.x + align_width - line_width,
1006    }
1007}
1008
1009#[cfg(test)]
1010mod tests {
1011    use super::*;
1012    use crate::{
1013        AppContext as _, Context, FontId, GlyphId, IntoElement, Render, ShapedGlyph, ShapedRun,
1014        Styled, TestAppContext, TextRun, Underline, canvas, font, hsla,
1015    };
1016    use std::rc::Rc;
1017
1018    /// Helper: build a ShapedLine from glyph descriptors without the platform text system.
1019    /// Each glyph is described as (byte_index, x_position).
1020    fn make_shaped_line(
1021        text: &str,
1022        glyphs: &[(usize, f32)],
1023        width: f32,
1024        decorations: &[DecorationRun],
1025    ) -> ShapedLine {
1026        let shaped_glyphs: Vec<ShapedGlyph> = glyphs
1027            .iter()
1028            .map(|&(index, x)| ShapedGlyph {
1029                id: GlyphId(0),
1030                position: point(px(x), px(0.0)),
1031                index,
1032                is_emoji: false,
1033            })
1034            .collect();
1035
1036        ShapedLine {
1037            layout: Arc::new(LineLayout {
1038                font_size: px(16.0),
1039                width: px(width),
1040                ascent: px(12.0),
1041                descent: px(4.0),
1042                runs: vec![ShapedRun {
1043                    font_id: FontId(0),
1044                    glyphs: shaped_glyphs,
1045                }],
1046                len: text.len(),
1047            }),
1048            text: SharedString::new(text),
1049            decoration_runs: SmallVec::from(decorations.to_vec()),
1050        }
1051    }
1052
1053    #[test]
1054    fn test_aligned_line_paint_bounds() {
1055        let line = make_shaped_line("abcd", &[(0, 0.), (1, 10.), (2, 20.), (3, 30.)], 40., &[]);
1056        let origin = point(px(10.), px(20.));
1057        let line_height = px(16.);
1058        for (wrapped, align_width, expected_edges) in [
1059            (false, None, [(10., 50.), (10., 50.), (10., 50.)]),
1060            (true, None, [(10., 50.), (10., 50.), (10., 50.)]),
1061            (false, Some(40.), [(10., 50.), (10., 50.), (10., 50.)]),
1062            (true, Some(40.), [(10., 50.), (10., 50.), (10., 50.)]),
1063            (false, Some(100.), [(10., 50.), (10., 80.), (10., 110.)]),
1064            (true, Some(100.), [(10., 50.), (10., 70.), (10., 110.)]),
1065            (false, Some(10.), [(10., 50.), (-5., 50.), (-20., 50.)]),
1066            (true, Some(10.), [(10., 50.), (5., 50.), (0., 50.)]),
1067            (false, Some(0.), [(10., 50.), (-10., 50.), (-30., 50.)]),
1068            (true, Some(0.), [(10., 50.), (0., 50.), (-10., 50.)]),
1069        ] {
1070            let boundaries = if wrapped {
1071                vec![WrapBoundary {
1072                    run_ix: 0,
1073                    glyph_ix: 2,
1074                }]
1075            } else {
1076                Vec::new()
1077            };
1078            for (align, (left, right)) in [TextAlign::Left, TextAlign::Center, TextAlign::Right]
1079                .into_iter()
1080                .zip(expected_edges)
1081            {
1082                assert_eq!(
1083                    line_paint_bounds(
1084                        origin,
1085                        &line.layout,
1086                        line_height,
1087                        align,
1088                        align_width.map(px),
1089                        &boundaries,
1090                    ),
1091                    Bounds::new(
1092                        point(px(left), origin.y),
1093                        size(px(right - left), px(if wrapped { 32. } else { 16. })),
1094                    ),
1095                    "wrapped={wrapped}, align={align:?}, align_width={align_width:?}",
1096                );
1097            }
1098        }
1099        let empty = make_shaped_line("", &[], 0., &[]);
1100        for align in [TextAlign::Left, TextAlign::Center, TextAlign::Right] {
1101            assert_eq!(
1102                line_paint_bounds(
1103                    origin,
1104                    &empty.layout,
1105                    line_height,
1106                    align,
1107                    Some(px(100.)),
1108                    &[]
1109                ),
1110                Bounds::new(origin, size(Pixels::ZERO, line_height)),
1111            );
1112        }
1113    }
1114
1115    #[gpui::test]
1116    fn test_underline_handler_matches_default_paint(cx: &mut TestAppContext) {
1117        test_underline_handler_at_scales(cx, |window, cx| {
1118            let first_style = UnderlineStyle {
1119                thickness: px(1.),
1120                color: Some(hsla(0., 1., 0.5, 1.)),
1121                wavy: true,
1122            };
1123            let last_style = UnderlineStyle {
1124                color: Some(hsla(0.5, 1., 0.5, 1.)),
1125                wavy: false,
1126                ..first_style
1127            };
1128            let fallback_style = UnderlineStyle {
1129                color: Some(black()),
1130                ..first_style
1131            };
1132            let decoration = DecorationRun {
1133                len: 1,
1134                color: black(),
1135                background_color: None,
1136                underline: Some(first_style),
1137                strikethrough: None,
1138            };
1139            let line = underline_test_line(
1140                "aébcde",
1141                &[
1142                    decoration.clone(),
1143                    DecorationRun {
1144                        len: 2,
1145                        ..decoration.clone()
1146                    },
1147                    DecorationRun {
1148                        underline: None,
1149                        ..decoration.clone()
1150                    },
1151                    DecorationRun {
1152                        underline: Some(UnderlineStyle {
1153                            color: None,
1154                            ..first_style
1155                        }),
1156                        ..decoration.clone()
1157                    },
1158                    DecorationRun {
1159                        underline: Some(last_style),
1160                        ..decoration.clone()
1161                    },
1162                    DecorationRun {
1163                        underline: Some(last_style),
1164                        ..decoration
1165                    },
1166                ],
1167                false,
1168                window,
1169            );
1170            assert_eq!(line.width(), px(48.));
1171            for origin_x in [-3.25, 0., 4.25] {
1172                for (align, align_width, offset) in [
1173                    (TextAlign::Left, None, 0.),
1174                    (TextAlign::Center, None, 0.),
1175                    (TextAlign::Right, None, 0.),
1176                    (TextAlign::Left, Some(px(96.)), 0.),
1177                    (TextAlign::Center, Some(px(96.)), 24.),
1178                    (TextAlign::Right, Some(px(96.)), 48.),
1179                ] {
1180                    let origin = point(px(origin_x), px(12.25));
1181                    let line_height = px(20.);
1182                    window.next_frame.scene.clear();
1183                    line.paint(origin, line_height, align, align_width, window, cx)
1184                        .unwrap();
1185                    let original = window.next_frame.scene.underlines.clone();
1186                    window.next_frame.scene.clear();
1187                    line.layout
1188                        .paint(
1189                            origin,
1190                            line_height,
1191                            align,
1192                            align_width,
1193                            &line.decoration_runs,
1194                            window,
1195                            cx,
1196                        )
1197                        .unwrap();
1198                    assert_underline_primitives_eq(&window.next_frame.scene.underlines, &original);
1199
1200                    window.next_frame.scene.clear();
1201                    let mut strokes = Vec::new();
1202                    line.paint_with_underline_handler(
1203                        origin,
1204                        line_height,
1205                        align,
1206                        align_width,
1207                        window,
1208                        cx,
1209                        |range, origin, width, style, window| {
1210                            strokes.push((range, origin, width, *style));
1211                            window.paint_underline(origin, width, style);
1212                        },
1213                    )
1214                    .unwrap();
1215                    let start = px(origin_x + offset);
1216                    let y = origin.y + underline_y_offset(line_height, line.ascent, line.descent);
1217                    assert_eq!(
1218                        strokes,
1219                        [
1220                            (0..3, point(start, y), px(16.), first_style),
1221                            (4..5, point(start + px(24.), y), px(8.), fallback_style),
1222                            (5..7, point(start + px(32.), y), px(16.), last_style),
1223                        ]
1224                    );
1225                    assert_underline_primitives_eq(&window.next_frame.scene.underlines, &original);
1226
1227                    window.next_frame.scene.clear();
1228                    let mut captured = Vec::new();
1229                    line.paint_with_underline_handler(
1230                        origin,
1231                        line_height,
1232                        align,
1233                        align_width,
1234                        window,
1235                        cx,
1236                        |range, origin, width, style, _| {
1237                            captured.push((range, origin, width, *style))
1238                        },
1239                    )
1240                    .unwrap();
1241                    assert_eq!(captured, strokes);
1242                    assert_eq!(window.next_frame.scene.underlines.len(), 0);
1243                }
1244            }
1245            for text in ["", "abc"] {
1246                let line = underline_test_line(text, &[], false, window);
1247                let mut calls = 0;
1248                line.paint_with_underline_handler(
1249                    point(px(4.), px(10.)),
1250                    px(20.),
1251                    TextAlign::Left,
1252                    None,
1253                    window,
1254                    cx,
1255                    |_, _, _, _, _| calls += 1,
1256                )
1257                .unwrap();
1258                assert_eq!(calls, 0);
1259            }
1260        });
1261    }
1262
1263    #[gpui::test]
1264    fn test_underline_handler_reports_zero_advance_geometry(cx: &mut TestAppContext) {
1265        test_underline_handler_at_scales(cx, |window, cx| {
1266            let first_style = UnderlineStyle {
1267                thickness: px(1.),
1268                color: Some(black()),
1269                wavy: true,
1270            };
1271            let last_style = UnderlineStyle {
1272                wavy: false,
1273                ..first_style
1274            };
1275            let decoration = DecorationRun {
1276                len: 1,
1277                color: black(),
1278                background_color: None,
1279                underline: Some(first_style),
1280                strikethrough: None,
1281            };
1282            let line = underline_test_line(
1283                "ab",
1284                &[
1285                    decoration.clone(),
1286                    DecorationRun {
1287                        underline: Some(last_style),
1288                        ..decoration
1289                    },
1290                ],
1291                true,
1292                window,
1293            );
1294            let half_width = cx
1295                .text_system()
1296                .bounding_box(line.runs[0].font_id, line.font_size)
1297                .size
1298                .width
1299                / 2.;
1300            let origin = point(px(40.25), px(10.25));
1301            let line_height = px(20.);
1302            let y = origin.y + underline_y_offset(line_height, line.ascent, line.descent);
1303            for (align, offset) in [
1304                (TextAlign::Left, 0.),
1305                (TextAlign::Center, 16.),
1306                (TextAlign::Right, 32.),
1307            ] {
1308                window.next_frame.scene.clear();
1309                line.paint(origin, line_height, align, Some(px(32.)), window, cx)
1310                    .unwrap();
1311                let original = window.next_frame.scene.underlines.clone();
1312                window.next_frame.scene.clear();
1313                let mut strokes = Vec::new();
1314                line.paint_with_underline_handler(
1315                    origin,
1316                    line_height,
1317                    align,
1318                    Some(px(32.)),
1319                    window,
1320                    cx,
1321                    |range, origin, width, style, window| {
1322                        strokes.push((range, origin, width, *style));
1323                        window.paint_underline(origin, width, style);
1324                    },
1325                )
1326                .unwrap();
1327                let end = origin.x + px(offset);
1328                let start = point(end - half_width, y);
1329                let width = end - start.x;
1330                assert_eq!(
1331                    strokes,
1332                    [
1333                        (0..1, start, width, first_style),
1334                        (1..2, start, width, last_style),
1335                    ]
1336                );
1337                assert_underline_primitives_eq(&window.next_frame.scene.underlines, &original);
1338            }
1339        });
1340    }
1341
1342    #[gpui::test]
1343    fn test_underline_handler_matches_wrapped_paint(cx: &mut TestAppContext) {
1344        test_underline_handler_at_scales(cx, |window, cx| {
1345            let style = UnderlineStyle {
1346                thickness: px(1.),
1347                color: Some(black()),
1348                wavy: true,
1349            };
1350            for zero_advance in [false, true] {
1351                let line = underline_test_line(
1352                    "abcd",
1353                    &[DecorationRun {
1354                        len: 4,
1355                        color: black(),
1356                        background_color: None,
1357                        underline: Some(style),
1358                        strikethrough: None,
1359                    }],
1360                    zero_advance,
1361                    window,
1362                );
1363                let half_width = cx
1364                    .text_system()
1365                    .bounding_box(line.runs[0].font_id, line.font_size)
1366                    .size
1367                    .width
1368                    / 2.;
1369                let origin = point(px(40.25), px(10.25));
1370                let line_height = px(20.);
1371                let y = origin.y + underline_y_offset(line_height, line.ascent, line.descent);
1372                let wrapped = WrappedLine {
1373                    layout: Arc::new(WrappedLineLayout {
1374                        unwrapped_layout: line.layout,
1375                        wrap_boundaries: SmallVec::from_buf([WrapBoundary {
1376                            run_ix: 0,
1377                            glyph_ix: 2,
1378                        }]),
1379                        wrap_width: Some(px(16.)),
1380                    }),
1381                    text: line.text,
1382                    decoration_runs: line.decoration_runs.into_vec(),
1383                };
1384                window.next_frame.scene.clear();
1385                wrapped
1386                    .paint(origin, line_height, TextAlign::Left, None, window, cx)
1387                    .unwrap();
1388                let original = window.next_frame.scene.underlines.clone();
1389                window.next_frame.scene.clear();
1390                let mut strokes = Vec::new();
1391                paint_line(
1392                    origin,
1393                    &wrapped.unwrapped_layout,
1394                    line_height,
1395                    TextAlign::Left,
1396                    Some(px(16.)),
1397                    &wrapped.decoration_runs,
1398                    &wrapped.wrap_boundaries,
1399                    window,
1400                    cx,
1401                    &mut |range, origin, width, style, window| {
1402                        strokes.push((range, origin, width, *style));
1403                        window.paint_underline(origin, width, style);
1404                    },
1405                )
1406                .unwrap();
1407                let (start, width) = if zero_advance {
1408                    let start = origin.x - half_width;
1409                    (start, origin.x - start)
1410                } else {
1411                    (origin.x, px(16.))
1412                };
1413                assert_eq!(
1414                    strokes,
1415                    [
1416                        (0..4, point(start, y), width, style),
1417                        (0..4, point(start, y + line_height), width, style),
1418                    ]
1419                );
1420                assert_underline_primitives_eq(&window.next_frame.scene.underlines, &original);
1421            }
1422        });
1423    }
1424
1425    #[test]
1426    fn test_split_at_invariants() {
1427        // Split "abcdef" at every possible byte index and verify structural invariants.
1428        let line = make_shaped_line(
1429            "abcdef",
1430            &[
1431                (0, 0.0),
1432                (1, 10.0),
1433                (2, 20.0),
1434                (3, 30.0),
1435                (4, 40.0),
1436                (5, 50.0),
1437            ],
1438            60.0,
1439            &[],
1440        );
1441
1442        for i in 0..=6 {
1443            let (left, right) = line.split_at(i);
1444
1445            assert_eq!(
1446                left.width() + right.width(),
1447                line.width(),
1448                "widths must sum at split={i}"
1449            );
1450            assert_eq!(
1451                left.len() + right.len(),
1452                line.len(),
1453                "lengths must sum at split={i}"
1454            );
1455            assert_eq!(
1456                format!("{}{}", left.text.as_ref(), right.text.as_ref()),
1457                "abcdef",
1458                "text must concatenate at split={i}"
1459            );
1460            assert_eq!(left.font_size, line.font_size, "font_size at split={i}");
1461            assert_eq!(right.ascent, line.ascent, "ascent at split={i}");
1462            assert_eq!(right.descent, line.descent, "descent at split={i}");
1463        }
1464
1465        // Edge: split at 0 produces no left runs, full content on right
1466        let (left, right) = line.split_at(0);
1467        assert_eq!(left.runs.len(), 0);
1468        assert_eq!(right.runs[0].glyphs.len(), 6);
1469
1470        // Edge: split at end produces full content on left, no right runs
1471        let (left, right) = line.split_at(6);
1472        assert_eq!(left.runs[0].glyphs.len(), 6);
1473        assert_eq!(right.runs.len(), 0);
1474    }
1475
1476    #[test]
1477    fn test_split_at_glyph_rebasing() {
1478        // Two font runs (simulating a font fallback boundary at byte 3):
1479        //   run A (FontId 0): glyphs at bytes 0,1,2  positions 0,10,20
1480        //   run B (FontId 1): glyphs at bytes 3,4,5  positions 30,40,50
1481        // Successive splits simulate the incremental splitting done during wrap.
1482        let line = ShapedLine {
1483            layout: Arc::new(LineLayout {
1484                font_size: px(16.0),
1485                width: px(60.0),
1486                ascent: px(12.0),
1487                descent: px(4.0),
1488                runs: vec![
1489                    ShapedRun {
1490                        font_id: FontId(0),
1491                        glyphs: vec![
1492                            ShapedGlyph {
1493                                id: GlyphId(0),
1494                                position: point(px(0.0), px(0.0)),
1495                                index: 0,
1496                                is_emoji: false,
1497                            },
1498                            ShapedGlyph {
1499                                id: GlyphId(0),
1500                                position: point(px(10.0), px(0.0)),
1501                                index: 1,
1502                                is_emoji: false,
1503                            },
1504                            ShapedGlyph {
1505                                id: GlyphId(0),
1506                                position: point(px(20.0), px(0.0)),
1507                                index: 2,
1508                                is_emoji: false,
1509                            },
1510                        ],
1511                    },
1512                    ShapedRun {
1513                        font_id: FontId(1),
1514                        glyphs: vec![
1515                            ShapedGlyph {
1516                                id: GlyphId(0),
1517                                position: point(px(30.0), px(0.0)),
1518                                index: 3,
1519                                is_emoji: false,
1520                            },
1521                            ShapedGlyph {
1522                                id: GlyphId(0),
1523                                position: point(px(40.0), px(0.0)),
1524                                index: 4,
1525                                is_emoji: false,
1526                            },
1527                            ShapedGlyph {
1528                                id: GlyphId(0),
1529                                position: point(px(50.0), px(0.0)),
1530                                index: 5,
1531                                is_emoji: false,
1532                            },
1533                        ],
1534                    },
1535                ],
1536                len: 6,
1537            }),
1538            text: "abcdef".into(),
1539            decoration_runs: SmallVec::new(),
1540        };
1541
1542        // First split at byte 2 — mid-run in run A
1543        let (first, remainder) = line.split_at(2);
1544        assert_eq!(first.text.as_ref(), "ab");
1545        assert_eq!(first.runs.len(), 1);
1546        assert_eq!(first.runs[0].font_id, FontId(0));
1547
1548        // Remainder "cdef" should have two runs: tail of A (1 glyph) + all of B (3 glyphs)
1549        assert_eq!(remainder.text.as_ref(), "cdef");
1550        assert_eq!(remainder.runs.len(), 2);
1551        assert_eq!(remainder.runs[0].font_id, FontId(0));
1552        assert_eq!(remainder.runs[0].glyphs.len(), 1);
1553        assert_eq!(remainder.runs[0].glyphs[0].index, 0);
1554        assert_eq!(remainder.runs[0].glyphs[0].position.x, px(0.0));
1555        assert_eq!(remainder.runs[1].font_id, FontId(1));
1556        assert_eq!(remainder.runs[1].glyphs[0].index, 1);
1557        assert_eq!(remainder.runs[1].glyphs[0].position.x, px(10.0));
1558
1559        // Second split at byte 2 within remainder — crosses the run boundary
1560        let (second, final_part) = remainder.split_at(2);
1561        assert_eq!(second.text.as_ref(), "cd");
1562        assert_eq!(final_part.text.as_ref(), "ef");
1563        assert_eq!(final_part.runs[0].glyphs[0].index, 0);
1564        assert_eq!(final_part.runs[0].glyphs[0].position.x, px(0.0));
1565
1566        // Widths must sum across all three pieces
1567        assert_eq!(
1568            first.width() + second.width() + final_part.width(),
1569            line.width()
1570        );
1571    }
1572
1573    #[test]
1574    fn test_split_at_decorations() {
1575        // Three decoration runs: red [0..2), green [2..5), blue [5..6).
1576        // Split at byte 3 — red goes entirely left, green straddles, blue goes entirely right.
1577        let red = Hsla {
1578            h: 0.0,
1579            s: 1.0,
1580            l: 0.5,
1581            a: 1.0,
1582        };
1583        let green = Hsla {
1584            h: 0.3,
1585            s: 1.0,
1586            l: 0.5,
1587            a: 1.0,
1588        };
1589        let blue = Hsla {
1590            h: 0.6,
1591            s: 1.0,
1592            l: 0.5,
1593            a: 1.0,
1594        };
1595
1596        let line = make_shaped_line(
1597            "abcdef",
1598            &[
1599                (0, 0.0),
1600                (1, 10.0),
1601                (2, 20.0),
1602                (3, 30.0),
1603                (4, 40.0),
1604                (5, 50.0),
1605            ],
1606            60.0,
1607            &[
1608                DecorationRun {
1609                    len: 2,
1610                    color: red,
1611                    background_color: None,
1612                    underline: None,
1613                    strikethrough: None,
1614                },
1615                DecorationRun {
1616                    len: 3,
1617                    color: green,
1618                    background_color: None,
1619                    underline: None,
1620                    strikethrough: None,
1621                },
1622                DecorationRun {
1623                    len: 1,
1624                    color: blue,
1625                    background_color: None,
1626                    underline: None,
1627                    strikethrough: None,
1628                },
1629            ],
1630        );
1631
1632        let (left, right) = line.split_at(3);
1633
1634        // Left: red(2) + green(1) — green straddled, left portion has len 1
1635        assert_eq!(left.decoration_runs.len(), 2);
1636        assert_eq!(left.decoration_runs[0].len, 2);
1637        assert_eq!(left.decoration_runs[0].color, red);
1638        assert_eq!(left.decoration_runs[1].len, 1);
1639        assert_eq!(left.decoration_runs[1].color, green);
1640
1641        // Right: green(2) + blue(1) — green straddled, right portion has len 2
1642        assert_eq!(right.decoration_runs.len(), 2);
1643        assert_eq!(right.decoration_runs[0].len, 2);
1644        assert_eq!(right.decoration_runs[0].color, green);
1645        assert_eq!(right.decoration_runs[1].len, 1);
1646        assert_eq!(right.decoration_runs[1].color, blue);
1647    }
1648
1649    struct UnderlineHandlerTestView(Rc<dyn Fn(&mut Window, &mut App)>);
1650
1651    impl Render for UnderlineHandlerTestView {
1652        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1653            let paint = self.0.clone();
1654            canvas(
1655                |_, _, _| {},
1656                move |_, _, window, cx| {
1657                    window.with_element_opacity(Some(0.5), |window| paint(window, cx));
1658                },
1659            )
1660            .size_full()
1661        }
1662    }
1663
1664    fn test_underline_handler_at_scales(
1665        cx: &mut TestAppContext,
1666        paint: impl Fn(&mut Window, &mut App) + 'static,
1667    ) {
1668        let window = cx.add_window(move |_, _| UnderlineHandlerTestView(Rc::new(paint)));
1669        for scale in [1., 1.25, 1.5, 2., 3.] {
1670            cx.simulate_window_scale_factor_change(window.into(), scale);
1671            cx.update_window(window.into(), |_, window, cx| window.draw(cx).clear(cx))
1672                .unwrap();
1673        }
1674    }
1675
1676    fn underline_test_line(
1677        text: &str,
1678        decorations: &[DecorationRun],
1679        zero_advance: bool,
1680        window: &Window,
1681    ) -> ShapedLine {
1682        let mut line = window.text_system().shape_line(
1683            SharedString::new(text),
1684            px(16.),
1685            &[TextRun {
1686                len: text.len(),
1687                font: font(".ZedMono"),
1688                color: black(),
1689                ..TextRun::default()
1690            }],
1691            None,
1692        );
1693        line.decoration_runs = SmallVec::from(decorations.to_vec());
1694        let layout = &line.layout;
1695        let mut runs = layout.runs.clone();
1696        let advance = if zero_advance { px(0.) } else { px(8.) };
1697        let mut width = px(0.);
1698        for glyph in runs.iter_mut().flat_map(|run| &mut run.glyphs) {
1699            glyph.position.x = width;
1700            width += advance;
1701        }
1702        line.layout = Arc::new(LineLayout {
1703            font_size: layout.font_size,
1704            width,
1705            ascent: layout.ascent,
1706            descent: layout.descent,
1707            runs,
1708            len: layout.len,
1709        });
1710        line
1711    }
1712
1713    fn assert_underline_primitives_eq(actual: &[Underline], expected: &[Underline]) {
1714        assert_eq!(actual.len(), expected.len());
1715        for (actual, expected) in actual.iter().zip(expected) {
1716            assert_eq!(actual.bounds, expected.bounds);
1717            assert_eq!(actual.content_mask, expected.content_mask);
1718            assert_eq!(actual.color, expected.color);
1719            assert_eq!(actual.thickness, expected.thickness);
1720            assert_eq!(actual.wavy, expected.wavy);
1721            assert_eq!(actual.order, expected.order);
1722            assert_eq!(actual.pad, expected.pad);
1723        }
1724    }
1725
1726    #[test]
1727    fn test_cursor_preserves_shaping_metadata_across_runs() {
1728        let line = ShapedLine {
1729            layout: Arc::new(LineLayout {
1730                font_size: px(16.0),
1731                width: px(50.0),
1732                ascent: px(12.0),
1733                descent: px(4.0),
1734                runs: vec![
1735                    ShapedRun {
1736                        font_id: FontId(3),
1737                        glyphs: vec![
1738                            ShapedGlyph {
1739                                id: GlyphId(11),
1740                                position: point(px(0.0), px(1.0)),
1741                                index: 0,
1742                                is_emoji: true,
1743                            },
1744                            ShapedGlyph {
1745                                id: GlyphId(12),
1746                                position: point(px(17.0), px(1.0)),
1747                                index: 1,
1748                                is_emoji: false,
1749                            },
1750                            ShapedGlyph {
1751                                id: GlyphId(13),
1752                                position: point(px(19.0), px(-1.0)),
1753                                index: 1,
1754                                is_emoji: false,
1755                            },
1756                        ],
1757                    },
1758                    ShapedRun {
1759                        font_id: FontId(8),
1760                        glyphs: vec![
1761                            ShapedGlyph {
1762                                id: GlyphId(21),
1763                                position: point(px(25.0), px(1.0)),
1764                                index: 5,
1765                                is_emoji: true,
1766                            },
1767                            ShapedGlyph {
1768                                id: GlyphId(22),
1769                                position: point(px(41.0), px(1.0)),
1770                                index: 7,
1771                                is_emoji: false,
1772                            },
1773                        ],
1774                    },
1775                ],
1776                len: 10,
1777            }),
1778            text: "a😀bcdef".into(),
1779            decoration_runs: SmallVec::new(),
1780        };
1781        let mut cursor = line.cursor();
1782        let first = cursor.take_until(5);
1783        assert_eq!(first.text.as_ref(), "a😀");
1784        assert_eq!(first.runs[0].font_id, FontId(3));
1785        assert_eq!(first.runs[0].glyphs[0].id, GlyphId(11));
1786        assert!(first.runs[0].glyphs[0].is_emoji);
1787        assert_eq!(first.runs[0].glyphs[1].index, 1);
1788        assert_eq!(first.runs[0].glyphs[1].position, point(px(17.0), px(1.0)));
1789        assert_eq!(first.runs[0].glyphs.len(), 3);
1790        assert_eq!(first.runs[0].glyphs[2].index, 1);
1791        assert_eq!(first.runs[0].glyphs[2].position, point(px(19.0), px(-1.0)));
1792        assert_eq!(cursor.x_offset(), px(25.0));
1793
1794        let second = cursor.take_until(7);
1795        assert_eq!(second.text.as_ref(), "bc");
1796        assert_eq!(second.runs[0].font_id, FontId(8));
1797        assert_eq!(second.runs[0].glyphs[0].id, GlyphId(21));
1798        assert_eq!(second.runs[0].glyphs[0].index, 0);
1799        assert_eq!(second.runs[0].glyphs[0].position, point(px(0.0), px(1.0)));
1800        assert_eq!(cursor.x_offset(), px(41.0));
1801
1802        let final_part = cursor.take_until(10);
1803        assert_eq!(final_part.text.as_ref(), "def");
1804        assert_eq!(final_part.runs[0].font_id, FontId(8));
1805        assert_eq!(final_part.runs[0].glyphs[0].id, GlyphId(22));
1806        assert_eq!(final_part.runs[0].glyphs[0].index, 0);
1807        assert_eq!(
1808            final_part.runs[0].glyphs[0].position,
1809            point(px(0.0), px(1.0))
1810        );
1811    }
1812
1813    #[test]
1814    fn test_cursor_preserves_existing_visual_order_splitting() {
1815        let line = make_shaped_line("abc", &[(0, 0.0), (2, 10.0), (1, 20.0)], 30.0, &[]);
1816        let mut cursor = line.cursor();
1817        let mut remainder = line.clone();
1818        let mut previous_boundary = 0;
1819        for boundary in [0, 1, 2, 3] {
1820            let (expected, rest) = remainder.split_at(boundary - previous_boundary);
1821            let actual = cursor.take_until(boundary);
1822            assert_eq!(actual.text, expected.text);
1823            assert_eq!(actual.width(), expected.width());
1824            assert_eq!(actual.runs.len(), expected.runs.len());
1825            for (actual, expected) in actual.runs.iter().zip(&expected.runs) {
1826                assert_eq!(actual.font_id, expected.font_id);
1827                assert_eq!(actual.glyphs.len(), expected.glyphs.len());
1828                for (actual, expected) in actual.glyphs.iter().zip(&expected.glyphs) {
1829                    assert_eq!(actual.id, expected.id);
1830                    assert_eq!(actual.index, expected.index);
1831                    assert_eq!(actual.position, expected.position);
1832                }
1833            }
1834            assert_eq!(cursor.x_offset(), line.x_for_index(boundary));
1835            remainder = rest;
1836            previous_boundary = boundary;
1837        }
1838    }
1839
1840    #[test]
1841    fn test_cursor_partitions_one_decoration_across_three_chunks() {
1842        let line = make_shaped_line(
1843            "abcdef",
1844            &[
1845                (0, 0.0),
1846                (1, 10.0),
1847                (2, 20.0),
1848                (3, 30.0),
1849                (4, 40.0),
1850                (5, 50.0),
1851            ],
1852            60.0,
1853            &[DecorationRun {
1854                len: 6,
1855                color: Hsla {
1856                    h: 0.2,
1857                    s: 0.4,
1858                    l: 0.6,
1859                    a: 1.0,
1860                },
1861                background_color: None,
1862                underline: None,
1863                strikethrough: None,
1864            }],
1865        );
1866        let mut cursor = line.cursor();
1867        assert_eq!(cursor.take_until(2).decoration_runs[0].len, 2);
1868        assert_eq!(cursor.take_until(4).decoration_runs[0].len, 2);
1869        assert_eq!(cursor.take_until(6).decoration_runs[0].len, 2);
1870    }
1871
1872    #[test]
1873    fn test_cursor_matches_successive_splits_at_ordered_boundaries() {
1874        let decorations: Vec<_> = [2, 0, 3, 1]
1875            .into_iter()
1876            .map(|len| DecorationRun {
1877                len,
1878                color: Hsla {
1879                    h: len as f32 / 10.0,
1880                    s: 0.5,
1881                    l: 0.5,
1882                    a: 1.0,
1883                },
1884                background_color: Some(black()),
1885                underline: None,
1886                strikethrough: None,
1887            })
1888            .collect();
1889        let line = make_shaped_line(
1890            "abcdef",
1891            &[(0, 5.0), (0, 5.0), (2, 15.0), (4, 25.0), (5, 35.0)],
1892            45.0,
1893            &decorations,
1894        );
1895        for first in 0..=line.len() {
1896            for second in first..=line.len() {
1897                let mut cursor = line.cursor();
1898                let mut remainder = line.clone();
1899                let mut previous_boundary = 0;
1900                let mut total_width = px(0.0);
1901                let mut text = String::new();
1902                for boundary in [first, second, line.len(), line.len()] {
1903                    let (expected, rest) = remainder.split_at(boundary - previous_boundary);
1904                    let actual = cursor.take_until(boundary);
1905                    assert_eq!(actual.text, expected.text);
1906                    assert_eq!(actual.len(), expected.len());
1907                    assert_eq!(actual.width(), expected.width());
1908                    assert_eq!(actual.runs.len(), expected.runs.len());
1909                    for (actual, expected) in actual.runs.iter().zip(&expected.runs) {
1910                        assert_eq!(actual.font_id, expected.font_id);
1911                        assert_eq!(actual.glyphs.len(), expected.glyphs.len());
1912                        for (actual, expected) in actual.glyphs.iter().zip(&expected.glyphs) {
1913                            assert_eq!(actual.id, expected.id);
1914                            assert_eq!(actual.index, expected.index);
1915                            assert_eq!(actual.position, expected.position);
1916                        }
1917                    }
1918                    assert_eq!(actual.decoration_runs.len(), expected.decoration_runs.len());
1919                    for (actual, expected) in
1920                        actual.decoration_runs.iter().zip(&expected.decoration_runs)
1921                    {
1922                        assert_eq!(actual.len, expected.len);
1923                        assert_eq!(actual.color, expected.color);
1924                        assert_eq!(actual.background_color, expected.background_color);
1925                    }
1926                    total_width += actual.width();
1927                    text.push_str(&actual.text);
1928                    remainder = rest;
1929                    previous_boundary = boundary;
1930                }
1931                assert_eq!(total_width, line.width());
1932                assert_eq!(text, line.text.as_ref());
1933            }
1934        }
1935    }
1936
1937    #[test]
1938    fn test_cursor_empty_chunks_and_repeated_boundaries() {
1939        let line = make_shaped_line("ab", &[(0, 5.0), (1, 15.0)], 20.0, &[]);
1940        let mut cursor = line.cursor();
1941        assert_eq!(cursor.take_until(0).text.as_ref(), "");
1942        assert_eq!(cursor.take_until(0).text.as_ref(), "");
1943        assert_eq!(cursor.take_until(1).text.as_ref(), "a");
1944        assert_eq!(cursor.take_until(2).text.as_ref(), "b");
1945        assert_eq!(cursor.take_until(2).text.as_ref(), "");
1946        let empty = make_shaped_line("", &[], 0.0, &[]);
1947        let piece = empty.cursor().take_until(0);
1948        assert!(piece.text.is_empty());
1949        assert!(piece.runs.is_empty());
1950        assert_eq!(piece.width(), px(0.0));
1951    }
1952
1953    #[test]
1954    fn test_cursor_rejects_invalid_boundaries() {
1955        let line = make_shaped_line("é", &[(0, 0.0)], 10.0, &[]);
1956        let mut cursor = line.cursor();
1957        assert!(
1958            std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1959                cursor.take_until(1);
1960            }))
1961            .is_err()
1962        );
1963        let mut cursor = line.cursor();
1964        cursor.take_until(2);
1965        assert!(
1966            std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1967                cursor.take_until(0);
1968            }))
1969            .is_err()
1970        );
1971        let mut cursor = line.cursor();
1972        assert!(
1973            std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1974                cursor.take_until(3);
1975            }))
1976            .is_err()
1977        );
1978    }
1979}