From b3ab49124044f4090467d12c451cebb1adf69900 Mon Sep 17 00:00:00 2001 From: temportalflux Date: Tue, 16 Jun 2026 10:15:13 -0400 Subject: [PATCH] flip cursor implementation to have an element and a state-entity --- crates/gpui_elements/src/input.rs | 9 +- crates/gpui_elements/src/input/cursor.rs | 107 ++++++++++++++++------ crates/gpui_elements/src/input/element.rs | 6 +- crates/gpui_elements/src/input/paint.rs | 59 ++++++------ crates/gpui_elements/src/input/state.rs | 3 +- 5 files changed, 111 insertions(+), 73 deletions(-) diff --git a/crates/gpui_elements/src/input.rs b/crates/gpui_elements/src/input.rs index 2c83d493c7..1a2097e106 100644 --- a/crates/gpui_elements/src/input.rs +++ b/crates/gpui_elements/src/input.rs @@ -11,7 +11,7 @@ mod storage; pub(self) mod unicode; pub use colors::*; -pub(self) use cursor::*; +pub use cursor::*; pub use element::*; pub(self) use history::*; pub use layout::*; @@ -22,10 +22,5 @@ pub use storage::*; fn make_element(app: &mut gpui::App) -> impl gpui::IntoElement { use gpui::AppContext; let state = app.new(|cx| InputState::new(cx)); - let cursor = app.new(|cx| { - let mut cursor = Cursor::new(None); - cursor.subscribe_to(&state, cx); - cursor - }); - input(&state, app).cursor(cursor) + input(&state, app).text_cursor(default_cursor(&state, app)) } diff --git a/crates/gpui_elements/src/input/cursor.rs b/crates/gpui_elements/src/input/cursor.rs index 0f094c9223..a522fb33ea 100644 --- a/crates/gpui_elements/src/input/cursor.rs +++ b/crates/gpui_elements/src/input/cursor.rs @@ -1,5 +1,5 @@ use gpui::{ - Bounds, Context, Element, Entity, EventEmitter, Hsla, IntoElement, Pixels, Point, Render, + App, Bounds, Context, Element, Entity, EventEmitter, Hsla, IntoElement, Pixels, Point, Render, Subscription, }; use smallvec::SmallVec; @@ -13,35 +13,63 @@ pub const DEFAULT_BLINK_INTERVAL: Duration = Duration::from_millis(500); /// 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 struct Cursor { - interval: Duration, - generation: usize, - visible: bool, - active: bool, - paused: bool, + state: Entity, color: Hsla, /// Tracks whether we were focused on the last update. was_focused: bool, point: Point, height: Pixels, +} + +pub struct CursorState { + interval: Duration, + generation: usize, + visible: bool, + active: bool, + paused: bool, #[allow(dead_code)] subscriptions: SmallVec<[Subscription; 2]>, } +impl Default for CursorState { + fn default() -> Self { + Self { + interval: Duration::ZERO, + generation: Default::default(), + visible: true, + active: Default::default(), + paused: Default::default(), + subscriptions: SmallVec::new(), + } + } +} + +#[track_caller] +pub fn cursor(state: Entity) -> Cursor { + Cursor::new(state) +} + +#[track_caller] +pub fn default_cursor(emitter: &Entity, cx: &mut App) -> Cursor +where + E: EventEmitter, +{ + use gpui::AppContext; + cursor(cx.new(|cx| { + let mut cursor = CursorState::default().blink_interval_default(); + cursor.subscribe_to(emitter, cx); + cursor + })) +} impl Cursor { - /// Initializes the cursor blinking with the cursor already being visible. #[track_caller] - pub fn new(interval: Option) -> Self { + fn new(state: Entity) -> Self { Self { - interval: interval.unwrap_or_default(), - generation: 0, - visible: true, - active: false, - paused: false, + state, color: Hsla::white(), was_focused: false, point: Point::default(), height: Pixels::ZERO, - subscriptions: SmallVec::new(), } } @@ -49,29 +77,38 @@ impl Cursor { self.color = color; self } +} + +impl CursorState { + pub fn blink_interval_default(mut self) -> Self { + self.interval = DEFAULT_BLINK_INTERVAL; + self + } + + pub fn blink_interval(mut self, interval: Duration) -> Self { + self.interval = interval; + self + } pub fn subscribe_to(&mut self, emitter: &Entity, cx: &mut Context) where E: EventEmitter, { - let handle = cx.subscribe(emitter, |cursor, _emitter, event, cx| match event { + let handle = cx.subscribe(emitter, |state, _emitter, event, cx| match event { CursorTrigger::PauseBlinkingForUserAction => { - cursor.pause_blinking(cx); - cx.notify(); + if !state.interval.is_zero() { + state.pause_blinking(cx); + cx.notify(); + } } }); self.subscriptions.push(handle); } - /// Returns whether the cursor should currently be rendered. - pub fn visible(&self) -> bool { - self.visible - } - /// Activates cursor blinking. /// /// While active, the cursor will alternate between visible and hidden states at the configured interval. Has no effect if already active. - pub fn enable(&mut self, cx: &mut Context) { + fn enable(&mut self, cx: &mut Context) { if self.active { return; } @@ -86,7 +123,7 @@ impl Cursor { /// /// Marks the cursor as invisible and pauses blinking indefinitely. `enable` must be called explicitly to resume visibility and blinking. /// Call `pause_blinking` instead if you want to temporarily stop blinking while keeping the cursor visible. - pub fn disable(&mut self, cx: &mut Context) { + fn disable(&mut self, cx: &mut Context) { self.active = false; self.visible = false; self.paused = false; @@ -94,7 +131,7 @@ impl Cursor { } /// Temporarily pauses blinking and leaves the cursor visible. Blinking will resume after the pre-established interval elapses from the time this is called. - pub fn pause_blinking(&mut self, cx: &mut Context) { + fn pause_blinking(&mut self, cx: &mut Context) { if !self.visible { self.visible = true; cx.notify(); @@ -142,13 +179,15 @@ impl Cursor { }) .detach(); } +} +impl Cursor { pub fn update_input( &mut self, is_focused: bool, pos: Point, line_height: Pixels, - cx: &mut Context, + cx: &mut App, ) -> bool { let was_focused = self.was_focused; self.was_focused = is_focused; @@ -156,17 +195,25 @@ impl Cursor { self.point = pos; self.height = line_height; - match (self.interval.is_zero(), is_focused, was_focused) { + match ( + self.state.read(cx).interval.is_zero(), + is_focused, + was_focused, + ) { (true, _, _) => true, (false, true, false) => { - self.enable(cx); + self.state.update(cx, |state, cx| { + state.enable(cx); + }); true } (false, false, true) => { - self.disable(cx); + self.state.update(cx, |state, cx| { + state.disable(cx); + }); false } - (false, _, _) => self.visible, + (false, _, _) => self.state.read(cx).visible, } } } diff --git a/crates/gpui_elements/src/input/element.rs b/crates/gpui_elements/src/input/element.rs index 2c64e59784..5409136f9d 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 { @@ -159,8 +159,8 @@ impl Input { self } - pub fn cursor(mut self, entity: Entity) -> Self { - self.cursor = Some(entity); + pub fn text_cursor(mut self, cursor: Cursor) -> Self { + self.cursor = Some(cursor); self } } diff --git a/crates/gpui_elements/src/input/paint.rs b/crates/gpui_elements/src/input/paint.rs index 7c28a1b43a..3b25fdbcf1 100644 --- a/crates/gpui_elements/src/input/paint.rs +++ b/crates/gpui_elements/src/input/paint.rs @@ -67,10 +67,9 @@ impl Element for Input { } } - if let Some(cursor) = &self.cursor { - let (layout_id, layout) = cursor.update(cx, |cursor, cx| { - cursor.request_layout(global_id, inspector_id, window, cx) - }); + if let Some(cursor) = &mut self.cursor { + let (layout_id, layout) = + cursor.request_layout(global_id, inspector_id, window, cx); child_layout_ids.push(layout_id); cursor_layout = Some(layout); } @@ -133,16 +132,14 @@ impl Element for Input { window.with_element_offset(scroll_offset, |window| { 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, - ) - }); + let prepaint = cursor.prepaint( + global_id, + inspector_id, + bounds, + layout, + window, + cx, + ); cursor_prepaint = Some(prepaint); } _ => {} @@ -211,26 +208,24 @@ impl Element for Input { &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, + 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, ); - if is_focused && visible && context.snapshot.selected_range.is_empty() { - cursor.paint( - global_id, - inspector_id, - bounds, - layout, - prepaint, - window, - cx, - ); - } - }); + } } _ => {} } diff --git a/crates/gpui_elements/src/input/state.rs b/crates/gpui_elements/src/input/state.rs index f20262534c..ed5fbf206c 100644 --- a/crates/gpui_elements/src/input/state.rs +++ b/crates/gpui_elements/src/input/state.rs @@ -16,8 +16,10 @@ use unicode_segmentation::UnicodeSegmentation; #[derive(Clone, Debug)] pub enum InputStateEvent { /// Emitted when the input gains focus. + /// TODO: an emit was removed from element painting Focus, /// Emitted when the input loses focus. + /// TODO: an emit was removed from element painting Blur, /// Emitted when the text content changes. TextChanged, @@ -30,7 +32,6 @@ impl EventEmitter for InputState {} #[derive(Clone, Debug)] pub enum CursorTrigger { - // TODO: cursor needs to receive this PauseBlinkingForUserAction, } impl EventEmitter for InputState {}