Skip to main content

gpui/text_system/
line.rs

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