refactor(text_input): make on_input optional

this prevents a redraw of every surface in an application whenever the input changes.
This commit is contained in:
Ashley Wulber 2025-02-21 14:30:30 -05:00
parent ab438b87f9
commit 306995febf
No known key found for this signature in database
GPG key ID: 5216D4F46A90A820
3 changed files with 91 additions and 37 deletions

View file

@ -469,7 +469,7 @@ where
text_input("", self.input_color) text_input("", self.input_color)
.on_input(move |s| on_update(ColorPickerUpdate::Input(s))) .on_input(move |s| on_update(ColorPickerUpdate::Input(s)))
.on_paste(move |s| on_update(ColorPickerUpdate::Input(s))) .on_paste(move |s| on_update(ColorPickerUpdate::Input(s)))
.on_submit(on_update(ColorPickerUpdate::AppliedColor)) .on_submit(move |_| on_update(ColorPickerUpdate::AppliedColor))
.leading_icon( .leading_icon(
color_button( color_button(
None, None,

View file

@ -200,7 +200,7 @@ pub struct TextInput<'a, Message> {
error: Option<Cow<'a, str>>, error: Option<Cow<'a, str>>,
on_input: Option<Box<dyn Fn(String) -> Message + 'a>>, on_input: Option<Box<dyn Fn(String) -> Message + 'a>>,
on_paste: Option<Box<dyn Fn(String) -> Message + 'a>>, on_paste: Option<Box<dyn Fn(String) -> Message + 'a>>,
on_submit: Option<Message>, on_submit: Option<Box<dyn Fn(String) -> Message + 'a>>,
on_toggle_edit: Option<Box<dyn Fn(bool) -> Message + 'a>>, on_toggle_edit: Option<Box<dyn Fn(bool) -> Message + 'a>>,
leading_icon: Option<Element<'a, Message, crate::Theme, crate::Renderer>>, leading_icon: Option<Element<'a, Message, crate::Theme, crate::Renderer>>,
trailing_icon: Option<Element<'a, Message, crate::Theme, crate::Renderer>>, trailing_icon: Option<Element<'a, Message, crate::Theme, crate::Renderer>>,
@ -211,6 +211,8 @@ pub struct TextInput<'a, Message> {
line_height: text::LineHeight, line_height: text::LineHeight,
helper_line_height: text::LineHeight, helper_line_height: text::LineHeight,
always_active: bool, always_active: bool,
/// The text input tracks and manages the input value in its state.
manage_value: bool,
} }
impl<'a, Message> TextInput<'a, Message> impl<'a, Message> TextInput<'a, Message>
@ -255,6 +257,7 @@ where
label: None, label: None,
helper_text: None, helper_text: None,
always_active: false, always_active: false,
manage_value: false,
} }
} }
@ -340,14 +343,24 @@ where
/// Sets the message that should be produced when the [`TextInput`] is /// Sets the message that should be produced when the [`TextInput`] is
/// focused and the enter key is pressed. /// focused and the enter key is pressed.
pub fn on_submit(self, message: Message) -> Self { pub fn on_submit<F>(mut self, callback: F) -> Self
self.on_submit_maybe(Some(message)) 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 /// Maybe sets the message that should be produced when the [`TextInput`] is
/// focused and the enter key is pressed. /// focused and the enter key is pressed.
pub fn on_submit_maybe(mut self, message: Option<Message>) -> Self { pub fn on_submit_maybe<F>(mut self, callback: Option<F>) -> Self
self.on_submit = message; where
F: 'a + Fn(String) -> Message,
{
if let Some(callback) = callback {
self.on_submit = Some(Box::new(callback));
} else {
self.on_submit = None;
}
self self
} }
@ -416,6 +429,12 @@ where
self self
} }
/// 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;
self
}
/// Draws the [`TextInput`] with the given [`Renderer`], overriding its /// Draws the [`TextInput`] with the given [`Renderer`], overriding its
/// [`Value`] if provided. /// [`Value`] if provided.
/// ///
@ -526,9 +545,14 @@ where
fn diff(&mut self, tree: &mut Tree) { fn diff(&mut self, tree: &mut Tree) {
let state = tree.state.downcast_mut::<State>(); let state = tree.state.downcast_mut::<State>();
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() {
self.value = state.tracked_value.clone();
// std::mem::swap(&mut state.tracked_value, &mut self.value);
}
// Unfocus text input if it becomes disabled // Unfocus text input if it becomes disabled
if self.on_input.is_none() { if self.on_input.is_none() && !self.manage_value {
state.last_click = None; state.last_click = None;
state.is_focused = None; state.is_focused = None;
state.is_pasting = None; state.is_pasting = None;
@ -597,6 +621,11 @@ where
state.is_focused = None; state.is_focused = None;
} }
// Stop pasting if input becomes disabled
if !self.manage_value && self.on_input.is_none() {
state.is_pasting = None;
}
let mut children: Vec<_> = self let mut children: Vec<_> = self
.leading_icon .leading_icon
.iter_mut() .iter_mut()
@ -824,13 +853,14 @@ where
self.is_editable, self.is_editable,
self.on_input.as_deref(), self.on_input.as_deref(),
self.on_paste.as_deref(), self.on_paste.as_deref(),
&self.on_submit, self.on_submit.as_deref(),
self.on_toggle_edit.as_deref(), self.on_toggle_edit.as_deref(),
|| tree.state.downcast_mut::<State>(), || tree.state.downcast_mut::<State>(),
self.on_create_dnd_source.as_deref(), self.on_create_dnd_source.as_deref(),
dnd_id, dnd_id,
line_height, line_height,
layout, layout,
self.manage_value,
) )
} }
@ -856,7 +886,7 @@ where
&self.placeholder, &self.placeholder,
self.size, self.size,
self.font, self.font,
self.on_input.is_none(), self.on_input.is_none() && !self.manage_value,
self.is_secure, self.is_secure,
self.leading_icon.as_ref(), self.leading_icon.as_ref(),
self.trailing_icon.as_ref(), self.trailing_icon.as_ref(),
@ -925,7 +955,11 @@ where
} }
let mut children = layout.children(); let mut children = layout.children();
let layout = children.next().unwrap(); let layout = children.next().unwrap();
mouse_interaction(layout, cursor_position, self.on_input.is_none()) mouse_interaction(
layout,
cursor_position,
self.on_input.is_none() && !self.manage_value,
)
} }
fn id(&self) -> Option<Id> { fn id(&self) -> Option<Id> {
@ -1236,13 +1270,14 @@ pub fn update<'a, Message: 'static>(
is_editable: bool, is_editable: bool,
on_input: Option<&dyn Fn(String) -> Message>, on_input: Option<&dyn Fn(String) -> Message>,
on_paste: Option<&dyn Fn(String) -> Message>, on_paste: Option<&dyn Fn(String) -> Message>,
on_submit: &Option<Message>, on_submit: Option<&dyn Fn(String) -> Message>,
on_toggle_edit: Option<&dyn Fn(bool) -> Message>, on_toggle_edit: Option<&dyn Fn(bool) -> Message>,
state: impl FnOnce() -> &'a mut State, state: impl FnOnce() -> &'a mut State,
#[allow(unused_variables)] on_start_dnd_source: Option<&dyn Fn(State) -> Message>, #[allow(unused_variables)] on_start_dnd_source: Option<&dyn Fn(State) -> Message>,
#[allow(unused_variables)] dnd_id: u128, #[allow(unused_variables)] dnd_id: u128,
line_height: text::LineHeight, line_height: text::LineHeight,
layout: Layout<'_>, layout: Layout<'_>,
manage_value: bool,
) -> event::Status ) -> event::Status
where where
Message: Clone, Message: Clone,
@ -1264,7 +1299,7 @@ where
| Event::Touch(touch::Event::FingerPressed { .. }) => { | Event::Touch(touch::Event::FingerPressed { .. }) => {
let state = state(); let state = state();
let click_position = if on_input.is_some() { let click_position = if on_input.is_some() || manage_value {
cursor.position_over(layout.bounds()) cursor.position_over(layout.bounds())
} else { } else {
None None
@ -1299,7 +1334,7 @@ where
// single click that is on top of the selected text // single click that is on top of the selected text
// is the click on selected text? // is the click on selected text?
if let Some(on_input) = on_input { if manage_value || on_input.is_some() {
let left = start.min(end); let left = start.min(end);
let right = end.max(start); let right = end.max(start);
@ -1339,8 +1374,11 @@ where
let contents = editor.contents(); let contents = editor.contents();
let unsecured_value = Value::new(&contents); let unsecured_value = Value::new(&contents);
state.tracked_value = unsecured_value.clone();
if let Some(on_input) = on_input {
let message = (on_input)(contents); let message = (on_input)(contents);
shell.publish(message); shell.publish(message);
}
if let Some(on_start_dnd) = on_start_dnd_source { if let Some(on_start_dnd) = on_start_dnd_source {
shell.publish(on_start_dnd(state.clone())); shell.publish(on_start_dnd(state.clone()));
} }
@ -1531,7 +1569,7 @@ where
let state = state(); let state = state();
if let Some(focus) = &mut state.is_focused { if let Some(focus) = &mut state.is_focused {
let Some(on_input) = on_input else { if !manage_value && on_input.is_none() {
return event::Status::Ignored; return event::Status::Ignored;
}; };
@ -1546,7 +1584,7 @@ where
match key { match key {
keyboard::Key::Named(keyboard::key::Named::Enter) => { keyboard::Key::Named(keyboard::key::Named::Enter) => {
if let Some(on_submit) = on_submit.clone() { if let Some(on_submit) = on_submit.clone() {
shell.publish(on_submit); shell.publish((on_submit)(unsecured_value.to_string()));
} }
} }
keyboard::Key::Named(keyboard::key::Named::Backspace) => { keyboard::Key::Named(keyboard::key::Named::Backspace) => {
@ -1566,9 +1604,11 @@ where
let contents = editor.contents(); let contents = editor.contents();
let unsecured_value = Value::new(&contents); let unsecured_value = Value::new(&contents);
state.tracked_value = unsecured_value.clone();
if let Some(on_input) = on_input {
let message = (on_input)(editor.contents()); let message = (on_input)(editor.contents());
shell.publish(message); shell.publish(message);
}
let value = if is_secure { let value = if is_secure {
unsecured_value.secure() unsecured_value.secure()
} else { } else {
@ -1592,8 +1632,12 @@ where
editor.delete(); editor.delete();
let contents = editor.contents(); let contents = editor.contents();
let unsecured_value = Value::new(&contents); let unsecured_value = Value::new(&contents);
if let Some(on_input) = on_input {
let message = (on_input)(contents); let message = (on_input)(contents);
state.tracked_value = unsecured_value.clone();
shell.publish(message); shell.publish(message);
}
let value = if is_secure { let value = if is_secure {
unsecured_value.secure() unsecured_value.secure()
} else { } else {
@ -1671,12 +1715,14 @@ where
let mut editor = Editor::new(value, &mut state.cursor); let mut editor = Editor::new(value, &mut state.cursor);
editor.delete(); editor.delete();
let content = editor.contents();
let message = (on_input)(editor.contents()); state.tracked_value = Value::new(&content);
if let Some(on_input) = on_input {
let message = (on_input)(content);
shell.publish(message); shell.publish(message);
} }
} }
}
keyboard::Key::Character(c) keyboard::Key::Character(c)
if "v" == c && state.keyboard_modifiers.command() => if "v" == c && state.keyboard_modifiers.command() =>
{ {
@ -1699,13 +1745,16 @@ where
let contents = editor.contents(); let contents = editor.contents();
let unsecured_value = Value::new(&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 { let message = if let Some(paste) = &on_paste {
(paste)(contents) (paste)(contents)
} else { } else {
(on_input)(contents) (on_input)(contents)
}; };
shell.publish(message);
shell.publish(message);
}
state.is_pasting = Some(content); state.is_pasting = Some(content);
let value = if is_secure { let value = if is_secure {
@ -1750,8 +1799,11 @@ where
} }
let contents = editor.contents(); let contents = editor.contents();
let unsecured_value = Value::new(&contents); let unsecured_value = Value::new(&contents);
state.tracked_value = unsecured_value.clone();
if let Some(on_input) = on_input {
let message = (on_input)(contents); let message = (on_input)(contents);
shell.publish(message); shell.publish(message);
}
focus.updated_at = Instant::now(); focus.updated_at = Instant::now();
LAST_FOCUS_UPDATE.with(|x| x.set(focus.updated_at)); LAST_FOCUS_UPDATE.with(|x| x.set(focus.updated_at));
@ -1926,7 +1978,7 @@ where
editor.paste(Value::new(content.as_str())); editor.paste(Value::new(content.as_str()));
let contents = editor.contents(); let contents = editor.contents();
let unsecured_value = Value::new(&contents); let unsecured_value = Value::new(&contents);
state.tracked_value = unsecured_value.clone();
if let Some(on_paste) = on_paste.as_ref() { if let Some(on_paste) = on_paste.as_ref() {
let message = (on_paste)(contents); let message = (on_paste)(contents);
shell.publish(message); shell.publish(message);
@ -2408,6 +2460,7 @@ pub(crate) struct DndOfferState;
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
#[must_use] #[must_use]
pub struct State { pub struct State {
pub tracked_value: Value,
pub value: crate::Plain, pub value: crate::Plain,
pub placeholder: crate::Plain, pub placeholder: crate::Plain,
pub label: crate::Plain, pub label: crate::Plain,
@ -2482,6 +2535,7 @@ impl State {
/// Creates a new [`State`], representing a focused [`TextInput`]. /// Creates a new [`State`], representing a focused [`TextInput`].
pub fn focused(is_secure: bool, is_read_only: bool) -> Self { pub fn focused(is_secure: bool, is_read_only: bool) -> Self {
Self { Self {
tracked_value: Value::default(),
is_secure, is_secure,
value: crate::Plain::default(), value: crate::Plain::default(),
placeholder: crate::Plain::default(), placeholder: crate::Plain::default(),

View file

@ -8,7 +8,7 @@ use unicode_segmentation::UnicodeSegmentation;
/// ///
/// [`TextInput`]: crate::widget::TextInput /// [`TextInput`]: crate::widget::TextInput
// TODO: Reduce allocations, cache results (?) // TODO: Reduce allocations, cache results (?)
#[derive(Debug, Clone)] #[derive(Default, Debug, Clone, PartialEq)]
pub struct Value { pub struct Value {
graphemes: Vec<String>, graphemes: Vec<String>,
} }