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