rework state tracking so that action handlers can access the state of a text field, which occurs outside of the render/frame stack
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
use gpui::{Action, AppContext, Context, InteractiveElement, Window};
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use gpui::{Action, AppContext, Context, InteractiveElement, WeakEntity, Window};
|
||||
|
||||
/// The key context used for input element keybindings.
|
||||
pub const DEFAULT_INPUT_CONTEXT: &str = "Input";
|
||||
@@ -234,133 +236,83 @@ pub trait EditableTextActionHandler<'app>: Sized {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) trait EditableInputActionElement: super::StateBackedElement {
|
||||
fn register_action<A: Action>(
|
||||
pub(super) trait EditableInputActionElement {
|
||||
type State: 'static;
|
||||
|
||||
fn state_entity_rc(&self) -> &Rc<RefCell<WeakEntity<Self::State>>>;
|
||||
|
||||
fn register_action<ActionType>(
|
||||
&mut self,
|
||||
init_props: Self::InitProps,
|
||||
listener: fn(&mut Self::State, &A, &mut Window, &mut Context<Self::State>),
|
||||
listener: fn(&mut Self::State, &ActionType, &mut Window, &mut Context<Self::State>),
|
||||
) where
|
||||
Self: InteractiveElement,
|
||||
ActionType: Action + std::fmt::Debug,
|
||||
{
|
||||
let entity_rc = self.state_entity_rc().clone();
|
||||
self.interactivity()
|
||||
.on_action::<A>(move |action, window, cx| {
|
||||
let state = Self::get_or_init_state(&init_props, window, cx);
|
||||
state.update(cx, |state, cx| {
|
||||
listener(state, action, window, cx);
|
||||
});
|
||||
.on_action::<ActionType>(move |action, window, cx| {
|
||||
let weak_entity = entity_rc.borrow();
|
||||
if let Some(entity) = weak_entity.upgrade() {
|
||||
entity.update(cx, |state, cx| {
|
||||
listener(state, action, window, cx);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn register_actions(&mut self)
|
||||
where
|
||||
Self: InteractiveElement,
|
||||
Self::InitProps: Clone,
|
||||
Self::State:
|
||||
for<'app> EditableTextActionHandler<'app, Context = gpui::Context<'app, Self::State>>,
|
||||
{
|
||||
use super::actions::*;
|
||||
let init_props = self.init_props();
|
||||
self.register_action(init_props.clone(), |state, action, window, cx| {
|
||||
state.escape(action, window, cx)
|
||||
});
|
||||
self.register_action(init_props.clone(), |state, action, window, cx| {
|
||||
state.insert_enter(action, window, cx)
|
||||
});
|
||||
self.register_action(init_props.clone(), |state, action, window, cx| {
|
||||
state.insert_tab(action, window, cx)
|
||||
});
|
||||
self.register_action(init_props.clone(), |state, action, window, cx| {
|
||||
println!("backspace");
|
||||
state.backspace(action, window, cx)
|
||||
});
|
||||
self.register_action(init_props.clone(), |state, action, window, cx| {
|
||||
state.delete(action, window, cx)
|
||||
});
|
||||
self.register_action(init_props.clone(), |state, action, window, cx| {
|
||||
self.register_action(|state, action, window, cx| state.escape(action, window, cx));
|
||||
self.register_action(|state, action, window, cx| state.insert_enter(action, window, cx));
|
||||
self.register_action(|state, action, window, cx| state.insert_tab(action, window, cx));
|
||||
self.register_action(|state, action, window, cx| state.backspace(action, window, cx));
|
||||
self.register_action(|state, action, window, cx| state.delete(action, window, cx));
|
||||
self.register_action(|state, action, window, cx| {
|
||||
state.delete_word_left(action, window, cx)
|
||||
});
|
||||
self.register_action(init_props.clone(), |state, action, window, cx| {
|
||||
self.register_action(|state, action, window, cx| {
|
||||
state.delete_word_right(action, window, cx)
|
||||
});
|
||||
self.register_action(init_props.clone(), |state, action, window, cx| {
|
||||
self.register_action(|state, action, window, cx| {
|
||||
state.delete_to_line_start(action, window, cx)
|
||||
});
|
||||
self.register_action(init_props.clone(), |state, action, window, cx| {
|
||||
self.register_action(|state, action, window, cx| {
|
||||
state.delete_to_line_end(action, window, cx)
|
||||
});
|
||||
self.register_action(init_props.clone(), |state, action, window, cx| {
|
||||
state.nav_left(action, window, cx)
|
||||
});
|
||||
self.register_action(init_props.clone(), |state, action, window, cx| {
|
||||
state.nav_right(action, window, cx)
|
||||
});
|
||||
self.register_action(init_props.clone(), |state, action, window, cx| {
|
||||
state.nav_up(action, window, cx)
|
||||
});
|
||||
self.register_action(init_props.clone(), |state, action, window, cx| {
|
||||
state.nav_down(action, window, cx)
|
||||
});
|
||||
self.register_action(init_props.clone(), |state, action, window, cx| {
|
||||
state.nav_line_start(action, window, cx)
|
||||
});
|
||||
self.register_action(init_props.clone(), |state, action, window, cx| {
|
||||
state.nav_line_end(action, window, cx)
|
||||
});
|
||||
self.register_action(init_props.clone(), |state, action, window, cx| {
|
||||
state.nav_start(action, window, cx)
|
||||
});
|
||||
self.register_action(init_props.clone(), |state, action, window, cx| {
|
||||
state.nav_end(action, window, cx)
|
||||
});
|
||||
self.register_action(init_props.clone(), |state, action, window, cx| {
|
||||
state.nav_left_word(action, window, cx)
|
||||
});
|
||||
self.register_action(init_props.clone(), |state, action, window, cx| {
|
||||
state.nav_right_word(action, window, cx)
|
||||
});
|
||||
self.register_action(init_props.clone(), |state, action, window, cx| {
|
||||
state.select_all(action, window, cx)
|
||||
});
|
||||
self.register_action(init_props.clone(), |state, action, window, cx| {
|
||||
state.select_left(action, window, cx)
|
||||
});
|
||||
self.register_action(init_props.clone(), |state, action, window, cx| {
|
||||
state.select_right(action, window, cx)
|
||||
});
|
||||
self.register_action(init_props.clone(), |state, action, window, cx| {
|
||||
state.select_up(action, window, cx)
|
||||
});
|
||||
self.register_action(init_props.clone(), |state, action, window, cx| {
|
||||
state.select_down(action, window, cx)
|
||||
});
|
||||
self.register_action(init_props.clone(), |state, action, window, cx| {
|
||||
state.select_start(action, window, cx)
|
||||
});
|
||||
self.register_action(init_props.clone(), |state, action, window, cx| {
|
||||
state.select_end(action, window, cx)
|
||||
});
|
||||
self.register_action(init_props.clone(), |state, action, window, cx| {
|
||||
self.register_action(|state, action, window, cx| state.nav_left(action, window, cx));
|
||||
self.register_action(|state, action, window, cx| state.nav_right(action, window, cx));
|
||||
self.register_action(|state, action, window, cx| state.nav_up(action, window, cx));
|
||||
self.register_action(|state, action, window, cx| state.nav_down(action, window, cx));
|
||||
self.register_action(|state, action, window, cx| state.nav_line_start(action, window, cx));
|
||||
self.register_action(|state, action, window, cx| state.nav_line_end(action, window, cx));
|
||||
self.register_action(|state, action, window, cx| state.nav_start(action, window, cx));
|
||||
self.register_action(|state, action, window, cx| state.nav_end(action, window, cx));
|
||||
self.register_action(|state, action, window, cx| state.nav_left_word(action, window, cx));
|
||||
self.register_action(|state, action, window, cx| state.nav_right_word(action, window, cx));
|
||||
self.register_action(|state, action, window, cx| state.select_all(action, window, cx));
|
||||
self.register_action(|state, action, window, cx| state.select_left(action, window, cx));
|
||||
self.register_action(|state, action, window, cx| state.select_right(action, window, cx));
|
||||
self.register_action(|state, action, window, cx| state.select_up(action, window, cx));
|
||||
self.register_action(|state, action, window, cx| state.select_down(action, window, cx));
|
||||
self.register_action(|state, action, window, cx| state.select_start(action, window, cx));
|
||||
self.register_action(|state, action, window, cx| state.select_end(action, window, cx));
|
||||
self.register_action(|state, action, window, cx| {
|
||||
state.select_left_word(action, window, cx)
|
||||
});
|
||||
self.register_action(init_props.clone(), |state, action, window, cx| {
|
||||
self.register_action(|state, action, window, cx| {
|
||||
state.select_right_word(action, window, cx)
|
||||
});
|
||||
self.register_action(init_props.clone(), |state, action, window, cx| {
|
||||
state.cut(action, window, cx)
|
||||
});
|
||||
self.register_action(init_props.clone(), |state, action, window, cx| {
|
||||
state.copy(action, window, cx)
|
||||
});
|
||||
self.register_action(init_props.clone(), |state, action, window, cx| {
|
||||
state.paste(action, window, cx)
|
||||
});
|
||||
self.register_action(init_props.clone(), |state, action, window, cx| {
|
||||
state.undo(action, window, cx)
|
||||
});
|
||||
self.register_action(init_props.clone(), |state, action, window, cx| {
|
||||
state.redo(action, window, cx)
|
||||
});
|
||||
self.register_action(init_props.clone(), |state, action, window, cx| {
|
||||
self.register_action(|state, action, window, cx| state.cut(action, window, cx));
|
||||
self.register_action(|state, action, window, cx| state.copy(action, window, cx));
|
||||
self.register_action(|state, action, window, cx| state.paste(action, window, cx));
|
||||
self.register_action(|state, action, window, cx| state.undo(action, window, cx));
|
||||
self.register_action(|state, action, window, cx| state.redo(action, window, cx));
|
||||
self.register_action(|state, action, window, cx| {
|
||||
state.show_character_palette(action, window, cx)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use crate::editable_text::{
|
||||
InitStorage, StateBackedElement, TextInputState, TextInputStateBase, TextLayoutWrapping,
|
||||
TextLineSegment,
|
||||
InitStorage, TextInputState, TextInputStateBase, TextLayoutWrapping, TextLineSegment,
|
||||
actions::{DEFAULT_INPUT_CONTEXT, EditableInputActionElement},
|
||||
};
|
||||
use gpui::{
|
||||
@@ -8,20 +7,20 @@ use gpui::{
|
||||
ElementInputHandler, Entity, FocusHandle, Focusable, Hitbox, HitboxBehavior, Hsla,
|
||||
InteractiveElement, Interactivity, IntoElement, MouseButton, MouseDownEvent, MouseMoveEvent,
|
||||
MouseUpEvent, PaintQuad, Pixels, Point, ScrollWheelEvent, SharedString, Style, StyleRefinement,
|
||||
Styled, TextAlign, TextStyle, Window, WrappedLine, fill, point, px, size,
|
||||
Styled, TextAlign, TextStyle, WeakEntity, Window, WrappedLine, fill, point, px, size,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use std::{cell::RefCell, ops::Range, rc::Rc, sync::Arc};
|
||||
|
||||
#[track_caller]
|
||||
pub fn input(id: impl Into<ElementId>) -> TextInputElement {
|
||||
let element_id = id.into();
|
||||
let mut this = TextInputElement {
|
||||
placeholder: None,
|
||||
interactivity: Interactivity::new(),
|
||||
state_key: Rc::new(RefCell::new((element_id.clone(), InitStorage::default()))),
|
||||
state_entity: Rc::new(RefCell::new(WeakEntity::new_invalid())),
|
||||
init_storage: InitStorage::default(),
|
||||
placeholder: None,
|
||||
};
|
||||
this.interactivity.element_id = Some(element_id);
|
||||
this.interactivity.element_id = Some(id.into());
|
||||
|
||||
this = this.key_context(DEFAULT_INPUT_CONTEXT);
|
||||
this.register_actions();
|
||||
@@ -31,9 +30,13 @@ pub fn input(id: impl Into<ElementId>) -> TextInputElement {
|
||||
|
||||
// TODO: Disabled flag/state?
|
||||
pub struct TextInputElement {
|
||||
placeholder: Option<SharedString>,
|
||||
interactivity: Interactivity,
|
||||
state_key: Rc<RefCell<(ElementId, InitStorage)>>,
|
||||
// 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<TextInputState>>>,
|
||||
init_storage: InitStorage,
|
||||
placeholder: Option<SharedString>,
|
||||
}
|
||||
|
||||
impl InteractiveElement for TextInputElement {
|
||||
@@ -55,28 +58,10 @@ impl IntoElement for TextInputElement {
|
||||
}
|
||||
}
|
||||
|
||||
impl EditableInputActionElement for TextInputElement {}
|
||||
impl super::StateBackedElement for TextInputElement {
|
||||
impl EditableInputActionElement for TextInputElement {
|
||||
type State = TextInputState;
|
||||
type InitProps = Rc<RefCell<(ElementId, InitStorage)>>;
|
||||
|
||||
fn init_props(&self) -> Self::InitProps {
|
||||
self.state_key.clone()
|
||||
}
|
||||
|
||||
fn get_or_init_state(
|
||||
init_props: &Self::InitProps,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Entity<TextInputState> {
|
||||
let (element_id, init_storage) = init_props.borrow().clone();
|
||||
println!("get_or_init_state {element_id:?}");
|
||||
// TODO: This can only be called during layout, prepaint, or paint. It cannot be called during an action handler, which occurs between frames.
|
||||
// Get the state from the app using the element's id as the key.
|
||||
// If it doesnt exist, initialize a new state with the user's desired storage medium.
|
||||
window.use_keyed_state(element_id, cx, |_window, cx| {
|
||||
TextInputState::new(init_storage.exec(cx), cx)
|
||||
})
|
||||
fn state_entity_rc(&self) -> &Rc<RefCell<WeakEntity<Self::State>>> {
|
||||
&self.state_entity
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,9 +88,8 @@ impl PrepaintElement {
|
||||
}
|
||||
|
||||
pub mod element {
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use super::*;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
#[doc(hidden)]
|
||||
pub struct LayoutState {
|
||||
@@ -142,9 +126,21 @@ impl Element for TextInputElement {
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::App,
|
||||
) -> (gpui::LayoutId, Self::RequestLayoutState) {
|
||||
let mut resolved_text_style = None;
|
||||
// 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| {
|
||||
println!("init input state {:?}", cx.weak_entity());
|
||||
TextInputState::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.borrow_mut() = state.downgrade();
|
||||
state
|
||||
}
|
||||
};
|
||||
|
||||
let state = self.get_state(window, cx);
|
||||
let mut resolved_text_style = None;
|
||||
|
||||
self.interactivity
|
||||
.track_focus(state.read(cx).focus_handle(cx));
|
||||
|
||||
@@ -90,7 +90,6 @@ impl EntityInputHandler for TextInputState {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let range_utf8 = self.internal.ime_resolve_range(range_utf16);
|
||||
println!("{range_utf8:?} {text_to_insert:?}");
|
||||
self.internal
|
||||
.replace_text_in_range_bytes(range_utf8, text_to_insert, cx);
|
||||
//self.mark_layout_dirty();
|
||||
|
||||
@@ -3,8 +3,8 @@ use crate::editable_text::{
|
||||
notify::{TextChanged, TextHistoryPushed},
|
||||
};
|
||||
use gpui::{
|
||||
App, Bounds, ClipboardItem, Entity, FocusHandle, Focusable, Hsla, NavigationDirection, Pixels,
|
||||
Point, SharedString, TextRun, TextStyle, UTF16Selection, Window, WrappedLine, point,
|
||||
App, Bounds, ClipboardItem, FocusHandle, Focusable, Hsla, NavigationDirection, Pixels, Point,
|
||||
SharedString, TextRun, TextStyle, UTF16Selection, Window, WrappedLine, point,
|
||||
};
|
||||
use std::{ops::Range, sync::Arc};
|
||||
|
||||
@@ -14,23 +14,6 @@ pub trait TextStateNotifier {
|
||||
fn emit_history(&mut self, event: TextHistoryPushed);
|
||||
}
|
||||
|
||||
pub(super) trait StateBackedElement {
|
||||
type State: 'static;
|
||||
type InitProps: 'static;
|
||||
|
||||
fn init_props(&self) -> Self::InitProps;
|
||||
|
||||
fn get_or_init_state(
|
||||
init_props: &Self::InitProps,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Entity<Self::State>;
|
||||
|
||||
fn get_state(&self, window: &mut Window, cx: &mut App) -> Entity<Self::State> {
|
||||
Self::get_or_init_state(&self.init_props(), window, cx)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TextInputStateBase {
|
||||
storage: Box<dyn UnicodeTextStorage>,
|
||||
|
||||
|
||||
@@ -1,29 +1,37 @@
|
||||
use crate::editable_text::{
|
||||
InitStorage, StateBackedElement, TextAreaState, actions::EditableInputActionElement,
|
||||
InitStorage, TextAreaState,
|
||||
actions::{DEFAULT_INPUT_CONTEXT, EditableInputActionElement},
|
||||
};
|
||||
use gpui::{
|
||||
App, Element, ElementId, Entity, Hitbox, InteractiveElement, Interactivity, IntoElement,
|
||||
SharedString, StyleRefinement, Styled, TextStyle, Window,
|
||||
Element, ElementId, Entity, Hitbox, InteractiveElement, Interactivity, IntoElement,
|
||||
SharedString, StyleRefinement, Styled, TextStyle, WeakEntity,
|
||||
};
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
pub fn text_area(id: impl Into<ElementId>) -> TextAreaElement {
|
||||
let mut this = TextAreaElement {
|
||||
id: id.into(),
|
||||
placeholder: None,
|
||||
interactivity: Interactivity::new(),
|
||||
state_entity: Rc::new(RefCell::new(WeakEntity::new_invalid())),
|
||||
init_storage: InitStorage::default(),
|
||||
placeholder: None,
|
||||
};
|
||||
this = this.key_context(super::actions::DEFAULT_INPUT_CONTEXT);
|
||||
this.interactivity.element_id = Some(id.into());
|
||||
|
||||
this = this.key_context(DEFAULT_INPUT_CONTEXT);
|
||||
this.register_actions();
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
// TODO: Disabled flag/state?
|
||||
pub struct TextAreaElement {
|
||||
id: ElementId,
|
||||
placeholder: Option<SharedString>,
|
||||
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<TextAreaState>>>,
|
||||
init_storage: InitStorage,
|
||||
placeholder: Option<SharedString>,
|
||||
}
|
||||
|
||||
impl InteractiveElement for TextAreaElement {
|
||||
@@ -45,25 +53,10 @@ impl IntoElement for TextAreaElement {
|
||||
}
|
||||
}
|
||||
|
||||
impl EditableInputActionElement for TextAreaElement {}
|
||||
impl super::StateBackedElement for TextAreaElement {
|
||||
impl EditableInputActionElement for TextAreaElement {
|
||||
type State = TextAreaState;
|
||||
type InitProps = (ElementId, InitStorage);
|
||||
|
||||
fn init_props(&self) -> Self::InitProps {
|
||||
(self.id.clone(), self.init_storage.clone())
|
||||
}
|
||||
|
||||
fn get_or_init_state(
|
||||
init_props: &Self::InitProps,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Entity<Self::State> {
|
||||
// Get the state from the app using the element's id as the key.
|
||||
// If it doesnt exist, initialize a new state with the user's desired storage medium.
|
||||
window.use_keyed_state(init_props.0.clone(), cx, |_window, cx| {
|
||||
TextAreaState::new(init_props.1.exec(cx), cx)
|
||||
})
|
||||
fn state_entity_rc(&self) -> &Rc<RefCell<WeakEntity<Self::State>>> {
|
||||
&self.state_entity
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,9 +94,20 @@ impl Element for TextAreaElement {
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::App,
|
||||
) -> (gpui::LayoutId, Self::RequestLayoutState) {
|
||||
let mut resolved_text_style = None;
|
||||
// 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| {
|
||||
TextAreaState::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.borrow_mut() = state.downgrade();
|
||||
state
|
||||
}
|
||||
};
|
||||
|
||||
let state = self.get_state(window, cx);
|
||||
let mut resolved_text_style = None;
|
||||
|
||||
let layout_id = self.interactivity.request_layout(
|
||||
global_id,
|
||||
|
||||
Reference in New Issue
Block a user