From 1675c73726602b3d89d8480ef0da381fa1e9dd72 Mon Sep 17 00:00:00 2001 From: temportalflux Date: Sat, 6 Jun 2026 13:36:30 -0400 Subject: [PATCH] separate update_line_layouts from other mutations in prepaint --- crates/gpui_elements/src/input/paint.rs | 22 +++--- crates/gpui_elements/src/input/state.rs | 78 +++++++++---------- .../src/input/state_input_handler.rs | 2 +- 3 files changed, 48 insertions(+), 54 deletions(-) diff --git a/crates/gpui_elements/src/input/paint.rs b/crates/gpui_elements/src/input/paint.rs index 2bf64d13f8..f5f1bf600c 100644 --- a/crates/gpui_elements/src/input/paint.rs +++ b/crates/gpui_elements/src/input/paint.rs @@ -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, marked_range: Option>, cursor_offset: usize, - line_layouts: Vec, + line_layouts: Vec, 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, color: Hsla, quad_offset_y: Pixels, diff --git a/crates/gpui_elements/src/input/state.rs b/crates/gpui_elements/src/input/state.rs index 0a9a3bbf7b..7c4d174109 100644 --- a/crates/gpui_elements/src/input/state.rs +++ b/crates/gpui_elements/src/input/state.rs @@ -43,7 +43,7 @@ pub struct InputState { pub(super) selection_reversed: bool, pub(super) marked_range: Option>, pub(super) line_height: Pixels, - pub(super) line_layouts: Vec, + pub(super) logical_lines: Vec, pub(super) wrap_width: Option, pub(super) text_style: Option, 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, /// 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, 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 } diff --git a/crates/gpui_elements/src/input/state_input_handler.rs b/crates/gpui_elements/src/input/state_input_handler.rs index a9b9e3d481..4fe80ee16b 100644 --- a/crates/gpui_elements/src/input/state_input_handler.rs +++ b/crates/gpui_elements/src/input/state_input_handler.rs @@ -142,7 +142,7 @@ impl EntityInputHandler for super::InputState { ) -> Option> { 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(