//! Text editors display a multi-line text input for text editing. //! //! # Example //! ```no_run //! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } //! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; //! # //! use iced::widget::text_editor; //! //! struct State { //! content: text_editor::Content, //! } //! //! #[derive(Debug, Clone)] //! enum Message { //! Edit(text_editor::Action) //! } //! //! fn view(state: &State) -> Element<'_, Message> { //! text_editor(&state.content) //! .placeholder("Type something here...") //! .on_action(Message::Edit) //! .into() //! } //! //! fn update(state: &mut State, message: Message) { //! match message { //! Message::Edit(action) => { //! state.content.perform(action); //! } //! } //! } //! ``` use crate::core::alignment; use crate::core::clipboard::{self, Clipboard}; use crate::core::input_method; use crate::core::keyboard; use crate::core::keyboard::key; use crate::core::layout::{self, Layout}; use crate::core::mouse; use crate::core::renderer; use crate::core::text::editor::{Cursor, Editor as _}; use crate::core::text::highlighter::{self, Highlighter}; use crate::core::text::{self, LineHeight, Text, Wrapping}; use crate::core::time::{Duration, Instant}; use crate::core::widget::operation; use crate::core::widget::{self, Widget}; use crate::core::window; use crate::core::{ Background, Border, Color, Element, Event, InputMethod, Length, Padding, Pixels, Point, Rectangle, Shell, Size, SmolStr, Theme, Vector, }; use std::borrow::Cow; use std::cell::RefCell; use std::fmt; use std::ops::DerefMut; use std::ops::Range; use std::sync::Arc; pub use text::editor::{Action, Edit, Line, LineEnding, Motion}; /// A multi-line text input. /// /// # Example /// ```no_run /// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } /// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; /// # /// use iced::widget::text_editor; /// /// struct State { /// content: text_editor::Content, /// } /// /// #[derive(Debug, Clone)] /// enum Message { /// Edit(text_editor::Action) /// } /// /// fn view(state: &State) -> Element<'_, Message> { /// text_editor(&state.content) /// .placeholder("Type something here...") /// .on_action(Message::Edit) /// .into() /// } /// /// fn update(state: &mut State, message: Message) { /// match message { /// Message::Edit(action) => { /// state.content.perform(action); /// } /// } /// } /// ``` #[allow(missing_debug_implementations)] pub struct TextEditor< 'a, Highlighter, Message, Theme = crate::Theme, Renderer = crate::Renderer, > where Highlighter: text::Highlighter, Theme: Catalog, Renderer: text::Renderer, { content: &'a Content, placeholder: Option>, font: Option, text_size: Option, line_height: LineHeight, width: Length, height: Length, min_height: f32, max_height: f32, padding: Padding, wrapping: Wrapping, class: Theme::Class<'a>, key_binding: Option Option> + 'a>>, on_edit: Option Message + 'a>>, highlighter_settings: Highlighter::Settings, highlighter_format: fn( &Highlighter::Highlight, &Theme, ) -> highlighter::Format, last_status: Option, } impl<'a, Message, Theme, Renderer> TextEditor<'a, highlighter::PlainText, Message, Theme, Renderer> where Theme: Catalog, Renderer: text::Renderer, { /// Creates new [`TextEditor`] with the given [`Content`]. pub fn new(content: &'a Content) -> Self { Self { content, placeholder: None, font: None, text_size: None, line_height: LineHeight::default(), width: Length::Fill, height: Length::Shrink, min_height: 0.0, max_height: f32::INFINITY, padding: Padding::new(5.0), wrapping: Wrapping::default(), class: Theme::default(), key_binding: None, on_edit: None, highlighter_settings: (), highlighter_format: |_highlight, _theme| { highlighter::Format::default() }, last_status: None, } } } impl<'a, Highlighter, Message, Theme, Renderer> TextEditor<'a, Highlighter, Message, Theme, Renderer> where Highlighter: text::Highlighter, Theme: Catalog, Renderer: text::Renderer, { /// Sets the placeholder of the [`TextEditor`]. pub fn placeholder( mut self, placeholder: impl text::IntoFragment<'a>, ) -> Self { self.placeholder = Some(placeholder.into_fragment()); self } /// Sets the width of the [`TextEditor`]. pub fn width(mut self, width: impl Into) -> Self { self.width = Length::from(width.into()); self } /// Sets the height of the [`TextEditor`]. pub fn height(mut self, height: impl Into) -> Self { self.height = height.into(); self } /// Sets the minimum height of the [`TextEditor`]. pub fn min_height(mut self, min_height: impl Into) -> Self { self.min_height = min_height.into().0; self } /// Sets the maximum height of the [`TextEditor`]. pub fn max_height(mut self, max_height: impl Into) -> Self { self.max_height = max_height.into().0; self } /// Sets the message that should be produced when some action is performed in /// the [`TextEditor`]. /// /// If this method is not called, the [`TextEditor`] will be disabled. pub fn on_action( mut self, on_edit: impl Fn(Action) -> Message + 'a, ) -> Self { self.on_edit = Some(Box::new(on_edit)); self } /// Sets the [`Font`] of the [`TextEditor`]. /// /// [`Font`]: text::Renderer::Font pub fn font(mut self, font: impl Into) -> Self { self.font = Some(font.into()); self } /// Sets the text size of the [`TextEditor`]. pub fn size(mut self, size: impl Into) -> Self { self.text_size = Some(size.into()); self } /// Sets the [`text::LineHeight`] of the [`TextEditor`]. pub fn line_height( mut self, line_height: impl Into, ) -> Self { self.line_height = line_height.into(); self } /// Sets the [`Padding`] of the [`TextEditor`]. pub fn padding(mut self, padding: impl Into) -> Self { self.padding = padding.into(); self } /// Sets the [`Wrapping`] strategy of the [`TextEditor`]. pub fn wrapping(mut self, wrapping: Wrapping) -> Self { self.wrapping = wrapping; self } /// Highlights the [`TextEditor`] using the given syntax and theme. #[cfg(feature = "highlighter")] pub fn highlight( self, syntax: &str, theme: iced_highlighter::Theme, ) -> TextEditor<'a, iced_highlighter::Highlighter, Message, Theme, Renderer> where Renderer: text::Renderer, { self.highlight_with::( iced_highlighter::Settings { theme, token: syntax.to_owned(), }, |highlight, _theme| highlight.to_format(), ) } /// Highlights the [`TextEditor`] with the given [`Highlighter`] and /// a strategy to turn its highlights into some text format. pub fn highlight_with( self, settings: H::Settings, to_format: fn( &H::Highlight, &Theme, ) -> highlighter::Format, ) -> TextEditor<'a, H, Message, Theme, Renderer> { TextEditor { content: self.content, placeholder: self.placeholder, font: self.font, text_size: self.text_size, line_height: self.line_height, width: self.width, height: self.height, min_height: self.min_height, max_height: self.max_height, padding: self.padding, wrapping: self.wrapping, class: self.class, key_binding: self.key_binding, on_edit: self.on_edit, highlighter_settings: settings, highlighter_format: to_format, last_status: self.last_status, } } /// 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> + '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 where Theme::Class<'a>: From>, { self.class = (Box::new(style) as StyleFn<'a, Theme>).into(); self } /// Sets the style class of the [`TextEditor`]. #[cfg(feature = "advanced")] #[must_use] pub fn class(mut self, class: impl Into>) -> Self { self.class = class.into(); self } fn input_method<'b>( &self, state: &'b State, renderer: &Renderer, layout: Layout<'_>, ) -> InputMethod<&'b str> { let Some(Focus { is_window_focused: true, .. }) = &state.focus else { return InputMethod::Disabled; }; let bounds = layout.bounds(); let internal = self.content.0.borrow_mut(); let text_bounds = bounds.shrink(self.padding); let translation = text_bounds.position() - Point::ORIGIN; let cursor = match internal.editor.cursor() { Cursor::Caret(position) => position, Cursor::Selection(ranges) => { ranges.first().cloned().unwrap_or_default().position() } }; let line_height = self.line_height.to_absolute( self.text_size.unwrap_or_else(|| renderer.default_size()), ); let position = cursor + translation + Vector::new(0.0, f32::from(line_height)); InputMethod::Enabled { position, purpose: input_method::Purpose::Normal, preedit: state.preedit.as_ref().map(input_method::Preedit::as_ref), } } } /// The content of a [`TextEditor`]. pub struct Content(RefCell>) where R: text::Renderer; struct Internal where R: text::Renderer, { editor: R::Editor, is_dirty: bool, } impl Content where R: text::Renderer, { /// Creates an empty [`Content`]. pub fn new() -> Self { Self::with_text("") } /// Creates a [`Content`] with the given text. pub fn with_text(text: &str) -> Self { Self(RefCell::new(Internal { editor: R::Editor::with_text(text), is_dirty: true, })) } /// Performs an [`Action`] on the [`Content`]. pub fn perform(&mut self, action: Action) { let internal = self.0.get_mut(); internal.editor.perform(action); internal.is_dirty = true; } /// Returns the amount of lines of the [`Content`]. pub fn line_count(&self) -> usize { self.0.borrow().editor.line_count() } /// Returns the text of the line at the given index, if it exists. pub fn line(&self, index: usize) -> Option> { let internal = self.0.borrow(); let line = internal.editor.line(index)?; Some(Line { text: Cow::Owned(line.text.into_owned()), ending: line.ending, }) } /// Returns an iterator of the text of the lines in the [`Content`]. pub fn lines(&self) -> impl Iterator> { (0..) .map(|i| self.line(i)) .take_while(Option::is_some) .flatten() } /// Returns the text of the [`Content`]. pub fn text(&self) -> String { let mut contents = String::new(); let mut lines = self.lines().peekable(); while let Some(line) = lines.next() { contents.push_str(&line.text); if lines.peek().is_some() { contents.push_str(if line.ending == LineEnding::None { LineEnding::default().as_str() } else { line.ending.as_str() }); } } contents } /// Returns the kind of [`LineEnding`] used for separating lines in the [`Content`]. pub fn line_ending(&self) -> Option { Some(self.line(0)?.ending) } /// Returns the selected text of the [`Content`]. pub fn selection(&self) -> Option { self.0.borrow().editor.selection() } /// Returns the current cursor position of the [`Content`]. pub fn cursor_position(&self) -> (usize, usize) { self.0.borrow().editor.cursor_position() } } impl Default for Content where Renderer: text::Renderer, { fn default() -> Self { Self::new() } } impl fmt::Debug for Content where Renderer: text::Renderer, Renderer::Editor: fmt::Debug, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let internal = self.0.borrow(); f.debug_struct("Content") .field("editor", &internal.editor) .field("is_dirty", &internal.is_dirty) .finish() } } /// The state of a [`TextEditor`]. #[derive(Debug)] pub struct State { focus: Option, preedit: Option, last_click: Option, drag_click: Option, partial_scroll: f32, highlighter: RefCell, highlighter_settings: Highlighter::Settings, highlighter_format_address: usize, } #[derive(Debug, Clone)] struct Focus { updated_at: Instant, now: Instant, is_window_focused: bool, } impl Focus { const CURSOR_BLINK_INTERVAL_MILLIS: u128 = 500; fn now() -> Self { let now = Instant::now(); Self { updated_at: now, now, is_window_focused: true, } } fn is_cursor_visible(&self) -> bool { self.is_window_focused && ((self.now - self.updated_at).as_millis() / Self::CURSOR_BLINK_INTERVAL_MILLIS) % 2 == 0 } } impl State { /// Returns whether the [`TextEditor`] is currently focused or not. pub fn is_focused(&self) -> bool { self.focus.is_some() } } impl operation::Focusable for State { fn is_focused(&self) -> bool { self.focus.is_some() } fn focus(&mut self) { self.focus = Some(Focus::now()); } fn unfocus(&mut self) { self.focus = None; } } impl Widget for TextEditor<'_, Highlighter, Message, Theme, Renderer> where Highlighter: text::Highlighter, Theme: Catalog, Renderer: text::Renderer, { fn tag(&self) -> widget::tree::Tag { widget::tree::Tag::of::>() } fn state(&self) -> widget::tree::State { widget::tree::State::new(State { focus: None, preedit: None, last_click: None, drag_click: None, partial_scroll: 0.0, highlighter: RefCell::new(Highlighter::new( &self.highlighter_settings, )), highlighter_settings: self.highlighter_settings.clone(), highlighter_format_address: self.highlighter_format as usize, }) } fn size(&self) -> Size { Size { width: self.width, height: self.height, } } fn layout( &self, tree: &mut widget::Tree, renderer: &Renderer, limits: &layout::Limits, ) -> iced_renderer::core::layout::Node { let mut internal = self.content.0.borrow_mut(); let state = tree.state.downcast_mut::>(); if state.highlighter_format_address != self.highlighter_format as usize { state.highlighter.borrow_mut().change_line(0); state.highlighter_format_address = self.highlighter_format as usize; } if state.highlighter_settings != self.highlighter_settings { state .highlighter .borrow_mut() .update(&self.highlighter_settings); state.highlighter_settings = self.highlighter_settings.clone(); } let limits = limits .width(self.width) .height(self.height) .min_height(self.min_height) .max_height(self.max_height); internal.editor.update( limits.shrink(self.padding).max(), self.font.unwrap_or_else(|| renderer.default_font()), self.text_size.unwrap_or_else(|| renderer.default_size()), self.line_height, self.wrapping, state.highlighter.borrow_mut().deref_mut(), ); match self.height { Length::Fill | Length::FillPortion(_) | Length::Fixed(_) => { layout::Node::new(limits.max()) } Length::Shrink => { let min_bounds = internal.editor.min_bounds(); layout::Node::new( limits .height(min_bounds.height) .max() .expand(Size::new(0.0, self.padding.vertical())), ) } } } fn update( &mut self, tree: &mut widget::Tree, event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, _viewport: &Rectangle, ) { let Some(on_edit) = self.on_edit.as_ref() else { return; }; let state = tree.state.downcast_mut::>(); let is_redraw = matches!( event, Event::Window(window::Event::RedrawRequested(_now)), ); match event { Event::Window(window::Event::Unfocused) => { if let Some(focus) = &mut state.focus { focus.is_window_focused = false; } } Event::Window(window::Event::Focused) => { if let Some(focus) = &mut state.focus { focus.is_window_focused = true; focus.updated_at = Instant::now(); shell.request_redraw(); } } Event::Window(window::Event::RedrawRequested(now)) => { if let Some(focus) = &mut state.focus { if focus.is_window_focused { focus.now = *now; let millis_until_redraw = Focus::CURSOR_BLINK_INTERVAL_MILLIS - (focus.now - focus.updated_at).as_millis() % Focus::CURSOR_BLINK_INTERVAL_MILLIS; shell.request_redraw_at( focus.now + Duration::from_millis( millis_until_redraw as u64, ), ); } } } _ => {} } if let Some(update) = Update::from_event( event, state, layout.bounds(), self.padding, cursor, self.key_binding.as_deref(), ) { match update { Update::Click(click) => { let action = match click.kind() { mouse::click::Kind::Single => { Action::Click(click.position()) } mouse::click::Kind::Double => Action::SelectWord, mouse::click::Kind::Triple => Action::SelectLine, }; state.focus = Some(Focus::now()); state.last_click = Some(click); state.drag_click = Some(click.kind()); shell.publish(on_edit(action)); shell.capture_event(); } 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(); if bounds.height >= i32::MAX as f32 { return; } let lines = lines + state.partial_scroll; state.partial_scroll = lines.fract(); shell.publish(on_edit(Action::Scroll { lines: lines as i32, })); shell.capture_event(); } Update::InputMethod(update) => match update { Ime::Toggle(is_open) => { state.preedit = is_open.then(input_method::Preedit::new); shell.request_redraw(); } Ime::Preedit { content, selection } => { state.preedit = Some(input_method::Preedit { content, selection, text_size: self.text_size, }); shell.request_redraw(); } Ime::Commit(text) => { shell.publish(on_edit(Action::Edit(Edit::Paste( Arc::new(text), )))); } }, Update::Binding(binding) => { fn apply_binding< H: text::Highlighter, R: text::Renderer, Message, >( binding: Binding, content: &Content, state: &mut State, 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.focus = None; 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); } } } if !matches!(binding, Binding::Unfocus) { shell.capture_event(); } apply_binding( binding, self.content, state, on_edit, clipboard, shell, ); if let Some(focus) = &mut state.focus { focus.updated_at = Instant::now(); } } } } let status = { let is_disabled = self.on_edit.is_none(); let is_hovered = cursor.is_over(layout.bounds()); if is_disabled { Status::Disabled } else if state.focus.is_some() { Status::Focused { is_hovered } } else if is_hovered { Status::Hovered } else { Status::Active } }; if is_redraw { self.last_status = Some(status); shell.request_input_method( &self.input_method(state, renderer, layout), ); } else if self .last_status .is_some_and(|last_status| status != last_status) { shell.request_redraw(); } } fn draw( &self, tree: &widget::Tree, renderer: &mut Renderer, theme: &Theme, _defaults: &renderer::Style, layout: Layout<'_>, _cursor: mouse::Cursor, _viewport: &Rectangle, ) { let bounds = layout.bounds(); let mut internal = self.content.0.borrow_mut(); let state = tree.state.downcast_ref::>(); let font = self.font.unwrap_or_else(|| renderer.default_font()); internal.editor.highlight( font, state.highlighter.borrow_mut().deref_mut(), |highlight| (self.highlighter_format)(highlight, theme), ); let style = theme .style(&self.class, self.last_status.unwrap_or(Status::Active)); renderer.fill_quad( renderer::Quad { bounds, border: style.border, ..renderer::Quad::default() }, style.background, ); let text_bounds = bounds.shrink(self.padding); if internal.editor.is_empty() { if let Some(placeholder) = self.placeholder.clone() { renderer.fill_text( Text { content: placeholder.into_owned(), bounds: text_bounds.size(), size: self .text_size .unwrap_or_else(|| renderer.default_size()), line_height: self.line_height, font, horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, shaping: text::Shaping::Advanced, wrapping: self.wrapping, }, text_bounds.position(), style.placeholder, text_bounds, ); } } else { renderer.fill_editor( &internal.editor, text_bounds.position(), style.value, text_bounds, ); } let translation = text_bounds.position() - Point::ORIGIN; if let Some(focus) = state.focus.as_ref() { match internal.editor.cursor() { Cursor::Caret(position) if focus.is_cursor_visible() => { let cursor = Rectangle::new( position + translation, Size::new( 1.0, self.line_height .to_absolute(self.text_size.unwrap_or_else( || renderer.default_size(), )) .into(), ), ); if let Some(clipped_cursor) = text_bounds.intersection(&cursor) { renderer.fill_quad( renderer::Quad { bounds: clipped_cursor, ..renderer::Quad::default() }, style.value, ); } } Cursor::Selection(ranges) => { for range in ranges.into_iter().filter_map(|range| { text_bounds.intersection(&(range + translation)) }) { renderer.fill_quad( renderer::Quad { bounds: range, ..renderer::Quad::default() }, style.selection, ); } } Cursor::Caret(_) => {} } } } fn mouse_interaction( &self, _state: &widget::Tree, layout: Layout<'_>, cursor: mouse::Cursor, _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { let is_disabled = self.on_edit.is_none(); if cursor.is_over(layout.bounds()) { if is_disabled { mouse::Interaction::NotAllowed } else { mouse::Interaction::Text } } else { mouse::Interaction::default() } } fn operate( &self, tree: &mut widget::Tree, layout: Layout<'_>, _renderer: &Renderer, operation: &mut dyn widget::Operation, ) { let state = tree.state.downcast_mut::>(); operation.focusable(None, layout.bounds(), state); } } impl<'a, Highlighter, Message, Theme, Renderer> From> for Element<'a, Message, Theme, Renderer> where Highlighter: text::Highlighter, Message: 'a, Theme: Catalog + 'a, Renderer: text::Renderer, { fn from( text_editor: TextEditor<'a, Highlighter, Message, Theme, Renderer>, ) -> Self { Self::new(text_editor) } } /// A binding to an action in the [`TextEditor`]. #[derive(Debug, Clone, PartialEq)] pub enum Binding { /// Unfocus the [`TextEditor`]. Unfocus, /// 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), /// Produce the given message. Custom(Message), } /// 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, /// The current [`Status`] of the [`TextEditor`]. pub status: Status, } impl Binding { /// Returns the default [`Binding`] for the given key press. pub fn from_key_press(event: KeyPress) -> Option { let KeyPress { key, modifiers, text, status, } = event; if !matches!(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) if text.is_none() || text.as_deref() == Some("\u{7f}") => { 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 { Click(mouse::Click), Drag(Point), Release, Scroll(f32), InputMethod(Ime), Binding(Binding), } enum Ime { Toggle(bool), Preedit { content: String, selection: Option>, }, Commit(String), } impl Update { fn from_event( event: &Event, state: &State, bounds: Rectangle, padding: Padding, cursor: mouse::Cursor, key_binding: Option<&dyn Fn(KeyPress) -> Option>>, ) -> Option { let binding = |binding| Some(Update::Binding(binding)); match event { Event::Mouse(event) => match event { mouse::Event::ButtonPressed(mouse::Button::Left) => { if let Some(cursor_position) = cursor.position_in(bounds) { let cursor_position = cursor_position - Vector::new(padding.top, padding.left); let click = mouse::Click::new( cursor_position, mouse::Button::Left, state.last_click, ); Some(Update::Click(click)) } else if state.focus.is_some() { binding(Binding::Unfocus) } else { None } } mouse::Event::ButtonReleased(mouse::Button::Left) => { Some(Update::Release) } mouse::Event::CursorMoved { .. } => match state.drag_click { Some(mouse::click::Kind::Single) => { let cursor_position = cursor.position_in(bounds)? - Vector::new(padding.top, padding.left); Some(Update::Drag(cursor_position)) } _ => None, }, mouse::Event::WheelScrolled { delta } if cursor.is_over(bounds) => { Some(Update::Scroll(match delta { mouse::ScrollDelta::Lines { y, .. } => { if y.abs() > 0.0 { y.signum() * -(y.abs() * 4.0).max(1.0) } else { 0.0 } } mouse::ScrollDelta::Pixels { y, .. } => -y / 4.0, })) } _ => None, }, Event::InputMethod(event) => match event { input_method::Event::Opened | input_method::Event::Closed => { Some(Update::InputMethod(Ime::Toggle(matches!( event, input_method::Event::Opened )))) } input_method::Event::Preedit(content, selection) if state.focus.is_some() => { Some(Update::InputMethod(Ime::Preedit { content: content.clone(), selection: selection.clone(), })) } input_method::Event::Commit(content) if state.focus.is_some() => { Some(Update::InputMethod(Ime::Commit(content.clone()))) } _ => None, }, Event::Keyboard(keyboard::Event::KeyPressed { key, modifiers, text, .. }) => { let status = if state.focus.is_some() { Status::Focused { is_hovered: cursor.is_over(bounds), } } else { Status::Active }; let key_press = KeyPress { key: key.clone(), modifiers: *modifiers, text: text.clone(), status, }; if let Some(key_binding) = key_binding { key_binding(key_press) } else { Binding::from_key_press(key_press) } .map(Self::Binding) } _ => None, } } } fn motion(key: key::Named) -> Option { match key { key::Named::ArrowLeft => Some(Motion::Left), key::Named::ArrowRight => Some(Motion::Right), key::Named::ArrowUp => Some(Motion::Up), key::Named::ArrowDown => Some(Motion::Down), key::Named::Home => Some(Motion::Home), key::Named::End => Some(Motion::End), key::Named::PageUp => Some(Motion::PageUp), key::Named::PageDown => Some(Motion::PageDown), _ => None, } } /// The possible status of a [`TextEditor`]. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Status { /// The [`TextEditor`] can be interacted with. Active, /// The [`TextEditor`] is being hovered. Hovered, /// The [`TextEditor`] is focused. Focused { /// Whether the [`TextEditor`] is hovered, while focused. is_hovered: bool, }, /// The [`TextEditor`] cannot be interacted with. Disabled, } /// The appearance of a text input. #[derive(Debug, Clone, Copy, PartialEq)] pub struct Style { /// The [`Background`] of the text input. pub background: Background, /// The [`Border`] of the text input. pub border: Border, /// The [`Color`] of the icon of the text input. pub icon: Color, /// The [`Color`] of the placeholder of the text input. pub placeholder: Color, /// The [`Color`] of the value of the text input. pub value: Color, /// The [`Color`] of the selection of the text input. pub selection: Color, } /// The theme catalog of a [`TextEditor`]. pub trait Catalog { /// The item class of the [`Catalog`]. type Class<'a>; /// The default class produced by the [`Catalog`]. fn default<'a>() -> Self::Class<'a>; /// The [`Style`] of a class with the given status. fn style(&self, class: &Self::Class<'_>, status: Status) -> Style; } /// A styling function for a [`TextEditor`]. pub type StyleFn<'a, Theme> = Box Style + 'a>; impl Catalog for Theme { type Class<'a> = StyleFn<'a, Self>; fn default<'a>() -> Self::Class<'a> { Box::new(default) } fn style(&self, class: &Self::Class<'_>, status: Status) -> Style { class(self, status) } } /// The default style of a [`TextEditor`]. pub fn default(theme: &Theme, status: Status) -> Style { let palette = theme.extended_palette(); let active = Style { background: Background::Color(palette.background.base.color), border: Border { radius: 2.0.into(), width: 1.0, color: palette.background.strong.color, }, icon: palette.background.weak.text, placeholder: palette.background.strong.color, value: palette.background.base.text, selection: palette.primary.weak.color, }; match status { Status::Active => active, Status::Hovered => Style { border: Border { color: palette.background.base.text, ..active.border }, ..active }, Status::Focused { .. } => Style { border: Border { color: palette.primary.strong.color, ..active.border }, ..active }, Status::Disabled => Style { background: Background::Color(palette.background.weak.color), value: active.placeholder, ..active }, } }