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:
1 parent
aa14836bb4
commit
7c46498fe8
2 files changed
+126
-54
No files matched your search
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
App, Bounds, DevicePixels, Half, Hsla, LineLayout, Pixels, Point, RenderGlyphParams, Result,
|
||||
ShapedGlyph, ShapedRun, SharedString, StrikethroughStyle, TextAlign, UnderlineStyle, Window,
|
||||
WrapBoundary, WrappedLineLayout, black, fill, point, px, size,
|
||||
SharedString, StrikethroughStyle, TextAlign, UnderlineStyle, Window, WrapBoundary,
|
||||
WrappedLineLayout, black, fill, point, px, size,
|
||||
};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use smallvec::SmallVec;
|
||||
@@ -139,38 +139,7 @@ impl ShapedLine {
|
||||
/// split into two with adjusted lengths.
|
||||
/// - `font_size`, `ascent`, and `descent` are copied to both halves.
|
||||
pub fn split_at(&self, byte_index: usize) -> (ShapedLine, ShapedLine) {
|
||||
let x_offset = self.layout.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.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,
|
||||
});
|
||||
}
|
||||
}
|
||||
let (left_layout, right_layout) = self.layout.split_at(byte_index);
|
||||
|
||||
// Partition decoration runs. A run straddling the boundary is split into two.
|
||||
let mut left_decorations = SmallVec::new();
|
||||
@@ -219,31 +188,14 @@ impl ShapedLine {
|
||||
SharedString::new(&self.text[byte_index..])
|
||||
};
|
||||
|
||||
let left_width = x_offset;
|
||||
let right_width = self.layout.width - left_width;
|
||||
|
||||
let left = ShapedLine {
|
||||
layout: Arc::new(LineLayout {
|
||||
font_size: self.layout.font_size,
|
||||
width: left_width,
|
||||
ascent: self.layout.ascent,
|
||||
descent: self.layout.descent,
|
||||
runs: left_runs,
|
||||
len: byte_index,
|
||||
}),
|
||||
layout: Arc::new(left_layout),
|
||||
text: left_text,
|
||||
decoration_runs: left_decorations,
|
||||
};
|
||||
|
||||
let right = ShapedLine {
|
||||
layout: Arc::new(LineLayout {
|
||||
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,
|
||||
}),
|
||||
layout: Arc::new(right_layout),
|
||||
text: right_text,
|
||||
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.
|
||||
#[derive(Default, Debug, Deref, DerefMut)]
|
||||
pub struct WrappedLine {
|
||||
@@ -752,7 +762,7 @@ fn aligned_origin_x(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{FontId, GlyphId};
|
||||
use crate::{FontId, GlyphId, ShapedGlyph, ShapedRun};
|
||||
|
||||
/// Helper: build a ShapedLine from glyph descriptors without the platform text system.
|
||||
/// Each glyph is described as (byte_index, x_position).
|
||||
|
||||
@@ -125,6 +125,68 @@ impl LineLayout {
|
||||
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(
|
||||
&self,
|
||||
text: &str,
|
||||
|
||||
Reference in new issue
Block a user