From 177c88c3f42c6192beaada6b8afab892c8ccbf2c Mon Sep 17 00:00:00 2001 From: Ultraxime Date: Tue, 3 Dec 2024 00:43:39 +0100 Subject: [PATCH 1/3] Rewriting the logic of number_input --- Cargo.toml | 8 +- examples/number_input.rs | 20 +- examples/typed_input.rs | 18 +- src/widget/helpers.rs | 6 +- src/widget/number_input.rs | 450 +++++++++++++++++++++++++------------ src/widget/typed_input.rs | 57 ++--- 6 files changed, 353 insertions(+), 206 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a929b05..8ca5719 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,12 +60,12 @@ default = [ [dependencies] cfg-if = "1.0" -chrono = { version = "0.4.38", optional = true, features = ["wasmbind"]} +chrono = { version = "0.4.38", optional = true, features = ["wasmbind"] } +getrandom = { version = "0.2", features = ["js"] } +iced_fonts = "0.1.1" itertools = { version = "0.13.0", optional = true } num-format = { version = "0.4.4", optional = true } num-traits = { version = "0.2.19", optional = true } -iced_fonts = "0.1.1" -getrandom = { version = "0.2", features = ["js"] } web-time = "1.1.0" [dependencies.iced] @@ -162,4 +162,4 @@ name = "wrap" required-features = ["wrap", "number_input"] [lib] -crate-type = ["cdylib", "rlib"] \ No newline at end of file +crate-type = ["cdylib", "rlib"] diff --git a/examples/number_input.rs b/examples/number_input.rs index bb17d0a..8c8aae3 100644 --- a/examples/number_input.rs +++ b/examples/number_input.rs @@ -15,7 +15,8 @@ pub struct NumberInputDemo { #[derive(Debug, Clone)] pub enum Message { - NumInpChanged(Result), + NumInpChanged(f32), + NumInpSubmitted, } fn main() -> iced::Result { @@ -34,19 +35,22 @@ fn main() -> iced::Result { impl NumberInputDemo { fn update(&mut self, message: self::Message) { - if let Message::NumInpChanged(Ok(val)) = message { - println!("Value changed to {:?}", val); - self.value = val; - } else if let Message::NumInpChanged(Err(_)) = message { - println!("Error Value reset to 0.0"); - self.value = 0.0; + match message { + Message::NumInpChanged(val) => { + println!("Value changed to {:?}", val); + self.value = val; + } + Message::NumInpSubmitted => { + println!("Value submitted"); + } } } fn view(&self) -> Element { let lb_minute = Text::new("Number Input:"); - let txt_minute = number_input(self.value, -10.0..250.0, Message::NumInpChanged) + let txt_minute = number_input(&self.value, -10.0..250.0, Message::NumInpChanged) .style(number_input::number_input::primary) + .on_submit(Message::NumInpSubmitted) .step(0.5); Container::new( diff --git a/examples/typed_input.rs b/examples/typed_input.rs index 521ec29..9372dd0 100644 --- a/examples/typed_input.rs +++ b/examples/typed_input.rs @@ -15,7 +15,8 @@ pub struct TypedInputDemo { #[derive(Debug, Clone)] pub enum Message { - TypedInpChanged(Result), + TypedInpChanged(f32), + TypedInpSubmit(Result), } fn main() -> iced::Result { @@ -34,16 +35,23 @@ fn main() -> iced::Result { impl TypedInputDemo { fn update(&mut self, message: self::Message) { - if let Message::TypedInpChanged(Ok(val)) = message { - println!("Value changed to {:?}", val); - self.value = val; + match message { + Message::TypedInpChanged(value) => { + println!("Value changed to {}", value); + self.value = value; + } + Message::TypedInpSubmit(Ok(value)) => println!("Value submitted: {}", value), + Message::TypedInpSubmit(Err(text)) => { + println!("Value submitted while invalid: {}", text) + } } } fn view(&self) -> Element { let lb_minute = Text::new("Typed Input:"); let txt_minute = typed_input::TypedInput::new("Placeholder", &self.value) - .on_input(Message::TypedInpChanged); + .on_input(Message::TypedInpChanged) + .on_submit(Message::TypedInpSubmit); Container::new( Row::new() diff --git a/src/widget/helpers.rs b/src/widget/helpers.rs index a1e328a..cc76d07 100644 --- a/src/widget/helpers.rs +++ b/src/widget/helpers.rs @@ -308,7 +308,7 @@ where /// [`NumberInput`]: crate::NumberInput #[must_use] pub fn number_input<'a, T, Message, Theme, Renderer, F>( - value: T, + value: &T, bounds: impl RangeBounds, on_change: F, ) -> crate::NumberInput<'a, T, Message, Theme, Renderer> @@ -316,7 +316,7 @@ where Message: Clone + 'a, Renderer: iced::advanced::text::Renderer, Theme: crate::style::number_input::ExtendedCatalog, - F: 'static + Fn(Result) -> Message + Copy, + F: 'static + Fn(T) -> Message + Copy, T: 'static + num_traits::Num + num_traits::NumAssignOps @@ -342,7 +342,7 @@ where Message: Clone, Renderer: iced::advanced::text::Renderer, Theme: iced::widget::text_input::Catalog, - F: 'static + Fn(Result) -> Message + Copy, + F: 'static + Fn(T) -> Message + Copy, T: 'static + std::fmt::Display + std::str::FromStr + Clone, { crate::TypedInput::new("", value).on_input(on_change) diff --git a/src/widget/number_input.rs b/src/widget/number_input.rs index 74e7ed1..35bfd74 100644 --- a/src/widget/number_input.rs +++ b/src/widget/number_input.rs @@ -75,32 +75,45 @@ where /// The step for each modify of the [`NumberInput`]. step: T, /// The min value of the [`NumberInput`]. - min: T, + min: Bound, /// The max value of the [`NumberInput`]. - max: T, + max: Bound, /// The content padding of the [`NumberInput`]. padding: iced::Padding, /// The text size of the [`NumberInput`]. size: Option, /// The underlying element of the [`NumberInput`]. - content: TypedInput<'a, T, Message, Theme, Renderer>, + content: TypedInput<'a, T, InternalMessage, Theme, Renderer>, /// The ``on_change`` event of the [`NumberInput`]. - on_change: Box) -> Message>, + on_change: Box Message>, + /// The ``on_submit`` event of the [`NumberInput`]. + #[allow(clippy::type_complexity)] + on_submit: Option, + /// The ``on_paste`` event of the [`NumberInput`] + on_paste: Option Message>>, /// The style of the [`NumberInput`]. class: ::Class<'a>, /// The font text of the [`NumberInput`]. font: Renderer::Font, - /// The Width to use for the ``NumberBox`` Default is ``Length::Fill`` - width: Length, + // /// The Width to use for the ``NumberBox`` Default is ``Length::Fill`` + // width: Length, /// Ignore mouse scroll events for the [`NumberInput`] Default is ``false``. ignore_scroll_events: bool, /// Ignore drawing increase and decrease buttons [`NumberInput`] Default is ``false``. ignore_buttons: bool, } +#[derive(Debug, Clone, PartialEq)] +#[allow(clippy::enum_variant_names)] +enum InternalMessage { + OnChange(T), + OnSubmit(Result), + OnPaste(T), +} + impl<'a, T, Message, Theme, Renderer> NumberInput<'a, T, Message, Theme, Renderer> where - T: Num + NumAssignOps + PartialOrd + Display + FromStr + Copy + Bounded, + T: Num + NumAssignOps + PartialOrd + Display + FromStr + Clone + Bounded + 'a, Message: Clone + 'a, Renderer: iced::advanced::text::Renderer, Theme: number_input::ExtendedCatalog, @@ -108,33 +121,34 @@ where /// Creates a new [`NumberInput`]. /// /// It expects: - /// - some [`State`] /// - the current value - /// - the max value + /// - the bound values /// - a function that produces a message when the [`NumberInput`] changes - pub fn new(value: T, bounds: impl RangeBounds, on_change: F) -> Self + pub fn new(value: &T, bounds: impl RangeBounds, on_change: F) -> Self where - F: 'static + Fn(Result) -> Message + Copy, + F: 'static + Fn(T) -> Message + Clone, T: 'static, { let padding = DEFAULT_PADDING; Self { - value, + value: value.clone(), step: T::one(), - min: Self::set_min(bounds.start_bound()), - max: Self::set_max(bounds.end_bound()), + min: bounds.start_bound().cloned(), + max: bounds.end_bound().cloned(), padding, size: None, - content: TypedInput::new("", &value) - .on_input(on_change) + content: TypedInput::new("", value) + .on_input(InternalMessage::OnChange) .padding(padding) .width(Length::Fixed(127.0)) .class(Theme::default_input()), on_change: Box::new(on_change), + on_submit: None, + on_paste: None, class: ::default(), font: Renderer::Font::default(), - width: Length::Shrink, + // width: Length::Shrink, ignore_scroll_events: false, ignore_buttons: false, } @@ -149,19 +163,26 @@ where /// ``` #[must_use] pub fn bounds(mut self, bounds: impl RangeBounds) -> Self { - self.min = Self::set_min(bounds.start_bound()); - self.max = Self::set_max(bounds.end_bound()); + self.min = bounds.start_bound().cloned(); + self.max = bounds.end_bound().cloned(); self } - /// Sets the content width of the [`NumberInput`]. + /// Sets the width of the [`NumberInput`]. #[must_use] - pub fn content_width(mut self, width: impl Into) -> Self { + pub fn width(mut self, width: impl Into) -> Self { self.content = self.content.width(width); self } + /// Sets the width of the [`NumberInput`]. + #[deprecated(since = "0.11.1", note = "use `width` instead")] + #[must_use] + pub fn content_width(self, width: impl Into) -> Self { + self.width(width) + } + /// Sets the [`Font`] of the [`Text`]. /// /// [`Font`]: iced::Font @@ -194,7 +215,19 @@ where /// focused and the enter key is pressed. #[must_use] pub fn on_submit(mut self, message: Message) -> Self { - self.content = self.content.on_submit(move |_| message.clone()); + self.content = self.content.on_submit(InternalMessage::OnSubmit); + self.on_submit = Some(message); + self + } + + /// Sets the message that should be produced when some text is pasted into the [`NumberInput`], resulting in a valid value + #[must_use] + pub fn on_paste(mut self, callback: F) -> Self + where + F: 'a + Fn(T) -> Message, + { + self.content = self.content.on_paste(InternalMessage::OnPaste); + self.on_paste = Some(Box::new(callback)); self } @@ -233,49 +266,77 @@ where self } - /// Sets the width of the [`NumberInput`]. - #[must_use] - pub fn width(mut self, width: impl Into) -> Self { - self.width = width.into(); - self - } - /// Decrease current value by step of the [`NumberInput`]. fn decrease_value(&mut self, shell: &mut Shell) { - if self.value < self.min + self.step { - self.value = self.min; + if self.valid(&(self.value.clone() - self.step.clone())) { + self.value -= self.step.clone(); + } else if self.value > self.min() { + self.value = self.min(); } else { - self.value -= self.step; + return; } - shell.publish((self.on_change)(Ok(self.value))); + shell.publish((self.on_change)(self.value.clone())); } /// Increase current value by step of the [`NumberInput`]. fn increase_value(&mut self, shell: &mut Shell) { - if self.value > self.max - self.step { - self.value = self.max; + if self.valid(&(self.value.clone() + self.step.clone())) { + self.value += self.step.clone(); + } else if self.value < self.max() { + self.value = self.max(); } else { - self.value += self.step; + return; } - shell.publish((self.on_change)(Ok(self.value))); + + shell.publish((self.on_change)(self.value.clone())); } - fn set_min(min: Bound<&T>) -> T { - match min { - Bound::Included(n) | Bound::Excluded(n) => *n, + fn min(&self) -> T { + match &self.min { + Bound::Included(n) => n.clone(), + Bound::Excluded(n) => n.clone() + self.step.clone(), Bound::Unbounded => T::min_value(), } } - fn set_max(max: Bound<&T>) -> T { - match max { - Bound::Included(n) => *n, - Bound::Excluded(n) => *n - T::one(), + fn max(&self) -> T { + match &self.max { + Bound::Included(n) => n.clone(), + Bound::Excluded(n) => n.clone() - self.step.clone(), Bound::Unbounded => T::max_value(), } } + fn valid(&self, value: &T) -> bool { + (match &self.min { + Bound::Included(n) if *n > *value => false, + Bound::Excluded(n) if *n >= *value => false, + _ => true, + }) && (match &self.max { + Bound::Included(n) if *n < *value => false, + Bound::Excluded(n) if *n <= *value => false, + _ => true, + }) + } + + fn can_increase(&self) -> bool { + self.valid(&(self.value.clone() + self.step.clone())) || self.value < self.max() + } + + fn can_decrease(&self) -> bool { + self.valid(&(self.value.clone() - self.step.clone())) || self.value > self.min() + } + + fn disabled(&self) -> bool { + match (&self.min, &self.max) { + (Bound::Included(n) | Bound::Excluded(n), Bound::Included(m) | Bound::Excluded(m)) => { + *n >= *m + } + _ => false, + } + } + /// Sets the style of the input of the [`NumberInput`]. #[must_use] pub fn input_style( @@ -310,7 +371,7 @@ where impl<'a, T, Message, Theme, Renderer> Widget for NumberInput<'a, T, Message, Theme, Renderer> where - T: Num + NumAssignOps + PartialOrd + Display + FromStr + ToString + Copy + Bounded, + T: Num + NumAssignOps + PartialOrd + Display + FromStr + ToString + Clone + Bounded + 'a, Message: 'a + Clone, Renderer: 'a + iced::advanced::text::Renderer, Theme: number_input::ExtendedCatalog, @@ -343,7 +404,8 @@ where } fn size(&self) -> Size { - Size::new(self.width, Length::Shrink) + // Size::new(self.width, Length::Shrink) + Widget::size(&self.content) } fn layout(&self, tree: &mut Tree, renderer: &Renderer, limits: &Limits) -> Node { @@ -454,9 +516,11 @@ where .expect("fail to get decrease mod layout") .bounds(); - if self.min == self.max { + if self.disabled() { return event::Status::Ignored; } + let can_decrease = self.can_decrease(); + let can_increase = self.can_increase(); let cursor_position = cursor.position().unwrap_or_default(); let mouse_over_widget = layout.bounds().contains(cursor_position); @@ -464,128 +528,177 @@ where let mouse_over_dec = dec_bounds.contains(cursor_position); let mouse_over_button = mouse_over_inc || mouse_over_dec; + let modifiers = state.state.downcast_mut::(); + let mut value = self.content.text().to_owned(); + let child = state.children.get_mut(0).expect("fail to get child"); let text_input = child .state .downcast_mut::>(); - let modifiers = state.state.downcast_mut::(); - let current_text = self.content.text().to_owned(); + let mut messages = Vec::new(); + let mut sub_shell = Shell::new(&mut messages); - let mut forward_to_text = |event, shell, child, clipboard| { - self.content.on_event( - child, event, content, cursor, renderer, clipboard, shell, viewport, + let mut forward_to_text = |widget: &mut Self, child, clipboard| { + widget.content.on_event( + child, + event.clone(), + content, + cursor, + renderer, + clipboard, + &mut sub_shell, + viewport, ) }; - match &event { - Event::Keyboard(ke) => { + let mut check_value = |value: &str| { + if let Ok(value) = T::from_str(value) { + self.valid(&value) + } else if value.is_empty() { + self.value = T::zero(); + true + } else { + false + } + }; + + let status = match &event { + Event::Keyboard(key) => { if !text_input.is_focused() { return event::Status::Ignored; } - let (key, text) = match ke { - keyboard::Event::KeyPressed { key, text, .. } => (key, text), - keyboard::Event::ModifiersChanged(_) => { - return forward_to_text(event, shell, child, clipboard) - } + + match key { + keyboard::Event::ModifiersChanged(_) => forward_to_text(self, child, clipboard), keyboard::Event::KeyReleased { .. } => return event::Status::Ignored, - }; - match text { - Some(text) => { - if text == "\u{1}" || text == "\u{3}" { - // CTRL + a and CTRL + c - forward_to_text(event, shell, child, clipboard) - } else if key == &keyboard::Key::Named(keyboard::key::Named::Backspace) { - // Backspace - - let mut new_val = current_text; - match text_input.cursor().state(&Value::new(&new_val)) { - cursor::State::Index(idx) if idx >= 1 && idx <= new_val.len() => { - _ = new_val.remove(idx - 1); - } - cursor::State::Selection { start, end } - if start <= new_val.len() && end <= new_val.len() => - { - new_val.replace_range(start.min(end)..start.max(end), ""); + keyboard::Event::KeyPressed { + key, + text, + modifiers, + .. + } => { + let cursor = text_input.cursor(); + + match key.as_ref() { + keyboard::Key::Character("c" | "a") if modifiers.command() => { + forward_to_text(self, child, clipboard) + } + keyboard::Key::Character("x") if modifiers.command() => { + if let Some((start, end)) = cursor.selection(&Value::new(&value)) { + let _ = value.drain(start..end); + if check_value(&value) { + forward_to_text(self, child, clipboard) + } else { + return event::Status::Ignored; + } + } else { + return event::Status::Ignored; } - _ => return event::Status::Ignored, } + keyboard::Key::Character("v") if modifiers.command() => { + let Some(paste) = + clipboard.read(iced::advanced::clipboard::Kind::Standard) + else { + return event::Status::Ignored; + }; + match cursor.state(&Value::new(&value)) { + cursor::State::Index(idx) => { + let () = value.insert_str(idx, &paste); + } + cursor::State::Selection { start, end } if end >= start => { + let () = value.replace_range(start..end, &paste); + } + cursor::State::Selection { start, end } => { + let () = value.replace_range(end..start, &paste); + } + } - if new_val.is_empty() { - new_val = T::zero().to_string(); + if check_value(&value) { + forward_to_text(self, child, clipboard) + } else { + return event::Status::Ignored; + } } - - match T::from_str(&new_val) { - Ok(val) - if val >= self.min && val <= self.max && val != self.value => - { - self.value = val; - forward_to_text(event, shell, child, clipboard) + keyboard::Key::Named(keyboard::key::Named::Enter) => { + forward_to_text(self, child, clipboard) + } + keyboard::Key::Named(keyboard::key::Named::Backspace) => { + match cursor.state(&Value::new(&value)) { + cursor::State::Selection { start, end } => { + let _ = value.drain(start..end); + } + cursor::State::Index(idx) if idx > 0 => { + let _ = value.remove(idx - 1); + } + cursor::State::Index(_) => return event::Status::Ignored, } - Ok(val) if val >= self.min && val <= self.max => { - forward_to_text(event, shell, child, clipboard) + + if check_value(&value) { + forward_to_text(self, child, clipboard) + } else { + return event::Status::Ignored; } - Ok(_) => event::Status::Captured, - _ => forward_to_text(event, shell, child, clipboard), } - } else { - let input = if text == "\u{16}" { - // CTRL + v - match clipboard.read(iced::advanced::clipboard::Kind::Standard) { - Some(paste) => paste, - None => return event::Status::Ignored, + keyboard::Key::Named(keyboard::key::Named::Delete) => { + match cursor.state(&Value::new(&value)) { + cursor::State::Selection { start, end } => { + let _ = value.drain(start..end); + } + cursor::State::Index(idx) if idx < value.len() => { + let _ = value.remove(idx); + } + cursor::State::Index(_) => return event::Status::Ignored, } - } else { - text.to_string() - }; - - let input = input.trim(); - let mut new_val = current_text; - match text_input.cursor().state(&Value::new(&new_val)) { - cursor::State::Index(idx) if idx <= new_val.len() => { - new_val.insert_str(idx, input); + if check_value(&value) { + forward_to_text(self, child, clipboard) + } else { + return event::Status::Ignored; } - cursor::State::Selection { start, end } - if start <= new_val.len() && end <= new_val.len() => - { - new_val.replace_range(start.min(end)..end.max(start), input); - } - _ => return event::Status::Ignored, } + keyboard::Key::Named(keyboard::key::Named::ArrowDown) + if can_decrease => + { + self.decrease_value(shell); - match T::from_str(&new_val) { - Ok(val) - if val >= self.min && val <= self.max && val != self.value => - { - self.value = val; - forward_to_text(event, shell, child, clipboard) - } - Ok(val) if val >= self.min && val <= self.max => { - forward_to_text(event, shell, child, clipboard) - } - Ok(_) => event::Status::Captured, - _ => forward_to_text(event, shell, child, clipboard), + event::Status::Captured + } + keyboard::Key::Named(keyboard::key::Named::ArrowUp) if can_increase => { + self.increase_value(shell); + + event::Status::Captured } + keyboard::Key::Named( + keyboard::key::Named::ArrowLeft + | keyboard::key::Named::ArrowRight + | keyboard::key::Named::Home + | keyboard::key::Named::End, + ) => forward_to_text(self, child, clipboard), + _ => match text { + Some(text) => { + match cursor.state(&Value::new(&value)) { + cursor::State::Index(idx) => { + let () = value.insert_str(idx, text); + } + cursor::State::Selection { start, end } if end >= start => { + let () = value.replace_range(start..end, text); + } + cursor::State::Selection { start, end } => { + let () = value.replace_range(end..start, text); + } + } + + if check_value(&value) { + forward_to_text(self, child, clipboard) + } else { + return event::Status::Ignored; + } + } + None => return event::Status::Ignored, + }, } } - None => match key { - keyboard::Key::Named(keyboard::key::Named::ArrowDown) => { - self.decrease_value(shell); - event::Status::Captured - } - keyboard::Key::Named(keyboard::key::Named::ArrowUp) => { - self.increase_value(shell); - event::Status::Captured - } - keyboard::Key::Named( - keyboard::key::Named::ArrowLeft - | keyboard::key::Named::ArrowRight - | keyboard::key::Named::Home - | keyboard::key::Named::End, - ) => forward_to_text(event, shell, child, clipboard), - _ => event::Status::Ignored, - }, } } Event::Mouse(mouse::Event::WheelScrolled { delta }) @@ -624,8 +737,51 @@ where } event::Status::Captured } - _ => forward_to_text(event, shell, child, clipboard), + _ => forward_to_text(self, child, clipboard), + }; + if let Some(redraw) = sub_shell.redraw_request() { + shell.request_redraw(redraw); + } + if sub_shell.is_layout_invalid() { + shell.invalidate_layout(); + } + if sub_shell.are_widgets_invalid() { + shell.invalidate_widgets(); + } + + for message in messages { + match message { + InternalMessage::OnChange(value) => { + if self.value != value { + self.value = value.clone(); + shell.publish((self.on_change)(value)); + }; + shell.invalidate_layout(); + } + InternalMessage::OnSubmit(result) => { + if let Err(text) = result { + assert!( + text.is_empty(), + "We shouldn't be able to submit a number input with an invalid value" + ); + } + if let Some(on_submit) = &self.on_submit { + shell.publish(on_submit.clone()); + }; + shell.invalidate_layout(); + } + InternalMessage::OnPaste(value) => { + if self.value != value { + self.value = value.clone(); + if let Some(on_paste) = &self.on_paste { + shell.publish(on_paste(value)); + } + }; + shell.invalidate_layout(); + } + } } + status } fn mouse_interaction( @@ -652,8 +808,8 @@ where .expect("fail to get decrease mod layout") .bounds(); let is_mouse_over = bounds.contains(cursor.position().unwrap_or_default()); - let is_decrease_disabled = self.value <= self.min || self.min == self.max; - let is_increase_disabled = self.value >= self.max || self.min == self.max; + let is_decrease_disabled = !self.can_decrease(); + let is_increase_disabled = !self.can_increase(); let mouse_over_decrease = dec_bounds.contains(cursor.position().unwrap_or_default()); let mouse_over_increase = inc_bounds.contains(cursor.position().unwrap_or_default()); @@ -702,8 +858,8 @@ where cursor, viewport, ); - let is_decrease_disabled = self.value <= self.min || self.min == self.max; - let is_increase_disabled = self.value >= self.max || self.min == self.max; + let is_decrease_disabled = !self.can_decrease(); + let is_increase_disabled = !self.can_increase(); let decrease_btn_style = if is_decrease_disabled { style::number_input::Catalog::style(theme, &self.class, Status::Disabled) @@ -812,7 +968,7 @@ pub struct ModifierState { impl<'a, T, Message, Theme, Renderer> From> for Element<'a, Message, Theme, Renderer> where - T: 'a + Num + NumAssignOps + PartialOrd + Display + FromStr + Copy + Bounded, + T: 'a + Num + NumAssignOps + PartialOrd + Display + FromStr + Clone + Bounded, Message: 'a + Clone, Renderer: 'a + iced::advanced::text::Renderer, Theme: 'a + number_input::ExtendedCatalog, diff --git a/src/widget/typed_input.rs b/src/widget/typed_input.rs index 7ad578e..f8f6d1f 100644 --- a/src/widget/typed_input.rs +++ b/src/widget/typed_input.rs @@ -50,12 +50,12 @@ where text_input: text_input::TextInput<'a, InternalMessage, Theme, Renderer>, text: String, /// The ``on_change`` event of the [`TypedInput`]. - on_change: Option) -> Message>>, + on_change: Option Message>>, /// The ``on_submit`` event of the [`TypedInput`]. #[allow(clippy::type_complexity)] on_submit: Option) -> Message>>, /// The ``on_paste`` event of the [`TypedInput`] - on_paste: Option) -> Message>>, + on_paste: Option Message>>, } #[derive(Debug, Clone, PartialEq)] @@ -118,7 +118,7 @@ where #[must_use] pub fn on_input(mut self, callback: F) -> Self where - F: 'a + Fn(Result) -> Message, + F: 'a + Fn(T) -> Message, { self.text_input = self.text_input.on_input(InternalMessage::OnChange); self.on_change = Some(Box::new(callback)); @@ -146,7 +146,7 @@ where #[must_use] pub fn on_paste(mut self, callback: F) -> Self where - F: 'a + Fn(Result) -> Message, + F: 'a + Fn(T) -> Message, { self.text_input = self.text_input.on_paste(InternalMessage::OnPaste); self.on_paste = Some(Box::new(callback)); @@ -348,58 +348,37 @@ where InternalMessage::OnChange(value) => { self.text = value; - if self.text.ends_with('.') { - self.text.push('0'); - } - - let value = match T::from_str(&self.text) { - Ok(val) if self.value != val => { - self.value = val.clone(); - Ok(val) + if let Ok(value) = T::from_str(&self.text) { + if self.value != value { + self.value = value.clone(); + if let Some(on_change) = &self.on_change { + shell.publish(on_change(value)); + } } - Ok(val) => Ok(val), - Err(_) => Err(self.text.clone()), }; - - if let Some(on_change) = &self.on_change { - shell.publish(on_change(value)); - } - shell.invalidate_layout(); } InternalMessage::OnSubmit => { if let Some(on_submit) = &self.on_submit { - if self.text.ends_with('.') { - self.text.push('0'); - } - let value = match T::from_str(&self.text) { Ok(v) => Ok(v), Err(_) => Err(self.text.clone()), }; shell.publish(on_submit(value)); - } + }; + shell.invalidate_layout(); } InternalMessage::OnPaste(value) => { self.text = value; - if self.text.ends_with('.') { - self.text.push('0'); - } - - let value = match T::from_str(&self.text) { - Ok(val) if self.value != val => { - self.value = val.clone(); - Ok(val) + if let Ok(value) = T::from_str(&self.text) { + if self.value != value { + self.value = value.clone(); + if let Some(on_paste) = &self.on_paste { + shell.publish(on_paste(value)); + } } - Ok(val) => Ok(val), - Err(_) => Err(self.text.clone()), }; - - if let Some(on_paste) = &self.on_paste { - shell.publish(on_paste(value)); - } - shell.invalidate_layout(); } } From 794ce0667a6507a3d90e16d7e419127b48009963 Mon Sep 17 00:00:00 2001 From: Ultraxime Date: Tue, 3 Dec 2024 01:09:47 +0100 Subject: [PATCH 2/3] Adding some comment on the number_input --- .pre-commit-config.yaml | 4 +-- CHANGELOG.md | 2 +- README.md | 1 - src/widget/number_input.rs | 65 ++++++++++++++++++++++++++++++++++---- 4 files changed, 62 insertions(+), 10 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 199c01f..5616651 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -36,7 +36,7 @@ repos: - id: clippy - repo: https://github.com/DevinR528/cargo-sort - rev: v1.0.9 + rev: v1.1.0 hooks: - id: cargo-sort - repo: local diff --git a/CHANGELOG.md b/CHANGELOG.md index 62bc725..daeadc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.10.0] - 2024-09-18 -### Added +### Added - Typed Input @Ultraxime ### Changes diff --git a/README.md b/README.md index 5d08a3e..630ade1 100644 --- a/README.md +++ b/README.md @@ -164,4 +164,3 @@ Also included in this feature, are two widgets `sidebar::column::FlushColumn` an ### Color palette This crate adds a predefined color palette based on the [CSS color palette](https://www.w3schools.com/cssref/css_colors.asp). - diff --git a/src/widget/number_input.rs b/src/widget/number_input.rs index 35bfd74..e396c35 100644 --- a/src/widget/number_input.rs +++ b/src/widget/number_input.rs @@ -292,6 +292,8 @@ where shell.publish((self.on_change)(self.value.clone())); } + /// Returns the lower value possible + /// if the bound is excluded the bound is increased by the step fn min(&self) -> T { match &self.min { Bound::Included(n) => n.clone(), @@ -300,6 +302,8 @@ where } } + /// Returns the higher value possible + /// if the bound is excluded the bound is decreased by the step fn max(&self) -> T { match &self.max { Bound::Included(n) => n.clone(), @@ -308,6 +312,7 @@ where } } + /// Checks if the value is within the bounds fn valid(&self, value: &T) -> bool { (match &self.min { Bound::Included(n) if *n > *value => false, @@ -320,14 +325,18 @@ where }) } + /// Checks if the value can be increased by the step fn can_increase(&self) -> bool { self.valid(&(self.value.clone() + self.step.clone())) || self.value < self.max() } + /// Checks if the value can be decreased by the step fn can_decrease(&self) -> bool { self.valid(&(self.value.clone() - self.step.clone())) || self.value > self.min() } + /// Checks if the [`NumberInput`] is disabled + /// Meaning that the bounds are too tight for the value to change fn disabled(&self) -> bool { match (&self.min, &self.max) { (Bound::Included(n) | Bound::Excluded(n), Bound::Included(m) | Bound::Excluded(m)) => { @@ -360,7 +369,7 @@ where self } - /// Sets the [`Id`] of the underlying [`TextInput`]. + /// Sets the [`Id`](text_input::Id) of the underlying [`TextInput`](iced::widget::TextInput). #[must_use] pub fn id(mut self, id: impl Into) -> Self { self.content = self.content.id(id.into()); @@ -536,9 +545,11 @@ where .state .downcast_mut::>(); + // We use a secondary shell to select handle the event of the underlying [`TypedInput`] let mut messages = Vec::new(); let mut sub_shell = Shell::new(&mut messages); + // Function to forward the event to the underlying [`TypedInput`] let mut forward_to_text = |widget: &mut Self, child, clipboard| { widget.content.on_event( child, @@ -552,6 +563,7 @@ where ) }; + // Check if the value that would result from the input is valid and within bound let mut check_value = |value: &str| { if let Ok(value) = T::from_str(value) { self.valid(&value) @@ -581,12 +593,20 @@ where let cursor = text_input.cursor(); match key.as_ref() { + // Enter + keyboard::Key::Named(keyboard::key::Named::Enter) => { + forward_to_text(self, child, clipboard) + } + // Copy and selecting all keyboard::Key::Character("c" | "a") if modifiers.command() => { forward_to_text(self, child, clipboard) } + // Cut keyboard::Key::Character("x") if modifiers.command() => { + // We need a selection to cut if let Some((start, end)) = cursor.selection(&Value::new(&value)) { let _ = value.drain(start..end); + // We check that once this part is cut, it's still a number if check_value(&value) { forward_to_text(self, child, clipboard) } else { @@ -596,12 +616,15 @@ where return event::Status::Ignored; } } + // Paste keyboard::Key::Character("v") if modifiers.command() => { + // We need something to paste let Some(paste) = clipboard.read(iced::advanced::clipboard::Kind::Standard) else { return event::Status::Ignored; }; + // We replace the selection or paste the text at the cursor match cursor.state(&Value::new(&value)) { cursor::State::Index(idx) => { let () = value.insert_str(idx, &paste); @@ -609,54 +632,70 @@ where cursor::State::Selection { start, end } if end >= start => { let () = value.replace_range(start..end, &paste); } + // we need to invert the selection to be sure the end is after the start cursor::State::Selection { start, end } => { let () = value.replace_range(end..start, &paste); } } + // We check if it's now a valid number if check_value(&value) { forward_to_text(self, child, clipboard) } else { return event::Status::Ignored; } } - keyboard::Key::Named(keyboard::key::Named::Enter) => { - forward_to_text(self, child, clipboard) - } + // Backspace keyboard::Key::Named(keyboard::key::Named::Backspace) => { + // We remove either the selection or the character before the cursor match cursor.state(&Value::new(&value)) { - cursor::State::Selection { start, end } => { + cursor::State::Selection { start, end } if end >= start => { let _ = value.drain(start..end); } + // we need to invert the selection to be sure the end is after the start + cursor::State::Selection { start, end } => { + let _ = value.drain(end..start); + } + // We need the cursor not at the start cursor::State::Index(idx) if idx > 0 => { let _ = value.remove(idx - 1); } cursor::State::Index(_) => return event::Status::Ignored, } + // We check if it's now a valid number if check_value(&value) { forward_to_text(self, child, clipboard) } else { return event::Status::Ignored; } } + // Delete keyboard::Key::Named(keyboard::key::Named::Delete) => { + // We remove either the selection or the character after the cursor match cursor.state(&Value::new(&value)) { - cursor::State::Selection { start, end } => { + cursor::State::Selection { start, end } if end >= start => { let _ = value.drain(start..end); } + // we need to invert the selection to be sure the end is after the start + cursor::State::Selection { start, end } => { + let _ = value.drain(end..start); + } + // We need the cursor not at the end cursor::State::Index(idx) if idx < value.len() => { let _ = value.remove(idx); } cursor::State::Index(_) => return event::Status::Ignored, } + // We check if it's now a valid number if check_value(&value) { forward_to_text(self, child, clipboard) } else { return event::Status::Ignored; } } + // Arrow Down, decrease by step keyboard::Key::Named(keyboard::key::Named::ArrowDown) if can_decrease => { @@ -664,19 +703,24 @@ where event::Status::Captured } + // Arrow Up, increase by step keyboard::Key::Named(keyboard::key::Named::ArrowUp) if can_increase => { self.increase_value(shell); event::Status::Captured } + // Mouvement of the cursor keyboard::Key::Named( keyboard::key::Named::ArrowLeft | keyboard::key::Named::ArrowRight | keyboard::key::Named::Home | keyboard::key::Named::End, ) => forward_to_text(self, child, clipboard), + // Everything else _ => match text { + // If we are trying to input text Some(text) => { + // We replace the selection or insert the text at the cursor match cursor.state(&Value::new(&value)) { cursor::State::Index(idx) => { let () = value.insert_str(idx, text); @@ -684,23 +728,27 @@ where cursor::State::Selection { start, end } if end >= start => { let () = value.replace_range(start..end, text); } + // we need to invert the selection to be sure the end is after the start cursor::State::Selection { start, end } => { let () = value.replace_range(end..start, text); } } + // We check if it's now a valid number if check_value(&value) { forward_to_text(self, child, clipboard) } else { return event::Status::Ignored; } } + // If we are not trying to input text None => return event::Status::Ignored, }, } } } } + // Mouse scroll event Event::Mouse(mouse::Event::WheelScrolled { delta }) if mouse_over_widget && !self.ignore_scroll_events => { @@ -715,6 +763,7 @@ where } event::Status::Captured } + // Clicking on the buttons up or down Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) if mouse_over_button && !self.ignore_buttons => { @@ -727,6 +776,7 @@ where } event::Status::Captured } + // Releasing the buttons Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) if mouse_over_button => { @@ -737,8 +787,11 @@ where } event::Status::Captured } + // Any other event are just forwarded _ => forward_to_text(self, child, clipboard), }; + + // We forward the shell of the [`TypedInput`] to the application if let Some(redraw) = sub_shell.redraw_request() { shell.request_redraw(redraw); } From a1b8133ff002b4de21938f6671e1f6185b26c4cd Mon Sep 17 00:00:00 2001 From: Ultraxime Date: Tue, 3 Dec 2024 01:43:29 +0100 Subject: [PATCH 3/3] Correcting the examples --- examples/widget_id_return/main.rs | 2 +- examples/widget_id_return/numberinput.rs | 18 +++++++----------- examples/wrap.rs | 19 +++++++++---------- src/widget/number_input.rs | 2 +- 4 files changed, 18 insertions(+), 23 deletions(-) diff --git a/examples/widget_id_return/main.rs b/examples/widget_id_return/main.rs index 7e5b99c..8d230c9 100644 --- a/examples/widget_id_return/main.rs +++ b/examples/widget_id_return/main.rs @@ -41,7 +41,7 @@ use numberinput::*; impl NumberInputDemo { fn update(&mut self, message: self::Message) { let Message::GenericF32Input((id, val)) = message; - self.value[id].value = val.get_data().unwrap_or_default(); + self.value[id].value = val.get_data(); } fn view(&self) -> Element { diff --git a/examples/widget_id_return/numberinput.rs b/examples/widget_id_return/numberinput.rs index 28891ae..3549028 100644 --- a/examples/widget_id_return/numberinput.rs +++ b/examples/widget_id_return/numberinput.rs @@ -1,4 +1,4 @@ -use iced::{Element, Length}; +use iced::Element; use iced_aw::style::number_input::Style; use iced_aw::NumberInput; use num_traits::{bounds::Bounded, Num, NumAssignOps}; @@ -14,17 +14,16 @@ pub struct NumInput { #[derive(Debug, Clone, PartialEq, Eq)] pub enum NumInputMessage { - Change(Result), + Change(V), } impl NumInputMessage where V: Num + NumAssignOps + PartialOrd + Display + FromStr + Copy + Bounded, { - pub fn get_data(&self) -> Result { + pub fn get_data(&self) -> V { match self { - NumInputMessage::Change(Ok(data)) => Ok(*data), - NumInputMessage::Change(Err(data)) => Err(data.clone()), + NumInputMessage::Change(data) => *data, } } } @@ -33,10 +32,9 @@ impl NumInputMessage where V: Eq + Copy, { - pub fn get_enum(&self) -> Result { + pub fn get_enum(&self) -> V { match self { - NumInputMessage::Change(Ok(data)) => Ok(*data), - NumInputMessage::Change(Err(data)) => Err(data.clone()), + NumInputMessage::Change(data) => *data, } } } @@ -70,9 +68,7 @@ where V: 'static, M: 'static + Clone, { - let mut input = NumberInput::new(self.value, min..max, NumInputMessage::Change) - .step(step) - .width(Length::Shrink); + let mut input = NumberInput::new(&self.value, min..max, NumInputMessage::Change).step(step); if let Some(style) = style { input = input.style(move |_theme, _status| style); diff --git a/examples/wrap.rs b/examples/wrap.rs index 357b692..7a9933c 100644 --- a/examples/wrap.rs +++ b/examples/wrap.rs @@ -89,9 +89,9 @@ struct StrButton { #[derive(Debug, Clone)] enum Message { ChangeAlign(WrapAlign), - ChangeSpacing(Result), - ChangeLineSpacing(Result), - ChangeMinimalLength(Result), + ChangeSpacing(f32), + ChangeLineSpacing(f32), + ChangeMinimalLength(f32), } impl RandStrings { @@ -100,16 +100,15 @@ impl RandStrings { Message::ChangeAlign(align) => { self.align = align.into(); } - Message::ChangeSpacing(Ok(num)) => { + Message::ChangeSpacing(num) => { self.spacing = num; } - Message::ChangeLineSpacing(Ok(num)) => { + Message::ChangeLineSpacing(num) => { self.line_spacing = num; } - Message::ChangeMinimalLength(Ok(num)) => { + Message::ChangeMinimalLength(num) => { self.line_minimal_length = num; } - _ => {} } } @@ -148,7 +147,7 @@ impl RandStrings { let spacing_input = Column::new() .push(Text::new("spacing")) .push(NumberInput::new( - self.spacing, + &self.spacing, 0.0..500.0, Message::ChangeSpacing, )); @@ -156,7 +155,7 @@ impl RandStrings { Column::new() .push(Text::new("line spacing")) .push(NumberInput::new( - self.line_spacing, + &self.line_spacing, 0.0..500.0, Message::ChangeLineSpacing, )); @@ -164,7 +163,7 @@ impl RandStrings { Column::new() .push(Text::new("line minimal length")) .push(NumberInput::new( - self.line_minimal_length, + &self.line_minimal_length, 0.0..999.9, Message::ChangeMinimalLength, )); diff --git a/src/widget/number_input.rs b/src/widget/number_input.rs index e396c35..629e78c 100644 --- a/src/widget/number_input.rs +++ b/src/widget/number_input.rs @@ -159,7 +159,7 @@ where /// ``` /// use iced_aw::widget::number_input; /// // Creates a range from -5 till 5. - /// let input: iced_aw::NumberInput<'_, _, _, iced::Theme, iced::Renderer> = number_input(4 /* my_value */, 0..=4, |_| () /* my_message */).bounds(-5..=5); + /// let input: iced_aw::NumberInput<'_, _, _, iced::Theme, iced::Renderer> = number_input(&4 /* my_value */, 0..=4, |_| () /* my_message */).bounds(-5..=5); /// ``` #[must_use] pub fn bounds(mut self, bounds: impl RangeBounds) -> Self {