diff --git a/crates/gpui_elements/src/editable_text.rs b/crates/gpui_elements/src/editable_text.rs index d46021f3c2..97acf86577 100644 --- a/crates/gpui_elements/src/editable_text.rs +++ b/crates/gpui_elements/src/editable_text.rs @@ -2,6 +2,7 @@ pub mod actions; mod input_element; mod input_state; pub mod notify; +mod shared_element; mod shared_state; mod storage; mod text_area_element; diff --git a/crates/gpui_elements/src/editable_text/actions.rs b/crates/gpui_elements/src/editable_text/actions.rs index c27fc40d7f..d597b17496 100644 --- a/crates/gpui_elements/src/editable_text/actions.rs +++ b/crates/gpui_elements/src/editable_text/actions.rs @@ -2,6 +2,8 @@ use std::{cell::RefCell, rc::Rc}; use gpui::{Action, AppContext, Context, InteractiveElement, WeakEntity, Window}; +use crate::editable_text::StateBackedEditableText; + /// The key context used for input element keybindings. pub const DEFAULT_INPUT_CONTEXT: &str = "Input"; @@ -236,9 +238,7 @@ pub trait EditableTextActionHandler<'app>: Sized { } } -pub(super) trait EditableInputActionElement { - type State: 'static; - +pub(super) trait EditableInputActionElement: StateBackedEditableText { fn state_entity_rc(&self) -> &Rc>>; fn register_action( diff --git a/crates/gpui_elements/src/editable_text/input_element.rs b/crates/gpui_elements/src/editable_text/input_element.rs index f61a9987c3..740eebaa2e 100644 --- a/crates/gpui_elements/src/editable_text/input_element.rs +++ b/crates/gpui_elements/src/editable_text/input_element.rs @@ -1,16 +1,13 @@ use crate::editable_text::{ - InitStorage, TextInputState, TextInputStateBase, TextLayoutWrapping, TextLineSegment, + InitStorage, StateBackedEditableText, TextInputState, actions::{DEFAULT_INPUT_CONTEXT, EditableInputActionElement}, + shared_element::{self, EditableTextElement}, }; use gpui::{ - Along, App, Axis, Bounds, ContentMask, CursorStyle, DispatchPhase, Display, Element, ElementId, - ElementInputHandler, Entity, FocusHandle, Focusable, Hitbox, HitboxBehavior, Hsla, - InteractiveElement, Interactivity, IntoElement, MouseButton, MouseDownEvent, MouseMoveEvent, - MouseUpEvent, PaintQuad, Pixels, Point, ScrollWheelEvent, SharedString, Style, StyleRefinement, - Styled, TextAlign, TextStyle, WeakEntity, Window, WrappedLine, fill, point, px, size, + App, Bounds, Element, ElementId, InteractiveElement, Interactivity, IntoElement, Pixels, + SharedString, StyleRefinement, Styled, WeakEntity, Window, }; -use smallvec::SmallVec; -use std::{cell::RefCell, ops::Range, rc::Rc, sync::Arc}; +use std::{cell::RefCell, rc::Rc}; #[track_caller] pub fn input(id: impl Into) -> TextInputElement { @@ -65,58 +62,33 @@ impl IntoElement for TextInputElement { } } -impl EditableInputActionElement for TextInputElement { +impl StateBackedEditableText for TextInputElement { type State = TextInputState; +} + +impl EditableInputActionElement for TextInputElement { fn state_entity_rc(&self) -> &Rc>> { &self.state_entity } } -enum PrepaintElement { - Line { - line: Arc, - point: Point, - align: TextAlign, - }, - Quad(PaintQuad), -} -impl PrepaintElement { - fn build_quads( - offset_corners: Vec<(Point, Point)>, - origin: Point, - color: Hsla, - ) -> impl Iterator { - let iter = offset_corners.into_iter(); - iter.map(move |(offset_start, offset_end)| { - let bounds = Bounds::from_corners(origin + offset_start, origin + offset_end); - PrepaintElement::Quad(fill(bounds, color)) - }) - } -} - -pub mod element { - use super::*; - use smallvec::SmallVec; - - #[doc(hidden)] - pub struct LayoutState { - pub state: Entity, - pub text_style: TextStyle, +impl EditableTextElement for TextInputElement { + fn init_state(&self, cx: &mut gpui::prelude::Context) -> Self::State { + Self::State::new(self.init_storage.exec(cx), cx) } - #[doc(hidden)] - pub struct PrepaintState { - pub hitbox: Option, - pub focus_handle: FocusHandle, - pub(super) elements: SmallVec<[PrepaintElement; 3]>, - pub scroll_offset: Point, - pub caret_visible: bool, + fn placeholder(&self) -> &Option { + &self.placeholder + } + + fn should_wrap(&self) -> bool { + false } } impl Element for TextInputElement { - type RequestLayoutState = element::LayoutState; - type PrepaintState = element::PrepaintState; + type RequestLayoutState = shared_element::LayoutState; + type PrepaintState = shared_element::PrepaintState; fn id(&self) -> Option { self.interactivity.element_id.clone() @@ -130,464 +102,42 @@ impl Element for TextInputElement { &mut self, global_id: Option<&gpui::GlobalElementId>, inspector_id: Option<&gpui::InspectorElementId>, - window: &mut gpui::Window, - cx: &mut gpui::App, + window: &mut Window, + cx: &mut App, ) -> (gpui::LayoutId, Self::RequestLayoutState) { - // Fetches or initializes the internal state of the field - let state = match &self.interactivity.element_id { - None => unimplemented!("all input elements must be assigned an id"), - Some(element_id) => { - let state = window.use_keyed_state(element_id.clone(), cx, |_window, cx| { - println!("init input state {:?}", cx.weak_entity()); - TextInputState::new(self.init_storage.exec(cx), cx) - }); - // store a reference to the entity owned by the element for access in action handlers - *self.state_entity.borrow_mut() = state.downgrade(); - state - } - }; - - let mut resolved_text_style = None; - - self.interactivity - .track_focus(state.read(cx).focus_handle(cx)); - let layout_id = self.interactivity.request_layout( - global_id, - inspector_id, - window, - cx, - |element_style, window, cx| { - window.with_text_style(element_style.text_style().cloned(), |window| { - resolved_text_style = Some(window.text_style()); - - let style = element_style.clone(); - // TODO: Does this need to propagate the line_height as the element's height? - window.request_layout(style, None, cx) - }) - }, - ); - - let layout_state = Self::RequestLayoutState { - state, - text_style: resolved_text_style.unwrap_or_else(|| window.text_style()), - }; - (layout_id, layout_state) + self.shared_request_layout(global_id, inspector_id, window, cx) } fn prepaint( &mut self, global_id: Option<&gpui::GlobalElementId>, inspector_id: Option<&gpui::InspectorElementId>, - bounds: gpui::Bounds, + bounds: Bounds, request_layout: &mut Self::RequestLayoutState, - window: &mut gpui::Window, - cx: &mut gpui::App, + window: &mut Window, + cx: &mut App, ) -> Self::PrepaintState { - struct InteractivityPrepaint { - hitbox: Option, - scroll_offset: Point, - } - // TODO: how do we enable scrolling? overflow on interactivity? - let prepaint = self.interactivity.prepaint( - global_id, - inspector_id, - bounds, - bounds.size, - window, - cx, - |_style, scroll_offset, hitbox, window, _cx| { - let hitbox = - hitbox.or_else(|| Some(window.insert_hitbox(bounds, HitboxBehavior::Normal))); - InteractivityPrepaint { - hitbox, - scroll_offset, - } - }, - ); - let InteractivityPrepaint { - hitbox, - scroll_offset, - } = prepaint; - - 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 - - // TODO: no wrapping in single-line - let wrap_width = Some(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 = 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 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(); - // TODO: Cursor blinking - let cursor_visible = true; // input.cursor_visible(); - - let mut elements = SmallVec::new(); - - let line_height = window.line_height(); - let is_range_contained_by_range = - |text_range: &Range, containing_range: &Range| { - if text_range.is_empty() { - containing_range.start <= text_range.start - && containing_range.end > text_range.start - } else { - containing_range.end > text_range.start - && containing_range.start < text_range.end - } - }; - let mut carent_point = Point::default(); - for segment in input.line_segments() { - 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; - let line_visible = line_bottom >= Pixels::ZERO && line_y <= bounds.size.height; - if !line_visible { - continue; - } - - // TODO: First render all lines (underlines for IME), then all selections, then cursor if no selection - - if let Some(wrapped) = &segment.wrapped_line { - let point = bounds.origin + point(Pixels::ZERO, line_y); - elements.push(PrepaintElement::Line { - line: wrapped.clone(), - point, - align: TextAlign::Left, - }); - } - - let segment_is_empty = segment.text_range.is_empty(); - - if is_range_contained_by_range(&segment.text_range, &selection) { - if segment_is_empty { - const EMPTY_LINE_SELECTION_WIDTH: Pixels = px(6.); - elements.push(PrepaintElement::Quad(fill( - Bounds::from_corners( - bounds.origin + point(Pixels::ZERO, line_y), - bounds.origin + point(EMPTY_LINE_SELECTION_WIDTH, line_y + line_height), - ), - selection_color, - ))); - } else { - let offset_corners = build_quad_over_text( - &selection, - segment, - line_y, - line_height, - Pixels::ZERO, - ); - elements.extend(PrepaintElement::build_quads( - offset_corners, - bounds.origin, - selection_color, - )); - } - } - - if !segment_is_empty && let Some(ime_range) = &ime_range { - if !ime_range.is_empty() - && is_range_contained_by_range(&segment.text_range, &ime_range) - { - const MARKED_TEXT_UNDERLINE_THICKNESS: f32 = 2.0; - let underline_thickness = px(MARKED_TEXT_UNDERLINE_THICKNESS); - let underline_offset = line_height - underline_thickness; - - let offset_corners = build_quad_over_text( - &ime_range, - segment, - line_y, - line_height, - underline_offset, - ); - elements.extend(PrepaintElement::build_quads( - offset_corners, - bounds.origin, - selection_color, - )); - } - } - - let is_cursor_in_line = if segment_is_empty { - caret_pos == segment.text_range.start - } else { - segment.text_range.contains(&caret_pos) || caret_pos == segment.text_range.end - }; - if is_cursor_in_line && let Some(wrapped) = &segment.wrapped_line { - let local_offset = caret_pos.saturating_sub(segment.text_range.start); - let caret_px = wrapped - .position_for_index(local_offset, line_height) - .unwrap_or_default(); - carent_point = caret_px + point(Pixels::ZERO, line_y); - } - } - - let is_focused = focus_handle.is_focused(window); - if !showing_placeholder && is_focused && cursor_visible { - const CURSOR_WIDTH: f32 = 2.0; - let quad = fill( - Bounds::new( - bounds.origin + carent_point - scroll_offset, - size(gpui::px(CURSOR_WIDTH), line_height), - ), - caret_color, - ); - elements.push(PrepaintElement::Quad(quad)); - } - - Self::PrepaintState { - hitbox, - focus_handle, - elements, - scroll_offset, - caret_visible: cursor_visible, - } + self.shared_prepaint(global_id, inspector_id, bounds, request_layout, window, cx) } fn paint( &mut self, global_id: Option<&gpui::GlobalElementId>, inspector_id: Option<&gpui::InspectorElementId>, - bounds: gpui::Bounds, + bounds: Bounds, request_layout: &mut Self::RequestLayoutState, prepaint: &mut Self::PrepaintState, - window: &mut gpui::Window, - cx: &mut gpui::App, + window: &mut Window, + cx: &mut App, ) { - if let Some(hitbox) = &prepaint.hitbox { - window.set_cursor_style(CursorStyle::IBeam, hitbox); - } - - let perform_paint = |style: &Style, window: &mut Window, cx: &mut App| { - if style.display == Display::None { - return; - } - - // NOTE: Skip when disabled - let ime_handler = ElementInputHandler::new(bounds, request_layout.state.clone()); - window.handle_input(&prepaint.focus_handle, ime_handler, cx); - - use super::actions::EditableTextActionHandler; - let get_relative_position = { - let bounds = bounds.clone(); - move |position: Point| { - // Converts a screen position to a position relative to the text area origin, - // adjusted for scroll offset. - let scroll_distance = gpui::px(0.); // TODO: STUB - (position - bounds.origin) - .apply_along(Axis::Horizontal, |pos| pos + scroll_distance) - } - }; - window.on_mouse_event({ - let state = request_layout.state.clone(); - move |event: &MouseDownEvent, phase, window, cx| { - if phase != DispatchPhase::Bubble { - return; - } - if !bounds.contains(&event.position) { - return; - } - if event.button != MouseButton::Left { - return; - } - - let text_position = get_relative_position(event.position); - state.update(cx, |state, cx| { - state.on_mouse_down(event, text_position, window, cx); - }); - } - }); - window.on_mouse_event({ - let state = request_layout.state.clone(); - move |event: &MouseUpEvent, phase, window, cx| { - if phase != DispatchPhase::Bubble { - return; - } - if event.button != MouseButton::Left { - return; - } - - state.update(cx, |state, cx| { - state.on_mouse_up(event, window, cx); - }); - } - }); - window.on_mouse_event({ - let state = request_layout.state.clone(); - move |event: &MouseMoveEvent, phase, window, cx| { - if phase != DispatchPhase::Bubble { - return; - } - - let text_position = get_relative_position(event.position); - state.update(cx, |state, cx| { - state.on_mouse_move(event, text_position, window, cx); - }); - } - }); - window.on_mouse_event({ - let state = request_layout.state.clone(); - /* - let content_size = match axis { - gpui::Axis::Horizontal => { - let state = input.read(cx); - let line = state.lines().first(); - let line = line.and_then(|l| l.wrapped_line.as_ref()); - line.map(|w| w.width()).unwrap_or(px(0.)) - } - gpui::Axis::Vertical => input.read(cx).total_content_height(), - }; - let max_scroll = (content_size - bounds.size.along(axis)).max(px(0.)); - */ - // TODO: Scroll mouse wheel - move |event: &ScrollWheelEvent, phase, _window, cx| { - if phase != DispatchPhase::Bubble { - return; - } - if !bounds.contains(&event.position) { - return; - } - - // use shift to alter horizontal scroll on text area - //event.modifiers.shift; - - /* - let pixel_delta = event.delta.pixel_delta(px(20.)); - state.update(cx, |state, cx| { - let delta = match axis { - gpui::Axis::Horizontal => pixel_delta.y, - gpui::Axis::Vertical => { - if pixel_delta.x.abs() > pixel_delta.y.abs() { - pixel_delta.x - } else { - pixel_delta.y - } - } - }; - state.apply_scroll_delta(delta, max_scroll); - cx.notify(); - }); - */ - } - }); - - window.with_content_mask(Some(ContentMask { bounds }), |window| { - let line_h = window.line_height(); - let mut lines = Vec::with_capacity(prepaint.elements.len()); - for element in prepaint.elements.drain(..) { - match element { - PrepaintElement::Line { line, point, align } => { - let _ = line.paint(point, line_h, align, Some(bounds), window, cx); - lines.push(line); - } - PrepaintElement::Quad(quad) => window.paint_quad(quad), - } - } - - // TODO: Render marked IME underlines - }); - }; - self.interactivity.paint( + self.shared_paint( global_id, inspector_id, - bounds.clone(), - prepaint.hitbox.as_ref(), + bounds, + request_layout, + prepaint, window, cx, - perform_paint, ); } } - -fn build_quad_over_text( - containing_range: &Range, - segment: &TextLineSegment, - line_y: Pixels, - line_height: Pixels, - offset_y: Pixels, -) -> Vec<(Point, Point)> { - let Some(wrapped) = &segment.wrapped_line else { - return vec![]; - }; - - let line_start = segment.text_range.start; - let line_end = segment.text_range.end; - - let subrange_start = containing_range.start.max(line_start) - line_start; - let subrange_end = containing_range.end.min(line_end) - line_start; - - let start_pos = wrapped - .position_for_index(subrange_start, line_height) - .unwrap_or_default(); - let end_pos = wrapped - .position_for_index(subrange_end, line_height) - .unwrap_or_else(|| { - let last_line_y = line_height * (segment.num_visual_lines - 1) as f32; - point(wrapped.width(), last_line_y) - }); - - let start_visual_line = (start_pos.y / line_height).floor() as usize; - let end_visual_line = (end_pos.y / line_height).floor() as usize; - - if start_visual_line == end_visual_line { - vec![( - point(start_pos.x, line_y + start_pos.y + offset_y), - point(end_pos.x, line_y + start_pos.y + line_height), - )] - } else { - let line_width = wrapped.width(); - let middle_lines = (start_visual_line + 1)..end_visual_line; - let mut quad_corners = Vec::with_capacity(middle_lines.end - middle_lines.start + 2); - - quad_corners.push(( - point(start_pos.x, line_y + start_pos.y + offset_y), - point(line_width, line_y + start_pos.y + line_height), - )); - - // Middle visual lines - for visual_line in (start_visual_line + 1)..end_visual_line { - let y = line_height * visual_line as f32; - quad_corners.push(( - point(Pixels::ZERO, line_y + y + offset_y), - point(line_width, line_y + y + line_height), - )); - } - - // Last visual line - quad_corners.push(( - point(Pixels::ZERO, line_y + end_pos.y + offset_y), - point(end_pos.x, line_y + end_pos.y + line_height), - )); - - quad_corners - } -} diff --git a/crates/gpui_elements/src/editable_text/shared_element.rs b/crates/gpui_elements/src/editable_text/shared_element.rs new file mode 100644 index 0000000000..f740b46587 --- /dev/null +++ b/crates/gpui_elements/src/editable_text/shared_element.rs @@ -0,0 +1,518 @@ +use crate::editable_text::{ + TextInputStateBase, TextLayoutWrapping, 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, + WrappedLine, fill, point, px, size, +}; +use smallvec::SmallVec; +use std::{ops::Range, sync::Arc}; + +pub enum PrepaintElement { + Line { + line: Arc, + point: Point, + align: TextAlign, + }, + Quad(PaintQuad), +} +impl PrepaintElement { + fn build_quads( + offset_corners: Vec<(Point, Point)>, + origin: Point, + color: Hsla, + ) -> impl Iterator { + let iter = offset_corners.into_iter(); + iter.map(move |(offset_start, offset_end)| { + let bounds = Bounds::from_corners(origin + offset_start, origin + offset_end); + PrepaintElement::Quad(fill(bounds, color)) + }) + } +} + +#[doc(hidden)] +pub struct LayoutState { + pub state: Entity, + pub text_style: TextStyle, +} + +#[doc(hidden)] +pub struct PrepaintState { + pub hitbox: Option, + pub focus_handle: FocusHandle, + pub elements: SmallVec<[PrepaintElement; 3]>, + pub scroll_offset: Point, + pub caret_visible: bool, +} + +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, + global_id: Option<&gpui::GlobalElementId>, + inspector_id: Option<&gpui::InspectorElementId>, + window: &mut gpui::Window, + cx: &mut gpui::App, + ) -> (gpui::LayoutId, LayoutState) { + // Fetches or initializes the internal state of the field + let state = match &self.interactivity().element_id { + None => unimplemented!("all input elements must be assigned an id"), + Some(element_id) => { + let state = window + .use_keyed_state(element_id.clone(), cx, |_window, cx| self.init_state(cx)); + // store a reference to the entity owned by the element for access in action handlers + *self.state_entity_rc().borrow_mut() = state.downgrade(); + state + } + }; + + let mut resolved_text_style = None; + + self.interactivity() + .track_focus(state.read(cx).focus_handle(cx)); + let layout_id = self.interactivity().request_layout( + global_id, + inspector_id, + window, + cx, + |element_style, window, cx| { + window.with_text_style(element_style.text_style().cloned(), |window| { + resolved_text_style = Some(window.text_style()); + + let style = element_style.clone(); + // TODO: Does this need to propagate the line_height as the element's height? + window.request_layout(style, None, cx) + }) + }, + ); + + let layout_state = LayoutState { + state, + text_style: resolved_text_style.unwrap_or_else(|| window.text_style()), + }; + (layout_id, layout_state) + } + + fn shared_prepaint( + &mut self, + global_id: Option<&gpui::GlobalElementId>, + inspector_id: Option<&gpui::InspectorElementId>, + bounds: gpui::Bounds, + request_layout: &mut LayoutState, + window: &mut gpui::Window, + cx: &mut gpui::App, + ) -> PrepaintState { + struct InteractivityPrepaint { + hitbox: Option, + scroll_offset: Point, + } + // TODO: how do we enable scrolling? overflow on interactivity? + let prepaint = self.interactivity().prepaint( + global_id, + inspector_id, + bounds, + bounds.size, + window, + cx, + |_style, scroll_offset, hitbox, window, _cx| { + let hitbox = + hitbox.or_else(|| Some(window.insert_hitbox(bounds, HitboxBehavior::Normal))); + InteractivityPrepaint { + hitbox, + scroll_offset, + } + }, + ); + let InteractivityPrepaint { + hitbox, + scroll_offset, + } = prepaint; + + 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(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 = 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 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(); + // TODO: Cursor blinking + let cursor_visible = true; // input.cursor_visible(); + + let mut elements = SmallVec::new(); + + let line_height = window.line_height(); + let is_range_contained_by_range = + |text_range: &Range, containing_range: &Range| { + if text_range.is_empty() { + containing_range.start <= text_range.start + && containing_range.end > text_range.start + } else { + containing_range.end > text_range.start + && containing_range.start < text_range.end + } + }; + let mut carent_point = Point::default(); + for segment in input.line_segments() { + 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; + let line_visible = line_bottom >= Pixels::ZERO && line_y <= bounds.size.height; + if !line_visible { + continue; + } + + // TODO: First render all lines (underlines for IME), then all selections, then cursor if no selection + + if let Some(wrapped) = &segment.wrapped_line { + let point = bounds.origin + point(Pixels::ZERO, line_y); + elements.push(PrepaintElement::Line { + line: wrapped.clone(), + point, + align: TextAlign::Left, + }); + } + + let segment_is_empty = segment.text_range.is_empty(); + + if is_range_contained_by_range(&segment.text_range, &selection) { + if segment_is_empty { + const EMPTY_LINE_SELECTION_WIDTH: Pixels = px(6.); + elements.push(PrepaintElement::Quad(fill( + Bounds::from_corners( + bounds.origin + point(Pixels::ZERO, line_y), + bounds.origin + point(EMPTY_LINE_SELECTION_WIDTH, line_y + line_height), + ), + selection_color, + ))); + } else { + let offset_corners = build_quad_over_text( + &selection, + segment, + line_y, + line_height, + Pixels::ZERO, + ); + elements.extend(PrepaintElement::build_quads( + offset_corners, + bounds.origin, + selection_color, + )); + } + } + + if !segment_is_empty && let Some(ime_range) = &ime_range { + if !ime_range.is_empty() + && is_range_contained_by_range(&segment.text_range, &ime_range) + { + const MARKED_TEXT_UNDERLINE_THICKNESS: f32 = 2.0; + let underline_thickness = px(MARKED_TEXT_UNDERLINE_THICKNESS); + let underline_offset = line_height - underline_thickness; + + let offset_corners = build_quad_over_text( + &ime_range, + segment, + line_y, + line_height, + underline_offset, + ); + elements.extend(PrepaintElement::build_quads( + offset_corners, + bounds.origin, + selection_color, + )); + } + } + + let is_cursor_in_line = if segment_is_empty { + caret_pos == segment.text_range.start + } else { + segment.text_range.contains(&caret_pos) || caret_pos == segment.text_range.end + }; + if is_cursor_in_line && let Some(wrapped) = &segment.wrapped_line { + let local_offset = caret_pos.saturating_sub(segment.text_range.start); + let caret_px = wrapped + .position_for_index(local_offset, line_height) + .unwrap_or_default(); + carent_point = caret_px + point(Pixels::ZERO, line_y); + } + } + + let is_focused = focus_handle.is_focused(window); + if !showing_placeholder && is_focused && cursor_visible { + const CURSOR_WIDTH: f32 = 2.0; + let quad = fill( + Bounds::new( + bounds.origin + carent_point - scroll_offset, + size(gpui::px(CURSOR_WIDTH), line_height), + ), + caret_color, + ); + elements.push(PrepaintElement::Quad(quad)); + } + + PrepaintState { + hitbox, + focus_handle, + elements, + scroll_offset, + caret_visible: cursor_visible, + } + } + + fn shared_paint( + &mut self, + global_id: Option<&gpui::GlobalElementId>, + inspector_id: Option<&gpui::InspectorElementId>, + bounds: gpui::Bounds, + request_layout: &mut LayoutState, + prepaint: &mut PrepaintState, + window: &mut gpui::Window, + cx: &mut gpui::App, + ) where + Self::State: + for<'app> EditableTextActionHandler<'app, Context = Context<'app, Self::State>>, + { + if let Some(hitbox) = &prepaint.hitbox { + window.set_cursor_style(CursorStyle::IBeam, hitbox); + } + + let perform_paint = |style: &Style, window: &mut Window, cx: &mut App| { + if style.display == Display::None { + return; + } + + // NOTE: Skip when disabled + let ime_handler = ElementInputHandler::new(bounds, request_layout.state.clone()); + window.handle_input(&prepaint.focus_handle, ime_handler, cx); + + let get_relative_position = { + let bounds = bounds.clone(); + move |position: Point| { + // Converts a screen position to a position relative to the text area origin, + // adjusted for scroll offset. + let scroll_distance = gpui::px(0.); // TODO: STUB + (position - bounds.origin) + .apply_along(Axis::Horizontal, |pos| pos + scroll_distance) + } + }; + window.on_mouse_event({ + let state = request_layout.state.clone(); + move |event: &MouseDownEvent, phase, window, cx| { + if phase != DispatchPhase::Bubble { + return; + } + if !bounds.contains(&event.position) { + return; + } + if event.button != MouseButton::Left { + return; + } + + let text_position = get_relative_position(event.position); + state.update(cx, |state, cx| { + state.on_mouse_down(event, text_position, window, cx); + }); + } + }); + window.on_mouse_event({ + let state = request_layout.state.clone(); + move |event: &MouseUpEvent, phase, window, cx| { + if phase != DispatchPhase::Bubble { + return; + } + if event.button != MouseButton::Left { + return; + } + + state.update(cx, |state, cx| { + state.on_mouse_up(event, window, cx); + }); + } + }); + window.on_mouse_event({ + let state = request_layout.state.clone(); + move |event: &MouseMoveEvent, phase, window, cx| { + if phase != DispatchPhase::Bubble { + return; + } + + let text_position = get_relative_position(event.position); + state.update(cx, |state, cx| { + state.on_mouse_move(event, text_position, window, cx); + }); + } + }); + window.on_mouse_event({ + let state = request_layout.state.clone(); + /* + let content_size = match axis { + gpui::Axis::Horizontal => { + let state = input.read(cx); + let line = state.lines().first(); + let line = line.and_then(|l| l.wrapped_line.as_ref()); + line.map(|w| w.width()).unwrap_or(px(0.)) + } + gpui::Axis::Vertical => input.read(cx).total_content_height(), + }; + let max_scroll = (content_size - bounds.size.along(axis)).max(px(0.)); + */ + // TODO: Scroll mouse wheel + move |event: &ScrollWheelEvent, phase, _window, cx| { + if phase != DispatchPhase::Bubble { + return; + } + if !bounds.contains(&event.position) { + return; + } + + // use shift to alter horizontal scroll on text area + //event.modifiers.shift; + + /* + let pixel_delta = event.delta.pixel_delta(px(20.)); + state.update(cx, |state, cx| { + let delta = match axis { + gpui::Axis::Horizontal => pixel_delta.y, + gpui::Axis::Vertical => { + if pixel_delta.x.abs() > pixel_delta.y.abs() { + pixel_delta.x + } else { + pixel_delta.y + } + } + }; + state.apply_scroll_delta(delta, max_scroll); + cx.notify(); + }); + */ + } + }); + + window.with_content_mask(Some(ContentMask { bounds }), |window| { + let line_h = window.line_height(); + let mut lines = Vec::with_capacity(prepaint.elements.len()); + for element in prepaint.elements.drain(..) { + match element { + PrepaintElement::Line { line, point, align } => { + let _ = line.paint(point, line_h, align, Some(bounds), window, cx); + lines.push(line); + } + PrepaintElement::Quad(quad) => window.paint_quad(quad), + } + } + }); + }; + self.interactivity().paint( + global_id, + inspector_id, + bounds.clone(), + prepaint.hitbox.as_ref(), + window, + cx, + perform_paint, + ); + } +} + +fn build_quad_over_text( + containing_range: &Range, + segment: &TextLineSegment, + line_y: Pixels, + line_height: Pixels, + offset_y: Pixels, +) -> Vec<(Point, Point)> { + let Some(wrapped) = &segment.wrapped_line else { + return vec![]; + }; + + let line_start = segment.text_range.start; + let line_end = segment.text_range.end; + + let subrange_start = containing_range.start.max(line_start) - line_start; + let subrange_end = containing_range.end.min(line_end) - line_start; + + let start_pos = wrapped + .position_for_index(subrange_start, line_height) + .unwrap_or_default(); + let end_pos = wrapped + .position_for_index(subrange_end, line_height) + .unwrap_or_else(|| { + let last_line_y = line_height * (segment.num_visual_lines - 1) as f32; + point(wrapped.width(), last_line_y) + }); + + let start_visual_line = (start_pos.y / line_height).floor() as usize; + let end_visual_line = (end_pos.y / line_height).floor() as usize; + + if start_visual_line == end_visual_line { + vec![( + point(start_pos.x, line_y + start_pos.y + offset_y), + point(end_pos.x, line_y + start_pos.y + line_height), + )] + } else { + let line_width = wrapped.width(); + let middle_lines = (start_visual_line + 1)..end_visual_line; + let mut quad_corners = Vec::with_capacity(middle_lines.end - middle_lines.start + 2); + + quad_corners.push(( + point(start_pos.x, line_y + start_pos.y + offset_y), + point(line_width, line_y + start_pos.y + line_height), + )); + + // Middle visual lines + for visual_line in (start_visual_line + 1)..end_visual_line { + let y = line_height * visual_line as f32; + quad_corners.push(( + point(Pixels::ZERO, line_y + y + offset_y), + point(line_width, line_y + y + line_height), + )); + } + + // Last visual line + quad_corners.push(( + point(Pixels::ZERO, line_y + end_pos.y + offset_y), + point(end_pos.x, line_y + end_pos.y + line_height), + )); + + quad_corners + } +} diff --git a/crates/gpui_elements/src/editable_text/shared_state.rs b/crates/gpui_elements/src/editable_text/shared_state.rs index fb47a13a5b..c7d0f33449 100644 --- a/crates/gpui_elements/src/editable_text/shared_state.rs +++ b/crates/gpui_elements/src/editable_text/shared_state.rs @@ -1,10 +1,12 @@ use crate::editable_text::{ TextBoundary, UnicodeTextStorage, + actions::EditableTextActionHandler, notify::{TextChanged, TextHistoryPushed}, }; use gpui::{ - App, Bounds, ClipboardItem, FocusHandle, Focusable, Hsla, NavigationDirection, Pixels, Point, - SharedString, TextRun, TextStyle, UTF16Selection, Window, WrappedLine, point, + App, Bounds, ClipboardItem, EntityInputHandler, FocusHandle, Focusable, Hsla, + NavigationDirection, Pixels, Point, SharedString, TextRun, TextStyle, UTF16Selection, Window, + WrappedLine, point, }; use std::{ops::Range, sync::Arc}; @@ -14,6 +16,14 @@ pub trait TextStateNotifier { fn emit_history(&mut self, event: TextHistoryPushed); } +pub trait StateBackedEditableText { + type State: 'static + + std::ops::Deref + + std::ops::DerefMut + + EntityInputHandler + + for<'app> EditableTextActionHandler<'app>; +} + pub struct TextInputStateBase { storage: Box, 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 edee4a4056..3c0293ab4c 100644 --- a/crates/gpui_elements/src/editable_text/text_area_element.rs +++ b/crates/gpui_elements/src/editable_text/text_area_element.rs @@ -1,10 +1,11 @@ use crate::editable_text::{ - InitStorage, TextAreaState, + InitStorage, StateBackedEditableText, TextAreaState, actions::{DEFAULT_INPUT_CONTEXT, EditableInputActionElement}, + shared_element::{self, EditableTextElement}, }; use gpui::{ - Element, ElementId, Entity, Hitbox, InteractiveElement, Interactivity, IntoElement, - SharedString, StyleRefinement, Styled, TextStyle, WeakEntity, + App, Bounds, Element, ElementId, Entity, Hitbox, InteractiveElement, Interactivity, + IntoElement, Pixels, SharedString, StyleRefinement, Styled, TextStyle, WeakEntity, Window, }; use std::{cell::RefCell, rc::Rc}; @@ -34,6 +35,13 @@ pub struct TextAreaElement { placeholder: Option, } +impl TextAreaElement { + pub fn placeholder(mut self, text: impl Into) -> Self { + self.placeholder = Some(text.into()); + self + } +} + impl InteractiveElement for TextAreaElement { fn interactivity(&mut self) -> &mut Interactivity { &mut self.interactivity @@ -53,31 +61,33 @@ impl IntoElement for TextAreaElement { } } -impl EditableInputActionElement for TextAreaElement { +impl StateBackedEditableText for TextAreaElement { type State = TextAreaState; +} + +impl EditableInputActionElement for TextAreaElement { fn state_entity_rc(&self) -> &Rc>> { &self.state_entity } } -pub mod element { - use super::*; - - #[doc(hidden)] - pub struct LayoutState { - pub state: Entity, - pub text_style: TextStyle, +impl EditableTextElement for TextAreaElement { + fn init_state(&self, cx: &mut gpui::prelude::Context) -> Self::State { + Self::State::new(self.init_storage.exec(cx), cx) } - #[doc(hidden)] - pub struct PrepaintState { - pub hitbox: Option, + fn placeholder(&self) -> &Option { + &self.placeholder + } + + fn should_wrap(&self) -> bool { + true } } impl Element for TextAreaElement { - type RequestLayoutState = element::LayoutState; - type PrepaintState = element::PrepaintState; + type RequestLayoutState = shared_element::LayoutState; + type PrepaintState = shared_element::PrepaintState; fn id(&self) -> Option { self.interactivity.element_id.clone() @@ -91,74 +101,42 @@ impl Element for TextAreaElement { &mut self, global_id: Option<&gpui::GlobalElementId>, inspector_id: Option<&gpui::InspectorElementId>, - window: &mut gpui::Window, - cx: &mut gpui::App, + window: &mut Window, + cx: &mut App, ) -> (gpui::LayoutId, Self::RequestLayoutState) { - // Fetches or initializes the internal state of the field - let state = match &self.interactivity.element_id { - None => unimplemented!("all input elements must be assigned an id"), - Some(element_id) => { - let state = window.use_keyed_state(element_id.clone(), cx, |_window, cx| { - TextAreaState::new(self.init_storage.exec(cx), cx) - }); - // store a reference to the entity owned by the element for access in action handlers - *self.state_entity.borrow_mut() = state.downgrade(); - state - } - }; - - let mut resolved_text_style = None; - - let layout_id = self.interactivity.request_layout( - global_id, - inspector_id, - window, - cx, - |element_style, window, cx| { - window.with_text_style(element_style.text_style().cloned(), |window| { - resolved_text_style = Some(window.text_style()); - - let mut style = element_style.clone(); - if let gpui::Length::Auto = style.size.width { - style.size.width = gpui::relative(1.).into(); - } - if let gpui::Length::Auto = style.size.height { - style.size.height = gpui::relative(1.).into(); - } - window.request_layout(style, None, cx) - }) - }, - ); - - let layout_state = element::LayoutState { - state, - text_style: resolved_text_style.unwrap_or_else(|| window.text_style()), - }; - (layout_id, layout_state) + self.shared_request_layout(global_id, inspector_id, window, cx) } fn prepaint( &mut self, - id: Option<&gpui::GlobalElementId>, + global_id: Option<&gpui::GlobalElementId>, inspector_id: Option<&gpui::InspectorElementId>, - bounds: gpui::Bounds, + bounds: Bounds, request_layout: &mut Self::RequestLayoutState, - window: &mut gpui::Window, - cx: &mut gpui::App, + window: &mut Window, + cx: &mut App, ) -> Self::PrepaintState { - todo!() + self.shared_prepaint(global_id, inspector_id, bounds, request_layout, window, cx) } fn paint( &mut self, - id: Option<&gpui::GlobalElementId>, + global_id: Option<&gpui::GlobalElementId>, inspector_id: Option<&gpui::InspectorElementId>, - bounds: gpui::Bounds, + bounds: Bounds, request_layout: &mut Self::RequestLayoutState, prepaint: &mut Self::PrepaintState, - window: &mut gpui::Window, - cx: &mut gpui::App, + window: &mut Window, + cx: &mut App, ) { - todo!() + self.shared_paint( + global_id, + inspector_id, + bounds, + request_layout, + prepaint, + window, + cx, + ); } }