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