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.to_string()),
 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}