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