Skip to main content

gpui/text_system/
line.rs

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