gpui: Add split and paint APIs on LineLayout (#60831)

`ShapedLine` is nearly 3KB by value because it inlines a
`SmallVec<[DecorationRun; 32]>`. A consumer that builds wrapped lines
out of many small uniformly styled fragments only needs the shaped
glyphs plus a single decoration run it can track itself, but it ends up
moving and reallocating that mostly dead capacity constantly because
splitting and painting are only reachable through `ShapedLine`.

This moves the glyph-partitioning half of `ShapedLine::split_at` down to
a new `LineLayout::split_at`, and adds
`LineLayout::paint`/`paint_background` methods that take explicit
decoration runs. Together with the already-public `layout_line`, callers
can now hold a bare `Arc<LineLayout>` and their own decorations.
`ShapedLine::split_at` delegates to the new method and behaves the same.

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [ ] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

---

Release Notes:

- N/A
This commit is contained in:
Agus Zubiaga authored and GitHub committed 2026-08-19 17:40:37 +00:00
1 parent aa14836bb4
commit 7c46498fe8
2 files changed
+126 -54

No files matched your search

+64 -54
View File
@@ -1,7 +1,7 @@
use crate::{ use crate::{
App, Bounds, DevicePixels, Half, Hsla, LineLayout, Pixels, Point, RenderGlyphParams, Result, App, Bounds, DevicePixels, Half, Hsla, LineLayout, Pixels, Point, RenderGlyphParams, Result,
ShapedGlyph, ShapedRun, SharedString, StrikethroughStyle, TextAlign, UnderlineStyle, Window, SharedString, StrikethroughStyle, TextAlign, UnderlineStyle, Window, WrapBoundary,
WrapBoundary, WrappedLineLayout, black, fill, point, px, size, WrappedLineLayout, black, fill, point, px, size,
}; };
use derive_more::{Deref, DerefMut}; use derive_more::{Deref, DerefMut};
use smallvec::SmallVec; use smallvec::SmallVec;
@@ -139,38 +139,7 @@ impl ShapedLine {
/// split into two with adjusted lengths. /// split into two with adjusted lengths.
/// - `font_size`, `ascent`, and `descent` are copied to both halves. /// - `font_size`, `ascent`, and `descent` are copied to both halves.
pub fn split_at(&self, byte_index: usize) -> (ShapedLine, ShapedLine) { pub fn split_at(&self, byte_index: usize) -> (ShapedLine, ShapedLine) {
let x_offset = self.layout.x_for_index(byte_index); let (left_layout, right_layout) = self.layout.split_at(byte_index);
// Partition glyph runs. A single run may contribute glyphs to both halves.
let mut left_runs = Vec::new();
let mut right_runs = Vec::new();
for run in &self.layout.runs {
let split_pos = run.glyphs.partition_point(|g| g.index < byte_index);
if split_pos > 0 {
left_runs.push(ShapedRun {
font_id: run.font_id,
glyphs: run.glyphs[..split_pos].to_vec(),
});
}
if split_pos < run.glyphs.len() {
let right_glyphs = run.glyphs[split_pos..]
.iter()
.map(|g| ShapedGlyph {
id: g.id,
position: point(g.position.x - x_offset, g.position.y),
index: g.index - byte_index,
is_emoji: g.is_emoji,
})
.collect();
right_runs.push(ShapedRun {
font_id: run.font_id,
glyphs: right_glyphs,
});
}
}
// Partition decoration runs. A run straddling the boundary is split into two. // Partition decoration runs. A run straddling the boundary is split into two.
let mut left_decorations = SmallVec::new(); let mut left_decorations = SmallVec::new();
@@ -219,31 +188,14 @@ impl ShapedLine {
SharedString::new(&self.text[byte_index..]) SharedString::new(&self.text[byte_index..])
}; };
let left_width = x_offset;
let right_width = self.layout.width - left_width;
let left = ShapedLine { let left = ShapedLine {
layout: Arc::new(LineLayout { layout: Arc::new(left_layout),
font_size: self.layout.font_size,
width: left_width,
ascent: self.layout.ascent,
descent: self.layout.descent,
runs: left_runs,
len: byte_index,
}),
text: left_text, text: left_text,
decoration_runs: left_decorations, decoration_runs: left_decorations,
}; };
let right = ShapedLine { let right = ShapedLine {
layout: Arc::new(LineLayout { layout: Arc::new(right_layout),
font_size: self.layout.font_size,
width: right_width,
ascent: self.layout.ascent,
descent: self.layout.descent,
runs: right_runs,
len: self.layout.len - byte_index,
}),
text: right_text, text: right_text,
decoration_runs: right_decorations, decoration_runs: right_decorations,
}; };
@@ -252,6 +204,64 @@ impl ShapedLine {
} }
} }
impl LineLayout {
/// Paint this layout to the window, using the given decoration runs to color
/// glyphs and draw underlines and strikethroughs.
///
/// This is a lower-level alternative to [`ShapedLine::paint`] for callers that
/// hold a bare layout and track decorations themselves.
pub fn paint(
&self,
origin: Point<Pixels>,
line_height: Pixels,
align: TextAlign,
align_width: Option<Pixels>,
decoration_runs: &[DecorationRun],
window: &mut Window,
cx: &mut App,
) -> Result<()> {
paint_line(
origin,
self,
line_height,
align,
align_width,
decoration_runs,
&[],
window,
cx,
)
}
/// Paint the background of this layout to the window, using the given
/// decoration runs to determine background colors.
///
/// This is a lower-level alternative to [`ShapedLine::paint_background`] for
/// callers that hold a bare layout and track decorations themselves.
pub fn paint_background(
&self,
origin: Point<Pixels>,
line_height: Pixels,
align: TextAlign,
align_width: Option<Pixels>,
decoration_runs: &[DecorationRun],
window: &mut Window,
cx: &mut App,
) -> Result<()> {
paint_line_background(
origin,
self,
line_height,
align,
align_width,
decoration_runs,
&[],
window,
cx,
)
}
}
/// A line of text that has been shaped, decorated, and wrapped by the text layout system. /// A line of text that has been shaped, decorated, and wrapped by the text layout system.
#[derive(Default, Debug, Deref, DerefMut)] #[derive(Default, Debug, Deref, DerefMut)]
pub struct WrappedLine { pub struct WrappedLine {
@@ -752,7 +762,7 @@ fn aligned_origin_x(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::{FontId, GlyphId}; use crate::{FontId, GlyphId, ShapedGlyph, ShapedRun};
/// Helper: build a ShapedLine from glyph descriptors without the platform text system. /// Helper: build a ShapedLine from glyph descriptors without the platform text system.
/// Each glyph is described as (byte_index, x_position). /// Each glyph is described as (byte_index, x_position).
@@ -125,6 +125,68 @@ impl LineLayout {
None None
} }
/// Split this layout at a byte index, returning `(prefix, suffix)`.
///
/// - `prefix` contains glyphs for bytes `[0, byte_index)` with original positions.
/// Its width equals the x-advance up to the split point.
/// - `suffix` contains glyphs for bytes `[byte_index, len)` with positions
/// shifted left so the first glyph starts at x=0, and byte indices rebased to 0.
/// - `font_size`, `ascent`, and `descent` are copied to both halves.
pub fn split_at(&self, byte_index: usize) -> (LineLayout, LineLayout) {
let x_offset = self.x_for_index(byte_index);
// Partition glyph runs. A single run may contribute glyphs to both halves.
let mut left_runs = Vec::new();
let mut right_runs = Vec::new();
for run in &self.runs {
let split_pos = run.glyphs.partition_point(|g| g.index < byte_index);
if split_pos > 0 {
left_runs.push(ShapedRun {
font_id: run.font_id,
glyphs: run.glyphs[..split_pos].to_vec(),
});
}
if split_pos < run.glyphs.len() {
let right_glyphs = run.glyphs[split_pos..]
.iter()
.map(|g| ShapedGlyph {
id: g.id,
position: point(g.position.x - x_offset, g.position.y),
index: g.index - byte_index,
is_emoji: g.is_emoji,
})
.collect();
right_runs.push(ShapedRun {
font_id: run.font_id,
glyphs: right_glyphs,
});
}
}
let left = LineLayout {
font_size: self.font_size,
width: x_offset,
ascent: self.ascent,
descent: self.descent,
runs: left_runs,
len: byte_index,
};
let right = LineLayout {
font_size: self.font_size,
width: self.width - x_offset,
ascent: self.ascent,
descent: self.descent,
runs: right_runs,
len: self.len - byte_index,
};
(left, right)
}
fn compute_wrap_boundaries( fn compute_wrap_boundaries(
&self, &self,
text: &str, text: &str,