separate update_line_layouts from other mutations in prepaint

This commit is contained in:
temportalflux
2026-07-11 09:31:07 -04:00
parent 4037fc5a92
commit 1675c73726
3 changed files with 48 additions and 54 deletions
+12 -10
View File
@@ -1,4 +1,4 @@
use crate::input::{Input, InputLineLayout, InputState, PaintColors};
use crate::input::{Input, InputLogicalLine, InputState, PaintColors};
use gpui::{
Along, App, Bounds, ContentMask, CursorStyle, DispatchPhase, Element, ElementId,
ElementInputHandler, Entity, Focusable, GlobalElementId, Hitbox, HitboxBehavior, Hsla,
@@ -86,14 +86,16 @@ impl Element for Input {
.line_height_in_pixels(window.rem_size());
let wrap_width = match self.input.read(cx).get_layout() {
super::InputLayout::SingleLine => px(100000.),
super::InputLayout::MultiLine => bounds.size.width,
super::InputLayout::SingleLine => None,
super::InputLayout::MultiLine => Some(bounds.size.width),
};
self.input.update(cx, |input, _cx| {
input.available_height = bounds.size.height;
input.available_width = bounds.size.width;
input.update_line_layouts(wrap_width, line_height, &layout_state.text_style, window);
input.line_height = line_height;
input.set_text_style(&layout_state.text_style);
input.update_line_layouts(wrap_width, window);
});
let hitbox = self.interactivity.prepaint(
@@ -147,7 +149,7 @@ impl Element for Input {
let precomputed_first_line = match (snapshot.layout, snapshot.line_layouts.first()) {
(
super::InputLayout::SingleLine,
Some(InputLineLayout {
Some(InputLogicalLine {
wrapped_line: Some(wrapped_line),
..
}),
@@ -191,7 +193,7 @@ struct InputStateSnapshot {
selected_range: Range<usize>,
marked_range: Option<Range<usize>>,
cursor_offset: usize,
line_layouts: Vec<InputLineLayout>,
line_layouts: Vec<InputLogicalLine>,
scroll_offset: Pixels,
line_height: Pixels,
}
@@ -201,7 +203,7 @@ impl InputStateSnapshot {
let selected_range = input_state.selected_range().clone();
let marked_range = input_state.marked_range().cloned();
let cursor_offset = input_state.cursor_offset();
let line_layouts = input_state.line_layouts.clone();
let line_layouts = input_state.logical_lines.clone();
let scroll_offset = input_state.scroll_offset;
let line_height = input_state.line_height;
Self {
@@ -299,7 +301,7 @@ impl<'app> PaintContext<'app> {
let content_size = match axis {
gpui::Axis::Horizontal => {
let state = input.read(cx);
let line = state.line_layouts.first();
let line = state.logical_lines.first();
let line = line.and_then(|l| l.wrapped_line.as_ref());
line.map(|w| w.width()).unwrap_or(px(0.))
}
@@ -644,7 +646,7 @@ impl<'app> PaintContext<'app> {
));
}
fn is_line_visible(&self, line: &InputLineLayout) -> bool {
fn is_line_visible(&self, line: &InputLogicalLine) -> bool {
let line_y = line.y_offset - self.snapshot.scroll_offset;
let line_bottom = line_y + self.snapshot.line_height * line.visual_line_count as f32;
line_bottom >= px(0.) && line_y <= self.bounds.size.height
@@ -657,7 +659,7 @@ impl<'app> PaintContext<'app> {
fn paint_line_range(
&self,
window: &mut Window,
line: &InputLineLayout,
line: &InputLogicalLine,
subrange: &Range<usize>,
color: Hsla,
quad_offset_y: Pixels,
+35 -43
View File
@@ -43,7 +43,7 @@ pub struct InputState {
pub(super) selection_reversed: bool,
pub(super) marked_range: Option<Range<usize>>,
pub(super) line_height: Pixels,
pub(super) line_layouts: Vec<InputLineLayout>,
pub(super) logical_lines: Vec<InputLogicalLine>,
pub(super) wrap_width: Option<Pixels>,
pub(super) text_style: Option<TextStyle>,
pub(super) needs_layout: bool,
@@ -74,7 +74,7 @@ pub struct InputState {
/// A logical line corresponds to content between newlines in the input text.
/// When text wrapping is enabled, a logical line may span multiple visual lines.
#[derive(Clone, Debug)]
pub(super) struct InputLineLayout {
pub(super) struct InputLogicalLine {
/// The utf8 byte range in the content string that this line covers.
pub text_range: Range<usize>,
/// The shaped and wrapped text for this line, if available.
@@ -112,7 +112,7 @@ impl InputState {
selection_reversed: false,
marked_range: None,
line_height: px(0.),
line_layouts: Vec::new(),
logical_lines: Vec::new(),
wrap_width: None,
text_style: None,
needs_layout: true,
@@ -188,13 +188,8 @@ impl InputState {
}
/// Sets the text style used for layout. Marks layout as dirty if the style changed.
pub(crate) fn set_text_style(&mut self, style: &TextStyle) {
let changed = self
.text_style
.as_ref()
.map_or(true, |current| current != style);
if changed {
pub(super) fn set_text_style(&mut self, style: &TextStyle) {
if self.text_style.as_ref() != Some(style) {
self.text_style = Some(style.clone());
self.needs_layout = true;
}
@@ -896,7 +891,7 @@ impl InputState {
let target_visual_line_idx = (visual_line_idx as i32 + direction).max(0) as usize;
let mut current_visual_line = 0;
for layout in self.line_layouts.iter() {
for layout in self.logical_lines.iter() {
let visual_lines_in_layout = layout.visual_line_count;
if target_visual_line_idx < current_visual_line + visual_lines_in_layout {
@@ -934,13 +929,13 @@ impl InputState {
}
fn find_visual_line_and_x_offset(&self, offset: usize) -> (usize, f32) {
if self.line_layouts.is_empty() {
if self.logical_lines.is_empty() {
return (0, 0.0);
}
let mut visual_line_idx = 0;
for line in &self.line_layouts {
for line in &self.logical_lines {
if line.text_range.is_empty() {
if offset == line.text_range.start {
return (visual_line_idx, 0.0);
@@ -968,7 +963,7 @@ impl InputState {
return 0;
}
for line in self.line_layouts.iter() {
for line in self.logical_lines.iter() {
let line_height_total = self.line_height * line.visual_line_count as f32;
if position.y >= line.y_offset && position.y < line.y_offset + line_height_total {
@@ -995,7 +990,7 @@ impl InputState {
}
pub(crate) fn scroll_to_cursor(&mut self) {
if self.line_layouts.is_empty() {
if self.logical_lines.is_empty() {
return;
}
@@ -1013,7 +1008,7 @@ impl InputState {
let line_height = self.line_height;
for line in &self.line_layouts {
for line in &self.logical_lines {
let is_cursor_in_line = if line.text_range.is_empty() {
cursor_offset == line.text_range.start
} else {
@@ -1056,7 +1051,7 @@ impl InputState {
}
// For single-line input, get cursor x position from the first (only) line
let Some(line) = self.line_layouts.first() else {
let Some(line) = self.logical_lines.first() else {
return;
};
@@ -1085,28 +1080,23 @@ impl InputState {
self.scroll_offset = self.scroll_offset.max(px(0.));
}
pub(crate) fn update_line_layouts(
&mut self,
width: Pixels,
line_height: Pixels,
text_style: &TextStyle,
window: &mut Window,
) {
self.line_height = line_height;
self.set_text_style(text_style);
if !self.needs_layout && self.wrap_width == Some(width) {
/// Called internally during prepaint to layout the content into logical lines based on viewport bounds wrapping.
pub(crate) fn update_line_layouts(&mut self, wrap_width: Option<Pixels>, window: &mut Window) {
if !self.needs_layout && self.wrap_width == wrap_width {
return;
}
let Some(text_style) = &self.text_style else {
return;
};
self.line_layouts.clear();
self.wrap_width = Some(width);
self.logical_lines.clear();
self.wrap_width = wrap_width;
let text_color = text_style.color;
let font_size = text_style.font_size.to_pixels(window.rem_size());
if self.content.is_empty() {
self.line_layouts.push(InputLineLayout {
self.logical_lines.push(InputLogicalLine {
text_range: 0..0,
wrapped_line: None,
y_offset: px(0.),
@@ -1125,19 +1115,19 @@ impl InputState {
.map(|pos| current_pos + pos)
.unwrap_or(self.content.len());
let line_text = &self.content[current_pos..line_end];
let line_slice = &self.content[current_pos..line_end];
if line_text.is_empty() {
self.line_layouts.push(InputLineLayout {
if line_slice.is_empty() {
self.logical_lines.push(InputLogicalLine {
text_range: current_pos..current_pos,
wrapped_line: None,
y_offset,
visual_line_count: 1,
});
y_offset += line_height;
y_offset += self.line_height;
} else {
let run = TextRun {
len: line_text.len(),
len: line_slice.len(),
font: text_style.font(),
color: text_color,
background_color: None,
@@ -1148,19 +1138,19 @@ impl InputState {
let wrapped_lines = window
.text_system()
.shape_text(
SharedString::from(line_text.to_string()),
SharedString::from(line_slice.to_string()),
font_size,
&[run],
Some(width),
wrap_width,
None,
)
.unwrap_or_default();
for wrapped in wrapped_lines {
let visual_line_count = wrapped.wrap_boundaries().len() + 1;
let line_height_total = line_height * visual_line_count as f32;
let line_height_total = self.line_height * visual_line_count as f32;
self.line_layouts.push(InputLineLayout {
self.logical_lines.push(InputLogicalLine {
text_range: current_pos..line_end,
wrapped_line: Some(Arc::new(wrapped)),
y_offset,
@@ -1179,7 +1169,7 @@ impl InputState {
}
if self.content.ends_with('\n') {
self.line_layouts.push(InputLineLayout {
self.logical_lines.push(InputLogicalLine {
text_range: self.content.len()..self.content.len(),
wrapped_line: None,
y_offset,
@@ -1192,7 +1182,7 @@ impl InputState {
}
pub(crate) fn total_content_height(&self) -> Pixels {
self.line_layouts
self.logical_lines
.last()
.map(|last| last.y_offset + self.line_height * last.visual_line_count as f32)
.unwrap_or(px(0.))
@@ -1373,7 +1363,9 @@ mod tests {
let mut input = InputState::new(cx).layout(InputLayout::MultiLine);
input.content = content.to_string().into();
input.selected_range = range;
input.update_line_layouts(px(500.), px(20.), &TextStyle::default(), window);
input.line_height = px(20.);
input.set_text_style(&TextStyle::default());
input.update_line_layouts(Some(px(500.)), window);
input
});
TestView { input }
@@ -142,7 +142,7 @@ impl EntityInputHandler for super::InputState {
) -> Option<Bounds<Pixels>> {
let range = self.utf_range_16to8(&range_utf16);
for line in &self.line_layouts {
for line in &self.logical_lines {
if line.text_range.is_empty() {
if range.start == line.text_range.start {
return Some(Bounds::from_corners(