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 = Bounds::new(
540        origin,
541        size(
542            layout.width,
543            line_height * (wrap_boundaries.len() as f32 + 1.),
544        ),
545    );
546    window.paint_layer(line_bounds, |window| {
547        let padding_top = (line_height - layout.ascent - layout.descent) / 2.;
548        let baseline_offset = point(px(0.), padding_top + layout.ascent);
549        let underline_y_offset = underline_y_offset(line_height, layout.ascent, layout.descent);
550        let mut decoration_runs = decoration_runs.iter();
551        let mut wraps = wrap_boundaries.iter().peekable();
552        let mut run_end = 0;
553        let mut color = black();
554        let mut current_underline: Option<(Point<Pixels>, UnderlineStyle, Range<usize>)> = None;
555        let mut current_strikethrough: Option<(Point<Pixels>, StrikethroughStyle)> = None;
556        let text_system = cx.text_system().clone();
557        let mut glyph_origin = point(
558            aligned_origin_x(
559                origin,
560                align_width.unwrap_or(layout.width),
561                px(0.0),
562                &align,
563                layout,
564                wraps.peek(),
565            ),
566            origin.y,
567        );
568        let mut prev_glyph_position = Point::default();
569        let mut max_glyph_size = size(px(0.), px(0.));
570        let mut first_glyph_x = origin.x;
571        for (run_ix, run) in layout.runs.iter().enumerate() {
572            max_glyph_size = text_system.bounding_box(run.font_id, layout.font_size).size;
573
574            for (glyph_ix, glyph) in run.glyphs.iter().enumerate() {
575                glyph_origin.x += glyph.position.x - prev_glyph_position.x;
576                if glyph_ix == 0 && run_ix == 0 {
577                    first_glyph_x = glyph_origin.x;
578                }
579
580                if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) {
581                    wraps.next();
582                    if let Some((underline_origin, underline_style, underline_range)) =
583                        current_underline.as_mut()
584                    {
585                        if glyph_origin.x == underline_origin.x {
586                            underline_origin.x -= max_glyph_size.width.half();
587                        };
588                        paint_underline(
589                            underline_range.clone(),
590                            *underline_origin,
591                            glyph_origin.x - underline_origin.x,
592                            underline_style,
593                            window,
594                        );
595                        if glyph.index < run_end {
596                            underline_origin.x = origin.x;
597                            underline_origin.y += line_height;
598                        } else {
599                            current_underline = None;
600                        }
601                    }
602                    if let Some((strikethrough_origin, strikethrough_style)) =
603                        current_strikethrough.as_mut()
604                    {
605                        if glyph_origin.x == strikethrough_origin.x {
606                            strikethrough_origin.x -= max_glyph_size.width.half();
607                        };
608                        window.paint_strikethrough(
609                            *strikethrough_origin,
610                            glyph_origin.x - strikethrough_origin.x,
611                            strikethrough_style,
612                        );
613                        if glyph.index < run_end {
614                            strikethrough_origin.x = origin.x;
615                            strikethrough_origin.y += line_height;
616                        } else {
617                            current_strikethrough = None;
618                        }
619                    }
620
621                    glyph_origin.x = aligned_origin_x(
622                        origin,
623                        align_width.unwrap_or(layout.width),
624                        glyph.position.x,
625                        &align,
626                        layout,
627                        wraps.peek(),
628                    );
629                    glyph_origin.y += line_height;
630                }
631                prev_glyph_position = glyph.position;
632
633                let mut finished_underline: Option<(Point<Pixels>, UnderlineStyle, Range<usize>)> =
634                    None;
635                let mut finished_strikethrough: Option<(Point<Pixels>, StrikethroughStyle)> = None;
636                if glyph.index >= run_end {
637                    let mut style_run = decoration_runs.next();
638
639                    // ignore style runs that apply to a partial glyph
640                    while let Some(run) = style_run {
641                        if glyph.index < run_end + (run.len as usize) {
642                            break;
643                        }
644                        run_end += run.len as usize;
645                        style_run = decoration_runs.next();
646                    }
647
648                    if let Some(style_run) = style_run {
649                        let style_run_start = run_end;
650                        if let Some((_, underline_style, underline_range)) = &mut current_underline
651                        {
652                            if style_run.underline.as_ref() != Some(underline_style) {
653                                finished_underline = current_underline.take();
654                            } else {
655                                underline_range.end = style_run_start + style_run.len as usize;
656                            }
657                        }
658                        if let Some(run_underline) = style_run.underline.as_ref() {
659                            current_underline.get_or_insert((
660                                point(glyph_origin.x, glyph_origin.y + underline_y_offset),
661                                UnderlineStyle {
662                                    color: Some(run_underline.color.unwrap_or(style_run.color)),
663                                    thickness: run_underline.thickness,
664                                    wavy: run_underline.wavy,
665                                },
666                                style_run_start..style_run_start + style_run.len as usize,
667                            ));
668                        }
669                        if let Some((_, strikethrough_style)) = &mut current_strikethrough
670                            && style_run.strikethrough.as_ref() != Some(strikethrough_style)
671                        {
672                            finished_strikethrough = current_strikethrough.take();
673                        }
674                        if let Some(run_strikethrough) = style_run.strikethrough.as_ref() {
675                            current_strikethrough.get_or_insert((
676                                point(
677                                    glyph_origin.x,
678                                    glyph_origin.y
679                                        + (((layout.ascent * 0.5) + baseline_offset.y) * 0.5),
680                                ),
681                                StrikethroughStyle {
682                                    color: Some(run_strikethrough.color.unwrap_or(style_run.color)),
683                                    thickness: run_strikethrough.thickness,
684                                },
685                            ));
686                        }
687
688                        run_end += style_run.len as usize;
689                        color = style_run.color;
690                    } else {
691                        run_end = layout.len;
692                        finished_underline = current_underline.take();
693                        finished_strikethrough = current_strikethrough.take();
694                    }
695                }
696
697                if let Some((mut underline_origin, underline_style, underline_range)) =
698                    finished_underline
699                {
700                    if underline_origin.x == glyph_origin.x {
701                        underline_origin.x -= max_glyph_size.width.half();
702                    };
703                    paint_underline(
704                        underline_range,
705                        underline_origin,
706                        glyph_origin.x - underline_origin.x,
707                        &underline_style,
708                        window,
709                    );
710                }
711
712                if let Some((mut strikethrough_origin, strikethrough_style)) =
713                    finished_strikethrough
714                {
715                    if strikethrough_origin.x == glyph_origin.x {
716                        strikethrough_origin.x -= max_glyph_size.width.half();
717                    };
718                    window.paint_strikethrough(
719                        strikethrough_origin,
720                        glyph_origin.x - strikethrough_origin.x,
721                        &strikethrough_style,
722                    );
723                }
724
725                let max_glyph_bounds = Bounds {
726                    origin: glyph_origin,
727                    size: max_glyph_size,
728                };
729
730                let content_mask = window.content_mask();
731                if max_glyph_bounds.intersects(&content_mask.bounds) {
732                    let vertical_offset = point(px(0.0), glyph.position.y);
733                    if glyph.is_emoji {
734                        window.paint_emoji(
735                            glyph_origin + baseline_offset + vertical_offset,
736                            run.font_id,
737                            glyph.id,
738                            layout.font_size,
739                        )?;
740                    } else {
741                        window.paint_glyph(
742                            glyph_origin + baseline_offset + vertical_offset,
743                            run.font_id,
744                            glyph.id,
745                            layout.font_size,
746                            color,
747                        )?;
748                    }
749                }
750            }
751        }
752
753        let mut last_line_end_x = first_glyph_x + layout.width;
754        if let Some(boundary) = wrap_boundaries.last() {
755            let run = &layout.runs[boundary.run_ix];
756            let glyph = &run.glyphs[boundary.glyph_ix];
757            last_line_end_x -= glyph.position.x;
758        }
759
760        if let Some((mut underline_start, underline_style, underline_range)) =
761            current_underline.take()
762        {
763            if last_line_end_x == underline_start.x {
764                underline_start.x -= max_glyph_size.width.half()
765            };
766            paint_underline(
767                underline_range,
768                underline_start,
769                last_line_end_x - underline_start.x,
770                &underline_style,
771                window,
772            );
773        }
774
775        if let Some((mut strikethrough_start, strikethrough_style)) = current_strikethrough.take() {
776            if last_line_end_x == strikethrough_start.x {
777                strikethrough_start.x -= max_glyph_size.width.half()
778            };
779            window.paint_strikethrough(
780                strikethrough_start,
781                last_line_end_x - strikethrough_start.x,
782                &strikethrough_style,
783            );
784        }
785
786        Ok(())
787    })
788}
789
790fn paint_line_background(
791    origin: Point<Pixels>,
792    layout: &LineLayout,
793    line_height: Pixels,
794    align: TextAlign,
795    align_width: Option<Pixels>,
796    decoration_runs: &[DecorationRun],
797    wrap_boundaries: &[WrapBoundary],
798    window: &mut Window,
799    cx: &mut App,
800) -> Result<()> {
801    let line_bounds = Bounds::new(
802        origin,
803        size(
804            layout.width,
805            line_height * (wrap_boundaries.len() as f32 + 1.),
806        ),
807    );
808    window.paint_layer(line_bounds, |window| {
809        let mut decoration_runs = decoration_runs.iter();
810        let mut wraps = wrap_boundaries.iter().peekable();
811        let mut run_end = 0;
812        let mut current_background: Option<(Point<Pixels>, Hsla)> = None;
813        let text_system = cx.text_system().clone();
814        let mut glyph_origin = point(
815            aligned_origin_x(
816                origin,
817                align_width.unwrap_or(layout.width),
818                px(0.0),
819                &align,
820                layout,
821                wraps.peek(),
822            ),
823            origin.y,
824        );
825        let mut prev_glyph_position = Point::default();
826        let mut max_glyph_size = size(px(0.), px(0.));
827        for (run_ix, run) in layout.runs.iter().enumerate() {
828            max_glyph_size = text_system.bounding_box(run.font_id, layout.font_size).size;
829
830            for (glyph_ix, glyph) in run.glyphs.iter().enumerate() {
831                glyph_origin.x += glyph.position.x - prev_glyph_position.x;
832
833                if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) {
834                    wraps.next();
835                    if let Some((background_origin, background_color)) = current_background.as_mut()
836                    {
837                        if glyph_origin.x == background_origin.x {
838                            background_origin.x -= max_glyph_size.width.half()
839                        }
840                        window.paint_quad(fill(
841                            Bounds {
842                                origin: *background_origin,
843                                size: size(glyph_origin.x - background_origin.x, line_height),
844                            },
845                            *background_color,
846                        ));
847                        if glyph.index < run_end {
848                            background_origin.x = origin.x;
849                            background_origin.y += line_height;
850                        } else {
851                            current_background = None;
852                        }
853                    }
854
855                    glyph_origin.x = aligned_origin_x(
856                        origin,
857                        align_width.unwrap_or(layout.width),
858                        glyph.position.x,
859                        &align,
860                        layout,
861                        wraps.peek(),
862                    );
863                    glyph_origin.y += line_height;
864                }
865                prev_glyph_position = glyph.position;
866
867                let mut finished_background: Option<(Point<Pixels>, Hsla)> = None;
868                if glyph.index >= run_end {
869                    let mut style_run = decoration_runs.next();
870
871                    // ignore style runs that apply to a partial glyph
872                    while let Some(run) = style_run {
873                        if glyph.index < run_end + (run.len as usize) {
874                            break;
875                        }
876                        run_end += run.len as usize;
877                        style_run = decoration_runs.next();
878                    }
879
880                    if let Some(style_run) = style_run {
881                        if let Some((_, background_color)) = &mut current_background
882                            && style_run.background_color.as_ref() != Some(background_color)
883                        {
884                            finished_background = current_background.take();
885                        }
886                        if let Some(run_background) = style_run.background_color {
887                            current_background.get_or_insert((
888                                point(glyph_origin.x, glyph_origin.y),
889                                run_background,
890                            ));
891                        }
892                        run_end += style_run.len as usize;
893                    } else {
894                        run_end = layout.len;
895                        finished_background = current_background.take();
896                    }
897                }
898
899                if let Some((mut background_origin, background_color)) = finished_background {
900                    let mut width = glyph_origin.x - background_origin.x;
901                    if background_origin.x == glyph_origin.x {
902                        background_origin.x -= max_glyph_size.width.half();
903                    };
904                    window.paint_quad(fill(
905                        Bounds {
906                            origin: background_origin,
907                            size: size(width, line_height),
908                        },
909                        background_color,
910                    ));
911                }
912            }
913        }
914
915        let mut last_line_end_x = origin.x + layout.width;
916        if let Some(boundary) = wrap_boundaries.last() {
917            let run = &layout.runs[boundary.run_ix];
918            let glyph = &run.glyphs[boundary.glyph_ix];
919            last_line_end_x -= glyph.position.x;
920        }
921
922        if let Some((mut background_origin, background_color)) = current_background.take() {
923            if last_line_end_x == background_origin.x {
924                background_origin.x -= max_glyph_size.width.half()
925            };
926            window.paint_quad(fill(
927                Bounds {
928                    origin: background_origin,
929                    size: size(last_line_end_x - background_origin.x, line_height),
930                },
931                background_color,
932            ));
933        }
934
935        Ok(())
936    })
937}
938
939fn aligned_origin_x(
940    origin: Point<Pixels>,
941    align_width: Pixels,
942    last_glyph_x: Pixels,
943    align: &TextAlign,
944    layout: &LineLayout,
945    wrap_boundary: Option<&&WrapBoundary>,
946) -> Pixels {
947    let end_of_line = if let Some(WrapBoundary { run_ix, glyph_ix }) = wrap_boundary {
948        layout.runs[*run_ix].glyphs[*glyph_ix].position.x
949    } else {
950        layout.width
951    };
952
953    let line_width = end_of_line - last_glyph_x;
954
955    match align {
956        TextAlign::Left => origin.x,
957        TextAlign::Center => (origin.x * 2.0 + align_width - line_width) / 2.0,
958        TextAlign::Right => origin.x + align_width - line_width,
959    }
960}
961
962#[cfg(test)]
963mod tests {
964    use super::*;
965    use crate::{
966        AppContext as _, Context, FontId, GlyphId, IntoElement, Render, ShapedGlyph, ShapedRun,
967        Styled, TestAppContext, TextRun, Underline, canvas, font, hsla,
968    };
969    use std::rc::Rc;
970
971    /// Helper: build a ShapedLine from glyph descriptors without the platform text system.
972    /// Each glyph is described as (byte_index, x_position).
973    fn make_shaped_line(
974        text: &str,
975        glyphs: &[(usize, f32)],
976        width: f32,
977        decorations: &[DecorationRun],
978    ) -> ShapedLine {
979        let shaped_glyphs: Vec<ShapedGlyph> = glyphs
980            .iter()
981            .map(|&(index, x)| ShapedGlyph {
982                id: GlyphId(0),
983                position: point(px(x), px(0.0)),
984                index,
985                is_emoji: false,
986            })
987            .collect();
988
989        ShapedLine {
990            layout: Arc::new(LineLayout {
991                font_size: px(16.0),
992                width: px(width),
993                ascent: px(12.0),
994                descent: px(4.0),
995                runs: vec![ShapedRun {
996                    font_id: FontId(0),
997                    glyphs: shaped_glyphs,
998                }],
999                len: text.len(),
1000            }),
1001            text: SharedString::new(text),
1002            decoration_runs: SmallVec::from(decorations.to_vec()),
1003        }
1004    }
1005
1006    #[gpui::test]
1007    fn test_underline_handler_matches_default_paint(cx: &mut TestAppContext) {
1008        test_underline_handler_at_scales(cx, |window, cx| {
1009            let first_style = UnderlineStyle {
1010                thickness: px(1.),
1011                color: Some(hsla(0., 1., 0.5, 1.)),
1012                wavy: true,
1013            };
1014            let last_style = UnderlineStyle {
1015                color: Some(hsla(0.5, 1., 0.5, 1.)),
1016                wavy: false,
1017                ..first_style
1018            };
1019            let fallback_style = UnderlineStyle {
1020                color: Some(black()),
1021                ..first_style
1022            };
1023            let decoration = DecorationRun {
1024                len: 1,
1025                color: black(),
1026                background_color: None,
1027                underline: Some(first_style),
1028                strikethrough: None,
1029            };
1030            let line = underline_test_line(
1031                "aébcde",
1032                &[
1033                    decoration.clone(),
1034                    DecorationRun {
1035                        len: 2,
1036                        ..decoration.clone()
1037                    },
1038                    DecorationRun {
1039                        underline: None,
1040                        ..decoration.clone()
1041                    },
1042                    DecorationRun {
1043                        underline: Some(UnderlineStyle {
1044                            color: None,
1045                            ..first_style
1046                        }),
1047                        ..decoration.clone()
1048                    },
1049                    DecorationRun {
1050                        underline: Some(last_style),
1051                        ..decoration.clone()
1052                    },
1053                    DecorationRun {
1054                        underline: Some(last_style),
1055                        ..decoration
1056                    },
1057                ],
1058                false,
1059                window,
1060            );
1061            assert_eq!(line.width(), px(48.));
1062            for origin_x in [-3.25, 0., 4.25] {
1063                for (align, align_width, offset) in [
1064                    (TextAlign::Left, None, 0.),
1065                    (TextAlign::Center, None, 0.),
1066                    (TextAlign::Right, None, 0.),
1067                    (TextAlign::Left, Some(px(96.)), 0.),
1068                    (TextAlign::Center, Some(px(96.)), 24.),
1069                    (TextAlign::Right, Some(px(96.)), 48.),
1070                ] {
1071                    let origin = point(px(origin_x), px(12.25));
1072                    let line_height = px(20.);
1073                    window.next_frame.scene.clear();
1074                    line.paint(origin, line_height, align, align_width, window, cx)
1075                        .unwrap();
1076                    let original = window.next_frame.scene.underlines.clone();
1077                    window.next_frame.scene.clear();
1078                    line.layout
1079                        .paint(
1080                            origin,
1081                            line_height,
1082                            align,
1083                            align_width,
1084                            &line.decoration_runs,
1085                            window,
1086                            cx,
1087                        )
1088                        .unwrap();
1089                    assert_underline_primitives_eq(&window.next_frame.scene.underlines, &original);
1090
1091                    window.next_frame.scene.clear();
1092                    let mut strokes = Vec::new();
1093                    line.paint_with_underline_handler(
1094                        origin,
1095                        line_height,
1096                        align,
1097                        align_width,
1098                        window,
1099                        cx,
1100                        |range, origin, width, style, window| {
1101                            strokes.push((range, origin, width, *style));
1102                            window.paint_underline(origin, width, style);
1103                        },
1104                    )
1105                    .unwrap();
1106                    let start = px(origin_x + offset);
1107                    let y = origin.y + underline_y_offset(line_height, line.ascent, line.descent);
1108                    assert_eq!(
1109                        strokes,
1110                        [
1111                            (0..3, point(start, y), px(16.), first_style),
1112                            (4..5, point(start + px(24.), y), px(8.), fallback_style),
1113                            (5..7, point(start + px(32.), y), px(16.), last_style),
1114                        ]
1115                    );
1116                    assert_underline_primitives_eq(&window.next_frame.scene.underlines, &original);
1117
1118                    window.next_frame.scene.clear();
1119                    let mut captured = Vec::new();
1120                    line.paint_with_underline_handler(
1121                        origin,
1122                        line_height,
1123                        align,
1124                        align_width,
1125                        window,
1126                        cx,
1127                        |range, origin, width, style, _| {
1128                            captured.push((range, origin, width, *style))
1129                        },
1130                    )
1131                    .unwrap();
1132                    assert_eq!(captured, strokes);
1133                    assert_eq!(window.next_frame.scene.underlines.len(), 0);
1134                }
1135            }
1136            for text in ["", "abc"] {
1137                let line = underline_test_line(text, &[], false, window);
1138                let mut calls = 0;
1139                line.paint_with_underline_handler(
1140                    point(px(4.), px(10.)),
1141                    px(20.),
1142                    TextAlign::Left,
1143                    None,
1144                    window,
1145                    cx,
1146                    |_, _, _, _, _| calls += 1,
1147                )
1148                .unwrap();
1149                assert_eq!(calls, 0);
1150            }
1151        });
1152    }
1153
1154    #[gpui::test]
1155    fn test_underline_handler_reports_zero_advance_geometry(cx: &mut TestAppContext) {
1156        test_underline_handler_at_scales(cx, |window, cx| {
1157            let first_style = UnderlineStyle {
1158                thickness: px(1.),
1159                color: Some(black()),
1160                wavy: true,
1161            };
1162            let last_style = UnderlineStyle {
1163                wavy: false,
1164                ..first_style
1165            };
1166            let decoration = DecorationRun {
1167                len: 1,
1168                color: black(),
1169                background_color: None,
1170                underline: Some(first_style),
1171                strikethrough: None,
1172            };
1173            let line = underline_test_line(
1174                "ab",
1175                &[
1176                    decoration.clone(),
1177                    DecorationRun {
1178                        underline: Some(last_style),
1179                        ..decoration
1180                    },
1181                ],
1182                true,
1183                window,
1184            );
1185            let half_width = cx
1186                .text_system()
1187                .bounding_box(line.runs[0].font_id, line.font_size)
1188                .size
1189                .width
1190                / 2.;
1191            let origin = point(px(40.25), px(10.25));
1192            let line_height = px(20.);
1193            let y = origin.y + underline_y_offset(line_height, line.ascent, line.descent);
1194            for (align, offset) in [
1195                (TextAlign::Left, 0.),
1196                (TextAlign::Center, 16.),
1197                (TextAlign::Right, 32.),
1198            ] {
1199                window.next_frame.scene.clear();
1200                line.paint(origin, line_height, align, Some(px(32.)), window, cx)
1201                    .unwrap();
1202                let original = window.next_frame.scene.underlines.clone();
1203                window.next_frame.scene.clear();
1204                let mut strokes = Vec::new();
1205                line.paint_with_underline_handler(
1206                    origin,
1207                    line_height,
1208                    align,
1209                    Some(px(32.)),
1210                    window,
1211                    cx,
1212                    |range, origin, width, style, window| {
1213                        strokes.push((range, origin, width, *style));
1214                        window.paint_underline(origin, width, style);
1215                    },
1216                )
1217                .unwrap();
1218                let end = origin.x + px(offset);
1219                let start = point(end - half_width, y);
1220                let width = end - start.x;
1221                assert_eq!(
1222                    strokes,
1223                    [
1224                        (0..1, start, width, first_style),
1225                        (1..2, start, width, last_style),
1226                    ]
1227                );
1228                assert_underline_primitives_eq(&window.next_frame.scene.underlines, &original);
1229            }
1230        });
1231    }
1232
1233    #[gpui::test]
1234    fn test_underline_handler_matches_wrapped_paint(cx: &mut TestAppContext) {
1235        test_underline_handler_at_scales(cx, |window, cx| {
1236            let style = UnderlineStyle {
1237                thickness: px(1.),
1238                color: Some(black()),
1239                wavy: true,
1240            };
1241            for zero_advance in [false, true] {
1242                let line = underline_test_line(
1243                    "abcd",
1244                    &[DecorationRun {
1245                        len: 4,
1246                        color: black(),
1247                        background_color: None,
1248                        underline: Some(style),
1249                        strikethrough: None,
1250                    }],
1251                    zero_advance,
1252                    window,
1253                );
1254                let half_width = cx
1255                    .text_system()
1256                    .bounding_box(line.runs[0].font_id, line.font_size)
1257                    .size
1258                    .width
1259                    / 2.;
1260                let origin = point(px(40.25), px(10.25));
1261                let line_height = px(20.);
1262                let y = origin.y + underline_y_offset(line_height, line.ascent, line.descent);
1263                let wrapped = WrappedLine {
1264                    layout: Arc::new(WrappedLineLayout {
1265                        unwrapped_layout: line.layout,
1266                        wrap_boundaries: SmallVec::from_buf([WrapBoundary {
1267                            run_ix: 0,
1268                            glyph_ix: 2,
1269                        }]),
1270                        wrap_width: Some(px(16.)),
1271                    }),
1272                    text: line.text,
1273                    decoration_runs: line.decoration_runs.into_vec(),
1274                };
1275                window.next_frame.scene.clear();
1276                wrapped
1277                    .paint(origin, line_height, TextAlign::Left, None, window, cx)
1278                    .unwrap();
1279                let original = window.next_frame.scene.underlines.clone();
1280                window.next_frame.scene.clear();
1281                let mut strokes = Vec::new();
1282                paint_line(
1283                    origin,
1284                    &wrapped.unwrapped_layout,
1285                    line_height,
1286                    TextAlign::Left,
1287                    Some(px(16.)),
1288                    &wrapped.decoration_runs,
1289                    &wrapped.wrap_boundaries,
1290                    window,
1291                    cx,
1292                    &mut |range, origin, width, style, window| {
1293                        strokes.push((range, origin, width, *style));
1294                        window.paint_underline(origin, width, style);
1295                    },
1296                )
1297                .unwrap();
1298                let (start, width) = if zero_advance {
1299                    let start = origin.x - half_width;
1300                    (start, origin.x - start)
1301                } else {
1302                    (origin.x, px(16.))
1303                };
1304                assert_eq!(
1305                    strokes,
1306                    [
1307                        (0..4, point(start, y), width, style),
1308                        (0..4, point(start, y + line_height), width, style),
1309                    ]
1310                );
1311                assert_underline_primitives_eq(&window.next_frame.scene.underlines, &original);
1312            }
1313        });
1314    }
1315
1316    #[test]
1317    fn test_split_at_invariants() {
1318        // Split "abcdef" at every possible byte index and verify structural invariants.
1319        let line = make_shaped_line(
1320            "abcdef",
1321            &[
1322                (0, 0.0),
1323                (1, 10.0),
1324                (2, 20.0),
1325                (3, 30.0),
1326                (4, 40.0),
1327                (5, 50.0),
1328            ],
1329            60.0,
1330            &[],
1331        );
1332
1333        for i in 0..=6 {
1334            let (left, right) = line.split_at(i);
1335
1336            assert_eq!(
1337                left.width() + right.width(),
1338                line.width(),
1339                "widths must sum at split={i}"
1340            );
1341            assert_eq!(
1342                left.len() + right.len(),
1343                line.len(),
1344                "lengths must sum at split={i}"
1345            );
1346            assert_eq!(
1347                format!("{}{}", left.text.as_ref(), right.text.as_ref()),
1348                "abcdef",
1349                "text must concatenate at split={i}"
1350            );
1351            assert_eq!(left.font_size, line.font_size, "font_size at split={i}");
1352            assert_eq!(right.ascent, line.ascent, "ascent at split={i}");
1353            assert_eq!(right.descent, line.descent, "descent at split={i}");
1354        }
1355
1356        // Edge: split at 0 produces no left runs, full content on right
1357        let (left, right) = line.split_at(0);
1358        assert_eq!(left.runs.len(), 0);
1359        assert_eq!(right.runs[0].glyphs.len(), 6);
1360
1361        // Edge: split at end produces full content on left, no right runs
1362        let (left, right) = line.split_at(6);
1363        assert_eq!(left.runs[0].glyphs.len(), 6);
1364        assert_eq!(right.runs.len(), 0);
1365    }
1366
1367    #[test]
1368    fn test_split_at_glyph_rebasing() {
1369        // Two font runs (simulating a font fallback boundary at byte 3):
1370        //   run A (FontId 0): glyphs at bytes 0,1,2  positions 0,10,20
1371        //   run B (FontId 1): glyphs at bytes 3,4,5  positions 30,40,50
1372        // Successive splits simulate the incremental splitting done during wrap.
1373        let line = ShapedLine {
1374            layout: Arc::new(LineLayout {
1375                font_size: px(16.0),
1376                width: px(60.0),
1377                ascent: px(12.0),
1378                descent: px(4.0),
1379                runs: vec![
1380                    ShapedRun {
1381                        font_id: FontId(0),
1382                        glyphs: vec![
1383                            ShapedGlyph {
1384                                id: GlyphId(0),
1385                                position: point(px(0.0), px(0.0)),
1386                                index: 0,
1387                                is_emoji: false,
1388                            },
1389                            ShapedGlyph {
1390                                id: GlyphId(0),
1391                                position: point(px(10.0), px(0.0)),
1392                                index: 1,
1393                                is_emoji: false,
1394                            },
1395                            ShapedGlyph {
1396                                id: GlyphId(0),
1397                                position: point(px(20.0), px(0.0)),
1398                                index: 2,
1399                                is_emoji: false,
1400                            },
1401                        ],
1402                    },
1403                    ShapedRun {
1404                        font_id: FontId(1),
1405                        glyphs: vec![
1406                            ShapedGlyph {
1407                                id: GlyphId(0),
1408                                position: point(px(30.0), px(0.0)),
1409                                index: 3,
1410                                is_emoji: false,
1411                            },
1412                            ShapedGlyph {
1413                                id: GlyphId(0),
1414                                position: point(px(40.0), px(0.0)),
1415                                index: 4,
1416                                is_emoji: false,
1417                            },
1418                            ShapedGlyph {
1419                                id: GlyphId(0),
1420                                position: point(px(50.0), px(0.0)),
1421                                index: 5,
1422                                is_emoji: false,
1423                            },
1424                        ],
1425                    },
1426                ],
1427                len: 6,
1428            }),
1429            text: "abcdef".into(),
1430            decoration_runs: SmallVec::new(),
1431        };
1432
1433        // First split at byte 2 — mid-run in run A
1434        let (first, remainder) = line.split_at(2);
1435        assert_eq!(first.text.as_ref(), "ab");
1436        assert_eq!(first.runs.len(), 1);
1437        assert_eq!(first.runs[0].font_id, FontId(0));
1438
1439        // Remainder "cdef" should have two runs: tail of A (1 glyph) + all of B (3 glyphs)
1440        assert_eq!(remainder.text.as_ref(), "cdef");
1441        assert_eq!(remainder.runs.len(), 2);
1442        assert_eq!(remainder.runs[0].font_id, FontId(0));
1443        assert_eq!(remainder.runs[0].glyphs.len(), 1);
1444        assert_eq!(remainder.runs[0].glyphs[0].index, 0);
1445        assert_eq!(remainder.runs[0].glyphs[0].position.x, px(0.0));
1446        assert_eq!(remainder.runs[1].font_id, FontId(1));
1447        assert_eq!(remainder.runs[1].glyphs[0].index, 1);
1448        assert_eq!(remainder.runs[1].glyphs[0].position.x, px(10.0));
1449
1450        // Second split at byte 2 within remainder — crosses the run boundary
1451        let (second, final_part) = remainder.split_at(2);
1452        assert_eq!(second.text.as_ref(), "cd");
1453        assert_eq!(final_part.text.as_ref(), "ef");
1454        assert_eq!(final_part.runs[0].glyphs[0].index, 0);
1455        assert_eq!(final_part.runs[0].glyphs[0].position.x, px(0.0));
1456
1457        // Widths must sum across all three pieces
1458        assert_eq!(
1459            first.width() + second.width() + final_part.width(),
1460            line.width()
1461        );
1462    }
1463
1464    #[test]
1465    fn test_split_at_decorations() {
1466        // Three decoration runs: red [0..2), green [2..5), blue [5..6).
1467        // Split at byte 3 — red goes entirely left, green straddles, blue goes entirely right.
1468        let red = Hsla {
1469            h: 0.0,
1470            s: 1.0,
1471            l: 0.5,
1472            a: 1.0,
1473        };
1474        let green = Hsla {
1475            h: 0.3,
1476            s: 1.0,
1477            l: 0.5,
1478            a: 1.0,
1479        };
1480        let blue = Hsla {
1481            h: 0.6,
1482            s: 1.0,
1483            l: 0.5,
1484            a: 1.0,
1485        };
1486
1487        let line = make_shaped_line(
1488            "abcdef",
1489            &[
1490                (0, 0.0),
1491                (1, 10.0),
1492                (2, 20.0),
1493                (3, 30.0),
1494                (4, 40.0),
1495                (5, 50.0),
1496            ],
1497            60.0,
1498            &[
1499                DecorationRun {
1500                    len: 2,
1501                    color: red,
1502                    background_color: None,
1503                    underline: None,
1504                    strikethrough: None,
1505                },
1506                DecorationRun {
1507                    len: 3,
1508                    color: green,
1509                    background_color: None,
1510                    underline: None,
1511                    strikethrough: None,
1512                },
1513                DecorationRun {
1514                    len: 1,
1515                    color: blue,
1516                    background_color: None,
1517                    underline: None,
1518                    strikethrough: None,
1519                },
1520            ],
1521        );
1522
1523        let (left, right) = line.split_at(3);
1524
1525        // Left: red(2) + green(1) — green straddled, left portion has len 1
1526        assert_eq!(left.decoration_runs.len(), 2);
1527        assert_eq!(left.decoration_runs[0].len, 2);
1528        assert_eq!(left.decoration_runs[0].color, red);
1529        assert_eq!(left.decoration_runs[1].len, 1);
1530        assert_eq!(left.decoration_runs[1].color, green);
1531
1532        // Right: green(2) + blue(1) — green straddled, right portion has len 2
1533        assert_eq!(right.decoration_runs.len(), 2);
1534        assert_eq!(right.decoration_runs[0].len, 2);
1535        assert_eq!(right.decoration_runs[0].color, green);
1536        assert_eq!(right.decoration_runs[1].len, 1);
1537        assert_eq!(right.decoration_runs[1].color, blue);
1538    }
1539
1540    struct UnderlineHandlerTestView(Rc<dyn Fn(&mut Window, &mut App)>);
1541
1542    impl Render for UnderlineHandlerTestView {
1543        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1544            let paint = self.0.clone();
1545            canvas(
1546                |_, _, _| {},
1547                move |_, _, window, cx| {
1548                    window.with_element_opacity(Some(0.5), |window| paint(window, cx));
1549                },
1550            )
1551            .size_full()
1552        }
1553    }
1554
1555    fn test_underline_handler_at_scales(
1556        cx: &mut TestAppContext,
1557        paint: impl Fn(&mut Window, &mut App) + 'static,
1558    ) {
1559        let window = cx.add_window(move |_, _| UnderlineHandlerTestView(Rc::new(paint)));
1560        for scale in [1., 1.25, 1.5, 2., 3.] {
1561            cx.simulate_window_scale_factor_change(window.into(), scale);
1562            cx.update_window(window.into(), |_, window, cx| window.draw(cx).clear(cx))
1563                .unwrap();
1564        }
1565    }
1566
1567    fn underline_test_line(
1568        text: &str,
1569        decorations: &[DecorationRun],
1570        zero_advance: bool,
1571        window: &Window,
1572    ) -> ShapedLine {
1573        let mut line = window.text_system().shape_line(
1574            SharedString::new(text),
1575            px(16.),
1576            &[TextRun {
1577                len: text.len(),
1578                font: font(".ZedMono"),
1579                color: black(),
1580                ..TextRun::default()
1581            }],
1582            None,
1583        );
1584        line.decoration_runs = SmallVec::from(decorations.to_vec());
1585        let layout = &line.layout;
1586        let mut runs = layout.runs.clone();
1587        let advance = if zero_advance { px(0.) } else { px(8.) };
1588        let mut width = px(0.);
1589        for glyph in runs.iter_mut().flat_map(|run| &mut run.glyphs) {
1590            glyph.position.x = width;
1591            width += advance;
1592        }
1593        line.layout = Arc::new(LineLayout {
1594            font_size: layout.font_size,
1595            width,
1596            ascent: layout.ascent,
1597            descent: layout.descent,
1598            runs,
1599            len: layout.len,
1600        });
1601        line
1602    }
1603
1604    fn assert_underline_primitives_eq(actual: &[Underline], expected: &[Underline]) {
1605        assert_eq!(actual.len(), expected.len());
1606        for (actual, expected) in actual.iter().zip(expected) {
1607            assert_eq!(actual.bounds, expected.bounds);
1608            assert_eq!(actual.content_mask, expected.content_mask);
1609            assert_eq!(actual.color, expected.color);
1610            assert_eq!(actual.thickness, expected.thickness);
1611            assert_eq!(actual.wavy, expected.wavy);
1612            assert_eq!(actual.order, expected.order);
1613            assert_eq!(actual.pad, expected.pad);
1614        }
1615    }
1616
1617    #[test]
1618    fn test_cursor_preserves_shaping_metadata_across_runs() {
1619        let line = ShapedLine {
1620            layout: Arc::new(LineLayout {
1621                font_size: px(16.0),
1622                width: px(50.0),
1623                ascent: px(12.0),
1624                descent: px(4.0),
1625                runs: vec![
1626                    ShapedRun {
1627                        font_id: FontId(3),
1628                        glyphs: vec![
1629                            ShapedGlyph {
1630                                id: GlyphId(11),
1631                                position: point(px(0.0), px(1.0)),
1632                                index: 0,
1633                                is_emoji: true,
1634                            },
1635                            ShapedGlyph {
1636                                id: GlyphId(12),
1637                                position: point(px(17.0), px(1.0)),
1638                                index: 1,
1639                                is_emoji: false,
1640                            },
1641                            ShapedGlyph {
1642                                id: GlyphId(13),
1643                                position: point(px(19.0), px(-1.0)),
1644                                index: 1,
1645                                is_emoji: false,
1646                            },
1647                        ],
1648                    },
1649                    ShapedRun {
1650                        font_id: FontId(8),
1651                        glyphs: vec![
1652                            ShapedGlyph {
1653                                id: GlyphId(21),
1654                                position: point(px(25.0), px(1.0)),
1655                                index: 5,
1656                                is_emoji: true,
1657                            },
1658                            ShapedGlyph {
1659                                id: GlyphId(22),
1660                                position: point(px(41.0), px(1.0)),
1661                                index: 7,
1662                                is_emoji: false,
1663                            },
1664                        ],
1665                    },
1666                ],
1667                len: 10,
1668            }),
1669            text: "a😀bcdef".into(),
1670            decoration_runs: SmallVec::new(),
1671        };
1672        let mut cursor = line.cursor();
1673        let first = cursor.take_until(5);
1674        assert_eq!(first.text.as_ref(), "a😀");
1675        assert_eq!(first.runs[0].font_id, FontId(3));
1676        assert_eq!(first.runs[0].glyphs[0].id, GlyphId(11));
1677        assert!(first.runs[0].glyphs[0].is_emoji);
1678        assert_eq!(first.runs[0].glyphs[1].index, 1);
1679        assert_eq!(first.runs[0].glyphs[1].position, point(px(17.0), px(1.0)));
1680        assert_eq!(first.runs[0].glyphs.len(), 3);
1681        assert_eq!(first.runs[0].glyphs[2].index, 1);
1682        assert_eq!(first.runs[0].glyphs[2].position, point(px(19.0), px(-1.0)));
1683        assert_eq!(cursor.x_offset(), px(25.0));
1684
1685        let second = cursor.take_until(7);
1686        assert_eq!(second.text.as_ref(), "bc");
1687        assert_eq!(second.runs[0].font_id, FontId(8));
1688        assert_eq!(second.runs[0].glyphs[0].id, GlyphId(21));
1689        assert_eq!(second.runs[0].glyphs[0].index, 0);
1690        assert_eq!(second.runs[0].glyphs[0].position, point(px(0.0), px(1.0)));
1691        assert_eq!(cursor.x_offset(), px(41.0));
1692
1693        let final_part = cursor.take_until(10);
1694        assert_eq!(final_part.text.as_ref(), "def");
1695        assert_eq!(final_part.runs[0].font_id, FontId(8));
1696        assert_eq!(final_part.runs[0].glyphs[0].id, GlyphId(22));
1697        assert_eq!(final_part.runs[0].glyphs[0].index, 0);
1698        assert_eq!(
1699            final_part.runs[0].glyphs[0].position,
1700            point(px(0.0), px(1.0))
1701        );
1702    }
1703
1704    #[test]
1705    fn test_cursor_preserves_existing_visual_order_splitting() {
1706        let line = make_shaped_line("abc", &[(0, 0.0), (2, 10.0), (1, 20.0)], 30.0, &[]);
1707        let mut cursor = line.cursor();
1708        let mut remainder = line.clone();
1709        let mut previous_boundary = 0;
1710        for boundary in [0, 1, 2, 3] {
1711            let (expected, rest) = remainder.split_at(boundary - previous_boundary);
1712            let actual = cursor.take_until(boundary);
1713            assert_eq!(actual.text, expected.text);
1714            assert_eq!(actual.width(), expected.width());
1715            assert_eq!(actual.runs.len(), expected.runs.len());
1716            for (actual, expected) in actual.runs.iter().zip(&expected.runs) {
1717                assert_eq!(actual.font_id, expected.font_id);
1718                assert_eq!(actual.glyphs.len(), expected.glyphs.len());
1719                for (actual, expected) in actual.glyphs.iter().zip(&expected.glyphs) {
1720                    assert_eq!(actual.id, expected.id);
1721                    assert_eq!(actual.index, expected.index);
1722                    assert_eq!(actual.position, expected.position);
1723                }
1724            }
1725            assert_eq!(cursor.x_offset(), line.x_for_index(boundary));
1726            remainder = rest;
1727            previous_boundary = boundary;
1728        }
1729    }
1730
1731    #[test]
1732    fn test_cursor_partitions_one_decoration_across_three_chunks() {
1733        let line = make_shaped_line(
1734            "abcdef",
1735            &[
1736                (0, 0.0),
1737                (1, 10.0),
1738                (2, 20.0),
1739                (3, 30.0),
1740                (4, 40.0),
1741                (5, 50.0),
1742            ],
1743            60.0,
1744            &[DecorationRun {
1745                len: 6,
1746                color: Hsla {
1747                    h: 0.2,
1748                    s: 0.4,
1749                    l: 0.6,
1750                    a: 1.0,
1751                },
1752                background_color: None,
1753                underline: None,
1754                strikethrough: None,
1755            }],
1756        );
1757        let mut cursor = line.cursor();
1758        assert_eq!(cursor.take_until(2).decoration_runs[0].len, 2);
1759        assert_eq!(cursor.take_until(4).decoration_runs[0].len, 2);
1760        assert_eq!(cursor.take_until(6).decoration_runs[0].len, 2);
1761    }
1762
1763    #[test]
1764    fn test_cursor_matches_successive_splits_at_ordered_boundaries() {
1765        let decorations: Vec<_> = [2, 0, 3, 1]
1766            .into_iter()
1767            .map(|len| DecorationRun {
1768                len,
1769                color: Hsla {
1770                    h: len as f32 / 10.0,
1771                    s: 0.5,
1772                    l: 0.5,
1773                    a: 1.0,
1774                },
1775                background_color: Some(black()),
1776                underline: None,
1777                strikethrough: None,
1778            })
1779            .collect();
1780        let line = make_shaped_line(
1781            "abcdef",
1782            &[(0, 5.0), (0, 5.0), (2, 15.0), (4, 25.0), (5, 35.0)],
1783            45.0,
1784            &decorations,
1785        );
1786        for first in 0..=line.len() {
1787            for second in first..=line.len() {
1788                let mut cursor = line.cursor();
1789                let mut remainder = line.clone();
1790                let mut previous_boundary = 0;
1791                let mut total_width = px(0.0);
1792                let mut text = String::new();
1793                for boundary in [first, second, line.len(), line.len()] {
1794                    let (expected, rest) = remainder.split_at(boundary - previous_boundary);
1795                    let actual = cursor.take_until(boundary);
1796                    assert_eq!(actual.text, expected.text);
1797                    assert_eq!(actual.len(), expected.len());
1798                    assert_eq!(actual.width(), expected.width());
1799                    assert_eq!(actual.runs.len(), expected.runs.len());
1800                    for (actual, expected) in actual.runs.iter().zip(&expected.runs) {
1801                        assert_eq!(actual.font_id, expected.font_id);
1802                        assert_eq!(actual.glyphs.len(), expected.glyphs.len());
1803                        for (actual, expected) in actual.glyphs.iter().zip(&expected.glyphs) {
1804                            assert_eq!(actual.id, expected.id);
1805                            assert_eq!(actual.index, expected.index);
1806                            assert_eq!(actual.position, expected.position);
1807                        }
1808                    }
1809                    assert_eq!(actual.decoration_runs.len(), expected.decoration_runs.len());
1810                    for (actual, expected) in
1811                        actual.decoration_runs.iter().zip(&expected.decoration_runs)
1812                    {
1813                        assert_eq!(actual.len, expected.len);
1814                        assert_eq!(actual.color, expected.color);
1815                        assert_eq!(actual.background_color, expected.background_color);
1816                    }
1817                    total_width += actual.width();
1818                    text.push_str(&actual.text);
1819                    remainder = rest;
1820                    previous_boundary = boundary;
1821                }
1822                assert_eq!(total_width, line.width());
1823                assert_eq!(text, line.text.as_ref());
1824            }
1825        }
1826    }
1827
1828    #[test]
1829    fn test_cursor_empty_chunks_and_repeated_boundaries() {
1830        let line = make_shaped_line("ab", &[(0, 5.0), (1, 15.0)], 20.0, &[]);
1831        let mut cursor = line.cursor();
1832        assert_eq!(cursor.take_until(0).text.as_ref(), "");
1833        assert_eq!(cursor.take_until(0).text.as_ref(), "");
1834        assert_eq!(cursor.take_until(1).text.as_ref(), "a");
1835        assert_eq!(cursor.take_until(2).text.as_ref(), "b");
1836        assert_eq!(cursor.take_until(2).text.as_ref(), "");
1837        let empty = make_shaped_line("", &[], 0.0, &[]);
1838        let piece = empty.cursor().take_until(0);
1839        assert!(piece.text.is_empty());
1840        assert!(piece.runs.is_empty());
1841        assert_eq!(piece.width(), px(0.0));
1842    }
1843
1844    #[test]
1845    fn test_cursor_rejects_invalid_boundaries() {
1846        let line = make_shaped_line("é", &[(0, 0.0)], 10.0, &[]);
1847        let mut cursor = line.cursor();
1848        assert!(
1849            std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1850                cursor.take_until(1);
1851            }))
1852            .is_err()
1853        );
1854        let mut cursor = line.cursor();
1855        cursor.take_until(2);
1856        assert!(
1857            std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1858                cursor.take_until(0);
1859            }))
1860            .is_err()
1861        );
1862        let mut cursor = line.cursor();
1863        assert!(
1864            std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1865                cursor.take_until(3);
1866            }))
1867            .is_err()
1868        );
1869    }
1870}