From d6d35ab410f2272673121dd86e76ee93a868cb61 Mon Sep 17 00:00:00 2001 From: temportalflux Date: Tue, 7 Jul 2026 10:50:30 -0400 Subject: [PATCH] add external api for access to caret by routing it through keyed element data --- crates/gpui_elements/src/editable_text.rs | 22 +++++++++++ .../src/editable_text/element.rs | 38 ++++++++++++++++--- .../gpui_elements/src/editable_text/state.rs | 19 +--------- 3 files changed, 55 insertions(+), 24 deletions(-) diff --git a/crates/gpui_elements/src/editable_text.rs b/crates/gpui_elements/src/editable_text.rs index 9b61ebb099..29650020ba 100644 --- a/crates/gpui_elements/src/editable_text.rs +++ b/crates/gpui_elements/src/editable_text.rs @@ -115,6 +115,28 @@ //! # } //! ``` //! +//! To use a blinking caret, construct a caret entity with a blinking interval when constructing the state: +//! ``` +//! # use gpui::{prelude::*, App, Entity, Window, AppContext, ElementId}; +//! # fn test(window: &mut Window, cx: &mut App) -> gpui_elements::editable_text::EditableTextElement { +//! use gpui_elements::editable_text::{text_input, EditableTextState, TextChanged, Caret}; +//! let id = ElementId::from("my_input"); +//! +//! let state = EditableTextState::use_keyed(id.clone(), window, cx); +//! +//! // Ensure the caret exists, linked to the input element by id. +//! window.use_keyed_state(id.clone(), cx, |window, cx| { +//! // using the default interval of 500ms +//! let mut caret = Caret::default().blink_interval_default(); +//! // ensures the caret receives events from the input state during typing & other actions +//! caret.subscribe_to(&state, cx); +//! caret +//! }); +//! +//! text_input(id).state(state.downgrade()) +//! # } +//! ``` +//! //! You can view more complex examples in the gpui crate examples. //! TODO: there is no example with editable text yet, and we should link it here when there is. //! diff --git a/crates/gpui_elements/src/editable_text/element.rs b/crates/gpui_elements/src/editable_text/element.rs index e98e27b88d..d405302f49 100644 --- a/crates/gpui_elements/src/editable_text/element.rs +++ b/crates/gpui_elements/src/editable_text/element.rs @@ -1,5 +1,5 @@ use crate::editable_text::{ - EditableTextState, + Caret, EditableTextState, actions::{DEFAULT_INPUT_CONTEXT, EditableTextActionElement, EditableTextActionHandler}, layout::{EditableTextLayoutResult, EditableTextLayoutState, TextLineSegment}, }; @@ -216,6 +216,7 @@ struct PrelayoutState { #[doc(hidden)] pub struct LayoutState { state: Entity, + caret: Entity, } struct InteractivityPrepaint { @@ -253,6 +254,7 @@ impl Element for EditableTextElement { cx: &mut App, ) -> (gpui::LayoutId, Self::RequestLayoutState) { let entity = self.find_or_create_state(window, cx); + let caret = self.find_or_create_caret(&entity, window, cx); // Read new state information from the underlying entity. // Block-wrapped so that the state being read is dropped before continuing. @@ -302,7 +304,13 @@ impl Element for EditableTextElement { }, ); - (layout_id, LayoutState { state: entity }) + ( + layout_id, + LayoutState { + state: entity, + caret, + }, + ) } fn prepaint( @@ -316,16 +324,17 @@ impl Element for EditableTextElement { ) -> Self::PrepaintState { // should reflect the text content layout size of the stored text, // so that scrolling can take it into account during prepaint. - let (content_size, caret_entity, focus_handle) = { + let (content_size, focus_handle) = { let state = request_layout.state.read(cx); let content_size = state.layout_data.state.size.unwrap_or_else(|| bounds.size); - let caret = state.caret_entity().clone(); let focus_handle = state.focus_handle(cx); - (content_size, caret, focus_handle) + (content_size, focus_handle) }; let is_focused = focus_handle.is_focused(window); - let caret_visible = caret_entity.update(cx, |caret, cx| caret.update_focus(is_focused, cx)); + let caret_visible = request_layout + .caret + .update(cx, |caret, cx| caret.update_focus(is_focused, cx)); window.set_focus_handle(&focus_handle, cx); let prepaint = self.interactivity.prepaint( @@ -443,6 +452,23 @@ impl EditableTextElement { state } + fn find_or_create_caret( + &self, + state: &Entity, + window: &mut Window, + cx: &mut App, + ) -> Entity { + let Some(element_id) = self.interactivity.element_id.clone() else { + unimplemented!("all input elements must be assigned an id") + }; + + window.use_keyed_state(element_id, cx, |_window, cx| { + let mut caret = Caret::default(); + caret.subscribe_to(state, cx); + caret + }) + } + fn process_frame_events( prepaint: &PrepaintState, bounds: Bounds, diff --git a/crates/gpui_elements/src/editable_text/state.rs b/crates/gpui_elements/src/editable_text/state.rs index f0968b741b..525411fd19 100644 --- a/crates/gpui_elements/src/editable_text/state.rs +++ b/crates/gpui_elements/src/editable_text/state.rs @@ -1,7 +1,7 @@ use crate::editable_text::{ StringStorage, TextBoundary, UnicodeTextStorage, actions::EditableTextActionHandler, - caret::{Caret, CaretNotify}, + caret::CaretNotify, history::EditableTextHistory, layout::{EditableTextLayoutResult, TextLineSegment}, }; @@ -63,8 +63,6 @@ pub struct EditableTextState { /// The storage medium backing this element-state. Hypothetically supports both /// std String and other crates (e.g. long document text). storage: Box, - /// The caret entity which has internal state for features like blinking - caret: Entity, /// The utf-8 character range that is currently selected by the user. /// Valid both when start < end and start > end (which dictates the direction of the selection). @@ -163,18 +161,8 @@ impl EditableTextState { /// # } /// ``` pub fn new(storage: impl UnicodeTextStorage + 'static, cx: &mut Context) -> Self { - use gpui::AppContext; - let caret = cx.new({ - let state_entity = cx.entity(); - move |cx| { - let mut caret = Caret::default().blink_interval_default(); - caret.subscribe_to(&state_entity, cx); - caret - } - }); Self { storage: Box::new(storage), - caret, selected_range: 0.into(), marked_range: None, @@ -222,11 +210,6 @@ impl EditableTextState { } } - /// Returns a reference to the entity owning the state of the [`Caret`] (e.g. its blinking state). - pub(super) fn caret_entity(&self) -> &Entity { - &self.caret - } - /// Returns the position of the caret in utf8 character space. pub(super) fn caret_pos(&self) -> usize { self.selected_range.start