//! 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 iced_wgpu::core::alignment; use iced_wgpu::core::clipboard::{self, Clipboard}; use iced_wgpu::core::input_method; use iced_wgpu::core::keyboard; use iced_wgpu::core::keyboard::key; use iced_wgpu::core::layout::{self, Layout}; use iced_wgpu::core::mouse; use iced_wgpu::core::renderer::{self, Renderer as _}; use iced_wgpu::core::text::editor::Editor as _; use iced_wgpu::core::text::Renderer as _; use iced_wgpu::core::text::highlighter::{self, Highlighter}; use iced_wgpu::core::text::paragraph::Paragraph as _; use iced_wgpu::core::text::{self, LineHeight, Span, Text, Wrapping}; use iced_wgpu::core::theme; use iced_wgpu::core::time::{Duration, Instant}; use iced_wgpu::core::widget::operation; use iced_wgpu::core::widget::{self, Widget}; use iced_wgpu::core::window; use iced_wgpu::core::{ Background, Border, Color, Element, Event, Font, 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; use std::ops::DerefMut; use std::sync::Arc; pub use text::editor::{ Action, Cursor, Edit, Line, LineEnding, Motion, Position, Selection, }; /// An anchored child element rendered at a line boundary within the text widget. /// The caller builds these using existing rendering code; the widget just draws them in order. pub struct AnchoredItem<'a, Message, Theme = iced_wgpu::core::Theme> { pub after_line: usize, pub height: f32, pub element: Element<'a, Message, Theme, iced_wgpu::Renderer>, } /// Walk the content stream (text lines + anchored items) and map widget-space y to text-space y. fn stream_y_to_text_y(y: f32, items: &[AnchoredItem<'_, M, T>], line_h: f32, line_count: usize) -> f32 { let mut text_y = 0.0f32; let mut widget_y = 0.0f32; let mut item_idx = 0; for line in 0..line_count { if y < widget_y + line_h { return text_y + (y - widget_y); } text_y += line_h; widget_y += line_h; while item_idx < items.len() && items[item_idx].after_line == line { let ih = items[item_idx].height; if y < widget_y + ih { return text_y; } widget_y += ih; item_idx += 1; } } text_y + (y - widget_y).max(0.0) } /// Cumulative height of anchored items before a given text line. fn items_height_before_line(items: &[AnchoredItem<'_, M, T>], line: usize) -> f32 { items.iter() .filter(|it| it.after_line < line) .map(|it| it.height) .sum() } /// Total height of all anchored items. fn total_items_height(items: &[AnchoredItem<'_, M, T>]) -> f32 { items.iter().map(|it| it.height).sum() } /// Build iced Spans from a LayoutRun's glyphs, grouping consecutive glyphs by color. /// `font_size_px` drives perceptual brightness compensation against the /// dark-theme background — see `oklab::lighten_for_size`. fn build_color_spans<'a>( text: &'a str, glyphs: &[cosmic_text::LayoutGlyph], font_size_px: f32, ) -> Vec> { fn cosmic_to_iced(c: cosmic_text::Color, font_size_px: f32) -> Color { let raw = Color::from_rgba8(c.r(), c.g(), c.b(), c.a() as f32 / 255.0); crate::oklab::lighten_for_size(raw, font_size_px) } if glyphs.is_empty() { return vec![Span::new(text)]; } let mut spans = Vec::new(); let mut seg_start = 0usize; let mut cur_color: Option = glyphs.first().and_then(|g| g.color_opt); for glyph in glyphs { if glyph.color_opt != cur_color { let end = glyph.start.min(text.len()); if end > seg_start { let mut span = Span::new(&text[seg_start..end]); if let Some(c) = cur_color { span = span.color(cosmic_to_iced(c, font_size_px)); } spans.push(span); } seg_start = end; cur_color = glyph.color_opt; } } if seg_start < text.len() { let mut span = Span::new(&text[seg_start..]); if let Some(c) = cur_color { span = span.color(cosmic_to_iced(c, font_size_px)); } spans.push(span); } if spans.is_empty() { spans.push(Span::new(text)); } spans } /// 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); /// } /// } /// } /// ``` pub struct TextEditor< 'a, Highlighter, Message, Theme = iced_wgpu::core::Theme, > where Highlighter: text::Highlighter, Theme: Catalog, { id: Option, 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, // Acord extensions anchored_children: Vec>, gutter_offset: usize, is_focused_block: bool, } impl<'a, Message, Theme> TextEditor<'a, highlighter::PlainText, Message, Theme> where Theme: Catalog, { /// Creates new [`TextEditor`] with the given [`Content`]. pub fn new(content: &'a Content) -> Self { Self { id: None, 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: ::default(), key_binding: None, on_edit: None, highlighter_settings: (), highlighter_format: |_highlight, _theme| { highlighter::Format::default() }, last_status: None, anchored_children: Vec::new(), gutter_offset: 0, is_focused_block: false, } } /// Sets the [`Id`](widget::Id) of the [`TextEditor`]. pub fn id(mut self, id: impl Into) -> Self { self.id = Some(id.into()); self } } impl<'a, Highlighter, Message, Theme> TextEditor<'a, Highlighter, Message, Theme> where Highlighter: text::Highlighter, Theme: Catalog, { /// 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`]. /// 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`] 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> { TextEditor { id: self.id, 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, anchored_children: self.anchored_children, gutter_offset: self.gutter_offset, is_focused_block: self.is_focused_block, } } /// 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`]. #[must_use] pub fn class(mut self, class: impl Into>) -> Self { self.class = class.into(); self } /// Sets the anchored child elements to draw at line boundaries. /// Items must be sorted by after_line. pub fn anchored(mut self, items: Vec>) -> Self { self.anchored_children = items; self } /// Sets the global line offset for gutter numbering. pub fn gutter_offset(mut self, offset: usize) -> Self { self.gutter_offset = offset; self } /// Marks this widget as the focused editing block. pub fn focused(mut self, focused: bool) -> Self { self.is_focused_block = focused; self } fn input_method<'b>( &self, state: &'b State, renderer: &iced_wgpu::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.selection() { Selection::Caret(position) => position, Selection::Range(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 adjusted = if self.anchored_children.is_empty() { cursor } else { let line_h: f32 = line_height.into(); let line = (cursor.y / line_h).round() as usize; let offset = items_height_before_line(&self.anchored_children, line); Point::new(cursor.x, cursor.y + offset) }; let position = adjusted + translation; InputMethod::Enabled { cursor: Rectangle::new( position, Size::new(1.0, f32::from(line_height)), ), 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); struct Internal { editor: iced_graphics::text::Editor, } impl Content { /// 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: ::with_text(text), })) } /// Performs an [`Action`] on the [`Content`]. pub fn perform(&mut self, action: Action) { let internal = self.0.get_mut(); internal.editor.perform(action); } /// Moves the current cursor to reflect the given one. pub fn move_to(&mut self, cursor: Cursor) { let internal = self.0.get_mut(); internal.editor.move_to(cursor); } /// Returns the current cursor position of the [`Content`]. pub fn cursor(&self) -> Cursor { self.0.borrow().editor.cursor() } /// 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 selected text of the [`Content`]. pub fn selection(&self) -> Option { self.0.borrow().editor.copy() } /// Returns the kind of [`LineEnding`] used for separating lines in the [`Content`]. pub fn line_ending(&self) -> Option { Some(self.line(0)?.ending) } /// Returns whether or not the the [`Content`] is empty. pub fn is_empty(&self) -> bool { self.0.borrow().editor.is_empty() } } impl Clone for Content { fn clone(&self) -> Self { Self::with_text(&self.text()) } } impl Default for Content { fn default() -> Self { Self::new() } } impl fmt::Debug for Content { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let internal = self.0.borrow(); f.debug_struct("Content") .field("editor", &internal.editor) .finish() } } /// The state of a [`TextEditor`]. #[derive(Debug)] pub struct State { focus: Option, preedit: Option, last_click: Option, drag_click: Option, partial_scroll: f32, last_theme: RefCell>, highlighter: RefCell, highlighter_settings: Highlighter::Settings, highlighter_format_address: usize, /// Paragraphs built during draw() — kept alive so the renderer's Weak refs /// survive until the prepare() phase processes them. retained_paragraphs: RefCell>, } #[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) .is_multiple_of(2) } } 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> where Highlighter: text::Highlighter, Theme: Catalog, { 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, last_theme: RefCell::default(), highlighter: RefCell::new(Highlighter::new( &self.highlighter_settings, )), highlighter_settings: self.highlighter_settings.clone(), highlighter_format_address: self.highlighter_format as usize, retained_paragraphs: RefCell::new(Vec::new()), }) } fn children(&self) -> Vec { self.anchored_children.iter().map(|item| widget::Tree::new(&item.element)).collect() } fn diff(&self, tree: &mut widget::Tree) { tree.diff_children(&self.anchored_children.iter().map(|item| &item.element).collect::>()); } fn size(&self) -> Size { Size { width: self.width, height: self.height, } } fn layout( &mut self, tree: &mut widget::Tree, renderer: &iced_wgpu::Renderer, limits: &layout::Limits, ) -> 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(), ); let line_h: f32 = self.line_height.to_absolute( self.text_size.unwrap_or_else(|| renderer.default_size()), ).into(); let extra = total_items_height(&self.anchored_children); // Compute child layouts at their stream positions let mut child_nodes = Vec::with_capacity(self.anchored_children.len()); let child_limits = layout::Limits::new( Size::ZERO, Size::new(limits.shrink(self.padding).max().width, f32::INFINITY), ); let mut stream_y = 0.0f32; let mut next_child = 0; let line_count = internal.editor.line_count(); for line in 0..line_count { stream_y += line_h; while next_child < self.anchored_children.len() && self.anchored_children[next_child].after_line == line { let child = &mut self.anchored_children[next_child]; let mut node = child.element.as_widget_mut().layout( &mut tree.children[next_child], renderer, &child_limits, ); node = node.move_to(Point::new(self.padding.left, self.padding.top + stream_y)); child.height = node.bounds().height; stream_y += child.height; child_nodes.push(node); next_child += 1; } } // Remaining children after last line while next_child < self.anchored_children.len() { let child = &mut self.anchored_children[next_child]; let mut node = child.element.as_widget_mut().layout( &mut tree.children[next_child], renderer, &child_limits, ); node = node.move_to(Point::new(self.padding.left, self.padding.top + stream_y)); child.height = node.bounds().height; stream_y += child.height; child_nodes.push(node); next_child += 1; } match self.height { Length::Fill | Length::FillPortion(_) | Length::Fixed(_) => { let mut size = limits.max(); size.height += extra; layout::Node::with_children(size, child_nodes) } Length::Shrink => { let min_bounds = internal.editor.min_bounds(); layout::Node::with_children( limits .height(min_bounds.height + extra) .max() .expand(Size::new(0.0, self.padding.y())), child_nodes, ) } } } fn update( &mut self, tree: &mut widget::Tree, event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &iced_wgpu::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, _viewport: &Rectangle, ) { // Forward events to anchored children first if !self.anchored_children.is_empty() { let children_layouts: Vec<_> = layout.children().collect(); for (i, child) in self.anchored_children.iter_mut().enumerate() { if i < children_layouts.len() && i < tree.children.len() { child.element.as_widget_mut().update( &mut tree.children[i], event, children_layouts[i], cursor, renderer, clipboard, shell, _viewport, ); if shell.is_event_captured() { return; } } } } 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 && 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(), ) { let line_h: f32 = self.line_height.to_absolute( self.text_size.unwrap_or_else(|| renderer.default_size()), ).into(); match update { Update::Click(click) => { let action = match click.kind() { mouse::click::Kind::Single => { let mut pos = click.position(); if !self.anchored_children.is_empty() { let lc = self.content.0.borrow().editor.line_count(); pos.y = stream_y_to_text_y(pos.y, &self.anchored_children, line_h, lc); } Action::Click(pos) } 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) => { let mut pos = position; if !self.anchored_children.is_empty() { let lc = self.content.0.borrow().editor.line_count(); pos.y = stream_y_to_text_y(pos.y, &self.anchored_children, line_h, lc); } shell.publish(on_edit(Action::Drag(pos))); } 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, 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 iced_wgpu::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()); let theme_name = theme.name(); if state .last_theme .borrow() .as_ref() .is_none_or(|last_theme| last_theme != theme_name) { state.highlighter.borrow_mut().change_line(0); let _ = state.last_theme.borrow_mut().replace(theme_name.to_owned()); } 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); let text_size = self.text_size.unwrap_or_else(|| renderer.default_size()); let line_h: f32 = self.line_height.to_absolute(text_size).into(); 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: text_size, line_height: self.line_height, font, align_x: text::Alignment::Default, align_y: alignment::Vertical::Top, shaping: text::Shaping::Advanced, wrapping: self.wrapping, }, text_bounds.position(), style.placeholder, text_bounds, ); } } else if self.anchored_children.is_empty() { renderer.fill_editor( &internal.editor, text_bounds.position(), style.value, text_bounds, ); } else { // Sequential stream: text lines (layer 0) interleaved with // anchored children (layer 1) in one continuous pass. let buffer = internal.editor.buffer(); let line_count = buffer.lines.len(); let mut stream_y = 0.0f32; let mut child_idx = 0; let children_layouts: Vec<_> = layout.children().collect(); // Build paragraphs and retain in widget State. fill_paragraph // stores Weak refs — the Paragraphs must survive until the // renderer's prepare() phase. State lives in the widget tree. { let mut paras = state.retained_paragraphs.borrow_mut(); paras.clear(); for i in 0..line_count { let line_text = buffer.lines[i].text(); let glyphs: Vec = buffer.lines[i].layout_opt() .map(|layouts| layouts.iter().flat_map(|l| l.glyphs.iter().cloned()).collect()) .unwrap_or_default(); let spans = build_color_spans(line_text, &glyphs, f32::from(text_size)); paras.push(iced_graphics::text::Paragraph::with_spans(Text { content: spans.as_slice(), bounds: Size::new(text_bounds.width, line_h), size: text_size, line_height: self.line_height, font, align_x: text::Alignment::Default, align_y: alignment::Vertical::Top, shaping: text::Shaping::Advanced, wrapping: self.wrapping, })); } } let paras = state.retained_paragraphs.borrow(); for line_i in 0..line_count { let y = text_bounds.y + line_i as f32 * line_h + stream_y; renderer.fill_paragraph( ¶s[line_i], Point::new(text_bounds.x, y), style.value, text_bounds, ); // After this line, draw any anchored children while child_idx < self.anchored_children.len() && self.anchored_children[child_idx].after_line == line_i { if child_idx < children_layouts.len() { self.anchored_children[child_idx].element.as_widget().draw( &tree.children[child_idx], renderer, theme, _defaults, children_layouts[child_idx], _cursor, _viewport, ); } stream_y += self.anchored_children[child_idx].height; child_idx += 1; } } // Draw remaining children after last text line while child_idx < self.anchored_children.len() { if child_idx < children_layouts.len() { self.anchored_children[child_idx].element.as_widget().draw( &tree.children[child_idx], renderer, theme, _defaults, children_layouts[child_idx], _cursor, _viewport, ); } child_idx += 1; } } let translation = text_bounds.position() - Point::ORIGIN; if let Some(focus) = state.focus.as_ref() { let adjust_y = |pos: Point| -> Point { if self.anchored_children.is_empty() { pos } else { let line = (pos.y / line_h).round() as usize; let offset = items_height_before_line(&self.anchored_children, line); Point::new(pos.x, pos.y + offset) } }; match internal.editor.selection() { Selection::Caret(position) if focus.is_cursor_visible() => { let position = adjust_y(position); let cursor = Rectangle::new( position + translation, Size::new(1.0, line_h), ); if let Some(clipped_cursor) = text_bounds.intersection(&cursor) { renderer.fill_quad( renderer::Quad { bounds: clipped_cursor, ..renderer::Quad::default() }, style.value, ); } } Selection::Range(ranges) => { for range in ranges.into_iter().map(|r| { let adjusted = Rectangle::new( adjust_y(r.position()), r.size(), ); adjusted + translation }).filter_map(|r| text_bounds.intersection(&r)) { renderer.fill_quad( renderer::Quad { bounds: range, ..renderer::Quad::default() }, style.selection, ); } } Selection::Caret(_) => {} } } } fn mouse_interaction( &self, _tree: &widget::Tree, layout: Layout<'_>, cursor: mouse::Cursor, _viewport: &Rectangle, _renderer: &iced_wgpu::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( &mut self, tree: &mut widget::Tree, layout: Layout<'_>, _renderer: &iced_wgpu::Renderer, operation: &mut dyn widget::Operation, ) { let state = tree.state.downcast_mut::>(); operation.focusable(self.id.as_ref(), layout.bounds(), state); } } impl<'a, Highlighter, Message, Theme> From> for Element<'a, Message, Theme, iced_wgpu::Renderer> where Highlighter: text::Highlighter, Message: 'a, Theme: Catalog + 'a, { fn from( text_editor: TextEditor<'a, Highlighter, Message, Theme>, ) -> 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 original key pressed without modifiers applied to it. /// /// You should use this key for combinations (e.g. Ctrl+C). pub key: keyboard::Key, /// The key pressed with modifiers applied to it. /// /// You should use this key for any single key bindings (e.g. motions). pub modified_key: keyboard::Key, /// The physical key pressed. /// /// You should use this key for layout-independent bindings. pub physical_key: keyboard::key::Physical, /// 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, modified_key, physical_key, modifiers, text, status, } = event; if !matches!(status, Status::Focused { .. }) { return None; } let combination = match key.to_latin(physical_key) { Some('c') if modifiers.command() => Some(Self::Copy), Some('x') if modifiers.command() => Some(Self::Cut), Some('v') if modifiers.command() && !modifiers.alt() => { Some(Self::Paste) } Some('a') if modifiers.command() => Some(Self::SelectAll), _ => None, }; if let Some(binding) = combination { return Some(binding); } #[cfg(target_os = "macos")] let modified_key = convert_macos_shortcut(&key, modifiers).unwrap_or(modified_key); match modified_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), _ => { 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.left, padding.top); 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.left, padding.top); 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, modified_key, physical_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(), modified_key: modified_key.clone(), physical_key: *physical_key, 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 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: theme::Base { /// 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, }, placeholder: palette.secondary.base.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, placeholder: palette.background.strongest.color, ..active }, } } #[cfg(target_os = "macos")] pub fn convert_macos_shortcut( key: &keyboard::Key, modifiers: keyboard::Modifiers, ) -> Option { if modifiers != keyboard::Modifiers::CTRL { return None; } let key = match key.as_ref() { keyboard::Key::Character("b") => key::Named::ArrowLeft, keyboard::Key::Character("f") => key::Named::ArrowRight, keyboard::Key::Character("a") => key::Named::Home, keyboard::Key::Character("e") => key::Named::End, keyboard::Key::Character("h") => key::Named::Backspace, keyboard::Key::Character("d") => key::Named::Delete, _ => return None, }; Some(keyboard::Key::Named(key)) }