//! Display a multi-line text input for text editing. use crate::core::event::{self, Event}; use crate::core::keyboard; 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}; use crate::core::widget::{self, Widget}; use crate::core::{ Clipboard, Color, Element, Length, Padding, Pixels, Rectangle, Shell, Vector, }; use std::cell::RefCell; use std::fmt; use std::ops::DerefMut; use std::sync::Arc; pub use crate::style::text_editor::{Appearance, StyleSheet}; pub use text::editor::{Action, Edit, Motion}; /// A multi-line text input. #[allow(missing_debug_implementations)] pub struct TextEditor<'a, Highlighter, Message, Renderer = crate::Renderer> where Highlighter: text::Highlighter, Renderer: text::Renderer, Renderer::Theme: StyleSheet, { content: &'a Content, font: Option, text_size: Option, line_height: LineHeight, width: Length, height: Length, padding: Padding, style: ::Style, on_edit: Option Message + 'a>>, highlighter_settings: Highlighter::Settings, highlighter_format: fn( &Highlighter::Highlight, &Renderer::Theme, ) -> highlighter::Format, } impl<'a, Message, Renderer> TextEditor<'a, highlighter::PlainText, Message, Renderer> where Renderer: text::Renderer, Renderer::Theme: StyleSheet, { /// Creates new [`TextEditor`] with the given [`Content`]. pub fn new(content: &'a Content) -> Self { Self { content, font: None, text_size: None, line_height: LineHeight::default(), width: Length::Fill, height: Length::Fill, padding: Padding::new(5.0), style: Default::default(), on_edit: None, highlighter_settings: (), highlighter_format: |_highlight, _theme| { highlighter::Format::default() }, } } } impl<'a, Highlighter, Message, Renderer> TextEditor<'a, Highlighter, Message, Renderer> where Highlighter: text::Highlighter, Renderer: text::Renderer, Renderer::Theme: StyleSheet, { /// 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 [`Padding`] of the [`TextEditor`]. pub fn padding(mut self, padding: impl Into) -> Self { self.padding = padding.into(); self } /// Highlights the [`TextEditor`] with the given [`Highlighter`] and /// a strategy to turn its highlights into some text format. pub fn highlight( self, settings: H::Settings, to_format: fn( &H::Highlight, &Renderer::Theme, ) -> highlighter::Format, ) -> TextEditor<'a, H, Message, Renderer> { TextEditor { content: self.content, font: self.font, text_size: self.text_size, line_height: self.line_height, width: self.width, height: self.height, padding: self.padding, style: self.style, on_edit: self.on_edit, highlighter_settings: settings, highlighter_format: to_format, } } } /// 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() } } struct State { is_focused: bool, last_click: Option, drag_click: Option, highlighter: RefCell, highlighter_settings: Highlighter::Settings, highlighter_format_address: usize, } impl<'a, Highlighter, Message, Renderer> Widget for TextEditor<'a, Highlighter, Message, Renderer> where Highlighter: text::Highlighter, Renderer: text::Renderer, Renderer::Theme: StyleSheet, { fn tag(&self) -> widget::tree::Tag { widget::tree::Tag::of::>() } fn state(&self) -> widget::tree::State { widget::tree::State::new(State { is_focused: false, last_click: None, drag_click: None, highlighter: RefCell::new(Highlighter::new( &self.highlighter_settings, )), highlighter_settings: self.highlighter_settings.clone(), highlighter_format_address: self.highlighter_format as usize, }) } fn width(&self) -> Length { self.width } fn height(&self) -> Length { 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(); } internal.editor.update( limits.pad(self.padding).max(), self.font.unwrap_or_else(|| renderer.default_font()), self.text_size.unwrap_or_else(|| renderer.default_size()), self.line_height, state.highlighter.borrow_mut().deref_mut(), ); layout::Node::new(limits.max()) } 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::>(); let Some(update) = Update::from_event( event, state, layout.bounds(), self.padding, cursor, ) 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.is_focused = true; state.last_click = Some(click); state.drag_click = Some(click.kind()); shell.publish(on_edit(action)); } 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(selection); } } Update::Paste => { if let Some(contents) = clipboard.read() { shell.publish(on_edit(Action::Edit(Edit::Paste( Arc::new(contents), )))); } } } event::Status::Captured } fn draw( &self, tree: &widget::Tree, renderer: &mut Renderer, theme: &::Theme, style: &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::>(); internal.editor.highlight( self.font.unwrap_or_else(|| renderer.default_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 appearance = if is_disabled { theme.disabled(&self.style) } else if state.is_focused { theme.focused(&self.style) } else if is_mouse_over { theme.hovered(&self.style) } else { theme.active(&self.style) }; renderer.fill_quad( renderer::Quad { bounds, border_radius: appearance.border_radius, border_width: appearance.border_width, border_color: appearance.border_color, }, appearance.background, ); renderer.fill_editor( &internal.editor, bounds.position() + Vector::new(self.padding.left, self.padding.top), style.text_color, ); let translation = Vector::new( bounds.x + self.padding.left, bounds.y + self.padding.top, ); if state.is_focused { match internal.editor.cursor() { Cursor::Caret(position) => { let position = position + translation; if bounds.contains(position) { renderer.fill_quad( renderer::Quad { bounds: Rectangle { x: position.x, y: position.y, width: 1.0, height: self .line_height .to_absolute( self.text_size.unwrap_or_else( || renderer.default_size(), ), ) .into(), }, border_radius: 0.0.into(), border_width: 0.0, border_color: Color::TRANSPARENT, }, theme.value_color(&self.style), ); } } Cursor::Selection(ranges) => { for range in ranges.into_iter().filter_map(|range| { bounds.intersection(&(range + translation)) }) { renderer.fill_quad( renderer::Quad { bounds: range, border_radius: 0.0.into(), border_width: 0.0, border_color: Color::TRANSPARENT, }, theme.selection_color(&self.style), ); } } } } } 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() } } } impl<'a, Highlighter, Message, Renderer> From> for Element<'a, Message, Renderer> where Highlighter: text::Highlighter, Message: 'a, Renderer: text::Renderer, Renderer::Theme: StyleSheet, { fn from( text_editor: TextEditor<'a, Highlighter, Message, Renderer>, ) -> Self { Self::new(text_editor) } } enum Update { Click(mouse::Click), Unfocus, Release, Action(Action), Copy, Paste, } impl Update { fn from_event( event: Event, state: &State, bounds: Rectangle, padding: Padding, cursor: mouse::Cursor, ) -> Option { let action = |action| Some(Update::Action(action)); let edit = |edit| action(Action::Edit(edit)); 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, state.last_click, ); Some(Update::Click(click)) } else if state.is_focused { Some(Update::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); action(Action::Drag(cursor_position)) } _ => None, }, mouse::Event::WheelScrolled { delta } if cursor.is_over(bounds) => { action(Action::Scroll { lines: match delta { mouse::ScrollDelta::Lines { y, .. } => { if y.abs() > 0.0 { (y.signum() * -(y.abs() * 4.0).max(1.0)) as i32 } else { 0 } } mouse::ScrollDelta::Pixels { y, .. } => { (-y / 4.0) as i32 } }, }) } _ => None, }, Event::Keyboard(event) => match event { keyboard::Event::KeyPressed { key_code, modifiers, } if state.is_focused => { if let Some(motion) = motion(key_code) { let motion = if platform::is_jump_modifier_pressed(modifiers) { motion.widen() } else { motion }; return action(if modifiers.shift() { Action::Select(motion) } else { Action::Move(motion) }); } match key_code { keyboard::KeyCode::Enter => edit(Edit::Enter), keyboard::KeyCode::Backspace => edit(Edit::Backspace), keyboard::KeyCode::Delete => edit(Edit::Delete), keyboard::KeyCode::Escape => Some(Self::Unfocus), keyboard::KeyCode::C if modifiers.command() => { Some(Self::Copy) } keyboard::KeyCode::V if modifiers.command() && !modifiers.alt() => { Some(Self::Paste) } _ => None, } } keyboard::Event::CharacterReceived(c) if state.is_focused => { edit(Edit::Insert(c)) } _ => None, }, _ => None, } } } fn motion(key_code: keyboard::KeyCode) -> Option { match key_code { keyboard::KeyCode::Left => Some(Motion::Left), keyboard::KeyCode::Right => Some(Motion::Right), keyboard::KeyCode::Up => Some(Motion::Up), keyboard::KeyCode::Down => Some(Motion::Down), keyboard::KeyCode::Home => Some(Motion::Home), keyboard::KeyCode::End => Some(Motion::End), keyboard::KeyCode::PageUp => Some(Motion::PageUp), keyboard::KeyCode::PageDown => Some(Motion::PageDown), _ => None, } } mod platform { use crate::core::keyboard; pub fn is_jump_modifier_pressed(modifiers: keyboard::Modifiers) -> bool { if cfg!(target_os = "macos") { modifiers.alt() } else { modifiers.control() } } }