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