register text-based actions with <input> element via traits

This commit is contained in:
temportalflux
2026-07-11 09:31:07 -04:00
parent 7c1658a24a
commit bad745a761
3 changed files with 193 additions and 17 deletions
@@ -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<A: Action>(
&mut self,
init_props: Self::InitProps,
listener: fn(&mut Self::State, &A, &mut Window, &mut Context<Self::State>),
) where
Self: InteractiveElement,
{
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);
});
});
}
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)
});
}
}
@@ -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<ElementId>) -> TextInputElement {
@@ -11,9 +13,10 @@ pub fn input(id: impl Into<ElementId>) -> 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<SharedString>,
interactivity: Interactivity,
init_storage: Option<Box<dyn Fn(&mut App) -> Box<dyn UnicodeTextStorage>>>,
init_storage: InitStorage,
}
impl InteractiveElement for TextInputElement {
@@ -44,6 +47,39 @@ impl IntoElement for TextInputElement {
}
}
#[derive(Clone, Default)]
pub(super) struct InitStorage(Option<Rc<dyn Fn(&mut App) -> Box<dyn UnicodeTextStorage>>>);
impl InitStorage {
fn exec(&self, cx: &mut App) -> Box<dyn UnicodeTextStorage> {
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<TextInputState> {
// 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<TextInputState>,
@@ -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,
@@ -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<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>,