diff --git a/Cargo.lock b/Cargo.lock index d78049525a..e01de885c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2427,6 +2427,7 @@ version = "0.1.0" dependencies = [ "async-io", "gpui", + "smallvec", "unicode-segmentation", ] diff --git a/crates/gpui_elements/Cargo.toml b/crates/gpui_elements/Cargo.toml index b0c8b016a1..7220c878bb 100644 --- a/crates/gpui_elements/Cargo.toml +++ b/crates/gpui_elements/Cargo.toml @@ -15,6 +15,7 @@ ignored = ["gpui"] gpui.workspace = true async-io.workspace = true unicode-segmentation.workspace = true +smallvec.workspace = true [dev-dependencies] gpui = { path = "../gpui", features = ["test-support"] } diff --git a/crates/gpui_elements/src/input/cursor.rs b/crates/gpui_elements/src/input/cursor.rs index cdf58be6ea..83620e5f5c 100644 --- a/crates/gpui_elements/src/input/cursor.rs +++ b/crates/gpui_elements/src/input/cursor.rs @@ -4,6 +4,8 @@ use std::time::Duration; /// Default interval for cursor blinking. pub const DEFAULT_BLINK_INTERVAL: Duration = Duration::from_millis(500); +pub trait Cursor {} + /// Configuration for cursor blinking, to be provided to InputState. pub enum CursorBlinkType<'app> { /// The cursor will not blink. diff --git a/crates/gpui_elements/src/input/element.rs b/crates/gpui_elements/src/input/element.rs index 31f7eabfaf..0c3e314567 100644 --- a/crates/gpui_elements/src/input/element.rs +++ b/crates/gpui_elements/src/input/element.rs @@ -1,7 +1,7 @@ -use crate::input::{InputColors, InputState}; +use crate::input::{Cursor, InputColors, InputState}; use gpui::{ - Action, App, Context, Entity, FocusHandle, Focusable, Hsla, InteractiveElement, Interactivity, - IntoElement, SharedString, StyleRefinement, Styled, Window, + Action, AnyElement, App, Context, Entity, FocusHandle, Focusable, Hsla, InteractiveElement, + Interactivity, IntoElement, SharedString, StyleRefinement, Styled, Window, }; #[track_caller] @@ -15,6 +15,7 @@ pub struct Input { pub(super) interactivity: Interactivity, pub(super) placeholder: Option, pub(super) colors: InputColors, + pub(super) cursor: Option, } impl Input { @@ -26,6 +27,7 @@ impl Input { interactivity: Interactivity::new(), placeholder: None, colors: InputColors::default(), + cursor: None, }; input.register_actions(); input @@ -163,6 +165,15 @@ impl Input { self.colors.marked = color; self } + + pub fn cursor(mut self, entity: Entity) -> Self + where + T: Cursor, + Entity: Into, + { + self.cursor = Some(entity.into()); + self + } } fn register_action( diff --git a/crates/gpui_elements/src/input/paint.rs b/crates/gpui_elements/src/input/paint.rs index f93f1cee5d..1e86382405 100644 --- a/crates/gpui_elements/src/input/paint.rs +++ b/crates/gpui_elements/src/input/paint.rs @@ -1,11 +1,12 @@ use crate::input::{Input, InputColors, InputLayoutData, InputLogicalLine, InputState}; use gpui::{ - Along, App, Axis, Bounds, ContentMask, CursorStyle, DispatchPhase, Element, ElementId, + Along, App, Axis, Bounds, ContentMask, CursorStyle, DispatchPhase, Display, Element, ElementId, ElementInputHandler, Entity, Focusable, GlobalElementId, Hitbox, HitboxBehavior, Hsla, InspectorElementId, LayoutId, Length, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, ScrollWheelEvent, SharedString, Style, TextAlign, TextRun, TextStyle, Window, fill, point, px, relative, size, }; +use smallvec::SmallVec; use std::ops::Range; const CURSOR_WIDTH: f32 = 2.0; @@ -13,6 +14,8 @@ const MARKED_TEXT_UNDERLINE_THICKNESS: f32 = 2.0; pub struct InputLayoutState { text_style: TextStyle, + #[allow(dead_code)] + child_layout_ids: SmallVec<[LayoutId; 2]>, } pub struct InputPrepaintState { @@ -39,6 +42,7 @@ impl Element for Input { cx: &mut App, ) -> (LayoutId, Self::RequestLayoutState) { let mut resolved_text_style = None; + let mut child_layout_ids = SmallVec::new(); let layout_id = self.interactivity.request_layout( global_id, @@ -46,12 +50,13 @@ impl Element for Input { window, cx, |element_style, window, cx| { - let layout = self.input.read(cx).layout_style(); window.with_text_style(element_style.text_style().cloned(), |window| { + let state = self.input.read(cx); + resolved_text_style = Some(window.text_style()); let mut layout_style = element_style.clone(); - if matches!(layout, super::InputLayoutStyle::MultiLine) { + if matches!(state.layout_style(), super::InputLayoutStyle::MultiLine) { if let Length::Auto = layout_style.size.width { layout_style.size.width = relative(1.).into(); } @@ -59,13 +64,21 @@ impl Element for Input { layout_style.size.height = relative(1.).into(); } } - window.request_layout(layout_style, None, cx) + + child_layout_ids = self + .cursor + .iter_mut() + .map(|cursor| cursor.request_layout(window, cx)) + .collect::>(); + + window.request_layout(layout_style, child_layout_ids.iter().copied(), cx) }) }, ); let layout_state = InputLayoutState { text_style: resolved_text_style.unwrap_or_else(|| window.text_style()), + child_layout_ids, }; (layout_id, layout_state) } @@ -106,8 +119,19 @@ impl Element for Input { bounds.size, window, cx, - |_style, _point, hitbox, window, _cx| { - hitbox.or_else(|| Some(window.insert_hitbox(bounds, HitboxBehavior::Normal))) + |style, scroll_offset, hitbox, window, cx| { + let hitbox = + hitbox.or_else(|| Some(window.insert_hitbox(bounds, HitboxBehavior::Normal))); + + if style.display != Display::None { + window.with_element_offset(scroll_offset, |window| { + if let Some(cursor) = &mut self.cursor { + cursor.prepaint(window, cx); + } + }); + } + + hitbox }, ); @@ -146,7 +170,11 @@ impl Element for Input { input.toggle_cursor_on_focus_change(is_focused, cx) }); - let perform_paint = |_style: &Style, window: &mut Window, cx: &mut App| { + let perform_paint = |style: &Style, window: &mut Window, cx: &mut App| { + if style.display == Display::None { + return; + } + let context = PaintContext { snapshot, is_focused, @@ -159,6 +187,10 @@ impl Element for Input { context.process_mouse_events(&self.input, window, cx); window.with_content_mask(Some(ContentMask { bounds }), |window| { context.paint(window, cx); + + if let Some(cursor) = &mut self.cursor { + cursor.paint(window, cx); + } }); }; self.interactivity.paint( diff --git a/crates/gpui_elements/src/input/state.rs b/crates/gpui_elements/src/input/state.rs index 8c53f6b386..cf0560fa58 100644 --- a/crates/gpui_elements/src/input/state.rs +++ b/crates/gpui_elements/src/input/state.rs @@ -2,8 +2,8 @@ use super::actions::*; use crate::input::{CursorBlinkType, InputLayoutStyle, InputStorage}; use gpui::{ App, AppContext, ClipboardItem, Context, Entity, EntityId, EntityInputHandler, EventEmitter, - FocusHandle, Focusable, NavigationDirection, Pixels, Point, SharedString, Size, Subscription, - TextRun, TextStyle, Window, WrappedLine, point, px, + FocusHandle, Focusable, NavigationDirection, Pixels, Point, Render, SharedString, Size, + Subscription, TextRun, TextStyle, Window, WrappedLine, point, px, }; use std::{ ops::Range, @@ -74,9 +74,6 @@ pub struct InputState { history_undo_stack: Vec, /// Stack of undone states for redo. history_redo_stack: Vec, - - /// Optional entity and subscription tracking the blinking of the text cursor. - cursor_blink: Option<(Entity, Subscription)>, } /// Data built during element prepaint that is stored in InputState for conveinence @@ -126,7 +123,7 @@ impl InputState { /// Creates a new `Input` with the specified multiline setting. /// Cursor blinking is enabled by default. pub fn new(cx: &mut Context) -> Self { - let mut this = Self { + Self { entity_id: cx.entity_id(), focus_handle: cx.focus_handle(), content: Box::new(super::Standard::default()), @@ -148,30 +145,7 @@ impl InputState { history_grouping_interval: super::DEFAULT_GROUP_INTERVAL, history_undo_stack: Vec::new(), history_redo_stack: Vec::new(), - - cursor_blink: None, - }; - // TODO: This is unoptimal for non-blinking cases, since the entity is generated and then discarded. - this = this.cursor_blink(CursorBlinkType::Enabled { - app: cx, - interval: None, - }); - this - } - - /// Configure how often the cursor should blink when the input element has focus. - pub fn cursor_blink<'app>(mut self, args: CursorBlinkType<'app>) -> Self { - self.cursor_blink = match args { - CursorBlinkType::Disabled => None, - CursorBlinkType::Enabled { app: cx, interval } => { - let interval = interval.unwrap_or(super::DEFAULT_BLINK_INTERVAL); - let cursor_blink = cx.new(|_cx| super::CursorBlink::new(interval)); - let entity_id = self.entity_id; - let subscription = cx.observe(&cursor_blink, move |_, cx| cx.notify(entity_id)); - Some((cursor_blink, subscription)) - } - }; - self + } } /// Returns the current text content.