diff --git a/crates/gpui_elements/src/editable_text.rs b/crates/gpui_elements/src/editable_text.rs index 2a22f1b115..19f6db2d4f 100644 --- a/crates/gpui_elements/src/editable_text.rs +++ b/crates/gpui_elements/src/editable_text.rs @@ -65,7 +65,7 @@ //! .min_w_10().max_w_128() //! .min_h_24().max_h_128() //! .whitespace_normal() // default -//! .overflow_x_scroll().overflow_y_scroll() +//! .overflow_y_scroll() //! # } //! ``` //! @@ -74,16 +74,44 @@ //! 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, cx: &mut App| { -//! println!("text changed to: {:?}", state.read(cx).as_str()); -//! }) +//! # 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}; +//! +//! // A unique id to the editable text element within the outer scope. +//! let id = ElementId::from("my_input"); +//! +//! // Find or lazily create the state entity backing the element. +//! // Then attach the entity to the element, thereby keeping it alive across consecutive frames. +//! let state = EditableTextState::use_keyed(id.clone(), window, cx); +//! +//! // This will trigger on every character input or other mutation to the underlying string +//! cx.subscribe(&state, |state, _: &TextChanged, cx| { +//! println!("{:?}", state.read(cx).as_str()); +//! }).detach(); +//! +//! // Using state explicitly attaches the state we already have attached to the ElementId. +//! text_input(id).state(state.downgrade()) +//! # } +//! ``` +//! +//! You can configure the default value of the editable text by using [`use_keyed_init`]: +//! ``` +//! # use gpui::{prelude::*, App, Entity, Window, AppContext, ElementId}; +//! # use gpui_elements::editable_text::{text_input, EditableTextState, StringStorage}; +//! # fn test(window: &mut Window, cx: &mut App) -> gpui_elements::editable_text::EditableTextElement { +//! let id = ElementId::from("my_input"); +//! +//! // The function parameter will only be called when the state is created/initialized. +//! // All successive renders across consecutive frames will re-use the existing state. +//! let _state = EditableTextState::use_keyed_init(id.clone(), window, cx, +//! |_window, _cx| StringStorage::from("this is some default text content")); +//! +//! // Its also plausible to omit the state function call. The element will try to find the state +//! // according to its id (which we are trusting here was garunteed to be at that id above). +//! // Despite this functionality, its recommended that callers which construct a state explicitly +//! // provide it to the element, at least for clarity and debugging. +//! text_input(id) //! # } //! ``` //! diff --git a/crates/gpui_elements/src/editable_text/element.rs b/crates/gpui_elements/src/editable_text/element.rs index e62ae19d44..8ab57800e1 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, FRcTextChanged, InitStorage, + EditableTextState, actions::{DEFAULT_INPUT_CONTEXT, EditableTextActionElement, EditableTextActionHandler}, layout::{TextInputLayoutData, TextLineSegment}, }; @@ -24,11 +24,9 @@ pub fn editable_text(id: impl Into) -> EditableTextElement { interactivity: Interactivity::default(), state_entity: Rc::new(RefCell::new(WeakEntity::new_invalid())), supports_multiline: true, - init_storage: InitStorage::default(), placeholder: None, accepts_input: true, colors: EditableTextColors::default(), - on_text_changed: None, }; this.interactivity.element_id = Some(id.into()); @@ -53,24 +51,16 @@ pub fn text_area(id: impl Into) -> EditableTextElement { } /// An input field which users can type text into. -/// -/// EditableText elements require a storage medium to be specified -/// (defaulting to [`StringStorage`](super::StringStorage)). -/// Use [`with_storage`](Self::with_storage) to configure the storage medium, -/// or [`default_value`](Self::default_value) to specify the content of the default storage medium. -/// pub struct EditableTextElement { interactivity: Interactivity, // Populated on first render with an entity stored/attached to the view. // This reference is shared with the action handlers, which are processed between renders // and therefore cannot otherwise access state attached to the view. state_entity: Rc>>, - init_storage: InitStorage, supports_multiline: bool, placeholder: Option, accepts_input: bool, colors: EditableTextColors, - on_text_changed: Option, } /// EditableText styling that goes beyond what Style/StyleRefinement supports @@ -103,6 +93,15 @@ impl Default for EditableTextColors { } impl EditableTextElement { + /// Assigns the underlying state of this element, which should persist across multiple frames. + /// The user should either create the entity once or utilize `Window::use_keyed_state` + /// to create an entity intrinsicly linked to the element. + /// If no state is configured, one will be linked to this element on first render via `Window::use_keyed_state`. + pub fn state(self, state: WeakEntity) -> Self { + *self.state_entity.borrow_mut() = state; + self + } + /// Configures whether the field supports multiple lines of text. /// Disabling this prevents actions like `enter` and navigating up and down. /// @@ -119,22 +118,6 @@ impl EditableTextElement { self } - /// Swaps the default storage container (standard String) with a custom initializer of [`UnicodeTextStorage`]. - pub fn with_storage(mut self, fn_init: impl Into) -> Self { - self.init_storage = fn_init.into(); - self - } - - /// Swaps the default storage container. The new initializer is a standard String using the provided value. - /// - /// Incompatible with [`with_storage`] (they establish the same internal value). - /// If you initialize custom storage, you should be able to initialize its default value. - pub fn default_value(mut self, value: impl Into) -> Self { - let storage = super::StringStorage::from(value.into()); - self.init_storage = InitStorage::new_typed(move |_cx| storage.clone()); - self - } - /// Configures whether the element can accept input (effectively means "is the element currently enabled"). pub fn accepts_input(mut self, enabled: bool) -> Self { self.accepts_input = enabled; @@ -173,20 +156,6 @@ 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(mut self, f: F) -> Self - where - F: 'static + Fn(&Entity, &mut App), - { - self.on_text_changed = Some(Rc::new(f)); - self - } } impl InteractiveElement for EditableTextElement { @@ -252,19 +221,18 @@ impl Element for EditableTextElement { cx: &mut App, ) -> (gpui::LayoutId, Self::RequestLayoutState) { // Fetches or initializes the internal state of the field - let state = match &self.interactivity().element_id { - None => unimplemented!("all input elements must be assigned an id"), - Some(element_id) => { - let state = window.use_keyed_state(element_id.clone(), cx, |_window, cx| { - EditableTextState::new(self.init_storage.exec(cx), cx) - }); - // 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 - } + let state = self.state_entity.borrow().upgrade(); + let state = match state { + Some(entity) => entity, + None => match &self.interactivity.element_id { + None => unimplemented!("all input elements must be assigned an id"), + Some(element_id) => { + let state = EditableTextState::use_keyed(element_id.clone(), window, cx); + // store a reference to the entity owned by the element for access in action handlers + *self.state_entity_rc().borrow_mut() = state.downgrade(); + state + } + }, }; let show_placeholder; diff --git a/crates/gpui_elements/src/editable_text/state.rs b/crates/gpui_elements/src/editable_text/state.rs index 150bac7811..c706f86c65 100644 --- a/crates/gpui_elements/src/editable_text/state.rs +++ b/crates/gpui_elements/src/editable_text/state.rs @@ -1,23 +1,15 @@ use crate::editable_text::{ - TextBoundary, UnicodeTextStorage, + StringStorage, TextBoundary, UnicodeTextStorage, actions::EditableTextActionHandler, caret::{Caret, CaretNotify}, history::EditableTextHistory, layout::TextInputLayoutData, }; use gpui::{ - App, Bounds, ClipboardItem, Context, Entity, EntityInputHandler, EventEmitter, FocusHandle, - Focusable, NavigationDirection, Pixels, Point, UTF16Selection, Window, point, + App, Bounds, ClipboardItem, Context, ElementId, Entity, EntityInputHandler, EventEmitter, + FocusHandle, Focusable, NavigationDirection, Pixels, Point, UTF16Selection, Window, point, }; -use std::{borrow::Cow, ops::Range, rc::Rc}; - -/// 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, &mut App) + 'static>; +use std::{borrow::Cow, ops::Range}; /// Internal state for EditableText elements. pub struct EditableTextState { @@ -27,10 +19,6 @@ pub struct EditableTextState { /// The caret entity which has internal state for features like blinking caret: Entity, - /// 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, - /// 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). @@ -60,6 +48,12 @@ pub struct EditableTextState { impl EventEmitter for EditableTextState {} +/// Event emitted when an `EditableTextState` is changed. +/// +/// This is not suitable for input sanitation (which should occur before the mutation). +pub struct TextChanged; +impl EventEmitter for EditableTextState {} + impl Focusable for EditableTextState { fn focus_handle(&self, _: &App) -> FocusHandle { self.focus_handle.clone() @@ -73,7 +67,55 @@ impl AsRef for EditableTextState { } impl EditableTextState { - pub fn new(storage: impl Into>, cx: &mut Context) -> Self { + /// Uses a pre-existing state attached to the element at `key`, as long as the element has existed over consecutive frames. + /// If the state does not yet exist, a new one is created using the default [`UnicodeTextStorage`] medium. + pub fn use_keyed(key: impl Into, window: &mut Window, cx: &mut App) -> Entity { + Self::use_keyed_init(key, window, cx, |_, _| StringStorage::default()) + } + + /// Uses a pre-existing state attached to the element at `key`, as long as the element has existed over consecutive frames. + /// If the state does not yet exist, a new one is created calling `init` to create a [`UnicodeTextStorage`] medium. + /// + /// ``` + /// # use gpui::{RenderOnce, Window, App, IntoElement, ElementId}; + /// # use gpui_elements::editable_text::{EditableTextState, StringStorage, editable_text}; + /// pub struct Form; + /// impl RenderOnce for Form { + /// fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { + /// let field_a_id = ElementId::from("field_a"); + /// let field_a = EditableTextState::use_keyed_init(field_a_id.clone(), window, cx, + /// |_window, _cx| StringStorage::from("this is some default editable text content")); + /// editable_text(field_a_id).state(field_a.downgrade()) + /// } + /// } + /// ``` + pub fn use_keyed_init( + key: impl Into, + window: &mut Window, + cx: &mut App, + init: F, + ) -> Entity + where + F: 'static + Fn(&mut Window, &mut Context<'_, EditableTextState>) -> StorageType, + StorageType: 'static + UnicodeTextStorage, + { + window.use_keyed_state(key, cx, |window, cx| Self::new(init(window, cx), cx)) + } + + /// Creates a new EditableText state with a given storage medium. + /// + /// Does not intrinsicly handle the state being attached to an element + /// over multiple frames (e.g. via [`RenderOnce`]). Use [`use_keyed`] or [`use_keyed_init`] for that. + /// + /// Expected to be called via [`AppContext::new`] such as: + /// ``` + /// # use gpui::{AppContext, Window, App, Entity}; + /// # use gpui_elements::editable_text::{StringStorage, EditableTextState}; + /// # fn new(_window: &mut Window, cx: &mut App) -> Entity { + /// cx.new(|cx| EditableTextState::new(StringStorage::default(), cx)) + /// # } + /// ``` + pub fn new(storage: impl UnicodeTextStorage + 'static, cx: &mut Context) -> Self { use gpui::AppContext; let caret = cx.new({ let state_entity = cx.entity(); @@ -84,7 +126,7 @@ impl EditableTextState { } }); Self { - storage: storage.into(), + storage: Box::new(storage), caret, selected_range: 0..0, @@ -97,7 +139,6 @@ 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(), } @@ -108,6 +149,14 @@ impl EditableTextState { self.storage().content_utf8() } + /// Replaces the contents of the stored text with the provided string slice. + pub fn emplace(&mut self, content: &str, cx: &mut Context) { + let len = self.storage.content_utf8().len(); + self.replace_text(0..len, content); + self.emit_text_changed(cx); + cx.notify(); + } + /// Returns the storage medium created for this field. pub(super) fn storage(&self) -> &Box { &self.storage @@ -195,15 +244,7 @@ impl EditableTextState { } fn emit_text_changed(&self, cx: &mut Context) { - 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)); + cx.emit(TextChanged); } } @@ -1110,8 +1151,7 @@ mod tests { } fn default_state(content: &str, cx: &mut Context) -> EditableTextState { - let storage = Box::new(StringStorage::from(content)) as Box; - EditableTextState::new(storage, cx) + EditableTextState::new(StringStorage::from(content), cx) } fn create_test_input( diff --git a/crates/gpui_elements/src/editable_text/storage.rs b/crates/gpui_elements/src/editable_text/storage.rs index 1adb0cf434..24b4d0307c 100644 --- a/crates/gpui_elements/src/editable_text/storage.rs +++ b/crates/gpui_elements/src/editable_text/storage.rs @@ -1,5 +1,5 @@ -use gpui::{App, NavigationDirection}; -use std::{ops::Range, rc::Rc}; +use gpui::NavigationDirection; +use std::ops::Range; use unicode_segmentation::UnicodeSegmentation; /// Describes a boundary within a chunk of text. @@ -14,39 +14,6 @@ pub enum TextBoundary { Document, } -/// Allocated Fn initializer for EditableText storage medium [`UnicodeTextStorage`]. -/// Defaults to empty/none, resulting in [`StringStorage`] when executed. -#[derive(Clone, Default)] -pub struct InitStorage(Option Box>>); - -impl InitStorage { - /// Basic constructor which requires that the output of the func is a `dyn UnicodeTextStorage`. - pub fn new(f: F) -> Self - where - F: 'static + Fn(&mut App) -> Box, - { - Self(Some(Rc::new(f))) - } - - /// Wrapper around [`new`] which automatically casts the output of the provided func to [`UnicodeTextStorage`]. - pub fn new_typed(f: F) -> Self - where - F: 'static + Fn(&mut App) -> R, - R: 'static + UnicodeTextStorage, - { - Self(Some(Rc::new(move |cx| { - Box::new(f(cx)) as Box - }))) - } - - pub(super) fn exec(&self, cx: &mut App) -> Box { - match &self.0 { - None => Box::new(StringStorage::default()), - Some(init) => (*init)(cx), - } - } -} - /// Implement this trait to create a storage medium that can be used as the content of EditableText elements. /// Default implementation is [`StringStorage`]. pub trait UnicodeTextStorage {