Skip to main content

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