diff --git a/crates/gpui_elements/src/editable_text/actions.rs b/crates/gpui_elements/src/editable_text/actions.rs index a97bb7f658..9b11909925 100644 --- a/crates/gpui_elements/src/editable_text/actions.rs +++ b/crates/gpui_elements/src/editable_text/actions.rs @@ -1,4 +1,4 @@ -use gpui::{App, AppContext, Window}; +use gpui::{Action, App, AppContext, Context, InteractiveElement, Window}; /// The key context used for input element keybindings. pub const DEFAULT_INPUT_CONTEXT: &str = "Input"; @@ -233,3 +233,134 @@ pub trait EditableTextActionHandler<'app>: Sized { ) { } } + +pub trait EditableInputActionElement: super::StateBackedElement { + fn register_action( + &mut self, + init_props: Self::InitProps, + listener: fn(&mut Self::State, &A, &mut Window, &mut Context), + ) where + Self: InteractiveElement, + { + 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); + }); + }); + } + + 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| { + 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| { + state.delete_word_left(action, window, cx) + }); + self.register_action(init_props.clone(), |state, action, window, cx| { + state.delete_word_right(action, window, cx) + }); + self.register_action(init_props.clone(), |state, action, window, cx| { + state.delete_to_line_start(action, window, cx) + }); + self.register_action(init_props.clone(), |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| { + state.select_left_word(action, window, cx) + }); + self.register_action(init_props.clone(), |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| { + 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 483ee4b871..52257aabec 100644 --- a/crates/gpui_elements/src/editable_text/input_element.rs +++ b/crates/gpui_elements/src/editable_text/input_element.rs @@ -1,9 +1,11 @@ +use crate::editable_text::{ + EditableInputActionElement, StateBackedElement, TextInputState, UnicodeTextStorage, +}; use gpui::{ App, Element, ElementId, Entity, Hitbox, InteractiveElement, Interactivity, IntoElement, - Length, SharedString, StyleRefinement, Styled, TextStyle, + SharedString, StyleRefinement, Styled, TextStyle, Window, }; - -use crate::editable_text::{TextInputState, UnicodeTextStorage}; +use std::rc::Rc; #[track_caller] pub fn input(id: impl Into) -> TextInputElement { @@ -11,9 +13,10 @@ pub fn input(id: impl Into) -> TextInputElement { id: id.into(), placeholder: None, interactivity: Interactivity::new(), - init_storage: None, + init_storage: InitStorage::default(), }; this = this.key_context(super::DEFAULT_INPUT_CONTEXT); + this.register_actions(); this } @@ -22,7 +25,7 @@ pub struct TextInputElement { id: ElementId, placeholder: Option, interactivity: Interactivity, - init_storage: Option Box>>, + init_storage: InitStorage, } impl InteractiveElement for TextInputElement { @@ -44,6 +47,39 @@ impl IntoElement for TextInputElement { } } +#[derive(Clone, Default)] +pub(super) struct InitStorage(Option Box>>); +impl InitStorage { + fn exec(&self, cx: &mut App) -> Box { + match &self.0 { + None => Box::new(String::new()), + Some(init) => (*init)(cx), + } + } +} + +impl EditableInputActionElement for TextInputElement {} +impl super::StateBackedElement for TextInputElement { + type State = TextInputState; + 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| { + TextInputState::new(init_props.1.exec(cx), cx) + }) + } +} + #[doc(hidden)] pub struct LayoutState { state: Entity, @@ -76,15 +112,7 @@ impl Element for TextInputElement { ) -> (gpui::LayoutId, Self::RequestLayoutState) { let mut resolved_text_style = None; - // 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. - let state = window.use_keyed_state(self.id.clone(), cx, |_window, cx| { - let storage = match &self.init_storage { - None => Box::new(String::new()), - Some(init_storage) => (*init_storage)(cx), - }; - TextInputState::new(storage, cx) - }); + let state = self.get_state(window, cx); let layout_id = self.interactivity.request_layout( global_id, diff --git a/crates/gpui_elements/src/editable_text/shared_state.rs b/crates/gpui_elements/src/editable_text/shared_state.rs index bc1c87b549..e67d3af202 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, ClipboardItem, FocusHandle, Focusable, NavigationDirection, Pixels, Point, UTF16Selection, - Window, + App, ClipboardItem, Entity, FocusHandle, Focusable, NavigationDirection, Pixels, Point, + UTF16Selection, Window, }; use std::ops::Range; @@ -14,6 +14,23 @@ 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,