Skip to main content

material_ui_rs/widget/component/
combobox.rs

1//! Material 3 searchable combobox constructors with token-backed layout defaults.
2
3use std::cell::RefCell;
4use std::fmt::{self, Display};
5
6use iced_widget::core::keyboard::key;
7use iced_widget::core::text as core_text;
8use iced_widget::core::text::paragraph;
9use iced_widget::core::time::Instant;
10use iced_widget::core::widget::{self, Widget};
11use iced_widget::core::{
12    Clipboard, Color, Element, Event, Layout, Length, Padding, Pixels, Point, Rectangle, Shell,
13    Size, Vector, keyboard, layout, mouse, overlay, renderer,
14};
15use iced_widget::overlay::menu as overlay_menu;
16use iced_widget::text::{self, LineHeight};
17use iced_widget::text_input::{self, Icon, TextInput};
18
19use super::menu_overlay;
20use super::{
21    MobileTextInputState, TextInputUpdateContext, absolute_line_height, draw_text_field_notched,
22    mobile_text_input_activation, register_mobile_text_region, select, sync_mobile_keyboard,
23    text_field_floating_label_notch, update_mobile_text_input,
24};
25use crate::style::{menu as menu_style, text_input as text_input_style};
26use crate::{Theme, tokens};
27
28#[derive(Clone)]
29enum DisplayValue<T> {
30    Option(T),
31    Input(String),
32}
33
34impl<T> fmt::Display for DisplayValue<T>
35where
36    T: fmt::Display,
37{
38    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39        match self {
40            Self::Option(option) => option.fmt(f),
41            Self::Input(input) => input.fmt(f),
42        }
43    }
44}
45
46/// Searchable select state.
47///
48/// The inner state keeps the current search query and filtered Material menu
49/// options. The public state also stores the original options so selected
50/// values and typed input can be mirrored by the demo/application state without
51/// replacing user-entered text on blur.
52pub struct State<T> {
53    options: Vec<T>,
54    inner: SearchState<DisplayValue<T>>,
55}
56
57impl<T> fmt::Debug for State<T>
58where
59    T: fmt::Debug,
60{
61    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62        f.debug_struct("State")
63            .field("options", &self.options)
64            .finish_non_exhaustive()
65    }
66}
67
68impl<T> State<T>
69where
70    T: fmt::Display + Clone,
71{
72    /// Creates a new [`State`] for a combobox with the given list of options.
73    pub fn new(options: Vec<T>) -> Self {
74        Self::with_selection(options, None)
75    }
76
77    /// Creates a new [`State`] for a combobox with the given list of options
78    /// and selected value.
79    pub fn with_selection(options: Vec<T>, selection: Option<&T>) -> Self {
80        let inner_options = inner_options(&options);
81        let inner_selection = selection.cloned().map(DisplayValue::Option);
82
83        Self {
84            options,
85            inner: SearchState::with_selection(inner_options, inner_selection.as_ref()),
86        }
87    }
88
89    /// Returns the original options.
90    pub fn options(&self) -> &[T] {
91        &self.options
92    }
93
94    /// Pushes a new option.
95    pub fn push(&mut self, new_option: T) {
96        self.inner.push(DisplayValue::Option(new_option.clone()));
97        self.options.push(new_option);
98    }
99
100    /// Returns ownership of the original options.
101    pub fn into_options(self) -> Vec<T> {
102        self.options
103    }
104
105    /// Synchronizes the internal query with the latest user input.
106    pub fn set_input(&mut self, input: impl Into<String>) {
107        let input = input.into();
108        let inner_selection = if input.is_empty() {
109            None
110        } else {
111            Some(DisplayValue::Input(input))
112        };
113
114        self.inner =
115            SearchState::with_selection(inner_options(&self.options), inner_selection.as_ref());
116    }
117
118    /// Synchronizes the internal query with the selected option.
119    pub fn set_selection(&mut self, selection: Option<&T>) {
120        let inner_selection = selection.cloned().map(DisplayValue::Option);
121
122        self.inner =
123            SearchState::with_selection(inner_options(&self.options), inner_selection.as_ref());
124    }
125
126    fn inner(&self) -> &SearchState<DisplayValue<T>> {
127        &self.inner
128    }
129}
130
131impl<T> Default for State<T>
132where
133    T: fmt::Display + Clone,
134{
135    fn default() -> Self {
136        Self::new(Vec::new())
137    }
138}
139
140/// Material combobox.
141pub struct Combobox<'a, T, Message, Renderer>
142where
143    T: fmt::Display + Clone,
144    Renderer: core_text::Renderer,
145{
146    inner: ComboboxCore<'a, DisplayValue<T>, Message, Renderer>,
147}
148
149impl<T, Message, Renderer> fmt::Debug for Combobox<'_, T, Message, Renderer>
150where
151    T: fmt::Display + Clone,
152    Renderer: core_text::Renderer,
153{
154    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155        f.debug_struct("Combobox").finish_non_exhaustive()
156    }
157}
158
159impl<'a, T, Message, Renderer> Combobox<'a, T, Message, Renderer>
160where
161    T: fmt::Display + Clone + 'static,
162    Renderer: core_text::Renderer + 'a,
163{
164    /// Sets the message that should be produced when text is typed.
165    pub fn on_input(mut self, on_input: impl Fn(String) -> Message + 'static) -> Self {
166        self.inner = self.inner.on_input(on_input);
167        self
168    }
169
170    /// Sets the message that will be produced when an option is hovered.
171    pub fn on_option_hovered(mut self, on_option_hovered: impl Fn(T) -> Message + 'static) -> Self {
172        self.inner = self.inner.on_option_hovered(move |value| match value {
173            DisplayValue::Option(option) => on_option_hovered(option),
174            DisplayValue::Input(_) => {
175                unreachable!("typed input is not a selectable option")
176            }
177        });
178        self
179    }
180
181    /// Sets the message that will be produced when the combobox opens.
182    pub fn on_open(mut self, message: Message) -> Self {
183        self.inner = self.inner.on_open(message);
184        self
185    }
186
187    /// Sets the message that will be produced when the combobox closes.
188    pub fn on_close(mut self, message: Message) -> Self {
189        self.inner = self.inner.on_close(message);
190        self
191    }
192
193    /// Sets the floating label of the combobox.
194    pub fn label(mut self, label: impl Into<String>) -> Self {
195        self.inner = self.inner.label(label);
196        self
197    }
198
199    /// Sets the padding.
200    pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
201        self.inner = self.inner.padding(padding);
202        self
203    }
204
205    /// Sets the font.
206    pub fn font(mut self, font: Renderer::Font) -> Self {
207        self.inner = self.inner.font(font);
208        self
209    }
210
211    /// Sets the trailing icon.
212    pub fn icon(mut self, icon: Icon<Renderer::Font>) -> Self {
213        self.inner = self.inner.icon(icon);
214        self
215    }
216
217    /// Sets the text size.
218    pub fn size(mut self, size: impl Into<Pixels>) -> Self {
219        self.inner = self.inner.size(size);
220        self
221    }
222
223    /// Sets the text line height.
224    pub fn line_height(mut self, line_height: impl Into<LineHeight>) -> Self {
225        self.inner = self.inner.line_height(line_height);
226        self
227    }
228
229    /// Sets the width.
230    pub fn width(mut self, width: impl Into<Length>) -> Self {
231        self.inner = self.inner.width(width);
232        self
233    }
234
235    /// Sets the menu height.
236    pub fn menu_height(mut self, menu_height: impl Into<Length>) -> Self {
237        self.inner = self.inner.menu_height(menu_height);
238        self
239    }
240
241    /// Sets the text shaping strategy.
242    pub fn text_shaping(mut self, shaping: text::Shaping) -> Self {
243        self.inner = self.inner.text_shaping(shaping);
244        self
245    }
246
247    /// Sets the input style.
248    pub fn input_style(
249        mut self,
250        style: impl Fn(&Theme, text_input::Status) -> text_input::Style + 'a,
251    ) -> Self
252    where
253        <Theme as text_input::Catalog>::Class<'a>: From<text_input::StyleFn<'a, Theme>>,
254    {
255        self.inner = self.inner.input_style(style);
256        self
257    }
258
259    /// Sets the menu style.
260    pub fn menu_style(mut self, style: impl Fn(&Theme) -> overlay_menu::Style + 'a) -> Self
261    where
262        <Theme as overlay_menu::Catalog>::Class<'a>: From<overlay_menu::StyleFn<'a, Theme>>,
263    {
264        self.inner = self.inner.menu_style(style);
265        self
266    }
267}
268
269impl<'a, T, Message, Renderer> From<Combobox<'a, T, Message, Renderer>>
270    for Element<'a, Message, Theme, Renderer>
271where
272    T: fmt::Display + Clone + 'static,
273    Message: Clone + 'a,
274    Renderer: core_text::Renderer + 'a,
275{
276    fn from(combobox: Combobox<'a, T, Message, Renderer>) -> Self {
277        Element::new(combobox.inner)
278    }
279}
280
281pub fn outlined<'a, T, Message, Renderer>(
282    state: &'a State<T>,
283    placeholder: &str,
284    selection: Option<&T>,
285    on_selected: impl Fn(T) -> Message + 'static,
286) -> Combobox<'a, T, Message, Renderer>
287where
288    T: fmt::Display + Clone + 'static,
289    Renderer: core_text::Renderer + 'a,
290{
291    outlined_with_input(state, placeholder, "", selection, on_selected)
292}
293
294pub fn outlined_with_input<'a, T, Message, Renderer>(
295    state: &'a State<T>,
296    placeholder: &str,
297    input: &str,
298    selection: Option<&T>,
299    on_selected: impl Fn(T) -> Message + 'static,
300) -> Combobox<'a, T, Message, Renderer>
301where
302    T: fmt::Display + Clone + 'static,
303    Renderer: core_text::Renderer + 'a,
304{
305    let display_value = if input.is_empty() {
306        selection.cloned().map(DisplayValue::Option)
307    } else {
308        Some(DisplayValue::Input(input.to_owned()))
309    };
310
311    let inner = ComboboxCore::new(state.inner(), placeholder, display_value.as_ref(), {
312        move |value| match value {
313            DisplayValue::Option(option) => on_selected(option),
314            DisplayValue::Input(_) => {
315                unreachable!("typed input is not a selectable option")
316            }
317        }
318    })
319    .padding(Padding {
320        top: tokens::component::text_field::TOP_SPACE,
321        right: tokens::component::text_field::TRAILING_SPACE,
322        bottom: tokens::component::text_field::BOTTOM_SPACE,
323        left: tokens::component::text_field::LEADING_SPACE,
324    })
325    .option_padding(select::menu_option_padding())
326    .size(tokens::component::text_field::INPUT_TEXT_SIZE)
327    .line_height(absolute_line_height(
328        tokens::component::text_field::INPUT_TEXT_LINE_HEIGHT,
329    ))
330    .width(Length::Fill)
331    .input_style(text_input_style::default)
332    .menu_style(menu_style::outlined_select);
333
334    Combobox { inner }
335}
336
337struct ComboboxCore<'a, T, Message, Renderer>
338where
339    T: Display + Clone,
340    Renderer: core_text::Renderer,
341{
342    state: &'a SearchState<T>,
343    text_input: TextInput<'a, TextInputEvent, Theme, Renderer>,
344    label: Option<String>,
345    font: Option<Renderer::Font>,
346    selection: text_input::Value,
347    on_selected: Box<dyn Fn(T) -> Message>,
348    on_option_hovered: Option<Box<dyn Fn(T) -> Message>>,
349    on_open: Option<Message>,
350    on_close: Option<Message>,
351    on_input: Option<Box<dyn Fn(String) -> Message>>,
352    input_padding: Padding,
353    option_padding: Padding,
354    size: Option<Pixels>,
355    line_height: LineHeight,
356    text_shaping: text::Shaping,
357    menu_class: <Theme as overlay_menu::Catalog>::Class<'a>,
358    menu_height: Length,
359}
360
361impl<T, Message, Renderer> fmt::Debug for ComboboxCore<'_, T, Message, Renderer>
362where
363    T: Display + Clone,
364    Renderer: core_text::Renderer,
365{
366    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
367        f.debug_struct("ComboboxCore").finish_non_exhaustive()
368    }
369}
370
371impl<'a, T, Message, Renderer> ComboboxCore<'a, T, Message, Renderer>
372where
373    T: Display + Clone,
374    Renderer: core_text::Renderer,
375{
376    fn new(
377        state: &'a SearchState<T>,
378        placeholder: &str,
379        selection: Option<&T>,
380        on_selected: impl Fn(T) -> Message + 'static,
381    ) -> Self {
382        let text_input = TextInput::new(placeholder, &state.value())
383            .on_input(TextInputEvent::TextChanged)
384            .class(<Theme as text_input::Catalog>::default());
385
386        let selection = selection.map(T::to_string).unwrap_or_default();
387
388        Self {
389            state,
390            text_input,
391            label: None,
392            font: None,
393            selection: text_input::Value::new(&selection),
394            on_selected: Box::new(on_selected),
395            on_option_hovered: None,
396            on_input: None,
397            on_open: None,
398            on_close: None,
399            input_padding: text_input::DEFAULT_PADDING,
400            option_padding: select::menu_option_padding(),
401            size: None,
402            line_height: LineHeight::default(),
403            text_shaping: text::Shaping::default(),
404            menu_class: <Theme as overlay_menu::Catalog>::default(),
405            menu_height: Length::Shrink,
406        }
407    }
408
409    fn on_input(mut self, on_input: impl Fn(String) -> Message + 'static) -> Self {
410        self.on_input = Some(Box::new(on_input));
411        self
412    }
413
414    fn on_option_hovered(mut self, on_option_hovered: impl Fn(T) -> Message + 'static) -> Self {
415        self.on_option_hovered = Some(Box::new(on_option_hovered));
416        self
417    }
418
419    fn on_open(mut self, message: Message) -> Self {
420        self.on_open = Some(message);
421        self
422    }
423
424    fn on_close(mut self, message: Message) -> Self {
425        self.on_close = Some(message);
426        self
427    }
428
429    fn label(mut self, label: impl Into<String>) -> Self {
430        self.label = Some(label.into());
431        self
432    }
433
434    fn padding(mut self, padding: impl Into<Padding>) -> Self {
435        self.input_padding = padding.into();
436        self.text_input = self.text_input.padding(self.input_padding);
437        self
438    }
439
440    fn option_padding(mut self, padding: impl Into<Padding>) -> Self {
441        self.option_padding = padding.into();
442        self
443    }
444
445    fn font(mut self, font: Renderer::Font) -> Self {
446        self.text_input = self.text_input.font(font);
447        self.font = Some(font);
448        self
449    }
450
451    fn icon(mut self, icon: Icon<Renderer::Font>) -> Self {
452        self.text_input = self.text_input.icon(icon);
453        self
454    }
455
456    fn size(mut self, size: impl Into<Pixels>) -> Self {
457        let size = size.into();
458
459        self.text_input = self.text_input.size(size);
460        self.size = Some(size);
461
462        self
463    }
464
465    fn line_height(mut self, line_height: impl Into<LineHeight>) -> Self {
466        self.line_height = line_height.into();
467        self.text_input = self.text_input.line_height(self.line_height);
468        self
469    }
470
471    fn width(mut self, width: impl Into<Length>) -> Self {
472        self.text_input = self.text_input.width(width);
473        self
474    }
475
476    fn menu_height(mut self, menu_height: impl Into<Length>) -> Self {
477        self.menu_height = menu_height.into();
478        self
479    }
480
481    fn text_shaping(mut self, shaping: text::Shaping) -> Self {
482        self.text_shaping = shaping;
483        self
484    }
485
486    fn input_style(
487        mut self,
488        style: impl Fn(&Theme, text_input::Status) -> text_input::Style + 'a,
489    ) -> Self
490    where
491        <Theme as text_input::Catalog>::Class<'a>: From<text_input::StyleFn<'a, Theme>>,
492    {
493        self.text_input = self.text_input.style(style);
494        self
495    }
496
497    fn menu_style(mut self, style: impl Fn(&Theme) -> overlay_menu::Style + 'a) -> Self
498    where
499        <Theme as overlay_menu::Catalog>::Class<'a>: From<overlay_menu::StyleFn<'a, Theme>>,
500    {
501        self.menu_class = Box::new(style) as overlay_menu::StyleFn<'a, Theme>;
502        self
503    }
504
505    fn intrinsic_menu_height(&self, option_count: usize, renderer: &Renderer) -> f32 {
506        let text_size = self.size.unwrap_or_else(|| renderer.default_size());
507        let option_height =
508            f32::from(self.line_height.to_absolute(text_size)) + self.option_padding.y();
509
510        option_height * option_count as f32
511    }
512}
513
514#[derive(Debug, Clone)]
515struct SearchState<T> {
516    options: Vec<T>,
517    inner: RefCell<Inner<T>>,
518}
519
520#[derive(Debug, Clone)]
521struct Inner<T> {
522    value: String,
523    option_matchers: Vec<String>,
524    filtered_options: Filtered<T>,
525}
526
527#[derive(Debug, Clone)]
528struct Filtered<T> {
529    options: Vec<T>,
530    updated: Instant,
531}
532
533impl<T> SearchState<T>
534where
535    T: Display + Clone,
536{
537    fn with_selection(options: Vec<T>, selection: Option<&T>) -> Self {
538        let value = selection.map(T::to_string).unwrap_or_default();
539        let option_matchers = build_matchers(&options);
540        let filtered_options = Filtered::new(
541            search(&options, &option_matchers, &value)
542                .cloned()
543                .collect(),
544        );
545
546        Self {
547            options,
548            inner: RefCell::new(Inner {
549                value,
550                option_matchers,
551                filtered_options,
552            }),
553        }
554    }
555
556    fn push(&mut self, new_option: T) {
557        let mut inner = self.inner.borrow_mut();
558
559        inner.option_matchers.push(build_matcher(&new_option));
560        self.options.push(new_option);
561
562        inner.filtered_options = Filtered::new(
563            search(&self.options, &inner.option_matchers, &inner.value)
564                .cloned()
565                .collect(),
566        );
567    }
568
569    fn value(&self) -> String {
570        let inner = self.inner.borrow();
571
572        inner.value.clone()
573    }
574
575    fn with_inner<O>(&self, f: impl FnOnce(&Inner<T>) -> O) -> O {
576        let inner = self.inner.borrow();
577
578        f(&inner)
579    }
580
581    fn with_inner_mut(&self, f: impl FnOnce(&mut Inner<T>)) {
582        let mut inner = self.inner.borrow_mut();
583
584        f(&mut inner);
585    }
586
587    fn sync_filtered_options(&self, options: &mut Filtered<T>) {
588        let inner = self.inner.borrow();
589
590        inner.filtered_options.sync(options);
591    }
592}
593
594impl<T> Filtered<T>
595where
596    T: Clone,
597{
598    fn new(options: Vec<T>) -> Self {
599        Self {
600            options,
601            updated: Instant::now(),
602        }
603    }
604
605    fn empty() -> Self {
606        Self {
607            options: Vec::new(),
608            updated: Instant::now(),
609        }
610    }
611
612    fn update(&mut self, options: Vec<T>) {
613        self.options = options;
614        self.updated = Instant::now();
615    }
616
617    fn sync(&self, other: &mut Filtered<T>) {
618        if other.updated != self.updated {
619            *other = self.clone();
620        }
621    }
622}
623
624struct MenuState<T, P: core_text::Paragraph> {
625    menu: menu_overlay::State,
626    mobile_input: MobileTextInputState,
627    hovered_option: Option<usize>,
628    new_selection: Option<T>,
629    filtered_options: Filtered<T>,
630    label: paragraph::Plain<P>,
631}
632
633#[derive(Debug, Clone)]
634enum TextInputEvent {
635    TextChanged(String),
636}
637
638impl<T, Message, Renderer> Widget<Message, Theme, Renderer>
639    for ComboboxCore<'_, T, Message, Renderer>
640where
641    T: Display + Clone + 'static,
642    Message: Clone,
643    Renderer: core_text::Renderer,
644{
645    fn size(&self) -> Size<Length> {
646        Widget::<TextInputEvent, Theme, Renderer>::size(&self.text_input)
647    }
648
649    fn layout(
650        &mut self,
651        tree: &mut widget::Tree,
652        renderer: &Renderer,
653        limits: &layout::Limits,
654    ) -> layout::Node {
655        if let Some(label) = &self.label {
656            let state = tree
657                .state
658                .downcast_mut::<MenuState<T, Renderer::Paragraph>>();
659            let label_size = Pixels(tokens::component::text_field::LABEL_TEXT_POPULATED_SIZE);
660            let label_line_height = LineHeight::Absolute(Pixels(
661                tokens::component::text_field::LABEL_TEXT_POPULATED_LINE_HEIGHT,
662            ));
663
664            let _ = state.label.update(core_text::Text {
665                content: label,
666                bounds: Size::new(
667                    f32::INFINITY,
668                    f32::from(label_line_height.to_absolute(label_size)),
669                ),
670                size: label_size,
671                line_height: label_line_height,
672                font: self.font.unwrap_or_else(|| renderer.default_font()),
673                align_x: text::Alignment::Default,
674                align_y: iced_widget::core::alignment::Vertical::Center,
675                shaping: self.text_shaping,
676                wrapping: text::Wrapping::None,
677            });
678        }
679
680        let is_focused = {
681            let text_input_state = tree.children[0]
682                .state
683                .downcast_ref::<text_input::State<Renderer::Paragraph>>();
684
685            text_input_state.is_focused()
686        };
687
688        self.text_input.layout(
689            &mut tree.children[0],
690            renderer,
691            limits,
692            (!is_focused).then_some(&self.selection),
693        )
694    }
695
696    fn tag(&self) -> widget::tree::Tag {
697        widget::tree::Tag::of::<MenuState<T, Renderer::Paragraph>>()
698    }
699
700    fn state(&self) -> widget::tree::State {
701        widget::tree::State::new(MenuState::<T, Renderer::Paragraph> {
702            menu: menu_overlay::State::new(),
703            mobile_input: MobileTextInputState::default(),
704            filtered_options: Filtered::empty(),
705            hovered_option: Some(0),
706            new_selection: None,
707            label: paragraph::Plain::default(),
708        })
709    }
710
711    fn children(&self) -> Vec<widget::Tree> {
712        vec![widget::Tree::new(&self.text_input as &dyn Widget<_, _, _>)]
713    }
714
715    fn diff(&self, _tree: &mut widget::Tree) {}
716
717    fn update(
718        &mut self,
719        tree: &mut widget::Tree,
720        event: &Event,
721        layout: Layout<'_>,
722        cursor: mouse::Cursor,
723        renderer: &Renderer,
724        clipboard: &mut dyn Clipboard,
725        shell: &mut Shell<'_, Message>,
726        viewport: &Rectangle,
727    ) {
728        let menu = tree
729            .state
730            .downcast_mut::<MenuState<T, Renderer::Paragraph>>();
731        let activation = mobile_text_input_activation(
732            true,
733            &mut menu.mobile_input,
734            event,
735            layout.bounds().intersection(viewport),
736            cursor,
737        );
738
739        let started_focused = {
740            let text_input_state = tree.children[0]
741                .state
742                .downcast_ref::<text_input::State<Renderer::Paragraph>>();
743
744            text_input_state.is_focused()
745        };
746        let mut published_message_to_shell = false;
747
748        let mut local_messages = Vec::new();
749        let mut local_shell = Shell::new(&mut local_messages);
750
751        update_mobile_text_input(
752            &mut self.text_input,
753            &mut tree.children[0],
754            event,
755            layout,
756            activation,
757            TextInputUpdateContext {
758                renderer,
759                clipboard,
760                shell: &mut local_shell,
761                viewport,
762            },
763        );
764
765        if local_shell.is_event_captured() {
766            shell.capture_event();
767        }
768
769        shell.request_redraw_at(local_shell.redraw_request());
770        shell.request_input_method(local_shell.input_method());
771
772        for message in local_messages {
773            let TextInputEvent::TextChanged(new_value) = message;
774
775            if let Some(on_input) = &self.on_input {
776                shell.publish((on_input)(new_value.clone()));
777            }
778
779            self.state.with_inner_mut(|state| {
780                menu.hovered_option = Some(0);
781                state.value = new_value;
782
783                state.filtered_options.update(
784                    search(&self.state.options, &state.option_matchers, &state.value)
785                        .cloned()
786                        .collect(),
787                );
788            });
789            shell.invalidate_layout();
790            shell.request_redraw();
791        }
792
793        let is_focused = {
794            let text_input_state = tree.children[0]
795                .state
796                .downcast_ref::<text_input::State<Renderer::Paragraph>>();
797
798            text_input_state.is_focused()
799        };
800
801        if is_focused {
802            self.state.with_inner(|state| {
803                if !started_focused && let Some(on_option_hovered) = &mut self.on_option_hovered {
804                    let hovered_option = menu.hovered_option.unwrap_or(0);
805
806                    if let Some(option) = state.filtered_options.options.get(hovered_option) {
807                        shell.publish(on_option_hovered(option.clone()));
808                        published_message_to_shell = true;
809                    }
810                }
811
812                if let Event::Keyboard(keyboard::Event::KeyPressed {
813                    key: keyboard::Key::Named(named_key),
814                    modifiers,
815                    ..
816                }) = event
817                {
818                    match (named_key, modifiers.shift()) {
819                        (key::Named::Enter, _) => {
820                            if let Some(index) = &menu.hovered_option
821                                && let Some(option) = state.filtered_options.options.get(*index)
822                            {
823                                menu.new_selection = Some(option.clone());
824                            }
825
826                            shell.capture_event();
827                            shell.request_redraw();
828                        }
829                        (key::Named::ArrowUp, _) | (key::Named::Tab, true) => {
830                            if let Some(index) = &mut menu.hovered_option {
831                                if *index == 0 {
832                                    *index = state.filtered_options.options.len().saturating_sub(1);
833                                } else {
834                                    *index = index.saturating_sub(1);
835                                }
836                            } else {
837                                menu.hovered_option = Some(0);
838                            }
839
840                            if let Some(on_option_hovered) = &mut self.on_option_hovered
841                                && let Some(option) = menu
842                                    .hovered_option
843                                    .and_then(|index| state.filtered_options.options.get(index))
844                            {
845                                shell.publish((on_option_hovered)(option.clone()));
846                                published_message_to_shell = true;
847                            }
848
849                            shell.capture_event();
850                            shell.request_redraw();
851                        }
852                        (key::Named::ArrowDown, _) | (key::Named::Tab, false)
853                            if !modifiers.shift() =>
854                        {
855                            if let Some(index) = &mut menu.hovered_option {
856                                if *index >= state.filtered_options.options.len().saturating_sub(1)
857                                {
858                                    *index = 0;
859                                } else {
860                                    *index = index.saturating_add(1).min(
861                                        state.filtered_options.options.len().saturating_sub(1),
862                                    );
863                                }
864                            } else {
865                                menu.hovered_option = Some(0);
866                            }
867
868                            if let Some(on_option_hovered) = &mut self.on_option_hovered
869                                && let Some(option) = menu
870                                    .hovered_option
871                                    .and_then(|index| state.filtered_options.options.get(index))
872                            {
873                                shell.publish((on_option_hovered)(option.clone()));
874                                published_message_to_shell = true;
875                            }
876
877                            shell.capture_event();
878                            shell.request_redraw();
879                        }
880                        _ => {}
881                    }
882                }
883            });
884        }
885
886        self.state.with_inner_mut(|state| {
887            if let Some(selection) = menu.new_selection.take() {
888                state.value = String::new();
889                state.filtered_options.update(self.state.options.clone());
890                menu.menu = menu_overlay::State::default();
891
892                shell.publish((self.on_selected)(selection));
893                published_message_to_shell = true;
894
895                let mut local_messages = Vec::new();
896                let mut local_shell = Shell::new(&mut local_messages);
897                self.text_input.update(
898                    &mut tree.children[0],
899                    &Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
900                    layout,
901                    mouse::Cursor::Unavailable,
902                    renderer,
903                    clipboard,
904                    &mut local_shell,
905                    viewport,
906                );
907                shell.request_input_method(local_shell.input_method());
908            }
909        });
910
911        let is_focused = {
912            let text_input_state = tree.children[0]
913                .state
914                .downcast_ref::<text_input::State<Renderer::Paragraph>>();
915
916            text_input_state.is_focused()
917        };
918
919        sync_mobile_keyboard(
920            started_focused,
921            is_focused,
922            activation.request_mobile_keyboard,
923        );
924
925        if started_focused != is_focused {
926            shell.invalidate_widgets();
927
928            if is_focused {
929                self.state.with_inner(|state| {
930                    menu.menu
931                        .start_open(state.filtered_options.options.len(), Instant::now());
932                });
933            }
934
935            if !published_message_to_shell {
936                if is_focused {
937                    if let Some(on_open) = self.on_open.take() {
938                        shell.publish(on_open);
939                    }
940                } else if let Some(on_close) = self.on_close.take() {
941                    shell.publish(on_close);
942                }
943            }
944        }
945    }
946
947    fn mouse_interaction(
948        &self,
949        tree: &widget::Tree,
950        layout: Layout<'_>,
951        cursor: mouse::Cursor,
952        viewport: &Rectangle,
953        renderer: &Renderer,
954    ) -> mouse::Interaction {
955        self.text_input
956            .mouse_interaction(&tree.children[0], layout, cursor, viewport, renderer)
957    }
958
959    fn draw(
960        &self,
961        tree: &widget::Tree,
962        renderer: &mut Renderer,
963        theme: &Theme,
964        _style: &renderer::Style,
965        layout: Layout<'_>,
966        cursor: mouse::Cursor,
967        viewport: &Rectangle,
968    ) {
969        register_mobile_text_region(true, layout.bounds(), viewport);
970
971        let is_focused = {
972            let text_input_state = tree.children[0]
973                .state
974                .downcast_ref::<text_input::State<Renderer::Paragraph>>();
975
976            text_input_state.is_focused()
977        };
978
979        let selection = if is_focused || self.selection.is_empty() {
980            None
981        } else {
982            Some(&self.selection)
983        };
984        let bounds = layout.bounds();
985        let is_hovered = cursor.is_over(bounds);
986        let label_x = bounds.x + tokens::component::text_field::LEADING_SPACE;
987        let label_width = if self.label.is_some() {
988            let state = tree
989                .state
990                .downcast_ref::<MenuState<T, Renderer::Paragraph>>();
991
992            state.label.min_width()
993        } else {
994            0.0
995        };
996        let label_notch = self.label.as_ref().and_then(|_| {
997            text_field_floating_label_notch(bounds, label_x, label_width, label_width, 1.0)
998        });
999        let outline_clip_width = if is_focused {
1000            tokens::component::text_field::FOCUS_OUTLINE_WIDTH
1001        } else {
1002            tokens::component::text_field::OUTLINE_WIDTH
1003        };
1004
1005        draw_text_field_notched(
1006            renderer,
1007            bounds,
1008            outline_clip_width,
1009            label_notch,
1010            |renderer| {
1011                self.text_input.draw(
1012                    &tree.children[0],
1013                    renderer,
1014                    theme,
1015                    layout,
1016                    cursor,
1017                    selection,
1018                    viewport,
1019                );
1020            },
1021        );
1022
1023        if let Some(label) = &self.label {
1024            let label_size = Pixels(tokens::component::text_field::LABEL_TEXT_POPULATED_SIZE);
1025            let label_line_height = LineHeight::Absolute(Pixels(
1026                tokens::component::text_field::LABEL_TEXT_POPULATED_LINE_HEIGHT,
1027            ));
1028            let label_height = f32::from(label_line_height.to_absolute(label_size));
1029            let label_y = bounds.y;
1030
1031            renderer.fill_text(
1032                core_text::Text {
1033                    content: label.clone(),
1034                    size: label_size,
1035                    line_height: label_line_height,
1036                    font: self.font.unwrap_or_else(|| renderer.default_font()),
1037                    bounds: Size::new(label_width, label_height),
1038                    align_x: text::Alignment::Default,
1039                    align_y: iced_widget::core::alignment::Vertical::Center,
1040                    shaping: self.text_shaping,
1041                    wrapping: text::Wrapping::None,
1042                },
1043                Point::new(label_x, label_y),
1044                combobox_label_color(theme, is_focused, is_hovered),
1045                *viewport,
1046            );
1047        }
1048    }
1049
1050    fn overlay<'b>(
1051        &'b mut self,
1052        tree: &'b mut widget::Tree,
1053        layout: Layout<'_>,
1054        renderer: &Renderer,
1055        viewport: &Rectangle,
1056        translation: Vector,
1057    ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
1058        let is_focused = {
1059            let text_input_state = tree.children[0]
1060                .state
1061                .downcast_ref::<text_input::State<Renderer::Paragraph>>();
1062
1063            text_input_state.is_focused()
1064        };
1065
1066        if is_focused {
1067            let MenuState {
1068                menu,
1069                filtered_options,
1070                hovered_option,
1071                ..
1072            } = tree
1073                .state
1074                .downcast_mut::<MenuState<T, Renderer::Paragraph>>();
1075
1076            self.state.sync_filtered_options(filtered_options);
1077
1078            if filtered_options.options.is_empty() {
1079                None
1080            } else {
1081                let bounds = layout.bounds();
1082
1083                let mut menu = menu_overlay::Menu::new(
1084                    menu,
1085                    &filtered_options.options,
1086                    hovered_option,
1087                    |selection| {
1088                        self.state.with_inner_mut(|state| {
1089                            state.value = String::new();
1090                            state.filtered_options.update(self.state.options.clone());
1091                        });
1092
1093                        tree.children[0]
1094                            .state
1095                            .downcast_mut::<text_input::State<Renderer::Paragraph>>()
1096                            .unfocus();
1097
1098                        (self.on_selected)(selection)
1099                    },
1100                    self.on_option_hovered.as_deref(),
1101                    &self.menu_class,
1102                )
1103                .width(bounds.width)
1104                .padding(self.option_padding)
1105                .text_line_height(self.line_height)
1106                .text_shaping(self.text_shaping);
1107
1108                if let Some(font) = self.font {
1109                    menu = menu.font(font);
1110                }
1111
1112                if let Some(size) = self.size {
1113                    menu = menu.text_size(size);
1114                }
1115
1116                let anchor = select::prefer_down_when_menu_fits(
1117                    layout.position() + translation,
1118                    *viewport,
1119                    bounds.height,
1120                    select::resolved_menu_height(
1121                        self.menu_height,
1122                        self.intrinsic_menu_height(filtered_options.options.len(), renderer),
1123                        viewport.height,
1124                    ),
1125                );
1126
1127                Some(menu.overlay(
1128                    anchor.position,
1129                    *viewport,
1130                    anchor.target_height,
1131                    self.menu_height,
1132                ))
1133            }
1134        } else {
1135            None
1136        }
1137    }
1138}
1139
1140impl<'a, T, Message, Renderer> From<ComboboxCore<'a, T, Message, Renderer>>
1141    for Element<'a, Message, Theme, Renderer>
1142where
1143    T: Display + Clone + 'static,
1144    Message: Clone + 'a,
1145    Renderer: core_text::Renderer + 'a,
1146{
1147    fn from(combobox: ComboboxCore<'a, T, Message, Renderer>) -> Self {
1148        Self::new(combobox)
1149    }
1150}
1151
1152fn combobox_label_color(theme: &Theme, is_focused: bool, is_hovered: bool) -> Color {
1153    let colors = theme.colors();
1154
1155    if is_focused {
1156        colors.primary.color
1157    } else if is_hovered {
1158        colors.surface.text
1159    } else {
1160        colors.surface.text_variant
1161    }
1162}
1163
1164fn search<'a, T, A>(
1165    options: impl IntoIterator<Item = T> + 'a,
1166    option_matchers: impl IntoIterator<Item = &'a A> + 'a,
1167    query: &'a str,
1168) -> impl Iterator<Item = T> + 'a
1169where
1170    A: AsRef<str> + 'a,
1171{
1172    let query: Vec<String> = query
1173        .to_lowercase()
1174        .split(|c: char| !c.is_ascii_alphanumeric())
1175        .map(String::from)
1176        .collect();
1177
1178    options
1179        .into_iter()
1180        .zip(option_matchers)
1181        .filter_map(move |(option, matcher)| {
1182            if query.iter().all(|part| matcher.as_ref().contains(part)) {
1183                Some(option)
1184            } else {
1185                None
1186            }
1187        })
1188}
1189
1190fn build_matchers<'a, T>(options: impl IntoIterator<Item = T> + 'a) -> Vec<String>
1191where
1192    T: Display + 'a,
1193{
1194    options.into_iter().map(build_matcher).collect()
1195}
1196
1197fn build_matcher<T>(option: T) -> String
1198where
1199    T: Display,
1200{
1201    let mut matcher = option.to_string();
1202    matcher.retain(|c| c.is_ascii_alphanumeric());
1203    matcher.to_lowercase()
1204}
1205
1206fn inner_options<T>(options: &[T]) -> Vec<DisplayValue<T>>
1207where
1208    T: Clone,
1209{
1210    options.iter().cloned().map(DisplayValue::Option).collect()
1211}
1212
1213#[cfg(test)]
1214#[path = "../../../tests/widget/component/combobox.rs"]
1215mod tests;