iced_aw/widget/
typed_input.rs

1//! Display fields that can only be filled with a specific type.
2//!
3//! *This API requires the following crate features to be activated: `typed_input`*
4
5use iced_core::layout::{Layout, Limits, Node};
6use iced_core::mouse::{self, Cursor};
7use iced_core::widget::{
8    Operation, Tree, Widget,
9    tree::{State, Tag},
10};
11use iced_core::{Clipboard, Shell, widget};
12use iced_core::{Element, Length, Padding, Pixels, Rectangle};
13use iced_core::{Event, Size};
14use iced_widget::text_input::{self, TextInput};
15
16use std::{fmt::Display, str::FromStr};
17
18/// The default padding
19const DEFAULT_PADDING: Padding = Padding::new(5.0);
20
21/// A field that can only be filled with a specific type.
22///
23/// # Example
24/// ```ignore
25/// # use iced_aw::TypedInput;
26/// #
27/// #[derive(Debug, Clone)]
28/// enum Message {
29///     TypedInputChanged(u32),
30/// }
31///
32/// let value = 12;
33///
34/// let input = TypedInput::new(
35///     value,
36///     Message::TypedInputChanged,
37/// );
38/// ```
39pub struct TypedInput<'a, T, Message, Theme = iced_widget::Theme, Renderer = iced_widget::Renderer>
40where
41    Renderer: iced_core::text::Renderer,
42    Theme: text_input::Catalog,
43{
44    /// The current value of the [`TypedInput`].
45    value: T,
46    /// The underlying element of the [`TypedInput`].
47    text_input: text_input::TextInput<'a, InternalMessage, Theme, Renderer>,
48    text: String,
49    /// The ``on_change`` event of the [`TypedInput`].
50    on_change: Option<Box<dyn 'a + Fn(T) -> Message>>,
51    /// The ``on_submit`` event of the [`TypedInput`].
52    #[allow(clippy::type_complexity)]
53    on_submit: Option<Box<dyn 'a + Fn(Result<T, String>) -> Message>>,
54    /// The ``on_paste`` event of the [`TypedInput`]
55    on_paste: Option<Box<dyn 'a + Fn(T) -> Message>>,
56}
57
58#[derive(Debug, Clone, PartialEq)]
59#[allow(clippy::enum_variant_names)]
60enum InternalMessage {
61    OnChange(String),
62    OnSubmit,
63    OnPaste(String),
64}
65
66impl<'a, T, Message, Theme, Renderer> TypedInput<'a, T, Message, Theme, Renderer>
67where
68    T: Display + FromStr,
69    Message: Clone,
70    Renderer: iced_core::text::Renderer,
71    Theme: text_input::Catalog,
72{
73    /// Creates a new [`TypedInput`].
74    ///
75    /// It expects:
76    /// - the current value
77    /// - a function that produces a message when the [`TypedInput`] changes
78    #[must_use]
79    pub fn new(placeholder: &str, value: &T) -> Self
80    where
81        T: 'a + Clone,
82    {
83        let padding = DEFAULT_PADDING;
84
85        Self {
86            value: value.clone(),
87            text_input: text_input::TextInput::new(placeholder, format!("{value}").as_str())
88                .padding(padding)
89                .width(Length::Fixed(127.0))
90                .class(<Theme as text_input::Catalog>::default()),
91            text: value.to_string(),
92            on_change: None,
93            on_submit: None,
94            on_paste: None,
95        }
96    }
97
98    /// Sets the [Id](widget::Id) of the internal [`TextInput`]
99    #[must_use]
100    pub fn id(mut self, id: impl Into<widget::Id>) -> Self {
101        self.text_input = self.text_input.id(id);
102        self
103    }
104
105    /// Convert the [`TypedInput`] into a secure password input
106    #[must_use]
107    pub fn secure(mut self, is_secure: bool) -> Self {
108        self.text_input = self.text_input.secure(is_secure);
109        self
110    }
111
112    /// Sets the message that should be produced when some valid text is typed into [`TypedInput`]
113    ///
114    /// If neither this method nor [`on_submit`](Self::on_submit) is called, the [`TypedInput`] will be disabled
115    #[must_use]
116    pub fn on_input<F>(mut self, callback: F) -> Self
117    where
118        F: 'a + Fn(T) -> Message,
119    {
120        self.text_input = self.text_input.on_input(InternalMessage::OnChange);
121        self.on_change = Some(Box::new(callback));
122        self
123    }
124
125    /// Sets the message that should be produced when some text is typed into the [`TypedInput`], if `Some`.
126    ///
127    /// If this is `None`, and there is no [`on_submit`](Self::on_submit) callback, the [`TypedInput`] will be disabled.
128    #[must_use]
129    pub fn on_input_maybe<F>(mut self, callback: Option<F>) -> Self
130    where
131        F: 'a + Fn(T) -> Message,
132    {
133        if let Some(callback) = callback {
134            self.text_input = self.text_input.on_input(InternalMessage::OnChange);
135            self.on_change = Some(Box::new(callback));
136        } else {
137            if self.on_submit.is_none() {
138                // Used to give a proper type to None, maybe someone can find a better way
139                #[allow(unused_assignments)]
140                let mut f = Some(InternalMessage::OnChange);
141                f = None;
142                self.text_input = self.text_input.on_input_maybe(f);
143            }
144            self.on_change = None;
145        }
146        self
147    }
148
149    /// Sets the message that should be produced when the [`TypedtInput`] is
150    /// focused and the enter key is pressed.
151    ///
152    /// If neither this method nor [`on_input`](Self::on_input) is called, the [`TypedInput`] will be disabled
153    #[must_use]
154    pub fn on_submit<F>(mut self, callback: F) -> Self
155    where
156        F: 'a + Fn(Result<T, String>) -> Message,
157    {
158        self.text_input = self
159            .text_input
160            .on_input(InternalMessage::OnChange)
161            .on_submit(InternalMessage::OnSubmit);
162        self.on_submit = Some(Box::new(callback));
163        self
164    }
165
166    /// Sets the message that should be produced when the [`TypedtInput`] is
167    /// focused and the enter key is pressed, if `Some`.
168    ///
169    /// If this is `None`, and there is no [`on_change`](Self::on_input) callback, the [`TypedInput`] will be disabled.
170    #[must_use]
171    pub fn on_submit_maybe<F>(mut self, callback: Option<F>) -> Self
172    where
173        F: 'a + Fn(Result<T, String>) -> Message,
174    {
175        if let Some(callback) = callback {
176            self.text_input = self
177                .text_input
178                .on_input(InternalMessage::OnChange)
179                .on_submit(InternalMessage::OnSubmit);
180            self.on_submit = Some(Box::new(callback));
181        } else {
182            if self.on_change.is_none() {
183                // Used to give a proper type to None, maybe someone can find a better way
184                #[allow(unused_assignments)]
185                let mut f = Some(InternalMessage::OnChange);
186                f = None;
187                self.text_input = self.text_input.on_input_maybe(f);
188            }
189            // Used to give a proper type to None, maybe someone can find a better way
190            #[allow(unused_assignments)]
191            let mut f = Some(InternalMessage::OnSubmit);
192            f = None;
193            self.text_input = self.text_input.on_submit_maybe(f);
194            self.on_change = None;
195        }
196        self
197    }
198
199    /// Sets the message that should be produced when some text is pasted into the [`TypedInput`], resulting in a valid value
200    #[must_use]
201    pub fn on_paste<F>(mut self, callback: F) -> Self
202    where
203        F: 'a + Fn(T) -> Message,
204    {
205        self.text_input = self.text_input.on_paste(InternalMessage::OnPaste);
206        self.on_paste = Some(Box::new(callback));
207        self
208    }
209
210    /// Sets the message that should be produced when some text is pasted into the [`TypedInput`], resulting in a valid value, if `Some`
211    #[must_use]
212    pub fn on_paste_maybe<F>(mut self, callback: Option<F>) -> Self
213    where
214        F: 'a + Fn(T) -> Message,
215    {
216        if let Some(callback) = callback {
217            self.text_input = self.text_input.on_paste(InternalMessage::OnPaste);
218            self.on_paste = Some(Box::new(callback));
219        } else {
220            // Used to give a proper type to None, maybe someone can find a better way
221            #[allow(unused_assignments)]
222            let mut f = Some(InternalMessage::OnPaste);
223            f = None;
224            self.text_input = self.text_input.on_paste_maybe(f);
225            self.on_paste = None;
226        }
227        self
228    }
229
230    /// Sets the [Font](iced_core::text::Renderer::Font) of the [`TypedInput`].
231    #[must_use]
232    pub fn font(mut self, font: Renderer::Font) -> Self {
233        self.text_input = self.text_input.font(font);
234        self
235    }
236
237    /// Sets the [Icon](iced_widget::text_input::Icon) of the [`TypedInput`]
238    #[must_use]
239    pub fn icon(mut self, icon: iced_widget::text_input::Icon<Renderer::Font>) -> Self {
240        self.text_input = self.text_input.icon(icon);
241        self
242    }
243
244    /// Sets the width of the [`TypedInput`].
245    #[must_use]
246    pub fn width(mut self, width: impl Into<Length>) -> Self {
247        self.text_input = self.text_input.width(width);
248        self
249    }
250
251    /// Sets the padding of the [`TypedInput`].
252    #[must_use]
253    pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
254        self.text_input = self.text_input.padding(padding);
255        self
256    }
257
258    /// Sets the text size of the [`TypedInput`].
259    #[must_use]
260    pub fn size(mut self, size: impl Into<Pixels>) -> Self {
261        self.text_input = self.text_input.size(size);
262        self
263    }
264
265    /// Sets the [`text::LineHeight`](iced_widget::text::LineHeight) of the [`TypedInput`].
266    #[must_use]
267    pub fn line_height(mut self, line_height: impl Into<iced_widget::text::LineHeight>) -> Self {
268        self.text_input = self.text_input.line_height(line_height);
269        self
270    }
271
272    /// Sets the horizontal alignment of the [`TypedInput`].
273    #[must_use]
274    pub fn align_x(mut self, alignment: impl Into<iced_core::alignment::Horizontal>) -> Self {
275        self.text_input = self.text_input.align_x(alignment);
276        self
277    }
278
279    /// Sets the style of the input of the [`TypedInput`].
280    #[must_use]
281    pub fn style(
282        mut self,
283        style: impl Fn(&Theme, text_input::Status) -> text_input::Style + 'a,
284    ) -> Self
285    where
286        <Theme as text_input::Catalog>::Class<'a>: From<text_input::StyleFn<'a, Theme>>,
287    {
288        self.text_input = self.text_input.style(style);
289        self
290    }
291
292    /// Sets the class of the input of the [`TypedInput`].
293    #[must_use]
294    pub fn class(mut self, class: impl Into<<Theme as text_input::Catalog>::Class<'a>>) -> Self {
295        self.text_input = self.text_input.class(class);
296        self
297    }
298
299    /// Gets the current text of the [`TypedInput`].
300    pub fn text(&self) -> &str {
301        &self.text
302    }
303}
304
305impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
306    for TypedInput<'a, T, Message, Theme, Renderer>
307where
308    T: Display + FromStr + Clone + PartialEq,
309    Message: 'a + Clone,
310    Renderer: 'a + iced_core::text::Renderer,
311    Theme: text_input::Catalog,
312{
313    fn tag(&self) -> Tag {
314        <TextInput<_, _, _> as Widget<_, _, _>>::tag(&self.text_input)
315    }
316    fn state(&self) -> State {
317        <TextInput<_, _, _> as Widget<_, _, _>>::state(&self.text_input)
318    }
319
320    fn children(&self) -> Vec<Tree> {
321        <TextInput<_, _, _> as Widget<_, _, _>>::children(&self.text_input)
322    }
323
324    fn diff(&self, state: &mut Tree) {
325        <TextInput<_, _, _> as Widget<_, _, _>>::diff(&self.text_input, state);
326    }
327
328    fn size(&self) -> Size<Length> {
329        <TextInput<_, _, _> as Widget<_, _, _>>::size(&self.text_input)
330    }
331
332    fn layout(&mut self, state: &mut Tree, renderer: &Renderer, limits: &Limits) -> Node {
333        <TextInput<_, _, _> as Widget<_, _, _>>::layout(
334            &mut self.text_input,
335            state,
336            renderer,
337            limits,
338        )
339    }
340
341    fn draw(
342        &self,
343        state: &Tree,
344        renderer: &mut Renderer,
345        theme: &Theme,
346        style: &iced_core::renderer::Style,
347        layout: Layout<'_>,
348        cursor: Cursor,
349        viewport: &Rectangle,
350    ) {
351        <TextInput<_, _, _> as Widget<_, _, _>>::draw(
352            &self.text_input,
353            state,
354            renderer,
355            theme,
356            style,
357            layout,
358            cursor,
359            viewport,
360        );
361    }
362
363    fn mouse_interaction(
364        &self,
365        state: &Tree,
366        layout: Layout<'_>,
367        cursor: Cursor,
368        viewport: &Rectangle,
369        renderer: &Renderer,
370    ) -> mouse::Interaction {
371        <TextInput<_, _, _> as Widget<_, _, _>>::mouse_interaction(
372            &self.text_input,
373            state,
374            layout,
375            cursor,
376            viewport,
377            renderer,
378        )
379    }
380
381    fn operate(
382        &mut self,
383        state: &mut Tree,
384        layout: Layout<'_>,
385        renderer: &Renderer,
386        operation: &mut dyn Operation<()>,
387    ) {
388        <TextInput<_, _, _> as Widget<_, _, _>>::operate(
389            &mut self.text_input,
390            state,
391            layout,
392            renderer,
393            operation,
394        );
395    }
396
397    #[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
398    fn update(
399        &mut self,
400        state: &mut Tree,
401        event: &Event,
402        layout: Layout<'_>,
403        cursor: Cursor,
404        renderer: &Renderer,
405        clipboard: &mut dyn Clipboard,
406        shell: &mut Shell<Message>,
407        viewport: &Rectangle,
408    ) {
409        let mut messages = Vec::new();
410        let mut sub_shell = Shell::new(&mut messages);
411        self.text_input.update(
412            state,
413            event,
414            layout,
415            cursor,
416            renderer,
417            clipboard,
418            &mut sub_shell,
419            viewport,
420        );
421
422        shell.request_redraw_at(sub_shell.redraw_request());
423
424        if sub_shell.is_layout_invalid() {
425            shell.invalidate_layout();
426        }
427        if sub_shell.are_widgets_invalid() {
428            shell.invalidate_widgets();
429        }
430
431        for message in messages {
432            match message {
433                InternalMessage::OnChange(value) => {
434                    self.text = value;
435
436                    if let Ok(value) = T::from_str(&self.text)
437                        && self.value != value
438                    {
439                        self.value = value.clone();
440                        if let Some(on_change) = &self.on_change {
441                            shell.publish(on_change(value));
442                        }
443                    }
444
445                    shell.invalidate_layout();
446                }
447                InternalMessage::OnSubmit => {
448                    if let Some(on_submit) = &self.on_submit {
449                        let value = match T::from_str(&self.text) {
450                            Ok(v) => Ok(v),
451                            Err(_) => Err(self.text.clone()),
452                        };
453                        shell.publish(on_submit(value));
454                    }
455
456                    shell.invalidate_layout();
457                }
458                InternalMessage::OnPaste(value) => {
459                    self.text = value;
460
461                    if let Ok(value) = T::from_str(&self.text)
462                        && self.value != value
463                    {
464                        self.value = value.clone();
465                        if let Some(on_paste) = &self.on_paste {
466                            shell.publish(on_paste(value));
467                        }
468                    }
469
470                    shell.invalidate_layout();
471                }
472            }
473        }
474    }
475}
476
477impl<'a, T, Message, Theme, Renderer> From<TypedInput<'a, T, Message, Theme, Renderer>>
478    for Element<'a, Message, Theme, Renderer>
479where
480    T: 'a + Display + FromStr + Clone + PartialEq,
481    Message: 'a + Clone,
482    Renderer: 'a + iced_core::text::Renderer,
483    Theme: 'a + text_input::Catalog,
484{
485    fn from(typed_input: TypedInput<'a, T, Message, Theme, Renderer>) -> Self {
486        Element::new(typed_input)
487    }
488}