add external api for access to caret by routing it through keyed element data

This commit is contained in:
temportalflux
2026-07-11 09:31:07 -04:00
parent 8f99657171
commit d6d35ab410
3 changed files with 55 additions and 24 deletions
+22
View File
@@ -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.
//!
@@ -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<EditableTextState>,
caret: Entity<Caret>,
}
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<EditableTextState>,
window: &mut Window,
cx: &mut App,
) -> Entity<Caret> {
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<Pixels>,
@@ -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<dyn UnicodeTextStorage>,
/// The caret entity which has internal state for features like blinking
caret: Entity<Caret>,
/// 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>) -> 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<Caret> {
&self.caret
}
/// Returns the position of the caret in utf8 character space.
pub(super) fn caret_pos(&self) -> usize {
self.selected_range.start