diff options
| author | 2024-01-19 20:41:52 +0100 | |
|---|---|---|
| committer | 2024-01-19 20:41:52 +0100 | |
| commit | 1781068e1c3a65551db1e832fdbaddba99124051 (patch) | |
| tree | 60e0b3854cc0541712572fbb0e56f14435951ea9 /widget/src/text | |
| parent | 41dec5bd203ff5b1574a33a17d5f7358ae1beea2 (diff) | |
| parent | 7ae7fcb89855002519bab752fd3686106ce448db (diff) | |
| download | iced-1781068e1c3a65551db1e832fdbaddba99124051.tar.gz iced-1781068e1c3a65551db1e832fdbaddba99124051.tar.bz2 iced-1781068e1c3a65551db1e832fdbaddba99124051.zip | |
Merge branch 'master' into remove-vertex-indexing
Diffstat (limited to '')
| -rw-r--r-- | widget/src/text_editor.rs | 736 | ||||
| -rw-r--r-- | widget/src/text_input.rs | 222 |
2 files changed, 846 insertions, 112 deletions
diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs new file mode 100644 index 00000000..09a0cac0 --- /dev/null +++ b/widget/src/text_editor.rs @@ -0,0 +1,736 @@ +//! Display a multi-line text input for text editing. +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}; +use crate::core::widget::{self, Widget}; +use crate::core::{ + Clipboard, Color, Element, Length, Padding, Pixels, Rectangle, Shell, Size, + 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<Renderer>, + font: Option<Renderer::Font>, + text_size: Option<Pixels>, + line_height: LineHeight, + width: Length, + height: Length, + padding: Padding, + style: <Renderer::Theme as StyleSheet>::Style, + on_edit: Option<Box<dyn Fn(Action) -> Message + 'a>>, + highlighter_settings: Highlighter::Settings, + highlighter_format: fn( + &Highlighter::Highlight, + &Renderer::Theme, + ) -> highlighter::Format<Renderer::Font>, +} + +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<Renderer>) -> 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<Renderer::Font>) -> Self { + self.font = Some(font.into()); + self + } + + /// Sets the [`Padding`] of the [`TextEditor`]. + pub fn padding(mut self, padding: impl Into<Padding>) -> 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<H: text::Highlighter>( + self, + settings: H::Settings, + to_format: fn( + &H::Highlight, + &Renderer::Theme, + ) -> highlighter::Format<Renderer::Font>, + ) -> 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, + } + } + + /// Sets the style of the [`TextEditor`]. + pub fn style( + mut self, + style: impl Into<<Renderer::Theme as StyleSheet>::Style>, + ) -> Self { + self.style = style.into(); + self + } +} + +/// The content of a [`TextEditor`]. +pub struct Content<R = crate::Renderer>(RefCell<Internal<R>>) +where + R: text::Renderer; + +struct Internal<R> +where + R: text::Renderer, +{ + editor: R::Editor, + is_dirty: bool, +} + +impl<R> Content<R> +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<impl std::ops::Deref<Target = str> + '_> { + 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<Item = impl std::ops::Deref<Target = str> + '_> { + struct Lines<'a, Renderer: text::Renderer> { + internal: std::cell::Ref<'a, Internal<Renderer>>, + current: usize, + } + + impl<'a, Renderer: text::Renderer> Iterator for Lines<'a, Renderer> { + type Item = std::cell::Ref<'a, str>; + + fn next(&mut self) -> Option<Self::Item> { + 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<String> { + 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<Renderer> Default for Content<Renderer> +where + Renderer: text::Renderer, +{ + fn default() -> Self { + Self::new() + } +} + +impl<Renderer> fmt::Debug for Content<Renderer> +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<Highlighter: text::Highlighter> { + is_focused: bool, + last_click: Option<mouse::Click>, + drag_click: Option<mouse::click::Kind>, + highlighter: RefCell<Highlighter>, + highlighter_settings: Highlighter::Settings, + highlighter_format_address: usize, +} + +impl<'a, Highlighter, Message, Renderer> Widget<Message, Renderer> + 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::<State<Highlighter>>() + } + + 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 size(&self) -> Size<Length> { + 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::<State<Highlighter>>(); + + 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.shrink(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::<State<Highlighter>>(); + + 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: &<Renderer as renderer::Renderer>::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::<State<Highlighter>>(); + + 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, + *viewport, + ); + + 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<TextEditor<'a, Highlighter, Message, Renderer>> + 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<H: Highlighter>( + event: Event, + state: &State<H>, + bounds: Rectangle, + padding: Padding, + cursor: mouse::Cursor, + ) -> Option<Self> { + 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, + modifiers, + text, + .. + } if state.is_focused => { + if let keyboard::Key::Named(named_key) = key.as_ref() { + if let Some(motion) = motion(named_key) { + 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.as_ref() { + keyboard::Key::Named(key::Named::Enter) => { + edit(Edit::Enter) + } + keyboard::Key::Named(key::Named::Backspace) => { + edit(Edit::Backspace) + } + keyboard::Key::Named(key::Named::Delete) => { + edit(Edit::Delete) + } + keyboard::Key::Named(key::Named::Escape) => { + Some(Self::Unfocus) + } + keyboard::Key::Character("c") + if modifiers.command() => + { + Some(Self::Copy) + } + keyboard::Key::Character("v") + if modifiers.command() && !modifiers.alt() => + { + Some(Self::Paste) + } + _ => { + let text = text?; + + edit(Edit::Insert( + text.chars().next().unwrap_or_default(), + )) + } + } + } + _ => None, + }, + _ => None, + } + } +} + +fn motion(key: key::Named) -> Option<Motion> { + 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, + } +} + +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() + } + } +} diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index 9e1fb796..c3dce8be 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -14,6 +14,7 @@ use editor::Editor; use crate::core::alignment; use crate::core::event::{self, Event}; use crate::core::keyboard; +use crate::core::keyboard::key; use crate::core::layout; use crate::core::mouse::{self, click}; use crate::core::renderer; @@ -238,6 +239,7 @@ where layout: Layout<'_>, cursor: mouse::Cursor, value: Option<&Value>, + viewport: &Rectangle, ) { draw( renderer, @@ -250,6 +252,7 @@ where self.is_secure, self.icon.as_ref(), &self.style, + viewport, ); } } @@ -281,12 +284,11 @@ where } } - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - Length::Shrink + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: Length::Shrink, + } } fn layout( @@ -362,7 +364,7 @@ where _style: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, - _viewport: &Rectangle, + viewport: &Rectangle, ) { draw( renderer, @@ -375,6 +377,7 @@ where self.is_secure, self.icon.as_ref(), &self.style, + viewport, ); } @@ -503,14 +506,11 @@ where { let font = font.unwrap_or_else(|| renderer.default_font()); let text_size = size.unwrap_or_else(|| renderer.default_size()); - let padding = padding.fit(Size::ZERO, limits.max()); - let limits = limits - .width(width) - .pad(padding) - .height(line_height.to_absolute(text_size)); + let height = line_height.to_absolute(text_size); - let text_bounds = limits.resolve(Size::ZERO); + let limits = limits.width(width).shrink(padding); + let text_bounds = limits.resolve(width, height, Size::ZERO); let placeholder_text = Text { font, @@ -523,18 +523,15 @@ where shaping: text::Shaping::Advanced, }; - renderer.update_paragraph(&mut state.placeholder, placeholder_text); + state.placeholder.update(placeholder_text); let secure_value = is_secure.then(|| value.secure()); let value = secure_value.as_ref().unwrap_or(value); - renderer.update_paragraph( - &mut state.value, - Text { - content: &value.to_string(), - ..placeholder_text - }, - ); + state.value.update(Text { + content: &value.to_string(), + ..placeholder_text + }); if let Some(icon) = icon { let icon_text = Text { @@ -548,45 +545,45 @@ where shaping: text::Shaping::Advanced, }; - renderer.update_paragraph(&mut state.icon, icon_text); + state.icon.update(icon_text); let icon_width = state.icon.min_width(); - let mut text_node = layout::Node::new( - text_bounds - Size::new(icon_width + icon.spacing, 0.0), - ); - - let mut icon_node = - layout::Node::new(Size::new(icon_width, text_bounds.height)); - - match icon.side { - Side::Left => { - text_node.move_to(Point::new( + let (text_position, icon_position) = match icon.side { + Side::Left => ( + Point::new( padding.left + icon_width + icon.spacing, padding.top, - )); - - icon_node.move_to(Point::new(padding.left, padding.top)); - } - Side::Right => { - text_node.move_to(Point::new(padding.left, padding.top)); - - icon_node.move_to(Point::new( + ), + Point::new(padding.left, padding.top), + ), + Side::Right => ( + Point::new(padding.left, padding.top), + Point::new( padding.left + text_bounds.width - icon_width, padding.top, - )); - } + ), + ), }; + let text_node = layout::Node::new( + text_bounds - Size::new(icon_width + icon.spacing, 0.0), + ) + .move_to(text_position); + + let icon_node = + layout::Node::new(Size::new(icon_width, text_bounds.height)) + .move_to(icon_position); + layout::Node::with_children( - text_bounds.pad(padding), + text_bounds.expand(padding), vec![text_node, icon_node], ) } else { - let mut text = layout::Node::new(text_bounds); - text.move_to(Point::new(padding.left, padding.top)); + let text = layout::Node::new(text_bounds) + .move_to(Point::new(padding.left, padding.top)); - layout::Node::with_children(text_bounds.pad(padding), vec![text]) + layout::Node::with_children(text_bounds.expand(padding), vec![text]) } } @@ -752,34 +749,7 @@ where return event::Status::Captured; } } - Event::Keyboard(keyboard::Event::CharacterReceived(c)) => { - let state = state(); - - if let Some(focus) = &mut state.is_focused { - let Some(on_input) = on_input else { - return event::Status::Ignored; - }; - - if state.is_pasting.is_none() - && !state.keyboard_modifiers.command() - && !c.is_control() - { - let mut editor = Editor::new(value, &mut state.cursor); - - editor.insert(c); - - let message = (on_input)(editor.contents()); - shell.publish(message); - - focus.updated_at = Instant::now(); - - update_cache(state, value); - - return event::Status::Captured; - } - } - } - Event::Keyboard(keyboard::Event::KeyPressed { key_code, .. }) => { + Event::Keyboard(keyboard::Event::KeyPressed { key, text, .. }) => { let state = state(); if let Some(focus) = &mut state.is_focused { @@ -790,14 +760,13 @@ where let modifiers = state.keyboard_modifiers; focus.updated_at = Instant::now(); - match key_code { - keyboard::KeyCode::Enter - | keyboard::KeyCode::NumpadEnter => { + match key.as_ref() { + keyboard::Key::Named(key::Named::Enter) => { if let Some(on_submit) = on_submit.clone() { shell.publish(on_submit); } } - keyboard::KeyCode::Backspace => { + keyboard::Key::Named(key::Named::Backspace) => { if platform::is_jump_modifier_pressed(modifiers) && state.cursor.selection(value).is_none() { @@ -817,7 +786,7 @@ where update_cache(state, value); } - keyboard::KeyCode::Delete => { + keyboard::Key::Named(key::Named::Delete) => { if platform::is_jump_modifier_pressed(modifiers) && state.cursor.selection(value).is_none() { @@ -839,7 +808,7 @@ where update_cache(state, value); } - keyboard::KeyCode::Left => { + keyboard::Key::Named(key::Named::ArrowLeft) => { if platform::is_jump_modifier_pressed(modifiers) && !is_secure { @@ -854,7 +823,7 @@ where state.cursor.move_left(value); } } - keyboard::KeyCode::Right => { + keyboard::Key::Named(key::Named::ArrowRight) => { if platform::is_jump_modifier_pressed(modifiers) && !is_secure { @@ -869,7 +838,7 @@ where state.cursor.move_right(value); } } - keyboard::KeyCode::Home => { + keyboard::Key::Named(key::Named::Home) => { if modifiers.shift() { state .cursor @@ -878,7 +847,7 @@ where state.cursor.move_to(0); } } - keyboard::KeyCode::End => { + keyboard::Key::Named(key::Named::End) => { if modifiers.shift() { state.cursor.select_range( state.cursor.start(value), @@ -888,7 +857,7 @@ where state.cursor.move_to(value.len()); } } - keyboard::KeyCode::C + keyboard::Key::Character("c") if state.keyboard_modifiers.command() => { if let Some((start, end)) = @@ -898,7 +867,7 @@ where .write(value.select(start, end).to_string()); } } - keyboard::KeyCode::X + keyboard::Key::Character("x") if state.keyboard_modifiers.command() => { if let Some((start, end)) = @@ -916,7 +885,7 @@ where update_cache(state, value); } - keyboard::KeyCode::V => { + keyboard::Key::Character("v") => { if state.keyboard_modifiers.command() && !state.keyboard_modifiers.alt() { @@ -953,12 +922,12 @@ where state.is_pasting = None; } } - keyboard::KeyCode::A + keyboard::Key::Character("a") if state.keyboard_modifiers.command() => { state.cursor.select_all(value); } - keyboard::KeyCode::Escape => { + keyboard::Key::Named(key::Named::Escape) => { state.is_focused = None; state.is_dragging = false; state.is_pasting = None; @@ -966,28 +935,55 @@ where state.keyboard_modifiers = keyboard::Modifiers::default(); } - keyboard::KeyCode::Tab - | keyboard::KeyCode::Up - | keyboard::KeyCode::Down => { + keyboard::Key::Named( + key::Named::Tab + | key::Named::ArrowUp + | key::Named::ArrowDown, + ) => { return event::Status::Ignored; } - _ => {} + _ => { + if let Some(text) = text { + let c = text.chars().next().unwrap_or_default(); + + if state.is_pasting.is_none() + && !state.keyboard_modifiers.command() + && !c.is_control() + { + let mut editor = + Editor::new(value, &mut state.cursor); + + editor.insert(c); + + let message = (on_input)(editor.contents()); + shell.publish(message); + + focus.updated_at = Instant::now(); + + update_cache(state, value); + + return event::Status::Captured; + } + } + } } return event::Status::Captured; } } - Event::Keyboard(keyboard::Event::KeyReleased { key_code, .. }) => { + Event::Keyboard(keyboard::Event::KeyReleased { key, .. }) => { let state = state(); if state.is_focused.is_some() { - match key_code { - keyboard::KeyCode::V => { + match key.as_ref() { + keyboard::Key::Character("v") => { state.is_pasting = None; } - keyboard::KeyCode::Tab - | keyboard::KeyCode::Up - | keyboard::KeyCode::Down => { + keyboard::Key::Named( + key::Named::Tab + | key::Named::ArrowUp + | key::Named::ArrowDown, + ) => { return event::Status::Ignored; } _ => {} @@ -1003,14 +999,14 @@ where state.keyboard_modifiers = modifiers; } - Event::Window(window::Event::Unfocused) => { + Event::Window(_, window::Event::Unfocused) => { let state = state(); if let Some(focus) = &mut state.is_focused { focus.is_window_focused = false; } } - Event::Window(window::Event::Focused) => { + Event::Window(_, window::Event::Focused) => { let state = state(); if let Some(focus) = &mut state.is_focused { @@ -1020,7 +1016,7 @@ where shell.request_redraw(window::RedrawRequest::NextFrame); } } - Event::Window(window::Event::RedrawRequested(now)) => { + Event::Window(_, window::Event::RedrawRequested(now)) => { let state = state(); if let Some(focus) = &mut state.is_focused { @@ -1058,6 +1054,7 @@ pub fn draw<Renderer>( is_secure: bool, icon: Option<&Icon<Renderer::Font>>, style: &<Renderer::Theme as StyleSheet>::Style, + viewport: &Rectangle, ) where Renderer: text::Renderer, Renderer::Theme: StyleSheet, @@ -1099,6 +1096,7 @@ pub fn draw<Renderer>( &state.icon, icon_layout.bounds().center(), appearance.icon_color, + *viewport, ); } @@ -1192,11 +1190,11 @@ pub fn draw<Renderer>( (None, 0.0) }; - let text_width = state.value.min_width(); - - let render = |renderer: &mut Renderer| { + let draw = |renderer: &mut Renderer, viewport| { if let Some((cursor, color)) = cursor { - renderer.fill_quad(cursor, color); + renderer.with_translation(Vector::new(-offset, 0.0), |renderer| { + renderer.fill_quad(cursor, color); + }); } else { renderer.with_translation(Vector::ZERO, |_| {}); } @@ -1207,7 +1205,8 @@ pub fn draw<Renderer>( } else { &state.value }, - Point::new(text_bounds.x, text_bounds.center_y()), + Point::new(text_bounds.x, text_bounds.center_y()) + - Vector::new(offset, 0.0), if text.is_empty() { theme.placeholder_color(style) } else if is_disabled { @@ -1215,15 +1214,14 @@ pub fn draw<Renderer>( } else { theme.value_color(style) }, + viewport, ); }; - if text_width > text_bounds.width { - renderer.with_layer(text_bounds, |renderer| { - renderer.with_translation(Vector::new(-offset, 0.0), render); - }); + if cursor.is_some() { + renderer.with_layer(text_bounds, |renderer| draw(renderer, *viewport)); } else { - render(renderer); + draw(renderer, text_bounds); } } @@ -1461,7 +1459,7 @@ fn replace_paragraph<Renderer>( let mut children_layout = layout.children(); let text_bounds = children_layout.next().unwrap().bounds(); - state.value = renderer.create_paragraph(Text { + state.value = Renderer::Paragraph::with_text(Text { font, line_height, content: &value.to_string(), |
