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