Skip to main content

iced_widget/
combo_box.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 crate::core::keyboard;
58use crate::core::keyboard::key;
59use crate::core::layout::{self, Layout};
60use crate::core::mouse;
61use crate::core::overlay;
62use crate::core::renderer;
63use crate::core::text;
64use crate::core::text::editor;
65use crate::core::text::input;
66use crate::core::widget::operation::Focusable as _;
67use crate::core::widget::{self, Widget};
68use crate::core::window;
69use crate::core::{
70    Element, Event, Font, Length, Padding, Pixels, Rectangle, Shell, Size, Theme, Vector,
71};
72use crate::overlay::menu;
73use crate::text::LineHeight;
74use crate::text_input;
75
76use std::fmt::Display;
77use std::sync::atomic::{self, AtomicU64};
78
79/// A widget for searching and selecting a single value from a list of options.
80///
81/// # Example
82/// ```no_run
83/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
84/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
85/// #
86/// use iced::widget::combo_box;
87///
88/// struct State {
89///    fruits: combo_box::State<Fruit>,
90///    favorite: Option<Fruit>,
91/// }
92///
93/// #[derive(Debug, Clone)]
94/// enum Fruit {
95///     Apple,
96///     Orange,
97///     Strawberry,
98///     Tomato,
99/// }
100///
101/// #[derive(Debug, Clone)]
102/// enum Message {
103///     FruitSelected(Fruit),
104/// }
105///
106/// fn view(state: &State) -> Element<'_, Message> {
107///     combo_box(
108///         &state.fruits,
109///         "Select your favorite fruit...",
110///         state.favorite.as_ref(),
111///         Message::FruitSelected
112///     )
113///     .into()
114/// }
115///
116/// fn update(state: &mut State, message: Message) {
117///     match message {
118///         Message::FruitSelected(fruit) => {
119///             state.favorite = Some(fruit);
120///         }
121///     }
122/// }
123///
124/// impl std::fmt::Display for Fruit {
125///     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126///         f.write_str(match self {
127///             Self::Apple => "Apple",
128///             Self::Orange => "Orange",
129///             Self::Strawberry => "Strawberry",
130///             Self::Tomato => "Tomato",
131///         })
132///     }
133/// }
134/// ```
135pub struct ComboBox<'a, T, Message, Theme = crate::Theme>
136where
137    Theme: Catalog,
138{
139    state: &'a State<T>,
140    id: Option<widget::Id>,
141    placeholder: text::Fragment<'a>,
142    selection: String,
143    width: Length,
144    line_height: Option<LineHeight>,
145    font: Option<Font>,
146    on_selected: Box<dyn Fn(T) -> Message + 'a>,
147    on_option_hovered: Option<Box<dyn Fn(T) -> Message + 'a>>,
148    on_open: Option<Message>,
149    on_close: Option<Message>,
150    on_input: Option<Box<dyn Fn(String) -> Message + 'a>>,
151    padding: Padding,
152    size: Option<Pixels>,
153    shaping: text::Shaping,
154    ellipsis: text::Ellipsis,
155    input_class: <Theme as text_input::Catalog>::Class<'a>,
156    menu_class: <Theme as menu::Catalog>::Class<'a>,
157    menu_height: Length,
158    last_status: Option<text_input::Status>,
159}
160
161impl<'a, T, Message, Theme> ComboBox<'a, T, Message, Theme>
162where
163    T: std::fmt::Display + Clone,
164    Theme: Catalog,
165{
166    /// Creates a new [`ComboBox`] with the given list of options, a placeholder,
167    /// the current selected value, and the message to produce when an option is
168    /// selected.
169    pub fn new(
170        state: &'a State<T>,
171        placeholder: impl text::IntoFragment<'a>,
172        selection: Option<&T>,
173        on_selected: impl Fn(T) -> Message + 'a,
174    ) -> Self {
175        Self {
176            state,
177            id: None,
178            placeholder: placeholder.into_fragment(),
179            selection: selection.map(T::to_string).unwrap_or_default(),
180            width: Length::Fill,
181            line_height: None,
182            font: None,
183            on_selected: Box::new(on_selected),
184            on_option_hovered: None,
185            on_input: None,
186            on_open: None,
187            on_close: None,
188            padding: text_input::DEFAULT_PADDING,
189            size: None,
190            shaping: text::Shaping::default(),
191            ellipsis: text::Ellipsis::End,
192            input_class: <Theme as Catalog>::default_input(),
193            menu_class: <Theme as Catalog>::default_menu(),
194            menu_height: Length::Shrink,
195            last_status: None,
196        }
197    }
198
199    /// Sets the [`widget::Id`] of the [`ComboBox`].
200    pub fn id(mut self, id: impl Into<widget::Id>) -> Self {
201        self.id = Some(id.into());
202        self
203    }
204
205    /// Sets the message that should be produced when some text is typed into
206    /// the [`ComboBox`].
207    pub fn on_input(mut self, on_input: impl Fn(String) -> Message + 'a) -> Self {
208        self.on_input = Some(Box::new(on_input));
209        self
210    }
211
212    /// Sets the message that will be produced when an option of the
213    /// [`ComboBox`] is hovered using the arrow keys.
214    pub fn on_option_hovered(mut self, on_option_hovered: impl Fn(T) -> Message + 'a) -> Self {
215        self.on_option_hovered = Some(Box::new(on_option_hovered));
216        self
217    }
218
219    /// Sets the message that will be produced when the  [`ComboBox`] is
220    /// opened.
221    pub fn on_open(mut self, message: Message) -> Self {
222        self.on_open = Some(message);
223        self
224    }
225
226    /// Sets the message that will be produced when the outside area
227    /// of the [`ComboBox`] is pressed.
228    pub fn on_close(mut self, message: Message) -> Self {
229        self.on_close = Some(message);
230        self
231    }
232
233    /// Sets the [`Padding`] of the [`ComboBox`].
234    pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
235        self.padding = padding.into();
236        self
237    }
238
239    /// Sets the [`Font`] of the [`ComboBox`].
240    ///
241    /// [`Font`]: crate::core::Font
242    pub fn font(mut self, font: Font) -> Self {
243        self.font = Some(font);
244        self
245    }
246
247    /// Sets the text sixe of the [`ComboBox`].
248    pub fn size(mut self, size: impl Into<Pixels>) -> Self {
249        self.size = Some(size.into());
250        self
251    }
252
253    /// Sets the width of the [`ComboBox`].
254    pub fn width(mut self, width: impl Into<Length>) -> Self {
255        self.width = width.into();
256        self
257    }
258
259    /// Sets the [`LineHeight`] of the [`ComboBox`].
260    pub fn line_height(mut self, line_height: impl Into<LineHeight>) -> Self {
261        self.line_height = Some(line_height.into());
262        self
263    }
264
265    /// Sets the height of the menu of the [`ComboBox`].
266    pub fn menu_height(mut self, menu_height: impl Into<Length>) -> Self {
267        self.menu_height = menu_height.into();
268        self
269    }
270
271    /// Sets the [`text::Shaping`] strategy of the [`ComboBox`].
272    pub fn shaping(mut self, shaping: text::Shaping) -> Self {
273        self.shaping = shaping;
274        self
275    }
276
277    /// Sets the [`text::Ellipsis`] strategy of the [`ComboBox`].
278    pub fn ellipsis(mut self, ellipsis: text::Ellipsis) -> Self {
279        self.ellipsis = ellipsis;
280        self
281    }
282
283    /// Sets the style of the input of the [`ComboBox`].
284    #[must_use]
285    pub fn input_style(
286        mut self,
287        style: impl Fn(&Theme, text_input::Status) -> text_input::Style + 'a,
288    ) -> Self
289    where
290        <Theme as text_input::Catalog>::Class<'a>: From<text_input::StyleFn<'a, Theme>>,
291    {
292        self.input_class = (Box::new(style) as text_input::StyleFn<'a, Theme>).into();
293        self
294    }
295
296    /// Sets the style of the menu of the [`ComboBox`].
297    #[must_use]
298    pub fn menu_style(mut self, style: impl Fn(&Theme) -> menu::Style + 'a) -> Self
299    where
300        <Theme as menu::Catalog>::Class<'a>: From<menu::StyleFn<'a, Theme>>,
301    {
302        self.menu_class = (Box::new(style) as menu::StyleFn<'a, Theme>).into();
303        self
304    }
305
306    /// Sets the style class of the input of the [`ComboBox`].
307    #[cfg(feature = "advanced")]
308    #[must_use]
309    pub fn input_class(
310        mut self,
311        class: impl Into<<Theme as text_input::Catalog>::Class<'a>>,
312    ) -> Self {
313        self.input_class = class.into();
314        self
315    }
316
317    /// Sets the style class of the menu of the [`ComboBox`].
318    #[cfg(feature = "advanced")]
319    #[must_use]
320    pub fn menu_class(mut self, class: impl Into<<Theme as menu::Catalog>::Class<'a>>) -> Self {
321        self.menu_class = class.into();
322        self
323    }
324}
325
326/// The local state of a [`ComboBox`].
327#[derive(Debug, Clone)]
328pub struct State<T> {
329    options: Vec<T>,
330    version: u64,
331}
332
333static VERSION: AtomicU64 = AtomicU64::new(0);
334
335impl<T> State<T>
336where
337    T: Display + Clone,
338{
339    /// Creates a new [`State`] for a [`ComboBox`] with the given list of options.
340    pub fn new(options: Vec<T>) -> Self {
341        Self {
342            options,
343            version: VERSION.fetch_add(1, atomic::Ordering::Relaxed),
344        }
345    }
346
347    /// Returns the options of the [`State`].
348    ///
349    /// These are the options provided when the [`State`]
350    /// was constructed with [`State::new`].
351    pub fn options(&self) -> &[T] {
352        &self.options
353    }
354
355    /// Pushes a new option to the [`State`].
356    pub fn push(&mut self, new_option: T) {
357        self.options.push(new_option);
358        self.version = VERSION.fetch_add(1, atomic::Ordering::Relaxed);
359    }
360
361    /// Returns ownership of the options of the [`State`].
362    pub fn into_options(self) -> Vec<T> {
363        self.options
364    }
365}
366
367impl<T> Default for State<T>
368where
369    T: Display + Clone,
370{
371    fn default() -> Self {
372        Self::new(Vec::new())
373    }
374}
375
376struct Internal<T, R: text::Renderer> {
377    editor: Editor<R>,
378    menu: menu::State,
379    hovered_option: Option<usize>,
380    option_matchers: Vec<String>,
381    filtered_options: Vec<T>,
382    version: u64,
383}
384
385impl<T: Display + Clone, R: text::Renderer> Internal<T, R> {
386    fn hovered_option(&self) -> usize {
387        let index = self.hovered_option.unwrap_or_default();
388
389        index.min(self.filtered_options.len().saturating_sub(1))
390    }
391
392    fn filter(&mut self, options: &[T], value: &str) {
393        self.option_matchers = build_matchers(options);
394        self.filtered_options = search(options, &self.option_matchers, value)
395            .cloned()
396            .collect();
397    }
398}
399
400struct Editor<R: text::Renderer> {
401    input: text::Input<R>,
402    selection: Option<String>,
403}
404
405impl<T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
406    for ComboBox<'_, T, Message, Theme>
407where
408    T: Display + Clone + 'static,
409    Message: Clone,
410    Theme: Catalog,
411    Renderer: text::Renderer + 'static,
412{
413    fn size(&self) -> Size<Length> {
414        Size {
415            width: self.width,
416            height: Length::Fit,
417        }
418    }
419
420    fn layout(
421        &mut self,
422        tree: &mut widget::Tree,
423        renderer: &Renderer,
424        limits: &layout::Limits,
425    ) -> layout::Node {
426        let state = tree.state.downcast_mut::<Internal<T, Renderer>>();
427
428        state.editor.input.layout(
429            renderer,
430            limits,
431            input::Layout {
432                width: self.width,
433                height: Length::Fit,
434                padding: self.padding,
435                placeholder: &self.placeholder,
436                font: self.font,
437                size: self.size,
438                line_height: self.line_height,
439                alignment: text::Alignment::Default,
440                multiline: None,
441                is_secure: false,
442            },
443        )
444    }
445
446    fn tag(&self) -> widget::tree::Tag {
447        widget::tree::Tag::of::<Internal<T, Renderer>>()
448    }
449
450    fn state(&self) -> widget::tree::State {
451        widget::tree::State::new(Internal::<T, Renderer> {
452            editor: Editor {
453                input: text::Input::new(),
454                selection: None,
455            },
456            menu: menu::State::new(),
457            filtered_options: Vec::new(),
458            option_matchers: Vec::new(),
459            hovered_option: Some(0),
460            version: 0,
461        })
462    }
463
464    fn diff(&mut self, tree: &mut widget::Tree) {
465        let state = tree.state.downcast_mut::<Internal<T, Renderer>>();
466
467        if state.version != self.state.version
468            || state.editor.selection.as_deref() != Some(&self.selection)
469        {
470            state.editor.input.overwrite(&self.selection);
471            state.editor.selection = Some(self.selection.clone());
472            state.filter(&self.state.options, &self.selection);
473
474            state.version = self.state.version;
475        }
476    }
477
478    fn update(
479        &mut self,
480        tree: &mut widget::Tree,
481        event: &Event,
482        layout: Layout<'_>,
483        cursor: mouse::Cursor,
484        _renderer: &Renderer,
485        shell: &mut Shell<'_, Message>,
486        _viewport: &Rectangle,
487    ) {
488        let internal = tree.state.downcast_mut::<Internal<T, Renderer>>();
489
490        let was_focused = internal.editor.input.is_focused();
491
492        let edit = internal.editor.input.update::<Message>(
493            event,
494            layout.bounds(),
495            cursor,
496            shell,
497            editor::Binding::from_key_press,
498        );
499
500        if edit.is_some() {
501            let value = internal.editor.input.value();
502
503            if let Some(on_input) = &self.on_input {
504                shell.publish(on_input(value.clone()));
505            }
506
507            internal.filter(&self.state.options, &value);
508        }
509
510        let is_focused = internal.editor.input.is_focused();
511
512        if is_focused {
513            if !was_focused {
514                internal.editor.input.overwrite("");
515                internal.filtered_options = self.state.options.clone();
516
517                if let Some(on_option_hovered) = &mut self.on_option_hovered {
518                    let hovered_option = internal.hovered_option.unwrap_or(0);
519
520                    if let Some(option) = internal.filtered_options.get(hovered_option) {
521                        shell.publish(on_option_hovered(option.clone()));
522                    }
523                }
524            }
525
526            if let Event::Keyboard(keyboard::Event::KeyPressed {
527                key: keyboard::Key::Named(named_key),
528                modifiers,
529                ..
530            }) = event
531            {
532                match (named_key, modifiers.shift()) {
533                    (key::Named::Enter, _) => {
534                        if let Some(option) = internal
535                            .filtered_options
536                            .get(internal.hovered_option())
537                            .cloned()
538                        {
539                            internal.menu = menu::State::default();
540                            internal.editor.selection = None;
541                            internal.editor.input.overwrite("");
542                            internal.editor.input.unfocus();
543
544                            shell.publish((self.on_selected)(option));
545                        }
546
547                        shell.capture_event();
548                        shell.request_redraw();
549                    }
550                    (key::Named::ArrowUp, _) | (key::Named::Tab, true) => {
551                        let index = internal.hovered_option();
552
553                        internal.hovered_option = Some(if index == 0 {
554                            internal.filtered_options.len().saturating_sub(1)
555                        } else {
556                            index.saturating_sub(1)
557                        });
558
559                        if let Some(on_option_hovered) = &mut self.on_option_hovered
560                            && let Some(option) = internal
561                                .hovered_option
562                                .and_then(|index| internal.filtered_options.get(index))
563                        {
564                            shell.publish((on_option_hovered)(option.clone()));
565                        }
566
567                        shell.capture_event();
568                        shell.request_redraw();
569                    }
570                    (key::Named::ArrowDown, _) | (key::Named::Tab, false) => {
571                        let index = internal.hovered_option();
572
573                        internal.hovered_option = Some(
574                            if index >= internal.filtered_options.len().saturating_sub(1) {
575                                0
576                            } else {
577                                index
578                                    .saturating_add(1)
579                                    .min(internal.filtered_options.len().saturating_sub(1))
580                            },
581                        );
582
583                        if let Some(on_option_hovered) = &mut self.on_option_hovered
584                            && let Some(option) = internal
585                                .hovered_option
586                                .and_then(|index| internal.filtered_options.get(index))
587                        {
588                            shell.publish((on_option_hovered)(option.clone()));
589                        }
590
591                        shell.capture_event();
592                        shell.request_redraw();
593                    }
594                    _ => {}
595                }
596            }
597        }
598
599        if was_focused != is_focused {
600            if is_focused {
601                if let Some(on_open) = self.on_open.take() {
602                    shell.publish(on_open);
603                }
604            } else if let Some(on_close) = self.on_close.take() {
605                internal.editor.input.overwrite(&self.selection);
606                shell.publish(on_close);
607            }
608        }
609
610        let status = if internal.editor.input.is_focused() {
611            text_input::Status::Focused {
612                is_hovered: cursor.is_over(layout.bounds()),
613            }
614        } else if cursor.is_over(layout.bounds()) {
615            text_input::Status::Hovered
616        } else {
617            text_input::Status::Active
618        };
619
620        if let Event::Window(window::Event::RedrawRequested(_now)) = event {
621            self.last_status = Some(status);
622
623            shell.request_input_method(
624                &internal
625                    .editor
626                    .input
627                    .input_method(layout.bounds().shrink(self.padding).position()),
628            );
629        } else if self
630            .last_status
631            .is_some_and(|last_status| status != last_status)
632        {
633            shell.request_redraw();
634        }
635    }
636
637    fn mouse_interaction(
638        &self,
639        _tree: &widget::Tree,
640        layout: Layout<'_>,
641        cursor: mouse::Cursor,
642        _viewport: &Rectangle,
643        _renderer: &Renderer,
644    ) -> mouse::Interaction {
645        if cursor.is_over(layout.bounds()) {
646            mouse::Interaction::Text
647        } else {
648            mouse::Interaction::default()
649        }
650    }
651
652    fn draw(
653        &self,
654        tree: &widget::Tree,
655        renderer: &mut Renderer,
656        theme: &Theme,
657        _style: &renderer::Style,
658        layout: Layout<'_>,
659        _cursor: mouse::Cursor,
660        viewport: &Rectangle,
661    ) {
662        let internal = tree.state.downcast_ref::<Internal<T, Renderer>>();
663
664        let bounds = layout.bounds();
665        let style = text_input::Catalog::style(
666            theme,
667            &self.input_class,
668            self.last_status.unwrap_or(text_input::Status::Disabled),
669        );
670
671        renderer.fill_quad(
672            renderer::Quad {
673                bounds,
674                border: style.border,
675                ..renderer::Quad::default()
676            },
677            style.background,
678        );
679
680        internal.editor.input.draw(
681            renderer,
682            bounds,
683            *viewport,
684            input::Style {
685                value: style.value,
686                selection: style.selection,
687                placeholder: style.placeholder,
688            },
689        );
690    }
691
692    fn overlay<'b>(
693        &'b mut self,
694        tree: &'b mut widget::Tree,
695        layout: Layout<'_>,
696        _renderer: &Renderer,
697        viewport: &Rectangle,
698        translation: Vector,
699    ) -> Vec<overlay::Element<'b, Message, Theme, Renderer>> {
700        let internal = tree.state.downcast_mut::<Internal<T, Renderer>>();
701        let is_focused = internal.editor.input.is_focused();
702
703        if is_focused {
704            let Internal {
705                menu,
706                filtered_options,
707                hovered_option,
708                editor,
709                ..
710            } = tree.state.downcast_mut::<Internal<T, Renderer>>();
711
712            if filtered_options.is_empty() {
713                Vec::new()
714            } else {
715                let bounds = layout.bounds();
716
717                let mut menu = menu::Menu::new(
718                    menu,
719                    filtered_options,
720                    hovered_option,
721                    &T::to_string,
722                    |selection| {
723                        editor.selection = None;
724                        editor.input.overwrite("");
725                        editor.input.unfocus();
726
727                        (self.on_selected)(selection)
728                    },
729                    self.on_option_hovered.as_deref(),
730                    &self.menu_class,
731                )
732                .width(bounds.width)
733                .padding(self.padding)
734                .shaping(self.shaping)
735                .ellipsis(self.ellipsis);
736
737                if let Some(font) = self.font {
738                    menu = menu.font(font);
739                }
740
741                if let Some(size) = self.size {
742                    menu = menu.text_size(size);
743                }
744
745                vec![menu.overlay(
746                    layout.position() + translation,
747                    *viewport,
748                    bounds.height,
749                    self.menu_height,
750                )]
751            }
752        } else {
753            Vec::new()
754        }
755    }
756
757    fn operate(
758        &mut self,
759        tree: &mut widget::Tree,
760        layout: Layout<'_>,
761        _renderer: &Renderer,
762        operation: &mut dyn widget::Operation,
763    ) {
764        let state = tree.state.downcast_mut::<Internal<T, Renderer>>();
765        let bounds = layout.bounds();
766
767        operation.focusable(self.id.as_ref(), bounds, &mut state.editor.input);
768        operation.text_input(self.id.as_ref(), bounds, &mut state.editor.input);
769    }
770}
771
772impl<'a, T, Message, Theme, Renderer> From<ComboBox<'a, T, Message, Theme>>
773    for Element<'a, Message, Theme, Renderer>
774where
775    T: Display + Clone + 'static,
776    Message: Clone + 'a,
777    Theme: Catalog + 'a,
778    Renderer: text::Renderer + 'static,
779{
780    fn from(combo_box: ComboBox<'a, T, Message, Theme>) -> Self {
781        Self::new(combo_box)
782    }
783}
784
785/// The theme catalog of a [`ComboBox`].
786pub trait Catalog: text_input::Catalog + menu::Catalog {
787    /// The default class for the text input of the [`ComboBox`].
788    fn default_input<'a>() -> <Self as text_input::Catalog>::Class<'a> {
789        <Self as text_input::Catalog>::default()
790    }
791
792    /// The default class for the menu of the [`ComboBox`].
793    fn default_menu<'a>() -> <Self as menu::Catalog>::Class<'a> {
794        <Self as menu::Catalog>::default()
795    }
796}
797
798impl Catalog for Theme {}
799
800fn search<'a, T, A>(
801    options: impl IntoIterator<Item = T> + 'a,
802    option_matchers: impl IntoIterator<Item = &'a A> + 'a,
803    query: &'a str,
804) -> impl Iterator<Item = T> + 'a
805where
806    A: AsRef<str> + 'a,
807{
808    let query: Vec<String> = query
809        .to_lowercase()
810        .split(|c: char| !c.is_ascii_alphanumeric())
811        .map(String::from)
812        .collect();
813
814    options
815        .into_iter()
816        .zip(option_matchers)
817        // Make sure each part of the query is found in the option
818        .filter_map(move |(option, matcher)| {
819            if query.iter().all(|part| matcher.as_ref().contains(part)) {
820                Some(option)
821            } else {
822                None
823            }
824        })
825}
826
827fn build_matchers<'a, T>(options: impl IntoIterator<Item = T> + 'a) -> Vec<String>
828where
829    T: Display + 'a,
830{
831    options.into_iter().map(build_matcher).collect()
832}
833
834fn build_matcher<T>(option: T) -> String
835where
836    T: Display,
837{
838    let mut matcher = option.to_string();
839    matcher.retain(|c| c.is_ascii_alphanumeric());
840    matcher.to_lowercase()
841}