Skip to main content

cosmic/widget/text_input/
input.rs

1// Copyright 2019 H�ctor Ram�n, Iced contributors
2// Copyright 2023 System76 <info@system76.com>
3// SPDX-License-Identifier: MIT
4
5//! Display fields that can be filled with text.
6//!
7//! A [`TextInput`] has some local [`State`].
8use std::borrow::Cow;
9use std::cell::{Cell, LazyCell};
10
11use crate::ext::ColorExt;
12use crate::theme::THEME;
13
14use super::cursor;
15pub use super::cursor::Cursor;
16use super::editor::Editor;
17use super::style::StyleSheet;
18pub use super::value::Value;
19
20use apply::Apply;
21use iced::Limits;
22use iced::clipboard::dnd::{DndAction, DndEvent, OfferEvent, SourceEvent};
23use iced::clipboard::mime::AsMimeTypes;
24use iced_core::event::{self, Event};
25use iced_core::input_method::{self, InputMethod, Preedit};
26use iced_core::mouse::{self, click};
27use iced_core::overlay::Group;
28use iced_core::renderer::{self, Renderer as CoreRenderer};
29use iced_core::text::{self, Affinity, Paragraph, Renderer, Text};
30use iced_core::time::{Duration, Instant};
31use iced_core::widget::Id;
32use iced_core::widget::operation::{self, Operation};
33use iced_core::widget::tree::{self, Tree};
34use iced_core::{
35    Background, Border, Clipboard, Color, Element, Layout, Length, Padding, Pixels, Point,
36    Rectangle, Shadow, Shell, Size, Vector, Widget, alignment, keyboard, layout, overlay, touch,
37    window,
38};
39use iced_runtime::{Action, Task, task};
40
41thread_local! {
42    // Prevents two inputs from being focused at the same time.
43    static LAST_FOCUS_UPDATE: LazyCell<Cell<Instant>> = LazyCell::new(|| Cell::new(Instant::now()));
44}
45
46/// Notify all text inputs that a different widget has taken focus.
47/// This causes any focused text input to unfocus on its next layout.
48pub fn notify_focus_change() {
49    LAST_FOCUS_UPDATE.with(|x| x.set(Instant::now()));
50}
51
52/// Creates a new [`TextInput`].
53///
54/// [`TextInput`]: widget::TextInput
55pub fn text_input<'a, Message>(
56    placeholder: impl Into<Cow<'a, str>>,
57    value: impl Into<Cow<'a, str>>,
58) -> TextInput<'a, Message>
59where
60    Message: Clone + 'static,
61{
62    TextInput::new(placeholder, value)
63}
64
65/// A text label which can transform into a text input on activation.
66pub fn editable_input<'a, Message: Clone + 'static>(
67    placeholder: impl Into<Cow<'a, str>>,
68    text: impl Into<Cow<'a, str>>,
69    editing: bool,
70    on_toggle_edit: impl Fn(bool) -> Message + 'a,
71) -> TextInput<'a, Message> {
72    // The trailing icon is a placeholder; diff() rebuilds it reactively
73    // based on the current is_read_only state and value content.
74    TextInput::new(placeholder, text)
75        .style(crate::theme::TextInput::EditableText)
76        .editable()
77        .editing(editing)
78        .on_toggle_edit(on_toggle_edit)
79        .trailing_icon(
80            crate::widget::icon::from_name("edit-symbolic")
81                .size(16)
82                .apply(crate::widget::container)
83                .padding(8)
84                .into(),
85        )
86}
87
88/// Creates a new search [`TextInput`].
89///
90/// [`TextInput`]: widget::TextInput
91pub fn search_input<'a, Message>(
92    placeholder: impl Into<Cow<'a, str>>,
93    value: impl Into<Cow<'a, str>>,
94) -> TextInput<'a, Message>
95where
96    Message: Clone + 'static,
97{
98    let spacing = THEME.lock().unwrap().cosmic().space_xxs();
99
100    TextInput::new(placeholder, value)
101        .padding([0, spacing])
102        .style(crate::theme::TextInput::Search)
103        .leading_icon(
104            crate::widget::icon::from_name("system-search-symbolic")
105                .size(16)
106                .apply(crate::widget::container)
107                .padding(8)
108                .into(),
109        )
110}
111/// Creates a new secure [`TextInput`].
112///
113/// [`TextInput`]: widget::TextInput
114pub fn secure_input<'a, Message>(
115    placeholder: impl Into<Cow<'a, str>>,
116    value: impl Into<Cow<'a, str>>,
117    on_visible_toggle: Option<Message>,
118    hidden: bool,
119) -> TextInput<'a, Message>
120where
121    Message: Clone + 'static,
122{
123    let spacing = THEME.lock().unwrap().cosmic().space_xxs();
124    let mut input = TextInput::new(placeholder, value)
125        .padding([0, spacing])
126        .style(crate::theme::TextInput::Default)
127        .leading_icon(
128            crate::widget::icon::from_name("system-lock-screen-symbolic")
129                .size(16)
130                .apply(crate::widget::container)
131                .padding(8)
132                .into(),
133        );
134    if hidden {
135        input = input.password();
136    }
137    if let Some(msg) = on_visible_toggle {
138        input.trailing_icon(
139            crate::widget::icon::from_name(if hidden {
140                "document-properties-symbolic"
141            } else {
142                "image-red-eye-symbolic"
143            })
144            .size(16)
145            .apply(crate::widget::button::custom)
146            .class(crate::theme::Button::Icon)
147            .on_press(msg)
148            .padding(8)
149            .into(),
150        )
151    } else {
152        input
153    }
154}
155
156/// Creates a new inline [`TextInput`].
157///
158/// [`TextInput`]: widget::TextInput
159pub fn inline_input<'a, Message>(
160    placeholder: impl Into<Cow<'a, str>>,
161    value: impl Into<Cow<'a, str>>,
162) -> TextInput<'a, Message>
163where
164    Message: Clone + 'static,
165{
166    let spacing = THEME.lock().unwrap().cosmic().space_xxs();
167
168    TextInput::new(placeholder, value)
169        .style(crate::theme::TextInput::Inline)
170        .padding(spacing)
171}
172
173pub(crate) const SUPPORTED_TEXT_MIME_TYPES: &[&str; 6] = &[
174    "text/plain;charset=utf-8",
175    "text/plain;charset=UTF-8",
176    "UTF8_STRING",
177    "STRING",
178    "text/plain",
179    "TEXT",
180];
181
182/// A field that can be filled with text.
183#[allow(missing_debug_implementations)]
184#[must_use]
185pub struct TextInput<'a, Message> {
186    id: Id,
187    placeholder: Cow<'a, str>,
188    value: Value,
189    is_secure: bool,
190    is_editable_variant: bool,
191    is_read_only: bool,
192    select_on_focus: bool,
193    double_click_select_delimiter: Option<char>,
194    font: Option<<crate::Renderer as iced_core::text::Renderer>::Font>,
195    width: Length,
196    padding: Padding,
197    size: Option<f32>,
198    helper_size: f32,
199    label: Option<Cow<'a, str>>,
200    helper_text: Option<Cow<'a, str>>,
201    error: Option<Cow<'a, str>>,
202    on_focus: Option<Message>,
203    on_unfocus: Option<Message>,
204    on_input: Option<Box<dyn Fn(String) -> Message + 'a>>,
205    on_paste: Option<Box<dyn Fn(String) -> Message + 'a>>,
206    on_tab: Option<Message>,
207    on_submit: Option<Box<dyn Fn(String) -> Message + 'a>>,
208    on_toggle_edit: Option<Box<dyn Fn(bool) -> Message + 'a>>,
209    leading_icon: Option<Element<'a, Message, crate::Theme, crate::Renderer>>,
210    trailing_icon: Option<Element<'a, Message, crate::Theme, crate::Renderer>>,
211    style: <crate::Theme as StyleSheet>::Style,
212    on_create_dnd_source: Option<Box<dyn Fn(State) -> Message + 'a>>,
213    surface_ids: Option<(window::Id, window::Id)>,
214    dnd_icon: bool,
215    line_height: text::LineHeight,
216    helper_line_height: text::LineHeight,
217    always_active: bool,
218    /// The text input tracks and manages the input value in its state.
219    manage_value: bool,
220    drag_threshold: f32,
221    window_id: window::Id,
222}
223
224impl<'a, Message> TextInput<'a, Message>
225where
226    Message: Clone + 'static,
227{
228    /// Creates a new [`TextInput`].
229    ///
230    /// It expects:
231    /// - a placeholder,
232    /// - the current value
233    pub fn new(placeholder: impl Into<Cow<'a, str>>, value: impl Into<Cow<'a, str>>) -> Self {
234        let spacing = THEME.lock().unwrap().cosmic().space_xxs();
235
236        let v: Cow<'a, str> = value.into();
237        TextInput {
238            id: Id::unique(),
239            placeholder: placeholder.into(),
240            value: Value::new(v.as_ref()),
241            is_secure: false,
242            is_editable_variant: false,
243            is_read_only: false,
244            select_on_focus: false,
245            double_click_select_delimiter: None,
246            font: None,
247            width: Length::Fill,
248            padding: spacing.into(),
249            size: None,
250            helper_size: 10.0,
251            helper_line_height: text::LineHeight::Absolute(14.0.into()),
252            on_focus: None,
253            on_unfocus: None,
254            on_input: None,
255            on_paste: None,
256            on_submit: None,
257            on_tab: None,
258            on_toggle_edit: None,
259            leading_icon: None,
260            trailing_icon: None,
261            error: None,
262            style: crate::theme::TextInput::default(),
263            on_create_dnd_source: None,
264            surface_ids: None,
265            dnd_icon: false,
266            line_height: text::LineHeight::default(),
267            label: None,
268            helper_text: None,
269            always_active: false,
270            manage_value: false,
271            drag_threshold: 20.0,
272            window_id: crate::widget::text_context_menu::current_window_id(),
273        }
274    }
275
276    #[inline]
277    fn dnd_id(&self) -> u128 {
278        match &self.id.0 {
279            iced_core::id::Internal::Custom(id, _) | iced_core::id::Internal::Unique(id) => {
280                *id as u128
281            }
282            _ => unreachable!(),
283        }
284    }
285
286    /// Sets the input to be always active.
287    /// This makes it behave as if it was always focused.
288    #[inline]
289    pub const fn always_active(mut self) -> Self {
290        self.always_active = true;
291        self
292    }
293
294    /// Sets the text of the [`TextInput`].
295    pub fn label(mut self, label: impl Into<Cow<'a, str>>) -> Self {
296        self.label = Some(label.into());
297        self
298    }
299
300    /// Sets the helper text of the [`TextInput`].
301    pub fn helper_text(mut self, helper_text: impl Into<Cow<'a, str>>) -> Self {
302        self.helper_text = Some(helper_text.into());
303        self
304    }
305
306    /// Sets the [`Id`] of the [`TextInput`].
307    #[inline]
308    pub fn id(mut self, id: Id) -> Self {
309        self.id = id;
310        self
311    }
312
313    /// Sets the error message of the [`TextInput`].
314    pub fn error(mut self, error: impl Into<Cow<'a, str>>) -> Self {
315        self.error = Some(error.into());
316        self
317    }
318
319    /// Sets the [`LineHeight`] of the [`TextInput`].
320    pub fn line_height(mut self, line_height: impl Into<text::LineHeight>) -> Self {
321        self.line_height = line_height.into();
322        self
323    }
324
325    /// Converts the [`TextInput`] into a secure password input.
326    #[inline]
327    pub const fn password(mut self) -> Self {
328        self.is_secure = true;
329        self
330    }
331
332    /// Applies behaviors unique to the `editable_input` variable.
333    #[inline]
334    pub(crate) const fn editable(mut self) -> Self {
335        self.is_editable_variant = true;
336        self
337    }
338
339    #[inline]
340    pub const fn editing(mut self, enable: bool) -> Self {
341        self.is_read_only = !enable;
342        self
343    }
344
345    /// Selects all text when the text input is focused
346    #[inline]
347    pub const fn select_on_focus(mut self, select_on_focus: bool) -> Self {
348        self.select_on_focus = select_on_focus;
349        self
350    }
351
352    /// Sets a delimiter character for double-click selection behavior.
353    ///
354    /// When set, double-clicking before the last occurrence of this character
355    /// selects from the start to that character. Double-clicking after the
356    /// delimiter uses normal word selection.
357    #[inline]
358    pub const fn double_click_select_delimiter(mut self, delimiter: char) -> Self {
359        self.double_click_select_delimiter = Some(delimiter);
360        self
361    }
362
363    /// Emits a message when an unfocused text input has been focused by click.
364    ///
365    /// This will not trigger if the input was focused externally by the application.
366    #[inline]
367    pub fn on_focus(mut self, on_focus: Message) -> Self {
368        self.on_focus = Some(on_focus);
369        self
370    }
371
372    /// Emits a message when a focused text input has been unfocused via the Tab or Esc key.
373    ///
374    /// This will not trigger if the input was unfocused externally by the application.
375    #[inline]
376    pub fn on_unfocus(mut self, on_unfocus: Message) -> Self {
377        self.on_unfocus = Some(on_unfocus);
378        self
379    }
380
381    /// Sets the message that should be produced when some text is typed into
382    /// the [`TextInput`].
383    ///
384    /// If this method is not called, the [`TextInput`] will be disabled.
385    pub fn on_input(mut self, callback: impl Fn(String) -> Message + 'a) -> Self {
386        self.on_input = Some(Box::new(callback));
387        self
388    }
389
390    /// Emits a message when a focused text input receives the Enter/Return key.
391    pub fn on_submit(mut self, callback: impl Fn(String) -> Message + 'a) -> Self {
392        self.on_submit = Some(Box::new(callback));
393        self
394    }
395
396    /// Optionally emits a message when a focused text input receives the Enter/Return key.
397    pub fn on_submit_maybe(self, callback: Option<impl Fn(String) -> Message + 'a>) -> Self {
398        if let Some(callback) = callback {
399            self.on_submit(callback)
400        } else {
401            self
402        }
403    }
404
405    /// Emits a message when the Tab key has been captured, which prevents focus from changing.
406    ///
407    /// If you do no want to capture the Tab key, use [`TextInput::on_unfocus`] instead.
408    #[inline]
409    pub fn on_tab(mut self, on_tab: Message) -> Self {
410        self.on_tab = Some(on_tab);
411        self
412    }
413
414    /// Emits a message when the editable state of the input changes.
415    pub fn on_toggle_edit(mut self, callback: impl Fn(bool) -> Message + 'a) -> Self {
416        self.on_toggle_edit = Some(Box::new(callback));
417        self
418    }
419
420    /// Sets the message that should be produced when some text is pasted into
421    /// the [`TextInput`].
422    pub fn on_paste(mut self, on_paste: impl Fn(String) -> Message + 'a) -> Self {
423        self.on_paste = Some(Box::new(on_paste));
424        self
425    }
426
427    /// Sets the [`Font`] of the [`TextInput`].
428    ///
429    /// [`Font`]: text::Renderer::Font
430    #[inline]
431    pub const fn font(
432        mut self,
433        font: <crate::Renderer as iced_core::text::Renderer>::Font,
434    ) -> Self {
435        self.font = Some(font);
436        self
437    }
438
439    /// Sets the start [`Icon`] of the [`TextInput`].
440    #[inline]
441    pub fn leading_icon(
442        mut self,
443        icon: Element<'a, Message, crate::Theme, crate::Renderer>,
444    ) -> Self {
445        self.leading_icon = Some(icon);
446        self
447    }
448
449    /// Sets the end [`Icon`] of the [`TextInput`].
450    #[inline]
451    pub fn trailing_icon(
452        mut self,
453        icon: Element<'a, Message, crate::Theme, crate::Renderer>,
454    ) -> Self {
455        self.trailing_icon = Some(icon);
456        self
457    }
458
459    /// Sets the width of the [`TextInput`].
460    pub fn width(mut self, width: impl Into<Length>) -> Self {
461        self.width = width.into();
462        self
463    }
464
465    /// Sets the [`Padding`] of the [`TextInput`].
466    pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
467        self.padding = padding.into();
468        self
469    }
470
471    /// Sets the text size of the [`TextInput`].
472    pub fn size(mut self, size: impl Into<Pixels>) -> Self {
473        self.size = Some(size.into().0);
474        self
475    }
476
477    /// Sets the style of the [`TextInput`].
478    pub fn style(mut self, style: impl Into<<crate::Theme as StyleSheet>::Style>) -> Self {
479        self.style = style.into();
480        self
481    }
482
483    /// Sets the text input to manage its input value or not
484    #[inline]
485    pub const fn manage_value(mut self, manage_value: bool) -> Self {
486        self.manage_value = manage_value;
487        self
488    }
489
490    /// Draws the [`TextInput`] with the given [`Renderer`], overriding its
491    /// [`Value`] if provided.
492    ///
493    /// [`Renderer`]: text::Renderer
494    #[allow(clippy::too_many_arguments)]
495    #[inline]
496    pub fn draw(
497        &self,
498        tree: &Tree,
499        renderer: &mut crate::Renderer,
500        theme: &crate::Theme,
501        layout: Layout<'_>,
502        cursor_position: mouse::Cursor,
503        value: Option<&Value>,
504        style: &renderer::Style,
505    ) {
506        let text_layout = self.text_layout(layout);
507        draw(
508            renderer,
509            theme,
510            layout,
511            text_layout,
512            cursor_position,
513            tree,
514            value.unwrap_or(&self.value),
515            &self.placeholder,
516            self.size,
517            self.font,
518            self.on_input.is_none(),
519            self.is_secure,
520            self.leading_icon.as_ref(),
521            self.trailing_icon.as_ref(),
522            &self.style,
523            self.dnd_icon,
524            self.line_height,
525            self.error.as_deref(),
526            self.label.as_deref(),
527            self.helper_text.as_deref(),
528            self.helper_size,
529            self.helper_line_height,
530            &layout.bounds(),
531            style,
532        );
533    }
534
535    /// Sets the start dnd handler of the [`TextInput`].
536    #[cfg(wayland_platform)]
537    pub fn on_start_dnd(mut self, on_start_dnd: impl Fn(State) -> Message + 'a) -> Self {
538        self.on_create_dnd_source = Some(Box::new(on_start_dnd));
539        self
540    }
541
542    /// Sets the window id of the [`TextInput`] and the window id of the drag icon.
543    /// Both ids are required to be unique.
544    /// This is required for the dnd to work.
545    #[inline]
546    pub const fn surface_ids(mut self, window_id: (window::Id, window::Id)) -> Self {
547        self.surface_ids = Some(window_id);
548        self
549    }
550
551    /// Sets the mode of this [`TextInput`] to be a drag and drop icon.
552    #[inline]
553    pub const fn dnd_icon(mut self, dnd_icon: bool) -> Self {
554        self.dnd_icon = dnd_icon;
555        self
556    }
557
558    pub fn on_clear(self, on_clear: Message) -> Self {
559        self.trailing_icon(
560            crate::widget::icon::from_name("edit-clear-symbolic")
561                .size(16)
562                .apply(crate::widget::button::custom)
563                .class(crate::theme::Button::Icon)
564                .on_press(on_clear)
565                .padding(8)
566                .into(),
567        )
568    }
569
570    /// Get the layout node of the actual text input
571    fn text_layout<'b>(&'a self, layout: Layout<'b>) -> Layout<'b> {
572        if self.dnd_icon {
573            layout
574        } else if self.label.is_some() {
575            let mut nodes = layout.children();
576            nodes.next();
577            nodes.next().unwrap()
578        } else {
579            layout.children().next().unwrap()
580        }
581    }
582
583    /// Set the drag threshold.
584    pub fn drag_threshold(mut self, drag_threshold: f32) -> Self {
585        self.drag_threshold = drag_threshold;
586        self
587    }
588
589    fn uses_popup_context_menu(&self) -> bool {
590        #[cfg(all(wayland_platform, feature = "winit"))]
591        if matches!(
592            crate::app::cosmic::WINDOWING_SYSTEM.get(),
593            Some(crate::app::cosmic::WindowingSystem::Wayland)
594        ) {
595            return true;
596        }
597        false
598    }
599}
600
601impl<Message> Widget<Message, crate::Theme, crate::Renderer> for TextInput<'_, Message>
602where
603    Message: Clone + 'static,
604{
605    #[inline]
606    fn tag(&self) -> tree::Tag {
607        tree::Tag::of::<State>()
608    }
609
610    #[inline]
611    fn state(&self) -> tree::State {
612        tree::State::new(State::new(
613            self.is_secure,
614            self.is_read_only,
615            self.always_active,
616            self.select_on_focus,
617        ))
618    }
619
620    fn diff(&mut self, tree: &mut Tree) {
621        let state = tree.state.downcast_mut::<State>();
622
623        if !self.manage_value || !self.value.is_empty() && state.tracked_value != self.value {
624            state.tracked_value = self.value.clone();
625        } else if self.value.is_empty() {
626            self.value = state.tracked_value.clone();
627            // std::mem::swap(&mut state.tracked_value, &mut self.value);
628        }
629        state.double_click_select_delimiter = self.double_click_select_delimiter;
630        // Unfocus text input if it becomes disabled
631        if self.on_input.is_none() && !self.manage_value {
632            state.last_click = None;
633            state.is_focused = state.is_focused.map(|mut f| {
634                f.focused = false;
635                f
636            });
637            state.is_pasting = None;
638            state.dragging_state = None;
639        }
640        let old_value = state
641            .value
642            .raw()
643            .buffer()
644            .lines
645            .iter()
646            .map(|l| l.text())
647            .collect::<String>();
648        if state.is_secure != self.is_secure
649            || old_value != self.value.to_string()
650            || state
651                .label
652                .raw()
653                .buffer()
654                .lines
655                .iter()
656                .map(|l| l.text())
657                .collect::<String>()
658                != self.label.as_deref().unwrap_or_default()
659            || state
660                .helper_text
661                .raw()
662                .buffer()
663                .lines
664                .iter()
665                .map(|l| l.text())
666                .collect::<String>()
667                != self.helper_text.as_deref().unwrap_or_default()
668        {
669            state.is_secure = self.is_secure;
670            state.dirty = true;
671        }
672
673        if self.always_active && !state.is_focused() {
674            let now = Instant::now();
675            LAST_FOCUS_UPDATE.with(|x| x.set(now));
676            state.is_focused = Some(Focus {
677                updated_at: now,
678                now,
679                focused: true,
680                needs_update: false,
681            });
682        }
683
684        // if the previous state was at the end of the text, keep it there
685        let old_value = Value::new(&old_value);
686        if state.is_focused()
687            && let cursor::State::Index(index) = state.cursor.state(&old_value)
688        {
689            if index == old_value.len() {
690                state.cursor.move_to(self.value.len());
691            }
692        }
693
694        if let Some(f) = state.is_focused.as_ref().filter(|f| f.focused) {
695            if f.updated_at != LAST_FOCUS_UPDATE.with(|f| f.get()) {
696                state.unfocus();
697                state.emit_unfocus = true;
698            }
699        }
700
701        if self.is_editable_variant {
702            if !state.is_focused() {
703                // Not yet interacted, use the widget's value
704                state.is_read_only = self.is_read_only;
705            } else {
706                // Already interacted, use the state
707                self.is_read_only = state.is_read_only;
708            }
709
710            let editing = !self.is_read_only;
711            let icon_name = if editing {
712                if self.value.is_empty() {
713                    "window-close-symbolic"
714                } else {
715                    "edit-clear-symbolic"
716                }
717            } else {
718                "edit-symbolic"
719            };
720
721            self.trailing_icon = Some(
722                crate::widget::icon::from_name(icon_name)
723                    .size(16)
724                    .apply(crate::widget::container)
725                    .padding(8)
726                    .into(),
727            );
728        } else {
729            self.is_read_only = state.is_read_only;
730        }
731
732        // Stop pasting if input becomes disabled
733        if !self.manage_value && self.on_input.is_none() {
734            state.is_pasting = None;
735        }
736
737        let mut children: Vec<_> = self
738            .leading_icon
739            .iter_mut()
740            .chain(self.trailing_icon.iter_mut())
741            .map(iced_core::Element::as_widget_mut)
742            .collect();
743        tree.diff_children(children.as_mut_slice());
744    }
745
746    fn children(&self) -> Vec<Tree> {
747        self.leading_icon
748            .iter()
749            .chain(self.trailing_icon.iter())
750            .map(|icon| Tree::new(icon))
751            .collect()
752    }
753
754    #[inline]
755    fn size(&self) -> Size<Length> {
756        Size {
757            width: self.width,
758            height: Length::Shrink,
759        }
760    }
761
762    fn layout(
763        &mut self,
764        tree: &mut Tree,
765        renderer: &crate::Renderer,
766        limits: &layout::Limits,
767    ) -> layout::Node {
768        let font = self.font.unwrap_or_else(|| renderer.default_font());
769        if self.dnd_icon {
770            let state = tree.state.downcast_mut::<State>();
771            let limits = limits.width(Length::Shrink).height(Length::Shrink);
772
773            let size = self.size.unwrap_or_else(|| renderer.default_size().0);
774
775            let bounds = limits.resolve(Length::Shrink, Length::Fill, Size::INFINITE);
776            let value_paragraph = &mut state.value;
777            let v = self.value.to_string();
778            value_paragraph.update(Text {
779                content: if self.value.is_empty() {
780                    self.placeholder.as_ref()
781                } else {
782                    &v
783                },
784                font,
785                bounds,
786                size: iced::Pixels(size),
787                align_x: text::Alignment::Left,
788                align_y: alignment::Vertical::Center,
789                line_height: text::LineHeight::default(),
790                shaping: text::Shaping::Advanced,
791                wrapping: text::Wrapping::None,
792                ellipsize: text::Ellipsize::None,
793            });
794
795            let Size { width, height } =
796                limits.resolve(Length::Shrink, Length::Shrink, value_paragraph.min_bounds());
797
798            let size = limits.resolve(width, height, Size::new(width, height));
799            layout::Node::with_children(size, vec![layout::Node::new(size)])
800        } else {
801            let res = layout(
802                renderer,
803                limits,
804                self.width,
805                self.padding,
806                self.size,
807                self.leading_icon.as_mut(),
808                self.trailing_icon.as_mut(),
809                self.line_height,
810                self.label.as_deref(),
811                self.helper_text.as_deref(),
812                self.helper_size,
813                self.helper_line_height,
814                font,
815                tree,
816            );
817
818            // XXX not ideal, but we need to update the cache when is_secure changes
819            let size = self.size.unwrap_or_else(|| renderer.default_size().0);
820            let line_height = self.line_height;
821            let state = tree.state.downcast_mut::<State>();
822            if state.dirty {
823                state.dirty = false;
824                let value = if self.is_secure {
825                    &self.value.secure()
826                } else {
827                    &self.value
828                };
829                replace_paragraph(
830                    state,
831                    Layout::new(&res),
832                    value,
833                    font,
834                    iced::Pixels(size),
835                    line_height,
836                    limits,
837                );
838            }
839            res
840        }
841    }
842
843    fn operate(
844        &mut self,
845        tree: &mut Tree,
846        layout: Layout<'_>,
847        renderer: &crate::Renderer,
848        operation: &mut dyn Operation,
849    ) {
850        operation.container(Some(&self.id), layout.bounds());
851        let state = tree.state.downcast_mut::<State>();
852
853        operation.focusable(Some(&self.id), layout.bounds(), state);
854        operation.text_input(Some(&self.id), layout.bounds(), state);
855    }
856
857    fn overlay<'b>(
858        &'b mut self,
859        tree: &'b mut Tree,
860        layout: Layout<'b>,
861        renderer: &crate::Renderer,
862        viewport: &Rectangle,
863        translation: Vector,
864    ) -> Option<overlay::Element<'b, Message, crate::Theme, crate::Renderer>> {
865        if !self.uses_popup_context_menu() {
866            let has_context_menu = tree
867                .state
868                .downcast_ref::<State>()
869                .context_menu_position
870                .is_some();
871            if has_context_menu {
872                let menu_bar_state = tree.state.downcast_ref::<State>().menu_bar_state.clone();
873                return crate::widget::text_context_menu::context_menu_overlay(
874                    self,
875                    tree,
876                    self.on_input.as_deref(),
877                    translation,
878                    menu_bar_state,
879                );
880            }
881        }
882
883        let mut layout_ = Vec::with_capacity(2);
884        if self.leading_icon.is_some() {
885            let mut children = self.text_layout(layout).children();
886            children.next();
887            layout_.push(children.next().unwrap());
888        }
889        if self.trailing_icon.is_some() {
890            let mut children = self.text_layout(layout).children();
891            children.next();
892            if self.leading_icon.is_some() {
893                children.next();
894            }
895            layout_.push(children.next().unwrap());
896        };
897        let children: Vec<overlay::Element<'_, Message, crate::Theme, crate::Renderer>> = self
898            .leading_icon
899            .iter_mut()
900            .chain(self.trailing_icon.iter_mut())
901            .zip(&mut tree.children)
902            .zip(layout_)
903            .filter_map(|((child, state), layout)| {
904                child
905                    .as_widget_mut()
906                    .overlay(state, layout, renderer, viewport, translation)
907            })
908            .collect();
909
910        (!children.is_empty()).then(|| Group::with_children(children).overlay())
911    }
912
913    fn update(
914        &mut self,
915        tree: &mut Tree,
916        event: &Event,
917        layout: Layout<'_>,
918        cursor_position: mouse::Cursor,
919        renderer: &crate::Renderer,
920        clipboard: &mut dyn Clipboard,
921        shell: &mut Shell<'_, Message>,
922        viewport: &Rectangle,
923    ) {
924        #[cfg(all(wayland_platform, feature = "winit"))]
925        if self.uses_popup_context_menu() {
926            let menu_bar_state = tree.state.downcast_ref::<State>().menu_bar_state.clone();
927            crate::widget::text_context_menu::dismiss_popup_on_event(
928                &menu_bar_state,
929                event,
930                self.window_id,
931            );
932        }
933
934        let text_layout = self.text_layout(layout);
935        let mut trailing_icon_layout = None;
936        let font = self.font.unwrap_or_else(|| renderer.default_font());
937        let size = self.size.unwrap_or_else(|| renderer.default_size().0);
938        let line_height = self.line_height;
939
940        // Disables editing of the editable variant when clicking outside of, or for tab focus changes.
941        if self.is_editable_variant {
942            if let Some(ref on_edit) = self.on_toggle_edit {
943                let state = tree.state.downcast_mut::<State>();
944                if !state.is_read_only && state.is_focused.is_some_and(|f| !f.focused) {
945                    state.is_read_only = true;
946                    shell.publish((on_edit)(false));
947                } else if let Some(f) = state.is_focused.as_mut().filter(|f| f.needs_update) {
948                    // TODO do we want to just move this to on_focus or on_unfocus for all inputs?
949                    f.needs_update = false;
950                    state.is_read_only = true;
951                    shell.publish((on_edit)(f.focused));
952                }
953            }
954        }
955
956        // Calculates the layout of the trailing icon button element.
957        if !tree.children.is_empty() {
958            let index = tree.children.len() - 1;
959            if let (Some(trailing_icon), Some(tree)) =
960                (self.trailing_icon.as_mut(), tree.children.get_mut(index))
961            {
962                trailing_icon_layout = Some(text_layout.children().last().unwrap());
963
964                // Enable custom buttons defined on the trailing icon position to be handled.
965                if !self.is_editable_variant {
966                    if let Some(trailing_layout) = trailing_icon_layout {
967                        let res = trailing_icon.as_widget_mut().update(
968                            tree,
969                            event,
970                            trailing_layout,
971                            cursor_position,
972                            renderer,
973                            clipboard,
974                            shell,
975                            viewport,
976                        );
977
978                        if shell.is_event_captured() {
979                            return;
980                        }
981                    }
982                }
983            }
984        }
985
986        // Unfocus on any click outside widget bounds.
987        if matches!(
988            event,
989            Event::Mouse(mouse::Event::ButtonPressed(_))
990                | Event::Touch(touch::Event::FingerPressed { .. })
991        ) && cursor_position.position_over(layout.bounds()).is_none()
992        {
993            let state = tree.state.downcast_mut::<State>();
994            state.is_focused = None;
995            state.context_menu_position = None;
996            state.dragging_state = None;
997            if let Some(on_unfocus) = self.on_unfocus.as_ref() {
998                shell.publish(on_unfocus.clone());
999            }
1000            return;
1001        }
1002
1003        let state = tree.state.downcast_mut::<State>();
1004
1005        if let Some(on_unfocus) = self.on_unfocus.as_ref() {
1006            if state.emit_unfocus {
1007                state.emit_unfocus = false;
1008                shell.publish(on_unfocus.clone());
1009            }
1010        }
1011
1012        let dnd_id = self.dnd_id();
1013        let id = Widget::id(self);
1014        update(
1015            id,
1016            event,
1017            text_layout.children().next().unwrap(),
1018            trailing_icon_layout,
1019            cursor_position,
1020            clipboard,
1021            shell,
1022            &mut self.value,
1023            size,
1024            font,
1025            self.is_editable_variant,
1026            self.is_secure,
1027            self.on_focus.as_ref(),
1028            self.on_unfocus.as_ref(),
1029            self.on_input.as_deref(),
1030            self.on_paste.as_deref(),
1031            self.on_submit.as_deref(),
1032            self.on_tab.as_ref(),
1033            self.on_toggle_edit.as_deref(),
1034            || tree.state.downcast_mut::<State>(),
1035            self.on_create_dnd_source.as_deref(),
1036            dnd_id,
1037            line_height,
1038            layout,
1039            self.manage_value,
1040            self.drag_threshold,
1041            self.always_active,
1042        );
1043
1044        // On Wayland: if right-click just set context_menu_position, create a popup instead.
1045        #[cfg(all(wayland_platform, feature = "winit"))]
1046        if matches!(
1047            crate::app::cosmic::WINDOWING_SYSTEM.get(),
1048            Some(crate::app::cosmic::WindowingSystem::Wayland)
1049        ) {
1050            let state = tree.state.downcast_ref::<State>();
1051            if state.context_menu_position.is_some() {
1052                let selected_text = state
1053                    .cursor()
1054                    .selection(&state.tracked_value)
1055                    .map(|(start, end)| state.tracked_value.select(start, end).to_string());
1056                let has_selection = selected_text.is_some();
1057                let click_position = state.context_menu_position.unwrap();
1058                let menu_bar_state = state.menu_bar_state.clone();
1059                let pending_action = state.pending_action.clone();
1060
1061                crate::widget::text_context_menu::create_text_context_popup(
1062                    click_position,
1063                    selected_text,
1064                    true,
1065                    has_selection,
1066                    &menu_bar_state,
1067                    &pending_action,
1068                    renderer,
1069                    viewport,
1070                    cursor_position,
1071                    self.window_id,
1072                );
1073
1074                let state = tree.state.downcast_mut::<State>();
1075                state.context_menu_position = None;
1076            }
1077
1078            // Process deferred actions from the popup.
1079            let state = tree.state.downcast_ref::<State>();
1080            let pending_action = state.pending_action.clone();
1081            if let Some(action) =
1082                crate::widget::text_context_menu::take_pending_action(&pending_action)
1083            {
1084                let state = tree.state.downcast_mut::<State>();
1085                match action {
1086                    crate::widget::text_context_menu::TextCtxAction::Copy => {}
1087                    crate::widget::text_context_menu::TextCtxAction::Cut => {
1088                        let contents = state.delete_selection();
1089                        if let Some(on_input) = self.on_input.as_deref() {
1090                            shell.publish((on_input)(contents));
1091                        }
1092                    }
1093                    crate::widget::text_context_menu::TextCtxAction::Paste => {
1094                        let content: String = clipboard
1095                            .read(iced_core::clipboard::Kind::Standard)
1096                            .unwrap_or_default();
1097                        let filtered: String =
1098                            content.chars().filter(|c| !c.is_control()).collect();
1099                        let contents = state.paste_text(&filtered);
1100                        if let Some(on_input) = self.on_input.as_deref() {
1101                            shell.publish((on_input)(contents));
1102                        }
1103                    }
1104                    crate::widget::text_context_menu::TextCtxAction::SelectAll => {
1105                        state.select_all();
1106                    }
1107                }
1108            }
1109        }
1110
1111        let state = tree.state.downcast_mut::<State>();
1112        let value = if self.is_secure {
1113            self.value.secure()
1114        } else {
1115            self.value.clone()
1116        };
1117        state.scroll_offset = offset(
1118            text_layout.children().next().unwrap().bounds(),
1119            &value,
1120            state,
1121        );
1122    }
1123
1124    #[inline]
1125    fn draw(
1126        &self,
1127        tree: &Tree,
1128        renderer: &mut crate::Renderer,
1129        theme: &crate::Theme,
1130        style: &renderer::Style,
1131        layout: Layout<'_>,
1132        cursor_position: mouse::Cursor,
1133        viewport: &Rectangle,
1134    ) {
1135        let text_layout = self.text_layout(layout);
1136        draw(
1137            renderer,
1138            theme,
1139            layout,
1140            text_layout,
1141            cursor_position,
1142            tree,
1143            &self.value,
1144            &self.placeholder,
1145            self.size,
1146            self.font,
1147            self.on_input.is_none() && !self.manage_value,
1148            self.is_secure,
1149            self.leading_icon.as_ref(),
1150            self.trailing_icon.as_ref(),
1151            &self.style,
1152            self.dnd_icon,
1153            self.line_height,
1154            self.error.as_deref(),
1155            self.label.as_deref(),
1156            self.helper_text.as_deref(),
1157            self.helper_size,
1158            self.helper_line_height,
1159            viewport,
1160            style,
1161        );
1162    }
1163
1164    fn mouse_interaction(
1165        &self,
1166        state: &Tree,
1167        layout: Layout<'_>,
1168        cursor_position: mouse::Cursor,
1169        viewport: &Rectangle,
1170        renderer: &crate::Renderer,
1171    ) -> mouse::Interaction {
1172        let layout = self.text_layout(layout);
1173        let mut index = 0;
1174        if let (Some(leading_icon), Some(tree)) =
1175            (self.leading_icon.as_ref(), state.children.get(index))
1176        {
1177            let leading_icon_layout = layout.children().nth(1).unwrap();
1178
1179            if cursor_position.is_over(leading_icon_layout.bounds()) {
1180                return leading_icon.as_widget().mouse_interaction(
1181                    tree,
1182                    layout,
1183                    cursor_position,
1184                    viewport,
1185                    renderer,
1186                );
1187            }
1188            index += 1;
1189        }
1190
1191        if self.trailing_icon.is_some() {
1192            let mut children = layout.children();
1193            children.next();
1194            // skip if there is no leading icon
1195            if self.leading_icon.is_some() {
1196                children.next();
1197            }
1198            let trailing_icon_layout = children.next().unwrap();
1199
1200            if cursor_position.is_over(trailing_icon_layout.bounds()) {
1201                if self.is_editable_variant {
1202                    return mouse::Interaction::Pointer;
1203                }
1204
1205                if let Some((trailing_icon, tree)) =
1206                    self.trailing_icon.as_ref().zip(state.children.get(index))
1207                {
1208                    return trailing_icon.as_widget().mouse_interaction(
1209                        tree,
1210                        layout,
1211                        cursor_position,
1212                        viewport,
1213                        renderer,
1214                    );
1215                }
1216            }
1217        }
1218        let mut children = layout.children();
1219        let layout = children.next().unwrap();
1220        mouse_interaction(
1221            layout,
1222            cursor_position,
1223            self.on_input.is_none() && !self.manage_value,
1224        )
1225    }
1226
1227    #[inline]
1228    fn id(&self) -> Option<Id> {
1229        Some(self.id.clone())
1230    }
1231
1232    #[inline]
1233    fn set_id(&mut self, id: Id) {
1234        self.id = id;
1235    }
1236
1237    fn drag_destinations(
1238        &self,
1239        _state: &Tree,
1240        layout: Layout<'_>,
1241        _renderer: &crate::Renderer,
1242        dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles,
1243    ) {
1244        if let Some(input) = layout.children().last() {
1245            let Rectangle {
1246                x,
1247                y,
1248                width,
1249                height,
1250            } = input.bounds();
1251            dnd_rectangles.push(iced::clipboard::dnd::DndDestinationRectangle {
1252                id: self.dnd_id(),
1253                rectangle: iced::clipboard::dnd::Rectangle {
1254                    x: x as f64,
1255                    y: y as f64,
1256                    width: width as f64,
1257                    height: height as f64,
1258                },
1259                mime_types: SUPPORTED_TEXT_MIME_TYPES
1260                    .iter()
1261                    .map(|s| Cow::Borrowed(*s))
1262                    .collect(),
1263                actions: DndAction::Move,
1264                preferred: DndAction::Move,
1265            });
1266        }
1267    }
1268}
1269
1270impl<'a, Message> From<TextInput<'a, Message>>
1271    for Element<'a, Message, crate::Theme, crate::Renderer>
1272where
1273    Message: 'static + Clone,
1274{
1275    fn from(
1276        text_input: TextInput<'a, Message>,
1277    ) -> Element<'a, Message, crate::Theme, crate::Renderer> {
1278        Element::new(text_input)
1279    }
1280}
1281
1282/// Produces a [`Task`] that focuses the [`TextInput`] with the given [`Id`].
1283pub fn focus<Message: 'static>(id: Id) -> Task<Message> {
1284    task::effect(Action::widget(operation::focusable::focus(id)))
1285}
1286
1287/// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the
1288/// end.
1289pub fn move_cursor_to_end<Message: 'static>(id: Id) -> Task<Message> {
1290    task::effect(Action::widget(operation::text_input::move_cursor_to_end(
1291        id,
1292    )))
1293}
1294
1295/// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the
1296/// front.
1297pub fn move_cursor_to_front<Message: 'static>(id: Id) -> Task<Message> {
1298    task::effect(Action::widget(operation::text_input::move_cursor_to_front(
1299        id,
1300    )))
1301}
1302
1303/// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the
1304/// provided position.
1305pub fn move_cursor_to<Message: 'static>(id: Id, position: usize) -> Task<Message> {
1306    task::effect(Action::widget(operation::text_input::move_cursor_to(
1307        id, position,
1308    )))
1309}
1310
1311/// Produces a [`Task`] that selects all the content of the [`TextInput`] with the given [`Id`].
1312pub fn select_all<Message: 'static>(id: Id) -> Task<Message> {
1313    task::effect(Action::widget(operation::text_input::select_all(id)))
1314}
1315
1316/// Produces a [`Task`] that selects a range of the content of the [`TextInput`] with the given
1317/// [`Id`].
1318pub fn select_range<Message: 'static>(id: Id, start: usize, end: usize) -> Task<Message> {
1319    task::effect(Action::widget(operation::text_input::select_range(
1320        id, start, end,
1321    )))
1322}
1323
1324/// Produces a [`Task`] that selects from the front to the last occurrence of the given character
1325/// in the [`TextInput`] with the given [`Id`], or selects all if not found.
1326pub fn select_until_last<Message: 'static>(id: Id, value: &str, ch: char) -> Task<Message> {
1327    let v = Value::new(value);
1328    let end = v.rfind_char(ch).unwrap_or(v.len());
1329    select_range(id, 0, end)
1330}
1331
1332/// Computes the layout of a [`TextInput`].
1333#[allow(clippy::cast_precision_loss)]
1334#[allow(clippy::too_many_arguments)]
1335#[allow(clippy::too_many_lines)]
1336pub fn layout<Message>(
1337    renderer: &crate::Renderer,
1338    limits: &layout::Limits,
1339    width: Length,
1340    padding: Padding,
1341    size: Option<f32>,
1342    leading_icon: Option<&mut Element<'_, Message, crate::Theme, crate::Renderer>>,
1343    trailing_icon: Option<&mut Element<'_, Message, crate::Theme, crate::Renderer>>,
1344    line_height: text::LineHeight,
1345    label: Option<&str>,
1346    helper_text: Option<&str>,
1347    helper_text_size: f32,
1348    helper_text_line_height: text::LineHeight,
1349    font: iced_core::Font,
1350    tree: &mut Tree,
1351) -> layout::Node {
1352    let limits = limits.width(width);
1353    let spacing = THEME.lock().unwrap().cosmic().space_xxs();
1354    let mut nodes = Vec::with_capacity(3);
1355
1356    let text_pos = if let Some(label) = label {
1357        let text_bounds = limits.resolve(width, Length::Shrink, Size::INFINITE);
1358        let state = tree.state.downcast_mut::<State>();
1359        let label_paragraph = &mut state.label;
1360        label_paragraph.update(Text {
1361            content: label,
1362            font,
1363            bounds: text_bounds,
1364            size: iced::Pixels(size.unwrap_or_else(|| renderer.default_size().0)),
1365            align_x: text::Alignment::Left,
1366            align_y: alignment::Vertical::Center,
1367            line_height,
1368            shaping: text::Shaping::Advanced,
1369            wrapping: text::Wrapping::None,
1370            ellipsize: text::Ellipsize::None,
1371        });
1372        let label_size = label_paragraph.min_bounds();
1373
1374        nodes.push(layout::Node::new(label_size));
1375        Vector::new(0.0, label_size.height + f32::from(spacing))
1376    } else {
1377        Vector::ZERO
1378    };
1379
1380    let text_size = size.unwrap_or_else(|| renderer.default_size().0);
1381    let mut text_input_height = line_height.to_absolute(text_size.into()).0;
1382    let padding = padding.fit(Size::ZERO, limits.max());
1383
1384    let helper_pos = if leading_icon.is_some() || trailing_icon.is_some() {
1385        let children = &mut tree.children;
1386        // TODO configurable icon spacing, maybe via appearance
1387        let limits_copy = limits;
1388
1389        let limits = limits.shrink(padding);
1390        let icon_spacing = 8.0;
1391        let mut c_i = 0;
1392        let (leading_icon_width, mut leading_icon) =
1393            if let Some((icon, tree)) = leading_icon.zip(children.get_mut(c_i)) {
1394                let size = icon.as_widget().size();
1395                let icon_node = icon.as_widget_mut().layout(
1396                    tree,
1397                    renderer,
1398                    &Limits::NONE.width(size.width).height(size.height),
1399                );
1400                text_input_height = text_input_height.max(icon_node.bounds().height);
1401                c_i += 1;
1402                (icon_node.bounds().width + icon_spacing, Some(icon_node))
1403            } else {
1404                (0.0, None)
1405            };
1406
1407        let (trailing_icon_width, mut trailing_icon) =
1408            if let Some((icon, tree)) = trailing_icon.zip(children.get_mut(c_i)) {
1409                let size = icon.as_widget().size();
1410                let icon_node = icon.as_widget_mut().layout(
1411                    tree,
1412                    renderer,
1413                    &Limits::NONE.width(size.width).height(size.height),
1414                );
1415                text_input_height = text_input_height.max(icon_node.bounds().height);
1416                (icon_node.bounds().width + icon_spacing, Some(icon_node))
1417            } else {
1418                (0.0, None)
1419            };
1420        let text_limits = limits
1421            .width(width)
1422            .height(line_height.to_absolute(text_size.into()));
1423        let text_bounds = text_limits.resolve(Length::Shrink, Length::Shrink, Size::INFINITE);
1424        let text_node = layout::Node::new(
1425            text_bounds - Size::new(leading_icon_width + trailing_icon_width, 0.0),
1426        )
1427        .move_to(Point::new(
1428            padding.left + leading_icon_width,
1429            padding.top
1430                + ((text_input_height - line_height.to_absolute(text_size.into()).0) / 2.0)
1431                    .max(0.0),
1432        ));
1433        let mut node_list: Vec<_> = Vec::with_capacity(3);
1434
1435        let text_node_bounds = text_node.bounds();
1436        node_list.push(text_node);
1437
1438        if let Some(leading_icon) = leading_icon.take() {
1439            node_list.push(leading_icon.clone().move_to(Point::new(
1440                padding.left,
1441                padding.top + ((text_input_height - leading_icon.bounds().height) / 2.0).max(0.0),
1442            )));
1443        }
1444        if let Some(trailing_icon) = trailing_icon.take() {
1445            let trailing_icon = trailing_icon.clone().move_to(Point::new(
1446                text_node_bounds.x + text_node_bounds.width + f32::from(spacing),
1447                padding.top + ((text_input_height - trailing_icon.bounds().height) / 2.0).max(0.0),
1448            ));
1449            node_list.push(trailing_icon);
1450        }
1451
1452        let text_input_size = Size::new(
1453            text_node_bounds.x + text_node_bounds.width + trailing_icon_width,
1454            text_input_height,
1455        )
1456        .expand(padding);
1457
1458        let input_limits = limits_copy
1459            .width(width)
1460            .height(text_input_height.max(text_input_size.height))
1461            .min_width(text_input_size.width);
1462        let input_bounds = input_limits.resolve(
1463            width,
1464            text_input_height.max(text_input_size.height),
1465            text_input_size,
1466        );
1467        let input_node = layout::Node::with_children(input_bounds, node_list).translate(text_pos);
1468        let y_pos = input_node.bounds().y + input_node.bounds().height + f32::from(spacing);
1469        nodes.push(input_node);
1470
1471        Vector::new(0.0, y_pos)
1472    } else {
1473        let limits = limits
1474            .width(width)
1475            .height(text_input_height + padding.y())
1476            .shrink(padding);
1477        let text_bounds = limits.resolve(Length::Shrink, Length::Shrink, Size::INFINITE);
1478
1479        let text = layout::Node::new(text_bounds).move_to(Point::new(padding.left, padding.top));
1480
1481        let node = layout::Node::with_children(text_bounds.expand(padding), vec![text])
1482            .translate(text_pos);
1483        let y_pos = node.bounds().y + node.bounds().height + f32::from(spacing);
1484
1485        nodes.push(node);
1486
1487        Vector::new(0.0, y_pos)
1488    };
1489
1490    if let Some(helper_text) = helper_text {
1491        let limits = limits
1492            .width(width)
1493            .shrink(padding)
1494            .height(helper_text_line_height.to_absolute(helper_text_size.into()));
1495        let text_bounds = limits.resolve(width, Length::Shrink, Size::INFINITE);
1496        let state = tree.state.downcast_mut::<State>();
1497        let helper_text_paragraph = &mut state.helper_text;
1498        helper_text_paragraph.update(Text {
1499            content: helper_text,
1500            font,
1501            bounds: text_bounds,
1502            size: iced::Pixels(helper_text_size),
1503            align_x: text::Alignment::Left,
1504            align_y: alignment::Vertical::Center,
1505            line_height: helper_text_line_height,
1506            shaping: text::Shaping::Advanced,
1507            wrapping: text::Wrapping::None,
1508            ellipsize: text::Ellipsize::None,
1509        });
1510        let helper_text_size = helper_text_paragraph.min_bounds();
1511        let helper_text_node = layout::Node::new(helper_text_size).translate(helper_pos);
1512        nodes.push(helper_text_node);
1513    };
1514
1515    let mut size = nodes.iter().fold(Size::ZERO, |size, node| {
1516        Size::new(
1517            size.width.max(node.bounds().width),
1518            size.height + node.bounds().height,
1519        )
1520    });
1521    size.height += (nodes.len() - 1) as f32 * f32::from(spacing);
1522
1523    let limits = limits
1524        .width(width)
1525        .height(size.height)
1526        .min_width(size.width);
1527
1528    layout::Node::with_children(limits.resolve(width, size.height, size), nodes)
1529}
1530
1531// TODO: Merge into widget method since iced has done the same.
1532/// Processes an [`Event`] and updates the [`State`] of a [`TextInput`]
1533/// accordingly.
1534#[allow(clippy::too_many_arguments)]
1535#[allow(clippy::too_many_lines)]
1536#[allow(clippy::missing_panics_doc)]
1537#[allow(clippy::cast_lossless)]
1538#[allow(clippy::cast_possible_truncation)]
1539pub fn update<'a, Message: Clone + 'static>(
1540    id: Option<Id>,
1541    event: &Event,
1542    text_layout: Layout<'_>,
1543    edit_button_layout: Option<Layout<'_>>,
1544    cursor: mouse::Cursor,
1545    clipboard: &mut dyn Clipboard,
1546    shell: &mut Shell<'_, Message>,
1547    value: &mut Value,
1548    size: f32,
1549    font: <crate::Renderer as iced_core::text::Renderer>::Font,
1550    is_editable_variant: bool,
1551    is_secure: bool,
1552    on_focus: Option<&Message>,
1553    on_unfocus: Option<&Message>,
1554    on_input: Option<&dyn Fn(String) -> Message>,
1555    on_paste: Option<&dyn Fn(String) -> Message>,
1556    on_submit: Option<&dyn Fn(String) -> Message>,
1557    on_tab: Option<&Message>,
1558    on_toggle_edit: Option<&dyn Fn(bool) -> Message>,
1559    state: impl FnOnce() -> &'a mut State,
1560    #[allow(unused_variables)] on_start_dnd_source: Option<&dyn Fn(State) -> Message>,
1561    #[allow(unused_variables)] dnd_id: u128,
1562    line_height: text::LineHeight,
1563    layout: Layout<'_>,
1564    manage_value: bool,
1565    drag_threshold: f32,
1566    always_active: bool,
1567) {
1568    let update_cache = |state, value| {
1569        replace_paragraph(
1570            state,
1571            layout,
1572            value,
1573            font,
1574            iced::Pixels(size),
1575            line_height,
1576            &Limits::NONE.max_width(text_layout.bounds().width),
1577        );
1578    };
1579
1580    let mut secured_value = if is_secure {
1581        value.secure()
1582    } else {
1583        value.clone()
1584    };
1585    let unsecured_value = value;
1586    let value = &mut secured_value;
1587
1588    // NOTE: Clicks must be captured to prevent mouse areas behind them handling the same clicks.
1589
1590    /// Mark a branch as cold
1591    #[inline]
1592    #[cold]
1593    fn cold() {}
1594
1595    let state = state();
1596
1597    // Any click outside clears focus and selection.
1598    if matches!(
1599        event,
1600        Event::Mouse(mouse::Event::ButtonPressed(_))
1601            | Event::Touch(touch::Event::FingerPressed { .. })
1602    ) && cursor.position_over(layout.bounds()).is_none()
1603    {
1604        state.is_focused = None;
1605        state.context_menu_position = None;
1606        state.dragging_state = None;
1607        if let Some(on_unfocus) = on_unfocus {
1608            shell.publish(on_unfocus.clone());
1609        }
1610        return;
1611    }
1612
1613    match event {
1614        Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) => {
1615            if let Some(pos) = cursor.position_over(layout.bounds()) {
1616                if !state.is_focused() {
1617                    state.focus();
1618                }
1619                state.context_menu_position = Some(pos);
1620                shell.capture_event();
1621                return;
1622            }
1623        }
1624
1625        Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
1626        | Event::Touch(touch::Event::FingerPressed { .. }) => {
1627            cold();
1628
1629            if state.context_menu_position.take().is_some() {
1630                shell.capture_event();
1631                return;
1632            }
1633
1634            let click_position = if on_input.is_some() || manage_value {
1635                cursor.position_over(layout.bounds())
1636            } else {
1637                None
1638            };
1639
1640            if let Some(cursor_position) = click_position {
1641                // Check if the edit button was clicked.
1642                if state.dragging_state.is_none()
1643                    && edit_button_layout.is_some_and(|l| cursor.is_over(l.bounds()))
1644                {
1645                    if is_editable_variant {
1646                        let has_content = !unsecured_value.is_empty();
1647                        let is_editing = !state.is_read_only;
1648
1649                        if is_editing && has_content {
1650                            if let Some(on_input) = on_input {
1651                                shell.publish((on_input)(String::new()));
1652                            }
1653
1654                            if manage_value {
1655                                *unsecured_value = Value::new("");
1656                                state.tracked_value = unsecured_value.clone();
1657
1658                                let cleared_value = if is_secure {
1659                                    unsecured_value.secure()
1660                                } else {
1661                                    unsecured_value.clone()
1662                                };
1663
1664                                update_cache(state, &cleared_value);
1665                            }
1666
1667                            state.move_cursor_to_end();
1668                        } else if is_editing {
1669                            // Close: toggle back to read-only and unfocus.
1670                            state.is_read_only = true;
1671                            state.unfocus();
1672
1673                            if let Some(on_toggle_edit) = on_toggle_edit {
1674                                shell.publish(on_toggle_edit(false));
1675                            }
1676                        } else {
1677                            // Edit: toggle to editing, select all, and focus.
1678                            state.is_read_only = false;
1679                            state.cursor.select_range(0, value.len());
1680
1681                            if let Some(on_toggle_edit) = on_toggle_edit {
1682                                shell.publish(on_toggle_edit(true));
1683                            }
1684
1685                            let now = Instant::now();
1686                            LAST_FOCUS_UPDATE.with(|x| x.set(now));
1687                            state.is_focused = Some(Focus {
1688                                updated_at: now,
1689                                now,
1690                                focused: true,
1691                                needs_update: false,
1692                            });
1693                        }
1694                    }
1695
1696                    shell.capture_event();
1697                    return;
1698                }
1699
1700                let target = {
1701                    let text_bounds = text_layout.bounds();
1702
1703                    let alignment_offset = alignment_offset(
1704                        text_bounds.width,
1705                        state.value.raw().min_width(),
1706                        effective_alignment(state.value.raw()),
1707                    );
1708
1709                    cursor_position.x - text_bounds.x - alignment_offset
1710                };
1711
1712                let click =
1713                    mouse::Click::new(cursor_position, mouse::Button::Left, state.last_click);
1714
1715                match (
1716                    &state.dragging_state,
1717                    click.kind(),
1718                    state.cursor().state(value),
1719                ) {
1720                    #[cfg(wayland_platform)]
1721                    (None, click::Kind::Single, cursor::State::Selection { start, end }) => {
1722                        let left = start.min(end);
1723                        let right = end.max(start);
1724
1725                        let (left_position, _left_offset) = measure_cursor_and_scroll_offset(
1726                            state.value.raw(),
1727                            text_layout.bounds(),
1728                            left,
1729                            value,
1730                            state.cursor.affinity(),
1731                            state.scroll_offset,
1732                        );
1733
1734                        let (right_position, _right_offset) = measure_cursor_and_scroll_offset(
1735                            state.value.raw(),
1736                            text_layout.bounds(),
1737                            right,
1738                            value,
1739                            state.cursor.affinity(),
1740                            state.scroll_offset,
1741                        );
1742
1743                        let selection_start = left_position.min(right_position);
1744                        let width = (right_position - left_position).abs();
1745                        let alignment_offset = alignment_offset(
1746                            text_layout.bounds().width,
1747                            state.value.raw().min_width(),
1748                            effective_alignment(state.value.raw()),
1749                        );
1750                        let selection_bounds = Rectangle {
1751                            x: text_layout.bounds().x + alignment_offset + selection_start
1752                                - state.scroll_offset,
1753                            y: text_layout.bounds().y,
1754                            width,
1755                            height: text_layout.bounds().height,
1756                        };
1757
1758                        if cursor.is_over(selection_bounds) && (on_input.is_some() || manage_value)
1759                        {
1760                            state.dragging_state = Some(DraggingState::PrepareDnd(cursor_position));
1761                            shell.capture_event();
1762                            return;
1763                        }
1764                        // clear selection and place cursor at click position
1765                        update_cache(state, value);
1766                        state.setting_selection(value, text_layout.bounds(), target);
1767                        state.dragging_state = None;
1768                        shell.capture_event();
1769                        return;
1770                    }
1771                    (None, click::Kind::Single, _) => {
1772                        state.setting_selection(value, text_layout.bounds(), target);
1773                    }
1774                    (None | Some(DraggingState::Selection), click::Kind::Double, _) => {
1775                        update_cache(state, value);
1776
1777                        if is_secure {
1778                            state.cursor.select_all(value);
1779                        } else {
1780                            let (position, affinity) =
1781                                find_cursor_position(text_layout.bounds(), value, state, target)
1782                                    .unwrap_or((0, text::Affinity::Before));
1783
1784                            state.cursor.set_affinity(affinity);
1785
1786                            if let Some(delimiter) = state.double_click_select_delimiter {
1787                                if let Some(delim_pos) = value.rfind_char(delimiter) {
1788                                    if position <= delim_pos {
1789                                        state.cursor.select_range(0, delim_pos);
1790                                    } else {
1791                                        state.cursor.select_range(delim_pos + 1, value.len());
1792                                    }
1793                                } else {
1794                                    state.cursor.select_all(value);
1795                                }
1796                            } else {
1797                                state.cursor.select_range(
1798                                    value.previous_start_of_word(position),
1799                                    value.next_end_of_word(position),
1800                                );
1801                            }
1802                        }
1803                        state.dragging_state = Some(DraggingState::Selection);
1804                    }
1805                    (None | Some(DraggingState::Selection), click::Kind::Triple, _) => {
1806                        update_cache(state, value);
1807                        state.cursor.select_all(value);
1808                        state.dragging_state = Some(DraggingState::Selection);
1809                    }
1810                    _ => {
1811                        state.dragging_state = None;
1812                    }
1813                }
1814
1815                // Focus on click of the text input, and ensure that the input is writable.
1816                if matches!(state.dragging_state, None | Some(DraggingState::Selection))
1817                    && (!state.is_focused() || (is_editable_variant && state.is_read_only))
1818                {
1819                    if !state.is_focused() {
1820                        if let Some(on_focus) = on_focus {
1821                            shell.publish(on_focus.clone());
1822                        }
1823                    }
1824
1825                    if state.is_read_only {
1826                        state.is_read_only = false;
1827                        state.cursor.select_range(0, value.len());
1828                        if let Some(on_toggle_edit) = on_toggle_edit {
1829                            let message = (on_toggle_edit)(true);
1830                            shell.publish(message);
1831                        }
1832                    }
1833
1834                    let now = Instant::now();
1835                    LAST_FOCUS_UPDATE.with(|x| x.set(now));
1836
1837                    state.is_focused = Some(Focus {
1838                        updated_at: now,
1839                        now,
1840                        focused: true,
1841                        needs_update: false,
1842                    });
1843                }
1844
1845                state.last_click = Some(click);
1846
1847                shell.request_redraw();
1848                shell.capture_event();
1849                return;
1850            } else {
1851                state.unfocus();
1852
1853                if let Some(on_unfocus) = on_unfocus {
1854                    shell.publish(on_unfocus.clone());
1855                }
1856            }
1857        }
1858        Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
1859        | Event::Touch(touch::Event::FingerLifted { .. } | touch::Event::FingerLost { .. }) => {
1860            cold();
1861            #[cfg(wayland_platform)]
1862            if matches!(state.dragging_state, Some(DraggingState::PrepareDnd(_))) {
1863                // clear selection and place cursor at click position
1864                update_cache(state, value);
1865                if let Some(position) = cursor.position_over(layout.bounds()) {
1866                    let target = {
1867                        let text_bounds = text_layout.bounds();
1868
1869                        let alignment_offset = alignment_offset(
1870                            text_bounds.width,
1871                            state.value.raw().min_width(),
1872                            effective_alignment(state.value.raw()),
1873                        );
1874
1875                        position.x - text_bounds.x - alignment_offset
1876                    };
1877                    state.setting_selection(value, text_layout.bounds(), target);
1878                }
1879            }
1880            state.dragging_state = None;
1881            if cursor.is_over(layout.bounds()) {
1882                shell.capture_event();
1883            }
1884            return;
1885        }
1886        Event::Mouse(mouse::Event::CursorMoved { position })
1887        | Event::Touch(touch::Event::FingerMoved { position, .. }) => {
1888            if matches!(state.dragging_state, Some(DraggingState::Selection)) {
1889                let target = {
1890                    let text_bounds = text_layout.bounds();
1891
1892                    let alignment_offset = alignment_offset(
1893                        text_bounds.width,
1894                        state.value.raw().min_width(),
1895                        effective_alignment(state.value.raw()),
1896                    );
1897
1898                    position.x - text_bounds.x - alignment_offset
1899                };
1900
1901                update_cache(state, value);
1902                let (position, affinity) =
1903                    find_cursor_position(text_layout.bounds(), value, state, target)
1904                        .unwrap_or((0, text::Affinity::Before));
1905
1906                state.cursor.set_affinity(affinity);
1907                state
1908                    .cursor
1909                    .select_range(state.cursor.start(value), position);
1910
1911                shell.request_redraw();
1912                shell.capture_event();
1913                return;
1914            }
1915            #[cfg(wayland_platform)]
1916            if let Some(DraggingState::PrepareDnd(start_position)) = state.dragging_state {
1917                let distance = ((position.x - start_position.x).powi(2)
1918                    + (position.y - start_position.y).powi(2))
1919                .sqrt();
1920
1921                if distance >= drag_threshold {
1922                    if is_secure {
1923                        return;
1924                    }
1925
1926                    let input_text = state.selected_text(&value.to_string()).unwrap_or_default();
1927                    state.dragging_state =
1928                        Some(DraggingState::Dnd(DndAction::empty(), input_text.clone()));
1929                    let mut editor = Editor::new(unsecured_value, &mut state.cursor);
1930                    editor.delete();
1931
1932                    let contents = editor.contents();
1933                    let unsecured_value = Value::new(&contents);
1934                    state.tracked_value = unsecured_value.clone();
1935                    if let Some(on_input) = on_input {
1936                        let message = (on_input)(contents);
1937                        shell.publish(message);
1938                    }
1939                    if let Some(on_start_dnd) = on_start_dnd_source {
1940                        shell.publish(on_start_dnd(state.clone()));
1941                    }
1942                    let state_clone = state.clone();
1943
1944                    iced_core::clipboard::start_dnd(
1945                        clipboard,
1946                        false,
1947                        id.map(iced_core::clipboard::DndSource::Widget),
1948                        Some(iced_core::clipboard::IconSurface::new(
1949                            Element::from(
1950                                TextInput::<'static, ()>::new("", input_text.clone())
1951                                    .dnd_icon(true),
1952                            ),
1953                            iced_core::widget::tree::State::new(state_clone),
1954                            Vector::ZERO,
1955                        )),
1956                        Box::new(TextInputString(input_text)),
1957                        DndAction::Move,
1958                    );
1959
1960                    update_cache(state, &unsecured_value);
1961                } else {
1962                    state.dragging_state = Some(DraggingState::PrepareDnd(start_position));
1963                }
1964
1965                shell.capture_event();
1966                return;
1967            }
1968        }
1969        Event::Keyboard(keyboard::Event::KeyPressed {
1970            key,
1971            text,
1972            physical_key,
1973            modifiers,
1974            ..
1975        }) => {
1976            state.keyboard_modifiers = *modifiers;
1977
1978            if let Some(focus) = state.is_focused.as_mut().filter(|f| f.focused) {
1979                if state.is_read_only || (!manage_value && on_input.is_none()) {
1980                    return;
1981                };
1982                let modifiers = state.keyboard_modifiers;
1983                focus.updated_at = Instant::now();
1984                LAST_FOCUS_UPDATE.with(|x| x.set(focus.updated_at));
1985
1986                // Ctrl/Command+A/C/V/X, plus the traditional alternate clipboard
1987                let clip_key = match key.as_ref() {
1988                    keyboard::Key::Named(keyboard::key::Named::Insert) if modifiers.shift() => {
1989                        Some('v')
1990                    }
1991                    keyboard::Key::Named(keyboard::key::Named::Insert) if modifiers.command() => {
1992                        Some('c')
1993                    }
1994                    keyboard::Key::Named(keyboard::key::Named::Delete) if modifiers.shift() => {
1995                        Some('x')
1996                    }
1997                    _ if modifiers.command() => key.to_latin(*physical_key),
1998                    _ => None,
1999                };
2000                {
2001                    match clip_key {
2002                        Some('c') => {
2003                            if !is_secure {
2004                                if let Some((start, end)) = state.cursor.selection(value) {
2005                                    clipboard.write(
2006                                        iced_core::clipboard::Kind::Standard,
2007                                        value.select(start, end).to_string(),
2008                                    );
2009                                }
2010                            }
2011                        }
2012                        // XXX if we want to allow cutting of secure text, we need to
2013                        // update the cache and decide which value to cut
2014                        Some('x') => {
2015                            if !is_secure {
2016                                if let Some((start, end)) = state.cursor.selection(value) {
2017                                    clipboard.write(
2018                                        iced_core::clipboard::Kind::Standard,
2019                                        value.select(start, end).to_string(),
2020                                    );
2021                                }
2022
2023                                let mut editor = Editor::new(value, &mut state.cursor);
2024                                editor.delete();
2025                                let content = editor.contents();
2026                                state.tracked_value = Value::new(&content);
2027                                if let Some(on_input) = on_input {
2028                                    let message = (on_input)(content);
2029                                    shell.publish(message);
2030                                }
2031                            }
2032                        }
2033                        Some('v') => {
2034                            let content = if let Some(content) = state.is_pasting.take() {
2035                                content
2036                            } else {
2037                                let content: String = clipboard
2038                                    .read(iced_core::clipboard::Kind::Standard)
2039                                    .unwrap_or_default()
2040                                    .chars()
2041                                    .filter(|c| !c.is_control())
2042                                    .collect();
2043
2044                                Value::new(&content)
2045                            };
2046
2047                            let mut editor = Editor::new(unsecured_value, &mut state.cursor);
2048
2049                            editor.paste(content.clone());
2050
2051                            let contents = editor.contents();
2052                            let unsecured_value = Value::new(&contents);
2053                            state.tracked_value = unsecured_value.clone();
2054
2055                            if let Some(on_input) = on_input {
2056                                let message = if let Some(paste) = &on_paste {
2057                                    (paste)(contents)
2058                                } else {
2059                                    (on_input)(contents)
2060                                };
2061
2062                                shell.publish(message);
2063                            }
2064
2065                            state.is_pasting = Some(content);
2066
2067                            let value = if is_secure {
2068                                unsecured_value.secure()
2069                            } else {
2070                                unsecured_value
2071                            };
2072
2073                            update_cache(state, &value);
2074                            shell.capture_event();
2075                            return;
2076                        }
2077
2078                        Some('a') => {
2079                            state.cursor.select_all(value);
2080                            shell.capture_event();
2081                            return;
2082                        }
2083
2084                        _ => {}
2085                    }
2086                }
2087
2088                // Capture keyboard inputs that should be submitted.
2089                if let Some(c) = text
2090                    .as_ref()
2091                    .and_then(|t| t.chars().next().filter(|c| !c.is_control()))
2092                {
2093                    if state.is_read_only || (!manage_value && on_input.is_none()) {
2094                        return;
2095                    };
2096
2097                    state.is_pasting = None;
2098
2099                    if !state.keyboard_modifiers.command() && !modifiers.control() {
2100                        let mut editor = Editor::new(unsecured_value, &mut state.cursor);
2101
2102                        editor.insert(c);
2103
2104                        let contents = editor.contents();
2105                        let unsecured_value = Value::new(&contents);
2106                        state.tracked_value = unsecured_value.clone();
2107
2108                        if let Some(on_input) = on_input {
2109                            let message = (on_input)(contents);
2110                            shell.publish(message);
2111                        }
2112
2113                        focus.updated_at = Instant::now();
2114                        LAST_FOCUS_UPDATE.with(|x| x.set(focus.updated_at));
2115
2116                        let value = if is_secure {
2117                            unsecured_value.secure()
2118                        } else {
2119                            unsecured_value
2120                        };
2121
2122                        update_cache(state, &value);
2123
2124                        shell.capture_event();
2125                        return;
2126                    }
2127                }
2128
2129                match key.as_ref() {
2130                    keyboard::Key::Named(keyboard::key::Named::Enter) => {
2131                        if let Some(on_submit) = on_submit {
2132                            shell.publish((on_submit)(unsecured_value.to_string()));
2133                        }
2134                    }
2135                    keyboard::Key::Named(keyboard::key::Named::Backspace) => {
2136                        if platform::is_jump_modifier_pressed(modifiers)
2137                            && state.cursor.selection(value).is_none()
2138                        {
2139                            if is_secure {
2140                                let cursor_pos = state.cursor.end(value);
2141                                state.cursor.select_range(0, cursor_pos);
2142                            } else {
2143                                state.cursor.select_left_by_words(value);
2144                            }
2145                        }
2146
2147                        let mut editor = Editor::new(unsecured_value, &mut state.cursor);
2148                        editor.backspace();
2149
2150                        let contents = editor.contents();
2151                        let unsecured_value = Value::new(&contents);
2152                        state.tracked_value = unsecured_value.clone();
2153                        if let Some(on_input) = on_input {
2154                            let message = (on_input)(editor.contents());
2155                            shell.publish(message);
2156                        }
2157                        let value = if is_secure {
2158                            unsecured_value.secure()
2159                        } else {
2160                            unsecured_value
2161                        };
2162                        update_cache(state, &value);
2163                    }
2164                    keyboard::Key::Named(keyboard::key::Named::Delete) => {
2165                        if platform::is_jump_modifier_pressed(modifiers)
2166                            && state.cursor.selection(value).is_none()
2167                        {
2168                            if is_secure {
2169                                let cursor_pos = state.cursor.end(unsecured_value);
2170                                state.cursor.select_range(cursor_pos, unsecured_value.len());
2171                            } else {
2172                                state.cursor.select_right_by_words(unsecured_value);
2173                            }
2174                        }
2175
2176                        let mut editor = Editor::new(unsecured_value, &mut state.cursor);
2177                        editor.delete();
2178                        let contents = editor.contents();
2179                        let unsecured_value = Value::new(&contents);
2180                        if let Some(on_input) = on_input {
2181                            let message = (on_input)(contents);
2182                            state.tracked_value = unsecured_value.clone();
2183                            shell.publish(message);
2184                        }
2185
2186                        let value = if is_secure {
2187                            unsecured_value.secure()
2188                        } else {
2189                            unsecured_value
2190                        };
2191
2192                        update_cache(state, &value);
2193                    }
2194                    keyboard::Key::Named(keyboard::key::Named::ArrowLeft) => {
2195                        let rtl = state.value.raw().is_rtl(0).unwrap_or(false);
2196                        let by_words = platform::is_jump_modifier_pressed(modifiers) && !is_secure;
2197
2198                        if modifiers.shift() {
2199                            state.cursor.select_visual(false, by_words, rtl, value);
2200                        } else {
2201                            state.cursor.move_visual(false, by_words, rtl, value);
2202                        }
2203                    }
2204                    keyboard::Key::Named(keyboard::key::Named::ArrowRight) => {
2205                        let rtl = state.value.raw().is_rtl(0).unwrap_or(false);
2206                        let by_words = platform::is_jump_modifier_pressed(modifiers) && !is_secure;
2207
2208                        if modifiers.shift() {
2209                            state.cursor.select_visual(true, by_words, rtl, value);
2210                        } else {
2211                            state.cursor.move_visual(true, by_words, rtl, value);
2212                        }
2213                    }
2214                    keyboard::Key::Named(keyboard::key::Named::Home) => {
2215                        if modifiers.shift() {
2216                            state.cursor.select_range(state.cursor.start(value), 0);
2217                        } else {
2218                            state.cursor.move_to(0);
2219                        }
2220                    }
2221                    keyboard::Key::Named(keyboard::key::Named::End) => {
2222                        if modifiers.shift() {
2223                            state
2224                                .cursor
2225                                .select_range(state.cursor.start(value), value.len());
2226                        } else {
2227                            state.cursor.move_to(value.len());
2228                        }
2229                    }
2230                    keyboard::Key::Named(keyboard::key::Named::Escape) => {
2231                        state.unfocus();
2232                        state.is_read_only = true;
2233
2234                        if let Some(on_unfocus) = on_unfocus {
2235                            shell.publish(on_unfocus.clone());
2236                        }
2237                    }
2238
2239                    keyboard::Key::Named(keyboard::key::Named::Tab) => {
2240                        if let Some(on_tab) = on_tab {
2241                            // Allow the application to decide how the event is handled.
2242                            // This could be to connect the text input to another text input.
2243                            // Or to connect the text input to a button.
2244                            shell.publish(on_tab.clone());
2245                        } else {
2246                            state.is_read_only = true;
2247
2248                            if let Some(on_unfocus) = on_unfocus {
2249                                shell.publish(on_unfocus.clone());
2250                            }
2251
2252                            return;
2253                        };
2254                    }
2255
2256                    keyboard::Key::Named(
2257                        keyboard::key::Named::ArrowUp | keyboard::key::Named::ArrowDown,
2258                    ) => {
2259                        return;
2260                    }
2261                    _ => {}
2262                }
2263
2264                shell.request_redraw();
2265                shell.capture_event();
2266                return;
2267            }
2268        }
2269        Event::Keyboard(keyboard::Event::KeyReleased { key, .. }) => {
2270            if state.is_focused() {
2271                match key {
2272                    keyboard::Key::Character(c) if "v" == c => {
2273                        state.is_pasting = None;
2274                    }
2275                    keyboard::Key::Named(keyboard::key::Named::Insert) => {
2276                        state.is_pasting = None;
2277                    }
2278                    keyboard::Key::Named(keyboard::key::Named::Tab)
2279                    | keyboard::Key::Named(keyboard::key::Named::ArrowUp)
2280                    | keyboard::Key::Named(keyboard::key::Named::ArrowDown) => {
2281                        return;
2282                    }
2283                    _ => {}
2284                }
2285
2286                shell.capture_event();
2287                return;
2288            }
2289        }
2290        Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
2291            state.keyboard_modifiers = *modifiers;
2292        }
2293        Event::InputMethod(event) => match event {
2294            input_method::Event::Opened | input_method::Event::Closed => {
2295                state.preedit =
2296                    matches!(event, input_method::Event::Opened).then(input_method::Preedit::new);
2297                shell.capture_event();
2298                return;
2299            }
2300            input_method::Event::Preedit(content, selection) => {
2301                if state.is_focused() {
2302                    state.preedit = Some(input_method::Preedit {
2303                        content: content.to_owned(),
2304                        selection: selection.clone(),
2305                        text_size: Some(size.into()),
2306                    });
2307                    shell.capture_event();
2308                    return;
2309                }
2310            }
2311            input_method::Event::Commit(text) => {
2312                let Some(focus) = state.is_focused.as_mut().filter(|f| f.focused) else {
2313                    return;
2314                };
2315                let Some(on_input) = on_input else {
2316                    return;
2317                };
2318                if state.is_read_only {
2319                    return;
2320                }
2321
2322                focus.updated_at = Instant::now();
2323                LAST_FOCUS_UPDATE.with(|x| x.set(focus.updated_at));
2324
2325                let mut editor = Editor::new(unsecured_value, &mut state.cursor);
2326                editor.paste(Value::new(&text));
2327
2328                let contents = editor.contents();
2329                let unsecured_value = Value::new(&contents);
2330                let message = if let Some(paste) = &on_paste {
2331                    (paste)(contents)
2332                } else {
2333                    (on_input)(contents)
2334                };
2335                shell.publish(message);
2336
2337                state.is_pasting = None;
2338                let value = if is_secure {
2339                    unsecured_value.secure()
2340                } else {
2341                    unsecured_value
2342                };
2343
2344                update_cache(state, &value);
2345                shell.capture_event();
2346                return;
2347            }
2348        },
2349        Event::Window(window::Event::RedrawRequested(now)) => {
2350            if let Some(focus) = state.is_focused.as_mut().filter(|f| f.focused) {
2351                focus.now = *now;
2352
2353                let millis_until_redraw = CURSOR_BLINK_INTERVAL_MILLIS
2354                    - (*now - focus.updated_at).as_millis() % CURSOR_BLINK_INTERVAL_MILLIS;
2355                shell.request_redraw_at(window::RedrawRequest::At(
2356                    now.checked_add(Duration::from_millis(millis_until_redraw as u64))
2357                        .unwrap_or(*now),
2358                ));
2359
2360                shell.request_input_method(&input_method(state, text_layout, unsecured_value));
2361            } else if always_active {
2362                shell.request_redraw();
2363            }
2364        }
2365        #[cfg(wayland_platform)]
2366        Event::Dnd(DndEvent::Source(SourceEvent::Finished | SourceEvent::Cancelled)) => {
2367            cold();
2368            if matches!(state.dragging_state, Some(DraggingState::Dnd(..))) {
2369                // TODO: restore value in text input
2370                state.dragging_state = None;
2371                shell.capture_event();
2372                return;
2373            }
2374        }
2375        #[cfg(wayland_platform)]
2376        Event::Dnd(DndEvent::Offer(
2377            rectangle,
2378            OfferEvent::Enter {
2379                x,
2380                y,
2381                mime_types,
2382                surface,
2383            },
2384        )) if *rectangle == Some(dnd_id) => {
2385            cold();
2386            let is_clicked = text_layout.bounds().contains(Point {
2387                x: *x as f32,
2388                y: *y as f32,
2389            });
2390
2391            let mut accepted = false;
2392            for m in mime_types {
2393                if SUPPORTED_TEXT_MIME_TYPES.contains(&m.as_str()) {
2394                    let clone = m.clone();
2395                    accepted = true;
2396                }
2397            }
2398            if accepted {
2399                let target = {
2400                    let text_bounds = text_layout.bounds();
2401
2402                    let alignment_offset = alignment_offset(
2403                        text_bounds.width,
2404                        state.value.raw().min_width(),
2405                        effective_alignment(state.value.raw()),
2406                    );
2407
2408                    *x as f32 - text_bounds.x - alignment_offset
2409                };
2410                state.dnd_offer =
2411                    DndOfferState::HandlingOffer(mime_types.clone(), DndAction::empty());
2412                // existing logic for setting the selection
2413                update_cache(state, value);
2414                let (position, affinity) =
2415                    find_cursor_position(text_layout.bounds(), value, state, target)
2416                        .unwrap_or((0, text::Affinity::Before));
2417
2418                state.cursor.set_affinity(affinity);
2419                state.cursor.move_to(position);
2420                shell.capture_event();
2421                return;
2422            }
2423        }
2424        #[cfg(wayland_platform)]
2425        Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Motion { x, y }))
2426            if *rectangle == Some(dnd_id) =>
2427        {
2428            let target = {
2429                let text_bounds = text_layout.bounds();
2430
2431                let alignment_offset = alignment_offset(
2432                    text_bounds.width,
2433                    state.value.raw().min_width(),
2434                    effective_alignment(state.value.raw()),
2435                );
2436
2437                *x as f32 - text_bounds.x - alignment_offset
2438            };
2439            // existing logic for setting the selection
2440            update_cache(state, value);
2441            let (position, affinity) =
2442                find_cursor_position(text_layout.bounds(), value, state, target)
2443                    .unwrap_or((0, text::Affinity::Before));
2444
2445            state.cursor.set_affinity(affinity);
2446            state.cursor.move_to(position);
2447            shell.capture_event();
2448            return;
2449        }
2450        #[cfg(wayland_platform)]
2451        Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Drop)) if *rectangle == Some(dnd_id) => {
2452            cold();
2453            if let DndOfferState::HandlingOffer(mime_types, _action) = state.dnd_offer.clone() {
2454                let Some(mime_type) = SUPPORTED_TEXT_MIME_TYPES
2455                    .iter()
2456                    .find(|&&m| mime_types.iter().any(|t| t == m))
2457                else {
2458                    state.dnd_offer = DndOfferState::None;
2459                    shell.capture_event();
2460                    return;
2461                };
2462                state.dnd_offer = DndOfferState::Dropped;
2463            }
2464
2465            return;
2466        }
2467        #[cfg(wayland_platform)]
2468        Event::Dnd(DndEvent::Offer(id, OfferEvent::LeaveDestination)) if Some(dnd_id) != *id => {}
2469        #[cfg(wayland_platform)]
2470        Event::Dnd(DndEvent::Offer(
2471            rectangle,
2472            OfferEvent::Leave | OfferEvent::LeaveDestination,
2473        )) => {
2474            cold();
2475            // ASHLEY TODO we should be able to reset but for now we don't if we are handling a
2476            // drop
2477            match state.dnd_offer {
2478                DndOfferState::Dropped => {}
2479                _ => {
2480                    state.dnd_offer = DndOfferState::None;
2481                }
2482            };
2483            shell.capture_event();
2484            return;
2485        }
2486        #[cfg(wayland_platform)]
2487        Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Data { data, mime_type }))
2488            if *rectangle == Some(dnd_id) =>
2489        {
2490            cold();
2491            if matches!(&state.dnd_offer, DndOfferState::Dropped) {
2492                state.dnd_offer = DndOfferState::None;
2493                if !SUPPORTED_TEXT_MIME_TYPES.contains(&mime_type.as_str()) || data.is_empty() {
2494                    shell.capture_event();
2495                    return;
2496                }
2497                let Ok(content) = String::from_utf8(data.clone()) else {
2498                    shell.capture_event();
2499                    return;
2500                };
2501
2502                let mut editor = Editor::new(unsecured_value, &mut state.cursor);
2503
2504                editor.paste(Value::new(content.as_str()));
2505                let contents = editor.contents();
2506                let unsecured_value = Value::new(&contents);
2507                state.tracked_value = unsecured_value.clone();
2508                if let Some(on_paste) = on_paste.as_ref() {
2509                    let message = (on_paste)(contents);
2510                    shell.publish(message);
2511                }
2512
2513                let value = if is_secure {
2514                    unsecured_value.secure()
2515                } else {
2516                    unsecured_value
2517                };
2518                update_cache(state, &value);
2519                shell.capture_event();
2520                return;
2521            }
2522            return;
2523        }
2524        _ => {}
2525    }
2526}
2527
2528fn input_method<'b>(
2529    state: &'b State,
2530    text_layout: Layout<'_>,
2531    value: &Value,
2532) -> InputMethod<&'b str> {
2533    if !state.is_focused() {
2534        return InputMethod::Disabled;
2535    };
2536
2537    let text_bounds = text_layout.bounds();
2538    let cursor_index = match state.cursor.state(value) {
2539        cursor::State::Index(position) => position,
2540        cursor::State::Selection { start, end } => start.min(end),
2541    };
2542    let (cursor, offset) = measure_cursor_and_scroll_offset(
2543        state.value.raw(),
2544        text_bounds,
2545        cursor_index,
2546        value,
2547        state.cursor.affinity(),
2548        state.scroll_offset,
2549    );
2550    InputMethod::Enabled {
2551        cursor: Rectangle::new(
2552            Point::new(text_bounds.x + cursor - offset, text_bounds.y),
2553            Size::new(1.0, text_bounds.height),
2554        ),
2555        purpose: if state.is_secure {
2556            input_method::Purpose::Secure
2557        } else {
2558            input_method::Purpose::Normal
2559        },
2560        preedit: state.preedit.as_ref().map(input_method::Preedit::as_ref),
2561    }
2562}
2563
2564/// Draws the [`TextInput`] with the given [`Renderer`], overriding its
2565/// [`Value`] if provided.
2566///
2567/// [`Renderer`]: text::Renderer
2568#[allow(clippy::too_many_arguments)]
2569#[allow(clippy::too_many_lines)]
2570#[allow(clippy::missing_panics_doc)]
2571pub fn draw<'a, Message>(
2572    renderer: &mut crate::Renderer,
2573    theme: &crate::Theme,
2574    layout: Layout<'_>,
2575    text_layout: Layout<'_>,
2576    cursor_position: mouse::Cursor,
2577    tree: &Tree,
2578    value: &Value,
2579    placeholder: &str,
2580    size: Option<f32>,
2581    font: Option<<crate::Renderer as iced_core::text::Renderer>::Font>,
2582    is_disabled: bool,
2583    is_secure: bool,
2584    icon: Option<&Element<'a, Message, crate::Theme, crate::Renderer>>,
2585    trailing_icon: Option<&Element<'a, Message, crate::Theme, crate::Renderer>>,
2586    style: &<crate::Theme as StyleSheet>::Style,
2587    dnd_icon: bool,
2588    line_height: text::LineHeight,
2589    error: Option<&str>,
2590    label: Option<&str>,
2591    helper_text: Option<&str>,
2592    helper_text_size: f32,
2593    helper_line_height: text::LineHeight,
2594    viewport: &Rectangle,
2595    renderer_style: &renderer::Style,
2596) {
2597    // all children should be icon images
2598    let children = &tree.children;
2599
2600    let state = tree.state.downcast_ref::<State>();
2601    let secure_value = is_secure.then(|| value.secure());
2602    let value = secure_value.as_ref().unwrap_or(value);
2603
2604    let mut children_layout = layout.children();
2605
2606    let (label_layout, layout, helper_text_layout) = if label.is_some() && helper_text.is_some() {
2607        let label_layout = children_layout.next();
2608        let layout = children_layout.next().unwrap();
2609        let helper_text_layout = children_layout.next();
2610        (label_layout, layout, helper_text_layout)
2611    } else if label.is_some() {
2612        let label_layout = children_layout.next();
2613        let layout = children_layout.next().unwrap();
2614        (label_layout, layout, None)
2615    } else if helper_text.is_some() {
2616        let layout = children_layout.next().unwrap();
2617        let helper_text_layout = children_layout.next();
2618        (None, layout, helper_text_layout)
2619    } else {
2620        let layout = children_layout.next().unwrap();
2621
2622        (None, layout, None)
2623    };
2624
2625    let mut children_layout = layout.children();
2626    let bounds = layout.bounds();
2627    // XXX Dnd widget may not have a layout with children, so we just use the text_layout
2628    let text_bounds = children_layout.next().unwrap_or(text_layout).bounds();
2629
2630    let is_mouse_over = cursor_position.is_over(bounds);
2631
2632    let appearance = if is_disabled {
2633        theme.disabled(style)
2634    } else if error.is_some() {
2635        theme.error(style)
2636    } else if state.is_focused() {
2637        theme.focused(style)
2638    } else if is_mouse_over {
2639        theme.hovered(style)
2640    } else {
2641        theme.active(style)
2642    };
2643
2644    let mut icon_color = appearance.icon_color.unwrap_or(renderer_style.icon_color);
2645    let mut text_color = appearance.text_color.unwrap_or(renderer_style.text_color);
2646
2647    // TODO: iced will not render alpha itself on text or icon colors.
2648    if is_disabled {
2649        let background = theme.current_container().component.base.into();
2650        icon_color = icon_color.blend_alpha(background, 0.5);
2651        text_color = text_color.blend_alpha(background, 0.5);
2652    }
2653
2654    // draw background and its border
2655    if let Some(border_offset) = appearance.border_offset {
2656        let offset_bounds = Rectangle {
2657            x: bounds.x - border_offset,
2658            y: bounds.y - border_offset,
2659            width: border_offset.mul_add(2.0, bounds.width),
2660            height: border_offset.mul_add(2.0, bounds.height),
2661        };
2662        renderer.fill_quad(
2663            renderer::Quad {
2664                bounds,
2665                border: Border {
2666                    radius: appearance.border_radius,
2667                    width: appearance.border_width,
2668                    ..Default::default()
2669                },
2670                shadow: Shadow {
2671                    offset: Vector::new(0.0, 1.0),
2672                    color: Color::TRANSPARENT,
2673                    blur_radius: 0.0,
2674                },
2675                snap: true,
2676            },
2677            appearance.background,
2678        );
2679        renderer.fill_quad(
2680            renderer::Quad {
2681                bounds: offset_bounds,
2682                border: Border {
2683                    width: appearance.border_width,
2684                    color: appearance.border_color,
2685                    radius: appearance.border_radius,
2686                },
2687                shadow: Shadow {
2688                    offset: Vector::new(0.0, 1.0),
2689                    color: Color::TRANSPARENT,
2690                    blur_radius: 0.0,
2691                },
2692                snap: true,
2693            },
2694            Background::Color(Color::TRANSPARENT),
2695        );
2696    } else {
2697        renderer.fill_quad(
2698            renderer::Quad {
2699                bounds,
2700                border: Border {
2701                    width: appearance.border_width,
2702                    color: appearance.border_color,
2703                    radius: appearance.border_radius,
2704                },
2705                shadow: Shadow {
2706                    offset: Vector::new(0.0, 1.0),
2707                    color: Color::TRANSPARENT,
2708                    blur_radius: 0.0,
2709                },
2710                snap: true,
2711            },
2712            appearance.background,
2713        );
2714    }
2715
2716    // draw the label if it exists
2717    if let (Some(label_layout), Some(label)) = (label_layout, label) {
2718        renderer.fill_text(
2719            Text {
2720                content: label.to_string(),
2721                size: iced::Pixels(size.unwrap_or_else(|| renderer.default_size().0)),
2722                font: font.unwrap_or_else(|| renderer.default_font()),
2723                bounds: label_layout.bounds().size(),
2724                align_x: text::Alignment::Left,
2725                align_y: alignment::Vertical::Top,
2726                line_height,
2727                shaping: text::Shaping::Advanced,
2728                wrapping: text::Wrapping::None,
2729                ellipsize: text::Ellipsize::None,
2730            },
2731            label_layout.bounds().position(),
2732            appearance.label_color,
2733            *viewport,
2734        );
2735    }
2736    let mut child_index = 0;
2737    let leading_icon_tree = children.get(child_index);
2738    // draw the start icon in the text input
2739    let has_start_icon = icon.is_some();
2740    if let (Some(icon), Some(tree)) = (icon, leading_icon_tree) {
2741        let mut children = text_layout.children();
2742        let _ = children.next().unwrap();
2743        let icon_layout = children.next().unwrap();
2744
2745        icon.as_widget().draw(
2746            tree,
2747            renderer,
2748            theme,
2749            &renderer::Style {
2750                icon_color,
2751                text_color,
2752                scale_factor: renderer_style.scale_factor,
2753            },
2754            icon_layout,
2755            cursor_position,
2756            viewport,
2757        );
2758        child_index += 1;
2759    }
2760
2761    let text = value.to_string();
2762    let font = font.unwrap_or_else(|| renderer.default_font());
2763    let size = size.unwrap_or_else(|| renderer.default_size().0);
2764    let text_width = state.value.min_width();
2765    let actual_width = text_width.max(text_bounds.width);
2766
2767    let radius_0 = THEME.lock().unwrap().cosmic().corner_radii.radius_0.into();
2768    #[cfg(wayland_platform)]
2769    let handling_dnd_offer = !matches!(state.dnd_offer, DndOfferState::None);
2770    #[cfg(not(wayland_platform))]
2771    let handling_dnd_offer = false;
2772    let (cursors, offset, is_selecting) = if let Some(focus) =
2773        state.is_focused.filter(|f| f.focused).or_else(|| {
2774            let now = Instant::now();
2775            handling_dnd_offer.then_some(Focus {
2776                needs_update: false,
2777                updated_at: now,
2778                now,
2779                focused: true,
2780            })
2781        }) {
2782        match state.cursor.state(value) {
2783            cursor::State::Index(position) => {
2784                let (text_value_width, _) = measure_cursor_and_scroll_offset(
2785                    state.value.raw(),
2786                    text_bounds,
2787                    position,
2788                    value,
2789                    state.cursor.affinity(),
2790                    state.scroll_offset,
2791                );
2792                let is_cursor_visible = handling_dnd_offer
2793                    || ((focus.now - focus.updated_at).as_millis() / CURSOR_BLINK_INTERVAL_MILLIS)
2794                        .is_multiple_of(2);
2795
2796                if is_cursor_visible && !dnd_icon {
2797                    (
2798                        vec![(
2799                            renderer::Quad {
2800                                bounds: Rectangle {
2801                                    x: (text_bounds.x + text_value_width).floor(),
2802                                    y: text_bounds.y,
2803                                    width: 1.0,
2804                                    height: text_bounds.height,
2805                                },
2806                                border: Border {
2807                                    width: 0.0,
2808                                    color: Color::TRANSPARENT,
2809                                    radius: radius_0,
2810                                },
2811                                shadow: Shadow {
2812                                    offset: Vector::ZERO,
2813                                    color: Color::TRANSPARENT,
2814                                    blur_radius: 0.0,
2815                                },
2816                                snap: true,
2817                            },
2818                            text_color,
2819                        )],
2820                        state.scroll_offset,
2821                        false,
2822                    )
2823                } else {
2824                    (
2825                        Vec::<(renderer::Quad, Color)>::new(),
2826                        if dnd_icon { 0.0 } else { state.scroll_offset },
2827                        false,
2828                    )
2829                }
2830            }
2831            cursor::State::Selection { start, end } => {
2832                let left = start.min(end);
2833                let right = end.max(start);
2834
2835                if dnd_icon {
2836                    (Vec::<(renderer::Quad, Color)>::new(), 0.0, true)
2837                } else {
2838                    let lo_byte = value.byte_index_at_grapheme(left);
2839                    let hi_byte = value.byte_index_at_grapheme(right);
2840
2841                    let rects = state.value.raw().highlight(
2842                        0,
2843                        (lo_byte, text::Affinity::After),
2844                        (hi_byte, text::Affinity::Before),
2845                    );
2846
2847                    let cursors: Vec<(renderer::Quad, Color)> = rects
2848                        .into_iter()
2849                        .map(|r| {
2850                            (
2851                                renderer::Quad {
2852                                    bounds: Rectangle {
2853                                        x: text_bounds.x + r.x,
2854                                        y: text_bounds.y,
2855                                        width: r.width,
2856                                        height: text_bounds.height,
2857                                    },
2858                                    border: Border {
2859                                        width: 0.0,
2860                                        color: Color::TRANSPARENT,
2861                                        radius: radius_0,
2862                                    },
2863                                    shadow: Shadow {
2864                                        offset: Vector::ZERO,
2865                                        color: Color::TRANSPARENT,
2866                                        blur_radius: 0.0,
2867                                    },
2868                                    snap: true,
2869                                },
2870                                appearance.selected_fill,
2871                            )
2872                        })
2873                        .collect();
2874
2875                    (cursors, state.scroll_offset, true)
2876                }
2877            }
2878        }
2879    } else {
2880        let unfocused_offset = match effective_alignment(state.value.raw()) {
2881            alignment::Horizontal::Right => {
2882                (state.value.raw().min_width() - text_bounds.width).max(0.0)
2883            }
2884            _ => 0.0,
2885        };
2886
2887        (
2888            Vec::<(renderer::Quad, Color)>::new(),
2889            unfocused_offset,
2890            false,
2891        )
2892    };
2893
2894    let render = |renderer: &mut crate::Renderer| {
2895        let alignment_offset = alignment_offset(
2896            text_bounds.width,
2897            state.value.raw().min_width(),
2898            effective_alignment(state.value.raw()),
2899        );
2900
2901        if cursors.is_empty() {
2902            renderer.with_translation(Vector::ZERO, |_| {});
2903        } else {
2904            renderer.with_translation(Vector::new(alignment_offset - offset, 0.0), |renderer| {
2905                for (quad, color) in &cursors {
2906                    renderer.fill_quad(*quad, *color);
2907                }
2908            });
2909        }
2910
2911        let bounds = Rectangle {
2912            x: text_bounds.x + alignment_offset - offset,
2913            y: text_bounds.center_y(),
2914            width: actual_width,
2915            ..text_bounds
2916        };
2917        let color = if text.is_empty() {
2918            appearance.placeholder_color
2919        } else {
2920            text_color
2921        };
2922
2923        renderer.fill_text(
2924            Text {
2925                content: if text.is_empty() {
2926                    placeholder.to_string()
2927                } else {
2928                    text.clone()
2929                },
2930                font,
2931                bounds: bounds.size(),
2932                size: iced::Pixels(size),
2933                align_x: text::Alignment::Default,
2934                align_y: alignment::Vertical::Center,
2935                line_height: text::LineHeight::default(),
2936                shaping: text::Shaping::Advanced,
2937                wrapping: text::Wrapping::None,
2938                ellipsize: text::Ellipsize::None,
2939            },
2940            bounds.position(),
2941            color,
2942            text_bounds,
2943        );
2944    };
2945
2946    // FIXME: we always must clip with a layer because of what appears to be a tiny-skia text clipping issue.
2947    // Otherwise overflowing text escapes the bounds of the input.
2948    renderer.with_layer(text_bounds, render);
2949
2950    let trailing_icon_tree = children.get(child_index);
2951
2952    // draw the end icon in the text input
2953    if let (Some(icon), Some(tree)) = (trailing_icon, trailing_icon_tree) {
2954        let mut children = text_layout.children();
2955        let mut icon_layout = children.next().unwrap();
2956        if has_start_icon {
2957            icon_layout = children.next().unwrap();
2958        }
2959        icon_layout = children.next().unwrap();
2960
2961        icon.as_widget().draw(
2962            tree,
2963            renderer,
2964            theme,
2965            &renderer::Style {
2966                icon_color,
2967                text_color,
2968                scale_factor: renderer_style.scale_factor,
2969            },
2970            icon_layout,
2971            cursor_position,
2972            viewport,
2973        );
2974    }
2975
2976    // draw the helper text if it exists
2977    if let (Some(helper_text_layout), Some(helper_text)) = (helper_text_layout, helper_text) {
2978        renderer.fill_text(
2979            Text {
2980                content: helper_text.to_string(), // TODO remove to_string?
2981                size: iced::Pixels(helper_text_size),
2982                font,
2983                bounds: helper_text_layout.bounds().size(),
2984                align_x: text::Alignment::Left,
2985                align_y: alignment::Vertical::Top,
2986                line_height: helper_line_height,
2987                shaping: text::Shaping::Advanced,
2988                wrapping: text::Wrapping::None,
2989                ellipsize: text::Ellipsize::None,
2990            },
2991            helper_text_layout.bounds().position(),
2992            text_color,
2993            *viewport,
2994        );
2995    }
2996}
2997
2998/// Computes the current [`mouse::Interaction`] of the [`TextInput`].
2999#[must_use]
3000pub fn mouse_interaction(
3001    layout: Layout<'_>,
3002    cursor_position: mouse::Cursor,
3003    is_disabled: bool,
3004) -> mouse::Interaction {
3005    if cursor_position.is_over(layout.bounds()) {
3006        if is_disabled {
3007            mouse::Interaction::NotAllowed
3008        } else {
3009            mouse::Interaction::Text
3010        }
3011    } else {
3012        mouse::Interaction::default()
3013    }
3014}
3015
3016/// A string which can be sent to the clipboard or drag-and-dropped.
3017#[derive(Debug, Clone)]
3018pub struct TextInputString(pub String);
3019
3020#[cfg(wayland_platform)]
3021impl AsMimeTypes for TextInputString {
3022    fn available(&self) -> Cow<'static, [String]> {
3023        Cow::Owned(
3024            SUPPORTED_TEXT_MIME_TYPES
3025                .iter()
3026                .cloned()
3027                .map(String::from)
3028                .collect::<Vec<_>>(),
3029        )
3030    }
3031
3032    fn as_bytes(&self, mime_type: &str) -> Option<Cow<'static, [u8]>> {
3033        if SUPPORTED_TEXT_MIME_TYPES.contains(&mime_type) {
3034            Some(Cow::Owned(self.0.clone().into_bytes()))
3035        } else {
3036            None
3037        }
3038    }
3039}
3040
3041#[derive(Debug, Clone, PartialEq)]
3042pub(crate) enum DraggingState {
3043    Selection,
3044    #[cfg(wayland_platform)]
3045    PrepareDnd(Point),
3046    #[cfg(wayland_platform)]
3047    Dnd(DndAction, String),
3048}
3049
3050#[cfg(wayland_platform)]
3051#[derive(Debug, Default, Clone)]
3052pub(crate) enum DndOfferState {
3053    #[default]
3054    None,
3055    HandlingOffer(Vec<String>, DndAction),
3056    Dropped,
3057}
3058#[derive(Debug, Default, Clone)]
3059#[cfg(not(wayland_platform))]
3060pub(crate) struct DndOfferState;
3061
3062/// The state of a [`TextInput`].
3063#[derive(Default, Clone)]
3064#[must_use]
3065pub struct State {
3066    pub tracked_value: Value,
3067    pub value: crate::Plain,
3068    pub placeholder: crate::Plain,
3069    pub label: crate::Plain,
3070    pub helper_text: crate::Plain,
3071    pub dirty: bool,
3072    pub is_secure: bool,
3073    pub is_read_only: bool,
3074    pub emit_unfocus: bool,
3075    select_on_focus: bool,
3076    double_click_select_delimiter: Option<char>,
3077    is_focused: Option<Focus>,
3078    dragging_state: Option<DraggingState>,
3079    dnd_offer: DndOfferState,
3080    is_pasting: Option<Value>,
3081    last_click: Option<mouse::Click>,
3082    cursor: Cursor,
3083    preedit: Option<Preedit>,
3084    keyboard_modifiers: keyboard::Modifiers,
3085    scroll_offset: f32,
3086    context_menu_position: Option<iced_core::Point>,
3087    pub(crate) menu_bar_state: crate::widget::menu::MenuBarState,
3088    pub(crate) pending_action: crate::widget::text_context_menu::PendingAction,
3089}
3090
3091impl std::fmt::Debug for State {
3092    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3093        f.debug_struct("State")
3094            .field("is_secure", &self.is_secure)
3095            .field("is_read_only", &self.is_read_only)
3096            .field("dirty", &self.dirty)
3097            .finish_non_exhaustive()
3098    }
3099}
3100
3101#[derive(Debug, Clone, Copy)]
3102struct Focus {
3103    updated_at: Instant,
3104    now: Instant,
3105    focused: bool,
3106    needs_update: bool,
3107}
3108
3109impl State {
3110    /// Creates a new [`State`], representing an unfocused [`TextInput`].
3111    pub fn new(
3112        is_secure: bool,
3113        is_read_only: bool,
3114        always_active: bool,
3115        select_on_focus: bool,
3116    ) -> Self {
3117        Self {
3118            is_secure,
3119            is_read_only,
3120            is_focused: always_active.then(|| {
3121                let now = Instant::now();
3122                Focus {
3123                    updated_at: now,
3124                    now,
3125                    focused: true,
3126                    needs_update: false,
3127                }
3128            }),
3129            select_on_focus,
3130            ..Self::default()
3131        }
3132    }
3133
3134    /// Returns the current value of the selected text in the [`TextInput`].
3135    #[must_use]
3136    pub fn selected_text(&self, text: &str) -> Option<String> {
3137        let value = Value::new(text);
3138        match self.cursor.state(&value) {
3139            cursor::State::Index(_) => None,
3140            cursor::State::Selection { start, end } => {
3141                let left = start.min(end);
3142                let right = end.max(start);
3143                Some(value.select(left, right).to_string())
3144            }
3145        }
3146    }
3147
3148    #[cfg(wayland_platform)]
3149    /// Returns the current value of the dragged text in the [`TextInput`].
3150    #[must_use]
3151    pub fn dragged_text(&self) -> Option<String> {
3152        match self.dragging_state.as_ref() {
3153            Some(DraggingState::Dnd(_, text)) => Some(text.clone()),
3154            _ => None,
3155        }
3156    }
3157
3158    /// Creates a new [`State`], representing a focused [`TextInput`].
3159    pub fn focused(is_secure: bool, is_read_only: bool) -> Self {
3160        Self {
3161            tracked_value: Value::default(),
3162            is_secure,
3163            value: crate::Plain::default(),
3164            placeholder: crate::Plain::default(),
3165            label: crate::Plain::default(),
3166            helper_text: crate::Plain::default(),
3167            is_read_only,
3168            emit_unfocus: false,
3169            is_focused: None,
3170            select_on_focus: false,
3171            double_click_select_delimiter: None,
3172            dragging_state: None,
3173            dnd_offer: DndOfferState::default(),
3174            is_pasting: None,
3175            last_click: None,
3176            cursor: Cursor::default(),
3177            preedit: None,
3178            keyboard_modifiers: keyboard::Modifiers::default(),
3179            scroll_offset: 0.0,
3180            dirty: false,
3181            context_menu_position: None,
3182            menu_bar_state: crate::widget::menu::MenuBarState::default(),
3183            pending_action: crate::widget::text_context_menu::pending_action(),
3184        }
3185    }
3186
3187    /// Returns whether the [`TextInput`] is currently focused or not.
3188    #[inline]
3189    #[must_use]
3190    pub fn is_focused(&self) -> bool {
3191        self.is_focused.is_some_and(|f| f.focused)
3192    }
3193
3194    /// Returns the [`Cursor`] of the [`TextInput`].
3195    #[inline]
3196    #[must_use]
3197    pub fn cursor(&self) -> Cursor {
3198        self.cursor
3199    }
3200
3201    /// Focuses the [`TextInput`].
3202    #[cold]
3203    pub fn focus(&mut self) {
3204        let now = Instant::now();
3205        LAST_FOCUS_UPDATE.with(|x| x.set(now));
3206        let was_focused = self.is_focused.is_some_and(|f| f.focused);
3207        self.is_read_only = false;
3208        self.is_focused = Some(Focus {
3209            updated_at: now,
3210            now,
3211            focused: true,
3212            needs_update: false,
3213        });
3214
3215        if was_focused {
3216            return;
3217        }
3218        if self.select_on_focus {
3219            self.select_all()
3220        } else {
3221            self.move_cursor_to_end();
3222        }
3223    }
3224
3225    /// Unfocuses the [`TextInput`].
3226    #[cold]
3227    pub(super) fn unfocus(&mut self) {
3228        self.cursor.clear_selection();
3229        self.last_click = None;
3230        self.is_focused = self.is_focused.map(|mut f| {
3231            f.focused = false;
3232            f.needs_update = false;
3233            f
3234        });
3235        self.dragging_state = None;
3236        self.is_pasting = None;
3237        self.keyboard_modifiers = keyboard::Modifiers::default();
3238    }
3239
3240    /// Moves the [`Cursor`] of the [`TextInput`] to the front of the input text.
3241    #[inline]
3242    pub fn move_cursor_to_front(&mut self) {
3243        self.cursor.move_to(0);
3244    }
3245
3246    /// Moves the [`Cursor`] of the [`TextInput`] to the end of the input text.
3247    #[inline]
3248    pub fn move_cursor_to_end(&mut self) {
3249        self.cursor.move_to(usize::MAX);
3250    }
3251
3252    /// Moves the [`Cursor`] of the [`TextInput`] to an arbitrary location.
3253    #[inline]
3254    pub fn move_cursor_to(&mut self, position: usize) {
3255        self.cursor.move_to(position);
3256    }
3257
3258    /// Selects all the content of the [`TextInput`].
3259    #[inline]
3260    pub fn select_all(&mut self) {
3261        self.cursor.select_range(0, usize::MAX);
3262    }
3263
3264    /// Selects a range of the content of the [`TextInput`].
3265    #[inline]
3266    pub fn select_range(&mut self, start: usize, end: usize) {
3267        self.cursor.select_range(start, end);
3268    }
3269
3270    /// Returns the context menu position, if a context menu should be shown.
3271    pub fn context_menu_position(&self) -> Option<iced_core::Point> {
3272        self.context_menu_position
3273    }
3274
3275    /// Sets or clears the context menu position.
3276    pub fn set_context_menu_position(&mut self, pos: Option<iced_core::Point>) {
3277        self.context_menu_position = pos;
3278    }
3279
3280    /// Deletes the current selection and returns the new text content.
3281    pub fn delete_selection(&mut self) -> String {
3282        let mut editor = super::editor::Editor::new(&mut self.tracked_value, &mut self.cursor);
3283        editor.delete();
3284        editor.contents()
3285    }
3286
3287    /// Pastes text at the current cursor position and returns the new text content.
3288    pub fn paste_text(&mut self, text: &str) -> String {
3289        let paste_value = super::value::Value::new(text);
3290        let mut editor = super::editor::Editor::new(&mut self.tracked_value, &mut self.cursor);
3291        editor.paste(paste_value);
3292        let contents = editor.contents();
3293        self.tracked_value = super::value::Value::new(&contents);
3294        contents
3295    }
3296
3297    pub(super) fn setting_selection(&mut self, value: &Value, bounds: Rectangle<f32>, target: f32) {
3298        let (position, affinity) = find_cursor_position(bounds, value, self, target)
3299            .unwrap_or((0, text::Affinity::Before));
3300
3301        self.cursor.set_affinity(affinity);
3302        self.cursor.move_to(position);
3303        self.dragging_state = Some(DraggingState::Selection);
3304    }
3305}
3306
3307impl operation::Focusable for State {
3308    #[inline]
3309    fn is_focused(&self) -> bool {
3310        Self::is_focused(self)
3311    }
3312
3313    #[inline]
3314    fn focus(&mut self) {
3315        Self::focus(self);
3316        if let Some(focus) = self.is_focused.as_mut() {
3317            focus.needs_update = true;
3318        }
3319    }
3320
3321    #[inline]
3322    fn unfocus(&mut self) {
3323        Self::unfocus(self);
3324        if let Some(focus) = self.is_focused.as_mut() {
3325            focus.needs_update = true;
3326        }
3327    }
3328}
3329
3330impl operation::TextInput for State {
3331    #[inline]
3332    fn move_cursor_to_front(&mut self) {
3333        Self::move_cursor_to_front(self);
3334    }
3335
3336    #[inline]
3337    fn move_cursor_to_end(&mut self) {
3338        Self::move_cursor_to_end(self);
3339    }
3340
3341    #[inline]
3342    fn move_cursor_to(&mut self, position: usize) {
3343        Self::move_cursor_to(self, position);
3344    }
3345
3346    #[inline]
3347    fn select_all(&mut self) {
3348        Self::select_all(self);
3349    }
3350
3351    fn text(&self) -> &str {
3352        todo!()
3353    }
3354
3355    #[inline]
3356    fn select_range(&mut self, start: usize, end: usize) {
3357        Self::select_range(self, start, end);
3358    }
3359}
3360
3361#[inline(never)]
3362fn measure_cursor_and_scroll_offset(
3363    paragraph: &impl text::Paragraph,
3364    text_bounds: Rectangle,
3365    cursor_index: usize,
3366    value: &Value,
3367    affinity: text::Affinity,
3368    current_offset: f32,
3369) -> (f32, f32) {
3370    let byte_index = value.byte_index_at_grapheme(cursor_index);
3371    let position = paragraph
3372        .cursor_position(0, byte_index, affinity)
3373        .unwrap_or(Point::ORIGIN);
3374
3375    // The visible window in paragraph coordinates is:
3376    //   [current_offset, current_offset + text_bounds.width]
3377    // Keep the cursor visible with a 5px margin on each side.
3378    let offset = if position.x > current_offset + text_bounds.width - 5.0 {
3379        // Cursor past right edge of visible window → scroll left
3380        (position.x + 5.0) - text_bounds.width
3381    } else if position.x < current_offset + 5.0 {
3382        // Cursor past left edge of visible window → scroll right
3383        position.x - 5.0
3384    } else {
3385        // Cursor is within visible window → keep current scroll
3386        current_offset
3387    };
3388
3389    let max_offset = (paragraph.min_width() - text_bounds.width).max(0.0);
3390    let offset = offset.clamp(0.0, max_offset);
3391
3392    (position.x, offset)
3393}
3394
3395/// Computes the position of the text cursor at the given X coordinate of
3396/// a [`TextInput`].
3397#[inline(never)]
3398fn find_cursor_position(
3399    text_bounds: Rectangle,
3400    value: &Value,
3401    state: &State,
3402    x: f32,
3403) -> Option<(usize, text::Affinity)> {
3404    let value_str = value.to_string();
3405
3406    let hit = state.value.raw().hit_test(Point::new(
3407        x + state.scroll_offset,
3408        text_bounds.height / 2.0,
3409    ))?;
3410    let char_offset = hit.cursor();
3411    let affinity = hit.affinity();
3412
3413    let grapheme_count = unicode_segmentation::UnicodeSegmentation::graphemes(
3414        &value_str[..char_offset.min(value_str.len())],
3415        true,
3416    )
3417    .count();
3418
3419    Some((grapheme_count, affinity))
3420}
3421
3422#[inline(never)]
3423fn replace_paragraph(
3424    state: &mut State,
3425    layout: Layout<'_>,
3426    value: &Value,
3427    font: <crate::Renderer as iced_core::text::Renderer>::Font,
3428    text_size: Pixels,
3429    line_height: text::LineHeight,
3430    limits: &layout::Limits,
3431) {
3432    let mut children_layout = layout.children();
3433    let text_bounds = children_layout.next().unwrap();
3434    let bounds = limits.resolve(
3435        Length::Shrink,
3436        Length::Fill,
3437        Size::new(0., text_bounds.bounds().height),
3438    );
3439
3440    state.value = crate::Plain::new(Text {
3441        font,
3442        line_height,
3443        content: value.to_string(),
3444        bounds,
3445        size: text_size,
3446        align_x: text::Alignment::Default,
3447        align_y: alignment::Vertical::Top,
3448        shaping: text::Shaping::Advanced,
3449        wrapping: text::Wrapping::None,
3450        ellipsize: text::Ellipsize::None,
3451    });
3452}
3453
3454const CURSOR_BLINK_INTERVAL_MILLIS: u128 = 500;
3455
3456mod platform {
3457    use iced_core::keyboard;
3458
3459    #[inline]
3460    pub fn is_jump_modifier_pressed(modifiers: keyboard::Modifiers) -> bool {
3461        if cfg!(target_os = "macos") {
3462            modifiers.alt()
3463        } else {
3464            modifiers.control()
3465        }
3466    }
3467}
3468
3469#[inline(never)]
3470fn offset(text_bounds: Rectangle, value: &Value, state: &State) -> f32 {
3471    if state.is_focused() {
3472        let cursor = state.cursor();
3473
3474        let focus_position = match cursor.state(value) {
3475            cursor::State::Index(i) => i,
3476            cursor::State::Selection { end, .. } => end,
3477        };
3478
3479        let (_, offset) = measure_cursor_and_scroll_offset(
3480            state.value.raw(),
3481            text_bounds,
3482            focus_position,
3483            value,
3484            state.cursor().affinity(),
3485            state.scroll_offset,
3486        );
3487
3488        offset
3489    } else {
3490        match effective_alignment(state.value.raw()) {
3491            alignment::Horizontal::Right => {
3492                (state.value.raw().min_width() - text_bounds.width).max(0.0)
3493            }
3494            _ => 0.0,
3495        }
3496    }
3497}
3498
3499#[inline(never)]
3500fn alignment_offset(
3501    text_bounds_width: f32,
3502    text_min_width: f32,
3503    alignment: alignment::Horizontal,
3504) -> f32 {
3505    if text_min_width > text_bounds_width {
3506        0.0
3507    } else {
3508        match alignment {
3509            alignment::Horizontal::Left => 0.0,
3510            alignment::Horizontal::Center => (text_bounds_width - text_min_width) / 2.0,
3511            alignment::Horizontal::Right => text_bounds_width - text_min_width,
3512        }
3513    }
3514}
3515
3516#[inline(never)]
3517fn effective_alignment(paragraph: &impl text::Paragraph) -> alignment::Horizontal {
3518    if paragraph.is_rtl(0).unwrap_or(false) {
3519        alignment::Horizontal::Right
3520    } else {
3521        alignment::Horizontal::Left
3522    }
3523}
3524
3525use iced_core::widget::tree::Tree as WidgetTree;
3526
3527impl<Message: Clone + 'static> iced_core::widget::text::HasSelectableText
3528    for TextInput<'_, Message>
3529{
3530    fn selected_text(&self, tree: &WidgetTree) -> Option<String> {
3531        let state = tree.state.downcast_ref::<State>();
3532        let (start, end) = state.cursor().selection(&state.tracked_value)?;
3533        Some(state.tracked_value.select(start, end).to_string())
3534    }
3535
3536    fn select_all(&self, tree: &mut WidgetTree) {
3537        let state = tree.state.downcast_mut::<State>();
3538        state.select_all();
3539    }
3540
3541    fn is_editable(&self) -> bool {
3542        true
3543    }
3544
3545    fn is_focused(&self, tree: &WidgetTree) -> bool {
3546        tree.state.downcast_ref::<State>().is_focused()
3547    }
3548
3549    fn context_menu_position(&self, tree: &WidgetTree) -> Option<iced_core::Point> {
3550        tree.state.downcast_ref::<State>().context_menu_position
3551    }
3552
3553    fn set_context_menu_position(&self, tree: &mut WidgetTree, pos: Option<iced_core::Point>) {
3554        tree.state.downcast_mut::<State>().context_menu_position = pos;
3555    }
3556
3557    fn delete_selection(&self, tree: &mut WidgetTree) -> Option<String> {
3558        let state = tree.state.downcast_mut::<State>();
3559        Some(state.delete_selection())
3560    }
3561
3562    fn paste_text(&self, tree: &mut WidgetTree, text: &str) -> Option<String> {
3563        let filtered: String = text.chars().filter(|c| !c.is_control()).collect();
3564        let state = tree.state.downcast_mut::<State>();
3565        Some(state.paste_text(&filtered))
3566    }
3567}