diff --git a/crates/gpui_elements/src/editable_text/actions.rs b/crates/gpui_elements/src/editable_text/actions.rs index 18e2e9f094..c27fc40d7f 100644 --- a/crates/gpui_elements/src/editable_text/actions.rs +++ b/crates/gpui_elements/src/editable_text/actions.rs @@ -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( +pub(super) trait EditableInputActionElement { + type State: 'static; + + fn state_entity_rc(&self) -> &Rc>>; + + fn register_action( &mut self, - init_props: Self::InitProps, - listener: fn(&mut Self::State, &A, &mut Window, &mut Context), + listener: fn(&mut Self::State, &ActionType, &mut Window, &mut Context), ) where Self: InteractiveElement, + ActionType: Action + std::fmt::Debug, { + let entity_rc = self.state_entity_rc().clone(); self.interactivity() - .on_action::(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::(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) }); } diff --git a/crates/gpui_elements/src/editable_text/input_element.rs b/crates/gpui_elements/src/editable_text/input_element.rs index 6137bed03c..c7a806f8a9 100644 --- a/crates/gpui_elements/src/editable_text/input_element.rs +++ b/crates/gpui_elements/src/editable_text/input_element.rs @@ -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) -> 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) -> TextInputElement { // TODO: Disabled flag/state? pub struct TextInputElement { - placeholder: Option, interactivity: Interactivity, - state_key: Rc>, + // 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, + placeholder: Option, } 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>; - - 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 { - 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>> { + &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)); diff --git a/crates/gpui_elements/src/editable_text/input_state.rs b/crates/gpui_elements/src/editable_text/input_state.rs index c8e75945b9..35d412242c 100644 --- a/crates/gpui_elements/src/editable_text/input_state.rs +++ b/crates/gpui_elements/src/editable_text/input_state.rs @@ -90,7 +90,6 @@ impl EntityInputHandler for TextInputState { cx: &mut Context, ) { 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(); diff --git a/crates/gpui_elements/src/editable_text/shared_state.rs b/crates/gpui_elements/src/editable_text/shared_state.rs index 80d8200b35..8c526f03d4 100644 --- a/crates/gpui_elements/src/editable_text/shared_state.rs +++ b/crates/gpui_elements/src/editable_text/shared_state.rs @@ -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; - - fn get_state(&self, window: &mut Window, cx: &mut App) -> Entity { - Self::get_or_init_state(&self.init_props(), window, cx) - } -} - pub struct TextInputStateBase { storage: Box, diff --git a/crates/gpui_elements/src/editable_text/text_area_element.rs b/crates/gpui_elements/src/editable_text/text_area_element.rs index b2edd4c762..edee4a4056 100644 --- a/crates/gpui_elements/src/editable_text/text_area_element.rs +++ b/crates/gpui_elements/src/editable_text/text_area_element.rs @@ -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) -> 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, 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, + placeholder: Option, } 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 { - // 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>> { + &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,