Skip to main content

iced_widget/
text_input.rs

1//! Text inputs display fields that can be filled with text.
2//!
3//! # Example
4//! ```no_run
5//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
6//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
7//! #
8//! use iced::widget::text_input;
9//!
10//! struct State {
11//!    content: String,
12//! }
13//!
14//! #[derive(Debug, Clone)]
15//! enum Message {
16//!     ContentChanged(String)
17//! }
18//!
19//! fn view(state: &State) -> Element<'_, Message> {
20//!     text_input("Type something here...", &state.content)
21//!         .on_input(Message::ContentChanged)
22//!         .into()
23//! }
24//!
25//! fn update(state: &mut State, message: Message) {
26//!     match message {
27//!         Message::ContentChanged(content) => {
28//!             state.content = content;
29//!         }
30//!     }
31//! }
32//! ```
33use crate::core::keyboard;
34use crate::core::layout;
35use crate::core::mouse;
36use crate::core::renderer;
37use crate::core::shell;
38use crate::core::text;
39use crate::core::text::editor;
40use crate::core::text::input;
41use crate::core::widget;
42use crate::core::widget::operation::{self, Focusable, Operation};
43use crate::core::widget::tree::{self, Tree};
44use crate::core::window;
45use crate::core::{
46    Background, Border, Color, Element, Event, Font, Layout, Length, Padding, Pixels, Rectangle,
47    Shell, Size, Theme, Widget,
48};
49
50/// A field that can be filled with text.
51///
52/// # Example
53/// ```no_run
54/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
55/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
56/// #
57/// use iced::widget::text_input;
58///
59/// struct State {
60///    content: String,
61/// }
62///
63/// #[derive(Debug, Clone)]
64/// enum Message {
65///     ContentChanged(String)
66/// }
67///
68/// fn view(state: &State) -> Element<'_, Message> {
69///     text_input("Type something here...", &state.content)
70///         .on_input(Message::ContentChanged)
71///         .into()
72/// }
73///
74/// fn update(state: &mut State, message: Message) {
75///     match message {
76///         Message::ContentChanged(content) => {
77///             state.content = content;
78///         }
79///     }
80/// }
81/// ```
82pub struct TextInput<'a, Message, Theme = crate::Theme>
83where
84    Theme: Catalog,
85{
86    id: Option<widget::Id>,
87    placeholder: text::Fragment<'a>,
88    value: text::Fragment<'a>,
89    is_secure: bool,
90    font: Option<Font>,
91    width: Length,
92    height: Length,
93    padding: Padding,
94    size: Option<Pixels>,
95    line_height: Option<text::LineHeight>,
96    alignment: text::Alignment,
97    multiline: Option<text::Wrapping>,
98    on_input: Option<Box<dyn Fn(String) -> Message + 'a>>,
99    on_paste: Option<Box<dyn Fn(String) -> Message + 'a>>,
100    on_submit: Option<Message>,
101    class: Theme::Class<'a>,
102    last_status: Option<Status>,
103}
104
105/// The default [`Padding`] of a [`TextInput`].
106pub const DEFAULT_PADDING: Padding = Padding::new(5.0);
107
108impl<'a, Message, Theme> TextInput<'a, Message, Theme>
109where
110    Message: Clone,
111    Theme: Catalog,
112{
113    /// Creates a new [`TextInput`] with the given placeholder and
114    /// its current value.
115    pub fn new(
116        placeholder: impl text::IntoFragment<'a>,
117        value: impl text::IntoFragment<'a>,
118    ) -> Self {
119        TextInput {
120            id: None,
121            placeholder: placeholder.into_fragment(),
122            value: value.into_fragment(),
123            is_secure: false,
124            font: None,
125            width: Length::Fill,
126            height: Length::Fit,
127            padding: DEFAULT_PADDING,
128            size: None,
129            line_height: None,
130            alignment: text::Alignment::Default,
131            multiline: None,
132            on_input: None,
133            on_paste: None,
134            on_submit: None,
135            class: Theme::default(),
136            last_status: None,
137        }
138    }
139
140    /// Sets the [`widget::Id`] of the [`TextInput`].
141    pub fn id(mut self, id: impl Into<widget::Id>) -> Self {
142        self.id = Some(id.into());
143        self
144    }
145
146    /// Converts the [`TextInput`] into a secure password input.
147    pub fn secure(mut self, is_secure: bool) -> Self {
148        self.is_secure = is_secure;
149        self
150    }
151
152    /// Sets the message that should be produced when some text is typed into
153    /// the [`TextInput`].
154    ///
155    /// If this method is not called, the [`TextInput`] will be disabled.
156    pub fn on_input(mut self, on_input: impl Fn(String) -> Message + 'a) -> Self {
157        self.on_input = Some(Box::new(on_input));
158        self
159    }
160
161    /// Sets the message that should be produced when some text is typed into
162    /// the [`TextInput`], if `Some`.
163    ///
164    /// If `None`, the [`TextInput`] will be disabled.
165    pub fn on_input_maybe(mut self, on_input: Option<impl Fn(String) -> Message + 'a>) -> Self {
166        self.on_input = on_input.map(|f| Box::new(f) as _);
167        self
168    }
169
170    /// Sets the message that should be produced when the [`TextInput`] is
171    /// focused and the enter key is pressed.
172    pub fn on_submit(mut self, message: Message) -> Self {
173        self.on_submit = Some(message);
174        self
175    }
176
177    /// Sets the message that should be produced when the [`TextInput`] is
178    /// focused and the enter key is pressed, if `Some`.
179    pub fn on_submit_maybe(mut self, on_submit: Option<Message>) -> Self {
180        self.on_submit = on_submit;
181        self
182    }
183
184    /// Sets the message that should be produced when some text is pasted into
185    /// the [`TextInput`].
186    pub fn on_paste(mut self, on_paste: impl Fn(String) -> Message + 'a) -> Self {
187        self.on_paste = Some(Box::new(on_paste));
188        self
189    }
190
191    /// Sets the message that should be produced when some text is pasted into
192    /// the [`TextInput`], if `Some`.
193    pub fn on_paste_maybe(mut self, on_paste: Option<impl Fn(String) -> Message + 'a>) -> Self {
194        self.on_paste = on_paste.map(|f| Box::new(f) as _);
195        self
196    }
197
198    /// Sets the [`Font`] of the [`TextInput`].
199    ///
200    /// [`Font`]: crate::core::Font
201    pub fn font(mut self, font: Font) -> Self {
202        self.font = Some(font);
203        self
204    }
205
206    /// Sets the width of the [`TextInput`].
207    pub fn width(mut self, width: impl Into<Length>) -> Self {
208        self.width = width.into();
209        self
210    }
211
212    /// Sets the [`Padding`] of the [`TextInput`].
213    pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
214        self.padding = padding.into();
215        self
216    }
217
218    /// Sets the text size of the [`TextInput`].
219    pub fn size(mut self, size: impl Into<Pixels>) -> Self {
220        self.size = Some(size.into());
221        self
222    }
223
224    /// Sets the [`text::LineHeight`] of the [`TextInput`].
225    pub fn line_height(mut self, line_height: impl Into<text::LineHeight>) -> Self {
226        self.line_height = Some(line_height.into());
227        self
228    }
229
230    /// Sets the horizontal alignment of the [`TextInput`].
231    pub fn align_x(mut self, alignment: impl Into<text::Alignment>) -> Self {
232        self.alignment = alignment.into();
233        self
234    }
235
236    /// Sets the multiline behavior of the [`TextInput`].
237    ///
238    /// `None` will behave as a single line input.
239    pub fn multiline(mut self, wrapping: Option<text::Wrapping>) -> Self {
240        self.multiline = wrapping;
241        self
242    }
243
244    /// Sets the style of the [`TextInput`].
245    #[must_use]
246    pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
247    where
248        Theme::Class<'a>: From<StyleFn<'a, Theme>>,
249    {
250        self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
251        self
252    }
253
254    /// Sets the style class of the [`TextInput`].
255    #[must_use]
256    pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
257        self.class = class.into();
258        self
259    }
260}
261
262impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> for TextInput<'_, Message, Theme>
263where
264    Message: Clone,
265    Theme: Catalog,
266    Renderer: text::Renderer + 'static,
267{
268    fn tag(&self) -> tree::Tag {
269        tree::Tag::of::<State<Renderer>>()
270    }
271
272    fn state(&self) -> tree::State {
273        tree::State::new(State::<Renderer>::new())
274    }
275
276    fn size(&self) -> Size<Length> {
277        Size {
278            width: self.width,
279            height: Length::Shrink,
280        }
281    }
282
283    fn layout(
284        &mut self,
285        tree: &mut Tree,
286        renderer: &Renderer,
287        limits: &layout::Limits,
288    ) -> layout::Node {
289        let state = tree.state.downcast_mut::<State<Renderer>>();
290
291        if state.value != self.value
292            && state
293                .transaction
294                .as_ref()
295                .is_none_or(shell::Tracking::is_processed)
296        {
297            state.input.overwrite(self.value.as_ref());
298            state.value = self.value.clone().into_owned();
299        }
300
301        state.input.layout(
302            renderer,
303            limits,
304            input::Layout {
305                width: self.width,
306                height: self.height,
307                padding: self.padding,
308                placeholder: self.placeholder.as_ref(),
309                font: self.font,
310                size: self.size,
311                line_height: self.line_height,
312                alignment: self.alignment,
313                multiline: self.multiline,
314                is_secure: self.is_secure,
315            },
316        )
317    }
318
319    fn operate(
320        &mut self,
321        tree: &mut Tree,
322        layout: Layout<'_>,
323        _renderer: &Renderer,
324        operation: &mut dyn Operation,
325    ) {
326        let state = tree.state.downcast_mut::<State<Renderer>>();
327
328        operation.text_input(self.id.as_ref(), layout.bounds(), state);
329        operation.focusable(self.id.as_ref(), layout.bounds(), state);
330    }
331
332    fn update(
333        &mut self,
334        tree: &mut Tree,
335        event: &Event,
336        layout: Layout<'_>,
337        cursor: mouse::Cursor,
338        _renderer: &Renderer,
339        shell: &mut Shell<'_, Message>,
340        _viewport: &Rectangle,
341    ) {
342        let state = state::<Renderer>(tree);
343        let is_disabled = self.on_input.is_none();
344
345        if let Some(on_input) = &self.on_input {
346            let edit = state
347                .input
348                .update(event, layout.bounds(), cursor, shell, |key_press| {
349                    if let Some(on_submit) = &self.on_submit
350                        && key_press.is_focused
351                        && key_press.modified_key
352                            == keyboard::Key::Named(keyboard::key::Named::Enter)
353                    {
354                        return Some(editor::Binding::Custom(on_submit.clone()));
355                    }
356
357                    editor::Binding::from_key_press(key_press)
358                });
359
360            if let Some(edit) = edit {
361                let on_input = if let Some(on_paste) = &self.on_paste
362                    && edit.has_pasted
363                {
364                    on_paste
365                } else {
366                    on_input
367                };
368
369                state.value = state.input.value();
370                state.transaction = Some(shell.publish_and_track(on_input(state.value.clone())));
371            }
372        }
373
374        let status = if is_disabled {
375            Status::Disabled
376        } else if state.input.is_focused() {
377            Status::Focused {
378                is_hovered: cursor.is_over(layout.bounds()),
379            }
380        } else if cursor.is_over(layout.bounds()) {
381            Status::Hovered
382        } else {
383            Status::Active
384        };
385
386        if let Event::Window(window::Event::RedrawRequested(_now)) = event {
387            self.last_status = Some(status);
388
389            shell.request_input_method(
390                &state
391                    .input
392                    .input_method(layout.bounds().shrink(self.padding).position()),
393            );
394        } else if self
395            .last_status
396            .is_some_and(|last_status| status != last_status)
397        {
398            shell.request_redraw();
399        }
400    }
401
402    fn draw(
403        &self,
404        tree: &Tree,
405        renderer: &mut Renderer,
406        theme: &Theme,
407        _style: &renderer::Style,
408        layout: Layout<'_>,
409        _cursor: mouse::Cursor,
410        viewport: &Rectangle,
411    ) {
412        let state = tree.state.downcast_ref::<State<Renderer>>();
413        let style = theme.style(&self.class, self.last_status.unwrap_or(Status::Disabled));
414        let bounds = layout.bounds();
415
416        renderer.fill_quad(
417            renderer::Quad {
418                bounds,
419                border: style.border,
420                ..renderer::Quad::default()
421            },
422            style.background,
423        );
424
425        state.input.draw(
426            renderer,
427            bounds,
428            *viewport,
429            input::Style {
430                value: style.value,
431                selection: style.selection,
432                placeholder: style.placeholder,
433            },
434        );
435    }
436
437    fn mouse_interaction(
438        &self,
439        _tree: &Tree,
440        layout: Layout<'_>,
441        cursor: mouse::Cursor,
442        _viewport: &Rectangle,
443        _renderer: &Renderer,
444    ) -> mouse::Interaction {
445        if cursor.is_over(layout.bounds()) {
446            if self.on_input.is_none() {
447                mouse::Interaction::Idle
448            } else {
449                mouse::Interaction::Text
450            }
451        } else {
452            mouse::Interaction::default()
453        }
454    }
455}
456
457impl<'a, Message, Theme, Renderer> From<TextInput<'a, Message, Theme>>
458    for Element<'a, Message, Theme, Renderer>
459where
460    Message: Clone + 'a,
461    Theme: Catalog + 'a,
462    Renderer: text::Renderer + 'static,
463{
464    fn from(text_input: TextInput<'a, Message, Theme>) -> Element<'a, Message, Theme, Renderer> {
465        Element::new(text_input)
466    }
467}
468
469/// The state of a [`TextInput`].
470struct State<R: text::Renderer> {
471    input: text::Input<R>,
472    value: String,
473    transaction: Option<shell::Tracking>,
474}
475
476fn state<Renderer: text::Renderer + 'static>(tree: &mut Tree) -> &mut State<Renderer> {
477    tree.state.downcast_mut::<State<Renderer>>()
478}
479
480impl<R: text::Renderer> State<R> {
481    /// Creates a new [`State`], representing an unfocused [`TextInput`].
482    fn new() -> Self {
483        Self {
484            input: text::Input::new(),
485            value: String::new(),
486            transaction: None,
487        }
488    }
489}
490
491impl<R: text::Renderer> operation::Focusable for State<R> {
492    fn is_focused(&self) -> bool {
493        self.input.is_focused()
494    }
495
496    fn focus(&mut self) {
497        self.input.focus();
498    }
499
500    fn unfocus(&mut self) {
501        self.input.unfocus();
502    }
503}
504
505impl<R: text::Renderer> operation::TextInput for State<R> {
506    fn text(&self) -> text::Fragment<'_> {
507        if self.input.is_empty() {
508            text::Fragment::Borrowed(self.input.placeholder())
509        } else {
510            text::Fragment::Owned(self.input.value())
511        }
512    }
513
514    fn move_cursor_to_front(&mut self) {
515        self.input.move_cursor_to_front();
516    }
517
518    fn move_cursor_to_end(&mut self) {
519        self.input.move_cursor_to_end();
520    }
521
522    fn move_cursor_to(&mut self, position: text::Position) {
523        self.input.move_cursor_to(position);
524    }
525
526    fn select_all(&mut self) {
527        self.input.select_all();
528    }
529
530    fn select_range(&mut self, start: text::Position, end: text::Position) {
531        self.input.select_range(start, end);
532    }
533}
534
535/// The possible status of a [`TextInput`].
536#[derive(Debug, Clone, Copy, PartialEq, Eq)]
537pub enum Status {
538    /// The [`TextInput`] can be interacted with.
539    Active,
540    /// The [`TextInput`] is being hovered.
541    Hovered,
542    /// The [`TextInput`] is focused.
543    Focused {
544        /// Whether the [`TextInput`] is hovered, while focused.
545        is_hovered: bool,
546    },
547    /// The [`TextInput`] cannot be interacted with.
548    Disabled,
549}
550
551/// The appearance of a text input.
552#[derive(Debug, Clone, Copy, PartialEq)]
553pub struct Style {
554    /// The [`Background`] of the text input.
555    pub background: Background,
556    /// The [`Border`] of the text input.
557    pub border: Border,
558    /// The [`Color`] of the placeholder of the text input.
559    pub placeholder: Color,
560    /// The [`Color`] of the value of the text input.
561    pub value: Color,
562    /// The [`Color`] of the selection of the text input.
563    pub selection: Color,
564}
565
566/// The theme catalog of a [`TextInput`].
567pub trait Catalog: Sized {
568    /// The item class of the [`Catalog`].
569    type Class<'a>;
570
571    /// The default class produced by the [`Catalog`].
572    fn default<'a>() -> Self::Class<'a>;
573
574    /// The [`Style`] of a class with the given status.
575    fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
576}
577
578/// A styling function for a [`TextInput`].
579///
580/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`.
581pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
582
583impl Catalog for Theme {
584    type Class<'a> = StyleFn<'a, Self>;
585
586    fn default<'a>() -> Self::Class<'a> {
587        Box::new(default)
588    }
589
590    fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
591        class(self, status)
592    }
593}
594
595/// The default style of a [`TextInput`].
596pub fn default(theme: &Theme, status: Status) -> Style {
597    let palette = theme.palette();
598
599    let active = Style {
600        background: Background::Color(palette.background.base.color),
601        border: Border {
602            radius: 2.0.into(),
603            width: 1.0,
604            color: palette.background.strong.color,
605        },
606        placeholder: palette.secondary.base.color,
607        value: palette.background.base.text,
608        selection: palette.primary.weak.color,
609    };
610
611    match status {
612        Status::Active => active,
613        Status::Hovered => Style {
614            border: Border {
615                color: palette.background.base.text,
616                ..active.border
617            },
618            ..active
619        },
620        Status::Focused { .. } => Style {
621            border: Border {
622                color: palette.primary.strong.color,
623                ..active.border
624            },
625            ..active
626        },
627        Status::Disabled => Style {
628            background: Background::Color(palette.background.weak.color),
629            value: active.placeholder,
630            placeholder: palette.background.strongest.color,
631            ..active
632        },
633    }
634}