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