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