diff --git a/examples/typed_input/src/main.rs b/examples/typed_input/src/main.rs index 11f69252..7d298751 100644 --- a/examples/typed_input/src/main.rs +++ b/examples/typed_input/src/main.rs @@ -37,8 +37,8 @@ impl TypedInputDemo { fn view(&self) -> Element { let lb_minute = Text::new("Typed Input:"); - let txt_minute = - typed_input::TypedInput::new("Placeholder", &self.value, Message::TypedInpChanged); + let txt_minute = typed_input::TypedInput::new("Placeholder", &self.value) + .on_input(Message::TypedInpChanged); Container::new( Row::new() diff --git a/src/widgets/helpers.rs b/src/widgets/helpers.rs index 536ccfe2..bcc1eaaf 100644 --- a/src/widgets/helpers.rs +++ b/src/widgets/helpers.rs @@ -310,10 +310,10 @@ where pub fn number_input<'a, T, Message, Theme, Renderer, F>( value: T, bounds: impl RangeBounds, - on_changed: F, + on_change: F, ) -> crate::NumberInput<'a, T, Message, Theme, Renderer> where - Message: Clone, + Message: Clone + 'a, Renderer: iced::advanced::text::Renderer, Theme: crate::style::number_input::ExtendedCatalog, F: 'static + Fn(T) -> Message + Copy, @@ -326,7 +326,7 @@ where + Copy + Bounded, { - crate::NumberInput::new(value, bounds, on_changed) + crate::NumberInput::new(value, bounds, on_change) } #[cfg(feature = "typed_input")] @@ -336,7 +336,7 @@ where #[must_use] pub fn typed_input<'a, T, Message, Theme, Renderer, F>( value: &T, - on_changed: F, + on_change: F, ) -> crate::TypedInput<'a, T, Message, Theme, Renderer> where Message: Clone, @@ -345,7 +345,7 @@ where F: 'static + Fn(T) -> Message + Copy, T: 'static + std::fmt::Display + std::str::FromStr + Clone, { - crate::TypedInput::new("", value, on_changed) + crate::TypedInput::new("", value).on_input(on_change) } #[cfg(feature = "selection_list")] diff --git a/src/widgets/number_input.rs b/src/widgets/number_input.rs index 7aca58d8..841dc2d1 100644 --- a/src/widgets/number_input.rs +++ b/src/widgets/number_input.rs @@ -19,8 +19,8 @@ use iced::{ text_input::{self, cursor, Value}, Column, Container, Row, Text, }, - Alignment, Background, Border, Color, Element, Event, Length, Padding, Pixels, Point, - Rectangle, Shadow, Size, + Alignment, Background, Border, Color, Element, Event, Length, Padding, Point, Rectangle, + Shadow, Size, }; use num_traits::{bounds::Bounded, Num, NumAssignOps}; use std::{ @@ -78,9 +78,9 @@ where /// The max value of the [`NumberInput`]. max: T, /// The content padding of the [`NumberInput`]. - padding: f32, + padding: iced::Padding, /// The text size of the [`NumberInput`]. - size: Option, + size: Option, /// The underlying element of the [`NumberInput`]. content: TypedInput<'a, T, Message, Theme, Renderer>, /// The ``on_change`` event of the [`NumberInput`]. @@ -100,7 +100,7 @@ where impl<'a, T, Message, Theme, Renderer> NumberInput<'a, T, Message, Theme, Renderer> where T: Num + NumAssignOps + PartialOrd + Display + FromStr + Copy + Bounded, - Message: Clone, + Message: Clone + 'a, Renderer: iced::advanced::text::Renderer, Theme: number_input::ExtendedCatalog, { @@ -111,12 +111,12 @@ where /// - the current value /// - the max value /// - a function that produces a message when the [`NumberInput`] changes - pub fn new(value: T, bounds: impl RangeBounds, on_changed: F) -> Self + pub fn new(value: T, bounds: impl RangeBounds, on_change: F) -> Self where F: 'static + Fn(T) -> Message + Copy, T: 'static, { - let padding = DEFAULT_PADDING; + let padding = DEFAULT_PADDING.into(); Self { value, @@ -125,11 +125,12 @@ where max: Self::set_max(bounds.end_bound()), padding, size: None, - content: TypedInput::new("", &value, on_changed) + content: TypedInput::new("", &value) + .on_input(on_change) .padding(padding) .width(Length::Fixed(127.0)) .class(Theme::default_input()), - on_change: Box::new(on_changed), + on_change: Box::new(on_change), class: ::default(), font: Renderer::Font::default(), width: Length::Shrink, @@ -154,7 +155,7 @@ where /// Sets the content width of the [`NumberInput`]. #[must_use] - pub fn content_width(mut self, width: Length) -> Self { + pub fn content_width(mut self, width: impl Into) -> Self { self.content = self.content.width(width); self } @@ -191,21 +192,23 @@ 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(message); + self.content = self.content.on_submit(move |_| message.clone()); self } /// Sets the padding of the [`NumberInput`]. #[must_use] - pub fn padding(mut self, units: f32) -> Self { - self.padding = units; - self.content = self.content.padding(units); + pub fn padding(mut self, padding: impl Into) -> Self { + let padding = padding.into(); + self.padding = padding; + self.content = self.content.padding(padding); self } /// Sets the text size of the [`NumberInput`]. #[must_use] - pub fn size(mut self, size: f32) -> Self { + pub fn size(mut self, size: impl Into) -> Self { + let size = size.into(); self.size = Some(size); self.content = self.content.size(size); self @@ -335,17 +338,16 @@ where } fn layout(&self, tree: &mut Tree, renderer: &Renderer, limits: &Limits) -> Node { - let padding = Padding::from(self.padding); let num_size = self.size(); let limits = limits .width(num_size.width) .height(Length::Shrink) - .shrink(padding); + .shrink(self.padding); let content = self .content .layout(&mut tree.children[0], renderer, &limits); let limits2 = Limits::new(Size::new(0.0, 0.0), content.size()); - let txt_size = self.size.unwrap_or_else(|| renderer.default_size().0); + let txt_size = self.size.unwrap_or_else(|| renderer.default_size()); let icon_size = txt_size * 2.5 / 4.0; let btn_mod = |c| { @@ -354,7 +356,12 @@ where .center_x(Length::Shrink) }; - let element = if self.padding < DEFAULT_PADDING { + let default_padding = Padding::from(DEFAULT_PADDING); + + let element = if self.padding.top < default_padding.top + || self.padding.bottom < default_padding.bottom + || self.padding.right < default_padding.right + { Element::new( Row::::new() .spacing(1) @@ -712,9 +719,9 @@ where style::number_input::Catalog::style(theme, &self.class, Status::Active) }; - let txt_size = self.size.unwrap_or_else(|| renderer.default_size().0); + let txt_size = self.size.unwrap_or_else(|| renderer.default_size()); - let icon_size = Pixels(txt_size * 2.5 / 4.0); + let icon_size = txt_size * 2.5 / 4.0; if self.ignore_buttons { return; diff --git a/src/widgets/typed_input.rs b/src/widgets/typed_input.rs index 582e8d5e..d9db55ba 100644 --- a/src/widgets/typed_input.rs +++ b/src/widgets/typed_input.rs @@ -14,7 +14,7 @@ use iced::{ widget::text_input::{self, TextInput}, Event, Size, }; -use iced::{Element, Length, Rectangle}; +use iced::{Element, Length, Padding, Pixels, Rectangle}; use std::{fmt::Display, str::FromStr}; @@ -41,33 +41,36 @@ const DEFAULT_PADDING: f32 = 5.0; /// ``` pub struct TypedInput<'a, T, Message, Theme = iced::Theme, Renderer = iced::Renderer> where - Renderer: iced::advanced::text::Renderer, + Renderer: iced::advanced::text::Renderer, Theme: text_input::Catalog, { /// The current value of the [`TypedInput`]. value: T, - /// The underlying element of the [`TypeInput`]. + /// The underlying element of the [`TypedInput`]. text_input: text_input::TextInput<'a, InternalMessage, Theme, Renderer>, text: String, - /// The ``on_change`` event of the [`TextInput`]. - on_change: Box Message>, - /// The ``on_change`` event of the [`TextInput`]. - on_submit: Option, - /// The font text of the [`TextInput`]. - font: Renderer::Font, + /// The ``on_change`` event of the [`TypedInput`]. + 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>>, } #[derive(Debug, Clone, PartialEq)] +#[allow(clippy::enum_variant_names)] enum InternalMessage { OnChange(String), OnSubmit, + OnPaste(String), } impl<'a, T, Message, Theme, Renderer> TypedInput<'a, T, Message, Theme, Renderer> where T: Display + FromStr, Message: Clone, - Renderer: iced::advanced::text::Renderer, + Renderer: iced::advanced::text::Renderer, Theme: text_input::Catalog, { /// Creates a new [`TypedInput`]. @@ -75,9 +78,9 @@ where /// It expects: /// - the current value /// - a function that produces a message when the [`TypedInput`] changes - pub fn new(placeholder: &str, value: &T, on_changed: F) -> Self + #[must_use] + pub fn new(placeholder: &str, value: &T) -> Self where - F: 'static + Fn(T) -> Message + Copy, T: 'a + Clone, { let padding = DEFAULT_PADDING; @@ -85,64 +88,113 @@ where Self { value: value.clone(), text_input: text_input::TextInput::new(placeholder, format!("{value}").as_str()) - .on_input(InternalMessage::OnChange) - .on_submit(InternalMessage::OnSubmit) .padding(padding) .width(Length::Fixed(127.0)) .class(::default()), text: value.to_string(), - on_change: Box::new(on_changed), + on_change: None, on_submit: None, - font: Renderer::Font::default(), + on_paste: None, } } - /// Gets the text value of the [`TypedInput`]. - pub fn text(&self) -> &str { - &self.text + /// Sets the [Id](text_input::Id) of the internal [`TextInput`] + #[must_use] + pub fn id(mut self, id: text_input::Id) -> Self { + self.text_input = self.text_input.id(id); + self } - /// Sets the width of the [`TypedInput`]. + /// Convert the [`TypedInput`] into a secure password input #[must_use] - pub fn width(mut self, width: Length) -> Self { - self.text_input = self.text_input.width(width); + pub fn secure(mut self, is_secure: bool) -> Self { + self.text_input = self.text_input.secure(is_secure); self } - /// Sets the [`Font`] of the [`Text`]. + /// Sets the message that should be produced when some valid text is typed into [`TypedInput`] /// - /// [`Font`]: core::Font - /// [`Text`]: core::widget::Text - #[allow(clippy::needless_pass_by_value)] + /// If neither this method nor [`on_submit`](Self::on_submit) is called, the [`TypedInput`] will be disabled #[must_use] - pub fn font(mut self, font: Renderer::Font) -> Self { - self.font = font; - self.text_input = self.text_input.font(font); + pub fn on_input(mut self, callback: F) -> Self + where + F: 'a + Fn(T) -> Message, + { + self.text_input = self.text_input.on_input(InternalMessage::OnChange); + self.on_change = Some(Box::new(callback)); self } /// Sets the message that should be produced when the [`TextInput`] is /// focused and the enter key is pressed. + /// + /// If neither this method nor [`on_input`](Self::on_input) is called, the [`TypedInput`] will be disabled + #[must_use] + pub fn on_submit(mut self, callback: F) -> Self + where + F: 'a + Fn(Result) -> Message, + { + self.text_input = self + .text_input + .on_input(InternalMessage::OnChange) + .on_submit(InternalMessage::OnSubmit); + self.on_submit = Some(Box::new(callback)); + self + } + + /// Sets the message that should be produced when some text is pasted into the [`TypedInput`], resulting in a valid value + #[must_use] + pub fn on_paste(mut self, callback: F) -> Self + where + F: 'a + Fn(T) -> Message, + { + self.text_input = self.text_input.on_paste(InternalMessage::OnPaste); + self.on_paste = Some(Box::new(callback)); + self + } + + /// Sets the [Font](iced::advanced::text::Renderer::Font) of the [`TypedInput`]. + #[must_use] + pub fn font(mut self, font: Renderer::Font) -> Self { + self.text_input = self.text_input.font(font); + self + } + + /// Sets the [Icon](iced::widget::text_input::Icon) of the [`TypedInput`] #[must_use] - pub fn on_submit(mut self, message: Message) -> Self { - self.on_submit = Some(message); + pub fn icon(mut self, icon: iced::widget::text_input::Icon) -> Self { + self.text_input = self.text_input.icon(icon); + self + } + + /// Sets the width of the [`TypedInput`]. + #[must_use] + pub fn width(mut self, width: impl Into) -> Self { + self.text_input = self.text_input.width(width); self } /// Sets the padding of the [`TypedInput`]. #[must_use] - pub fn padding(mut self, units: f32) -> Self { - self.text_input = self.text_input.padding(units); + pub fn padding(mut self, padding: impl Into) -> Self { + self.text_input = self.text_input.padding(padding); self } /// Sets the text size of the [`TypedInput`]. #[must_use] - pub fn size(mut self, size: f32) -> Self { + pub fn size(mut self, size: impl Into) -> Self { self.text_input = self.text_input.size(size); self } + /// Sets the [`text::LineHeight`](iced::widget::text::LineHeight) of the [`TypedInput`]. + #[must_use] + pub fn line_height(mut self, line_height: impl Into) -> Self { + self.text_input = self.text_input.line_height(line_height); + self + } + /// Sets the style of the input of the [`TypedInput`]. #[must_use] pub fn style( @@ -162,6 +214,11 @@ where self.text_input = self.text_input.class(class); self } + + /// Gets the current text of the [`TypedInput`]. + pub fn text(&self) -> &str { + &self.text + } } impl<'a, T, Message, Theme, Renderer> Widget @@ -169,7 +226,7 @@ impl<'a, T, Message, Theme, Renderer> Widget where T: Display + FromStr + Clone + PartialEq, Message: 'a + Clone, - Renderer: 'a + iced::advanced::text::Renderer, + Renderer: 'a + iced::advanced::text::Renderer, Theme: text_input::Catalog, { fn tag(&self) -> Tag { @@ -293,16 +350,34 @@ where if let Ok(val) = T::from_str(&self.text) { if self.value != val { self.value = val.clone(); - shell.publish((self.on_change)(val)); + if let Some(on_change) = &self.on_change { + shell.publish(on_change(val)); + } } } shell.invalidate_layout(); } InternalMessage::OnSubmit => { if let Some(on_submit) = &self.on_submit { - shell.publish(on_submit.clone()); + let value = match T::from_str(&self.text) { + Ok(v) => Ok(v), + Err(_) => Err(self.text.clone()), + }; + shell.publish(on_submit(value)); } } + InternalMessage::OnPaste(value) => { + self.text = value; + if let Ok(val) = T::from_str(&self.text) { + if self.value != val { + self.value = val.clone(); + if let Some(on_paste) = &self.on_paste { + shell.publish(on_paste(val)); + } + } + } + shell.invalidate_layout(); + } } } status @@ -314,7 +389,7 @@ impl<'a, T, Message, Theme, Renderer> From, + Renderer: 'a + iced::advanced::text::Renderer, Theme: 'a + text_input::Catalog, { fn from(typed_input: TypedInput<'a, T, Message, Theme, Renderer>) -> Self {