separate update_line_layouts from other mutations in prepaint
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user