diff --git a/crates/gpui_elements/src/editable_text/input_element.rs b/crates/gpui_elements/src/editable_text/input_element.rs index 3bc0cbf85f..c50a830eed 100644 --- a/crates/gpui_elements/src/editable_text/input_element.rs +++ b/crates/gpui_elements/src/editable_text/input_element.rs @@ -85,10 +85,6 @@ impl EditableTextElement for TextInputElement { fn placeholder(&self) -> &Option { &self.placeholder } - - fn should_wrap(&self) -> bool { - false - } } impl Element for TextInputElement { diff --git a/crates/gpui_elements/src/editable_text/shared_element.rs b/crates/gpui_elements/src/editable_text/shared_element.rs index 64571a02b0..0d093d81b5 100644 --- a/crates/gpui_elements/src/editable_text/shared_element.rs +++ b/crates/gpui_elements/src/editable_text/shared_element.rs @@ -1,12 +1,12 @@ use crate::editable_text::{ - TextInputStateBase, TextLayoutWrapping, TextLineSegment, + TextInputLayoutData, TextLineSegment, actions::{EditableInputActionElement, EditableTextActionHandler}, }; use gpui::{ Along, App, Axis, Bounds, ContentMask, Context, CursorStyle, DispatchPhase, Display, ElementInputHandler, Entity, FocusHandle, Focusable, Hitbox, HitboxBehavior, Hsla, InteractiveElement, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, - Pixels, Point, ScrollWheelEvent, SharedString, Style, TextAlign, TextStyle, Window, + Pixels, Point, ScrollWheelEvent, SharedString, Size, Style, TextAlign, TextLayout, Window, WrappedLine, fill, point, px, size, }; use smallvec::SmallVec; @@ -52,7 +52,6 @@ pub struct PrepaintState { pub trait EditableTextElement: InteractiveElement + EditableInputActionElement { fn init_state(&self, cx: &mut Context) -> Self::State; fn placeholder(&self) -> &Option; - fn should_wrap(&self) -> bool; fn shared_request_layout( &mut self, @@ -73,21 +72,156 @@ pub trait EditableTextElement: InteractiveElement + EditableInputActionElement { } }; - // TODO: This required a gpui api change in order to sync the focus handle between Interactivity and TextInputStateBase - self.interactivity() - .track_focus(state.read(cx).focus_handle(cx)); + let focus_handle; + let show_placeholder; + let storage_version; + { + let state = state.read(cx); + focus_handle = state.focus_handle(cx); + show_placeholder = state.storage().content_utf8().is_empty(); + storage_version = state.storage().version(); + } + // TODO: This required a gpui api change in order to sync the focus handle between Interactivity and TextInputStateBase + self.interactivity().track_focus(focus_handle); + + let placeholder = self.placeholder().clone(); + let placeholder_color = Hsla::white().opacity(0.5); // TODO: as an element param let layout_id = self.interactivity().request_layout( global_id, inspector_id, window, cx, |style, window, cx| { + let state = state.clone(); window.with_text_style(style.text_style().cloned(), |window| { - //let text_style = window.text_style(); + // NOTE: Loosely mirrors TextLayout::layout + let text_layout_id = window.request_measured_layout(Default::default(), { + let text_style = window.text_style(); + let font_size = text_style.font_size.to_pixels(window.rem_size()); + let line_height = window.pixel_snap( + text_style + .line_height + .to_pixels(font_size.into(), window.rem_size()), + ); + move |known_dimensions, available_space, window, cx| { + let text: SharedString; + let color: Hsla; + let prev_wrap_width: Option; + let prev_size: Option>; + let last_seen_storage_version: u16; - // TODO: allocate the interior text layout and provide it as a child to the interactivity layout - window.request_layout(style.clone(), None, cx) + { + let state = state.read(cx); + match show_placeholder { + false => { + text = SharedString::from(state.storage().content_utf8()); + color = text_style.color; + } + true => { + text = placeholder.clone().unwrap_or_default(); + color = placeholder_color; + } + } + prev_wrap_width = state.layout_data.wrap_width; + prev_size = state.layout_data.size; + last_seen_storage_version = + state.layout_data.last_seen_storage_version; + } + + let runs = vec![gpui::TextRun { + len: text.len(), + font: text_style.font(), + color, + background_color: None, + underline: None, + strikethrough: None, + }]; + + let wrap_width = TextLayout::evaluate_wrap_width( + &text_style.white_space, + known_dimensions, + available_space, + ); + + let truncation = TextLayout::evaluate_overflow( + &text_style, + known_dimensions, + available_space, + ); + + if let Some(size) = prev_size + && (wrap_width.is_none() || wrap_width == prev_wrap_width) + && truncation.width.is_none() + && storage_version == last_seen_storage_version + { + return size; + } + + let (text, runs) = TextLayout::apply_truncation( + text, + &text_style, + font_size, + wrap_width, + &truncation, + &runs, + cx, + ); + + let wrapped_lines = window + .text_system() + .shape_text( + text, + font_size, + &runs, + wrap_width, + text_style.line_clamp, + ) + .unwrap_or_default(); + + // Build the size of the text and convert the wrapped_lines into + // lines that will be cached in state and painted. + let mut size: Size = Size::default(); + let mut pos_y = 0; + let mut line_start = 0; + let mut lines = Vec::with_capacity(wrapped_lines.len()); + for line in wrapped_lines { + let line_size = line.size(line_height); + size.height += line_size.height; + size.width = size.width.max(line_size.width).ceil(); + + let num_visual_lines = line.wrap_boundaries().len() + 1; + let line_len = line.len(); + lines.push(TextLineSegment { + text_range: line_start..line_start + line_len, + wrapped_line: Some(Arc::new(line)), + pos_y, + num_visual_lines, + }); + line_start += line_len; + pos_y += num_visual_lines; + } + + let layout_data = TextInputLayoutData { + wrap_width, + size: Some(size), + last_seen_storage_version, + lines, + lines_represent_placeholder: show_placeholder, + }; + + // Update the state for use in prepaint, paint, and action handlers. + // request_measured_layout caches this scope for processing later + // between layout and prepaint, so we cant just copy/move these values to the outer scope. + state.update(cx, move |state, _cx| { + state.layout_data = layout_data; + }); + + size + } + }); + + window.request_layout(style.clone(), Some(text_layout_id), cx) }) }, ); @@ -144,48 +278,15 @@ pub trait EditableTextElement: InteractiveElement + EditableInputActionElement { bounds }; - //let text_color = request_layout.text_style.color; - let placeholder_color = Hsla::white().opacity(0.5); // TODO: as an element param let selection_color = Hsla::blue().opacity(0.5); // TODO: as an element param let caret_color = Hsla::white(); // TODO: as an element param - /* - let wrap_width = self.should_wrap().then_some(inner_bounds.size.width); - let showing_placeholder = request_layout.state.update(cx, |state, _cx| { - let wrapping = TextLayoutWrapping::new( - request_layout.text_style.clone(), - wrap_width, - state.storage().version(), - ); - let show_placeholder = state.storage().content_utf8().is_empty(); - state.layout_data.bounds = inner_bounds; - if state.layout_wrapping.integrate(wrapping) { - let (display_text, color) = match show_placeholder { - false => (state.storage().content_utf8(), text_color), - true => { - let value = self.placeholder().as_ref(); - let value = value.map(SharedString::as_str).unwrap_or_default(); - (value, placeholder_color) - } - }; - state.layout_data.lines = TextInputStateBase::build_wrapped_lines( - display_text, - &state.layout_wrapping, - window, - color, - ); - } - show_placeholder - }); - */ - let showing_placeholder = false; + let state = request_layout.state.read(cx); - let input = request_layout.state.read(cx); - - let focus_handle = input.focus_handle(cx); - let caret_pos = input.caret_pos(); - let selection = input.selected_range(); - let ime_range = input.marked_range(); + let focus_handle = state.focus_handle(cx); + let caret_pos = state.caret_pos(); + let selection = state.selected_range(); + let ime_range = state.marked_range(); // TODO: Cursor blinking let cursor_visible = true; // input.cursor_visible(); @@ -203,7 +304,7 @@ pub trait EditableTextElement: InteractiveElement + EditableInputActionElement { } }; let mut carent_point = Point::default(); - for segment in input.line_segments() { + for segment in &state.layout_data.lines { let line_distance_from_top = segment.pos_y * line_height; let line_y = line_distance_from_top - scroll_offset.y; let line_bottom = line_y + line_height * segment.num_visual_lines as f32; @@ -290,7 +391,7 @@ pub trait EditableTextElement: InteractiveElement + EditableInputActionElement { } let is_focused = focus_handle.is_focused(window); - if !showing_placeholder && is_focused && cursor_visible { + if !state.layout_data.lines_represent_placeholder && is_focused && cursor_visible { const CURSOR_WIDTH: f32 = 2.0; let quad = fill( Bounds::new( diff --git a/crates/gpui_elements/src/editable_text/shared_state.rs b/crates/gpui_elements/src/editable_text/shared_state.rs index c7d0f33449..97d92392f5 100644 --- a/crates/gpui_elements/src/editable_text/shared_state.rs +++ b/crates/gpui_elements/src/editable_text/shared_state.rs @@ -4,9 +4,8 @@ use crate::editable_text::{ notify::{TextChanged, TextHistoryPushed}, }; use gpui::{ - App, Bounds, ClipboardItem, EntityInputHandler, FocusHandle, Focusable, Hsla, - NavigationDirection, Pixels, Point, SharedString, TextRun, TextStyle, UTF16Selection, Window, - WrappedLine, point, + App, Bounds, ClipboardItem, EntityInputHandler, FocusHandle, Focusable, NavigationDirection, + Pixels, Point, Size, UTF16Selection, Window, WrappedLine, point, }; use std::{ops::Range, sync::Arc}; @@ -47,49 +46,21 @@ pub struct TextInputStateBase { focus_handle: FocusHandle, - pub(super) layout_wrapping: TextLayoutWrapping, pub(super) layout_data: TextInputLayoutData, } -#[derive(PartialEq)] -pub(super) struct TextLayoutWrapping { - text_style: TextStyle, - wrap_width: Option, - last_seen_storage_version: u16, -} -impl Default for TextLayoutWrapping { - fn default() -> Self { - Self { - text_style: Default::default(), - wrap_width: Default::default(), - last_seen_storage_version: u16::MAX, - } - } -} -impl TextLayoutWrapping { - pub fn new(text_style: TextStyle, wrap_width: Option, storage_version: u16) -> Self { - Self { - text_style, - wrap_width, - last_seen_storage_version: storage_version, - } - } - - pub fn integrate(&mut self, other: Self) -> bool { - let dirty = *self != other; - *self = other; - dirty - } -} - #[derive(Default)] pub(super) struct TextInputLayoutData { + /// The last known width at which the lines were wrapped. + pub wrap_width: Option, + /// The last known size of the text, as generated during layout. + pub size: Option>, + /// The last seen version of `storage` (for tracking when lines need to be reprocessed during layout) + pub last_seen_storage_version: u16, /// The `ShapedLine` produced by the painter's `prepaint`. /// Cached so IME `bounds_for_range` / `character_index_for_point` can evaluate without re-shaping. pub lines: Vec, - /// The bounds of the text area, in window coordinates. - /// Cached for IME operations. - pub bounds: Bounds, + pub lines_represent_placeholder: bool, } pub(super) struct TextLineSegment { /// The utf8 byte range in the content string that this line covers. @@ -125,7 +96,6 @@ impl TextInputStateBase { focus_handle: cx.focus_handle(), - layout_wrapping: TextLayoutWrapping::default(), layout_data: TextInputLayoutData::default(), } } @@ -163,101 +133,6 @@ impl TextInputStateBase { } impl TextInputStateBase { - pub(super) fn line_segments(&self) -> &Vec { - &self.layout_data.lines - } - - pub(super) fn build_wrapped_lines( - content: &str, - wrapping: &TextLayoutWrapping, - window: &Window, - color: Hsla, - ) -> Vec { - let text_style = &wrapping.text_style; - let font_size = text_style.font_size.to_pixels(window.rem_size()); - let mut lines = Vec::new(); - - if content.is_empty() { - lines.push(TextLineSegment { - text_range: 0..0, - wrapped_line: None, - pos_y: 0, - num_visual_lines: 1, - }); - return lines; - } - - let mut pos_y = 0; - let mut current_pos = 0; - - while current_pos < content.len() { - let line_end = content[current_pos..] - .find('\n') - .map(|pos| current_pos + pos) - .unwrap_or(content.len()); - - let line_slice = &content[current_pos..line_end]; - - if line_slice.is_empty() { - lines.push(TextLineSegment { - text_range: current_pos..current_pos, - wrapped_line: None, - pos_y, - num_visual_lines: 1, - }); - pos_y += 1; - } else { - let run = TextRun { - len: line_slice.len(), - font: text_style.font(), - color, - background_color: None, - underline: None, - strikethrough: None, - }; - - let wrapped_lines = window - .text_system() - .shape_text( - SharedString::from(line_slice.to_string()), - font_size, - &[run], - wrapping.wrap_width, - None, - ) - .unwrap_or_default(); - - for wrapped in wrapped_lines { - let num_visual_lines = wrapped.wrap_boundaries().len() + 1; - lines.push(TextLineSegment { - text_range: current_pos..line_end, - wrapped_line: Some(Arc::new(wrapped)), - pos_y, - num_visual_lines, - }); - pos_y += num_visual_lines; - } - } - - current_pos = if line_end < content.len() { - line_end + 1 - } else { - content.len() - }; - } - - if content.ends_with('\n') { - lines.push(TextLineSegment { - text_range: content.len()..content.len(), - wrapped_line: None, - pos_y, - num_visual_lines: 1, - }); - } - - lines - } - /// Returns the utf-8 character position of the start of the line that contains the provided pixel-point. pub fn index_for_pixel_point(&self, point: Point, line_height: Pixels) -> usize { let storage_len_utf8 = self.storage.content_utf8().len(); diff --git a/crates/gpui_elements/src/editable_text/text_area_element.rs b/crates/gpui_elements/src/editable_text/text_area_element.rs index a9fe9120dc..c10ec89ebd 100644 --- a/crates/gpui_elements/src/editable_text/text_area_element.rs +++ b/crates/gpui_elements/src/editable_text/text_area_element.rs @@ -4,8 +4,8 @@ use crate::editable_text::{ shared_element::{self, EditableTextElement}, }; use gpui::{ - App, Bounds, Element, ElementId, Entity, Hitbox, InteractiveElement, Interactivity, - IntoElement, Pixels, SharedString, StyleRefinement, Styled, TextStyle, WeakEntity, Window, + App, Bounds, Element, ElementId, InteractiveElement, Interactivity, IntoElement, Pixels, + SharedString, StyleRefinement, Styled, WeakEntity, Window, }; use std::{cell::RefCell, rc::Rc}; @@ -84,10 +84,6 @@ impl EditableTextElement for TextAreaElement { fn placeholder(&self) -> &Option { &self.placeholder } - - fn should_wrap(&self) -> bool { - true - } } impl Element for TextAreaElement {