//! 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::event::{self, Event}; 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, Length, Padding, Pixels, Point, Rectangle, Shell, Size, SmolStr, Theme, Vector, }; use std::cell::RefCell; use std::fmt; use std::ops::DerefMut; use std::sync::Arc; pub use text::editor::{Action, Edit, 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, 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, } 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, 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() }, } } } 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 height of the [`TextEditor`]. pub fn height(mut self, height: impl Into) -> Self { self.height = height.into(); 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 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, 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, } } /// 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 } } /// 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 + '_> { std::cell::Ref::filter_map(self.0.borrow(), |internal| { internal.editor.line(index) }) .ok() } /// Returns an iterator of the text of the lines in the [`Content`]. pub fn lines( &self, ) -> impl Iterator + '_> { struct Lines<'a, Renderer: text::Renderer> { internal: std::cell::Ref<'a, Internal>, current: usize, } impl<'a, Renderer: text::Renderer> Iterator for Lines<'a, Renderer> { type Item = std::cell::Ref<'a, str>; fn next(&mut self) -> Option { let line = std::cell::Ref::filter_map( std::cell::Ref::clone(&self.internal), |internal| internal.editor.line(self.current), ) .ok()?; self.current += 1; Some(line) } } Lines { internal: self.0.borrow(), current: 0, } } /// Returns the text of the [`Content`]. /// /// Lines are joined with `'\n'`. pub fn text(&self) -> String { let mut text = self.lines().enumerate().fold( String::new(), |mut contents, (i, line)| { if i > 0 { contents.push('\n'); } contents.push_str(&line); contents }, ); if !text.ends_with('\n') { text.push('\n'); } text } /// 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, last_click: Option, drag_click: Option, partial_scroll: f32, highlighter: RefCell, highlighter_settings: Highlighter::Settings, highlighter_format_address: usize, } #[derive(Debug, Clone, Copy)] 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<'a, Highlighter, Message, Theme, Renderer> Widget for TextEditor<'a, 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, 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); 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 on_event( &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, ) -> event::Status { let Some(on_edit) = self.on_edit.as_ref() else { return event::Status::Ignored; }; let state = tree.state.downcast_mut::>(); 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(window::RedrawRequest::NextFrame); } } 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 - (now - focus.updated_at).as_millis() % Focus::CURSOR_BLINK_INTERVAL_MILLIS; shell.request_redraw(window::RedrawRequest::At( now + Duration::from_millis( millis_until_redraw as u64, ), )); } } } _ => {} } let Some(update) = Update::from_event( event, state, layout.bounds(), self.padding, cursor, self.key_binding.as_deref(), ) else { return event::Status::Ignored; }; 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)); } 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 event::Status::Ignored; } let lines = lines + state.partial_scroll; state.partial_scroll = lines.fract(); shell.publish(on_edit(Action::Scroll { lines: lines as i32, })); } 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); } } } apply_binding( binding, self.content, state, on_edit, clipboard, shell, ); if let Some(focus) = &mut state.focus { focus.updated_at = Instant::now(); } } } event::Status::Captured } 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 is_disabled = self.on_edit.is_none(); let is_mouse_over = cursor.is_over(bounds); let status = if is_disabled { Status::Disabled } else if state.focus.is_some() { Status::Focused } else if is_mouse_over { Status::Hovered } else { Status::Active }; let style = theme.style(&self.class, status); 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(state, None); } } 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 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() => { 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), Binding(Binding), } 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::Keyboard(keyboard::Event::KeyPressed { key, modifiers, text, .. }) => { let status = if state.focus.is_some() { Status::Focused } else { Status::Active }; let key_press = KeyPress { key, modifiers, text, 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, /// The [`TextEditor`] cannot be interacted with. Disabled, } /// The appearance of a text input. #[derive(Debug, Clone, Copy)] 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 }, } }