diff --git a/crates/gpui_elements/src/editable_text/input_element.rs b/crates/gpui_elements/src/editable_text/input_element.rs index 61f3ec8a9e..0ffb850f2f 100644 --- a/crates/gpui_elements/src/editable_text/input_element.rs +++ b/crates/gpui_elements/src/editable_text/input_element.rs @@ -1,9 +1,12 @@ use crate::editable_text::{ - EditableInputActionElement, InitStorage, StateBackedElement, TextInputState, + EditableInputActionElement, InitStorage, StateBackedElement, TextInputLayoutData, + TextInputState, }; use gpui::{ - App, Element, ElementId, Entity, Hitbox, InteractiveElement, Interactivity, IntoElement, - SharedString, StyleRefinement, Styled, TextStyle, Window, + App, Bounds, ContentMask, CursorStyle, Display, Element, ElementId, ElementInputHandler, + Entity, FocusHandle, Focusable, Hitbox, HitboxBehavior, Hsla, InteractiveElement, + Interactivity, IntoElement, PaintQuad, Pixels, ShapedLine, SharedString, Style, + StyleRefinement, Styled, TextRun, TextStyle, Window, fill, point, size, }; #[track_caller] @@ -80,6 +83,13 @@ pub mod element { #[doc(hidden)] pub struct PrepaintState { pub hitbox: Option, + pub line: Option, + pub focus_handle: FocusHandle, + pub selection: Option, + pub caret_quad: Option, + pub scroll_x: Pixels, + pub display_text: SharedString, + pub caret_visible: bool, } } @@ -122,7 +132,7 @@ impl Element for TextInputElement { }, ); - let layout_state = element::LayoutState { + let layout_state = Self::RequestLayoutState { state, text_style: resolved_text_style.unwrap_or_else(|| window.text_style()), }; @@ -131,19 +141,119 @@ impl Element for TextInputElement { fn prepaint( &mut self, - id: Option<&gpui::GlobalElementId>, + global_id: Option<&gpui::GlobalElementId>, inspector_id: Option<&gpui::InspectorElementId>, bounds: gpui::Bounds, request_layout: &mut Self::RequestLayoutState, window: &mut gpui::Window, cx: &mut gpui::App, ) -> Self::PrepaintState { - todo!() + 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(); + // TODO: horizontal scroll + let scroll_x_input = Pixels::ZERO; //input.scroll_x(); + // TODO: Cursor blinking + let cursor_visible = true; // input.cursor_visible(); + + let text_color = Hsla::white(); // TODO: as an element param + let placeholder_color = Hsla::black().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 text_value = input.storage().content_utf8(); + let is_empty = text_value.is_empty(); + + let (display_text, run_color) = match is_empty { + // TODO: Can the SharedString allocation be avoided? + false => (SharedString::new(text_value), text_color), + true => { + let value = self.placeholder.as_ref().cloned().unwrap_or_default(); + (value, placeholder_color) + } + }; + + let style = window.text_style(); + let font_size = style.font_size.to_pixels(window.rem_size()); + let run = TextRun { + len: display_text.len(), + font: style.font(), + color: run_color, + background_color: None, + underline: None, + strikethrough: None, + }; + let line = window + .text_system() + .shape_line(display_text.clone(), font_size, &[run], None); + + let caret_x_line = line.x_for_index(caret_pos); + let cursor_thickness = gpui::px(2.0); + let max_cursor_x = (bounds.size.width - cursor_thickness).max(Pixels::ZERO); + let max_scroll_x = (line.width - max_cursor_x).max(Pixels::ZERO); + let mut scroll_x = scroll_x_input.clamp(Pixels::ZERO, max_scroll_x); + if caret_x_line < scroll_x { + scroll_x = caret_x_line; + } else if caret_x_line > scroll_x + max_cursor_x { + scroll_x = caret_x_line - max_cursor_x; + } + scroll_x = scroll_x.clamp(Pixels::ZERO, max_scroll_x); + + let (selection_quad, cursor_quad) = if !selection.is_empty() && !is_empty { + let start_x = line.x_for_index(selection.start); + let end_x = line.x_for_index(selection.end); + let quad = fill( + Bounds::from_corners( + point(bounds.left() + start_x.min(end_x) - scroll_x, bounds.top()), + point( + bounds.left() + start_x.max(end_x) - scroll_x, + bounds.bottom(), + ), + ), + selection_color, + ); + (Some(quad), None) + } else { + let cursor_paint_x = bounds.left() + caret_x_line - scroll_x; + let quad = fill( + Bounds::new( + point(cursor_paint_x, bounds.top()), + size(cursor_thickness, bounds.bottom() - bounds.top()), + ), + caret_color, + ); + (None, Some(quad)) + }; + + let hitbox = self.interactivity.prepaint( + global_id, + inspector_id, + bounds, + bounds.size, + window, + cx, + |style, scroll_offset, hitbox, window, cx| { + hitbox.or_else(|| Some(window.insert_hitbox(bounds, HitboxBehavior::Normal))) + }, + ); + + Self::PrepaintState { + hitbox, + line: Some(line), + focus_handle, + selection: selection_quad, + caret_quad: cursor_quad, + scroll_x, + display_text, + caret_visible: cursor_visible, + } } fn paint( &mut self, - id: Option<&gpui::GlobalElementId>, + global_id: Option<&gpui::GlobalElementId>, inspector_id: Option<&gpui::InspectorElementId>, bounds: gpui::Bounds, request_layout: &mut Self::RequestLayoutState, @@ -151,6 +261,66 @@ impl Element for TextInputElement { window: &mut gpui::Window, cx: &mut gpui::App, ) { - todo!() + if let Some(hitbox) = &prepaint.hitbox { + window.set_cursor_style(CursorStyle::IBeam, hitbox); + } + + // NOTE: Skip when disabled + let ime_handler = ElementInputHandler::new(bounds, request_layout.state.clone()); + window.handle_input(&prepaint.focus_handle, ime_handler, cx); + + let mut layout_data = TextInputLayoutData::default(); + let perform_paint = |style: &Style, window: &mut Window, cx: &mut App| { + if style.display == Display::None { + return; + } + layout_data = window.with_content_mask(Some(ContentMask { bounds }), |window| { + if let Some(sel) = prepaint.selection.take() { + window.paint_quad(sel); + } + + let line = prepaint + .line + .take() + .expect("prepaint always produces a line"); + let origin_x = bounds.left() - prepaint.scroll_x; + let _ = line.paint( + point(origin_x, bounds.top()), + window.line_height(), + gpui::TextAlign::Left, + None, + window, + cx, + ); + + // TODO: Render marked IME underlines + + let is_focused = prepaint.focus_handle.is_focused(window); + if is_focused + && prepaint.caret_visible + && let Some(cur) = prepaint.caret_quad.take() + { + window.paint_quad(cur); + } + + TextInputLayoutData { + lines: vec![line], + bounds, + } + }); + }; + self.interactivity.paint( + global_id, + inspector_id, + bounds, + prepaint.hitbox.as_ref(), + window, + cx, + perform_paint, + ); + + request_layout.state.update(cx, |state, _cx| { + *state.layout_data_mut() = layout_data; + }); } } diff --git a/crates/gpui_elements/src/editable_text/input_state.rs b/crates/gpui_elements/src/editable_text/input_state.rs index d5c784172e..790a2307a0 100644 --- a/crates/gpui_elements/src/editable_text/input_state.rs +++ b/crates/gpui_elements/src/editable_text/input_state.rs @@ -16,6 +16,19 @@ pub struct TextInputState { impl EventEmitter for TextInputState {} impl EventEmitter for TextInputState {} +impl std::ops::Deref for TextInputState { + type Target = TextInputStateBase; + + fn deref(&self) -> &Self::Target { + &self.internal + } +} +impl std::ops::DerefMut for TextInputState { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.internal + } +} + impl TextInputState { pub fn new(storage: impl Into>, cx: &mut Context) -> Self { let internal = TextInputStateBase::new(storage, cx); diff --git a/crates/gpui_elements/src/editable_text/shared_state.rs b/crates/gpui_elements/src/editable_text/shared_state.rs index e67d3af202..a024e545d9 100644 --- a/crates/gpui_elements/src/editable_text/shared_state.rs +++ b/crates/gpui_elements/src/editable_text/shared_state.rs @@ -3,8 +3,8 @@ use crate::editable_text::{ notify::{TextChanged, TextHistoryPushed}, }; use gpui::{ - App, ClipboardItem, Entity, FocusHandle, Focusable, NavigationDirection, Pixels, Point, - UTF16Selection, Window, + App, Bounds, ClipboardItem, Entity, FocusHandle, Focusable, NavigationDirection, Pixels, Point, + ShapedLine, UTF16Selection, Window, }; use std::ops::Range; @@ -53,6 +53,18 @@ pub struct TextInputStateBase { click_count: usize, focus_handle: FocusHandle, + + layout_data: TextInputLayoutData, +} + +#[derive(Default)] +pub(super) struct TextInputLayoutData { + /// 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, } impl Focusable for TextInputStateBase { @@ -74,6 +86,8 @@ impl TextInputStateBase { click_count: 0, focus_handle: cx.focus_handle(), + + layout_data: TextInputLayoutData::default(), } } @@ -103,6 +117,14 @@ impl TextInputStateBase { pub fn set_selected_range(&mut self, range: Range) { self.selected_range = range; } + + pub(super) fn layout_data(&self) -> &TextInputLayoutData { + &self.layout_data + } + + pub(super) fn layout_data_mut(&mut self) -> &mut TextInputLayoutData { + &mut self.layout_data + } } impl TextInputStateBase { diff --git a/crates/gpui_elements/src/editable_text/text_area_state.rs b/crates/gpui_elements/src/editable_text/text_area_state.rs index a69225e3ac..e3be1e0525 100644 --- a/crates/gpui_elements/src/editable_text/text_area_state.rs +++ b/crates/gpui_elements/src/editable_text/text_area_state.rs @@ -16,6 +16,19 @@ pub struct TextAreaState { impl EventEmitter for TextAreaState {} impl EventEmitter for TextAreaState {} +impl std::ops::Deref for TextAreaState { + type Target = TextInputStateBase; + + fn deref(&self) -> &Self::Target { + &self.internal + } +} +impl std::ops::DerefMut for TextAreaState { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.internal + } +} + impl TextAreaState { pub fn new(storage: impl Into>, cx: &mut Context) -> Self { let internal = TextInputStateBase::new(storage, cx);