Skip to main content

danceinterpreter_rs/ui/widget/
suggestion_text_input.rs

1//! Combo boxes display a dropdown list of searchable and selectable options.
2//!
3//! # Example
4//! ```no_run
5//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
6//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
7//! #
8//! use iced::widget::combo_box;
9//!
10//! struct State {
11//!    fruits: combo_box::State<Fruit>,
12//!    favorite: Option<Fruit>,
13//! }
14//!
15//! #[derive(Debug, Clone)]
16//! enum Fruit {
17//!     Apple,
18//!     Orange,
19//!     Strawberry,
20//!     Tomato,
21//! }
22//!
23//! #[derive(Debug, Clone)]
24//! enum Message {
25//!     FruitSelected(Fruit),
26//! }
27//!
28//! fn view(state: &State) -> Element<'_, Message> {
29//!     combo_box(
30//!         &state.fruits,
31//!         "Select your favorite fruit...",
32//!         state.favorite.as_ref(),
33//!         Message::FruitSelected
34//!     )
35//!     .into()
36//! }
37//!
38//! fn update(state: &mut State, message: Message) {
39//!     match message {
40//!         Message::FruitSelected(fruit) => {
41//!             state.favorite = Some(fruit);
42//!         }
43//!     }
44//! }
45//!
46//! impl std::fmt::Display for Fruit {
47//!     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48//!         f.write_str(match self {
49//!             Self::Apple => "Apple",
50//!             Self::Orange => "Orange",
51//!             Self::Strawberry => "Strawberry",
52//!             Self::Tomato => "Tomato",
53//!         })
54//!     }
55//! }
56//! ```
57use iced::advanced::{Clipboard, Layout, Shell, Widget, layout, renderer, text, widget};
58use iced::keyboard;
59use iced::keyboard::key;
60use iced::mouse;
61use iced::overlay;
62use iced::overlay::menu;
63use iced::time::Instant;
64use iced::{Element, Event, Length, Padding, Pixels, Rectangle, Size, Theme, Vector};
65
66use iced::advanced::text::LineHeight;
67use iced::widget::{TextInput, text_input};
68use std::cell::RefCell;
69use std::fmt::Display;
70
71/// A widget for searching and selecting a single value from a list of options.
72///
73/// # Example
74/// ```no_run
75/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
76/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
77/// #
78/// use iced::widget::combo_box;
79///
80/// struct State {
81///    fruits: combo_box::State<Fruit>,
82///    favorite: Option<Fruit>,
83/// }
84///
85/// #[derive(Debug, Clone)]
86/// enum Fruit {
87///     Apple,
88///     Orange,
89///     Strawberry,
90///     Tomato,
91/// }
92///
93/// #[derive(Debug, Clone)]
94/// enum Message {
95///     FruitSelected(Fruit),
96/// }
97///
98/// fn view(state: &State) -> Element<'_, Message> {
99///     combo_box(
100///         &state.fruits,
101///         "Select your favorite fruit...",
102///         state.favorite.as_ref(),
103///         Message::FruitSelected
104///     )
105///     .into()
106/// }
107///
108/// fn update(state: &mut State, message: Message) {
109///     match message {
110///         Message::FruitSelected(fruit) => {
111///             state.favorite = Some(fruit);
112///         }
113///     }
114/// }
115///
116/// impl std::fmt::Display for Fruit {
117///     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118///         f.write_str(match self {
119///             Self::Apple => "Apple",
120///             Self::Orange => "Orange",
121///             Self::Strawberry => "Strawberry",
122///             Self::Tomato => "Tomato",
123///         })
124///     }
125/// }
126/// ```
127pub struct SuggestionTextInput<'a, T, Message, Theme = iced::Theme, Renderer = iced::Renderer>
128where
129    Theme: Catalog,
130    Renderer: text::Renderer,
131{
132    state: &'a State<T>,
133    text_input: TextInput<'a, TextInputEvent, Theme, Renderer>,
134    font: Option<Renderer::Font>,
135    selection: text_input::Value,
136    on_selected: Box<dyn Fn(T) -> Message>,
137    on_option_hovered: Option<Box<dyn Fn(T) -> Message>>,
138    on_open: Option<Message>,
139    on_close: Option<Message>,
140    on_input: Option<Box<dyn Fn(String) -> Message>>,
141    padding: Padding,
142    size: Option<f32>,
143    text_shaping: text::Shaping,
144    menu_class: <Theme as menu::Catalog>::Class<'a>,
145    menu_height: Length,
146}
147
148impl<'a, T, Message, Theme, Renderer> SuggestionTextInput<'a, T, Message, Theme, Renderer>
149where
150    T: Display + Clone,
151    Theme: Catalog,
152    Renderer: text::Renderer,
153{
154    /// Creates a new [`SuggestionTextInput`] with the given list of options, a placeholder,
155    /// the current selected value, and the message to produce when an option is
156    /// selected.
157    pub fn new(
158        state: &'a State<T>,
159        placeholder: &str,
160        selection: Option<&T>,
161        on_selected: impl Fn(T) -> Message + 'static,
162    ) -> Self {
163        let text_input = TextInput::new(placeholder, &state.value())
164            .on_input(TextInputEvent::TextChanged)
165            .class(Theme::default_input());
166
167        let selection = selection.map(T::to_string).unwrap_or_default();
168
169        Self {
170            state,
171            text_input,
172            font: None,
173            selection: text_input::Value::new(&selection),
174            on_selected: Box::new(on_selected),
175            on_option_hovered: None,
176            on_input: None,
177            on_open: None,
178            on_close: None,
179            padding: text_input::DEFAULT_PADDING,
180            size: None,
181            text_shaping: text::Shaping::default(),
182            menu_class: <Theme as Catalog>::default_menu(),
183            menu_height: Length::Shrink,
184        }
185    }
186
187    /// Sets the message that should be produced when some text is typed into
188    /// the [`TextInput`] of the [`SuggestionTextInput`].
189    pub fn on_input(mut self, on_input: impl Fn(String) -> Message + 'static) -> Self {
190        self.on_input = Some(Box::new(on_input));
191        self
192    }
193
194    /// Sets the message that will be produced when an option of the
195    /// [`SuggestionTextInput`] is hovered using the arrow keys.
196    pub fn on_option_hovered(mut self, on_option_hovered: impl Fn(T) -> Message + 'static) -> Self {
197        self.on_option_hovered = Some(Box::new(on_option_hovered));
198        self
199    }
200
201    /// Sets the message that will be produced when the  [`SuggestionTextInput`] is
202    /// opened.
203    pub fn on_open(mut self, message: Message) -> Self {
204        self.on_open = Some(message);
205        self
206    }
207
208    /// Sets the message that will be produced when the outside area
209    /// of the [`SuggestionTextInput`] is pressed.
210    pub fn on_close(mut self, message: Message) -> Self {
211        self.on_close = Some(message);
212        self
213    }
214
215    /// Sets the [`Padding`] of the [`SuggestionTextInput`].
216    pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
217        self.padding = padding.into();
218        self.text_input = self.text_input.padding(self.padding);
219        self
220    }
221
222    /// Sets the [`Renderer::Font`] of the [`SuggestionTextInput`].
223    ///
224    /// [`Renderer::Font`]: text::Renderer
225    pub fn font(mut self, font: Renderer::Font) -> Self {
226        self.text_input = self.text_input.font(font);
227        self.font = Some(font);
228        self
229    }
230
231    /// Sets the [`text_input::Icon`] of the [`SuggestionTextInput`].
232    pub fn icon(mut self, icon: text_input::Icon<Renderer::Font>) -> Self {
233        self.text_input = self.text_input.icon(icon);
234        self
235    }
236
237    /// Sets the text size of the [`SuggestionTextInput`].
238    pub fn size(mut self, size: impl Into<Pixels>) -> Self {
239        let size = size.into();
240
241        self.text_input = self.text_input.size(size);
242        self.size = Some(size.0);
243
244        self
245    }
246
247    /// Sets the [`LineHeight`] of the [`SuggestionTextInput`].
248    pub fn line_height(self, line_height: impl Into<LineHeight>) -> Self {
249        Self {
250            text_input: self.text_input.line_height(line_height),
251            ..self
252        }
253    }
254
255    /// Sets the width of the [`SuggestionTextInput`].
256    pub fn width(self, width: impl Into<Length>) -> Self {
257        Self {
258            text_input: self.text_input.width(width),
259            ..self
260        }
261    }
262
263    /// Sets the height of the menu of the [`SuggestionTextInput`].
264    pub fn menu_height(mut self, menu_height: impl Into<Length>) -> Self {
265        self.menu_height = menu_height.into();
266        self
267    }
268
269    /// Sets the [`text::Shaping`] strategy of the [`SuggestionTextInput`].
270    pub fn text_shaping(mut self, shaping: text::Shaping) -> Self {
271        self.text_shaping = shaping;
272        self
273    }
274
275    /// Sets the style of the input of the [`SuggestionTextInput`].
276    #[must_use]
277    pub fn input_style(
278        mut self,
279        style: impl Fn(&Theme, text_input::Status) -> text_input::Style + 'a,
280    ) -> Self
281    where
282        <Theme as text_input::Catalog>::Class<'a>: From<text_input::StyleFn<'a, Theme>>,
283    {
284        self.text_input = self.text_input.style(style);
285        self
286    }
287
288    /// Sets the style of the menu of the [`SuggestionTextInput`].
289    #[must_use]
290    pub fn menu_style(mut self, style: impl Fn(&Theme) -> menu::Style + 'a) -> Self
291    where
292        <Theme as menu::Catalog>::Class<'a>: From<menu::StyleFn<'a, Theme>>,
293    {
294        self.menu_class = (Box::new(style) as menu::StyleFn<'a, Theme>).into();
295        self
296    }
297
298    /// Sets the style class of the input of the [`SuggestionTextInput`].
299    #[must_use]
300    pub fn input_class(
301        mut self,
302        class: impl Into<<Theme as text_input::Catalog>::Class<'a>>,
303    ) -> Self {
304        self.text_input = self.text_input.class(class);
305        self
306    }
307
308    /// Sets the style class of the menu of the [`SuggestionTextInput`].
309    #[must_use]
310    pub fn menu_class(mut self, class: impl Into<<Theme as menu::Catalog>::Class<'a>>) -> Self {
311        self.menu_class = class.into();
312        self
313    }
314}
315
316/// The local state of a [`SuggestionTextInput`].
317#[derive(Debug, Clone)]
318pub struct State<T> {
319    options: Vec<T>,
320    inner: RefCell<Inner<T>>,
321}
322
323#[derive(Debug, Clone)]
324struct Inner<T> {
325    value: String,
326    option_matchers: Vec<String>,
327    filtered_options: Filtered<T>,
328}
329
330#[derive(Debug, Clone)]
331struct Filtered<T> {
332    options: Vec<T>,
333    updated: Instant,
334}
335
336impl<T> State<T>
337where
338    T: Display + Clone,
339{
340    /// Creates a new [`State`] for a [`SuggestionTextInput`] with the given list of options.
341    pub fn new(options: Vec<T>) -> Self {
342        Self::with_selection(options, None)
343    }
344
345    /// Creates a new [`State`] for a [`SuggestionTextInput`] with the given list of options
346    /// and selected value.
347    pub fn with_selection(options: Vec<T>, selection: Option<&T>) -> Self {
348        let value = selection.map(T::to_string).unwrap_or_default();
349
350        // Pre-build "matcher" strings ahead of time so that search is fast
351        let option_matchers = build_matchers(&options);
352
353        let filtered_options = Filtered::new(
354            search(&options, &option_matchers, &value)
355                .cloned()
356                .collect(),
357        );
358
359        Self {
360            options,
361            inner: RefCell::new(Inner {
362                value,
363                option_matchers,
364                filtered_options,
365            }),
366        }
367    }
368
369    /// Returns the options of the [`State`].
370    ///
371    /// These are the options provided when the [`State`]
372    /// was constructed with [`State::new`].
373    pub fn options(&self) -> &[T] {
374        &self.options
375    }
376
377    /// Pushes a new option to the [`State`].
378    pub fn push(&mut self, new_option: T) {
379        let mut inner = self.inner.borrow_mut();
380
381        inner.option_matchers.push(build_matcher(&new_option));
382        self.options.push(new_option);
383
384        inner.filtered_options = Filtered::new(
385            search(&self.options, &inner.option_matchers, &inner.value)
386                .cloned()
387                .collect(),
388        );
389    }
390
391    /// Returns ownership of the options of the [`State`].
392    pub fn into_options(self) -> Vec<T> {
393        self.options
394    }
395
396    fn value(&self) -> String {
397        let inner = self.inner.borrow();
398
399        inner.value.clone()
400    }
401
402    fn with_inner<O>(&self, f: impl FnOnce(&Inner<T>) -> O) -> O {
403        let inner = self.inner.borrow();
404
405        f(&inner)
406    }
407
408    fn with_inner_mut(&self, f: impl FnOnce(&mut Inner<T>)) {
409        let mut inner = self.inner.borrow_mut();
410
411        f(&mut inner);
412    }
413
414    fn sync_filtered_options(&self, options: &mut Filtered<T>) {
415        let inner = self.inner.borrow();
416
417        inner.filtered_options.sync(options);
418    }
419}
420
421impl<T> Default for State<T>
422where
423    T: Display + Clone,
424{
425    fn default() -> Self {
426        Self::new(Vec::new())
427    }
428}
429
430impl<T> Filtered<T>
431where
432    T: Clone,
433{
434    fn new(options: Vec<T>) -> Self {
435        Self {
436            options,
437            updated: Instant::now(),
438        }
439    }
440
441    fn empty() -> Self {
442        Self {
443            options: vec![],
444            updated: Instant::now(),
445        }
446    }
447
448    fn update(&mut self, options: Vec<T>) {
449        self.options = options;
450        self.updated = Instant::now();
451    }
452
453    fn sync(&self, other: &mut Filtered<T>) {
454        if other.updated != self.updated {
455            *other = self.clone();
456        }
457    }
458}
459
460struct Menu<T> {
461    menu: menu::State,
462    hovered_option: Option<usize>,
463    new_selection: Option<T>,
464    filtered_options: Filtered<T>,
465}
466
467#[derive(Debug, Clone)]
468enum TextInputEvent {
469    TextChanged(String),
470}
471
472impl<T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
473    for SuggestionTextInput<'_, T, Message, Theme, Renderer>
474where
475    T: Display + Clone + 'static,
476    Message: Clone,
477    Theme: Catalog,
478    Renderer: text::Renderer,
479{
480    fn size(&self) -> Size<Length> {
481        Widget::<TextInputEvent, Theme, Renderer>::size(&self.text_input)
482    }
483
484    fn layout(
485        &mut self,
486        tree: &mut widget::Tree,
487        renderer: &Renderer,
488        limits: &layout::Limits,
489    ) -> layout::Node {
490        let is_focused = {
491            let text_input_state = tree.children[0]
492                .state
493                .downcast_ref::<text_input::State<Renderer::Paragraph>>();
494
495            text_input_state.is_focused()
496        };
497
498        self.text_input.layout(
499            &mut tree.children[0],
500            renderer,
501            limits,
502            (!is_focused).then_some(&self.selection),
503        )
504    }
505
506    fn draw(
507        &self,
508        tree: &widget::Tree,
509        renderer: &mut Renderer,
510        theme: &Theme,
511        _style: &renderer::Style,
512        layout: Layout<'_>,
513        cursor: mouse::Cursor,
514        viewport: &Rectangle,
515    ) {
516        let is_focused = {
517            let text_input_state = tree.children[0]
518                .state
519                .downcast_ref::<text_input::State<Renderer::Paragraph>>();
520
521            text_input_state.is_focused()
522        };
523
524        let selection = if is_focused || self.selection.is_empty() {
525            None
526        } else {
527            Some(&self.selection)
528        };
529
530        self.text_input.draw(
531            &tree.children[0],
532            renderer,
533            theme,
534            layout,
535            cursor,
536            selection,
537            viewport,
538        );
539    }
540
541    fn tag(&self) -> widget::tree::Tag {
542        widget::tree::Tag::of::<Menu<T>>()
543    }
544
545    fn state(&self) -> widget::tree::State {
546        widget::tree::State::new(Menu::<T> {
547            menu: menu::State::new(),
548            filtered_options: Filtered::empty(),
549            hovered_option: Some(0),
550            new_selection: None,
551        })
552    }
553
554    fn children(&self) -> Vec<widget::Tree> {
555        vec![widget::Tree::new(&self.text_input as &dyn Widget<_, _, _>)]
556    }
557
558    fn diff(&self, _tree: &mut widget::Tree) {
559        // do nothing so the children don't get cleared
560    }
561
562    fn update(
563        &mut self,
564        tree: &mut widget::Tree,
565        event: &Event,
566        layout: Layout<'_>,
567        cursor: mouse::Cursor,
568        renderer: &Renderer,
569        clipboard: &mut dyn Clipboard,
570        shell: &mut Shell<'_, Message>,
571        viewport: &Rectangle,
572    ) {
573        let menu = tree.state.downcast_mut::<Menu<T>>();
574
575        let started_focused = {
576            let text_input_state = tree.children[0]
577                .state
578                .downcast_ref::<text_input::State<Renderer::Paragraph>>();
579
580            text_input_state.is_focused()
581        };
582
583        // Create a new list of local messages
584        let mut local_messages = Vec::new();
585        let mut local_shell = Shell::new(&mut local_messages);
586
587        // Provide it to the widget
588        self.text_input.update(
589            &mut tree.children[0],
590            event,
591            layout,
592            cursor,
593            renderer,
594            clipboard,
595            &mut local_shell,
596            viewport,
597        );
598
599        if local_shell.is_event_captured() {
600            shell.capture_event();
601        }
602
603        shell.request_redraw_at(local_shell.redraw_request());
604        shell.request_input_method(local_shell.input_method());
605
606        // Then finally react to them here
607        for message in local_messages {
608            let TextInputEvent::TextChanged(new_value) = message;
609
610            if let Some(on_input) = &self.on_input {
611                shell.publish(on_input(new_value.clone()));
612            }
613
614            // Couple the filtered options with the `SuggestionTextInput`
615            // value and only recompute them when the value changes,
616            // instead of doing it in every `view` call
617            self.state.with_inner_mut(|state| {
618                menu.hovered_option = Some(0);
619                state.value = new_value;
620
621                state.filtered_options.update(
622                    search(&self.state.options, &state.option_matchers, &state.value)
623                        .cloned()
624                        .collect(),
625                );
626            });
627            shell.invalidate_layout();
628            shell.request_redraw();
629        }
630
631        let is_focused = {
632            let text_input_state = tree.children[0]
633                .state
634                .downcast_ref::<text_input::State<Renderer::Paragraph>>();
635
636            text_input_state.is_focused()
637        };
638
639        if is_focused {
640            self.state.with_inner(|state| {
641                if !started_focused && let Some(on_option_hovered) = &mut self.on_option_hovered {
642                    let hovered_option = menu.hovered_option.unwrap_or(0);
643
644                    if let Some(option) = state.filtered_options.options.get(hovered_option) {
645                        shell.publish(on_option_hovered(option.clone()));
646                    }
647                }
648
649                if let Event::Keyboard(keyboard::Event::KeyPressed {
650                    key: keyboard::Key::Named(named_key),
651                    modifiers,
652                    ..
653                }) = event
654                {
655                    let shift_modifier = modifiers.shift();
656                    match (named_key, shift_modifier) {
657                        (key::Named::Enter, _) => {
658                            if let Some(index) = &menu.hovered_option
659                                && let Some(option) = state.filtered_options.options.get(*index)
660                            {
661                                menu.new_selection = Some(option.clone());
662                            }
663
664                            shell.capture_event();
665                            shell.request_redraw();
666                        }
667                        (key::Named::ArrowUp, _) | (key::Named::Tab, true) => {
668                            if let Some(index) = &mut menu.hovered_option {
669                                if *index == 0 {
670                                    *index = state.filtered_options.options.len().saturating_sub(1);
671                                } else {
672                                    *index = index.saturating_sub(1);
673                                }
674                            } else {
675                                menu.hovered_option = Some(0);
676                            }
677
678                            if let Some(on_option_hovered) = &mut self.on_option_hovered
679                                && let Some(option) = menu
680                                    .hovered_option
681                                    .and_then(|index| state.filtered_options.options.get(index))
682                            {
683                                // Notify the selection
684                                shell.publish(on_option_hovered(option.clone()));
685                            }
686
687                            shell.capture_event();
688                            shell.request_redraw();
689                        }
690                        (key::Named::ArrowDown, _) | (key::Named::Tab, false)
691                            if !modifiers.shift() =>
692                        {
693                            if let Some(index) = &mut menu.hovered_option {
694                                if *index >= state.filtered_options.options.len().saturating_sub(1)
695                                {
696                                    *index = 0;
697                                } else {
698                                    *index = index.saturating_add(1).min(
699                                        state.filtered_options.options.len().saturating_sub(1),
700                                    );
701                                }
702                            } else {
703                                menu.hovered_option = Some(0);
704                            }
705
706                            if let Some(on_option_hovered) = &mut self.on_option_hovered
707                                && let Some(option) = menu
708                                    .hovered_option
709                                    .and_then(|index| state.filtered_options.options.get(index))
710                            {
711                                // Notify the selection
712                                shell.publish(on_option_hovered(option.clone()));
713                            }
714
715                            shell.capture_event();
716                            shell.request_redraw();
717                        }
718                        _ => {}
719                    }
720                }
721            });
722        }
723
724        // If the overlay menu has selected something
725        self.state.with_inner_mut(|state| {
726            if let Some(selection) = menu.new_selection.take() {
727                // Clear the value and reset the options and menu
728                state.value = String::new();
729                state.filtered_options.update(self.state.options.clone());
730                menu.menu = menu::State::default();
731
732                // Notify the selection
733                shell.publish((self.on_selected)(selection));
734
735                // Unfocus the input
736                let mut local_messages = Vec::new();
737                let mut local_shell = Shell::new(&mut local_messages);
738                self.text_input.update(
739                    &mut tree.children[0],
740                    &Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
741                    layout,
742                    mouse::Cursor::Unavailable,
743                    renderer,
744                    clipboard,
745                    &mut local_shell,
746                    viewport,
747                );
748                shell.request_input_method(local_shell.input_method());
749            }
750        });
751
752        let is_focused = {
753            let text_input_state = tree.children[0]
754                .state
755                .downcast_ref::<text_input::State<Renderer::Paragraph>>();
756
757            text_input_state.is_focused()
758        };
759
760        if started_focused != is_focused {
761            // Focus changed, invalidate widget tree to force a fresh `view`
762            shell.invalidate_widgets();
763
764            if is_focused {
765                if let Some(on_open) = self.on_open.take() {
766                    shell.publish(on_open);
767                }
768            } else if let Some(on_close) = self.on_close.take() {
769                shell.publish(on_close);
770            }
771        }
772    }
773
774    fn mouse_interaction(
775        &self,
776        tree: &widget::Tree,
777        layout: Layout<'_>,
778        cursor: mouse::Cursor,
779        viewport: &Rectangle,
780        renderer: &Renderer,
781    ) -> mouse::Interaction {
782        self.text_input
783            .mouse_interaction(&tree.children[0], layout, cursor, viewport, renderer)
784    }
785
786    fn overlay<'b>(
787        &'b mut self,
788        tree: &'b mut widget::Tree,
789        layout: Layout<'_>,
790        _renderer: &Renderer,
791        viewport: &Rectangle,
792        translation: Vector,
793    ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
794        let is_focused = {
795            let text_input_state = tree.children[0]
796                .state
797                .downcast_ref::<text_input::State<Renderer::Paragraph>>();
798
799            text_input_state.is_focused()
800        };
801
802        if is_focused {
803            let Menu {
804                menu,
805                filtered_options,
806                hovered_option,
807                ..
808            } = tree.state.downcast_mut::<Menu<T>>();
809
810            self.state.sync_filtered_options(filtered_options);
811
812            if filtered_options.options.is_empty() {
813                None
814            } else {
815                let bounds = layout.bounds();
816
817                let mut menu = menu::Menu::new(
818                    menu,
819                    &filtered_options.options,
820                    hovered_option,
821                    |selection| {
822                        self.state.with_inner_mut(|state| {
823                            state.value = String::new();
824                            state.filtered_options.update(self.state.options.clone());
825                        });
826
827                        tree.children[0]
828                            .state
829                            .downcast_mut::<text_input::State<Renderer::Paragraph>>()
830                            .unfocus();
831
832                        (self.on_selected)(selection)
833                    },
834                    self.on_option_hovered.as_deref(),
835                    &self.menu_class,
836                )
837                .width(bounds.width)
838                .padding(self.padding)
839                .text_shaping(self.text_shaping);
840
841                if let Some(font) = self.font {
842                    menu = menu.font(font);
843                }
844
845                if let Some(size) = self.size {
846                    menu = menu.text_size(size);
847                }
848
849                Some(menu.overlay(
850                    layout.position() + translation,
851                    *viewport,
852                    bounds.height,
853                    self.menu_height,
854                ))
855            }
856        } else {
857            None
858        }
859    }
860}
861
862impl<'a, T, Message, Theme, Renderer> From<SuggestionTextInput<'a, T, Message, Theme, Renderer>>
863    for Element<'a, Message, Theme, Renderer>
864where
865    T: Display + Clone + 'static,
866    Message: Clone + 'a,
867    Theme: Catalog + 'a,
868    Renderer: text::Renderer + 'a,
869{
870    fn from(suggestion_text_input: SuggestionTextInput<'a, T, Message, Theme, Renderer>) -> Self {
871        Self::new(suggestion_text_input)
872    }
873}
874
875/// The theme catalog of a [`SuggestionTextInput`].
876pub trait Catalog: text_input::Catalog + menu::Catalog {
877    /// The default class for the text input of the [`SuggestionTextInput`].
878    fn default_input<'a>() -> <Self as text_input::Catalog>::Class<'a> {
879        <Self as text_input::Catalog>::default()
880    }
881
882    /// The default class for the menu of the [`SuggestionTextInput`].
883    fn default_menu<'a>() -> <Self as menu::Catalog>::Class<'a> {
884        <Self as menu::Catalog>::default()
885    }
886}
887
888impl Catalog for Theme {}
889
890fn search<'a, T, A>(
891    options: impl IntoIterator<Item = T> + 'a,
892    option_matchers: impl IntoIterator<Item = &'a A> + 'a,
893    query: &'a str,
894) -> impl Iterator<Item = T> + 'a
895where
896    A: AsRef<str> + 'a,
897{
898    let query: Vec<String> = query
899        .to_lowercase()
900        .split(|c: char| !c.is_ascii_alphanumeric())
901        .map(String::from)
902        .collect();
903
904    options
905        .into_iter()
906        .zip(option_matchers)
907        // Make sure each part of the query is found in the option
908        .filter_map(move |(option, matcher)| {
909            if query.iter().all(|part| matcher.as_ref().contains(part)) {
910                Some(option)
911            } else {
912                None
913            }
914        })
915}
916
917fn build_matchers<'a, T>(options: impl IntoIterator<Item = T> + 'a) -> Vec<String>
918where
919    T: Display + 'a,
920{
921    options.into_iter().map(build_matcher).collect()
922}
923
924fn build_matcher<T>(option: T) -> String
925where
926    T: Display,
927{
928    let mut matcher = option.to_string();
929    matcher.retain(|c| c.is_ascii_alphanumeric());
930    matcher.to_lowercase()
931}