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::{Element, Event, Length, Padding, Pixels, Rectangle, Shell, Size, Theme, Vector};
70use crate::overlay::menu;
71use crate::text::LineHeight;
72use crate::text_input;
73
74use std::fmt::Display;
75use std::sync::atomic::{self, AtomicU64};
76
77/// A widget for searching and selecting a single value from a list of options.
78///
79/// # Example
80/// ```no_run
81/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
82/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
83/// #
84/// use iced::widget::combo_box;
85///
86/// struct State {
87///    fruits: combo_box::State<Fruit>,
88///    favorite: Option<Fruit>,
89/// }
90///
91/// #[derive(Debug, Clone)]
92/// enum Fruit {
93///     Apple,
94///     Orange,
95///     Strawberry,
96///     Tomato,
97/// }
98///
99/// #[derive(Debug, Clone)]
100/// enum Message {
101///     FruitSelected(Fruit),
102/// }
103///
104/// fn view(state: &State) -> Element<'_, Message> {
105///     combo_box(
106///         &state.fruits,
107///         "Select your favorite fruit...",
108///         state.favorite.as_ref(),
109///         Message::FruitSelected
110///     )
111///     .into()
112/// }
113///
114/// fn update(state: &mut State, message: Message) {
115///     match message {
116///         Message::FruitSelected(fruit) => {
117///             state.favorite = Some(fruit);
118///         }
119///     }
120/// }
121///
122/// impl std::fmt::Display for Fruit {
123///     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124///         f.write_str(match self {
125///             Self::Apple => "Apple",
126///             Self::Orange => "Orange",
127///             Self::Strawberry => "Strawberry",
128///             Self::Tomato => "Tomato",
129///         })
130///     }
131/// }
132/// ```
133pub struct ComboBox<'a, T, Message, Theme = crate::Theme, Renderer = crate::Renderer>
134where
135    Theme: Catalog,
136    Renderer: text::Renderer,
137{
138    state: &'a State<T>,
139    id: Option<widget::Id>,
140    placeholder: text::Fragment<'a>,
141    selection: String,
142    width: Length,
143    line_height: LineHeight,
144    font: Option<Renderer::Font>,
145    on_selected: Box<dyn Fn(T) -> Message + 'a>,
146    on_option_hovered: Option<Box<dyn Fn(T) -> Message + 'a>>,
147    on_open: Option<Message>,
148    on_close: Option<Message>,
149    on_input: Option<Box<dyn Fn(String) -> Message + 'a>>,
150    padding: Padding,
151    size: Option<Pixels>,
152    shaping: text::Shaping,
153    ellipsis: text::Ellipsis,
154    input_class: <Theme as text_input::Catalog>::Class<'a>,
155    menu_class: <Theme as menu::Catalog>::Class<'a>,
156    menu_height: Length,
157    last_status: Option<text_input::Status>,
158}
159
160impl<'a, T, Message, Theme, Renderer> ComboBox<'a, T, Message, Theme, Renderer>
161where
162    T: std::fmt::Display + Clone,
163    Theme: Catalog,
164    Renderer: text::Renderer,
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: LineHeight::default(),
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 [`Renderer::Font`] of the [`ComboBox`].
240    ///
241    /// [`Renderer::Font`]: text::Renderer
242    pub fn font(mut self, font: Renderer::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 = 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    new_selection: Option<T>,
381    option_matchers: Vec<String>,
382    filtered_options: Vec<T>,
383    version: u64,
384}
385
386impl<T: Display + Clone, R: text::Renderer> Internal<T, R> {
387    fn filter(&mut self, options: &[T]) {
388        self.option_matchers = build_matchers(options);
389        self.filtered_options = search(options, &self.option_matchers, &self.editor.value)
390            .cloned()
391            .collect();
392    }
393}
394
395struct Editor<R: text::Renderer> {
396    input: text::Input<R>,
397    value: String,
398    selection: Option<String>,
399}
400
401impl<T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
402    for ComboBox<'_, T, Message, Theme, Renderer>
403where
404    T: Display + Clone + 'static,
405    Message: Clone,
406    Theme: Catalog,
407    Renderer: text::Renderer + 'static,
408{
409    fn size(&self) -> Size<Length> {
410        Size {
411            width: self.width,
412            height: Length::Fit,
413        }
414    }
415
416    fn layout(
417        &mut self,
418        tree: &mut widget::Tree,
419        renderer: &Renderer,
420        limits: &layout::Limits,
421    ) -> layout::Node {
422        let state = tree.state.downcast_mut::<Internal<T, Renderer>>();
423
424        state.editor.input.layout(
425            renderer,
426            limits,
427            input::Layout {
428                width: self.width,
429                height: Length::Fit,
430                padding: self.padding,
431                placeholder: &self.placeholder,
432                font: self.font,
433                size: self.size,
434                line_height: self.line_height,
435                alignment: text::Alignment::Default,
436                multiline: None,
437            },
438        )
439    }
440
441    fn tag(&self) -> widget::tree::Tag {
442        widget::tree::Tag::of::<Internal<T, Renderer>>()
443    }
444
445    fn state(&self) -> widget::tree::State {
446        widget::tree::State::new(Internal::<T, Renderer> {
447            editor: Editor {
448                input: text::Input::new(),
449                value: String::new(),
450                selection: None,
451            },
452            menu: menu::State::new(),
453            filtered_options: Vec::new(),
454            option_matchers: Vec::new(),
455            hovered_option: Some(0),
456            new_selection: None,
457            version: 0,
458        })
459    }
460
461    fn diff(&mut self, tree: &mut widget::Tree) {
462        let state = tree.state.downcast_mut::<Internal<T, Renderer>>();
463
464        if state.version != self.state.version
465            || state.editor.selection.as_deref() != Some(&self.selection)
466        {
467            state.editor.input.overwrite(&self.selection);
468            state.editor.selection = Some(self.selection.clone());
469            state.editor.value = self.selection.clone();
470            state.filter(&self.state.options);
471
472            state.version = self.state.version;
473        }
474    }
475
476    fn update(
477        &mut self,
478        tree: &mut widget::Tree,
479        event: &Event,
480        layout: Layout<'_>,
481        cursor: mouse::Cursor,
482        _renderer: &Renderer,
483        shell: &mut Shell<'_, Message>,
484        _viewport: &Rectangle,
485    ) {
486        let internal = tree.state.downcast_mut::<Internal<T, Renderer>>();
487
488        let was_focused = internal.editor.input.is_focused();
489
490        let edit = internal.editor.input.update::<Message>(
491            event,
492            layout.bounds(),
493            cursor,
494            shell,
495            editor::Binding::from_key_press,
496        );
497
498        if edit.is_some() {
499            let value = internal.editor.input.value();
500
501            if let Some(on_input) = &self.on_input {
502                shell.publish(on_input(value.clone()));
503            }
504
505            internal.editor.value = value;
506            internal.filter(&self.state.options);
507        }
508
509        let is_focused = internal.editor.input.is_focused();
510
511        if is_focused {
512            if !was_focused {
513                internal.editor.input.overwrite("");
514                internal.editor.value.clear();
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(index) = &internal.hovered_option
535                            && let Some(option) = internal.filtered_options.get(*index)
536                        {
537                            internal.new_selection = Some(option.clone());
538                        }
539
540                        shell.capture_event();
541                        shell.request_redraw();
542                    }
543                    (key::Named::ArrowUp, _) | (key::Named::Tab, true) => {
544                        if let Some(index) = &mut internal.hovered_option {
545                            if *index == 0 {
546                                *index = internal.filtered_options.len().saturating_sub(1);
547                            } else {
548                                *index = index.saturating_sub(1);
549                            }
550                        } else {
551                            internal.hovered_option = Some(0);
552                        }
553
554                        if let Some(on_option_hovered) = &mut self.on_option_hovered
555                            && let Some(option) = internal
556                                .hovered_option
557                                .and_then(|index| internal.filtered_options.get(index))
558                        {
559                            // Notify the selection
560                            shell.publish((on_option_hovered)(option.clone()));
561                        }
562
563                        shell.capture_event();
564                        shell.request_redraw();
565                    }
566                    (key::Named::ArrowDown, _) | (key::Named::Tab, false) => {
567                        if let Some(index) = &mut internal.hovered_option {
568                            if *index >= internal.filtered_options.len().saturating_sub(1) {
569                                *index = 0;
570                            } else {
571                                *index = index
572                                    .saturating_add(1)
573                                    .min(internal.filtered_options.len().saturating_sub(1));
574                            }
575                        } else {
576                            internal.hovered_option = Some(0);
577                        }
578
579                        if let Some(on_option_hovered) = &mut self.on_option_hovered
580                            && let Some(option) = internal
581                                .hovered_option
582                                .and_then(|index| internal.filtered_options.get(index))
583                        {
584                            // Notify the selection
585                            shell.publish((on_option_hovered)(option.clone()));
586                        }
587
588                        shell.capture_event();
589                        shell.request_redraw();
590                    }
591                    _ => {}
592                }
593            }
594        }
595
596        // If the overlay menu has selected something
597        if let Some(selection) = internal.new_selection.take() {
598            // Clear the value and reset the options and menu
599            internal.menu = menu::State::default();
600
601            internal.editor.input.overwrite(&selection.to_string());
602            internal.editor.input.unfocus();
603            internal.editor.value = String::new();
604
605            internal.filter(&self.state.options);
606
607            // Notify the selection
608            shell.publish((self.on_selected)(selection));
609        }
610
611        if was_focused != is_focused {
612            if is_focused {
613                if let Some(on_open) = self.on_open.take() {
614                    shell.publish(on_open);
615                }
616            } else if let Some(on_close) = self.on_close.take() {
617                internal.editor.input.overwrite(&self.selection);
618                shell.publish(on_close);
619            }
620        }
621
622        let status = if internal.editor.input.is_focused() {
623            text_input::Status::Focused {
624                is_hovered: cursor.is_over(layout.bounds()),
625            }
626        } else if cursor.is_over(layout.bounds()) {
627            text_input::Status::Hovered
628        } else {
629            text_input::Status::Active
630        };
631
632        if let Event::Window(window::Event::RedrawRequested(_now)) = event {
633            self.last_status = Some(status);
634
635            shell.request_input_method(
636                &internal
637                    .editor
638                    .input
639                    .input_method(layout.bounds().shrink(self.padding).position()),
640            );
641        } else if self
642            .last_status
643            .is_some_and(|last_status| status != last_status)
644        {
645            shell.request_redraw();
646        }
647    }
648
649    fn mouse_interaction(
650        &self,
651        _tree: &widget::Tree,
652        layout: Layout<'_>,
653        cursor: mouse::Cursor,
654        _viewport: &Rectangle,
655        _renderer: &Renderer,
656    ) -> mouse::Interaction {
657        if cursor.is_over(layout.bounds()) {
658            mouse::Interaction::Text
659        } else {
660            mouse::Interaction::default()
661        }
662    }
663
664    fn draw(
665        &self,
666        tree: &widget::Tree,
667        renderer: &mut Renderer,
668        theme: &Theme,
669        _style: &renderer::Style,
670        layout: Layout<'_>,
671        _cursor: mouse::Cursor,
672        viewport: &Rectangle,
673    ) {
674        let internal = tree.state.downcast_ref::<Internal<T, Renderer>>();
675
676        let bounds = layout.bounds();
677        let style = text_input::Catalog::style(
678            theme,
679            &self.input_class,
680            self.last_status.unwrap_or(text_input::Status::Disabled),
681        );
682
683        renderer.fill_quad(
684            renderer::Quad {
685                bounds,
686                border: style.border,
687                ..renderer::Quad::default()
688            },
689            style.background,
690        );
691
692        internal.editor.input.draw(
693            renderer,
694            bounds,
695            *viewport,
696            input::Style {
697                value: style.value,
698                selection: style.selection,
699                placeholder: style.placeholder,
700            },
701        );
702    }
703
704    fn overlay<'b>(
705        &'b mut self,
706        tree: &'b mut widget::Tree,
707        layout: Layout<'_>,
708        _renderer: &Renderer,
709        viewport: &Rectangle,
710        translation: Vector,
711    ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
712        let internal = tree.state.downcast_mut::<Internal<T, Renderer>>();
713        let is_focused = internal.editor.input.is_focused();
714
715        if is_focused {
716            let Internal {
717                menu,
718                filtered_options,
719                hovered_option,
720                new_selection,
721                ..
722            } = tree.state.downcast_mut::<Internal<T, Renderer>>();
723
724            if filtered_options.is_empty() {
725                None
726            } else {
727                let bounds = layout.bounds();
728
729                let mut menu = menu::Menu::new(
730                    menu,
731                    filtered_options,
732                    hovered_option,
733                    &T::to_string,
734                    |selection| {
735                        *new_selection = Some(selection.clone());
736
737                        (self.on_selected)(selection)
738                    },
739                    self.on_option_hovered.as_deref(),
740                    &self.menu_class,
741                )
742                .width(bounds.width)
743                .padding(self.padding)
744                .shaping(self.shaping)
745                .ellipsis(self.ellipsis);
746
747                if let Some(font) = self.font {
748                    menu = menu.font(font);
749                }
750
751                if let Some(size) = self.size {
752                    menu = menu.text_size(size);
753                }
754
755                Some(menu.overlay(
756                    layout.position() + translation,
757                    *viewport,
758                    bounds.height,
759                    self.menu_height,
760                ))
761            }
762        } else {
763            None
764        }
765    }
766
767    fn operate(
768        &mut self,
769        tree: &mut widget::Tree,
770        layout: Layout<'_>,
771        _renderer: &Renderer,
772        operation: &mut dyn widget::Operation,
773    ) {
774        let state = tree.state.downcast_mut::<Internal<T, Renderer>>();
775        let bounds = layout.bounds();
776
777        operation.focusable(self.id.as_ref(), bounds, &mut state.editor.input);
778        operation.text_input(self.id.as_ref(), bounds, &mut state.editor.input);
779    }
780}
781
782impl<'a, T, Message, Theme, Renderer> From<ComboBox<'a, T, Message, Theme, Renderer>>
783    for Element<'a, Message, Theme, Renderer>
784where
785    T: Display + Clone + 'static,
786    Message: Clone + 'a,
787    Theme: Catalog + 'a,
788    Renderer: text::Renderer + 'static,
789{
790    fn from(combo_box: ComboBox<'a, T, Message, Theme, Renderer>) -> Self {
791        Self::new(combo_box)
792    }
793}
794
795/// The theme catalog of a [`ComboBox`].
796pub trait Catalog: text_input::Catalog + menu::Catalog {
797    /// The default class for the text input of the [`ComboBox`].
798    fn default_input<'a>() -> <Self as text_input::Catalog>::Class<'a> {
799        <Self as text_input::Catalog>::default()
800    }
801
802    /// The default class for the menu of the [`ComboBox`].
803    fn default_menu<'a>() -> <Self as menu::Catalog>::Class<'a> {
804        <Self as menu::Catalog>::default()
805    }
806}
807
808impl Catalog for Theme {}
809
810fn search<'a, T, A>(
811    options: impl IntoIterator<Item = T> + 'a,
812    option_matchers: impl IntoIterator<Item = &'a A> + 'a,
813    query: &'a str,
814) -> impl Iterator<Item = T> + 'a
815where
816    A: AsRef<str> + 'a,
817{
818    let query: Vec<String> = query
819        .to_lowercase()
820        .split(|c: char| !c.is_ascii_alphanumeric())
821        .map(String::from)
822        .collect();
823
824    options
825        .into_iter()
826        .zip(option_matchers)
827        // Make sure each part of the query is found in the option
828        .filter_map(move |(option, matcher)| {
829            if query.iter().all(|part| matcher.as_ref().contains(part)) {
830                Some(option)
831            } else {
832                None
833            }
834        })
835}
836
837fn build_matchers<'a, T>(options: impl IntoIterator<Item = T> + 'a) -> Vec<String>
838where
839    T: Display + 'a,
840{
841    options.into_iter().map(build_matcher).collect()
842}
843
844fn build_matcher<T>(option: T) -> String
845where
846    T: Display,
847{
848    let mut matcher = option.to_string();
849    matcher.retain(|c| c.is_ascii_alphanumeric());
850    matcher.to_lowercase()
851}