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                        if glyph.index < run_end {
 389                            underline_origin.x = origin.x;
 390                            underline_origin.y += line_height;
 391                        } else {
 392                            current_underline = None;
 393                        }
 394                    }
 395                    if let Some((strikethrough_origin, strikethrough_style)) =
 396                        current_strikethrough.as_mut()
 397                    {
 398                        if glyph_origin.x == strikethrough_origin.x {
 399                            strikethrough_origin.x -= max_glyph_size.width.half();
 400                        };
 401                        window.paint_strikethrough(
 402                            *strikethrough_origin,
 403                            glyph_origin.x - strikethrough_origin.x,
 404                            strikethrough_style,
 405                        );
 406                        if glyph.index < run_end {
 407                            strikethrough_origin.x = origin.x;
 408                            strikethrough_origin.y += line_height;
 409                        } else {
 410                            current_strikethrough = None;
 411                        }
 412                    }
 413
 414                    glyph_origin.x = aligned_origin_x(
 415                        origin,
 416                        align_width.unwrap_or(layout.width),
 417                        glyph.position.x,
 418                        &align,
 419                        layout,
 420                        wraps.peek(),
 421                    );
 422                    glyph_origin.y += line_height;
 423                }
 424                prev_glyph_position = glyph.position;
 425
 426                let mut finished_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
 427                let mut finished_strikethrough: Option<(Point<Pixels>, StrikethroughStyle)> = None;
 428                if glyph.index >= run_end {
 429                    let mut style_run = decoration_runs.next();
 430
 431                    // ignore style runs that apply to a partial glyph
 432                    while let Some(run) = style_run {
 433                        if glyph.index < run_end + (run.len as usize) {
 434                            break;
 435                        }
 436                        run_end += run.len as usize;
 437                        style_run = decoration_runs.next();
 438                    }
 439
 440                    if let Some(style_run) = style_run {
 441                        if let Some((_, underline_style)) = &mut current_underline
 442                            && style_run.underline.as_ref() != Some(underline_style)
 443                        {
 444                            finished_underline = current_underline.take();
 445                        }
 446                        if let Some(run_underline) = style_run.underline.as_ref() {
 447                            current_underline.get_or_insert((
 448                                point(
 449                                    glyph_origin.x,
 450                                    glyph_origin.y + baseline_offset.y + (layout.descent * 0.618),
 451                                ),
 452                                UnderlineStyle {
 453                                    color: Some(run_underline.color.unwrap_or(style_run.color)),
 454                                    thickness: run_underline.thickness,
 455                                    wavy: run_underline.wavy,
 456                                },
 457                            ));
 458                        }
 459                        if let Some((_, strikethrough_style)) = &mut current_strikethrough
 460                            && style_run.strikethrough.as_ref() != Some(strikethrough_style)
 461                        {
 462                            finished_strikethrough = current_strikethrough.take();
 463                        }
 464                        if let Some(run_strikethrough) = style_run.strikethrough.as_ref() {
 465                            current_strikethrough.get_or_insert((
 466                                point(
 467                                    glyph_origin.x,
 468                                    glyph_origin.y
 469                                        + (((layout.ascent * 0.5) + baseline_offset.y) * 0.5),
 470                                ),
 471                                StrikethroughStyle {
 472                                    color: Some(run_strikethrough.color.unwrap_or(style_run.color)),
 473                                    thickness: run_strikethrough.thickness,
 474                                },
 475                            ));
 476                        }
 477
 478                        run_end += style_run.len as usize;
 479                        color = style_run.color;
 480                    } else {
 481                        run_end = layout.len;
 482                        finished_underline = current_underline.take();
 483                        finished_strikethrough = current_strikethrough.take();
 484                    }
 485                }
 486
 487                if let Some((mut underline_origin, underline_style)) = finished_underline {
 488                    if underline_origin.x == glyph_origin.x {
 489                        underline_origin.x -= max_glyph_size.width.half();
 490                    };
 491                    window.paint_underline(
 492                        underline_origin,
 493                        glyph_origin.x - underline_origin.x,
 494                        &underline_style,
 495                    );
 496                }
 497
 498                if let Some((mut strikethrough_origin, strikethrough_style)) =
 499                    finished_strikethrough
 500                {
 501                    if strikethrough_origin.x == glyph_origin.x {
 502                        strikethrough_origin.x -= max_glyph_size.width.half();
 503                    };
 504                    window.paint_strikethrough(
 505                        strikethrough_origin,
 506                        glyph_origin.x - strikethrough_origin.x,
 507                        &strikethrough_style,
 508                    );
 509                }
 510
 511                let max_glyph_bounds = Bounds {
 512                    origin: glyph_origin,
 513                    size: max_glyph_size,
 514                };
 515
 516                let content_mask = window.content_mask();
 517                if max_glyph_bounds.intersects(&content_mask.bounds) {
 518                    let vertical_offset = point(px(0.0), glyph.position.y);
 519                    if glyph.is_emoji {
 520                        window.paint_emoji(
 521                            glyph_origin + baseline_offset + vertical_offset,
 522                            run.font_id,
 523                            glyph.id,
 524                            layout.font_size,
 525                        )?;
 526                    } else {
 527                        window.paint_glyph(
 528                            glyph_origin + baseline_offset + vertical_offset,
 529                            run.font_id,
 530                            glyph.id,
 531                            layout.font_size,
 532                            color,
 533                        )?;
 534                    }
 535                }
 536            }
 537        }
 538
 539        let mut last_line_end_x = first_glyph_x + layout.width;
 540        if let Some(boundary) = wrap_boundaries.last() {
 541            let run = &layout.runs[boundary.run_ix];
 542            let glyph = &run.glyphs[boundary.glyph_ix];
 543            last_line_end_x -= glyph.position.x;
 544        }
 545
 546        if let Some((mut underline_start, underline_style)) = current_underline.take() {
 547            if last_line_end_x == underline_start.x {
 548                underline_start.x -= max_glyph_size.width.half()
 549            };
 550            window.paint_underline(
 551                underline_start,
 552                last_line_end_x - underline_start.x,
 553                &underline_style,
 554            );
 555        }
 556
 557        if let Some((mut strikethrough_start, strikethrough_style)) = current_strikethrough.take() {
 558            if last_line_end_x == strikethrough_start.x {
 559                strikethrough_start.x -= max_glyph_size.width.half()
 560            };
 561            window.paint_strikethrough(
 562                strikethrough_start,
 563                last_line_end_x - strikethrough_start.x,
 564                &strikethrough_style,
 565            );
 566        }
 567
 568        Ok(())
 569    })
 570}
 571
 572fn paint_line_background(
 573    origin: Point<Pixels>,
 574    layout: &LineLayout,
 575    line_height: Pixels,
 576    align: TextAlign,
 577    align_width: Option<Pixels>,
 578    decoration_runs: &[DecorationRun],
 579    wrap_boundaries: &[WrapBoundary],
 580    window: &mut Window,
 581    cx: &mut App,
 582) -> Result<()> {
 583    let line_bounds = Bounds::new(
 584        origin,
 585        size(
 586            layout.width,
 587            line_height * (wrap_boundaries.len() as f32 + 1.),
 588        ),
 589    );
 590    window.paint_layer(line_bounds, |window| {
 591        let mut decoration_runs = decoration_runs.iter();
 592        let mut wraps = wrap_boundaries.iter().peekable();
 593        let mut run_end = 0;
 594        let mut current_background: Option<(Point<Pixels>, Hsla)> = None;
 595        let text_system = cx.text_system().clone();
 596        let mut glyph_origin = point(
 597            aligned_origin_x(
 598                origin,
 599                align_width.unwrap_or(layout.width),
 600                px(0.0),
 601                &align,
 602                layout,
 603                wraps.peek(),
 604            ),
 605            origin.y,
 606        );
 607        let mut prev_glyph_position = Point::default();
 608        let mut max_glyph_size = size(px(0.), px(0.));
 609        for (run_ix, run) in layout.runs.iter().enumerate() {
 610            max_glyph_size = text_system.bounding_box(run.font_id, layout.font_size).size;
 611
 612            for (glyph_ix, glyph) in run.glyphs.iter().enumerate() {
 613                glyph_origin.x += glyph.position.x - prev_glyph_position.x;
 614
 615                if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) {
 616                    wraps.next();
 617                    if let Some((background_origin, background_color)) = current_background.as_mut()
 618                    {
 619                        if glyph_origin.x == background_origin.x {
 620                            background_origin.x -= max_glyph_size.width.half()
 621                        }
 622                        window.paint_quad(fill(
 623                            Bounds {
 624                                origin: *background_origin,
 625                                size: size(glyph_origin.x - background_origin.x, line_height),
 626                            },
 627                            *background_color,
 628                        ));
 629                        if glyph.index < run_end {
 630                            background_origin.x = origin.x;
 631                            background_origin.y += line_height;
 632                        } else {
 633                            current_background = None;
 634                        }
 635                    }
 636
 637                    glyph_origin.x = aligned_origin_x(
 638                        origin,
 639                        align_width.unwrap_or(layout.width),
 640                        glyph.position.x,
 641                        &align,
 642                        layout,
 643                        wraps.peek(),
 644                    );
 645                    glyph_origin.y += line_height;
 646                }
 647                prev_glyph_position = glyph.position;
 648
 649                let mut finished_background: Option<(Point<Pixels>, Hsla)> = None;
 650                if glyph.index >= run_end {
 651                    let mut style_run = decoration_runs.next();
 652
 653                    // ignore style runs that apply to a partial glyph
 654                    while let Some(run) = style_run {
 655                        if glyph.index < run_end + (run.len as usize) {
 656                            break;
 657                        }
 658                        run_end += run.len as usize;
 659                        style_run = decoration_runs.next();
 660                    }
 661
 662                    if let Some(style_run) = style_run {
 663                        if let Some((_, background_color)) = &mut current_background
 664                            && style_run.background_color.as_ref() != Some(background_color)
 665                        {
 666                            finished_background = current_background.take();
 667                        }
 668                        if let Some(run_background) = style_run.background_color {
 669                            current_background.get_or_insert((
 670                                point(glyph_origin.x, glyph_origin.y),
 671                                run_background,
 672                            ));
 673                        }
 674                        run_end += style_run.len as usize;
 675                    } else {
 676                        run_end = layout.len;
 677                        finished_background = current_background.take();
 678                    }
 679                }
 680
 681                if let Some((mut background_origin, background_color)) = finished_background {
 682                    let mut width = glyph_origin.x - background_origin.x;
 683                    if background_origin.x == glyph_origin.x {
 684                        background_origin.x -= max_glyph_size.width.half();
 685                    };
 686                    window.paint_quad(fill(
 687                        Bounds {
 688                            origin: background_origin,
 689                            size: size(width, line_height),
 690                        },
 691                        background_color,
 692                    ));
 693                }
 694            }
 695        }
 696
 697        let mut last_line_end_x = origin.x + layout.width;
 698        if let Some(boundary) = wrap_boundaries.last() {
 699            let run = &layout.runs[boundary.run_ix];
 700            let glyph = &run.glyphs[boundary.glyph_ix];
 701            last_line_end_x -= glyph.position.x;
 702        }
 703
 704        if let Some((mut background_origin, background_color)) = current_background.take() {
 705            if last_line_end_x == background_origin.x {
 706                background_origin.x -= max_glyph_size.width.half()
 707            };
 708            window.paint_quad(fill(
 709                Bounds {
 710                    origin: background_origin,
 711                    size: size(last_line_end_x - background_origin.x, line_height),
 712                },
 713                background_color,
 714            ));
 715        }
 716
 717        Ok(())
 718    })
 719}
 720
 721fn aligned_origin_x(
 722    origin: Point<Pixels>,
 723    align_width: Pixels,
 724    last_glyph_x: Pixels,
 725    align: &TextAlign,
 726    layout: &LineLayout,
 727    wrap_boundary: Option<&&WrapBoundary>,
 728) -> Pixels {
 729    let end_of_line = if let Some(WrapBoundary { run_ix, glyph_ix }) = wrap_boundary {
 730        layout.runs[*run_ix].glyphs[*glyph_ix].position.x
 731    } else {
 732        layout.width
 733    };
 734
 735    let line_width = end_of_line - last_glyph_x;
 736
 737    match align {
 738        TextAlign::Left => origin.x,
 739        TextAlign::Center => (origin.x * 2.0 + align_width - line_width) / 2.0,
 740        TextAlign::Right => origin.x + align_width - line_width,
 741    }
 742}
 743
 744#[cfg(test)]
 745mod tests {
 746    use super::*;
 747    use crate::{FontId, GlyphId};
 748
 749    /// Helper: build a ShapedLine from glyph descriptors without the platform text system.
 750    /// Each glyph is described as (byte_index, x_position).
 751    fn make_shaped_line(
 752        text: &str,
 753        glyphs: &[(usize, f32)],
 754        width: f32,
 755        decorations: &[DecorationRun],
 756    ) -> ShapedLine {
 757        let shaped_glyphs: Vec<ShapedGlyph> = glyphs
 758            .iter()
 759            .map(|&(index, x)| ShapedGlyph {
 760                id: GlyphId(0),
 761                position: point(px(x), px(0.0)),
 762                index,
 763                is_emoji: false,
 764            })
 765            .collect();
 766
 767        ShapedLine {
 768            layout: Arc::new(LineLayout {
 769                font_size: px(16.0),
 770                width: px(width),
 771                ascent: px(12.0),
 772                descent: px(4.0),
 773                runs: vec![ShapedRun {
 774                    font_id: FontId(0),
 775                    glyphs: shaped_glyphs,
 776                }],
 777                len: text.len(),
 778            }),
 779            text: SharedString::new(text.to_string()),
 780            decoration_runs: SmallVec::from(decorations.to_vec()),
 781        }
 782    }
 783
 784    #[test]
 785    fn test_split_at_invariants() {
 786        // Split "abcdef" at every possible byte index and verify structural invariants.
 787        let line = make_shaped_line(
 788            "abcdef",
 789            &[
 790                (0, 0.0),
 791                (1, 10.0),
 792                (2, 20.0),
 793                (3, 30.0),
 794                (4, 40.0),
 795                (5, 50.0),
 796            ],
 797            60.0,
 798            &[],
 799        );
 800
 801        for i in 0..=6 {
 802            let (left, right) = line.split_at(i);
 803
 804            assert_eq!(
 805                left.width() + right.width(),
 806                line.width(),
 807                "widths must sum at split={i}"
 808            );
 809            assert_eq!(
 810                left.len() + right.len(),
 811                line.len(),
 812                "lengths must sum at split={i}"
 813            );
 814            assert_eq!(
 815                format!("{}{}", left.text.as_ref(), right.text.as_ref()),
 816                "abcdef",
 817                "text must concatenate at split={i}"
 818            );
 819            assert_eq!(left.font_size, line.font_size, "font_size at split={i}");
 820            assert_eq!(right.ascent, line.ascent, "ascent at split={i}");
 821            assert_eq!(right.descent, line.descent, "descent at split={i}");
 822        }
 823
 824        // Edge: split at 0 produces no left runs, full content on right
 825        let (left, right) = line.split_at(0);
 826        assert_eq!(left.runs.len(), 0);
 827        assert_eq!(right.runs[0].glyphs.len(), 6);
 828
 829        // Edge: split at end produces full content on left, no right runs
 830        let (left, right) = line.split_at(6);
 831        assert_eq!(left.runs[0].glyphs.len(), 6);
 832        assert_eq!(right.runs.len(), 0);
 833    }
 834
 835    #[test]
 836    fn test_split_at_glyph_rebasing() {
 837        // Two font runs (simulating a font fallback boundary at byte 3):
 838        //   run A (FontId 0): glyphs at bytes 0,1,2  positions 0,10,20
 839        //   run B (FontId 1): glyphs at bytes 3,4,5  positions 30,40,50
 840        // Successive splits simulate the incremental splitting done during wrap.
 841        let line = ShapedLine {
 842            layout: Arc::new(LineLayout {
 843                font_size: px(16.0),
 844                width: px(60.0),
 845                ascent: px(12.0),
 846                descent: px(4.0),
 847                runs: vec![
 848                    ShapedRun {
 849                        font_id: FontId(0),
 850                        glyphs: vec![
 851                            ShapedGlyph {
 852                                id: GlyphId(0),
 853                                position: point(px(0.0), px(0.0)),
 854                                index: 0,
 855                                is_emoji: false,
 856                            },
 857                            ShapedGlyph {
 858                                id: GlyphId(0),
 859                                position: point(px(10.0), px(0.0)),
 860                                index: 1,
 861                                is_emoji: false,
 862                            },
 863                            ShapedGlyph {
 864                                id: GlyphId(0),
 865                                position: point(px(20.0), px(0.0)),
 866                                index: 2,
 867                                is_emoji: false,
 868                            },
 869                        ],
 870                    },
 871                    ShapedRun {
 872                        font_id: FontId(1),
 873                        glyphs: vec![
 874                            ShapedGlyph {
 875                                id: GlyphId(0),
 876                                position: point(px(30.0), px(0.0)),
 877                                index: 3,
 878                                is_emoji: false,
 879                            },
 880                            ShapedGlyph {
 881                                id: GlyphId(0),
 882                                position: point(px(40.0), px(0.0)),
 883                                index: 4,
 884                                is_emoji: false,
 885                            },
 886                            ShapedGlyph {
 887                                id: GlyphId(0),
 888                                position: point(px(50.0), px(0.0)),
 889                                index: 5,
 890                                is_emoji: false,
 891                            },
 892                        ],
 893                    },
 894                ],
 895                len: 6,
 896            }),
 897            text: "abcdef".into(),
 898            decoration_runs: SmallVec::new(),
 899        };
 900
 901        // First split at byte 2 — mid-run in run A
 902        let (first, remainder) = line.split_at(2);
 903        assert_eq!(first.text.as_ref(), "ab");
 904        assert_eq!(first.runs.len(), 1);
 905        assert_eq!(first.runs[0].font_id, FontId(0));
 906
 907        // Remainder "cdef" should have two runs: tail of A (1 glyph) + all of B (3 glyphs)
 908        assert_eq!(remainder.text.as_ref(), "cdef");
 909        assert_eq!(remainder.runs.len(), 2);
 910        assert_eq!(remainder.runs[0].font_id, FontId(0));
 911        assert_eq!(remainder.runs[0].glyphs.len(), 1);
 912        assert_eq!(remainder.runs[0].glyphs[0].index, 0);
 913        assert_eq!(remainder.runs[0].glyphs[0].position.x, px(0.0));
 914        assert_eq!(remainder.runs[1].font_id, FontId(1));
 915        assert_eq!(remainder.runs[1].glyphs[0].index, 1);
 916        assert_eq!(remainder.runs[1].glyphs[0].position.x, px(10.0));
 917
 918        // Second split at byte 2 within remainder — crosses the run boundary
 919        let (second, final_part) = remainder.split_at(2);
 920        assert_eq!(second.text.as_ref(), "cd");
 921        assert_eq!(final_part.text.as_ref(), "ef");
 922        assert_eq!(final_part.runs[0].glyphs[0].index, 0);
 923        assert_eq!(final_part.runs[0].glyphs[0].position.x, px(0.0));
 924
 925        // Widths must sum across all three pieces
 926        assert_eq!(
 927            first.width() + second.width() + final_part.width(),
 928            line.width()
 929        );
 930    }
 931
 932    #[test]
 933    fn test_split_at_decorations() {
 934        // Three decoration runs: red [0..2), green [2..5), blue [5..6).
 935        // Split at byte 3 — red goes entirely left, green straddles, blue goes entirely right.
 936        let red = Hsla {
 937            h: 0.0,
 938            s: 1.0,
 939            l: 0.5,
 940            a: 1.0,
 941        };
 942        let green = Hsla {
 943            h: 0.3,
 944            s: 1.0,
 945            l: 0.5,
 946            a: 1.0,
 947        };
 948        let blue = Hsla {
 949            h: 0.6,
 950            s: 1.0,
 951            l: 0.5,
 952            a: 1.0,
 953        };
 954
 955        let line = make_shaped_line(
 956            "abcdef",
 957            &[
 958                (0, 0.0),
 959                (1, 10.0),
 960                (2, 20.0),
 961                (3, 30.0),
 962                (4, 40.0),
 963                (5, 50.0),
 964            ],
 965            60.0,
 966            &[
 967                DecorationRun {
 968                    len: 2,
 969                    color: red,
 970                    background_color: None,
 971                    underline: None,
 972                    strikethrough: None,
 973                },
 974                DecorationRun {
 975                    len: 3,
 976                    color: green,
 977                    background_color: None,
 978                    underline: None,
 979                    strikethrough: None,
 980                },
 981                DecorationRun {
 982                    len: 1,
 983                    color: blue,
 984                    background_color: None,
 985                    underline: None,
 986                    strikethrough: None,
 987                },
 988            ],
 989        );
 990
 991        let (left, right) = line.split_at(3);
 992
 993        // Left: red(2) + green(1) — green straddled, left portion has len 1
 994        assert_eq!(left.decoration_runs.len(), 2);
 995        assert_eq!(left.decoration_runs[0].len, 2);
 996        assert_eq!(left.decoration_runs[0].color, red);
 997        assert_eq!(left.decoration_runs[1].len, 1);
 998        assert_eq!(left.decoration_runs[1].color, green);
 999
1000        // Right: green(2) + blue(1) — green straddled, right portion has len 2
1001        assert_eq!(right.decoration_runs.len(), 2);
1002        assert_eq!(right.decoration_runs[0].len, 2);
1003        assert_eq!(right.decoration_runs[0].color, green);
1004        assert_eq!(right.decoration_runs[1].len, 1);
1005        assert_eq!(right.decoration_runs[1].color, blue);
1006    }
1007}