diff options
author | 2024-07-26 10:28:23 +0200 | |
---|---|---|
committer | 2024-07-26 11:02:04 +0200 | |
commit | 28d8b73846f49d23790e8c91c31fd2168014a6a4 (patch) | |
tree | 8955599a5ea79438a4fb238be7549c2c8a432d70 /widget | |
parent | 555ee3e9c66010c9a90c3ef55d61fbffd48e669d (diff) | |
download | iced-28d8b73846f49d23790e8c91c31fd2168014a6a4.tar.gz iced-28d8b73846f49d23790e8c91c31fd2168014a6a4.tar.bz2 iced-28d8b73846f49d23790e8c91c31fd2168014a6a4.zip |
Implement custom key binding support for `text_editor`
Diffstat (limited to 'widget')
-rw-r--r-- | widget/src/text_editor.rs | 390 |
1 files changed, 272 insertions, 118 deletions
diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 4871a0f5..fa7863da 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -13,8 +13,8 @@ use crate::core::text::{self, LineHeight, Text}; use crate::core::widget::operation; use crate::core::widget::{self, Widget}; use crate::core::{ - Background, Border, Color, Element, Length, Padding, Pixels, Rectangle, - Shell, Size, Theme, Vector, + Background, Border, Color, Element, Length, Padding, Pixels, Point, + Rectangle, Shell, Size, SmolStr, Theme, Vector, }; use std::cell::RefCell; @@ -46,6 +46,7 @@ pub struct TextEditor< height: Length, padding: Padding, class: Theme::Class<'a>, + key_binding: Option<Box<dyn Fn(KeyPress) -> Option<Binding<Message>> + 'a>>, on_edit: Option<Box<dyn Fn(Action) -> Message + 'a>>, highlighter_settings: Highlighter::Settings, highlighter_format: fn( @@ -72,6 +73,7 @@ where height: Length::Shrink, padding: Padding::new(5.0), class: Theme::default(), + key_binding: None, on_edit: None, highlighter_settings: (), highlighter_format: |_highlight, _theme| { @@ -164,12 +166,24 @@ where height: self.height, padding: self.padding, class: self.class, + key_binding: self.key_binding, on_edit: self.on_edit, highlighter_settings: settings, highlighter_format: to_format, } } + /// Sets the closure to produce key bindings on key presses. + /// + /// See [`Binding`] for the list of available bindings. + pub fn key_binding( + mut self, + key_binding: impl Fn(KeyPress) -> Option<Binding<Message>> + 'a, + ) -> Self { + self.key_binding = Some(Box::new(key_binding)); + self + } + /// Sets the style of the [`TextEditor`]. #[must_use] pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self @@ -475,6 +489,7 @@ where layout.bounds(), self.padding, cursor, + self.key_binding.as_deref(), ) else { return event::Status::Ignored; }; @@ -495,6 +510,12 @@ where shell.publish(on_edit(action)); } + Update::Drag(position) => { + shell.publish(on_edit(Action::Drag(position))); + } + Update::Release => { + state.drag_click = None; + } Update::Scroll(lines) => { let bounds = self.content.0.borrow().editor.bounds(); @@ -509,35 +530,102 @@ where lines: lines as i32, })); } - Update::Unfocus => { - state.is_focused = false; - state.drag_click = None; - } - Update::Release => { - state.drag_click = None; - } - Update::Action(action) => { - shell.publish(on_edit(action)); - } - Update::Copy => { - if let Some(selection) = self.content.selection() { - clipboard.write(clipboard::Kind::Standard, selection); - } - } - Update::Cut => { - if let Some(selection) = self.content.selection() { - clipboard.write(clipboard::Kind::Standard, selection); - shell.publish(on_edit(Action::Edit(Edit::Delete))); - } - } - Update::Paste => { - if let Some(contents) = - clipboard.read(clipboard::Kind::Standard) - { - shell.publish(on_edit(Action::Edit(Edit::Paste( - Arc::new(contents), - )))); + Update::Binding(binding) => { + fn apply_binding< + H: text::Highlighter, + R: text::Renderer, + Message, + >( + binding: Binding<Message>, + content: &Content<R>, + state: &mut State<H>, + on_edit: &dyn Fn(Action) -> Message, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) { + let mut publish = |action| shell.publish(on_edit(action)); + + match binding { + Binding::Unfocus => { + state.is_focused = false; + state.drag_click = None; + } + Binding::Copy => { + if let Some(selection) = content.selection() { + clipboard.write( + clipboard::Kind::Standard, + selection, + ); + } + } + Binding::Cut => { + if let Some(selection) = content.selection() { + clipboard.write( + clipboard::Kind::Standard, + selection, + ); + + publish(Action::Edit(Edit::Delete)); + } + } + Binding::Paste => { + if let Some(contents) = + clipboard.read(clipboard::Kind::Standard) + { + publish(Action::Edit(Edit::Paste(Arc::new( + contents, + )))); + } + } + Binding::Move(motion) => { + publish(Action::Move(motion)); + } + Binding::Select(motion) => { + publish(Action::Select(motion)); + } + Binding::SelectWord => { + publish(Action::SelectWord); + } + Binding::SelectLine => { + publish(Action::SelectLine); + } + Binding::SelectAll => { + publish(Action::SelectAll); + } + Binding::Insert(c) => { + publish(Action::Edit(Edit::Insert(c))); + } + Binding::Enter => { + publish(Action::Edit(Edit::Enter)); + } + Binding::Backspace => { + publish(Action::Edit(Edit::Backspace)); + } + Binding::Delete => { + publish(Action::Edit(Edit::Delete)); + } + Binding::Sequence(sequence) => { + for binding in sequence { + apply_binding( + binding, content, state, on_edit, + clipboard, shell, + ); + } + } + Binding::Custom(message) => { + shell.publish(message); + } + } } + + apply_binding( + binding, + self.content, + state, + on_edit, + clipboard, + shell, + ); } } @@ -731,27 +819,144 @@ where } } -enum Update { - Click(mouse::Click), - Scroll(f32), +/// A binding to an action in the [`TextEditor`]. +#[derive(Debug, Clone, PartialEq)] +pub enum Binding<Message> { + /// Unfocus the [`TextEditor`]. Unfocus, - Release, - Action(Action), + /// Copy the selection of the [`TextEditor`]. Copy, + /// Cut the selection of the [`TextEditor`]. Cut, + /// Paste the clipboard contents in the [`TextEditor`]. Paste, + /// Apply a [`Motion`]. + Move(Motion), + /// Select text with a given [`Motion`]. + Select(Motion), + /// Select the word at the current cursor. + SelectWord, + /// Select the line at the current cursor. + SelectLine, + /// Select the entire buffer. + SelectAll, + /// Insert the given character. + Insert(char), + /// Break the current line. + Enter, + /// Delete the previous character. + Backspace, + /// Delete the next character. + Delete, + /// A sequence of bindings to execute. + Sequence(Vec<Self>), + /// Produce the given message. + Custom(Message), } -impl Update { +/// A key press. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct KeyPress { + /// The key pressed. + pub key: keyboard::Key, + /// The state of the keyboard modifiers. + pub modifiers: keyboard::Modifiers, + /// The text produced by the key press. + pub text: Option<SmolStr>, + /// The current [`Status`] of the [`TextEditor`]. + pub status: Status, +} + +impl<Message> Binding<Message> { + /// Returns the default [`Binding`] for the given key press. + pub fn from_key_press(event: KeyPress) -> Option<Self> { + let KeyPress { + key, + modifiers, + text, + status, + } = event; + + if status != Status::Focused { + return None; + } + + match key.as_ref() { + keyboard::Key::Named(key::Named::Enter) => Some(Self::Enter), + keyboard::Key::Named(key::Named::Backspace) => { + Some(Self::Backspace) + } + keyboard::Key::Named(key::Named::Delete) => Some(Self::Delete), + keyboard::Key::Named(key::Named::Escape) => Some(Self::Unfocus), + keyboard::Key::Character("c") if modifiers.command() => { + Some(Self::Copy) + } + keyboard::Key::Character("x") if modifiers.command() => { + Some(Self::Cut) + } + keyboard::Key::Character("v") + if modifiers.command() && !modifiers.alt() => + { + Some(Self::Paste) + } + keyboard::Key::Character("a") if modifiers.command() => { + Some(Self::SelectAll) + } + _ => { + if let Some(text) = text { + let c = text.chars().find(|c| !c.is_control())?; + + Some(Self::Insert(c)) + } else if let keyboard::Key::Named(named_key) = key.as_ref() { + let motion = motion(named_key)?; + + let motion = if modifiers.macos_command() { + match motion { + Motion::Left => Motion::Home, + Motion::Right => Motion::End, + _ => motion, + } + } else { + motion + }; + + let motion = if modifiers.jump() { + motion.widen() + } else { + motion + }; + + Some(if modifiers.shift() { + Self::Select(motion) + } else { + Self::Move(motion) + }) + } else { + None + } + } + } + } +} + +enum Update<Message> { + Click(mouse::Click), + Drag(Point), + Release, + Scroll(f32), + Binding(Binding<Message>), +} + +impl<Message> Update<Message> { fn from_event<H: Highlighter>( event: Event, state: &State<H>, bounds: Rectangle, padding: Padding, cursor: mouse::Cursor, + key_binding: Option<&dyn Fn(KeyPress) -> Option<Binding<Message>>>, ) -> Option<Self> { - let action = |action| Some(Update::Action(action)); - let edit = |edit| action(Action::Edit(edit)); + let binding = |binding| Some(Update::Binding(binding)); match event { Event::Mouse(event) => match event { @@ -767,7 +972,7 @@ impl Update { Some(Update::Click(click)) } else if state.is_focused { - Some(Update::Unfocus) + binding(Binding::Unfocus) } else { None } @@ -780,7 +985,7 @@ impl Update { let cursor_position = cursor.position_in(bounds)? - Vector::new(padding.top, padding.left); - action(Action::Drag(cursor_position)) + Some(Update::Drag(cursor_position)) } _ => None, }, @@ -800,86 +1005,35 @@ impl Update { } _ => None, }, - Event::Keyboard(event) => match event { - keyboard::Event::KeyPressed { - key, - modifiers, - text, - .. - } if state.is_focused => { - match key.as_ref() { - keyboard::Key::Named(key::Named::Enter) => { - return edit(Edit::Enter); - } - keyboard::Key::Named(key::Named::Backspace) => { - return edit(Edit::Backspace); - } - keyboard::Key::Named(key::Named::Delete) => { - return edit(Edit::Delete); - } - keyboard::Key::Named(key::Named::Escape) => { - return Some(Self::Unfocus); - } - keyboard::Key::Character("c") - if modifiers.command() => - { - return Some(Self::Copy); - } - keyboard::Key::Character("x") - if modifiers.command() => - { - return Some(Self::Cut); - } - keyboard::Key::Character("v") - if modifiers.command() && !modifiers.alt() => - { - return Some(Self::Paste); - } - keyboard::Key::Character("a") - if modifiers.command() => - { - return Some(Self::Action(Action::SelectAll)); - } - _ => {} - } - - if let Some(text) = text { - if let Some(c) = text.chars().find(|c| !c.is_control()) - { - return edit(Edit::Insert(c)); - } - } - - if let keyboard::Key::Named(named_key) = key.as_ref() { - if let Some(motion) = motion(named_key) { - let motion = if modifiers.macos_command() { - match motion { - Motion::Left => Motion::Home, - Motion::Right => Motion::End, - _ => motion, - } - } else { - motion - }; - - let motion = if modifiers.jump() { - motion.widen() - } else { - motion - }; - - return action(if modifiers.shift() { - Action::Select(motion) - } else { - Action::Move(motion) - }); - } - } + Event::Keyboard(keyboard::Event::KeyPressed { + key, + modifiers, + text, + .. + }) => { + let status = if state.is_focused { + Status::Focused + } else { + Status::Active + }; - None + if let Some(key_binding) = key_binding { + key_binding(KeyPress { + key, + modifiers, + text, + status, + }) + } else { + Binding::from_key_press(KeyPress { + key, + modifiers, + text, + status, + }) } - _ => None, - }, + .map(Self::Binding) + } _ => None, } } |