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//! # use iced::widget::Widget;
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) -> impl Widget<Message> {
29//!     combo_box(
30//!         &state.fruits,
31//!         "Select your favorite fruit...",
32//!         state.favorite.as_ref(),
33//!         Message::FruitSelected
34//!     )
35//! }
36//!
37//! fn update(state: &mut State, message: Message) {
38//!     match message {
39//!         Message::FruitSelected(fruit) => {
40//!             state.favorite = Some(fruit);
41//!         }
42//!     }
43//! }
44//!
45//! impl std::fmt::Display for Fruit {
46//!     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47//!         f.write_str(match self {
48//!             Self::Apple => "Apple",
49//!             Self::Orange => "Orange",
50//!             Self::Strawberry => "Strawberry",
51//!             Self::Tomato => "Tomato",
52//!         })
53//!     }
54//! }
55//! ```
56use crate::core::keyboard;
57use crate::core::keyboard::key;
58use crate::core::layout::{self, Layout};
59use crate::core::mouse;
60use crate::core::overlay;
61use crate::core::renderer;
62use crate::core::text;
63use crate::core::text::editor;
64use crate::core::text::input;
65use crate::core::widget::operation::Focusable as _;
66use crate::core::widget::{self, Widget};
67use crate::core::window;
68use crate::core::{Event, Font, Length, Padding, Pixels, Rectangle, Shell, Size, Theme, Vector};
69use crate::overlay::menu::{self, Menu};
70use crate::text::LineHeight;
71use crate::text_input;
72
73use std::fmt::Display;
74use std::sync::atomic::{self, AtomicU64};
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/// # use iced::widget::Widget;
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) -> impl Widget<Message> {
104///     combo_box(
105///         &state.fruits,
106///         "Select your favorite fruit...",
107///         state.favorite.as_ref(),
108///         Message::FruitSelected
109///     )
110/// }
111///
112/// fn update(state: &mut State, message: Message) {
113///     match message {
114///         Message::FruitSelected(fruit) => {
115///             state.favorite = Some(fruit);
116///         }
117///     }
118/// }
119///
120/// impl std::fmt::Display for Fruit {
121///     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122///         f.write_str(match self {
123///             Self::Apple => "Apple",
124///             Self::Orange => "Orange",
125///             Self::Strawberry => "Strawberry",
126///             Self::Tomato => "Tomato",
127///         })
128///     }
129/// }
130/// ```
131pub struct ComboBox<'a, T, Message, Theme = crate::Theme>
132where
133    Theme: Catalog,
134{
135    state: &'a State<T>,
136    id: Option<widget::Id>,
137    placeholder: text::Fragment<'a>,
138    selection: String,
139    width: Length,
140    line_height: Option<LineHeight>,
141    font: Option<Font>,
142    on_selected: Box<dyn Fn(T) -> Message + 'a>,
143    on_option_hovered: Option<Box<dyn Fn(T) -> Message + 'a>>,
144    on_open: Option<Message>,
145    on_close: Option<Message>,
146    on_input: Option<Box<dyn Fn(String) -> Message + 'a>>,
147    padding: Padding,
148    size: Option<Pixels>,
149    shaping: text::Shaping,
150    ellipsis: text::Ellipsis,
151    input_class: <Theme as text_input::Catalog>::Class<'a>,
152    menu_class: <Theme as menu::Catalog>::Class<'a>,
153    menu_height: Length,
154    last_status: Option<text_input::Status>,
155}
156
157impl<'a, T, Message, Theme> ComboBox<'a, T, Message, Theme>
158where
159    T: std::fmt::Display + Clone,
160    Theme: Catalog,
161{
162    /// Creates a new [`ComboBox`] with the given list of options, a placeholder,
163    /// the current selected value, and the message to produce when an option is
164    /// selected.
165    pub fn new(
166        state: &'a State<T>,
167        placeholder: impl text::IntoFragment<'a>,
168        selection: Option<&T>,
169        on_selected: impl Fn(T) -> Message + 'a,
170    ) -> Self {
171        Self {
172            state,
173            id: None,
174            placeholder: placeholder.into_fragment(),
175            selection: selection.map(T::to_string).unwrap_or_default(),
176            width: Length::Fill,
177            line_height: None,
178            font: None,
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            shaping: text::Shaping::default(),
187            ellipsis: text::Ellipsis::End,
188            input_class: <Theme as Catalog>::default_input(),
189            menu_class: <Theme as Catalog>::default_menu(),
190            menu_height: Length::Fit,
191            last_status: None,
192        }
193    }
194
195    /// Sets the [`widget::Id`] of the [`ComboBox`].
196    pub fn id(mut self, id: impl Into<widget::Id>) -> Self {
197        self.id = Some(id.into());
198        self
199    }
200
201    /// Sets the message that should be produced when some text is typed into
202    /// the [`ComboBox`].
203    pub fn on_input(mut self, on_input: impl Fn(String) -> Message + 'a) -> 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(mut self, on_option_hovered: impl Fn(T) -> Message + 'a) -> Self {
211        self.on_option_hovered = Some(Box::new(on_option_hovered));
212        self
213    }
214
215    /// Sets the message that will be produced when the  [`ComboBox`] is
216    /// opened.
217    pub fn on_open(mut self, message: Message) -> Self {
218        self.on_open = Some(message);
219        self
220    }
221
222    /// Sets the message that will be produced when the outside area
223    /// of the [`ComboBox`] is pressed.
224    pub fn on_close(mut self, message: Message) -> Self {
225        self.on_close = Some(message);
226        self
227    }
228
229    /// Sets the [`Padding`] of the [`ComboBox`].
230    pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
231        self.padding = padding.into();
232        self
233    }
234
235    /// Sets the [`Font`] of the [`ComboBox`].
236    ///
237    /// [`Font`]: crate::core::Font
238    pub fn font(mut self, font: Font) -> Self {
239        self.font = Some(font);
240        self
241    }
242
243    /// Sets the text sixe of the [`ComboBox`].
244    pub fn size(mut self, size: impl Into<Pixels>) -> Self {
245        self.size = Some(size.into());
246        self
247    }
248
249    /// Sets the width of the [`ComboBox`].
250    pub fn width(mut self, width: impl Into<Length>) -> Self {
251        self.width = width.into();
252        self
253    }
254
255    /// Sets the [`LineHeight`] of the [`ComboBox`].
256    pub fn line_height(mut self, line_height: impl Into<LineHeight>) -> Self {
257        self.line_height = Some(line_height.into());
258        self
259    }
260
261    /// Sets the height of the menu of the [`ComboBox`].
262    pub fn menu_height(mut self, menu_height: impl Into<Length>) -> Self {
263        self.menu_height = menu_height.into();
264        self
265    }
266
267    /// Sets the [`text::Shaping`] strategy of the [`ComboBox`].
268    pub fn shaping(mut self, shaping: text::Shaping) -> Self {
269        self.shaping = shaping;
270        self
271    }
272
273    /// Sets the [`text::Ellipsis`] strategy of the [`ComboBox`].
274    pub fn ellipsis(mut self, ellipsis: text::Ellipsis) -> Self {
275        self.ellipsis = ellipsis;
276        self
277    }
278
279    /// Sets the style of the input of the [`ComboBox`].
280    #[must_use]
281    pub fn input_style(
282        mut self,
283        style: impl Fn(&Theme, text_input::Status) -> text_input::Style + 'a,
284    ) -> Self
285    where
286        <Theme as text_input::Catalog>::Class<'a>: From<text_input::StyleFn<'a, Theme>>,
287    {
288        self.input_class = (Box::new(style) as text_input::StyleFn<'a, Theme>).into();
289        self
290    }
291
292    /// Sets the style of the menu of the [`ComboBox`].
293    #[must_use]
294    pub fn menu_style(mut self, style: impl Fn(&Theme) -> menu::Style + 'a) -> Self
295    where
296        <Theme as menu::Catalog>::Class<'a>: From<menu::StyleFn<'a, Theme>>,
297    {
298        self.menu_class = (Box::new(style) as menu::StyleFn<'a, Theme>).into();
299        self
300    }
301
302    /// Sets the style class of the input of the [`ComboBox`].
303    #[cfg(feature = "advanced")]
304    #[must_use]
305    pub fn input_class(
306        mut self,
307        class: impl Into<<Theme as text_input::Catalog>::Class<'a>>,
308    ) -> Self {
309        self.input_class = class.into();
310        self
311    }
312
313    /// Sets the style class of the menu of the [`ComboBox`].
314    #[cfg(feature = "advanced")]
315    #[must_use]
316    pub fn menu_class(mut self, class: impl Into<<Theme as menu::Catalog>::Class<'a>>) -> Self {
317        self.menu_class = class.into();
318        self
319    }
320}
321
322/// The local state of a [`ComboBox`].
323#[derive(Debug, Clone)]
324pub struct State<T> {
325    options: Vec<T>,
326    version: u64,
327}
328
329static VERSION: AtomicU64 = AtomicU64::new(0);
330
331impl<T> State<T>
332where
333    T: Display + Clone,
334{
335    /// Creates a new [`State`] for a [`ComboBox`] with the given list of options.
336    pub fn new(options: Vec<T>) -> Self {
337        Self {
338            options,
339            version: VERSION.fetch_add(1, atomic::Ordering::Relaxed),
340        }
341    }
342
343    /// Returns the options of the [`State`].
344    ///
345    /// These are the options provided when the [`State`]
346    /// was constructed with [`State::new`].
347    pub fn options(&self) -> &[T] {
348        &self.options
349    }
350
351    /// Pushes a new option to the [`State`].
352    pub fn push(&mut self, new_option: T) {
353        self.options.push(new_option);
354        self.version = VERSION.fetch_add(1, atomic::Ordering::Relaxed);
355    }
356
357    /// Returns ownership of the options of the [`State`].
358    pub fn into_options(self) -> Vec<T> {
359        self.options
360    }
361}
362
363impl<T> Default for State<T>
364where
365    T: Display + Clone,
366{
367    fn default() -> Self {
368        Self::new(Vec::new())
369    }
370}
371
372struct Internal<T, R: text::Renderer> {
373    editor: Editor<R>,
374    menu: menu::State,
375    hovered_option: Option<usize>,
376    option_matchers: Vec<String>,
377    filtered_options: Vec<T>,
378    version: u64,
379}
380
381impl<T: Display + Clone, R: text::Renderer> Internal<T, R> {
382    fn hovered_option(&self) -> usize {
383        let index = self.hovered_option.unwrap_or_default();
384
385        index.min(self.filtered_options.len().saturating_sub(1))
386    }
387
388    fn filter(&mut self, options: &[T], value: &str) {
389        self.option_matchers = build_matchers(options);
390        self.filtered_options = search(options, &self.option_matchers, value)
391            .cloned()
392            .collect();
393    }
394}
395
396struct Editor<R: text::Renderer> {
397    input: text::Input<R>,
398    selection: Option<String>,
399}
400
401impl<T, Message, Theme> widget::Meta for ComboBox<'_, T, Message, Theme> where Theme: Catalog {}
402
403impl<T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
404    for ComboBox<'_, T, Message, Theme>
405where
406    T: Display + Clone + 'static,
407    Message: Clone,
408    Theme: Catalog,
409    Renderer: text::Renderer + 'static,
410{
411    fn size(&self) -> Size<Length> {
412        Size {
413            width: self.width,
414            height: Length::Fit,
415        }
416    }
417
418    fn layout(&mut self, tree: &mut widget::Tree, renderer: &Renderer, limits: &layout::Limits) {
419        let state = tree.state.downcast_mut::<Internal<T, Renderer>>();
420
421        tree.size = state.editor.input.layout(
422            renderer,
423            limits,
424            input::Layout {
425                width: self.width,
426                height: Length::Fit,
427                padding: self.padding,
428                placeholder: &self.placeholder,
429                font: self.font,
430                size: self.size,
431                line_height: self.line_height,
432                alignment: text::Alignment::Default,
433                multiline: None,
434                is_secure: false,
435            },
436        );
437    }
438
439    fn tag(&self) -> widget::tree::Tag {
440        widget::tree::Tag::of::<Internal<T, Renderer>>()
441    }
442
443    fn state(&self) -> widget::tree::State {
444        widget::tree::State::new(Internal::<T, Renderer> {
445            editor: Editor {
446                input: text::Input::new(),
447                selection: None,
448            },
449            menu: menu::State::new(),
450            filtered_options: Vec::new(),
451            option_matchers: Vec::new(),
452            hovered_option: Some(0),
453            version: 0,
454        })
455    }
456
457    fn diff(&mut self, tree: &mut widget::Tree) {
458        let state = tree.state.downcast_mut::<Internal<T, Renderer>>();
459
460        if state.version != self.state.version
461            || state.editor.selection.as_deref() != Some(&self.selection)
462        {
463            state.editor.input.overwrite(&self.selection);
464            state.editor.selection = Some(self.selection.clone());
465            state.filter(&self.state.options, &self.selection);
466            state.version = self.state.version;
467        }
468    }
469
470    fn update(
471        &mut self,
472        tree: &mut widget::Tree,
473        event: &Event,
474        layout: Layout,
475        cursor: mouse::Cursor,
476        _renderer: &Renderer,
477        shell: &mut Shell<'_, Message>,
478        _viewport: &Rectangle,
479    ) {
480        let internal = tree.state.downcast_mut::<Internal<T, Renderer>>();
481
482        let was_focused = internal.editor.input.is_focused();
483
484        let edit = internal.editor.input.update::<Message>(
485            event,
486            layout.bounds(),
487            cursor,
488            shell,
489            editor::Binding::from_key_press,
490        );
491
492        if edit.is_some() {
493            let value = internal.editor.input.value();
494
495            if let Some(on_input) = &self.on_input {
496                shell.publish(on_input(value.clone()));
497            }
498
499            internal.filter(&self.state.options, &value);
500        }
501
502        let is_focused = internal.editor.input.is_focused();
503
504        if is_focused {
505            if !was_focused {
506                internal.editor.input.overwrite("");
507                internal.filtered_options = self.state.options.clone();
508
509                if let Some(on_option_hovered) = &mut self.on_option_hovered {
510                    let hovered_option = internal.hovered_option.unwrap_or(0);
511
512                    if let Some(option) = internal.filtered_options.get(hovered_option) {
513                        shell.publish(on_option_hovered(option.clone()));
514                    }
515                }
516            }
517
518            if let Event::Keyboard(keyboard::Event::KeyPressed {
519                key: keyboard::Key::Named(named_key),
520                modifiers,
521                ..
522            }) = event
523            {
524                match (named_key, modifiers.shift()) {
525                    (key::Named::Enter, _) => {
526                        if let Some(option) = internal
527                            .filtered_options
528                            .get(internal.hovered_option())
529                            .cloned()
530                        {
531                            internal.menu = menu::State::default();
532                            internal.editor.selection = None;
533                            internal.editor.input.overwrite("");
534                            internal.editor.input.unfocus();
535
536                            shell.publish((self.on_selected)(option));
537                        }
538
539                        shell.capture_event();
540                        shell.request_redraw();
541                    }
542                    (key::Named::ArrowUp, _) | (key::Named::Tab, true) => {
543                        let index = internal.hovered_option();
544
545                        internal.hovered_option = Some(if index == 0 {
546                            internal.filtered_options.len().saturating_sub(1)
547                        } else {
548                            index.saturating_sub(1)
549                        });
550
551                        if let Some(on_option_hovered) = &mut self.on_option_hovered
552                            && let Some(option) = internal
553                                .hovered_option
554                                .and_then(|index| internal.filtered_options.get(index))
555                        {
556                            shell.publish((on_option_hovered)(option.clone()));
557                        }
558
559                        shell.capture_event();
560                        shell.request_redraw();
561                    }
562                    (key::Named::ArrowDown, _) | (key::Named::Tab, false) => {
563                        let index = internal.hovered_option();
564
565                        internal.hovered_option = Some(
566                            if index >= internal.filtered_options.len().saturating_sub(1) {
567                                0
568                            } else {
569                                index
570                                    .saturating_add(1)
571                                    .min(internal.filtered_options.len().saturating_sub(1))
572                            },
573                        );
574
575                        if let Some(on_option_hovered) = &mut self.on_option_hovered
576                            && let Some(option) = internal
577                                .hovered_option
578                                .and_then(|index| internal.filtered_options.get(index))
579                        {
580                            shell.publish((on_option_hovered)(option.clone()));
581                        }
582
583                        shell.capture_event();
584                        shell.request_redraw();
585                    }
586                    _ => {}
587                }
588            }
589        }
590
591        if was_focused != is_focused {
592            if is_focused {
593                if let Some(on_open) = self.on_open.take() {
594                    shell.publish(on_open);
595                }
596            } else if let Some(on_close) = self.on_close.take() {
597                internal.editor.input.overwrite(&self.selection);
598                shell.publish(on_close);
599            }
600        }
601
602        let status = if internal.editor.input.is_focused() {
603            text_input::Status::Focused {
604                is_hovered: cursor.is_over(layout.bounds()),
605            }
606        } else if cursor.is_over(layout.bounds()) {
607            text_input::Status::Hovered
608        } else {
609            text_input::Status::Active
610        };
611
612        if let Event::Window(window::Event::RedrawRequested(_now)) = event {
613            self.last_status = Some(status);
614
615            shell.request_input_method(
616                &internal
617                    .editor
618                    .input
619                    .input_method(layout.bounds().shrink(self.padding).position()),
620            );
621        } else if self
622            .last_status
623            .is_some_and(|last_status| status != last_status)
624        {
625            shell.request_redraw();
626        }
627    }
628
629    fn mouse_interaction(
630        &self,
631        _tree: &widget::Tree,
632        layout: Layout,
633        cursor: mouse::Cursor,
634        _viewport: &Rectangle,
635        _renderer: &Renderer,
636    ) -> mouse::Interaction {
637        if cursor.is_over(layout.bounds()) {
638            mouse::Interaction::Text
639        } else {
640            mouse::Interaction::default()
641        }
642    }
643
644    fn draw(
645        &self,
646        tree: &widget::Tree,
647        renderer: &mut Renderer,
648        theme: &Theme,
649        _style: &renderer::Style,
650        layout: Layout,
651        _cursor: mouse::Cursor,
652        viewport: &Rectangle,
653    ) {
654        let internal = tree.state.downcast_ref::<Internal<T, Renderer>>();
655
656        let bounds = layout.bounds();
657        let style = text_input::Catalog::style(
658            theme,
659            &self.input_class,
660            self.last_status.unwrap_or(text_input::Status::Disabled),
661        );
662
663        renderer.fill_quad(
664            renderer::Quad {
665                bounds,
666                border: style.border,
667                ..renderer::Quad::default()
668            },
669            style.background,
670        );
671
672        internal.editor.input.draw(
673            renderer,
674            bounds,
675            *viewport,
676            input::Style {
677                value: style.value,
678                selection: style.selection,
679                placeholder: style.placeholder,
680            },
681        );
682    }
683
684    fn overlay<'b>(
685        &'b mut self,
686        tree: &'b mut widget::Tree,
687        layout: Layout,
688        renderer: &Renderer,
689        _viewport: &Rectangle,
690        translation: Vector,
691        window: Size,
692    ) -> Vec<overlay::Element<'b, Message, Theme, Renderer>> {
693        let internal = tree.state.downcast_mut::<Internal<T, Renderer>>();
694        let is_focused = internal.editor.input.is_focused();
695
696        if is_focused {
697            if internal.filtered_options.is_empty() {
698                Vec::new()
699            } else {
700                let bounds = layout.bounds();
701                let position = layout.position() + translation;
702
703                let mut menu = Menu::new(
704                    &mut internal.menu,
705                    &internal.filtered_options,
706                    &mut internal.hovered_option,
707                    &T::to_string,
708                    |selection| {
709                        internal.editor.selection = None;
710                        internal.editor.input.overwrite("");
711                        internal.editor.input.unfocus();
712
713                        (self.on_selected)(selection)
714                    },
715                    self.on_option_hovered.as_deref(),
716                    &self.menu_class,
717                )
718                .width(bounds.width)
719                .height(self.menu_height)
720                .padding(self.padding)
721                .shaping(self.shaping)
722                .ellipsis(self.ellipsis);
723
724                if let Some(font) = self.font {
725                    menu = menu.font(font);
726                }
727
728                if let Some(size) = self.size {
729                    menu = menu.text_size(size);
730                }
731
732                vec![menu.overlay(renderer, position, window, bounds.height)]
733            }
734        } else {
735            Vec::new()
736        }
737    }
738
739    fn operate(
740        &mut self,
741        tree: &mut widget::Tree,
742        layout: Layout,
743        _viewport: &Rectangle,
744        _renderer: &Renderer,
745        operation: &mut dyn widget::Operation,
746    ) {
747        let state = tree.state.downcast_mut::<Internal<T, Renderer>>();
748        let bounds = layout.bounds();
749
750        operation.focusable(self.id.as_ref(), bounds, &mut state.editor.input);
751        operation.text_input(self.id.as_ref(), bounds, &mut state.editor.input);
752    }
753}
754
755/// The theme catalog of a [`ComboBox`].
756pub trait Catalog: text_input::Catalog + menu::Catalog {
757    /// The default class for the text input of the [`ComboBox`].
758    fn default_input<'a>() -> <Self as text_input::Catalog>::Class<'a> {
759        <Self as text_input::Catalog>::default()
760    }
761
762    /// The default class for the menu of the [`ComboBox`].
763    fn default_menu<'a>() -> <Self as menu::Catalog>::Class<'a> {
764        <Self as menu::Catalog>::default()
765    }
766}
767
768impl Catalog for Theme {}
769
770fn search<'a, T, A>(
771    options: impl IntoIterator<Item = T> + 'a,
772    option_matchers: impl IntoIterator<Item = &'a A> + 'a,
773    query: &'a str,
774) -> impl Iterator<Item = T> + 'a
775where
776    A: AsRef<str> + 'a,
777{
778    let query: Vec<String> = query
779        .to_lowercase()
780        .split(|c: char| !c.is_ascii_alphanumeric())
781        .map(String::from)
782        .collect();
783
784    options
785        .into_iter()
786        .zip(option_matchers)
787        // Make sure each part of the query is found in the option
788        .filter_map(move |(option, matcher)| {
789            if query.iter().all(|part| matcher.as_ref().contains(part)) {
790                Some(option)
791            } else {
792                None
793            }
794        })
795}
796
797fn build_matchers<'a, T>(options: impl IntoIterator<Item = T> + 'a) -> Vec<String>
798where
799    T: Display + 'a,
800{
801    options.into_iter().map(build_matcher).collect()
802}
803
804fn build_matcher<T>(option: T) -> String
805where
806    T: Display,
807{
808    let mut matcher = option.to_string();
809    matcher.retain(|c| c.is_ascii_alphanumeric());
810    matcher.to_lowercase()
811}