add on_text_changed callback binding to allow the caller to be notified on every change to the text
This commit is contained in:
@@ -17,21 +17,26 @@
|
||||
//! Standard library strings are not ideal though for large text documents. For such uses,
|
||||
//! it is encouraged that implementers consider rolling their own [`UnicodeTextStorage`] medium.
|
||||
//!
|
||||
//! Some sample usages:
|
||||
//! ### Usage Samples
|
||||
//!
|
||||
//! A single-line text input with a fixed width and text that does not wrap
|
||||
//! (overflow text is clipped and does not scroll).
|
||||
//! ```
|
||||
//! # use gpui::prelude::*;
|
||||
//! # fn test() -> gpui_elements::editable_text::EditableTextElement {
|
||||
//! use gpui_elements::editable_text::text_input;
|
||||
//! text_input("my_input")
|
||||
//! .placeholder("empty text")
|
||||
//! .w_5()
|
||||
//! .min_h_auto()
|
||||
//! .whitespace_nowrap()
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! A single-line text input with a flexible width and text that does not wrap, but will scroll if overflowing.
|
||||
//! ```
|
||||
//! # use gpui::{prelude::*, Hsla};
|
||||
//! # fn test() -> gpui_elements::editable_text::EditableTextElement {
|
||||
//! use gpui_elements::editable_text::text_input;
|
||||
//! text_input("my_input")
|
||||
//! .placeholder("empty text")
|
||||
@@ -41,10 +46,13 @@
|
||||
//! .min_h_auto()
|
||||
//! .whitespace_nowrap()
|
||||
//! .overflow_x_scroll()
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! A multi-line text area with flexible height, wrapping text, and scrolling overflow on both axes.
|
||||
//! ```
|
||||
//! # use gpui::{prelude::*, Hsla};
|
||||
//! # fn test() -> gpui_elements::editable_text::EditableTextElement {
|
||||
//! use gpui_elements::editable_text::text_area;
|
||||
//! text_area("message")
|
||||
//! .placeholder("empty text")
|
||||
@@ -54,12 +62,32 @@
|
||||
//! .min_h_24().max_h_128()
|
||||
//! .whitespace_normal() // default
|
||||
//! .overflow_x_scroll().overflow_y_scroll()
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! The user-inputted text can be accessed via event callbacks on the element.
|
||||
//! There is no callback representing the concept of "user is done editing". Its recommended that
|
||||
//! users write a [debounce](https://developer.mozilla.org/en-US/docs/Glossary/Debounce)
|
||||
//! or some way to detect "focus lost" to signify the user leaving the field.
|
||||
//! ```
|
||||
//! # use gpui::{prelude::*, App, Entity};
|
||||
//! # fn test() -> gpui_elements::editable_text::EditableTextElement {
|
||||
//! use gpui_elements::editable_text::{text_input, EditableTextState};
|
||||
//! text_input("my_input")
|
||||
//! .whitespace_nowrap()
|
||||
//! .overflow_x_scroll()
|
||||
//! // this will trigger on every character input or other mutation to the underlying string
|
||||
//! .on_text_changed(|state: &Entity<EditableTextState>, cx: &mut App| {
|
||||
//! println!("text changed to: {:?}", state.read(cx).as_str());
|
||||
//! })
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! 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.
|
||||
//!
|
||||
//! Backlog of not-yet implemented features:
|
||||
//! ### Backlog of not-yet implemented features:
|
||||
//! - detecting focus being lost on an EditableText field
|
||||
//! - text sanitation & validation (see no-op implementation of [`EditableTextState::validate_incoming_text`])
|
||||
//! - nav & select via PageUp/PageDown
|
||||
//! - screen reader support via a11y
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::editable_text::{
|
||||
EditableTextState, InitStorage,
|
||||
EditableTextState, FRcTextChanged, InitStorage,
|
||||
actions::{DEFAULT_INPUT_CONTEXT, EditableTextActionElement, EditableTextActionHandler},
|
||||
layout::{TextInputLayoutData, TextLineSegment},
|
||||
};
|
||||
@@ -28,6 +28,7 @@ pub fn editable_text(id: impl Into<ElementId>) -> EditableTextElement {
|
||||
placeholder: None,
|
||||
accepts_input: true,
|
||||
colors: EditableTextColors::default(),
|
||||
on_text_changed: None,
|
||||
};
|
||||
this.interactivity.element_id = Some(id.into());
|
||||
|
||||
@@ -69,6 +70,7 @@ pub struct EditableTextElement {
|
||||
placeholder: Option<SharedString>,
|
||||
accepts_input: bool,
|
||||
colors: EditableTextColors,
|
||||
on_text_changed: Option<FRcTextChanged>,
|
||||
}
|
||||
|
||||
/// EditableText styling that goes beyond what Style/StyleRefinement supports
|
||||
@@ -171,6 +173,20 @@ impl EditableTextElement {
|
||||
self.colors.ime_underline = color;
|
||||
self
|
||||
}
|
||||
|
||||
/// Assigns the callback to execute on after every change to the text.
|
||||
///
|
||||
/// This is not suitable for input sanitation (which should occur before the mutation).
|
||||
///
|
||||
/// Doesn't use the established emit/subscribe pattern normally found on entities.
|
||||
/// Reasoning and blockers are described in the documentation of [`FRcTextChanged`].
|
||||
pub fn on_text_changed<F>(mut self, f: F) -> Self
|
||||
where
|
||||
F: 'static + Fn(&Entity<EditableTextState>, &mut App),
|
||||
{
|
||||
self.on_text_changed = Some(Rc::new(f));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl InteractiveElement for EditableTextElement {
|
||||
@@ -244,6 +260,9 @@ impl Element for EditableTextElement {
|
||||
});
|
||||
// store a reference to the entity owned by the element for access in action handlers
|
||||
*self.state_entity_rc().borrow_mut() = state.downgrade();
|
||||
state.update(cx, |state, _cx| {
|
||||
state.on_text_changed = self.on_text_changed.clone();
|
||||
});
|
||||
state
|
||||
}
|
||||
};
|
||||
@@ -263,7 +282,14 @@ impl Element for EditableTextElement {
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: Unlike other elements, a FocusHandle is owned by the state.
|
||||
// This means the user is currently unable to provide a focus handle.
|
||||
// This was born out of Interactivity not having a way to read the focus handle.
|
||||
// This might be able to be eliminated entirely if focus handle can be passed thru on_mouse_down.
|
||||
|
||||
// TODO: This required a gpui api change in order to sync the focus handle between Interactivity and TextInputStateBase
|
||||
// maybe use `set_focus_handle` during prepaint?
|
||||
//window.set_focus_handle(focus_handle, cx);
|
||||
self.interactivity.track_focus(focus_handle);
|
||||
|
||||
let placeholder = self.placeholder.clone();
|
||||
|
||||
@@ -9,12 +9,17 @@ use gpui::{
|
||||
App, Bounds, ClipboardItem, Context, Entity, EntityInputHandler, EventEmitter, FocusHandle,
|
||||
Focusable, NavigationDirection, Pixels, Point, UTF16Selection, Window, point,
|
||||
};
|
||||
use std::{borrow::Cow, ops::Range};
|
||||
use std::{borrow::Cow, ops::Range, rc::Rc};
|
||||
|
||||
/// Event emitted via EditableText elements when the internal storage contents have changed
|
||||
pub struct TextChanged;
|
||||
/// Heap allocated function which is triggered when the text is changed.
|
||||
///
|
||||
/// This doesn't use the established [`Context::emit`] pattern because of limitations caused by
|
||||
/// [`Window::use_keyed_state`], where the [`EditableTextState`] entity is unavailable until
|
||||
/// [`Element::request_layout`] and therefore cannot be provided to the
|
||||
/// element's caller/constructor for usage via [`Context::subscribe`].
|
||||
pub type FRcTextChanged = Rc<dyn Fn(&Entity<EditableTextState>, &mut App) + 'static>;
|
||||
|
||||
/// Internal state for EditableText elements. There is no way to access this externally with the current api.
|
||||
/// Internal state for EditableText elements.
|
||||
pub struct EditableTextState {
|
||||
/// The storage medium backing this element-state. Hypothetically supports both
|
||||
/// std String and other crates (e.g. long document text).
|
||||
@@ -22,6 +27,10 @@ pub struct EditableTextState {
|
||||
/// The caret entity which has internal state for features like blinking
|
||||
caret: Entity<Caret>,
|
||||
|
||||
/// Callback for consumers to receive notifications that the storage has changed.
|
||||
/// See documentation of [`FRcTextChanged`] why this doesnt use the typical emit/subscribe approach.
|
||||
pub(super) on_text_changed: Option<FRcTextChanged>,
|
||||
|
||||
/// 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).
|
||||
/// Empty when start==end. The start of this range is always the current position of the caret (input cursor).
|
||||
@@ -49,7 +58,6 @@ pub struct EditableTextState {
|
||||
pub(super) layout_data: TextInputLayoutData,
|
||||
}
|
||||
|
||||
impl EventEmitter<TextChanged> for EditableTextState {}
|
||||
impl EventEmitter<CaretNotify> for EditableTextState {}
|
||||
|
||||
impl Focusable for EditableTextState {
|
||||
@@ -58,6 +66,12 @@ impl Focusable for EditableTextState {
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for EditableTextState {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl EditableTextState {
|
||||
pub fn new(storage: impl Into<Box<dyn UnicodeTextStorage>>, cx: &mut Context<Self>) -> Self {
|
||||
use gpui::AppContext;
|
||||
@@ -83,13 +97,19 @@ impl EditableTextState {
|
||||
focus_handle: cx.focus_handle(),
|
||||
// TODO: what is the best way to give users access to configure this via element
|
||||
history: Some(EditableTextHistory::default()),
|
||||
on_text_changed: None,
|
||||
|
||||
layout_data: TextInputLayoutData::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the current contents of [`storage`] as a string slice.
|
||||
pub fn as_str(&self) -> &str {
|
||||
self.storage().content_utf8()
|
||||
}
|
||||
|
||||
/// Returns the storage medium created for this field.
|
||||
pub fn storage(&self) -> &Box<dyn UnicodeTextStorage> {
|
||||
pub(super) fn storage(&self) -> &Box<dyn UnicodeTextStorage> {
|
||||
&self.storage
|
||||
}
|
||||
|
||||
@@ -109,12 +129,12 @@ impl EditableTextState {
|
||||
}
|
||||
|
||||
/// Returns a reference to the entity owning the state of the [`Caret`] (e.g. its blinking state).
|
||||
pub fn caret_entity(&self) -> &Entity<Caret> {
|
||||
pub(super) fn caret_entity(&self) -> &Entity<Caret> {
|
||||
&self.caret
|
||||
}
|
||||
|
||||
/// Returns the position of the caret in utf8 character space.
|
||||
pub fn caret_pos(&self) -> usize {
|
||||
pub(super) fn caret_pos(&self) -> usize {
|
||||
self.selected_range.start
|
||||
}
|
||||
|
||||
@@ -123,7 +143,7 @@ impl EditableTextState {
|
||||
}
|
||||
|
||||
/// Returns the IME marked range for character operations.
|
||||
pub fn marked_range(&self) -> Option<Range<usize>> {
|
||||
pub(super) fn marked_range(&self) -> Option<Range<usize>> {
|
||||
self.marked_range.clone()
|
||||
}
|
||||
}
|
||||
@@ -168,6 +188,18 @@ impl EditableTextState {
|
||||
self.selected_range = end_pos..end_pos;
|
||||
self.marked_range = None;
|
||||
}
|
||||
|
||||
fn emit_text_changed(&self, cx: &mut Context<Self>) {
|
||||
let Some(rc_callback) = self.on_text_changed.clone() else {
|
||||
return;
|
||||
};
|
||||
// Defer the emit until the end of the update cycle so that the state can be provided
|
||||
// as the subject instead of direct access to storage.
|
||||
// That way if the listener needs to mutate the stored contents, they can do so via
|
||||
// apis on Self (which will help retain caret and selection coherence).
|
||||
let entity = cx.entity();
|
||||
cx.defer(move |cx| (*rc_callback)(&entity, cx));
|
||||
}
|
||||
}
|
||||
|
||||
// Screen space (text layout engine output) & String space transformers
|
||||
@@ -409,7 +441,7 @@ impl EditableTextState {
|
||||
|
||||
self.replace_text(start..end, "");
|
||||
|
||||
cx.emit(TextChanged);
|
||||
self.emit_text_changed(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -608,7 +640,7 @@ impl EntityInputHandler for EditableTextState {
|
||||
let text_to_insert = self.validate_incoming_text(&range_utf8, text_to_insert);
|
||||
self.replace_text(range_utf8, text_to_insert);
|
||||
cx.emit(CaretNotify::PauseBlinking);
|
||||
cx.emit(TextChanged);
|
||||
self.emit_text_changed(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -625,7 +657,7 @@ impl EntityInputHandler for EditableTextState {
|
||||
self.replace_text(range.clone(), text_to_insert);
|
||||
self.ime_mark_text_in_range(&range, text_to_insert.len());
|
||||
self.ime_mark_selected_range(&range, &new_selected_range_utf16, text_to_insert.len());
|
||||
cx.emit(TextChanged);
|
||||
self.emit_text_changed(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -937,7 +969,7 @@ impl<'app> EditableTextActionHandler<Context<'app, Self>> for EditableTextState
|
||||
|
||||
self.replace_text(self.selected_range.clone(), "");
|
||||
}
|
||||
cx.emit(TextChanged);
|
||||
self.emit_text_changed(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -960,7 +992,7 @@ impl<'app> EditableTextActionHandler<Context<'app, Self>> for EditableTextState
|
||||
let range = self.ime_resolve_range(None);
|
||||
let text_to_insert = self.validate_incoming_text(&range, &text);
|
||||
self.replace_text(range, text_to_insert);
|
||||
cx.emit(TextChanged);
|
||||
self.emit_text_changed(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user