From c538d672df2ac0e4e38d8d642b161c48b50f3ddb Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Wed, 19 Mar 2025 16:43:43 +0100 Subject: [PATCH] improv(text_input): optimize, fix, and improve the text inputs --- src/widget/text_input/cursor.rs | 27 +- src/widget/text_input/editor.rs | 2 + src/widget/text_input/input.rs | 462 ++++++++++++++++++-------------- src/widget/text_input/value.rs | 9 + 4 files changed, 291 insertions(+), 209 deletions(-) diff --git a/src/widget/text_input/cursor.rs b/src/widget/text_input/cursor.rs index a42596e1..42f52da1 100644 --- a/src/widget/text_input/cursor.rs +++ b/src/widget/text_input/cursor.rs @@ -27,6 +27,7 @@ pub enum State { } impl Default for Cursor { + #[inline] fn default() -> Self { Self { state: State::Index(0), @@ -37,6 +38,7 @@ impl Default for Cursor { impl Cursor { /// Returns the [`State`] of the [`Cursor`]. #[must_use] + #[inline(never)] pub fn state(&self, value: &Value) -> State { match self.state { State::Index(index) => State::Index(index.min(value.len())), @@ -57,6 +59,7 @@ impl Cursor { /// /// `start` is guaranteed to be <= than `end`. #[must_use] + #[inline] pub fn selection(&self, value: &Value) -> Option<(usize, usize)> { match self.state(value) { State::Selection { start, end } => Some((start.min(end), start.max(end))), @@ -64,18 +67,22 @@ impl Cursor { } } + #[inline] pub(crate) fn move_to(&mut self, position: usize) { self.state = State::Index(position); } + #[inline] pub(crate) fn move_right(&mut self, value: &Value) { self.move_right_by_amount(value, 1); } + #[inline] pub(crate) fn move_right_by_words(&mut self, value: &Value) { self.move_to(value.next_end_of_word(self.right(value))); } + #[inline] pub(crate) fn move_right_by_amount(&mut self, value: &Value, amount: usize) { match self.state(value) { State::Index(index) => self.move_to(index.saturating_add(amount).min(value.len())), @@ -83,6 +90,7 @@ impl Cursor { } } + #[inline] pub(crate) fn move_left(&mut self, value: &Value) { match self.state(value) { State::Index(index) if index > 0 => self.move_to(index - 1), @@ -91,18 +99,21 @@ impl Cursor { } } + #[inline] pub(crate) fn move_left_by_words(&mut self, value: &Value) { self.move_to(value.previous_start_of_word(self.left(value))); } + #[inline] pub(crate) fn select_range(&mut self, start: usize, end: usize) { - if start == end { - self.state = State::Index(start); + self.state = if start == end { + State::Index(start) } else { - self.state = State::Selection { start, end }; - } + State::Selection { start, end } + }; } + #[inline] pub(crate) fn select_left(&mut self, value: &Value) { match self.state(value) { State::Index(index) if index > 0 => self.select_range(index, index - 1), @@ -111,6 +122,7 @@ impl Cursor { } } + #[inline] pub(crate) fn select_right(&mut self, value: &Value) { match self.state(value) { State::Index(index) if index < value.len() => self.select_range(index, index + 1), @@ -121,6 +133,7 @@ impl Cursor { } } + #[inline] pub(crate) fn select_left_by_words(&mut self, value: &Value) { match self.state(value) { State::Index(index) => self.select_range(index, value.previous_start_of_word(index)), @@ -130,6 +143,7 @@ impl Cursor { } } + #[inline] pub(crate) fn select_right_by_words(&mut self, value: &Value) { match self.state(value) { State::Index(index) => self.select_range(index, value.next_end_of_word(index)), @@ -139,10 +153,12 @@ impl Cursor { } } + #[inline] pub(crate) fn select_all(&mut self, value: &Value) { self.select_range(0, value.len()); } + #[inline] pub(crate) fn start(&self, value: &Value) -> usize { let start = match self.state { State::Index(index) => index, @@ -152,6 +168,7 @@ impl Cursor { start.min(value.len()) } + #[inline] pub(crate) fn end(&self, value: &Value) -> usize { let end = match self.state { State::Index(index) => index, @@ -161,6 +178,7 @@ impl Cursor { end.min(value.len()) } + #[inline] fn left(&self, value: &Value) -> usize { match self.state(value) { State::Index(index) => index, @@ -168,6 +186,7 @@ impl Cursor { } } + #[inline] fn right(&self, value: &Value) -> usize { match self.state(value) { State::Index(index) => index, diff --git a/src/widget/text_input/editor.rs b/src/widget/text_input/editor.rs index 301820f4..b8144761 100644 --- a/src/widget/text_input/editor.rs +++ b/src/widget/text_input/editor.rs @@ -10,11 +10,13 @@ pub struct Editor<'a> { } impl<'a> Editor<'a> { + #[inline] pub fn new(value: &'a mut Value, cursor: &'a mut Cursor) -> Editor<'a> { Editor { value, cursor } } #[must_use] + #[inline] pub fn contents(&self) -> String { self.value.to_string() } diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index 869d53eb..52fde469 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -18,9 +18,9 @@ use super::style::StyleSheet; pub use super::value::Value; use apply::Apply; +use iced::Limits; use iced::clipboard::dnd::{DndAction, DndEvent, OfferEvent, SourceEvent}; use iced::clipboard::mime::AsMimeTypes; -use iced::Limits; use iced_core::event::{self, Event}; use iced_core::mouse::{self, click}; use iced_core::overlay::Group; @@ -28,18 +28,18 @@ use iced_core::renderer::{self, Renderer as CoreRenderer}; use iced_core::text::{self, Paragraph, Renderer, Text}; use iced_core::time::{Duration, Instant}; use iced_core::touch; +use iced_core::widget::Id; use iced_core::widget::operation::{self, Operation}; use iced_core::widget::tree::{self, Tree}; -use iced_core::widget::Id; use iced_core::window; -use iced_core::{alignment, Background}; -use iced_core::{keyboard, Border, Shadow}; -use iced_core::{layout, overlay}; +use iced_core::{Background, alignment}; +use iced_core::{Border, Shadow, keyboard}; use iced_core::{ Clipboard, Color, Element, Layout, Length, Padding, Pixels, Point, Rectangle, Shell, Size, Vector, Widget, }; -use iced_runtime::{task, Action, Task}; +use iced_core::{layout, overlay}; +use iced_runtime::{Action, Task, task}; thread_local! { // Prevents two inputs from being focused at the same time. @@ -59,7 +59,7 @@ where TextInput::new(placeholder, value) } -/// A text label whiich can transform into a text input on activation. +/// A text label which can transform into a text input on activation. pub fn editable_input<'a, Message: Clone + 'static>( placeholder: impl Into>, text: impl Into>, @@ -182,7 +182,7 @@ pub struct TextInput<'a, Message> { placeholder: Cow<'a, str>, value: Value, is_secure: bool, - is_editable: bool, + is_editable_variant: bool, is_read_only: bool, select_on_focus: bool, font: Option<::Font>, @@ -193,8 +193,11 @@ pub struct TextInput<'a, Message> { label: Option>, helper_text: Option>, error: Option>, + on_focus: Option, + on_unfocus: Option, on_input: Option Message + 'a>>, on_paste: Option Message + 'a>>, + on_tab: Option, on_submit: Option Message + 'a>>, on_toggle_edit: Option Message + 'a>>, leading_icon: Option>, @@ -228,7 +231,7 @@ where placeholder: placeholder.into(), value: Value::new(v.as_ref()), is_secure: false, - is_editable: false, + is_editable_variant: false, is_read_only: false, select_on_focus: false, font: None, @@ -237,9 +240,12 @@ where size: None, helper_size: 10.0, helper_line_height: text::LineHeight::Absolute(14.0.into()), + on_focus: None, + on_unfocus: None, on_input: None, on_paste: None, on_submit: None, + on_tab: None, on_toggle_edit: None, leading_icon: None, trailing_icon: None, @@ -256,6 +262,7 @@ where } } + #[inline] fn dnd_id(&self) -> u128 { match &self.id.0 { iced_core::id::Internal::Custom(id, _) | iced_core::id::Internal::Unique(id) => { @@ -267,7 +274,8 @@ where /// Sets the input to be always active. /// This makes it behave as if it was always focused. - pub fn always_active(mut self) -> Self { + #[inline] + pub const fn always_active(mut self) -> Self { self.always_active = true; self } @@ -285,6 +293,7 @@ where } /// Sets the [`Id`] of the [`TextInput`]. + #[inline] pub fn id(mut self, id: Id) -> Self { self.id = id; self @@ -303,66 +312,85 @@ where } /// Converts the [`TextInput`] into a secure password input. - pub fn password(mut self) -> Self { + #[inline] + pub const fn password(mut self) -> Self { self.is_secure = true; self } - pub fn editable(mut self) -> Self { - self.is_editable = true; + /// Applies behaviors unique to the `editable_input` variable. + #[inline] + pub(crate) const fn editable(mut self) -> Self { + self.is_editable_variant = true; self } - pub fn editing(mut self, enable: bool) -> Self { + #[inline] + pub const fn editing(mut self, enable: bool) -> Self { self.is_read_only = !enable; self } /// Selects all text when the text input is focused - pub fn select_on_focus(mut self, select_on_focus: bool) -> Self { + #[inline] + pub const fn select_on_focus(mut self, select_on_focus: bool) -> Self { self.select_on_focus = select_on_focus; self } + /// Emits a message when an unfocused text input has been focused by click. + /// + /// This will not trigger if the input was focused externally by the application. + #[inline] + pub fn on_focus(mut self, on_focus: Message) -> Self { + self.on_focus = Some(on_focus); + self + } + + /// Emits a message when a focused text input has been unfocused via the Tab or Esc key. + /// + /// This will not trigger if the input was unfocused externally by the application. + #[inline] + pub fn on_unfocus(mut self, on_unfocus: Message) -> Self { + self.on_unfocus = Some(on_unfocus); + self + } + /// Sets the message that should be produced when some text is typed into /// the [`TextInput`]. /// /// If this method is not called, the [`TextInput`] will be disabled. - pub fn on_input(mut self, callback: F) -> Self - where - F: 'a + Fn(String) -> Message, - { + pub fn on_input(mut self, callback: impl Fn(String) -> Message + 'a) -> Self { self.on_input = Some(Box::new(callback)); self } - /// Sets the message that should be produced when the [`TextInput`] is - /// focused and the enter key is pressed. - pub fn on_submit(self, callback: F) -> Self - where - F: 'a + Fn(String) -> Message, - { - self.on_submit_maybe(Some(Box::new(callback))) - } - - /// Maybe sets the message that should be produced when the [`TextInput`] is - /// focused and the enter key is pressed. - pub fn on_submit_maybe(mut self, callback: Option) -> Self - where - F: 'a + Fn(String) -> Message, - { - if let Some(callback) = callback { - self.on_submit = Some(Box::new(callback)); - } else { - self.on_submit = None; - } + /// Emits a message when a focused text input receives the Enter/Return key. + pub fn on_submit(mut self, callback: impl Fn(String) -> Message + 'a) -> Self { + self.on_submit = Some(Box::new(callback)); self } - pub fn on_toggle_edit(mut self, callback: F) -> Self - where - F: 'a + Fn(bool) -> Message, - { + /// Optionally emits a message when a focused text input receives the Enter/Return key. + pub fn on_submit_maybe(self, callback: Option Message + 'a>) -> Self { + if let Some(callback) = callback { + self.on_submit(callback) + } else { + self + } + } + + /// Emits a message when the Tab key has been captured, which prevents focus from changing. + /// + /// If you do no want to capture the Tab key, use [`TextInput::on_unfocus`] instead. + #[inline] + pub fn on_tab(mut self, on_tab: Message) -> Self { + self.on_tab = Some(on_tab); + self + } + + /// Emits a message when the editable state of the input changes. + pub fn on_toggle_edit(mut self, callback: impl Fn(bool) -> Message + 'a) -> Self { self.on_toggle_edit = Some(Box::new(callback)); self } @@ -377,12 +405,17 @@ where /// Sets the [`Font`] of the [`TextInput`]. /// /// [`Font`]: text::Renderer::Font - pub fn font(mut self, font: ::Font) -> Self { + #[inline] + pub const fn font( + mut self, + font: ::Font, + ) -> Self { self.font = Some(font); self } /// Sets the start [`Icon`] of the [`TextInput`]. + #[inline] pub fn leading_icon( mut self, icon: Element<'a, Message, crate::Theme, crate::Renderer>, @@ -392,6 +425,7 @@ where } /// Sets the end [`Icon`] of the [`TextInput`]. + #[inline] pub fn trailing_icon( mut self, icon: Element<'a, Message, crate::Theme, crate::Renderer>, @@ -407,7 +441,7 @@ where } /// Sets the [`Padding`] of the [`TextInput`]. - pub fn padding>(mut self, padding: P) -> Self { + pub fn padding(mut self, padding: impl Into) -> Self { self.padding = padding.into(); self } @@ -425,8 +459,9 @@ where } /// Sets the text input to manage its input value or not - pub fn manage_value(mut self, manage_value: bool) -> Self { - self.manage_value = true; + #[inline] + pub const fn manage_value(mut self, manage_value: bool) -> Self { + self.manage_value = manage_value; self } @@ -435,6 +470,7 @@ where /// /// [`Renderer`]: text::Renderer #[allow(clippy::too_many_arguments)] + #[inline] pub fn draw( &self, tree: &Tree, @@ -484,13 +520,15 @@ where /// Sets the window id of the [`TextInput`] and the window id of the drag icon. /// Both ids are required to be unique. /// This is required for the dnd to work. - pub fn surface_ids(mut self, window_id: (window::Id, window::Id)) -> Self { + #[inline] + pub const fn surface_ids(mut self, window_id: (window::Id, window::Id)) -> Self { self.surface_ids = Some(window_id); self } /// Sets the mode of this [`TextInput`] to be a drag and drop icon. - pub fn dnd_icon(mut self, dnd_icon: bool) -> Self { + #[inline] + pub const fn dnd_icon(mut self, dnd_icon: bool) -> Self { self.dnd_icon = dnd_icon; self } @@ -508,6 +546,7 @@ where } /// Get the layout node of the actual text input + fn text_layout<'b>(&'a self, layout: Layout<'b>) -> Layout<'b> { if self.dnd_icon { layout @@ -525,10 +564,12 @@ impl Widget for TextInput<'_, M where Message: Clone + 'static, { + #[inline] fn tag(&self) -> tree::Tag { tree::Tag::of::() } + #[inline] fn state(&self) -> tree::State { tree::State::new(State::new( self.is_secure, @@ -540,6 +581,7 @@ where fn diff(&mut self, tree: &mut Tree) { let state = tree.state.downcast_mut::(); + if !self.manage_value || !self.value.is_empty() && state.tracked_value != self.value { state.tracked_value = self.value.clone(); } else if self.value.is_empty() { @@ -586,8 +628,6 @@ where state.dirty = true; } - self.is_read_only = state.is_read_only; - if self.always_active && state.is_focused.is_none() { let now = Instant::now(); LAST_FOCUS_UPDATE.with(|x| x.set(now)); @@ -607,12 +647,18 @@ where }; } - if !state.is_focused.as_ref().map_or(false, |f| { - f.updated_at == LAST_FOCUS_UPDATE.with(|f| f.get()) - }) { - state.is_focused = None; + if !state + .is_focused + .as_ref() + .map(|f| f.updated_at == LAST_FOCUS_UPDATE.with(|f| f.get())) + .unwrap_or_default() + { + state.unfocus(); + // state.is_read_only = true; } + self.is_read_only = state.is_read_only; + // Stop pasting if input becomes disabled if !self.manage_value && self.on_input.is_none() { state.is_pasting = None; @@ -635,6 +681,7 @@ where .collect() } + #[inline] fn size(&self) -> Size { Size { width: self.width, @@ -790,7 +837,8 @@ where let size = self.size.unwrap_or_else(|| renderer.default_size().0); let line_height = self.line_height; - if self.is_editable { + // Disables editing of the editable variant when clicking outside of it. + if self.is_editable_variant { if let Some(ref on_edit) = self.on_toggle_edit { let state = tree.state.downcast_mut::(); if !state.is_read_only && state.is_focused.is_none() { @@ -800,34 +848,38 @@ where } } + // Calculates the layout of the trailing icon button element. if !tree.children.is_empty() { let index = tree.children.len() - 1; if let (Some(trailing_icon), Some(tree)) = (self.trailing_icon.as_mut(), tree.children.get_mut(index)) { - let children = text_layout.children(); - trailing_icon_layout = Some(children.last().unwrap()); + trailing_icon_layout = Some(text_layout.children().last().unwrap()); - if let Some(trailing_layout) = trailing_icon_layout { - if cursor_position.is_over(trailing_layout.bounds()) { - let res = trailing_icon.as_widget_mut().on_event( - tree, - event.clone(), - trailing_layout, - cursor_position, - renderer, - clipboard, - shell, - viewport, - ); + // Enable custom buttons defined on the trailing icon position to be handled. + if !self.is_editable_variant { + if let Some(trailing_layout) = trailing_icon_layout { + if cursor_position.is_over(trailing_layout.bounds()) { + let res = trailing_icon.as_widget_mut().on_event( + tree, + event.clone(), + trailing_layout, + cursor_position, + renderer, + clipboard, + shell, + viewport, + ); - if res == event::Status::Captured { - return res; + if res == event::Status::Captured { + return res; + } } } } } } + let dnd_id = self.dnd_id(); let id = Widget::id(self); update( @@ -841,11 +893,14 @@ where &mut self.value, size, font, + self.is_editable_variant, self.is_secure, - self.is_editable, + self.on_focus.as_ref(), + self.on_unfocus.as_ref(), self.on_input.as_deref(), self.on_paste.as_deref(), self.on_submit.as_deref(), + self.on_tab.as_ref(), self.on_toggle_edit.as_deref(), || tree.state.downcast_mut::(), self.on_create_dnd_source.as_deref(), @@ -856,6 +911,7 @@ where ) } + #[inline] fn draw( &self, tree: &Tree, @@ -908,9 +964,7 @@ where if let (Some(leading_icon), Some(tree)) = (self.leading_icon.as_ref(), state.children.get(index)) { - let mut children = layout.children(); - children.next(); - let leading_icon_layout = children.next().unwrap(); + let leading_icon_layout = layout.children().nth(1).unwrap(); if cursor_position.is_over(leading_icon_layout.bounds()) { return leading_icon.as_widget().mouse_interaction( @@ -954,19 +1008,21 @@ where ) } + #[inline] fn id(&self) -> Option { Some(self.id.clone()) } + #[inline] fn set_id(&mut self, id: Id) { self.id = id; } fn drag_destinations( &self, - state: &Tree, + _state: &Tree, layout: Layout<'_>, - renderer: &crate::Renderer, + _renderer: &crate::Renderer, dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, ) { if let Some(input) = layout.children().last() { @@ -1251,18 +1307,21 @@ pub fn update<'a, Message: Clone + 'static>( id: Option, event: Event, text_layout: Layout<'_>, - trailing_icon_layout: Option>, + edit_button_layout: Option>, cursor: mouse::Cursor, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, value: &mut Value, size: f32, font: ::Font, + is_editable_variant: bool, is_secure: bool, - is_editable: bool, + on_focus: Option<&Message>, + on_unfocus: Option<&Message>, on_input: Option<&dyn Fn(String) -> Message>, on_paste: Option<&dyn Fn(String) -> Message>, on_submit: Option<&dyn Fn(String) -> Message>, + on_tab: Option<&Message>, on_toggle_edit: Option<&dyn Fn(bool) -> Message>, state: impl FnOnce() -> &'a mut State, #[allow(unused_variables)] on_start_dnd_source: Option<&dyn Fn(State) -> Message>, @@ -1285,9 +1344,15 @@ pub fn update<'a, Message: Clone + 'static>( // NOTE: Clicks must be captured to prevent mouse areas behind them handling the same clicks. + /// Mark a branch as cold + #[inline] + #[cold] + fn cold() {} + match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { + cold(); let state = state(); let click_position = if on_input.is_some() || manage_value { @@ -1296,18 +1361,30 @@ pub fn update<'a, Message: Clone + 'static>( None }; - if click_position.is_some() { - state.is_focused = state.is_focused.or_else(|| { - let now = Instant::now(); - LAST_FOCUS_UPDATE.with(|x| x.set(now)); - Some(Focus { - updated_at: now, - now, - }) - }); - } - if let Some(cursor_position) = click_position { + // Check if the edit button was clicked. + if state.dragging_state == None + && edit_button_layout.map_or(false, |l| cursor.is_over(l.bounds())) + { + if is_editable_variant { + state.is_read_only = !state.is_read_only; + state.move_cursor_to_end(); + + if let Some(on_toggle_edit) = on_toggle_edit { + shell.publish(on_toggle_edit(!state.is_read_only)); + } + + let now = Instant::now(); + LAST_FOCUS_UPDATE.with(|x| x.set(now)); + state.is_focused = Some(Focus { + updated_at: now, + now, + }); + } + + return event::Status::Captured; + } + let target = cursor_position.x - text_layout.bounds().x; let click = @@ -1324,7 +1401,7 @@ pub fn update<'a, Message: Clone + 'static>( // single click that is on top of the selected text // is the click on selected text? - if manage_value || on_input.is_some() { + if on_input.is_some() || manage_value { let left = start.min(end); let right = end.max(start); @@ -1393,40 +1470,14 @@ pub fn update<'a, Message: Clone + 'static>( update_cache(state, &unsecured_value); } else { update_cache(state, value); - // existing logic for setting the selection - let position = if target > 0.0 { - find_cursor_position(text_layout.bounds(), value, state, target) - } else { - None - }; - - state.cursor.move_to(position.unwrap_or(0)); - state.dragging_state = Some(DraggingState::Selection); + state.setting_selection(value, text_layout.bounds(), target); } } else { - // existing logic for setting the selection - let position = if target > 0.0 { - update_cache(state, value); - find_cursor_position(text_layout.bounds(), value, state, target) - } else { - None - }; - - state.cursor.move_to(position.unwrap_or(0)); - state.dragging_state = Some(DraggingState::Selection); + state.setting_selection(value, text_layout.bounds(), target); } } (None, click::Kind::Single, _) => { - // existing logic for setting the selection - let position = if target > 0.0 { - update_cache(state, value); - find_cursor_position(text_layout.bounds(), value, state, target) - } else { - None - }; - - state.cursor.move_to(position.unwrap_or(0)); - state.dragging_state = Some(DraggingState::Selection); + state.setting_selection(value, text_layout.bounds(), target); } (None | Some(DraggingState::Selection), click::Kind::Double, _) => { update_cache(state, value); @@ -1455,93 +1506,41 @@ pub fn update<'a, Message: Clone + 'static>( } } - // Enable write mode when an editable input label is clicked - if is_editable - && state.is_read_only + // Focus on click of the text input, and ensure that the input is writable. + if state.is_focused.is_none() && matches!(state.dragging_state, None | Some(DraggingState::Selection)) { - state.is_read_only = false; + if let Some(on_focus) = on_focus { + shell.publish(on_focus.clone()); + } + + if state.is_read_only { + state.is_read_only = false; + if let Some(on_toggle_edit) = on_toggle_edit { + let message = (on_toggle_edit)(true); + shell.publish(message); + } + } let now = Instant::now(); LAST_FOCUS_UPDATE.with(|x| x.set(now)); + state.is_focused = Some(Focus { updated_at: now, now, }); - - if let Some(on_toggle_edit) = on_toggle_edit { - let message = (on_toggle_edit)(!state.is_read_only); - shell.publish(message); - } } state.last_click = Some(click); return event::Status::Captured; - } - - let mut is_trailing_clicked = false; - if is_editable { - if let Some(trailing_layout) = trailing_icon_layout { - is_trailing_clicked = cursor.is_over(trailing_layout.bounds()); - if is_trailing_clicked { - if on_toggle_edit.is_some() { - let Some(pos) = cursor.position() else { - return event::Status::Ignored; - }; - - let click = - mouse::Click::new(pos, mouse::Button::Left, state.last_click); - - match ( - &state.dragging_state, - click.kind(), - state.cursor().state(value), - ) { - (None, click::Kind::Single, _) => { - state.is_read_only = !state.is_read_only; - if let Some(on_toggle_edit) = on_toggle_edit { - let message = (on_toggle_edit)(!state.is_read_only); - shell.publish(message); - - let now = Instant::now(); - LAST_FOCUS_UPDATE.with(|x| x.set(now)); - state.is_focused = Some(Focus { - updated_at: now, - now, - }); - - state.move_cursor_to_end(); - } - } - _ => { - state.dragging_state = None; - } - } - } - - return event::Status::Captured; - } - } - } - - // Condition met when mouse click is completely outside of the widget. - if !is_trailing_clicked && click_position.is_none() { - state.is_focused = None; - state.dragging_state = None; - state.is_pasting = None; - state.keyboard_modifiers = keyboard::Modifiers::default(); - state.is_read_only = true; - - // Ensure clicks outside emit the toggle edit message. - if let Some(on_toggle_edit) = on_toggle_edit { - let message = (on_toggle_edit)(false); - shell.publish(message); - } + } else { + state.unfocus(); } } Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) | Event::Touch(touch::Event::FingerLifted { .. } | touch::Event::FingerLost { .. }) => { + cold(); let state = state(); state.dragging_state = None; @@ -1573,14 +1572,10 @@ pub fn update<'a, Message: Clone + 'static>( let state = state(); if let Some(focus) = &mut state.is_focused { - if !manage_value && on_input.is_none() { + if state.is_read_only || (!manage_value && on_input.is_none()) { return event::Status::Ignored; }; - if state.is_read_only { - return event::Status::Ignored; - } - let modifiers = state.keyboard_modifiers; focus.updated_at = Instant::now(); LAST_FOCUS_UPDATE.with(|x| x.set(focus.updated_at)); @@ -1750,6 +1745,7 @@ pub fn update<'a, Message: Clone + 'static>( let contents = editor.contents(); let unsecured_value = Value::new(&contents); state.tracked_value = unsecured_value.clone(); + if let Some(on_input) = on_input { let message = if let Some(paste) = &on_paste { (paste)(contents) @@ -1759,6 +1755,7 @@ pub fn update<'a, Message: Clone + 'static>( shell.publish(message); } + state.is_pasting = Some(content); let value = if is_secure { @@ -1775,17 +1772,34 @@ pub fn update<'a, Message: Clone + 'static>( state.cursor.select_all(value); } keyboard::Key::Named(keyboard::key::Named::Escape) => { - state.is_focused = None; - state.dragging_state = None; - state.is_pasting = None; + state.unfocus(); + state.is_read_only = true; - state.keyboard_modifiers = keyboard::Modifiers::default(); + if let Some(on_unfocus) = on_unfocus { + shell.publish(on_unfocus.clone()); + } + } + + keyboard::Key::Named(keyboard::key::Named::Tab) => { + if let Some(on_tab) = on_tab { + // Allow the application to decide how the event is handled. + // This could be to connect the text input to another text input. + // Or to connect the text input to a button. + shell.publish(on_tab.clone()); + } else { + state.unfocus(); + state.is_read_only = true; + + if let Some(on_unfocus) = on_unfocus { + shell.publish(on_unfocus.clone()); + } + + return event::Status::Ignored; + }; } keyboard::Key::Named( - keyboard::key::Named::ArrowUp - | keyboard::key::Named::ArrowDown - | keyboard::key::Named::Tab, + keyboard::key::Named::ArrowUp | keyboard::key::Named::ArrowDown, ) => { return event::Status::Ignored; } @@ -1819,8 +1833,6 @@ pub fn update<'a, Message: Clone + 'static>( unsecured_value }; update_cache(state, &value); - - return event::Status::Captured; } } _ => {} @@ -1869,8 +1881,10 @@ pub fn update<'a, Message: Clone + 'static>( } #[cfg(feature = "wayland")] Event::Dnd(DndEvent::Source(SourceEvent::Finished | SourceEvent::Cancelled)) => { + cold(); let state = state(); if matches!(state.dragging_state, Some(DraggingState::Dnd(..))) { + // TODO: restore value in text input state.dragging_state = None; return event::Status::Captured; } @@ -1885,6 +1899,7 @@ pub fn update<'a, Message: Clone + 'static>( surface, }, )) if rectangle == Some(dnd_id) => { + cold(); let state = state(); let is_clicked = text_layout.bounds().contains(Point { x: x as f32, @@ -1934,6 +1949,7 @@ pub fn update<'a, Message: Clone + 'static>( } #[cfg(feature = "wayland")] Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Drop)) if rectangle == Some(dnd_id) => { + cold(); let state = state(); if let DndOfferState::HandlingOffer(mime_types, _action) = state.dnd_offer.clone() { let Some(mime_type) = SUPPORTED_TEXT_MIME_TYPES @@ -1953,6 +1969,7 @@ pub fn update<'a, Message: Clone + 'static>( rectangle, OfferEvent::Leave | OfferEvent::LeaveDestination, )) if rectangle == Some(dnd_id) => { + cold(); let state = state(); // ASHLEY TODO we should be able to reset but for now we don't if we are handling a // drop @@ -1968,6 +1985,7 @@ pub fn update<'a, Message: Clone + 'static>( Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Data { data, mime_type })) if rectangle == Some(dnd_id) => { + cold(); let state = state(); if let DndOfferState::Dropped = state.dnd_offer.clone() { state.dnd_offer = DndOfferState::None; @@ -2560,18 +2578,21 @@ impl State { } /// Returns whether the [`TextInput`] is currently focused or not. + #[inline] #[must_use] pub fn is_focused(&self) -> bool { self.is_focused.is_some() } /// Returns the [`Cursor`] of the [`TextInput`]. + #[inline] #[must_use] pub fn cursor(&self) -> Cursor { self.cursor } /// Focuses the [`TextInput`]. + #[cold] pub fn focus(&mut self) { let now = Instant::now(); LAST_FOCUS_UPDATE.with(|x| x.set(now)); @@ -2589,63 +2610,90 @@ impl State { } /// Unfocuses the [`TextInput`]. - pub fn unfocus(&mut self) { + #[cold] + pub(super) fn unfocus(&mut self) { self.is_focused = None; + self.dragging_state = None; + self.is_pasting = None; + self.keyboard_modifiers = keyboard::Modifiers::default(); } /// Moves the [`Cursor`] of the [`TextInput`] to the front of the input text. + #[inline] pub fn move_cursor_to_front(&mut self) { self.cursor.move_to(0); } /// Moves the [`Cursor`] of the [`TextInput`] to the end of the input text. + #[inline] pub fn move_cursor_to_end(&mut self) { self.cursor.move_to(usize::MAX); } /// Moves the [`Cursor`] of the [`TextInput`] to an arbitrary location. + #[inline] pub fn move_cursor_to(&mut self, position: usize) { self.cursor.move_to(position); } /// Selects all the content of the [`TextInput`]. + #[inline] pub fn select_all(&mut self) { self.cursor.select_range(0, usize::MAX); } + + pub(super) fn setting_selection(&mut self, value: &Value, bounds: Rectangle, target: f32) { + let position = if target > 0.0 { + find_cursor_position(bounds, value, self, target) + } else { + None + }; + + self.cursor.move_to(position.unwrap_or(0)); + self.dragging_state = Some(DraggingState::Selection); + } } impl operation::Focusable for State { + #[inline] fn is_focused(&self) -> bool { Self::is_focused(self) } + #[inline] fn focus(&mut self) { Self::focus(self); } + #[inline] fn unfocus(&mut self) { Self::unfocus(self); } } impl operation::TextInput for State { + #[inline] fn move_cursor_to_front(&mut self) { Self::move_cursor_to_front(self); } + #[inline] fn move_cursor_to_end(&mut self) { Self::move_cursor_to_end(self); } + #[inline] fn move_cursor_to(&mut self, position: usize) { Self::move_cursor_to(self, position); } + #[inline] fn select_all(&mut self) { Self::select_all(self); } } +#[inline(never)] fn measure_cursor_and_scroll_offset( paragraph: &impl text::Paragraph, text_bounds: Rectangle, @@ -2662,6 +2710,7 @@ fn measure_cursor_and_scroll_offset( /// Computes the position of the text cursor at the given X coordinate of /// a [`TextInput`]. +#[inline(never)] fn find_cursor_position( text_bounds: Rectangle, value: &Value, @@ -2686,6 +2735,7 @@ fn find_cursor_position( ) } +#[inline(never)] fn replace_paragraph( state: &mut State, layout: Layout<'_>, @@ -2715,6 +2765,7 @@ const CURSOR_BLINK_INTERVAL_MILLIS: u128 = 500; mod platform { use iced_core::keyboard; + #[inline] pub fn is_jump_modifier_pressed(modifiers: keyboard::Modifiers) -> bool { if cfg!(target_os = "macos") { modifiers.alt() @@ -2724,6 +2775,7 @@ mod platform { } } +#[inline(never)] fn offset(text_bounds: Rectangle, value: &Value, state: &State) -> f32 { if state.is_focused() { let cursor = state.cursor(); diff --git a/src/widget/text_input/value.rs b/src/widget/text_input/value.rs index dee3f110..60647db3 100644 --- a/src/widget/text_input/value.rs +++ b/src/widget/text_input/value.rs @@ -27,12 +27,14 @@ impl Value { /// /// A [`Value`] is empty when it contains no graphemes. #[must_use] + #[inline] pub fn is_empty(&self) -> bool { self.len() == 0 } /// Returns the total amount of graphemes in the [`Value`]. #[must_use] + #[inline] pub fn len(&self) -> usize { self.graphemes.len() } @@ -75,6 +77,7 @@ impl Value { /// Returns a new [`Value`] containing the graphemes from `start` until the /// given `end`. #[must_use] + #[inline] pub fn select(&self, start: usize, end: usize) -> Self { let graphemes = self.graphemes[start.min(self.len())..end.min(self.len())].to_vec(); @@ -84,6 +87,7 @@ impl Value { /// Returns a new [`Value`] containing the graphemes until the given /// `index`. #[must_use] + #[inline] pub fn until(&self, index: usize) -> Self { let graphemes = self.graphemes[..index.min(self.len())].to_vec(); @@ -91,6 +95,7 @@ impl Value { } /// Inserts a new `char` at the given grapheme `index`. + #[inline] pub fn insert(&mut self, index: usize, c: char) { self.graphemes.insert(index, c.to_string()); @@ -100,6 +105,7 @@ impl Value { } /// Inserts a bunch of graphemes at the given grapheme `index`. + #[inline] pub fn insert_many(&mut self, index: usize, mut value: Value) { let _ = self .graphemes @@ -107,11 +113,13 @@ impl Value { } /// Removes the grapheme at the given `index`. + #[inline] pub fn remove(&mut self, index: usize) { let _ = self.graphemes.remove(index); } /// Removes the graphemes from `start` to `end`. + #[inline] pub fn remove_many(&mut self, start: usize, end: usize) { let _ = self.graphemes.splice(start..end, std::iter::empty()); } @@ -129,6 +137,7 @@ impl Value { } impl ToString for Value { + #[inline] fn to_string(&self) -> String { self.graphemes.concat() }