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