input: dispatch editing keys through actions

This commit is contained in:
2026-10-29 15:27:56 +07:00
parent e2331f2ba5
commit 5f53e0c89f
6 changed files with 656 additions and 184 deletions
+595 -176
View File
@@ -8,15 +8,22 @@ use unicode_segmentation::UnicodeSegmentation;
use gpui::prelude::FluentBuilder as _;
use gpui::{
App, AppContext as _, Bounds, ClipboardItem, Context, DispatchPhase, ElementId, Entity,
EntityInputHandler, EventEmitter, Font, InteractiveElement as _, IntoElement, KeyDownEvent,
MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels,
RenderOnce, Role, SharedString, StatefulInteractiveElement as _, Styled as _, TextAlign,
TextRun, UTF16Selection, Window, canvas, div, fill, point, px,
EntityInputHandler, EventEmitter, Font, InteractiveElement as _, IntoElement, KeyBinding,
KeyDownEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement as _,
Pixels, RenderOnce, Role, SharedString, StatefulInteractiveElement as _, Styled as _,
TextAlign, TextRun, UTF16Selection, Window, actions, canvas, div, fill, point, px,
};
use gpui_native_theme::{ActiveTheme as _, ColorTokens, Size};
use crate::common::{Sizable, h_flex};
/// The key context input bindings match against.
pub const INPUT_CONTEXT: &str = "Input";
/// Consecutive edits within this interval merge into one undo entry.
const DEFAULT_GROUP_INTERVAL: Duration = Duration::from_millis(300);
/// Maximum undo entries kept; the oldest drops off the front.
const MAX_HISTORY: usize = 100;
/// Significant changes in an [`InputState`], observable via `cx.subscribe`.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum InputStateEvent {
@@ -30,6 +37,238 @@ pub enum InputStateEvent {
Submitted,
}
actions!(
input,
[
Escape,
Submit,
Backspace,
Delete,
DeleteWordLeft,
DeleteWordRight,
DeleteToBeginningOfLine,
Left,
Right,
SelectLeft,
SelectRight,
WordLeft,
WordRight,
SelectWordLeft,
SelectWordRight,
Home,
End,
SelectHome,
SelectEnd,
SelectAll,
Copy,
Cut,
Paste,
Undo,
Redo,
]
);
/// Keybindings for input actions.
#[derive(Clone, Debug)]
pub struct InputBindings {
pub escape: Option<KeyBinding>,
pub submit: Option<KeyBinding>,
pub backspace: Option<KeyBinding>,
pub delete: Option<KeyBinding>,
pub delete_word_left: Option<KeyBinding>,
pub delete_word_right: Option<KeyBinding>,
pub delete_to_beginning_of_line: Option<KeyBinding>,
pub left: Option<KeyBinding>,
pub right: Option<KeyBinding>,
pub select_left: Option<KeyBinding>,
pub select_right: Option<KeyBinding>,
pub word_left: Option<KeyBinding>,
pub word_right: Option<KeyBinding>,
pub select_word_left: Option<KeyBinding>,
pub select_word_right: Option<KeyBinding>,
pub home: Option<KeyBinding>,
pub end: Option<KeyBinding>,
pub select_home: Option<KeyBinding>,
pub select_end: Option<KeyBinding>,
pub select_all: Option<KeyBinding>,
pub copy: Option<KeyBinding>,
pub cut: Option<KeyBinding>,
pub paste: Option<KeyBinding>,
pub undo: Option<KeyBinding>,
pub redo: Option<KeyBinding>,
}
impl Default for InputBindings {
/// The platform-specific defaults: cmd on macOS, ctrl elsewhere.
fn default() -> Self {
let context = Some(INPUT_CONTEXT);
#[cfg(target_os = "macos")]
let bindings = Self {
escape: Some(KeyBinding::new("escape", Escape, context)),
submit: Some(KeyBinding::new("enter", Submit, context)),
backspace: Some(KeyBinding::new("backspace", Backspace, context)),
delete: Some(KeyBinding::new("delete", Delete, context)),
delete_word_left: Some(KeyBinding::new("alt-backspace", DeleteWordLeft, context)),
delete_word_right: Some(KeyBinding::new("alt-delete", DeleteWordRight, context)),
delete_to_beginning_of_line: Some(KeyBinding::new(
"cmd-backspace",
DeleteToBeginningOfLine,
context,
)),
left: Some(KeyBinding::new("left", Left, context)),
right: Some(KeyBinding::new("right", Right, context)),
select_left: Some(KeyBinding::new("shift-left", SelectLeft, context)),
select_right: Some(KeyBinding::new("shift-right", SelectRight, context)),
word_left: Some(KeyBinding::new("alt-left", WordLeft, context)),
word_right: Some(KeyBinding::new("alt-right", WordRight, context)),
select_word_left: Some(KeyBinding::new("alt-shift-left", SelectWordLeft, context)),
select_word_right: Some(KeyBinding::new("alt-shift-right", SelectWordRight, context)),
home: Some(KeyBinding::new("home", Home, context)),
end: Some(KeyBinding::new("end", End, context)),
select_home: Some(KeyBinding::new("shift-home", SelectHome, context)),
select_end: Some(KeyBinding::new("shift-end", SelectEnd, context)),
select_all: Some(KeyBinding::new("cmd-a", SelectAll, context)),
copy: Some(KeyBinding::new("cmd-c", Copy, context)),
cut: Some(KeyBinding::new("cmd-x", Cut, context)),
paste: Some(KeyBinding::new("cmd-v", Paste, context)),
undo: Some(KeyBinding::new("cmd-z", Undo, context)),
redo: Some(KeyBinding::new("cmd-shift-z", Redo, context)),
};
#[cfg(not(target_os = "macos"))]
let bindings = Self {
escape: Some(KeyBinding::new("escape", Escape, context)),
submit: Some(KeyBinding::new("enter", Submit, context)),
backspace: Some(KeyBinding::new("backspace", Backspace, context)),
delete: Some(KeyBinding::new("delete", Delete, context)),
delete_word_left: Some(KeyBinding::new("ctrl-backspace", DeleteWordLeft, context)),
delete_word_right: Some(KeyBinding::new("ctrl-delete", DeleteWordRight, context)),
delete_to_beginning_of_line: Some(KeyBinding::new(
"ctrl-shift-backspace",
DeleteToBeginningOfLine,
context,
)),
left: Some(KeyBinding::new("left", Left, context)),
right: Some(KeyBinding::new("right", Right, context)),
select_left: Some(KeyBinding::new("shift-left", SelectLeft, context)),
select_right: Some(KeyBinding::new("shift-right", SelectRight, context)),
word_left: Some(KeyBinding::new("ctrl-left", WordLeft, context)),
word_right: Some(KeyBinding::new("ctrl-right", WordRight, context)),
select_word_left: Some(KeyBinding::new("ctrl-shift-left", SelectWordLeft, context)),
select_word_right: Some(KeyBinding::new(
"ctrl-shift-right",
SelectWordRight,
context,
)),
home: Some(KeyBinding::new("home", Home, context)),
end: Some(KeyBinding::new("end", End, context)),
select_home: Some(KeyBinding::new("shift-home", SelectHome, context)),
select_end: Some(KeyBinding::new("shift-end", SelectEnd, context)),
select_all: Some(KeyBinding::new("ctrl-a", SelectAll, context)),
copy: Some(KeyBinding::new("ctrl-c", Copy, context)),
cut: Some(KeyBinding::new("ctrl-x", Cut, context)),
paste: Some(KeyBinding::new("ctrl-v", Paste, context)),
undo: Some(KeyBinding::new("ctrl-z", Undo, context)),
redo: Some(KeyBinding::new("ctrl-shift-z", Redo, context)),
};
bindings
}
}
impl InputBindings {
/// A table with every key unbound, for building custom bindings.
pub fn empty() -> Self {
Self {
escape: None,
submit: None,
backspace: None,
delete: None,
delete_word_left: None,
delete_word_right: None,
delete_to_beginning_of_line: None,
left: None,
right: None,
select_left: None,
select_right: None,
word_left: None,
word_right: None,
select_word_left: None,
select_word_right: None,
home: None,
end: None,
select_home: None,
select_end: None,
select_all: None,
copy: None,
cut: None,
paste: None,
undo: None,
redo: None,
}
}
/// Collects the table into keybindings.
pub fn into_bindings(self) -> Vec<KeyBinding> {
let mut bindings: Vec<KeyBinding> = [
self.escape,
self.submit,
self.backspace,
self.delete,
self.delete_word_left,
self.delete_word_right,
self.delete_to_beginning_of_line,
self.left,
self.right,
self.select_left,
self.select_right,
self.word_left,
self.word_right,
self.select_word_left,
self.select_word_right,
self.home,
self.end,
self.select_home,
self.select_end,
self.select_all,
self.copy,
self.cut,
self.paste,
self.undo,
self.redo,
]
.into_iter()
.flatten()
.collect();
#[cfg(target_os = "macos")]
{
let context = Some(INPUT_CONTEXT);
bindings.extend([
KeyBinding::new("cmd-left", Home, context),
KeyBinding::new("cmd-right", End, context),
KeyBinding::new("cmd-up", Home, context),
KeyBinding::new("cmd-down", End, context),
KeyBinding::new("cmd-shift-left", SelectHome, context),
KeyBinding::new("cmd-shift-right", SelectEnd, context),
]);
}
bindings
}
}
/// Registers input keybindings on the application.
///
/// Pass `None` for the platform defaults.
pub fn bind_input_keys<B>(cx: &mut App, bindings: B)
where
B: Into<Option<InputBindings>>,
{
cx.bind_keys(bindings.into().unwrap_or_default().into_bindings());
}
/// Input layout information for hit testing and caret positioning.
struct InputLayout {
line: gpui::ShapedLine,
@@ -43,11 +282,6 @@ type KeyDownHandler = std::rc::Rc<dyn Fn(&KeyDownEvent, &mut Window, &mut App)>;
/// Handler for mouse down events.
type MouseDownHandler = std::rc::Rc<dyn Fn(&MouseDownEvent, &mut Window, &mut App)>;
/// Consecutive edits within this interval merge into one undo entry.
const DEFAULT_GROUP_INTERVAL: Duration = Duration::from_millis(300);
/// Maximum undo entries kept; the oldest drops off the front.
const MAX_HISTORY: usize = 100;
/// History entry for undo/redo operations.
#[derive(Clone, Debug)]
struct HistoryEntry {
@@ -276,48 +510,6 @@ impl InputState {
}
}
/// Reverts the most recent edit group (`cmd-z`).
fn undo(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self.read_only {
return;
}
let Some(entry) = self.undo_stack.pop() else {
return;
};
let inverse = entry.reverse(&mut self.text);
self.redo_stack.push(inverse);
self.cursor = entry.cursor.min(self.text.len());
self.anchor = entry.anchor.min(self.text.len());
cx.emit(InputStateEvent::TextChanged);
self.notify(window, cx);
}
/// Reapplies the most recently undone edit group (`cmd-shift-z`).
fn redo(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self.read_only {
return;
}
let Some(entry) = self.redo_stack.pop() else {
return;
};
let inverse = entry.reverse(&mut self.text);
self.undo_stack.push(inverse);
// The caret lands at the end of the restored text,
// not where it was before the original edit.
let caret = entry.range.start + entry.old_text.len();
self.cursor = caret.min(self.text.len());
self.anchor = self.cursor;
cx.emit(InputStateEvent::TextChanged);
self.notify(window, cx);
}
/// The focus handle, for applications that focus the field programmatically.
pub fn focus_handle(&self) -> gpui::FocusHandle {
self.focus_handle.clone()
@@ -687,9 +879,7 @@ impl InputState {
}
}
/// The key-down handler: navigation, editing, clipboard, and submit.
///
/// Runs only while the field is focused.
/// The key-down handler: inserts typed characters.
fn handle_key_down(
&mut self,
event: &KeyDownEvent,
@@ -701,130 +891,202 @@ impl InputState {
}
let keystroke = &event.keystroke;
let modifiers = keystroke.modifiers;
// macOS: cmd jumps to the field ends,
// cmd-backspace deletes to the start, alt is the word modifier.
//
// Elsewhere ctrl is the word modifier
// and the Home/End keys already cover the ends.
#[cfg(target_os = "macos")]
let (command, word, jump) = (modifiers.platform, modifiers.alt, modifiers.platform);
#[cfg(not(target_os = "macos"))]
let (command, word, jump) = (modifiers.control, modifiers.control, false);
match keystroke.key.as_str() {
"left" => {
let to = if jump {
0
} else if word {
previous_word_start(&self.text, self.cursor)
} else {
previous_grapheme_start(&self.text, self.cursor)
};
self.move_cursor(to, modifiers.shift, window, cx);
cx.stop_propagation();
}
"right" => {
let to = if jump {
self.text.len()
} else if word {
next_word_end(&self.text, self.cursor)
} else {
next_grapheme_start(&self.text, self.cursor)
};
self.move_cursor(to, modifiers.shift, window, cx);
cx.stop_propagation();
}
"up" => {
if jump {
self.move_cursor(0, modifiers.shift, window, cx);
cx.stop_propagation();
}
}
"down" => {
if jump {
self.move_cursor(self.text.len(), modifiers.shift, window, cx);
cx.stop_propagation();
}
}
"home" => {
self.move_cursor(0, modifiers.shift, window, cx);
cx.stop_propagation();
}
"end" => {
self.move_cursor(self.text.len(), modifiers.shift, window, cx);
cx.stop_propagation();
}
"backspace" => {
if jump {
// macOS cmd-backspace deletes to the field start.
let range = 0..self.cursor;
self.replace_range(range, "", window, cx);
} else {
self.delete_backward(word, window, cx);
}
cx.stop_propagation();
}
"delete" => {
self.delete_forward(word, window, cx);
cx.stop_propagation();
}
"a" if command => {
let len = self.text.len();
self.select_range(0..len, window, cx);
cx.stop_propagation();
}
"c" if command => {
if self.copy_selection(window, cx) {
cx.stop_propagation();
}
}
"x" if command => {
// Read-only refuses cut entirely, clipboard included.
if !self.read_only && self.copy_selection(window, cx) {
self.delete_selection(window, cx);
cx.stop_propagation();
}
}
"v" if command => {
if let Some(item) = cx.read_from_clipboard()
&& let Some(text) = item.text()
{
let range = self.selection();
self.replace_range(range, &text, window, cx);
}
cx.stop_propagation();
}
"z" if command => {
if modifiers.shift {
self.redo(window, cx);
} else {
self.undo(window, cx);
}
cx.stop_propagation();
}
"enter" => {
cx.emit(InputStateEvent::Submitted);
cx.stop_propagation();
}
"escape" => {
window.blur(cx);
cx.stop_propagation();
}
"tab" => {}
_ => {
if let Some(text) = &keystroke.key_char
&& !modifiers.platform
&& !modifiers.control
{
let range = self.selection();
self.replace_range(range, text, window, cx);
cx.stop_propagation();
}
}
// Chars typed with a menu modifier are shortcuts, not text,
// and control characters (enter, tab) have their own semantics.
if let Some(text) = &keystroke.key_char
&& !keystroke.modifiers.platform
&& !keystroke.modifiers.control
&& text.chars().all(|ch| !ch.is_control())
{
let range = self.selection();
self.replace_range(range, text, window, cx);
cx.stop_propagation();
}
}
fn left(&mut self, _: &Left, window: &mut Window, cx: &mut Context<Self>) {
let to = previous_grapheme_start(&self.text, self.cursor);
self.move_cursor(to, false, window, cx);
}
fn select_left(&mut self, _: &SelectLeft, window: &mut Window, cx: &mut Context<Self>) {
let to = previous_grapheme_start(&self.text, self.cursor);
self.move_cursor(to, true, window, cx);
}
fn right(&mut self, _: &Right, window: &mut Window, cx: &mut Context<Self>) {
let to = next_grapheme_start(&self.text, self.cursor);
self.move_cursor(to, false, window, cx);
}
fn select_right(&mut self, _: &SelectRight, window: &mut Window, cx: &mut Context<Self>) {
let to = next_grapheme_start(&self.text, self.cursor);
self.move_cursor(to, true, window, cx);
}
fn word_left(&mut self, _: &WordLeft, window: &mut Window, cx: &mut Context<Self>) {
let to = previous_word_start(&self.text, self.cursor);
self.move_cursor(to, false, window, cx);
}
fn select_word_left(
&mut self,
_: &SelectWordLeft,
window: &mut Window,
cx: &mut Context<Self>,
) {
let to = previous_word_start(&self.text, self.cursor);
self.move_cursor(to, true, window, cx);
}
fn word_right(&mut self, _: &WordRight, window: &mut Window, cx: &mut Context<Self>) {
let to = next_word_end(&self.text, self.cursor);
self.move_cursor(to, false, window, cx);
}
fn select_word_right(
&mut self,
_: &SelectWordRight,
window: &mut Window,
cx: &mut Context<Self>,
) {
let to = next_word_end(&self.text, self.cursor);
self.move_cursor(to, true, window, cx);
}
fn home(&mut self, _: &Home, window: &mut Window, cx: &mut Context<Self>) {
self.move_cursor(0, false, window, cx);
}
fn end(&mut self, _: &End, window: &mut Window, cx: &mut Context<Self>) {
self.move_cursor(self.text.len(), false, window, cx);
}
fn select_home(&mut self, _: &SelectHome, window: &mut Window, cx: &mut Context<Self>) {
self.move_cursor(0, true, window, cx);
}
fn select_end(&mut self, _: &SelectEnd, window: &mut Window, cx: &mut Context<Self>) {
self.move_cursor(self.text.len(), true, window, cx);
}
fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context<Self>) {
self.delete_backward(false, window, cx);
}
fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context<Self>) {
self.delete_forward(false, window, cx);
}
fn delete_word_left(
&mut self,
_: &DeleteWordLeft,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.delete_backward(true, window, cx);
}
fn delete_word_right(
&mut self,
_: &DeleteWordRight,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.delete_forward(true, window, cx);
}
fn delete_to_beginning_of_line(
&mut self,
_: &DeleteToBeginningOfLine,
window: &mut Window,
cx: &mut Context<Self>,
) {
let range = 0..self.cursor;
self.replace_range(range, "", window, cx);
}
fn select_all(&mut self, _: &SelectAll, window: &mut Window, cx: &mut Context<Self>) {
let len = self.text.len();
self.select_range(0..len, window, cx);
}
fn copy(&mut self, _: &Copy, window: &mut Window, cx: &mut Context<Self>) {
if self.selection().is_empty() {
// Let an outer handler copy its own selection instead.
cx.propagate();
return;
}
self.copy_selection(window, cx);
}
fn cut(&mut self, _: &Cut, window: &mut Window, cx: &mut Context<Self>) {
// Read-only refuses cut entirely, clipboard included.
if self.read_only {
return;
}
if self.copy_selection(window, cx) {
self.delete_selection(window, cx);
}
}
fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
if let Some(item) = cx.read_from_clipboard()
&& let Some(text) = item.text()
{
let range = self.selection();
self.replace_range(range, &text, window, cx);
}
}
fn undo(&mut self, _: &Undo, window: &mut Window, cx: &mut Context<Self>) {
if self.read_only {
return;
}
let Some(entry) = self.undo_stack.pop() else {
return;
};
let inverse = entry.reverse(&mut self.text);
self.redo_stack.push(inverse);
self.cursor = entry.cursor.min(self.text.len());
self.anchor = entry.anchor.min(self.text.len());
cx.emit(InputStateEvent::TextChanged);
self.notify(window, cx);
}
fn redo(&mut self, _: &Redo, window: &mut Window, cx: &mut Context<Self>) {
if self.read_only {
return;
}
let Some(entry) = self.redo_stack.pop() else {
return;
};
let inverse = entry.reverse(&mut self.text);
self.undo_stack.push(inverse);
// The caret lands at the end of the restored text,
// not where it was before the original edit.
let caret = entry.range.start + entry.old_text.len();
self.cursor = caret.min(self.text.len());
self.anchor = self.cursor;
cx.emit(InputStateEvent::TextChanged);
self.notify(window, cx);
}
fn submit(&mut self, _: &Submit, _window: &mut Window, cx: &mut Context<Self>) {
cx.emit(InputStateEvent::Submitted);
}
fn escape(&mut self, _: &Escape, window: &mut Window, cx: &mut Context<Self>) {
window.blur(cx);
}
}
impl EntityInputHandler for InputState {
@@ -873,6 +1135,10 @@ impl EntityInputHandler for InputState {
window: &mut Window,
cx: &mut Context<Self>,
) {
// Simulated typing delivers named keys' control characters here
if text.chars().all(|ch| ch.is_control()) {
return;
}
let range = range_utf16.map_or_else(
|| self.selection(),
|range| utf16_range_to_utf8(&self.text, range),
@@ -1113,6 +1379,7 @@ impl RenderOnce for Input {
h_flex()
.id(self.id.clone())
.key_context(INPUT_CONTEXT)
.role(Role::TextInput)
.aria_label(fallback_label)
.relative()
@@ -1200,5 +1467,157 @@ impl RenderOnce for Input {
}
})
})
.on_action({
let state = self.state.clone();
move |action: &Escape, window, cx| {
state.update(cx, |state, cx| state.escape(action, window, cx))
}
})
.on_action({
let state = self.state.clone();
move |action: &Submit, window, cx| {
state.update(cx, |state, cx| state.submit(action, window, cx))
}
})
.on_action({
let state = self.state.clone();
move |action: &Backspace, window, cx| {
state.update(cx, |state, cx| state.backspace(action, window, cx))
}
})
.on_action({
let state = self.state.clone();
move |action: &Delete, window, cx| {
state.update(cx, |state, cx| state.delete(action, window, cx))
}
})
.on_action({
let state = self.state.clone();
move |action: &DeleteWordLeft, window, cx| {
state.update(cx, |state, cx| state.delete_word_left(action, window, cx))
}
})
.on_action({
let state = self.state.clone();
move |action: &DeleteWordRight, window, cx| {
state.update(cx, |state, cx| state.delete_word_right(action, window, cx))
}
})
.on_action({
let state = self.state.clone();
move |action: &DeleteToBeginningOfLine, window, cx| {
state.update(cx, |state, cx| {
state.delete_to_beginning_of_line(action, window, cx)
})
}
})
.on_action({
let state = self.state.clone();
move |action: &Left, window, cx| {
state.update(cx, |state, cx| state.left(action, window, cx))
}
})
.on_action({
let state = self.state.clone();
move |action: &Right, window, cx| {
state.update(cx, |state, cx| state.right(action, window, cx))
}
})
.on_action({
let state = self.state.clone();
move |action: &SelectLeft, window, cx| {
state.update(cx, |state, cx| state.select_left(action, window, cx))
}
})
.on_action({
let state = self.state.clone();
move |action: &SelectRight, window, cx| {
state.update(cx, |state, cx| state.select_right(action, window, cx))
}
})
.on_action({
let state = self.state.clone();
move |action: &WordLeft, window, cx| {
state.update(cx, |state, cx| state.word_left(action, window, cx))
}
})
.on_action({
let state = self.state.clone();
move |action: &WordRight, window, cx| {
state.update(cx, |state, cx| state.word_right(action, window, cx))
}
})
.on_action({
let state = self.state.clone();
move |action: &SelectWordLeft, window, cx| {
state.update(cx, |state, cx| state.select_word_left(action, window, cx))
}
})
.on_action({
let state = self.state.clone();
move |action: &SelectWordRight, window, cx| {
state.update(cx, |state, cx| state.select_word_right(action, window, cx))
}
})
.on_action({
let state = self.state.clone();
move |action: &Home, window, cx| {
state.update(cx, |state, cx| state.home(action, window, cx))
}
})
.on_action({
let state = self.state.clone();
move |action: &End, window, cx| {
state.update(cx, |state, cx| state.end(action, window, cx))
}
})
.on_action({
let state = self.state.clone();
move |action: &SelectHome, window, cx| {
state.update(cx, |state, cx| state.select_home(action, window, cx))
}
})
.on_action({
let state = self.state.clone();
move |action: &SelectEnd, window, cx| {
state.update(cx, |state, cx| state.select_end(action, window, cx))
}
})
.on_action({
let state = self.state.clone();
move |action: &SelectAll, window, cx| {
state.update(cx, |state, cx| state.select_all(action, window, cx))
}
})
.on_action({
let state = self.state.clone();
move |action: &Copy, window, cx| {
state.update(cx, |state, cx| state.copy(action, window, cx))
}
})
.on_action({
let state = self.state.clone();
move |action: &Cut, window, cx| {
state.update(cx, |state, cx| state.cut(action, window, cx))
}
})
.on_action({
let state = self.state.clone();
move |action: &Paste, window, cx| {
state.update(cx, |state, cx| state.paste(action, window, cx))
}
})
.on_action({
let state = self.state.clone();
move |action: &Undo, window, cx| {
state.update(cx, |state, cx| state.undo(action, window, cx))
}
})
.on_action({
let state = self.state.clone();
move |action: &Redo, window, cx| {
state.update(cx, |state, cx| state.redo(action, window, cx))
}
})
}
}
+11
View File
@@ -1,4 +1,8 @@
//! The components every application needs, one cargo feature each.
//!
//! Call [`init`] once at startup, before opening windows.
use gpui::App;
pub mod common;
@@ -31,3 +35,10 @@ pub mod avatar;
#[cfg(feature = "input")]
pub mod input;
/// Initializes everything the components need at startup.
pub fn init(cx: &mut App) {
gpui_native_theme::Theme::init(cx);
#[cfg(feature = "input")]
input::bind_input_keys(cx, None);
}