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 has_text = !state.tracked_value.is_empty();
1058                let clipboard_has_text = state.clipboard_has_text;
1059                let click_position = state.context_menu_position.unwrap();
1060                let menu_bar_state = state.menu_bar_state.clone();
1061                let pending_action = state.pending_action.clone();
1062
1063                crate::widget::text_context_menu::create_text_context_popup(
1064                    click_position,
1065                    selected_text,
1066                    true,
1067                    has_selection,
1068                    has_text,
1069                    clipboard_has_text,
1070                    &menu_bar_state,
1071                    &pending_action,
1072                    renderer,
1073                    viewport,
1074                    cursor_position,
1075                    self.window_id,
1076                );
1077
1078                let state = tree.state.downcast_mut::<State>();
1079                state.context_menu_position = None;
1080            }
1081
1082            // Process deferred actions from the popup.
1083            let state = tree.state.downcast_ref::<State>();
1084            let pending_action = state.pending_action.clone();
1085            if let Some(action) =
1086                crate::widget::text_context_menu::take_pending_action(&pending_action)
1087            {
1088                let state = tree.state.downcast_mut::<State>();
1089                match action {
1090                    crate::widget::text_context_menu::TextCtxAction::Copy => {}
1091                    crate::widget::text_context_menu::TextCtxAction::Cut => {
1092                        let contents = state.delete_selection();
1093                        if let Some(on_input) = self.on_input.as_deref() {
1094                            shell.publish((on_input)(contents));
1095                        }
1096                    }
1097                    crate::widget::text_context_menu::TextCtxAction::Paste => {
1098                        let content: String = clipboard
1099                            .read(iced_core::clipboard::Kind::Standard)
1100                            .unwrap_or_default();
1101                        let filtered: String =
1102                            content.chars().filter(|c| !c.is_control()).collect();
1103                        let contents = state.paste_text(&filtered);
1104                        if let Some(on_input) = self.on_input.as_deref() {
1105                            shell.publish((on_input)(contents));
1106                        }
1107                    }
1108                    crate::widget::text_context_menu::TextCtxAction::SelectAll => {
1109                        state.select_all();
1110                    }
1111                }
1112            }
1113        }
1114
1115        let state = tree.state.downcast_mut::<State>();
1116        let value = if self.is_secure {
1117            self.value.secure()
1118        } else {
1119            self.value.clone()
1120        };
1121        state.scroll_offset = offset(
1122            text_layout.children().next().unwrap().bounds(),
1123            &value,
1124            state,
1125        );
1126    }
1127
1128    #[inline]
1129    fn draw(
1130        &self,
1131        tree: &Tree,
1132        renderer: &mut crate::Renderer,
1133        theme: &crate::Theme,
1134        style: &renderer::Style,
1135        layout: Layout<'_>,
1136        cursor_position: mouse::Cursor,
1137        viewport: &Rectangle,
1138    ) {
1139        let text_layout = self.text_layout(layout);
1140        draw(
1141            renderer,
1142            theme,
1143            layout,
1144            text_layout,
1145            cursor_position,
1146            tree,
1147            &self.value,
1148            &self.placeholder,
1149            self.size,
1150            self.font,
1151            self.on_input.is_none() && !self.manage_value,
1152            self.is_secure,
1153            self.leading_icon.as_ref(),
1154            self.trailing_icon.as_ref(),
1155            &self.style,
1156            self.dnd_icon,
1157            self.line_height,
1158            self.error.as_deref(),
1159            self.label.as_deref(),
1160            self.helper_text.as_deref(),
1161            self.helper_size,
1162            self.helper_line_height,
1163            viewport,
1164            style,
1165        );
1166    }
1167
1168    fn mouse_interaction(
1169        &self,
1170        state: &Tree,
1171        layout: Layout<'_>,
1172        cursor_position: mouse::Cursor,
1173        viewport: &Rectangle,
1174        renderer: &crate::Renderer,
1175    ) -> mouse::Interaction {
1176        let layout = self.text_layout(layout);
1177        let mut index = 0;
1178        if let (Some(leading_icon), Some(tree)) =
1179            (self.leading_icon.as_ref(), state.children.get(index))
1180        {
1181            let leading_icon_layout = layout.children().nth(1).unwrap();
1182
1183            if cursor_position.is_over(leading_icon_layout.bounds()) {
1184                return leading_icon.as_widget().mouse_interaction(
1185                    tree,
1186                    layout,
1187                    cursor_position,
1188                    viewport,
1189                    renderer,
1190                );
1191            }
1192            index += 1;
1193        }
1194
1195        if self.trailing_icon.is_some() {
1196            let mut children = layout.children();
1197            children.next();
1198            // skip if there is no leading icon
1199            if self.leading_icon.is_some() {
1200                children.next();
1201            }
1202            let trailing_icon_layout = children.next().unwrap();
1203
1204            if cursor_position.is_over(trailing_icon_layout.bounds()) {
1205                if self.is_editable_variant {
1206                    return mouse::Interaction::Pointer;
1207                }
1208
1209                if let Some((trailing_icon, tree)) =
1210                    self.trailing_icon.as_ref().zip(state.children.get(index))
1211                {
1212                    return trailing_icon.as_widget().mouse_interaction(
1213                        tree,
1214                        layout,
1215                        cursor_position,
1216                        viewport,
1217                        renderer,
1218                    );
1219                }
1220            }
1221        }
1222        let mut children = layout.children();
1223        let layout = children.next().unwrap();
1224        mouse_interaction(
1225            layout,
1226            cursor_position,
1227            self.on_input.is_none() && !self.manage_value,
1228        )
1229    }
1230
1231    #[inline]
1232    fn id(&self) -> Option<Id> {
1233        Some(self.id.clone())
1234    }
1235
1236    #[inline]
1237    fn set_id(&mut self, id: Id) {
1238        self.id = id;
1239    }
1240
1241    fn drag_destinations(
1242        &self,
1243        _state: &Tree,
1244        layout: Layout<'_>,
1245        _renderer: &crate::Renderer,
1246        dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles,
1247    ) {
1248        if let Some(input) = layout.children().last() {
1249            let Rectangle {
1250                x,
1251                y,
1252                width,
1253                height,
1254            } = input.bounds();
1255            dnd_rectangles.push(iced::clipboard::dnd::DndDestinationRectangle {
1256                id: self.dnd_id(),
1257                rectangle: iced::clipboard::dnd::Rectangle {
1258                    x: x as f64,
1259                    y: y as f64,
1260                    width: width as f64,
1261                    height: height as f64,
1262                },
1263                mime_types: SUPPORTED_TEXT_MIME_TYPES
1264                    .iter()
1265                    .map(|s| Cow::Borrowed(*s))
1266                    .collect(),
1267                actions: DndAction::Move,
1268                preferred: DndAction::Move,
1269            });
1270        }
1271    }
1272}
1273
1274impl<'a, Message> From<TextInput<'a, Message>>
1275    for Element<'a, Message, crate::Theme, crate::Renderer>
1276where
1277    Message: 'static + Clone,
1278{
1279    fn from(
1280        text_input: TextInput<'a, Message>,
1281    ) -> Element<'a, Message, crate::Theme, crate::Renderer> {
1282        Element::new(text_input)
1283    }
1284}
1285
1286/// Produces a [`Task`] that focuses the [`TextInput`] with the given [`Id`].
1287pub fn focus<Message: 'static>(id: Id) -> Task<Message> {
1288    task::effect(Action::widget(operation::focusable::focus(id)))
1289}
1290
1291/// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the
1292/// end.
1293pub fn move_cursor_to_end<Message: 'static>(id: Id) -> Task<Message> {
1294    task::effect(Action::widget(operation::text_input::move_cursor_to_end(
1295        id,
1296    )))
1297}
1298
1299/// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the
1300/// front.
1301pub fn move_cursor_to_front<Message: 'static>(id: Id) -> Task<Message> {
1302    task::effect(Action::widget(operation::text_input::move_cursor_to_front(
1303        id,
1304    )))
1305}
1306
1307/// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the
1308/// provided position.
1309pub fn move_cursor_to<Message: 'static>(id: Id, position: usize) -> Task<Message> {
1310    task::effect(Action::widget(operation::text_input::move_cursor_to(
1311        id, position,
1312    )))
1313}
1314
1315/// Produces a [`Task`] that selects all the content of the [`TextInput`] with the given [`Id`].
1316pub fn select_all<Message: 'static>(id: Id) -> Task<Message> {
1317    task::effect(Action::widget(operation::text_input::select_all(id)))
1318}
1319
1320/// Produces a [`Task`] that selects a range of the content of the [`TextInput`] with the given
1321/// [`Id`].
1322pub fn select_range<Message: 'static>(id: Id, start: usize, end: usize) -> Task<Message> {
1323    task::effect(Action::widget(operation::text_input::select_range(
1324        id, start, end,
1325    )))
1326}
1327
1328/// Produces a [`Task`] that selects from the front to the last occurrence of the given character
1329/// in the [`TextInput`] with the given [`Id`], or selects all if not found.
1330pub fn select_until_last<Message: 'static>(id: Id, value: &str, ch: char) -> Task<Message> {
1331    let v = Value::new(value);
1332    let end = v.rfind_char(ch).unwrap_or(v.len());
1333    select_range(id, 0, end)
1334}
1335
1336/// Computes the layout of a [`TextInput`].
1337#[allow(clippy::cast_precision_loss)]
1338#[allow(clippy::too_many_arguments)]
1339#[allow(clippy::too_many_lines)]
1340pub fn layout<Message>(
1341    renderer: &crate::Renderer,
1342    limits: &layout::Limits,
1343    width: Length,
1344    padding: Padding,
1345    size: Option<f32>,
1346    leading_icon: Option<&mut Element<'_, Message, crate::Theme, crate::Renderer>>,
1347    trailing_icon: Option<&mut Element<'_, Message, crate::Theme, crate::Renderer>>,
1348    line_height: text::LineHeight,
1349    label: Option<&str>,
1350    helper_text: Option<&str>,
1351    helper_text_size: f32,
1352    helper_text_line_height: text::LineHeight,
1353    font: iced_core::Font,
1354    tree: &mut Tree,
1355) -> layout::Node {
1356    let limits = limits.width(width);
1357    let spacing = THEME.lock().unwrap().cosmic().space_xxs();
1358    let mut nodes = Vec::with_capacity(3);
1359
1360    let text_pos = if let Some(label) = label {
1361        let text_bounds = limits.resolve(width, Length::Shrink, Size::INFINITE);
1362        let state = tree.state.downcast_mut::<State>();
1363        let label_paragraph = &mut state.label;
1364        label_paragraph.update(Text {
1365            content: label,
1366            font,
1367            bounds: text_bounds,
1368            size: iced::Pixels(size.unwrap_or_else(|| renderer.default_size().0)),
1369            align_x: text::Alignment::Left,
1370            align_y: alignment::Vertical::Center,
1371            line_height,
1372            shaping: text::Shaping::Advanced,
1373            wrapping: text::Wrapping::None,
1374            ellipsize: text::Ellipsize::None,
1375        });
1376        let label_size = label_paragraph.min_bounds();
1377
1378        nodes.push(layout::Node::new(label_size));
1379        Vector::new(0.0, label_size.height + f32::from(spacing))
1380    } else {
1381        Vector::ZERO
1382    };
1383
1384    let text_size = size.unwrap_or_else(|| renderer.default_size().0);
1385    let mut text_input_height = line_height.to_absolute(text_size.into()).0;
1386    let padding = padding.fit(Size::ZERO, limits.max());
1387
1388    let helper_pos = if leading_icon.is_some() || trailing_icon.is_some() {
1389        let children = &mut tree.children;
1390        // TODO configurable icon spacing, maybe via appearance
1391        let limits_copy = limits;
1392
1393        let limits = limits.shrink(padding);
1394        let icon_spacing = 8.0;
1395        let mut c_i = 0;
1396        let (leading_icon_width, mut leading_icon) =
1397            if let Some((icon, tree)) = leading_icon.zip(children.get_mut(c_i)) {
1398                let size = icon.as_widget().size();
1399                let icon_node = icon.as_widget_mut().layout(
1400                    tree,
1401                    renderer,
1402                    &Limits::NONE.width(size.width).height(size.height),
1403                );
1404                text_input_height = text_input_height.max(icon_node.bounds().height);
1405                c_i += 1;
1406                (icon_node.bounds().width + icon_spacing, Some(icon_node))
1407            } else {
1408                (0.0, None)
1409            };
1410
1411        let (trailing_icon_width, mut trailing_icon) =
1412            if let Some((icon, tree)) = trailing_icon.zip(children.get_mut(c_i)) {
1413                let size = icon.as_widget().size();
1414                let icon_node = icon.as_widget_mut().layout(
1415                    tree,
1416                    renderer,
1417                    &Limits::NONE.width(size.width).height(size.height),
1418                );
1419                text_input_height = text_input_height.max(icon_node.bounds().height);
1420                (icon_node.bounds().width + icon_spacing, Some(icon_node))
1421            } else {
1422                (0.0, None)
1423            };
1424        let text_limits = limits
1425            .width(width)
1426            .height(line_height.to_absolute(text_size.into()));
1427        let text_bounds = text_limits.resolve(Length::Shrink, Length::Shrink, Size::INFINITE);
1428        let text_node = layout::Node::new(
1429            text_bounds - Size::new(leading_icon_width + trailing_icon_width, 0.0),
1430        )
1431        .move_to(Point::new(
1432            padding.left + leading_icon_width,
1433            padding.top
1434                + ((text_input_height - line_height.to_absolute(text_size.into()).0) / 2.0)
1435                    .max(0.0),
1436        ));
1437        let mut node_list: Vec<_> = Vec::with_capacity(3);
1438
1439        let text_node_bounds = text_node.bounds();
1440        node_list.push(text_node);
1441
1442        if let Some(leading_icon) = leading_icon.take() {
1443            node_list.push(leading_icon.clone().move_to(Point::new(
1444                padding.left,
1445                padding.top + ((text_input_height - leading_icon.bounds().height) / 2.0).max(0.0),
1446            )));
1447        }
1448        if let Some(trailing_icon) = trailing_icon.take() {
1449            let trailing_icon = trailing_icon.clone().move_to(Point::new(
1450                text_node_bounds.x + text_node_bounds.width + f32::from(spacing),
1451                padding.top + ((text_input_height - trailing_icon.bounds().height) / 2.0).max(0.0),
1452            ));
1453            node_list.push(trailing_icon);
1454        }
1455
1456        let text_input_size = Size::new(
1457            text_node_bounds.x + text_node_bounds.width + trailing_icon_width,
1458            text_input_height,
1459        )
1460        .expand(padding);
1461
1462        let input_limits = limits_copy
1463            .width(width)
1464            .height(text_input_height.max(text_input_size.height))
1465            .min_width(text_input_size.width);
1466        let input_bounds = input_limits.resolve(
1467            width,
1468            text_input_height.max(text_input_size.height),
1469            text_input_size,
1470        );
1471        let input_node = layout::Node::with_children(input_bounds, node_list).translate(text_pos);
1472        let y_pos = input_node.bounds().y + input_node.bounds().height + f32::from(spacing);
1473        nodes.push(input_node);
1474
1475        Vector::new(0.0, y_pos)
1476    } else {
1477        let limits = limits
1478            .width(width)
1479            .height(text_input_height + padding.y())
1480            .shrink(padding);
1481        let text_bounds = limits.resolve(Length::Shrink, Length::Shrink, Size::INFINITE);
1482
1483        let text = layout::Node::new(text_bounds).move_to(Point::new(padding.left, padding.top));
1484
1485        let node = layout::Node::with_children(text_bounds.expand(padding), vec![text])
1486            .translate(text_pos);
1487        let y_pos = node.bounds().y + node.bounds().height + f32::from(spacing);
1488
1489        nodes.push(node);
1490
1491        Vector::new(0.0, y_pos)
1492    };
1493
1494    if let Some(helper_text) = helper_text {
1495        let limits = limits
1496            .width(width)
1497            .shrink(padding)
1498            .height(helper_text_line_height.to_absolute(helper_text_size.into()));
1499        let text_bounds = limits.resolve(width, Length::Shrink, Size::INFINITE);
1500        let state = tree.state.downcast_mut::<State>();
1501        let helper_text_paragraph = &mut state.helper_text;
1502        helper_text_paragraph.update(Text {
1503            content: helper_text,
1504            font,
1505            bounds: text_bounds,
1506            size: iced::Pixels(helper_text_size),
1507            align_x: text::Alignment::Left,
1508            align_y: alignment::Vertical::Center,
1509            line_height: helper_text_line_height,
1510            shaping: text::Shaping::Advanced,
1511            wrapping: text::Wrapping::None,
1512            ellipsize: text::Ellipsize::None,
1513        });
1514        let helper_text_size = helper_text_paragraph.min_bounds();
1515        let helper_text_node = layout::Node::new(helper_text_size).translate(helper_pos);
1516        nodes.push(helper_text_node);
1517    };
1518
1519    let mut size = nodes.iter().fold(Size::ZERO, |size, node| {
1520        Size::new(
1521            size.width.max(node.bounds().width),
1522            size.height + node.bounds().height,
1523        )
1524    });
1525    size.height += (nodes.len() - 1) as f32 * f32::from(spacing);
1526
1527    let limits = limits
1528        .width(width)
1529        .height(size.height)
1530        .min_width(size.width);
1531
1532    layout::Node::with_children(limits.resolve(width, size.height, size), nodes)
1533}
1534
1535// TODO: Merge into widget method since iced has done the same.
1536/// Processes an [`Event`] and updates the [`State`] of a [`TextInput`]
1537/// accordingly.
1538#[allow(clippy::too_many_arguments)]
1539#[allow(clippy::too_many_lines)]
1540#[allow(clippy::missing_panics_doc)]
1541#[allow(clippy::cast_lossless)]
1542#[allow(clippy::cast_possible_truncation)]
1543pub fn update<'a, Message: Clone + 'static>(
1544    id: Option<Id>,
1545    event: &Event,
1546    text_layout: Layout<'_>,
1547    edit_button_layout: Option<Layout<'_>>,
1548    cursor: mouse::Cursor,
1549    clipboard: &mut dyn Clipboard,
1550    shell: &mut Shell<'_, Message>,
1551    value: &mut Value,
1552    size: f32,
1553    font: <crate::Renderer as iced_core::text::Renderer>::Font,
1554    is_editable_variant: bool,
1555    is_secure: bool,
1556    on_focus: Option<&Message>,
1557    on_unfocus: Option<&Message>,
1558    on_input: Option<&dyn Fn(String) -> Message>,
1559    on_paste: Option<&dyn Fn(String) -> Message>,
1560    on_submit: Option<&dyn Fn(String) -> Message>,
1561    on_tab: Option<&Message>,
1562    on_toggle_edit: Option<&dyn Fn(bool) -> Message>,
1563    state: impl FnOnce() -> &'a mut State,
1564    #[allow(unused_variables)] on_start_dnd_source: Option<&dyn Fn(State) -> Message>,
1565    #[allow(unused_variables)] dnd_id: u128,
1566    line_height: text::LineHeight,
1567    layout: Layout<'_>,
1568    manage_value: bool,
1569    drag_threshold: f32,
1570    always_active: bool,
1571) {
1572    let update_cache = |state, value| {
1573        replace_paragraph(
1574            state,
1575            layout,
1576            value,
1577            font,
1578            iced::Pixels(size),
1579            line_height,
1580            &Limits::NONE.max_width(text_layout.bounds().width),
1581        );
1582    };
1583
1584    let mut secured_value = if is_secure {
1585        value.secure()
1586    } else {
1587        value.clone()
1588    };
1589    let unsecured_value = value;
1590    let value = &mut secured_value;
1591
1592    // NOTE: Clicks must be captured to prevent mouse areas behind them handling the same clicks.
1593
1594    /// Mark a branch as cold
1595    #[inline]
1596    #[cold]
1597    fn cold() {}
1598
1599    let state = state();
1600
1601    // Any click outside clears focus and selection.
1602    if matches!(
1603        event,
1604        Event::Mouse(mouse::Event::ButtonPressed(_))
1605            | Event::Touch(touch::Event::FingerPressed { .. })
1606    ) && cursor.position_over(layout.bounds()).is_none()
1607    {
1608        state.is_focused = None;
1609        state.context_menu_position = None;
1610        state.dragging_state = None;
1611        if let Some(on_unfocus) = on_unfocus {
1612            shell.publish(on_unfocus.clone());
1613        }
1614        return;
1615    }
1616
1617    match event {
1618        Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) => {
1619            if let Some(pos) = cursor.position_over(layout.bounds()) {
1620                if !state.is_focused() {
1621                    state.focus();
1622                }
1623                state.context_menu_position = Some(pos);
1624                state.clipboard_has_text = iced_core::widget::text::clipboard_has_text(clipboard);
1625                shell.capture_event();
1626                return;
1627            }
1628        }
1629
1630        Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
1631        | Event::Touch(touch::Event::FingerPressed { .. }) => {
1632            cold();
1633
1634            if state.context_menu_position.take().is_some() {
1635                shell.capture_event();
1636                return;
1637            }
1638
1639            let click_position = if on_input.is_some() || manage_value {
1640                cursor.position_over(layout.bounds())
1641            } else {
1642                None
1643            };
1644
1645            if let Some(cursor_position) = click_position {
1646                // Check if the edit button was clicked.
1647                if state.dragging_state.is_none()
1648                    && edit_button_layout.is_some_and(|l| cursor.is_over(l.bounds()))
1649                {
1650                    if is_editable_variant {
1651                        let has_content = !unsecured_value.is_empty();
1652                        let is_editing = !state.is_read_only;
1653
1654                        if is_editing && has_content {
1655                            if let Some(on_input) = on_input {
1656                                shell.publish((on_input)(String::new()));
1657                            }
1658
1659                            if manage_value {
1660                                *unsecured_value = Value::new("");
1661                                state.tracked_value = unsecured_value.clone();
1662
1663                                let cleared_value = if is_secure {
1664                                    unsecured_value.secure()
1665                                } else {
1666                                    unsecured_value.clone()
1667                                };
1668
1669                                update_cache(state, &cleared_value);
1670                            }
1671
1672                            state.move_cursor_to_end();
1673                        } else if is_editing {
1674                            // Close: toggle back to read-only and unfocus.
1675                            state.is_read_only = true;
1676                            state.unfocus();
1677
1678                            if let Some(on_toggle_edit) = on_toggle_edit {
1679                                shell.publish(on_toggle_edit(false));
1680                            }
1681                        } else {
1682                            // Edit: toggle to editing, select all, and focus.
1683                            state.is_read_only = false;
1684                            state.cursor.select_range(0, value.len());
1685
1686                            if let Some(on_toggle_edit) = on_toggle_edit {
1687                                shell.publish(on_toggle_edit(true));
1688                            }
1689
1690                            let now = Instant::now();
1691                            LAST_FOCUS_UPDATE.with(|x| x.set(now));
1692                            state.is_focused = Some(Focus {
1693                                updated_at: now,
1694                                now,
1695                                focused: true,
1696                                needs_update: false,
1697                            });
1698                        }
1699                    }
1700
1701                    shell.capture_event();
1702                    return;
1703                }
1704
1705                let target = {
1706                    let text_bounds = text_layout.bounds();
1707
1708                    let alignment_offset = alignment_offset(
1709                        text_bounds.width,
1710                        state.value.raw().min_width(),
1711                        effective_alignment(state.value.raw()),
1712                    );
1713
1714                    cursor_position.x - text_bounds.x - alignment_offset
1715                };
1716
1717                let click =
1718                    mouse::Click::new(cursor_position, mouse::Button::Left, state.last_click);
1719
1720                match (
1721                    &state.dragging_state,
1722                    click.kind(),
1723                    state.cursor().state(value),
1724                ) {
1725                    #[cfg(wayland_platform)]
1726                    (None, click::Kind::Single, cursor::State::Selection { start, end }) => {
1727                        let left = start.min(end);
1728                        let right = end.max(start);
1729
1730                        let (left_position, _left_offset) = measure_cursor_and_scroll_offset(
1731                            state.value.raw(),
1732                            text_layout.bounds(),
1733                            left,
1734                            value,
1735                            state.cursor.affinity(),
1736                            state.scroll_offset,
1737                        );
1738
1739                        let (right_position, _right_offset) = measure_cursor_and_scroll_offset(
1740                            state.value.raw(),
1741                            text_layout.bounds(),
1742                            right,
1743                            value,
1744                            state.cursor.affinity(),
1745                            state.scroll_offset,
1746                        );
1747
1748                        let selection_start = left_position.min(right_position);
1749                        let width = (right_position - left_position).abs();
1750                        let alignment_offset = alignment_offset(
1751                            text_layout.bounds().width,
1752                            state.value.raw().min_width(),
1753                            effective_alignment(state.value.raw()),
1754                        );
1755                        let selection_bounds = Rectangle {
1756                            x: text_layout.bounds().x + alignment_offset + selection_start
1757                                - state.scroll_offset,
1758                            y: text_layout.bounds().y,
1759                            width,
1760                            height: text_layout.bounds().height,
1761                        };
1762
1763                        if cursor.is_over(selection_bounds) && (on_input.is_some() || manage_value)
1764                        {
1765                            state.dragging_state = Some(DraggingState::PrepareDnd(cursor_position));
1766                            shell.capture_event();
1767                            return;
1768                        }
1769                        // clear selection and place cursor at click position
1770                        update_cache(state, value);
1771                        state.setting_selection(value, text_layout.bounds(), target);
1772                        state.dragging_state = None;
1773                        shell.capture_event();
1774                        return;
1775                    }
1776                    (None, click::Kind::Single, _) => {
1777                        state.setting_selection(value, text_layout.bounds(), target);
1778                    }
1779                    (None | Some(DraggingState::Selection), click::Kind::Double, _) => {
1780                        update_cache(state, value);
1781
1782                        if is_secure {
1783                            state.cursor.select_all(value);
1784                        } else {
1785                            let (position, affinity) =
1786                                find_cursor_position(text_layout.bounds(), value, state, target)
1787                                    .unwrap_or((0, text::Affinity::Before));
1788
1789                            state.cursor.set_affinity(affinity);
1790
1791                            if let Some(delimiter) = state.double_click_select_delimiter {
1792                                if let Some(delim_pos) = value.rfind_char(delimiter) {
1793                                    if position <= delim_pos {
1794                                        state.cursor.select_range(0, delim_pos);
1795                                    } else {
1796                                        state.cursor.select_range(delim_pos + 1, value.len());
1797                                    }
1798                                } else {
1799                                    state.cursor.select_all(value);
1800                                }
1801                            } else {
1802                                state.cursor.select_range(
1803                                    value.previous_start_of_word(position),
1804                                    value.next_end_of_word(position),
1805                                );
1806                            }
1807                        }
1808                        state.dragging_state = Some(DraggingState::Selection);
1809                    }
1810                    (None | Some(DraggingState::Selection), click::Kind::Triple, _) => {
1811                        update_cache(state, value);
1812                        state.cursor.select_all(value);
1813                        state.dragging_state = Some(DraggingState::Selection);
1814                    }
1815                    _ => {
1816                        state.dragging_state = None;
1817                    }
1818                }
1819
1820                // Focus on click of the text input, and ensure that the input is writable.
1821                if matches!(state.dragging_state, None | Some(DraggingState::Selection))
1822                    && (!state.is_focused() || (is_editable_variant && state.is_read_only))
1823                {
1824                    if !state.is_focused() {
1825                        if let Some(on_focus) = on_focus {
1826                            shell.publish(on_focus.clone());
1827                        }
1828                    }
1829
1830                    if state.is_read_only {
1831                        state.is_read_only = false;
1832                        state.cursor.select_range(0, value.len());
1833                        if let Some(on_toggle_edit) = on_toggle_edit {
1834                            let message = (on_toggle_edit)(true);
1835                            shell.publish(message);
1836                        }
1837                    }
1838
1839                    let now = Instant::now();
1840                    LAST_FOCUS_UPDATE.with(|x| x.set(now));
1841
1842                    state.is_focused = Some(Focus {
1843                        updated_at: now,
1844                        now,
1845                        focused: true,
1846                        needs_update: false,
1847                    });
1848                }
1849
1850                state.last_click = Some(click);
1851
1852                shell.request_redraw();
1853                shell.capture_event();
1854                return;
1855            } else {
1856                state.unfocus();
1857
1858                if let Some(on_unfocus) = on_unfocus {
1859                    shell.publish(on_unfocus.clone());
1860                }
1861            }
1862        }
1863        Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
1864        | Event::Touch(touch::Event::FingerLifted { .. } | touch::Event::FingerLost { .. }) => {
1865            cold();
1866            #[cfg(wayland_platform)]
1867            if matches!(state.dragging_state, Some(DraggingState::PrepareDnd(_))) {
1868                // clear selection and place cursor at click position
1869                update_cache(state, value);
1870                if let Some(position) = cursor.position_over(layout.bounds()) {
1871                    let target = {
1872                        let text_bounds = text_layout.bounds();
1873
1874                        let alignment_offset = alignment_offset(
1875                            text_bounds.width,
1876                            state.value.raw().min_width(),
1877                            effective_alignment(state.value.raw()),
1878                        );
1879
1880                        position.x - text_bounds.x - alignment_offset
1881                    };
1882                    state.setting_selection(value, text_layout.bounds(), target);
1883                }
1884            }
1885            state.dragging_state = None;
1886            if cursor.is_over(layout.bounds()) {
1887                shell.capture_event();
1888            }
1889            return;
1890        }
1891        Event::Mouse(mouse::Event::CursorMoved { position })
1892        | Event::Touch(touch::Event::FingerMoved { position, .. }) => {
1893            if matches!(state.dragging_state, Some(DraggingState::Selection)) {
1894                let target = {
1895                    let text_bounds = text_layout.bounds();
1896
1897                    let alignment_offset = alignment_offset(
1898                        text_bounds.width,
1899                        state.value.raw().min_width(),
1900                        effective_alignment(state.value.raw()),
1901                    );
1902
1903                    position.x - text_bounds.x - alignment_offset
1904                };
1905
1906                update_cache(state, value);
1907                let (position, affinity) =
1908                    find_cursor_position(text_layout.bounds(), value, state, target)
1909                        .unwrap_or((0, text::Affinity::Before));
1910
1911                state.cursor.set_affinity(affinity);
1912                state
1913                    .cursor
1914                    .select_range(state.cursor.start(value), position);
1915
1916                shell.request_redraw();
1917                shell.capture_event();
1918                return;
1919            }
1920            #[cfg(wayland_platform)]
1921            if let Some(DraggingState::PrepareDnd(start_position)) = state.dragging_state {
1922                let distance = ((position.x - start_position.x).powi(2)
1923                    + (position.y - start_position.y).powi(2))
1924                .sqrt();
1925
1926                if distance >= drag_threshold {
1927                    if is_secure {
1928                        return;
1929                    }
1930
1931                    let input_text = state.selected_text(&value.to_string()).unwrap_or_default();
1932                    state.dragging_state =
1933                        Some(DraggingState::Dnd(DndAction::empty(), input_text.clone()));
1934                    let mut editor = Editor::new(unsecured_value, &mut state.cursor);
1935                    editor.delete();
1936
1937                    let contents = editor.contents();
1938                    let unsecured_value = Value::new(&contents);
1939                    state.tracked_value = unsecured_value.clone();
1940                    if let Some(on_input) = on_input {
1941                        let message = (on_input)(contents);
1942                        shell.publish(message);
1943                    }
1944                    if let Some(on_start_dnd) = on_start_dnd_source {
1945                        shell.publish(on_start_dnd(state.clone()));
1946                    }
1947                    let state_clone = state.clone();
1948
1949                    iced_core::clipboard::start_dnd(
1950                        clipboard,
1951                        false,
1952                        id.map(iced_core::clipboard::DndSource::Widget),
1953                        Some(iced_core::clipboard::IconSurface::new(
1954                            Element::from(
1955                                TextInput::<'static, ()>::new("", input_text.clone())
1956                                    .dnd_icon(true),
1957                            ),
1958                            iced_core::widget::tree::State::new(state_clone),
1959                            Vector::ZERO,
1960                        )),
1961                        Box::new(TextInputString(input_text)),
1962                        DndAction::Move,
1963                    );
1964
1965                    update_cache(state, &unsecured_value);
1966                } else {
1967                    state.dragging_state = Some(DraggingState::PrepareDnd(start_position));
1968                }
1969
1970                shell.capture_event();
1971                return;
1972            }
1973        }
1974        Event::Keyboard(keyboard::Event::KeyPressed {
1975            key,
1976            text,
1977            physical_key,
1978            modifiers,
1979            ..
1980        }) => {
1981            state.keyboard_modifiers = *modifiers;
1982
1983            if let Some(focus) = state.is_focused.as_mut().filter(|f| f.focused) {
1984                if state.is_read_only || (!manage_value && on_input.is_none()) {
1985                    return;
1986                };
1987                let modifiers = state.keyboard_modifiers;
1988                focus.updated_at = Instant::now();
1989                LAST_FOCUS_UPDATE.with(|x| x.set(focus.updated_at));
1990
1991                // Ctrl/Command+A/C/V/X, plus the traditional alternate clipboard
1992                let clip_key = match key.as_ref() {
1993                    keyboard::Key::Named(keyboard::key::Named::Insert) if modifiers.shift() => {
1994                        Some('v')
1995                    }
1996                    keyboard::Key::Named(keyboard::key::Named::Insert) if modifiers.command() => {
1997                        Some('c')
1998                    }
1999                    keyboard::Key::Named(keyboard::key::Named::Delete) if modifiers.shift() => {
2000                        Some('x')
2001                    }
2002                    _ if modifiers.command() => key.to_latin(*physical_key),
2003                    _ => None,
2004                };
2005                {
2006                    match clip_key {
2007                        Some('c') => {
2008                            if !is_secure {
2009                                if let Some((start, end)) = state.cursor.selection(value) {
2010                                    clipboard.write(
2011                                        iced_core::clipboard::Kind::Standard,
2012                                        value.select(start, end).to_string(),
2013                                    );
2014                                }
2015                            }
2016                        }
2017                        // XXX if we want to allow cutting of secure text, we need to
2018                        // update the cache and decide which value to cut
2019                        Some('x') => {
2020                            if !is_secure {
2021                                if let Some((start, end)) = state.cursor.selection(value) {
2022                                    clipboard.write(
2023                                        iced_core::clipboard::Kind::Standard,
2024                                        value.select(start, end).to_string(),
2025                                    );
2026                                }
2027
2028                                let mut editor = Editor::new(value, &mut state.cursor);
2029                                editor.delete();
2030                                let content = editor.contents();
2031                                state.tracked_value = Value::new(&content);
2032                                if let Some(on_input) = on_input {
2033                                    let message = (on_input)(content);
2034                                    shell.publish(message);
2035                                }
2036                            }
2037                        }
2038                        Some('v') => {
2039                            let content = if let Some(content) = state.is_pasting.take() {
2040                                content
2041                            } else {
2042                                let content: String = clipboard
2043                                    .read(iced_core::clipboard::Kind::Standard)
2044                                    .unwrap_or_default()
2045                                    .chars()
2046                                    .filter(|c| !c.is_control())
2047                                    .collect();
2048
2049                                Value::new(&content)
2050                            };
2051
2052                            let mut editor = Editor::new(unsecured_value, &mut state.cursor);
2053
2054                            editor.paste(content.clone());
2055
2056                            let contents = editor.contents();
2057                            let unsecured_value = Value::new(&contents);
2058                            state.tracked_value = unsecured_value.clone();
2059
2060                            if let Some(on_input) = on_input {
2061                                let message = if let Some(paste) = &on_paste {
2062                                    (paste)(contents)
2063                                } else {
2064                                    (on_input)(contents)
2065                                };
2066
2067                                shell.publish(message);
2068                            }
2069
2070                            state.is_pasting = Some(content);
2071
2072                            let value = if is_secure {
2073                                unsecured_value.secure()
2074                            } else {
2075                                unsecured_value
2076                            };
2077
2078                            update_cache(state, &value);
2079                            shell.capture_event();
2080                            return;
2081                        }
2082
2083                        Some('a') => {
2084                            state.cursor.select_all(value);
2085                            shell.capture_event();
2086                            return;
2087                        }
2088
2089                        _ => {}
2090                    }
2091                }
2092
2093                // Capture keyboard inputs that should be submitted.
2094                if let Some(c) = text
2095                    .as_ref()
2096                    .and_then(|t| t.chars().next().filter(|c| !c.is_control()))
2097                {
2098                    if state.is_read_only || (!manage_value && on_input.is_none()) {
2099                        return;
2100                    };
2101
2102                    state.is_pasting = None;
2103
2104                    if !state.keyboard_modifiers.command() && !modifiers.control() {
2105                        let mut editor = Editor::new(unsecured_value, &mut state.cursor);
2106
2107                        editor.insert(c);
2108
2109                        let contents = editor.contents();
2110                        let unsecured_value = Value::new(&contents);
2111                        state.tracked_value = unsecured_value.clone();
2112
2113                        if let Some(on_input) = on_input {
2114                            let message = (on_input)(contents);
2115                            shell.publish(message);
2116                        }
2117
2118                        focus.updated_at = Instant::now();
2119                        LAST_FOCUS_UPDATE.with(|x| x.set(focus.updated_at));
2120
2121                        let value = if is_secure {
2122                            unsecured_value.secure()
2123                        } else {
2124                            unsecured_value
2125                        };
2126
2127                        update_cache(state, &value);
2128
2129                        shell.capture_event();
2130                        return;
2131                    }
2132                }
2133
2134                match key.as_ref() {
2135                    keyboard::Key::Named(keyboard::key::Named::Enter) => {
2136                        if let Some(on_submit) = on_submit {
2137                            shell.publish((on_submit)(unsecured_value.to_string()));
2138                        }
2139                    }
2140                    keyboard::Key::Named(keyboard::key::Named::Backspace) => {
2141                        if platform::is_jump_modifier_pressed(modifiers)
2142                            && state.cursor.selection(value).is_none()
2143                        {
2144                            if is_secure {
2145                                let cursor_pos = state.cursor.end(value);
2146                                state.cursor.select_range(0, cursor_pos);
2147                            } else {
2148                                state.cursor.select_left_by_words(value);
2149                            }
2150                        }
2151
2152                        let mut editor = Editor::new(unsecured_value, &mut state.cursor);
2153                        editor.backspace();
2154
2155                        let contents = editor.contents();
2156                        let unsecured_value = Value::new(&contents);
2157                        state.tracked_value = unsecured_value.clone();
2158                        if let Some(on_input) = on_input {
2159                            let message = (on_input)(editor.contents());
2160                            shell.publish(message);
2161                        }
2162                        let value = if is_secure {
2163                            unsecured_value.secure()
2164                        } else {
2165                            unsecured_value
2166                        };
2167                        update_cache(state, &value);
2168                    }
2169                    keyboard::Key::Named(keyboard::key::Named::Delete) => {
2170                        if platform::is_jump_modifier_pressed(modifiers)
2171                            && state.cursor.selection(value).is_none()
2172                        {
2173                            if is_secure {
2174                                let cursor_pos = state.cursor.end(unsecured_value);
2175                                state.cursor.select_range(cursor_pos, unsecured_value.len());
2176                            } else {
2177                                state.cursor.select_right_by_words(unsecured_value);
2178                            }
2179                        }
2180
2181                        let mut editor = Editor::new(unsecured_value, &mut state.cursor);
2182                        editor.delete();
2183                        let contents = editor.contents();
2184                        let unsecured_value = Value::new(&contents);
2185                        if let Some(on_input) = on_input {
2186                            let message = (on_input)(contents);
2187                            state.tracked_value = unsecured_value.clone();
2188                            shell.publish(message);
2189                        }
2190
2191                        let value = if is_secure {
2192                            unsecured_value.secure()
2193                        } else {
2194                            unsecured_value
2195                        };
2196
2197                        update_cache(state, &value);
2198                    }
2199                    keyboard::Key::Named(keyboard::key::Named::ArrowLeft) => {
2200                        let rtl = state.value.raw().is_rtl(0).unwrap_or(false);
2201                        let by_words = platform::is_jump_modifier_pressed(modifiers) && !is_secure;
2202
2203                        if modifiers.shift() {
2204                            state.cursor.select_visual(false, by_words, rtl, value);
2205                        } else {
2206                            state.cursor.move_visual(false, by_words, rtl, value);
2207                        }
2208                    }
2209                    keyboard::Key::Named(keyboard::key::Named::ArrowRight) => {
2210                        let rtl = state.value.raw().is_rtl(0).unwrap_or(false);
2211                        let by_words = platform::is_jump_modifier_pressed(modifiers) && !is_secure;
2212
2213                        if modifiers.shift() {
2214                            state.cursor.select_visual(true, by_words, rtl, value);
2215                        } else {
2216                            state.cursor.move_visual(true, by_words, rtl, value);
2217                        }
2218                    }
2219                    keyboard::Key::Named(keyboard::key::Named::Home) => {
2220                        if modifiers.shift() {
2221                            state.cursor.select_range(state.cursor.start(value), 0);
2222                        } else {
2223                            state.cursor.move_to(0);
2224                        }
2225                    }
2226                    keyboard::Key::Named(keyboard::key::Named::End) => {
2227                        if modifiers.shift() {
2228                            state
2229                                .cursor
2230                                .select_range(state.cursor.start(value), value.len());
2231                        } else {
2232                            state.cursor.move_to(value.len());
2233                        }
2234                    }
2235                    keyboard::Key::Named(keyboard::key::Named::Escape) => {
2236                        state.unfocus();
2237                        state.is_read_only = true;
2238
2239                        if let Some(on_unfocus) = on_unfocus {
2240                            shell.publish(on_unfocus.clone());
2241                        }
2242
2243                        shell.request_redraw();
2244                        return;
2245                    }
2246
2247                    keyboard::Key::Named(keyboard::key::Named::Tab) => {
2248                        if let Some(on_tab) = on_tab {
2249                            // Allow the application to decide how the event is handled.
2250                            // This could be to connect the text input to another text input.
2251                            // Or to connect the text input to a button.
2252                            shell.publish(on_tab.clone());
2253                        } else {
2254                            state.is_read_only = true;
2255
2256                            if let Some(on_unfocus) = on_unfocus {
2257                                shell.publish(on_unfocus.clone());
2258                            }
2259
2260                            return;
2261                        };
2262                    }
2263
2264                    keyboard::Key::Named(
2265                        keyboard::key::Named::ArrowUp | keyboard::key::Named::ArrowDown,
2266                    ) => {
2267                        return;
2268                    }
2269                    _ => {}
2270                }
2271
2272                shell.request_redraw();
2273                shell.capture_event();
2274                return;
2275            }
2276        }
2277        Event::Keyboard(keyboard::Event::KeyReleased { key, .. }) => {
2278            if state.is_focused() {
2279                match key {
2280                    keyboard::Key::Character(c) if "v" == c => {
2281                        state.is_pasting = None;
2282                    }
2283                    keyboard::Key::Named(keyboard::key::Named::Insert) => {
2284                        state.is_pasting = None;
2285                    }
2286                    keyboard::Key::Named(keyboard::key::Named::Tab)
2287                    | keyboard::Key::Named(keyboard::key::Named::ArrowUp)
2288                    | keyboard::Key::Named(keyboard::key::Named::ArrowDown) => {
2289                        return;
2290                    }
2291                    _ => {}
2292                }
2293
2294                shell.capture_event();
2295                return;
2296            }
2297        }
2298        Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
2299            state.keyboard_modifiers = *modifiers;
2300        }
2301        Event::InputMethod(event) => match event {
2302            input_method::Event::Opened | input_method::Event::Closed => {
2303                state.preedit =
2304                    matches!(event, input_method::Event::Opened).then(input_method::Preedit::new);
2305                shell.capture_event();
2306                return;
2307            }
2308            input_method::Event::Preedit(content, selection) => {
2309                if state.is_focused() {
2310                    state.preedit = Some(input_method::Preedit {
2311                        content: content.to_owned(),
2312                        selection: selection.clone(),
2313                        text_size: Some(size.into()),
2314                    });
2315                    shell.capture_event();
2316                    return;
2317                }
2318            }
2319            input_method::Event::Commit(text) => {
2320                let Some(focus) = state.is_focused.as_mut().filter(|f| f.focused) else {
2321                    return;
2322                };
2323                let Some(on_input) = on_input else {
2324                    return;
2325                };
2326                if state.is_read_only {
2327                    return;
2328                }
2329
2330                focus.updated_at = Instant::now();
2331                LAST_FOCUS_UPDATE.with(|x| x.set(focus.updated_at));
2332
2333                let mut editor = Editor::new(unsecured_value, &mut state.cursor);
2334                editor.paste(Value::new(&text));
2335
2336                let contents = editor.contents();
2337                let unsecured_value = Value::new(&contents);
2338                let message = if let Some(paste) = &on_paste {
2339                    (paste)(contents)
2340                } else {
2341                    (on_input)(contents)
2342                };
2343                shell.publish(message);
2344
2345                state.is_pasting = None;
2346                let value = if is_secure {
2347                    unsecured_value.secure()
2348                } else {
2349                    unsecured_value
2350                };
2351
2352                update_cache(state, &value);
2353                shell.capture_event();
2354                return;
2355            }
2356        },
2357        Event::Window(window::Event::RedrawRequested(now)) => {
2358            if let Some(focus) = state.is_focused.as_mut().filter(|f| f.focused) {
2359                focus.now = *now;
2360
2361                let millis_until_redraw = CURSOR_BLINK_INTERVAL_MILLIS
2362                    - (*now - focus.updated_at).as_millis() % CURSOR_BLINK_INTERVAL_MILLIS;
2363                shell.request_redraw_at(window::RedrawRequest::At(
2364                    now.checked_add(Duration::from_millis(millis_until_redraw as u64))
2365                        .unwrap_or(*now),
2366                ));
2367
2368                shell.request_input_method(&input_method(state, text_layout, unsecured_value));
2369            } else if always_active {
2370                shell.request_redraw();
2371            }
2372        }
2373        #[cfg(wayland_platform)]
2374        Event::Dnd(DndEvent::Source(SourceEvent::Finished | SourceEvent::Cancelled)) => {
2375            cold();
2376            if matches!(state.dragging_state, Some(DraggingState::Dnd(..))) {
2377                // TODO: restore value in text input
2378                state.dragging_state = None;
2379                shell.capture_event();
2380                return;
2381            }
2382        }
2383        #[cfg(wayland_platform)]
2384        Event::Dnd(DndEvent::Offer(
2385            rectangle,
2386            OfferEvent::Enter {
2387                x,
2388                y,
2389                mime_types,
2390                surface,
2391            },
2392        )) if *rectangle == Some(dnd_id) => {
2393            cold();
2394            let is_clicked = text_layout.bounds().contains(Point {
2395                x: *x as f32,
2396                y: *y as f32,
2397            });
2398
2399            let mut accepted = false;
2400            for m in mime_types {
2401                if SUPPORTED_TEXT_MIME_TYPES.contains(&m.as_str()) {
2402                    let clone = m.clone();
2403                    accepted = true;
2404                }
2405            }
2406            if accepted {
2407                let target = {
2408                    let text_bounds = text_layout.bounds();
2409
2410                    let alignment_offset = alignment_offset(
2411                        text_bounds.width,
2412                        state.value.raw().min_width(),
2413                        effective_alignment(state.value.raw()),
2414                    );
2415
2416                    *x as f32 - text_bounds.x - alignment_offset
2417                };
2418                state.dnd_offer =
2419                    DndOfferState::HandlingOffer(mime_types.clone(), DndAction::empty());
2420                // existing logic for setting the selection
2421                update_cache(state, value);
2422                let (position, affinity) =
2423                    find_cursor_position(text_layout.bounds(), value, state, target)
2424                        .unwrap_or((0, text::Affinity::Before));
2425
2426                state.cursor.set_affinity(affinity);
2427                state.cursor.move_to(position);
2428                shell.capture_event();
2429                return;
2430            }
2431        }
2432        #[cfg(wayland_platform)]
2433        Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Motion { x, y }))
2434            if *rectangle == Some(dnd_id) =>
2435        {
2436            let target = {
2437                let text_bounds = text_layout.bounds();
2438
2439                let alignment_offset = alignment_offset(
2440                    text_bounds.width,
2441                    state.value.raw().min_width(),
2442                    effective_alignment(state.value.raw()),
2443                );
2444
2445                *x as f32 - text_bounds.x - alignment_offset
2446            };
2447            // existing logic for setting the selection
2448            update_cache(state, value);
2449            let (position, affinity) =
2450                find_cursor_position(text_layout.bounds(), value, state, target)
2451                    .unwrap_or((0, text::Affinity::Before));
2452
2453            state.cursor.set_affinity(affinity);
2454            state.cursor.move_to(position);
2455            shell.capture_event();
2456            return;
2457        }
2458        #[cfg(wayland_platform)]
2459        Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Drop)) if *rectangle == Some(dnd_id) => {
2460            cold();
2461            if let DndOfferState::HandlingOffer(mime_types, _action) = state.dnd_offer.clone() {
2462                let Some(mime_type) = SUPPORTED_TEXT_MIME_TYPES
2463                    .iter()
2464                    .find(|&&m| mime_types.iter().any(|t| t == m))
2465                else {
2466                    state.dnd_offer = DndOfferState::None;
2467                    shell.capture_event();
2468                    return;
2469                };
2470                state.dnd_offer = DndOfferState::Dropped;
2471            }
2472
2473            return;
2474        }
2475        #[cfg(wayland_platform)]
2476        Event::Dnd(DndEvent::Offer(id, OfferEvent::LeaveDestination)) if Some(dnd_id) != *id => {}
2477        #[cfg(wayland_platform)]
2478        Event::Dnd(DndEvent::Offer(
2479            rectangle,
2480            OfferEvent::Leave | OfferEvent::LeaveDestination,
2481        )) => {
2482            cold();
2483            // ASHLEY TODO we should be able to reset but for now we don't if we are handling a
2484            // drop
2485            match state.dnd_offer {
2486                DndOfferState::Dropped => {}
2487                _ => {
2488                    state.dnd_offer = DndOfferState::None;
2489                }
2490            };
2491            shell.capture_event();
2492            return;
2493        }
2494        #[cfg(wayland_platform)]
2495        Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Data { data, mime_type }))
2496            if *rectangle == Some(dnd_id) =>
2497        {
2498            cold();
2499            if matches!(&state.dnd_offer, DndOfferState::Dropped) {
2500                state.dnd_offer = DndOfferState::None;
2501                if !SUPPORTED_TEXT_MIME_TYPES.contains(&mime_type.as_str()) || data.is_empty() {
2502                    shell.capture_event();
2503                    return;
2504                }
2505                let Ok(content) = String::from_utf8(data.clone()) else {
2506                    shell.capture_event();
2507                    return;
2508                };
2509
2510                let mut editor = Editor::new(unsecured_value, &mut state.cursor);
2511
2512                editor.paste(Value::new(content.as_str()));
2513                let contents = editor.contents();
2514                let unsecured_value = Value::new(&contents);
2515                state.tracked_value = unsecured_value.clone();
2516                if let Some(on_paste) = on_paste.as_ref() {
2517                    let message = (on_paste)(contents);
2518                    shell.publish(message);
2519                }
2520
2521                let value = if is_secure {
2522                    unsecured_value.secure()
2523                } else {
2524                    unsecured_value
2525                };
2526                update_cache(state, &value);
2527                shell.capture_event();
2528                return;
2529            }
2530            return;
2531        }
2532        _ => {}
2533    }
2534}
2535
2536fn input_method<'b>(
2537    state: &'b State,
2538    text_layout: Layout<'_>,
2539    value: &Value,
2540) -> InputMethod<&'b str> {
2541    if !state.is_focused() {
2542        return InputMethod::Disabled;
2543    };
2544
2545    let text_bounds = text_layout.bounds();
2546    let cursor_index = match state.cursor.state(value) {
2547        cursor::State::Index(position) => position,
2548        cursor::State::Selection { start, end } => start.min(end),
2549    };
2550    let (cursor, offset) = measure_cursor_and_scroll_offset(
2551        state.value.raw(),
2552        text_bounds,
2553        cursor_index,
2554        value,
2555        state.cursor.affinity(),
2556        state.scroll_offset,
2557    );
2558    InputMethod::Enabled {
2559        cursor: Rectangle::new(
2560            Point::new(text_bounds.x + cursor - offset, text_bounds.y),
2561            Size::new(1.0, text_bounds.height),
2562        ),
2563        purpose: if state.is_secure {
2564            input_method::Purpose::Secure
2565        } else {
2566            input_method::Purpose::Normal
2567        },
2568        preedit: state.preedit.as_ref().map(input_method::Preedit::as_ref),
2569    }
2570}
2571
2572/// Draws the [`TextInput`] with the given [`Renderer`], overriding its
2573/// [`Value`] if provided.
2574///
2575/// [`Renderer`]: text::Renderer
2576#[allow(clippy::too_many_arguments)]
2577#[allow(clippy::too_many_lines)]
2578#[allow(clippy::missing_panics_doc)]
2579pub fn draw<'a, Message>(
2580    renderer: &mut crate::Renderer,
2581    theme: &crate::Theme,
2582    layout: Layout<'_>,
2583    text_layout: Layout<'_>,
2584    cursor_position: mouse::Cursor,
2585    tree: &Tree,
2586    value: &Value,
2587    placeholder: &str,
2588    size: Option<f32>,
2589    font: Option<<crate::Renderer as iced_core::text::Renderer>::Font>,
2590    is_disabled: bool,
2591    is_secure: bool,
2592    icon: Option<&Element<'a, Message, crate::Theme, crate::Renderer>>,
2593    trailing_icon: Option<&Element<'a, Message, crate::Theme, crate::Renderer>>,
2594    style: &<crate::Theme as StyleSheet>::Style,
2595    dnd_icon: bool,
2596    line_height: text::LineHeight,
2597    error: Option<&str>,
2598    label: Option<&str>,
2599    helper_text: Option<&str>,
2600    helper_text_size: f32,
2601    helper_line_height: text::LineHeight,
2602    viewport: &Rectangle,
2603    renderer_style: &renderer::Style,
2604) {
2605    // all children should be icon images
2606    let children = &tree.children;
2607
2608    let state = tree.state.downcast_ref::<State>();
2609    let secure_value = is_secure.then(|| value.secure());
2610    let value = secure_value.as_ref().unwrap_or(value);
2611
2612    let mut children_layout = layout.children();
2613
2614    let (label_layout, layout, helper_text_layout) = if label.is_some() && helper_text.is_some() {
2615        let label_layout = children_layout.next();
2616        let layout = children_layout.next().unwrap();
2617        let helper_text_layout = children_layout.next();
2618        (label_layout, layout, helper_text_layout)
2619    } else if label.is_some() {
2620        let label_layout = children_layout.next();
2621        let layout = children_layout.next().unwrap();
2622        (label_layout, layout, None)
2623    } else if helper_text.is_some() {
2624        let layout = children_layout.next().unwrap();
2625        let helper_text_layout = children_layout.next();
2626        (None, layout, helper_text_layout)
2627    } else {
2628        let layout = children_layout.next().unwrap();
2629
2630        (None, layout, None)
2631    };
2632
2633    let mut children_layout = layout.children();
2634    let bounds = layout.bounds();
2635    // XXX Dnd widget may not have a layout with children, so we just use the text_layout
2636    let text_bounds = children_layout.next().unwrap_or(text_layout).bounds();
2637
2638    let is_mouse_over = cursor_position.is_over(bounds);
2639
2640    let appearance = if is_disabled {
2641        theme.disabled(style)
2642    } else if error.is_some() {
2643        theme.error(style)
2644    } else if state.is_focused() {
2645        theme.focused(style)
2646    } else if is_mouse_over {
2647        theme.hovered(style)
2648    } else {
2649        theme.active(style)
2650    };
2651
2652    let mut icon_color = appearance.icon_color.unwrap_or(renderer_style.icon_color);
2653    let mut text_color = appearance.text_color.unwrap_or(renderer_style.text_color);
2654
2655    // TODO: iced will not render alpha itself on text or icon colors.
2656    if is_disabled {
2657        let background = theme.current_container().component.base.into();
2658        icon_color = icon_color.blend_alpha(background, 0.5);
2659        text_color = text_color.blend_alpha(background, 0.5);
2660    }
2661
2662    // draw background and its border
2663    if let Some(border_offset) = appearance.border_offset {
2664        let offset_bounds = Rectangle {
2665            x: bounds.x - border_offset,
2666            y: bounds.y - border_offset,
2667            width: border_offset.mul_add(2.0, bounds.width),
2668            height: border_offset.mul_add(2.0, bounds.height),
2669        };
2670        renderer.fill_quad(
2671            renderer::Quad {
2672                bounds,
2673                border: Border {
2674                    radius: appearance.border_radius,
2675                    width: appearance.border_width,
2676                    ..Default::default()
2677                },
2678                shadow: Shadow {
2679                    offset: Vector::new(0.0, 1.0),
2680                    color: Color::TRANSPARENT,
2681                    blur_radius: 0.0,
2682                },
2683                snap: true,
2684            },
2685            appearance.background,
2686        );
2687        renderer.fill_quad(
2688            renderer::Quad {
2689                bounds: offset_bounds,
2690                border: Border {
2691                    width: appearance.border_width,
2692                    color: appearance.border_color,
2693                    radius: appearance.border_radius,
2694                },
2695                shadow: Shadow {
2696                    offset: Vector::new(0.0, 1.0),
2697                    color: Color::TRANSPARENT,
2698                    blur_radius: 0.0,
2699                },
2700                snap: true,
2701            },
2702            Background::Color(Color::TRANSPARENT),
2703        );
2704    } else {
2705        renderer.fill_quad(
2706            renderer::Quad {
2707                bounds,
2708                border: Border {
2709                    width: appearance.border_width,
2710                    color: appearance.border_color,
2711                    radius: appearance.border_radius,
2712                },
2713                shadow: Shadow {
2714                    offset: Vector::new(0.0, 1.0),
2715                    color: Color::TRANSPARENT,
2716                    blur_radius: 0.0,
2717                },
2718                snap: true,
2719            },
2720            appearance.background,
2721        );
2722    }
2723
2724    // draw the label if it exists
2725    if let (Some(label_layout), Some(label)) = (label_layout, label) {
2726        renderer.fill_text(
2727            Text {
2728                content: label.to_string(),
2729                size: iced::Pixels(size.unwrap_or_else(|| renderer.default_size().0)),
2730                font: font.unwrap_or_else(|| renderer.default_font()),
2731                bounds: label_layout.bounds().size(),
2732                align_x: text::Alignment::Left,
2733                align_y: alignment::Vertical::Top,
2734                line_height,
2735                shaping: text::Shaping::Advanced,
2736                wrapping: text::Wrapping::None,
2737                ellipsize: text::Ellipsize::None,
2738            },
2739            label_layout.bounds().position(),
2740            appearance.label_color,
2741            *viewport,
2742        );
2743    }
2744    let mut child_index = 0;
2745    let leading_icon_tree = children.get(child_index);
2746    // draw the start icon in the text input
2747    let has_start_icon = icon.is_some();
2748    if let (Some(icon), Some(tree)) = (icon, leading_icon_tree) {
2749        let mut children = text_layout.children();
2750        let _ = children.next().unwrap();
2751        let icon_layout = children.next().unwrap();
2752
2753        icon.as_widget().draw(
2754            tree,
2755            renderer,
2756            theme,
2757            &renderer::Style {
2758                icon_color,
2759                text_color,
2760                scale_factor: renderer_style.scale_factor,
2761            },
2762            icon_layout,
2763            cursor_position,
2764            viewport,
2765        );
2766        child_index += 1;
2767    }
2768
2769    let text = value.to_string();
2770    let font = font.unwrap_or_else(|| renderer.default_font());
2771    let size = size.unwrap_or_else(|| renderer.default_size().0);
2772    let text_width = state.value.min_width();
2773    let actual_width = text_width.max(text_bounds.width);
2774
2775    let radius_0 = THEME.lock().unwrap().cosmic().corner_radii.radius_0.into();
2776    #[cfg(wayland_platform)]
2777    let handling_dnd_offer = !matches!(state.dnd_offer, DndOfferState::None);
2778    #[cfg(not(wayland_platform))]
2779    let handling_dnd_offer = false;
2780    let (cursors, offset, is_selecting) = if let Some(focus) =
2781        state.is_focused.filter(|f| f.focused).or_else(|| {
2782            let now = Instant::now();
2783            handling_dnd_offer.then_some(Focus {
2784                needs_update: false,
2785                updated_at: now,
2786                now,
2787                focused: true,
2788            })
2789        }) {
2790        match state.cursor.state(value) {
2791            cursor::State::Index(position) => {
2792                let (text_value_width, _) = measure_cursor_and_scroll_offset(
2793                    state.value.raw(),
2794                    text_bounds,
2795                    position,
2796                    value,
2797                    state.cursor.affinity(),
2798                    state.scroll_offset,
2799                );
2800                let is_cursor_visible = handling_dnd_offer
2801                    || ((focus.now - focus.updated_at).as_millis() / CURSOR_BLINK_INTERVAL_MILLIS)
2802                        .is_multiple_of(2);
2803
2804                if is_cursor_visible && !dnd_icon {
2805                    (
2806                        vec![(
2807                            renderer::Quad {
2808                                bounds: Rectangle {
2809                                    x: (text_bounds.x + text_value_width).floor(),
2810                                    y: text_bounds.y,
2811                                    width: 1.0,
2812                                    height: text_bounds.height,
2813                                },
2814                                border: Border {
2815                                    width: 0.0,
2816                                    color: Color::TRANSPARENT,
2817                                    radius: radius_0,
2818                                },
2819                                shadow: Shadow {
2820                                    offset: Vector::ZERO,
2821                                    color: Color::TRANSPARENT,
2822                                    blur_radius: 0.0,
2823                                },
2824                                snap: true,
2825                            },
2826                            text_color,
2827                        )],
2828                        state.scroll_offset,
2829                        false,
2830                    )
2831                } else {
2832                    (
2833                        Vec::<(renderer::Quad, Color)>::new(),
2834                        if dnd_icon { 0.0 } else { state.scroll_offset },
2835                        false,
2836                    )
2837                }
2838            }
2839            cursor::State::Selection { start, end } => {
2840                let left = start.min(end);
2841                let right = end.max(start);
2842
2843                if dnd_icon {
2844                    (Vec::<(renderer::Quad, Color)>::new(), 0.0, true)
2845                } else {
2846                    let lo_byte = value.byte_index_at_grapheme(left);
2847                    let hi_byte = value.byte_index_at_grapheme(right);
2848
2849                    let rects = state.value.raw().highlight(
2850                        0,
2851                        (lo_byte, text::Affinity::After),
2852                        (hi_byte, text::Affinity::Before),
2853                    );
2854
2855                    let cursors: Vec<(renderer::Quad, Color)> = rects
2856                        .into_iter()
2857                        .map(|r| {
2858                            (
2859                                renderer::Quad {
2860                                    bounds: Rectangle {
2861                                        x: text_bounds.x + r.x,
2862                                        y: text_bounds.y,
2863                                        width: r.width,
2864                                        height: text_bounds.height,
2865                                    },
2866                                    border: Border {
2867                                        width: 0.0,
2868                                        color: Color::TRANSPARENT,
2869                                        radius: radius_0,
2870                                    },
2871                                    shadow: Shadow {
2872                                        offset: Vector::ZERO,
2873                                        color: Color::TRANSPARENT,
2874                                        blur_radius: 0.0,
2875                                    },
2876                                    snap: true,
2877                                },
2878                                appearance.selected_fill,
2879                            )
2880                        })
2881                        .collect();
2882
2883                    (cursors, state.scroll_offset, true)
2884                }
2885            }
2886        }
2887    } else {
2888        let unfocused_offset = match effective_alignment(state.value.raw()) {
2889            alignment::Horizontal::Right => {
2890                (state.value.raw().min_width() - text_bounds.width).max(0.0)
2891            }
2892            _ => 0.0,
2893        };
2894
2895        (
2896            Vec::<(renderer::Quad, Color)>::new(),
2897            unfocused_offset,
2898            false,
2899        )
2900    };
2901
2902    let render = |renderer: &mut crate::Renderer| {
2903        let alignment_offset = alignment_offset(
2904            text_bounds.width,
2905            state.value.raw().min_width(),
2906            effective_alignment(state.value.raw()),
2907        );
2908
2909        let shift = Vector::new(alignment_offset - offset, 0.0);
2910        let fill_cursors = |renderer: &mut crate::Renderer| {
2911            renderer.with_translation(shift, |renderer| {
2912                for (quad, color) in &cursors {
2913                    renderer.fill_quad(*quad, *color);
2914                }
2915            });
2916        };
2917
2918        if !is_selecting {
2919            fill_cursors(renderer);
2920        }
2921
2922        let bounds = Rectangle {
2923            x: text_bounds.x + alignment_offset - offset,
2924            y: text_bounds.center_y(),
2925            width: actual_width,
2926            ..text_bounds
2927        };
2928        let color = if text.is_empty() {
2929            appearance.placeholder_color
2930        } else {
2931            text_color
2932        };
2933
2934        let text = Text {
2935            content: if text.is_empty() {
2936                placeholder.to_string()
2937            } else {
2938                text.clone()
2939            },
2940            font,
2941            bounds: bounds.size(),
2942            size: iced::Pixels(size),
2943            align_x: text::Alignment::Default,
2944            align_y: alignment::Vertical::Center,
2945            line_height: text::LineHeight::default(),
2946            shaping: text::Shaping::Advanced,
2947            wrapping: text::Wrapping::None,
2948            ellipsize: text::Ellipsize::None,
2949        };
2950        renderer.fill_text(text.clone(), bounds.position(), color, text_bounds);
2951
2952        // Redraw the same text in the selected color, clipped to the selection quads,
2953        // so glyph shaping and positioning stay identical to the unselected pass.
2954        if is_selecting {
2955            fill_cursors(renderer);
2956            for (quad, _) in &cursors {
2957                renderer.with_layer(quad.bounds + shift, |renderer| {
2958                    renderer.fill_text(
2959                        text.clone(),
2960                        bounds.position(),
2961                        appearance.selected_text_color,
2962                        text_bounds,
2963                    );
2964                });
2965            }
2966        }
2967    };
2968
2969    // FIXME: we always must clip with a layer because of what appears to be a tiny-skia text clipping issue.
2970    // Otherwise overflowing text escapes the bounds of the input.
2971    renderer.with_layer(text_bounds, render);
2972
2973    let trailing_icon_tree = children.get(child_index);
2974
2975    // draw the end icon in the text input
2976    if let (Some(icon), Some(tree)) = (trailing_icon, trailing_icon_tree) {
2977        let mut children = text_layout.children();
2978        let mut icon_layout = children.next().unwrap();
2979        if has_start_icon {
2980            icon_layout = children.next().unwrap();
2981        }
2982        icon_layout = children.next().unwrap();
2983
2984        icon.as_widget().draw(
2985            tree,
2986            renderer,
2987            theme,
2988            &renderer::Style {
2989                icon_color,
2990                text_color,
2991                scale_factor: renderer_style.scale_factor,
2992            },
2993            icon_layout,
2994            cursor_position,
2995            viewport,
2996        );
2997    }
2998
2999    // draw the helper text if it exists
3000    if let (Some(helper_text_layout), Some(helper_text)) = (helper_text_layout, helper_text) {
3001        renderer.fill_text(
3002            Text {
3003                content: helper_text.to_string(), // TODO remove to_string?
3004                size: iced::Pixels(helper_text_size),
3005                font,
3006                bounds: helper_text_layout.bounds().size(),
3007                align_x: text::Alignment::Left,
3008                align_y: alignment::Vertical::Top,
3009                line_height: helper_line_height,
3010                shaping: text::Shaping::Advanced,
3011                wrapping: text::Wrapping::None,
3012                ellipsize: text::Ellipsize::None,
3013            },
3014            helper_text_layout.bounds().position(),
3015            text_color,
3016            *viewport,
3017        );
3018    }
3019}
3020
3021/// Computes the current [`mouse::Interaction`] of the [`TextInput`].
3022#[must_use]
3023pub fn mouse_interaction(
3024    layout: Layout<'_>,
3025    cursor_position: mouse::Cursor,
3026    is_disabled: bool,
3027) -> mouse::Interaction {
3028    if cursor_position.is_over(layout.bounds()) {
3029        if is_disabled {
3030            mouse::Interaction::NotAllowed
3031        } else {
3032            mouse::Interaction::Text
3033        }
3034    } else {
3035        mouse::Interaction::default()
3036    }
3037}
3038
3039/// A string which can be sent to the clipboard or drag-and-dropped.
3040#[derive(Debug, Clone)]
3041pub struct TextInputString(pub String);
3042
3043#[cfg(wayland_platform)]
3044impl AsMimeTypes for TextInputString {
3045    fn available(&self) -> Cow<'static, [String]> {
3046        Cow::Owned(
3047            SUPPORTED_TEXT_MIME_TYPES
3048                .iter()
3049                .cloned()
3050                .map(String::from)
3051                .collect::<Vec<_>>(),
3052        )
3053    }
3054
3055    fn as_bytes(&self, mime_type: &str) -> Option<Cow<'static, [u8]>> {
3056        if SUPPORTED_TEXT_MIME_TYPES.contains(&mime_type) {
3057            Some(Cow::Owned(self.0.clone().into_bytes()))
3058        } else {
3059            None
3060        }
3061    }
3062}
3063
3064#[derive(Debug, Clone, PartialEq)]
3065pub(crate) enum DraggingState {
3066    Selection,
3067    #[cfg(wayland_platform)]
3068    PrepareDnd(Point),
3069    #[cfg(wayland_platform)]
3070    Dnd(DndAction, String),
3071}
3072
3073#[cfg(wayland_platform)]
3074#[derive(Debug, Default, Clone)]
3075pub(crate) enum DndOfferState {
3076    #[default]
3077    None,
3078    HandlingOffer(Vec<String>, DndAction),
3079    Dropped,
3080}
3081#[derive(Debug, Default, Clone)]
3082#[cfg(not(wayland_platform))]
3083pub(crate) struct DndOfferState;
3084
3085/// The state of a [`TextInput`].
3086#[derive(Default, Clone)]
3087#[must_use]
3088pub struct State {
3089    pub tracked_value: Value,
3090    pub value: crate::Plain,
3091    pub placeholder: crate::Plain,
3092    pub label: crate::Plain,
3093    pub helper_text: crate::Plain,
3094    pub dirty: bool,
3095    pub is_secure: bool,
3096    pub is_read_only: bool,
3097    pub emit_unfocus: bool,
3098    select_on_focus: bool,
3099    double_click_select_delimiter: Option<char>,
3100    is_focused: Option<Focus>,
3101    dragging_state: Option<DraggingState>,
3102    dnd_offer: DndOfferState,
3103    is_pasting: Option<Value>,
3104    last_click: Option<mouse::Click>,
3105    cursor: Cursor,
3106    preedit: Option<Preedit>,
3107    keyboard_modifiers: keyboard::Modifiers,
3108    scroll_offset: f32,
3109    context_menu_position: Option<iced_core::Point>,
3110    clipboard_has_text: bool,
3111    pub(crate) menu_bar_state: crate::widget::menu::MenuBarState,
3112    pub(crate) pending_action: crate::widget::text_context_menu::PendingAction,
3113}
3114
3115impl std::fmt::Debug for State {
3116    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3117        f.debug_struct("State")
3118            .field("is_secure", &self.is_secure)
3119            .field("is_read_only", &self.is_read_only)
3120            .field("dirty", &self.dirty)
3121            .finish_non_exhaustive()
3122    }
3123}
3124
3125#[derive(Debug, Clone, Copy)]
3126struct Focus {
3127    updated_at: Instant,
3128    now: Instant,
3129    focused: bool,
3130    needs_update: bool,
3131}
3132
3133impl State {
3134    /// Creates a new [`State`], representing an unfocused [`TextInput`].
3135    pub fn new(
3136        is_secure: bool,
3137        is_read_only: bool,
3138        always_active: bool,
3139        select_on_focus: bool,
3140    ) -> Self {
3141        Self {
3142            is_secure,
3143            is_read_only,
3144            is_focused: always_active.then(|| {
3145                let now = Instant::now();
3146                Focus {
3147                    updated_at: now,
3148                    now,
3149                    focused: true,
3150                    needs_update: false,
3151                }
3152            }),
3153            select_on_focus,
3154            ..Self::default()
3155        }
3156    }
3157
3158    /// Returns the current value of the selected text in the [`TextInput`].
3159    #[must_use]
3160    pub fn selected_text(&self, text: &str) -> Option<String> {
3161        let value = Value::new(text);
3162        match self.cursor.state(&value) {
3163            cursor::State::Index(_) => None,
3164            cursor::State::Selection { start, end } => {
3165                let left = start.min(end);
3166                let right = end.max(start);
3167                Some(value.select(left, right).to_string())
3168            }
3169        }
3170    }
3171
3172    #[cfg(wayland_platform)]
3173    /// Returns the current value of the dragged text in the [`TextInput`].
3174    #[must_use]
3175    pub fn dragged_text(&self) -> Option<String> {
3176        match self.dragging_state.as_ref() {
3177            Some(DraggingState::Dnd(_, text)) => Some(text.clone()),
3178            _ => None,
3179        }
3180    }
3181
3182    /// Creates a new [`State`], representing a focused [`TextInput`].
3183    pub fn focused(is_secure: bool, is_read_only: bool) -> Self {
3184        Self {
3185            tracked_value: Value::default(),
3186            is_secure,
3187            value: crate::Plain::default(),
3188            placeholder: crate::Plain::default(),
3189            label: crate::Plain::default(),
3190            helper_text: crate::Plain::default(),
3191            is_read_only,
3192            emit_unfocus: false,
3193            is_focused: None,
3194            select_on_focus: false,
3195            double_click_select_delimiter: None,
3196            dragging_state: None,
3197            dnd_offer: DndOfferState::default(),
3198            is_pasting: None,
3199            last_click: None,
3200            cursor: Cursor::default(),
3201            preedit: None,
3202            keyboard_modifiers: keyboard::Modifiers::default(),
3203            scroll_offset: 0.0,
3204            dirty: false,
3205            context_menu_position: None,
3206            clipboard_has_text: false,
3207            menu_bar_state: crate::widget::menu::MenuBarState::default(),
3208            pending_action: crate::widget::text_context_menu::pending_action(),
3209        }
3210    }
3211
3212    /// Returns whether the [`TextInput`] is currently focused or not.
3213    #[inline]
3214    #[must_use]
3215    pub fn is_focused(&self) -> bool {
3216        self.is_focused.is_some_and(|f| f.focused)
3217    }
3218
3219    /// Returns the [`Cursor`] of the [`TextInput`].
3220    #[inline]
3221    #[must_use]
3222    pub fn cursor(&self) -> Cursor {
3223        self.cursor
3224    }
3225
3226    /// Focuses the [`TextInput`].
3227    #[cold]
3228    pub fn focus(&mut self) {
3229        let now = Instant::now();
3230        LAST_FOCUS_UPDATE.with(|x| x.set(now));
3231        let was_focused = self.is_focused.is_some_and(|f| f.focused);
3232        self.is_read_only = false;
3233        self.is_focused = Some(Focus {
3234            updated_at: now,
3235            now,
3236            focused: true,
3237            needs_update: false,
3238        });
3239
3240        if was_focused {
3241            return;
3242        }
3243        if self.select_on_focus {
3244            self.select_all()
3245        } else {
3246            self.move_cursor_to_end();
3247        }
3248    }
3249
3250    /// Unfocuses the [`TextInput`].
3251    #[cold]
3252    pub(super) fn unfocus(&mut self) {
3253        self.cursor.clear_selection();
3254        self.last_click = None;
3255        self.is_focused = self.is_focused.map(|mut f| {
3256            f.focused = false;
3257            f.needs_update = false;
3258            f
3259        });
3260        self.dragging_state = None;
3261        self.is_pasting = None;
3262        self.keyboard_modifiers = keyboard::Modifiers::default();
3263    }
3264
3265    /// Moves the [`Cursor`] of the [`TextInput`] to the front of the input text.
3266    #[inline]
3267    pub fn move_cursor_to_front(&mut self) {
3268        self.cursor.move_to(0);
3269    }
3270
3271    /// Moves the [`Cursor`] of the [`TextInput`] to the end of the input text.
3272    #[inline]
3273    pub fn move_cursor_to_end(&mut self) {
3274        self.cursor.move_to(usize::MAX);
3275    }
3276
3277    /// Moves the [`Cursor`] of the [`TextInput`] to an arbitrary location.
3278    #[inline]
3279    pub fn move_cursor_to(&mut self, position: usize) {
3280        self.cursor.move_to(position);
3281    }
3282
3283    /// Selects all the content of the [`TextInput`].
3284    #[inline]
3285    pub fn select_all(&mut self) {
3286        self.cursor.select_range(0, usize::MAX);
3287    }
3288
3289    /// Selects a range of the content of the [`TextInput`].
3290    #[inline]
3291    pub fn select_range(&mut self, start: usize, end: usize) {
3292        self.cursor.select_range(start, end);
3293    }
3294
3295    /// Returns the context menu position, if a context menu should be shown.
3296    pub fn context_menu_position(&self) -> Option<iced_core::Point> {
3297        self.context_menu_position
3298    }
3299
3300    /// Sets or clears the context menu position.
3301    pub fn set_context_menu_position(&mut self, pos: Option<iced_core::Point>) {
3302        self.context_menu_position = pos;
3303    }
3304
3305    /// Deletes the current selection and returns the new text content.
3306    pub fn delete_selection(&mut self) -> String {
3307        let mut editor = super::editor::Editor::new(&mut self.tracked_value, &mut self.cursor);
3308        editor.delete();
3309        editor.contents()
3310    }
3311
3312    /// Pastes text at the current cursor position and returns the new text content.
3313    pub fn paste_text(&mut self, text: &str) -> String {
3314        let paste_value = super::value::Value::new(text);
3315        let mut editor = super::editor::Editor::new(&mut self.tracked_value, &mut self.cursor);
3316        editor.paste(paste_value);
3317        let contents = editor.contents();
3318        self.tracked_value = super::value::Value::new(&contents);
3319        contents
3320    }
3321
3322    pub(super) fn setting_selection(&mut self, value: &Value, bounds: Rectangle<f32>, target: f32) {
3323        let (position, affinity) = find_cursor_position(bounds, value, self, target)
3324            .unwrap_or((0, text::Affinity::Before));
3325
3326        self.cursor.set_affinity(affinity);
3327        self.cursor.move_to(position);
3328        self.dragging_state = Some(DraggingState::Selection);
3329    }
3330}
3331
3332impl operation::Focusable for State {
3333    #[inline]
3334    fn is_focused(&self) -> bool {
3335        Self::is_focused(self)
3336    }
3337
3338    #[inline]
3339    fn focus(&mut self) {
3340        Self::focus(self);
3341        if let Some(focus) = self.is_focused.as_mut() {
3342            focus.needs_update = true;
3343        }
3344    }
3345
3346    #[inline]
3347    fn unfocus(&mut self) {
3348        Self::unfocus(self);
3349        if let Some(focus) = self.is_focused.as_mut() {
3350            focus.needs_update = true;
3351        }
3352    }
3353}
3354
3355impl operation::TextInput for State {
3356    #[inline]
3357    fn move_cursor_to_front(&mut self) {
3358        Self::move_cursor_to_front(self);
3359    }
3360
3361    #[inline]
3362    fn move_cursor_to_end(&mut self) {
3363        Self::move_cursor_to_end(self);
3364    }
3365
3366    #[inline]
3367    fn move_cursor_to(&mut self, position: usize) {
3368        Self::move_cursor_to(self, position);
3369    }
3370
3371    #[inline]
3372    fn select_all(&mut self) {
3373        Self::select_all(self);
3374    }
3375
3376    fn text(&self) -> &str {
3377        todo!()
3378    }
3379
3380    #[inline]
3381    fn select_range(&mut self, start: usize, end: usize) {
3382        Self::select_range(self, start, end);
3383    }
3384}
3385
3386#[inline(never)]
3387fn measure_cursor_and_scroll_offset(
3388    paragraph: &impl text::Paragraph,
3389    text_bounds: Rectangle,
3390    cursor_index: usize,
3391    value: &Value,
3392    affinity: text::Affinity,
3393    current_offset: f32,
3394) -> (f32, f32) {
3395    let byte_index = value.byte_index_at_grapheme(cursor_index);
3396    let position = paragraph
3397        .cursor_position(0, byte_index, affinity)
3398        .unwrap_or(Point::ORIGIN);
3399
3400    // The visible window in paragraph coordinates is:
3401    //   [current_offset, current_offset + text_bounds.width]
3402    // Keep the cursor visible with a 5px margin on each side.
3403    let offset = if position.x > current_offset + text_bounds.width - 5.0 {
3404        // Cursor past right edge of visible window → scroll left
3405        (position.x + 5.0) - text_bounds.width
3406    } else if position.x < current_offset + 5.0 {
3407        // Cursor past left edge of visible window → scroll right
3408        position.x - 5.0
3409    } else {
3410        // Cursor is within visible window → keep current scroll
3411        current_offset
3412    };
3413
3414    let max_offset = (paragraph.min_width() - text_bounds.width).max(0.0);
3415    let offset = offset.clamp(0.0, max_offset);
3416
3417    (position.x, offset)
3418}
3419
3420/// Computes the position of the text cursor at the given X coordinate of
3421/// a [`TextInput`].
3422#[inline(never)]
3423fn find_cursor_position(
3424    text_bounds: Rectangle,
3425    value: &Value,
3426    state: &State,
3427    x: f32,
3428) -> Option<(usize, text::Affinity)> {
3429    let value_str = value.to_string();
3430
3431    let hit = state.value.raw().hit_test(Point::new(
3432        x + state.scroll_offset,
3433        text_bounds.height / 2.0,
3434    ))?;
3435    let char_offset = hit.cursor();
3436    let affinity = hit.affinity();
3437
3438    let grapheme_count = unicode_segmentation::UnicodeSegmentation::graphemes(
3439        &value_str[..char_offset.min(value_str.len())],
3440        true,
3441    )
3442    .count();
3443
3444    Some((grapheme_count, affinity))
3445}
3446
3447#[inline(never)]
3448fn replace_paragraph(
3449    state: &mut State,
3450    layout: Layout<'_>,
3451    value: &Value,
3452    font: <crate::Renderer as iced_core::text::Renderer>::Font,
3453    text_size: Pixels,
3454    line_height: text::LineHeight,
3455    limits: &layout::Limits,
3456) {
3457    let mut children_layout = layout.children();
3458    let text_bounds = children_layout.next().unwrap();
3459    let bounds = limits.resolve(
3460        Length::Shrink,
3461        Length::Fill,
3462        Size::new(0., text_bounds.bounds().height),
3463    );
3464
3465    state.value = crate::Plain::new(Text {
3466        font,
3467        line_height,
3468        content: value.to_string(),
3469        bounds,
3470        size: text_size,
3471        align_x: text::Alignment::Default,
3472        align_y: alignment::Vertical::Top,
3473        shaping: text::Shaping::Advanced,
3474        wrapping: text::Wrapping::None,
3475        ellipsize: text::Ellipsize::None,
3476    });
3477}
3478
3479const CURSOR_BLINK_INTERVAL_MILLIS: u128 = 500;
3480
3481mod platform {
3482    use iced_core::keyboard;
3483
3484    #[inline]
3485    pub fn is_jump_modifier_pressed(modifiers: keyboard::Modifiers) -> bool {
3486        if cfg!(target_os = "macos") {
3487            modifiers.alt()
3488        } else {
3489            modifiers.control()
3490        }
3491    }
3492}
3493
3494#[inline(never)]
3495fn offset(text_bounds: Rectangle, value: &Value, state: &State) -> f32 {
3496    if state.is_focused() {
3497        let cursor = state.cursor();
3498
3499        let focus_position = match cursor.state(value) {
3500            cursor::State::Index(i) => i,
3501            cursor::State::Selection { end, .. } => end,
3502        };
3503
3504        let (_, offset) = measure_cursor_and_scroll_offset(
3505            state.value.raw(),
3506            text_bounds,
3507            focus_position,
3508            value,
3509            state.cursor().affinity(),
3510            state.scroll_offset,
3511        );
3512
3513        offset
3514    } else {
3515        match effective_alignment(state.value.raw()) {
3516            alignment::Horizontal::Right => {
3517                (state.value.raw().min_width() - text_bounds.width).max(0.0)
3518            }
3519            _ => 0.0,
3520        }
3521    }
3522}
3523
3524#[inline(never)]
3525fn alignment_offset(
3526    text_bounds_width: f32,
3527    text_min_width: f32,
3528    alignment: alignment::Horizontal,
3529) -> f32 {
3530    if text_min_width > text_bounds_width {
3531        0.0
3532    } else {
3533        match alignment {
3534            alignment::Horizontal::Left => 0.0,
3535            alignment::Horizontal::Center => (text_bounds_width - text_min_width) / 2.0,
3536            alignment::Horizontal::Right => text_bounds_width - text_min_width,
3537        }
3538    }
3539}
3540
3541#[inline(never)]
3542fn effective_alignment(paragraph: &impl text::Paragraph) -> alignment::Horizontal {
3543    if paragraph.is_rtl(0).unwrap_or(false) {
3544        alignment::Horizontal::Right
3545    } else {
3546        alignment::Horizontal::Left
3547    }
3548}
3549
3550use iced_core::widget::tree::Tree as WidgetTree;
3551
3552impl<Message: Clone + 'static> iced_core::widget::text::HasSelectableText
3553    for TextInput<'_, Message>
3554{
3555    fn selected_text(&self, tree: &WidgetTree) -> Option<String> {
3556        let state = tree.state.downcast_ref::<State>();
3557        let (start, end) = state.cursor().selection(&state.tracked_value)?;
3558        Some(state.tracked_value.select(start, end).to_string())
3559    }
3560
3561    fn select_all(&self, tree: &mut WidgetTree) {
3562        let state = tree.state.downcast_mut::<State>();
3563        state.select_all();
3564    }
3565
3566    fn is_editable(&self) -> bool {
3567        true
3568    }
3569
3570    fn has_text(&self, tree: &WidgetTree) -> bool {
3571        !tree.state.downcast_ref::<State>().tracked_value.is_empty()
3572    }
3573
3574    fn clipboard_has_text(&self, tree: &WidgetTree) -> bool {
3575        tree.state.downcast_ref::<State>().clipboard_has_text
3576    }
3577
3578    fn is_focused(&self, tree: &WidgetTree) -> bool {
3579        tree.state.downcast_ref::<State>().is_focused()
3580    }
3581
3582    fn context_menu_position(&self, tree: &WidgetTree) -> Option<iced_core::Point> {
3583        tree.state.downcast_ref::<State>().context_menu_position
3584    }
3585
3586    fn set_context_menu_position(&self, tree: &mut WidgetTree, pos: Option<iced_core::Point>) {
3587        tree.state.downcast_mut::<State>().context_menu_position = pos;
3588    }
3589
3590    fn delete_selection(&self, tree: &mut WidgetTree) -> Option<String> {
3591        let state = tree.state.downcast_mut::<State>();
3592        Some(state.delete_selection())
3593    }
3594
3595    fn paste_text(&self, tree: &mut WidgetTree, text: &str) -> Option<String> {
3596        let filtered: String = text.chars().filter(|c| !c.is_control()).collect();
3597        let state = tree.state.downcast_mut::<State>();
3598        Some(state.paste_text(&filtered))
3599    }
3600}