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:
temportalflux
2026-07-11 09:31:07 -04:00
parent bf22400798
commit a8281b2555
5 changed files with 117 additions and 183 deletions
+53 -101
View File
@@ -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,