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,
5};
6use derive_more::{Deref, DerefMut};
7use smallvec::SmallVec;
8use std::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    /// The length of the line in utf-8 bytes.
54    #[allow(clippy::len_without_is_empty)]
55    pub fn len(&self) -> usize {
56        self.layout.len
57    }
58
59    /// The width of the shaped line in pixels.
60    ///
61    /// This is the glyph advance width computed by the text shaping system and is useful for
62    /// incrementally advancing a "pen" when painting multiple fragments on the same row.
63    pub fn width(&self) -> Pixels {
64        self.layout.width
65    }
66
67    /// Override the len, useful if you're rendering text a
68    /// as text b (e.g. rendering invisibles).
69    pub fn with_len(mut self, len: usize) -> Self {
70        let layout = self.layout.as_ref();
71        self.layout = Arc::new(LineLayout {
72            font_size: layout.font_size,
73            width: layout.width,
74            ascent: layout.ascent,
75            descent: layout.descent,
76            runs: layout.runs.clone(),
77            len,
78        });
79        self
80    }
81
82    /// Paint the line of text to the window.
83    pub fn paint(
84        &self,
85        origin: Point<Pixels>,
86        line_height: Pixels,
87        align: TextAlign,
88        align_width: Option<Pixels>,
89        window: &mut Window,
90        cx: &mut App,
91    ) -> Result<()> {
92        paint_line(
93            origin,
94            &self.layout,
95            line_height,
96            align,
97            align_width,
98            &self.decoration_runs,
99            &[],
100            window,
101            cx,
102        )?;
103
104        Ok(())
105    }
106
107    /// Paint the background of the line to the window.
108    pub fn paint_background(
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        paint_line_background(
118            origin,
119            &self.layout,
120            line_height,
121            align,
122            align_width,
123            &self.decoration_runs,
124            &[],
125            window,
126            cx,
127        )?;
128
129        Ok(())
130    }
131
132    /// Split this shaped line at a byte index, returning `(prefix, suffix)`.
133    ///
134    /// - `prefix` contains glyphs for bytes `[0, byte_index)` with original positions.
135    ///   Its width equals the x-advance up to the split point.
136    /// - `suffix` contains glyphs for bytes `[byte_index, len)` with positions
137    ///   shifted left so the first glyph starts at x=0, and byte indices rebased to 0.
138    /// - Decoration runs are partitioned at the boundary; a run that straddles it is
139    ///   split into two with adjusted lengths.
140    /// - `font_size`, `ascent`, and `descent` are copied to both halves.
141    pub fn split_at(&self, byte_index: usize) -> (ShapedLine, ShapedLine) {
142        let (left_layout, right_layout) = self.layout.split_at(byte_index);
143
144        // Partition decoration runs. A run straddling the boundary is split into two.
145        let mut left_decorations = SmallVec::new();
146        let mut right_decorations = SmallVec::new();
147        let mut decoration_offset = 0u32;
148        let split_point = byte_index as u32;
149
150        for decoration in &self.decoration_runs {
151            let run_end = decoration_offset + decoration.len;
152
153            if run_end <= split_point {
154                left_decorations.push(decoration.clone());
155            } else if decoration_offset >= split_point {
156                right_decorations.push(decoration.clone());
157            } else {
158                let left_len = split_point - decoration_offset;
159                let right_len = run_end - split_point;
160                left_decorations.push(DecorationRun {
161                    len: left_len,
162                    color: decoration.color,
163                    background_color: decoration.background_color,
164                    underline: decoration.underline,
165                    strikethrough: decoration.strikethrough,
166                });
167                right_decorations.push(DecorationRun {
168                    len: right_len,
169                    color: decoration.color,
170                    background_color: decoration.background_color,
171                    underline: decoration.underline,
172                    strikethrough: decoration.strikethrough,
173                });
174            }
175
176            decoration_offset = run_end;
177        }
178
179        // Split text
180        let left_text = if byte_index == self.text.len() {
181            self.text.clone()
182        } else {
183            SharedString::new(&self.text[..byte_index])
184        };
185        let right_text = if byte_index == 0 {
186            self.text.clone()
187        } else {
188            SharedString::new(&self.text[byte_index..])
189        };
190
191        let left = ShapedLine {
192            layout: Arc::new(left_layout),
193            text: left_text,
194            decoration_runs: left_decorations,
195        };
196
197        let right = ShapedLine {
198            layout: Arc::new(right_layout),
199            text: right_text,
200            decoration_runs: right_decorations,
201        };
202
203        (left, right)
204    }
205}
206
207impl LineLayout {
208    /// Paint this layout to the window, using the given decoration runs to color
209    /// glyphs and draw underlines and strikethroughs.
210    ///
211    /// This is a lower-level alternative to [`ShapedLine::paint`] for callers that
212    /// hold a bare layout and track decorations themselves.
213    pub fn paint(
214        &self,
215        origin: Point<Pixels>,
216        line_height: Pixels,
217        align: TextAlign,
218        align_width: Option<Pixels>,
219        decoration_runs: &[DecorationRun],
220        window: &mut Window,
221        cx: &mut App,
222    ) -> Result<()> {
223        paint_line(
224            origin,
225            self,
226            line_height,
227            align,
228            align_width,
229            decoration_runs,
230            &[],
231            window,
232            cx,
233        )
234    }
235
236    /// Paint the background of this layout to the window, using the given
237    /// decoration runs to determine background colors.
238    ///
239    /// This is a lower-level alternative to [`ShapedLine::paint_background`] for
240    /// callers that hold a bare layout and track decorations themselves.
241    pub fn paint_background(
242        &self,
243        origin: Point<Pixels>,
244        line_height: Pixels,
245        align: TextAlign,
246        align_width: Option<Pixels>,
247        decoration_runs: &[DecorationRun],
248        window: &mut Window,
249        cx: &mut App,
250    ) -> Result<()> {
251        paint_line_background(
252            origin,
253            self,
254            line_height,
255            align,
256            align_width,
257            decoration_runs,
258            &[],
259            window,
260            cx,
261        )
262    }
263}
264
265/// A line of text that has been shaped, decorated, and wrapped by the text layout system.
266#[derive(Default, Debug, Deref, DerefMut)]
267pub struct WrappedLine {
268    #[deref]
269    #[deref_mut]
270    pub(crate) layout: Arc<WrappedLineLayout>,
271    /// The text that was shaped for this line.
272    pub text: SharedString,
273    pub(crate) decoration_runs: Vec<DecorationRun>,
274}
275
276impl WrappedLine {
277    /// The length of the underlying, unwrapped layout, in utf-8 bytes.
278    #[allow(clippy::len_without_is_empty)]
279    pub fn len(&self) -> usize {
280        self.layout.len()
281    }
282
283    /// Paint this line of text to the window.
284    pub fn paint(
285        &self,
286        origin: Point<Pixels>,
287        line_height: Pixels,
288        align: TextAlign,
289        bounds: Option<Bounds<Pixels>>,
290        window: &mut Window,
291        cx: &mut App,
292    ) -> Result<()> {
293        let align_width = match bounds {
294            Some(bounds) => Some(bounds.size.width),
295            None => self.layout.wrap_width,
296        };
297
298        paint_line(
299            origin,
300            &self.layout.unwrapped_layout,
301            line_height,
302            align,
303            align_width,
304            &self.decoration_runs,
305            &self.wrap_boundaries,
306            window,
307            cx,
308        )?;
309
310        Ok(())
311    }
312
313    /// Paint the background of line of text to the window.
314    pub fn paint_background(
315        &self,
316        origin: Point<Pixels>,
317        line_height: Pixels,
318        align: TextAlign,
319        bounds: Option<Bounds<Pixels>>,
320        window: &mut Window,
321        cx: &mut App,
322    ) -> Result<()> {
323        let align_width = match bounds {
324            Some(bounds) => Some(bounds.size.width),
325            None => self.layout.wrap_width,
326        };
327
328        paint_line_background(
329            origin,
330            &self.layout.unwrapped_layout,
331            line_height,
332            align,
333            align_width,
334            &self.decoration_runs,
335            &self.wrap_boundaries,
336            window,
337            cx,
338        )?;
339
340        Ok(())
341    }
342}
343
344fn paint_line(
345    origin: Point<Pixels>,
346    layout: &LineLayout,
347    line_height: Pixels,
348    align: TextAlign,
349    align_width: Option<Pixels>,
350    decoration_runs: &[DecorationRun],
351    wrap_boundaries: &[WrapBoundary],
352    window: &mut Window,
353    cx: &mut App,
354) -> Result<()> {
355    let line_bounds = Bounds::new(
356        origin,
357        size(
358            layout.width,
359            line_height * (wrap_boundaries.len() as f32 + 1.),
360        ),
361    );
362    window.paint_layer(line_bounds, |window| {
363        let padding_top = (line_height - layout.ascent - layout.descent) / 2.;
364        let baseline_offset = point(px(0.), padding_top + layout.ascent);
365        let mut decoration_runs = decoration_runs.iter();
366        let mut wraps = wrap_boundaries.iter().peekable();
367        let mut run_end = 0;
368        let mut color = black();
369        let mut current_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
370        let mut current_strikethrough: Option<(Point<Pixels>, StrikethroughStyle)> = None;
371        let text_system = cx.text_system().clone();
372        let mut glyph_origin = point(
373            aligned_origin_x(
374                origin,
375                align_width.unwrap_or(layout.width),
376                px(0.0),
377                &align,
378                layout,
379                wraps.peek(),
380            ),
381            origin.y,
382        );
383        let mut prev_glyph_position = Point::default();
384        let mut max_glyph_size = size(px(0.), px(0.));
385        let mut first_glyph_x = origin.x;
386        for (run_ix, run) in layout.runs.iter().enumerate() {
387            max_glyph_size = text_system.bounding_box(run.font_id, layout.font_size).size;
388
389            for (glyph_ix, glyph) in run.glyphs.iter().enumerate() {
390                glyph_origin.x += glyph.position.x - prev_glyph_position.x;
391                if glyph_ix == 0 && run_ix == 0 {
392                    first_glyph_x = glyph_origin.x;
393                }
394
395                if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) {
396                    wraps.next();
397                    if let Some((underline_origin, underline_style)) = current_underline.as_mut() {
398                        if glyph_origin.x == underline_origin.x {
399                            underline_origin.x -= max_glyph_size.width.half();
400                        };
401                        window.paint_underline(
402                            *underline_origin,
403                            glyph_origin.x - underline_origin.x,
404                            underline_style,
405                        );
406                        if glyph.index < run_end {
407                            underline_origin.x = origin.x;
408                            underline_origin.y += line_height;
409                        } else {
410                            current_underline = None;
411                        }
412                    }
413                    if let Some((strikethrough_origin, strikethrough_style)) =
414                        current_strikethrough.as_mut()
415                    {
416                        if glyph_origin.x == strikethrough_origin.x {
417                            strikethrough_origin.x -= max_glyph_size.width.half();
418                        };
419                        window.paint_strikethrough(
420                            *strikethrough_origin,
421                            glyph_origin.x - strikethrough_origin.x,
422                            strikethrough_style,
423                        );
424                        if glyph.index < run_end {
425                            strikethrough_origin.x = origin.x;
426                            strikethrough_origin.y += line_height;
427                        } else {
428                            current_strikethrough = None;
429                        }
430                    }
431
432                    glyph_origin.x = aligned_origin_x(
433                        origin,
434                        align_width.unwrap_or(layout.width),
435                        glyph.position.x,
436                        &align,
437                        layout,
438                        wraps.peek(),
439                    );
440                    glyph_origin.y += line_height;
441                }
442                prev_glyph_position = glyph.position;
443
444                let mut finished_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
445                let mut finished_strikethrough: Option<(Point<Pixels>, StrikethroughStyle)> = None;
446                if glyph.index >= run_end {
447                    let mut style_run = decoration_runs.next();
448
449                    // ignore style runs that apply to a partial glyph
450                    while let Some(run) = style_run {
451                        if glyph.index < run_end + (run.len as usize) {
452                            break;
453                        }
454                        run_end += run.len as usize;
455                        style_run = decoration_runs.next();
456                    }
457
458                    if let Some(style_run) = style_run {
459                        if let Some((_, underline_style)) = &mut current_underline
460                            && style_run.underline.as_ref() != Some(underline_style)
461                        {
462                            finished_underline = current_underline.take();
463                        }
464                        if let Some(run_underline) = style_run.underline.as_ref() {
465                            current_underline.get_or_insert((
466                                point(
467                                    glyph_origin.x,
468                                    glyph_origin.y + baseline_offset.y + (layout.descent * 0.618),
469                                ),
470                                UnderlineStyle {
471                                    color: Some(run_underline.color.unwrap_or(style_run.color)),
472                                    thickness: run_underline.thickness,
473                                    wavy: run_underline.wavy,
474                                },
475                            ));
476                        }
477                        if let Some((_, strikethrough_style)) = &mut current_strikethrough
478                            && style_run.strikethrough.as_ref() != Some(strikethrough_style)
479                        {
480                            finished_strikethrough = current_strikethrough.take();
481                        }
482                        if let Some(run_strikethrough) = style_run.strikethrough.as_ref() {
483                            current_strikethrough.get_or_insert((
484                                point(
485                                    glyph_origin.x,
486                                    glyph_origin.y
487                                        + (((layout.ascent * 0.5) + baseline_offset.y) * 0.5),
488                                ),
489                                StrikethroughStyle {
490                                    color: Some(run_strikethrough.color.unwrap_or(style_run.color)),
491                                    thickness: run_strikethrough.thickness,
492                                },
493                            ));
494                        }
495
496                        run_end += style_run.len as usize;
497                        color = style_run.color;
498                    } else {
499                        run_end = layout.len;
500                        finished_underline = current_underline.take();
501                        finished_strikethrough = current_strikethrough.take();
502                    }
503                }
504
505                if let Some((mut underline_origin, underline_style)) = finished_underline {
506                    if underline_origin.x == glyph_origin.x {
507                        underline_origin.x -= max_glyph_size.width.half();
508                    };
509                    window.paint_underline(
510                        underline_origin,
511                        glyph_origin.x - underline_origin.x,
512                        &underline_style,
513                    );
514                }
515
516                if let Some((mut strikethrough_origin, strikethrough_style)) =
517                    finished_strikethrough
518                {
519                    if strikethrough_origin.x == glyph_origin.x {
520                        strikethrough_origin.x -= max_glyph_size.width.half();
521                    };
522                    window.paint_strikethrough(
523                        strikethrough_origin,
524                        glyph_origin.x - strikethrough_origin.x,
525                        &strikethrough_style,
526                    );
527                }
528
529                let max_glyph_bounds = Bounds {
530                    origin: glyph_origin,
531                    size: max_glyph_size,
532                };
533
534                let content_mask = window.content_mask();
535                if max_glyph_bounds.intersects(&content_mask.bounds) {
536                    let vertical_offset = point(px(0.0), glyph.position.y);
537                    if glyph.is_emoji {
538                        window.paint_emoji(
539                            glyph_origin + baseline_offset + vertical_offset,
540                            run.font_id,
541                            glyph.id,
542                            layout.font_size,
543                        )?;
544                    } else {
545                        window.paint_glyph(
546                            glyph_origin + baseline_offset + vertical_offset,
547                            run.font_id,
548                            glyph.id,
549                            layout.font_size,
550                            color,
551                        )?;
552                    }
553                }
554            }
555        }
556
557        let mut last_line_end_x = first_glyph_x + layout.width;
558        if let Some(boundary) = wrap_boundaries.last() {
559            let run = &layout.runs[boundary.run_ix];
560            let glyph = &run.glyphs[boundary.glyph_ix];
561            last_line_end_x -= glyph.position.x;
562        }
563
564        if let Some((mut underline_start, underline_style)) = current_underline.take() {
565            if last_line_end_x == underline_start.x {
566                underline_start.x -= max_glyph_size.width.half()
567            };
568            window.paint_underline(
569                underline_start,
570                last_line_end_x - underline_start.x,
571                &underline_style,
572            );
573        }
574
575        if let Some((mut strikethrough_start, strikethrough_style)) = current_strikethrough.take() {
576            if last_line_end_x == strikethrough_start.x {
577                strikethrough_start.x -= max_glyph_size.width.half()
578            };
579            window.paint_strikethrough(
580                strikethrough_start,
581                last_line_end_x - strikethrough_start.x,
582                &strikethrough_style,
583            );
584        }
585
586        Ok(())
587    })
588}
589
590fn paint_line_background(
591    origin: Point<Pixels>,
592    layout: &LineLayout,
593    line_height: Pixels,
594    align: TextAlign,
595    align_width: Option<Pixels>,
596    decoration_runs: &[DecorationRun],
597    wrap_boundaries: &[WrapBoundary],
598    window: &mut Window,
599    cx: &mut App,
600) -> Result<()> {
601    let line_bounds = Bounds::new(
602        origin,
603        size(
604            layout.width,
605            line_height * (wrap_boundaries.len() as f32 + 1.),
606        ),
607    );
608    window.paint_layer(line_bounds, |window| {
609        let mut decoration_runs = decoration_runs.iter();
610        let mut wraps = wrap_boundaries.iter().peekable();
611        let mut run_end = 0;
612        let mut current_background: Option<(Point<Pixels>, Hsla)> = None;
613        let text_system = cx.text_system().clone();
614        let mut glyph_origin = point(
615            aligned_origin_x(
616                origin,
617                align_width.unwrap_or(layout.width),
618                px(0.0),
619                &align,
620                layout,
621                wraps.peek(),
622            ),
623            origin.y,
624        );
625        let mut prev_glyph_position = Point::default();
626        let mut max_glyph_size = size(px(0.), px(0.));
627        for (run_ix, run) in layout.runs.iter().enumerate() {
628            max_glyph_size = text_system.bounding_box(run.font_id, layout.font_size).size;
629
630            for (glyph_ix, glyph) in run.glyphs.iter().enumerate() {
631                glyph_origin.x += glyph.position.x - prev_glyph_position.x;
632
633                if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) {
634                    wraps.next();
635                    if let Some((background_origin, background_color)) = current_background.as_mut()
636                    {
637                        if glyph_origin.x == background_origin.x {
638                            background_origin.x -= max_glyph_size.width.half()
639                        }
640                        window.paint_quad(fill(
641                            Bounds {
642                                origin: *background_origin,
643                                size: size(glyph_origin.x - background_origin.x, line_height),
644                            },
645                            *background_color,
646                        ));
647                        if glyph.index < run_end {
648                            background_origin.x = origin.x;
649                            background_origin.y += line_height;
650                        } else {
651                            current_background = None;
652                        }
653                    }
654
655                    glyph_origin.x = aligned_origin_x(
656                        origin,
657                        align_width.unwrap_or(layout.width),
658                        glyph.position.x,
659                        &align,
660                        layout,
661                        wraps.peek(),
662                    );
663                    glyph_origin.y += line_height;
664                }
665                prev_glyph_position = glyph.position;
666
667                let mut finished_background: Option<(Point<Pixels>, Hsla)> = None;
668                if glyph.index >= run_end {
669                    let mut style_run = decoration_runs.next();
670
671                    // ignore style runs that apply to a partial glyph
672                    while let Some(run) = style_run {
673                        if glyph.index < run_end + (run.len as usize) {
674                            break;
675                        }
676                        run_end += run.len as usize;
677                        style_run = decoration_runs.next();
678                    }
679
680                    if let Some(style_run) = style_run {
681                        if let Some((_, background_color)) = &mut current_background
682                            && style_run.background_color.as_ref() != Some(background_color)
683                        {
684                            finished_background = current_background.take();
685                        }
686                        if let Some(run_background) = style_run.background_color {
687                            current_background.get_or_insert((
688                                point(glyph_origin.x, glyph_origin.y),
689                                run_background,
690                            ));
691                        }
692                        run_end += style_run.len as usize;
693                    } else {
694                        run_end = layout.len;
695                        finished_background = current_background.take();
696                    }
697                }
698
699                if let Some((mut background_origin, background_color)) = finished_background {
700                    let mut width = glyph_origin.x - background_origin.x;
701                    if background_origin.x == glyph_origin.x {
702                        background_origin.x -= max_glyph_size.width.half();
703                    };
704                    window.paint_quad(fill(
705                        Bounds {
706                            origin: background_origin,
707                            size: size(width, line_height),
708                        },
709                        background_color,
710                    ));
711                }
712            }
713        }
714
715        let mut last_line_end_x = origin.x + layout.width;
716        if let Some(boundary) = wrap_boundaries.last() {
717            let run = &layout.runs[boundary.run_ix];
718            let glyph = &run.glyphs[boundary.glyph_ix];
719            last_line_end_x -= glyph.position.x;
720        }
721
722        if let Some((mut background_origin, background_color)) = current_background.take() {
723            if last_line_end_x == background_origin.x {
724                background_origin.x -= max_glyph_size.width.half()
725            };
726            window.paint_quad(fill(
727                Bounds {
728                    origin: background_origin,
729                    size: size(last_line_end_x - background_origin.x, line_height),
730                },
731                background_color,
732            ));
733        }
734
735        Ok(())
736    })
737}
738
739fn aligned_origin_x(
740    origin: Point<Pixels>,
741    align_width: Pixels,
742    last_glyph_x: Pixels,
743    align: &TextAlign,
744    layout: &LineLayout,
745    wrap_boundary: Option<&&WrapBoundary>,
746) -> Pixels {
747    let end_of_line = if let Some(WrapBoundary { run_ix, glyph_ix }) = wrap_boundary {
748        layout.runs[*run_ix].glyphs[*glyph_ix].position.x
749    } else {
750        layout.width
751    };
752
753    let line_width = end_of_line - last_glyph_x;
754
755    match align {
756        TextAlign::Left => origin.x,
757        TextAlign::Center => (origin.x * 2.0 + align_width - line_width) / 2.0,
758        TextAlign::Right => origin.x + align_width - line_width,
759    }
760}
761
762#[cfg(test)]
763mod tests {
764    use super::*;
765    use crate::{FontId, GlyphId, ShapedGlyph, ShapedRun};
766
767    /// Helper: build a ShapedLine from glyph descriptors without the platform text system.
768    /// Each glyph is described as (byte_index, x_position).
769    fn make_shaped_line(
770        text: &str,
771        glyphs: &[(usize, f32)],
772        width: f32,
773        decorations: &[DecorationRun],
774    ) -> ShapedLine {
775        let shaped_glyphs: Vec<ShapedGlyph> = glyphs
776            .iter()
777            .map(|&(index, x)| ShapedGlyph {
778                id: GlyphId(0),
779                position: point(px(x), px(0.0)),
780                index,
781                is_emoji: false,
782            })
783            .collect();
784
785        ShapedLine {
786            layout: Arc::new(LineLayout {
787                font_size: px(16.0),
788                width: px(width),
789                ascent: px(12.0),
790                descent: px(4.0),
791                runs: vec![ShapedRun {
792                    font_id: FontId(0),
793                    glyphs: shaped_glyphs,
794                }],
795                len: text.len(),
796            }),
797            text: SharedString::new(text),
798            decoration_runs: SmallVec::from(decorations.to_vec()),
799        }
800    }
801
802    #[test]
803    fn test_split_at_invariants() {
804        // Split "abcdef" at every possible byte index and verify structural invariants.
805        let line = make_shaped_line(
806            "abcdef",
807            &[
808                (0, 0.0),
809                (1, 10.0),
810                (2, 20.0),
811                (3, 30.0),
812                (4, 40.0),
813                (5, 50.0),
814            ],
815            60.0,
816            &[],
817        );
818
819        for i in 0..=6 {
820            let (left, right) = line.split_at(i);
821
822            assert_eq!(
823                left.width() + right.width(),
824                line.width(),
825                "widths must sum at split={i}"
826            );
827            assert_eq!(
828                left.len() + right.len(),
829                line.len(),
830                "lengths must sum at split={i}"
831            );
832            assert_eq!(
833                format!("{}{}", left.text.as_ref(), right.text.as_ref()),
834                "abcdef",
835                "text must concatenate at split={i}"
836            );
837            assert_eq!(left.font_size, line.font_size, "font_size at split={i}");
838            assert_eq!(right.ascent, line.ascent, "ascent at split={i}");
839            assert_eq!(right.descent, line.descent, "descent at split={i}");
840        }
841
842        // Edge: split at 0 produces no left runs, full content on right
843        let (left, right) = line.split_at(0);
844        assert_eq!(left.runs.len(), 0);
845        assert_eq!(right.runs[0].glyphs.len(), 6);
846
847        // Edge: split at end produces full content on left, no right runs
848        let (left, right) = line.split_at(6);
849        assert_eq!(left.runs[0].glyphs.len(), 6);
850        assert_eq!(right.runs.len(), 0);
851    }
852
853    #[test]
854    fn test_split_at_glyph_rebasing() {
855        // Two font runs (simulating a font fallback boundary at byte 3):
856        //   run A (FontId 0): glyphs at bytes 0,1,2  positions 0,10,20
857        //   run B (FontId 1): glyphs at bytes 3,4,5  positions 30,40,50
858        // Successive splits simulate the incremental splitting done during wrap.
859        let line = ShapedLine {
860            layout: Arc::new(LineLayout {
861                font_size: px(16.0),
862                width: px(60.0),
863                ascent: px(12.0),
864                descent: px(4.0),
865                runs: vec![
866                    ShapedRun {
867                        font_id: FontId(0),
868                        glyphs: vec![
869                            ShapedGlyph {
870                                id: GlyphId(0),
871                                position: point(px(0.0), px(0.0)),
872                                index: 0,
873                                is_emoji: false,
874                            },
875                            ShapedGlyph {
876                                id: GlyphId(0),
877                                position: point(px(10.0), px(0.0)),
878                                index: 1,
879                                is_emoji: false,
880                            },
881                            ShapedGlyph {
882                                id: GlyphId(0),
883                                position: point(px(20.0), px(0.0)),
884                                index: 2,
885                                is_emoji: false,
886                            },
887                        ],
888                    },
889                    ShapedRun {
890                        font_id: FontId(1),
891                        glyphs: vec![
892                            ShapedGlyph {
893                                id: GlyphId(0),
894                                position: point(px(30.0), px(0.0)),
895                                index: 3,
896                                is_emoji: false,
897                            },
898                            ShapedGlyph {
899                                id: GlyphId(0),
900                                position: point(px(40.0), px(0.0)),
901                                index: 4,
902                                is_emoji: false,
903                            },
904                            ShapedGlyph {
905                                id: GlyphId(0),
906                                position: point(px(50.0), px(0.0)),
907                                index: 5,
908                                is_emoji: false,
909                            },
910                        ],
911                    },
912                ],
913                len: 6,
914            }),
915            text: "abcdef".into(),
916            decoration_runs: SmallVec::new(),
917        };
918
919        // First split at byte 2 — mid-run in run A
920        let (first, remainder) = line.split_at(2);
921        assert_eq!(first.text.as_ref(), "ab");
922        assert_eq!(first.runs.len(), 1);
923        assert_eq!(first.runs[0].font_id, FontId(0));
924
925        // Remainder "cdef" should have two runs: tail of A (1 glyph) + all of B (3 glyphs)
926        assert_eq!(remainder.text.as_ref(), "cdef");
927        assert_eq!(remainder.runs.len(), 2);
928        assert_eq!(remainder.runs[0].font_id, FontId(0));
929        assert_eq!(remainder.runs[0].glyphs.len(), 1);
930        assert_eq!(remainder.runs[0].glyphs[0].index, 0);
931        assert_eq!(remainder.runs[0].glyphs[0].position.x, px(0.0));
932        assert_eq!(remainder.runs[1].font_id, FontId(1));
933        assert_eq!(remainder.runs[1].glyphs[0].index, 1);
934        assert_eq!(remainder.runs[1].glyphs[0].position.x, px(10.0));
935
936        // Second split at byte 2 within remainder — crosses the run boundary
937        let (second, final_part) = remainder.split_at(2);
938        assert_eq!(second.text.as_ref(), "cd");
939        assert_eq!(final_part.text.as_ref(), "ef");
940        assert_eq!(final_part.runs[0].glyphs[0].index, 0);
941        assert_eq!(final_part.runs[0].glyphs[0].position.x, px(0.0));
942
943        // Widths must sum across all three pieces
944        assert_eq!(
945            first.width() + second.width() + final_part.width(),
946            line.width()
947        );
948    }
949
950    #[test]
951    fn test_split_at_decorations() {
952        // Three decoration runs: red [0..2), green [2..5), blue [5..6).
953        // Split at byte 3 — red goes entirely left, green straddles, blue goes entirely right.
954        let red = Hsla {
955            h: 0.0,
956            s: 1.0,
957            l: 0.5,
958            a: 1.0,
959        };
960        let green = Hsla {
961            h: 0.3,
962            s: 1.0,
963            l: 0.5,
964            a: 1.0,
965        };
966        let blue = Hsla {
967            h: 0.6,
968            s: 1.0,
969            l: 0.5,
970            a: 1.0,
971        };
972
973        let line = make_shaped_line(
974            "abcdef",
975            &[
976                (0, 0.0),
977                (1, 10.0),
978                (2, 20.0),
979                (3, 30.0),
980                (4, 40.0),
981                (5, 50.0),
982            ],
983            60.0,
984            &[
985                DecorationRun {
986                    len: 2,
987                    color: red,
988                    background_color: None,
989                    underline: None,
990                    strikethrough: None,
991                },
992                DecorationRun {
993                    len: 3,
994                    color: green,
995                    background_color: None,
996                    underline: None,
997                    strikethrough: None,
998                },
999                DecorationRun {
1000                    len: 1,
1001                    color: blue,
1002                    background_color: None,
1003                    underline: None,
1004                    strikethrough: None,
1005                },
1006            ],
1007        );
1008
1009        let (left, right) = line.split_at(3);
1010
1011        // Left: red(2) + green(1) — green straddled, left portion has len 1
1012        assert_eq!(left.decoration_runs.len(), 2);
1013        assert_eq!(left.decoration_runs[0].len, 2);
1014        assert_eq!(left.decoration_runs[0].color, red);
1015        assert_eq!(left.decoration_runs[1].len, 1);
1016        assert_eq!(left.decoration_runs[1].color, green);
1017
1018        // Right: green(2) + blue(1) — green straddled, right portion has len 2
1019        assert_eq!(right.decoration_runs.len(), 2);
1020        assert_eq!(right.decoration_runs[0].len, 2);
1021        assert_eq!(right.decoration_runs[0].color, green);
1022        assert_eq!(right.decoration_runs[1].len, 1);
1023        assert_eq!(right.decoration_runs[1].color, blue);
1024    }
1025}