diff --git a/crates/gpui_elements/src/input/colors.rs b/crates/gpui_elements/src/input/colors.rs index f60f765280..8e9e638d0b 100644 --- a/crates/gpui_elements/src/input/colors.rs +++ b/crates/gpui_elements/src/input/colors.rs @@ -5,8 +5,6 @@ use gpui::Hsla; pub struct InputColors { /// This is the background color applied to the range of text that is currently selected by the user. pub selection: Hsla, - /// This is the color of the user's text cursor. - pub cursor: Hsla, /// This is the color of the placeholder string, when one is assigned and the text field is empty. pub placeholder: Hsla, pub marked: Hsla, @@ -16,7 +14,6 @@ impl Default for InputColors { fn default() -> Self { Self { selection: gpui::hsla(0.583, 0.519, 0.31, 1.0), - cursor: Hsla::white().opacity(0.8), marked: Hsla::white().opacity(0.6), placeholder: gpui::hsla(0., 0., 0.5, 1.0), } diff --git a/crates/gpui_elements/src/input/cursor.rs b/crates/gpui_elements/src/input/cursor.rs index 83620e5f5c..258776639b 100644 --- a/crates/gpui_elements/src/input/cursor.rs +++ b/crates/gpui_elements/src/input/cursor.rs @@ -1,47 +1,46 @@ -use gpui::Context; +use gpui::{Bounds, Context, Element, Hsla, IntoElement, Pixels, Point, Render}; 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. - Disabled, - /// The cursor will blink at some interval. - Enabled { - /// Provide the app so that the internal state to track cursor blinking can be created. - app: &'app mut gpui::App, - /// The interval to blink at. If none, the default value of 500ms is used (defined by `DEFAULT_BLINK_INTERVAL`). - interval: Option, - }, -} - /// The state of an input's cursor blinking. While active, the cursor's visibility changes at some interval. /// This blinking can be temporarily paused (e.g. during typing). -pub(super) struct CursorBlink { +pub struct Cursor { interval: Duration, generation: usize, visible: bool, active: bool, paused: bool, + color: Hsla, + /// Tracks whether we were focused on the last update. + was_focused: bool, + point: Point, + height: Pixels, } -impl CursorBlink { +impl Cursor { /// Initializes the cursor blinking with the cursor already being visible. #[track_caller] - pub fn new(interval: Duration) -> Self { + pub fn new(interval: Option) -> Self { Self { - interval, + interval: interval.unwrap_or_default(), generation: 0, visible: true, active: false, paused: false, + color: Hsla::white(), + was_focused: false, + point: Point::default(), + height: Pixels::ZERO, } } + pub fn color(mut self, color: Hsla) -> Self { + self.color = color; + self + } + /// Returns whether the cursor should currently be rendered. pub fn visible(&self) -> bool { self.visible @@ -121,4 +120,94 @@ impl CursorBlink { }) .detach(); } + + pub fn update_input( + &mut self, + is_focused: bool, + pos: Point, + line_height: Pixels, + cx: &mut Context, + ) -> bool { + let was_focused = self.was_focused; + self.was_focused = is_focused; + + self.point = pos; + self.height = line_height; + + match (self.interval.is_zero(), is_focused, was_focused) { + (true, _, _) => true, + (false, true, false) => { + self.enable(cx); + true + } + (false, false, true) => { + self.disable(cx); + false + } + (false, _, _) => self.visible, + } + } +} + +impl IntoElement for Cursor { + type Element = Self; + fn into_element(self) -> Self::Element { + self + } +} + +impl Element for Cursor { + type RequestLayoutState = (); + type PrepaintState = (); + + fn id(&self) -> Option { + None + } + + fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { + None + } + + fn request_layout( + &mut self, + _id: Option<&gpui::GlobalElementId>, + _inspector_id: Option<&gpui::InspectorElementId>, + window: &mut gpui::Window, + cx: &mut gpui::App, + ) -> (gpui::LayoutId, Self::RequestLayoutState) { + let layout_id = window.request_layout(gpui::Style::default(), None, cx); + (layout_id, ()) + } + + fn prepaint( + &mut self, + _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 { + () + } + + fn paint( + &mut self, + _id: Option<&gpui::GlobalElementId>, + _inspector_id: Option<&gpui::InspectorElementId>, + bounds: gpui::Bounds, + _request_layout: &mut Self::RequestLayoutState, + _prepaint: &mut Self::PrepaintState, + window: &mut gpui::Window, + _cx: &mut gpui::App, + ) { + const CURSOR_WIDTH: f32 = 2.0; + window.paint_quad(gpui::fill( + Bounds::new( + gpui::point(bounds.left(), bounds.top()) + self.point, + gpui::size(gpui::px(CURSOR_WIDTH), self.height), + ), + self.color, + )); + } } diff --git a/crates/gpui_elements/src/input/element.rs b/crates/gpui_elements/src/input/element.rs index 0c3e314567..2c64e59784 100644 --- a/crates/gpui_elements/src/input/element.rs +++ b/crates/gpui_elements/src/input/element.rs @@ -15,7 +15,7 @@ pub struct Input { pub(super) interactivity: Interactivity, pub(super) placeholder: Option, pub(super) colors: InputColors, - pub(super) cursor: Option, + pub(super) cursor: Option>, } impl Input { @@ -145,13 +145,6 @@ impl Input { self } - /// Sets the "cursor" color for the input element. - /// This is the color of the user's text cursor. - pub fn cursor_color(mut self, color: Hsla) -> Self { - self.colors.cursor = color; - self - } - /// Sets the "placeholder" color for the input element. /// This is the color of the placeholder string, when one is assigned and the text field is empty. pub fn placeholder_color(mut self, color: Hsla) -> Self { @@ -166,12 +159,8 @@ impl Input { self } - pub fn cursor(mut self, entity: Entity) -> Self - where - T: Cursor, - Entity: Into, - { - self.cursor = Some(entity.into()); + pub fn cursor(mut self, entity: Entity) -> Self { + self.cursor = Some(entity); self } } diff --git a/crates/gpui_elements/src/input/paint.rs b/crates/gpui_elements/src/input/paint.rs index 1e86382405..7c28a1b43a 100644 --- a/crates/gpui_elements/src/input/paint.rs +++ b/crates/gpui_elements/src/input/paint.rs @@ -1,4 +1,4 @@ -use crate::input::{Input, InputColors, InputLayoutData, InputLogicalLine, InputState}; +use crate::input::{Cursor, Input, InputColors, InputLayoutData, InputLogicalLine, InputState}; use gpui::{ Along, App, Axis, Bounds, ContentMask, CursorStyle, DispatchPhase, Display, Element, ElementId, ElementInputHandler, Entity, Focusable, GlobalElementId, Hitbox, HitboxBehavior, Hsla, @@ -9,17 +9,18 @@ use gpui::{ use smallvec::SmallVec; use std::ops::Range; -const CURSOR_WIDTH: f32 = 2.0; const MARKED_TEXT_UNDERLINE_THICKNESS: f32 = 2.0; pub struct InputLayoutState { text_style: TextStyle, #[allow(dead_code)] child_layout_ids: SmallVec<[LayoutId; 2]>, + cursor_layout: Option<::RequestLayoutState>, } pub struct InputPrepaintState { hitbox: Option, + cursor_prepaint: Option<::PrepaintState>, } impl Element for Input { @@ -43,6 +44,7 @@ impl Element for Input { ) -> (LayoutId, Self::RequestLayoutState) { let mut resolved_text_style = None; let mut child_layout_ids = SmallVec::new(); + let mut cursor_layout = None; let layout_id = self.interactivity.request_layout( global_id, @@ -65,11 +67,13 @@ impl Element for Input { } } - child_layout_ids = self - .cursor - .iter_mut() - .map(|cursor| cursor.request_layout(window, cx)) - .collect::>(); + if let Some(cursor) = &self.cursor { + let (layout_id, layout) = cursor.update(cx, |cursor, cx| { + cursor.request_layout(global_id, inspector_id, window, cx) + }); + child_layout_ids.push(layout_id); + cursor_layout = Some(layout); + } window.request_layout(layout_style, child_layout_ids.iter().copied(), cx) }) @@ -79,6 +83,7 @@ impl Element for Input { let layout_state = InputLayoutState { text_style: resolved_text_style.unwrap_or_else(|| window.text_style()), child_layout_ids, + cursor_layout, }; (layout_id, layout_state) } @@ -112,6 +117,7 @@ impl Element for Input { input.apply_layout_update(layout_data, window); }); + let mut cursor_prepaint = None; let hitbox = self.interactivity.prepaint( global_id, inspector_id, @@ -125,8 +131,21 @@ impl Element for Input { if style.display != Display::None { window.with_element_offset(scroll_offset, |window| { - if let Some(cursor) = &mut self.cursor { - cursor.prepaint(window, cx); + match (&mut self.cursor, &mut layout_state.cursor_layout) { + (Some(cursor), Some(layout)) => { + let prepaint = cursor.update(cx, |cursor, cx| { + cursor.prepaint( + global_id, + inspector_id, + bounds, + layout, + window, + cx, + ) + }); + cursor_prepaint = Some(prepaint); + } + _ => {} } }); } @@ -135,7 +154,10 @@ impl Element for Input { }, ); - InputPrepaintState { hitbox } + InputPrepaintState { + hitbox, + cursor_prepaint, + } } fn paint( @@ -166,10 +188,6 @@ impl Element for Input { let is_focused = focus_handle.is_focused(window); let colors = self.colors; - let is_cursor_visible = self.input.update(cx, |input, cx| { - input.toggle_cursor_on_focus_change(is_focused, cx) - }); - let perform_paint = |style: &Style, window: &mut Window, cx: &mut App| { if style.display == Display::None { return; @@ -182,14 +200,39 @@ impl Element for Input { text_style: &text_style, placeholder: placeholder.as_ref(), colors: &colors, - cursor_visible: is_cursor_visible, }; 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); + match ( + &mut self.cursor, + &mut layout_state.cursor_layout, + &mut prepaint_state.cursor_prepaint, + ) { + (Some(cursor), Some(layout), Some(prepaint)) => { + cursor.update(cx, |cursor, cx| { + let cursor_pos = context.find_cursor_position_in_layouts(); + let visible = cursor.update_input( + is_focused, + cursor_pos, + context.snapshot.line_height, + cx, + ); + if is_focused && visible && context.snapshot.selected_range.is_empty() { + cursor.paint( + global_id, + inspector_id, + bounds, + layout, + prepaint, + window, + cx, + ); + } + }); + } + _ => {} } }); }; @@ -253,7 +296,6 @@ struct PaintContext<'app> { text_style: &'app TextStyle, placeholder: Option<&'app SharedString>, colors: &'app InputColors, - cursor_visible: bool, } impl<'app> PaintContext<'app> { @@ -388,10 +430,6 @@ impl<'app> PaintContext<'app> { } self.paint_marked_underline(window); - - if self.is_focused && self.snapshot.selected_range.is_empty() && self.cursor_visible { - self.paint_cursor(window); - } } fn paint_selection(&self, window: &mut Window) { @@ -557,17 +595,6 @@ impl<'app> PaintContext<'app> { Point::default() } - fn paint_cursor(&self, window: &mut Window) { - let cursor_pos = self.find_cursor_position_in_layouts(); - window.paint_quad(fill( - Bounds::new( - point(self.bounds.left(), self.bounds.top()) + cursor_pos, - size(px(CURSOR_WIDTH), self.snapshot.line_height), - ), - self.colors.cursor, - )); - } - fn is_line_visible(&self, line: &InputLogicalLine) -> bool { let line_y = line.y_offset - self.snapshot.scroll_distance; let line_bottom = line_y + self.snapshot.line_height * line.visual_line_count as f32; diff --git a/crates/gpui_elements/src/input/state.rs b/crates/gpui_elements/src/input/state.rs index cf0560fa58..f20262534c 100644 --- a/crates/gpui_elements/src/input/state.rs +++ b/crates/gpui_elements/src/input/state.rs @@ -1,5 +1,5 @@ use super::actions::*; -use crate::input::{CursorBlinkType, InputLayoutStyle, InputStorage}; +use crate::input::{InputLayoutStyle, InputStorage}; use gpui::{ App, AppContext, ClipboardItem, Context, Entity, EntityId, EntityInputHandler, EventEmitter, FocusHandle, Focusable, NavigationDirection, Pixels, Point, Render, SharedString, Size, @@ -26,9 +26,15 @@ pub enum InputStateEvent { /// Emitted when a redo operation is performed. Redo, } - impl EventEmitter for InputState {} +#[derive(Clone, Debug)] +pub enum CursorTrigger { + // TODO: cursor needs to receive this + PauseBlinkingForUserAction, +} +impl EventEmitter for InputState {} + /// `Input` is the state model for text input components. It handles: /// - Text content storage and manipulation /// - Selection and cursor management @@ -56,8 +62,6 @@ pub struct InputState { layout_data: InputLayoutData, /// A reinterpretation of `content` as wrapped lines with layout information. Regenerated when content changes or the layout changes during element painting. logical_lines: Vec, - /// Tracks whether we were focused on the last update. - was_focused: bool, /// True while the user is in the act of highlighting a section of the text (e.g. during mouse pressed & dragging). is_selecting: bool, @@ -135,7 +139,6 @@ impl InputState { layout_data: InputLayoutData::default(), logical_lines: Vec::new(), - was_focused: false, is_selecting: false, last_click_position: None, @@ -164,7 +167,7 @@ impl InputState { self.history_undo_stack.clear(); self.history_redo_stack.clear(); self.mark_layout_dirty(); - self.pause_cursor_blink(cx); + cx.emit(CursorTrigger::PauseBlinkingForUserAction); cx.emit(InputStateEvent::TextChanged); cx.notify(); } @@ -305,7 +308,7 @@ impl InputState { self.marked_range.take(); self.mark_layout_dirty(); - self.pause_cursor_blink(cx); + cx.emit(CursorTrigger::PauseBlinkingForUserAction); cx.emit(InputStateEvent::TextChanged); cx.notify(); } @@ -429,7 +432,7 @@ impl InputState { } pub(super) fn up(&mut self, _: &Up, _window: &mut Window, cx: &mut Context) { - self.pause_cursor_blink(cx); + cx.emit(CursorTrigger::PauseBlinkingForUserAction); match self.layout_style { InputLayoutStyle::SingleLine => { // In single-line mode, up moves to start @@ -450,7 +453,7 @@ impl InputState { } pub(super) fn down(&mut self, _: &Down, _window: &mut Window, cx: &mut Context) { - self.pause_cursor_blink(cx); + cx.emit(CursorTrigger::PauseBlinkingForUserAction); match self.layout_style { InputLayoutStyle::SingleLine => { // In single-line mode, down moves to end @@ -480,7 +483,7 @@ impl InputState { } pub(super) fn select_up(&mut self, _: &SelectUp, _window: &mut Window, cx: &mut Context) { - self.pause_cursor_blink(cx); + cx.emit(CursorTrigger::PauseBlinkingForUserAction); match self.layout_style { InputLayoutStyle::SingleLine => { // In single-line mode, select_up selects to start @@ -503,7 +506,7 @@ impl InputState { _window: &mut Window, cx: &mut Context, ) { - self.pause_cursor_blink(cx); + cx.emit(CursorTrigger::PauseBlinkingForUserAction); match self.layout_style { InputLayoutStyle::SingleLine => { // In single-line mode, select_down selects to end @@ -808,13 +811,6 @@ impl InputState { self.content.update_utf8(range, text_to_insert); } - /// Temporarily pauses blinking and leaves the cursor visible. Blinking will resume after the pre-established interval elapses from the time this is called. - pub(super) fn pause_cursor_blink(&self, cx: &mut Context) { - if let Some((cursor_blink, _)) = &self.cursor_blink { - cursor_blink.update(cx, |cb, cx| cb.pause_blinking(cx)); - } - } - /// Records a patch for undo. Called before making changes to content. /// Returns true if a new entry was created, false if grouped with previous. pub(super) fn push_undo_patch(&mut self, range: Range, new_text_len: usize) { @@ -1110,37 +1106,8 @@ impl InputState { logical_lines } - /// Processes a focus-flag update during window paint, returning whether the cursor should be visible in this frame. - /// Returns false if the cursor is blinking and not currently visible. - pub(super) fn toggle_cursor_on_focus_change( - &mut self, - is_focused: bool, - cx: &mut Context, - ) -> bool { - // Update cursor blink based on focus changes - let was_focused = self.was_focused; - self.was_focused = is_focused; - - match &self.cursor_blink { - None => true, - Some((cursor_blink, _)) => match (is_focused, was_focused) { - (true, false) => { - cursor_blink.update(cx, |cursor, cx| cursor.enable(cx)); - cx.emit(InputStateEvent::Focus); - true - } - (false, true) => { - cursor_blink.update(cx, |cursor, cx| cursor.disable(cx)); - cx.emit(InputStateEvent::Blur); - false - } - _ => cursor_blink.read(cx).visible(), - }, - } - } - fn move_to(&mut self, offset: usize, cx: &mut Context) { - self.pause_cursor_blink(cx); + cx.emit(CursorTrigger::PauseBlinkingForUserAction); let offset = offset.min(self.content.len()); self.selected_range = offset..offset; self.selection_direction = NavigationDirection::Forward; @@ -1149,7 +1116,7 @@ impl InputState { } fn select_to(&mut self, offset: usize, cx: &mut Context) { - self.pause_cursor_blink(cx); + cx.emit(CursorTrigger::PauseBlinkingForUserAction); let offset = offset.min(self.content.len()); self.apply_selection_offset(offset); self.scroll_to_cursor(); diff --git a/crates/gpui_elements/src/input/state_input_handler.rs b/crates/gpui_elements/src/input/state_input_handler.rs index cf866bd8d5..a2ca7e26ab 100644 --- a/crates/gpui_elements/src/input/state_input_handler.rs +++ b/crates/gpui_elements/src/input/state_input_handler.rs @@ -1,4 +1,4 @@ -use crate::input::InputStateEvent; +use crate::input::{CursorTrigger, InputStateEvent}; use gpui::{ Bounds, Context, EntityInputHandler, NavigationDirection, Pixels, Point, UTF16Selection, Window, point, px, @@ -73,7 +73,7 @@ impl EntityInputHandler for super::InputState { self.set_marked_range(None); self.mark_layout_dirty(); - self.pause_cursor_blink(cx); + cx.emit(CursorTrigger::PauseBlinkingForUserAction); cx.emit(InputStateEvent::TextChanged); cx.notify(); }