rework element api to expose EditableTextState as a viable entity for users to create via use_keyed_state
This commit is contained in:
@@ -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<EditableTextState>, 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)
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
|
||||
@@ -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<ElementId>) -> 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<ElementId>) -> 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<RefCell<WeakEntity<EditableTextState>>>,
|
||||
init_storage: InitStorage,
|
||||
supports_multiline: bool,
|
||||
placeholder: Option<SharedString>,
|
||||
accepts_input: bool,
|
||||
colors: EditableTextColors,
|
||||
on_text_changed: Option<FRcTextChanged>,
|
||||
}
|
||||
|
||||
/// 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<EditableTextState>) -> 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<InitStorage>) -> 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<String>) -> 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<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 {
|
||||
@@ -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;
|
||||
|
||||
@@ -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<dyn Fn(&Entity<EditableTextState>, &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<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).
|
||||
@@ -60,6 +48,12 @@ pub struct EditableTextState {
|
||||
|
||||
impl EventEmitter<CaretNotify> 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<TextChanged> for EditableTextState {}
|
||||
|
||||
impl Focusable for EditableTextState {
|
||||
fn focus_handle(&self, _: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
@@ -73,7 +67,55 @@ impl AsRef<str> for EditableTextState {
|
||||
}
|
||||
|
||||
impl EditableTextState {
|
||||
pub fn new(storage: impl Into<Box<dyn UnicodeTextStorage>>, cx: &mut Context<Self>) -> 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<ElementId>, window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||
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<F, StorageType>(
|
||||
key: impl Into<ElementId>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
init: F,
|
||||
) -> Entity<Self>
|
||||
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<EditableTextState> {
|
||||
/// cx.new(|cx| EditableTextState::new(StringStorage::default(), cx))
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn new(storage: impl UnicodeTextStorage + 'static, cx: &mut Context<Self>) -> 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<Self>) {
|
||||
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<dyn UnicodeTextStorage> {
|
||||
&self.storage
|
||||
@@ -195,15 +244,7 @@ impl EditableTextState {
|
||||
}
|
||||
|
||||
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));
|
||||
cx.emit(TextChanged);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1110,8 +1151,7 @@ mod tests {
|
||||
}
|
||||
|
||||
fn default_state(content: &str, cx: &mut Context<EditableTextState>) -> EditableTextState {
|
||||
let storage = Box::new(StringStorage::from(content)) as Box<dyn UnicodeTextStorage>;
|
||||
EditableTextState::new(storage, cx)
|
||||
EditableTextState::new(StringStorage::from(content), cx)
|
||||
}
|
||||
|
||||
fn create_test_input(
|
||||
|
||||
@@ -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<Rc<dyn Fn(&mut App) -> Box<dyn UnicodeTextStorage>>>);
|
||||
|
||||
impl InitStorage {
|
||||
/// Basic constructor which requires that the output of the func is a `dyn UnicodeTextStorage`.
|
||||
pub fn new<F>(f: F) -> Self
|
||||
where
|
||||
F: 'static + Fn(&mut App) -> Box<dyn UnicodeTextStorage>,
|
||||
{
|
||||
Self(Some(Rc::new(f)))
|
||||
}
|
||||
|
||||
/// Wrapper around [`new`] which automatically casts the output of the provided func to [`UnicodeTextStorage`].
|
||||
pub fn new_typed<F, R>(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<dyn UnicodeTextStorage>
|
||||
})))
|
||||
}
|
||||
|
||||
pub(super) fn exec(&self, cx: &mut App) -> Box<dyn UnicodeTextStorage> {
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user