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, web_input_anchor,
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 visible_bounds = layout.bounds().intersection(viewport);
732        let activation = mobile_text_input_activation(
733            true,
734            &mut menu.mobile_input,
735            event,
736            visible_bounds,
737            cursor,
738        );
739
740        let started_focused = {
741            let text_input_state = tree.children[0]
742                .state
743                .downcast_ref::<text_input::State<Renderer::Paragraph>>();
744
745            text_input_state.is_focused()
746        };
747        let mut published_message_to_shell = false;
748
749        let mut local_messages = Vec::new();
750        let mut local_shell = Shell::new(&mut local_messages);
751
752        update_mobile_text_input(
753            &mut self.text_input,
754            &mut tree.children[0],
755            event,
756            layout,
757            activation,
758            TextInputUpdateContext {
759                renderer,
760                clipboard,
761                shell: &mut local_shell,
762                viewport,
763            },
764        );
765
766        if local_shell.is_event_captured() {
767            shell.capture_event();
768        }
769
770        shell.request_redraw_at(local_shell.redraw_request());
771        shell.request_input_method(local_shell.input_method());
772
773        for message in local_messages {
774            let TextInputEvent::TextChanged(new_value) = message;
775
776            if let Some(on_input) = &self.on_input {
777                shell.publish((on_input)(new_value.clone()));
778            }
779
780            self.state.with_inner_mut(|state| {
781                menu.hovered_option = Some(0);
782                state.value = new_value;
783
784                state.filtered_options.update(
785                    search(&self.state.options, &state.option_matchers, &state.value)
786                        .cloned()
787                        .collect(),
788                );
789            });
790            shell.invalidate_layout();
791            shell.request_redraw();
792        }
793
794        let is_focused = {
795            let text_input_state = tree.children[0]
796                .state
797                .downcast_ref::<text_input::State<Renderer::Paragraph>>();
798
799            text_input_state.is_focused()
800        };
801
802        if is_focused {
803            self.state.with_inner(|state| {
804                if !started_focused && let Some(on_option_hovered) = &mut self.on_option_hovered {
805                    let hovered_option = menu.hovered_option.unwrap_or(0);
806
807                    if let Some(option) = state.filtered_options.options.get(hovered_option) {
808                        shell.publish(on_option_hovered(option.clone()));
809                        published_message_to_shell = true;
810                    }
811                }
812
813                if let Event::Keyboard(keyboard::Event::KeyPressed {
814                    key: keyboard::Key::Named(named_key),
815                    modifiers,
816                    ..
817                }) = event
818                {
819                    match (named_key, modifiers.shift()) {
820                        (key::Named::Enter, _) => {
821                            if let Some(index) = &menu.hovered_option
822                                && let Some(option) = state.filtered_options.options.get(*index)
823                            {
824                                menu.new_selection = Some(option.clone());
825                            }
826
827                            shell.capture_event();
828                            shell.request_redraw();
829                        }
830                        (key::Named::ArrowUp, _) | (key::Named::Tab, true) => {
831                            if let Some(index) = &mut menu.hovered_option {
832                                if *index == 0 {
833                                    *index = state.filtered_options.options.len().saturating_sub(1);
834                                } else {
835                                    *index = index.saturating_sub(1);
836                                }
837                            } else {
838                                menu.hovered_option = Some(0);
839                            }
840
841                            if let Some(on_option_hovered) = &mut self.on_option_hovered
842                                && let Some(option) = menu
843                                    .hovered_option
844                                    .and_then(|index| state.filtered_options.options.get(index))
845                            {
846                                shell.publish((on_option_hovered)(option.clone()));
847                                published_message_to_shell = true;
848                            }
849
850                            shell.capture_event();
851                            shell.request_redraw();
852                        }
853                        (key::Named::ArrowDown, _) | (key::Named::Tab, false)
854                            if !modifiers.shift() =>
855                        {
856                            if let Some(index) = &mut menu.hovered_option {
857                                if *index >= state.filtered_options.options.len().saturating_sub(1)
858                                {
859                                    *index = 0;
860                                } else {
861                                    *index = index.saturating_add(1).min(
862                                        state.filtered_options.options.len().saturating_sub(1),
863                                    );
864                                }
865                            } else {
866                                menu.hovered_option = Some(0);
867                            }
868
869                            if let Some(on_option_hovered) = &mut self.on_option_hovered
870                                && let Some(option) = menu
871                                    .hovered_option
872                                    .and_then(|index| state.filtered_options.options.get(index))
873                            {
874                                shell.publish((on_option_hovered)(option.clone()));
875                                published_message_to_shell = true;
876                            }
877
878                            shell.capture_event();
879                            shell.request_redraw();
880                        }
881                        _ => {}
882                    }
883                }
884            });
885        }
886
887        self.state.with_inner_mut(|state| {
888            if let Some(selection) = menu.new_selection.take() {
889                state.value = String::new();
890                state.filtered_options.update(self.state.options.clone());
891                menu.menu = menu_overlay::State::default();
892
893                shell.publish((self.on_selected)(selection));
894                published_message_to_shell = true;
895
896                let mut local_messages = Vec::new();
897                let mut local_shell = Shell::new(&mut local_messages);
898                self.text_input.update(
899                    &mut tree.children[0],
900                    &Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
901                    layout,
902                    mouse::Cursor::Unavailable,
903                    renderer,
904                    clipboard,
905                    &mut local_shell,
906                    viewport,
907                );
908                shell.request_input_method(local_shell.input_method());
909            }
910        });
911
912        let is_focused = {
913            let text_input_state = tree.children[0]
914                .state
915                .downcast_ref::<text_input::State<Renderer::Paragraph>>();
916
917            text_input_state.is_focused()
918        };
919        let input_anchor = web_input_anchor(
920            shell.input_method(),
921            visible_bounds,
922            activation,
923            started_focused,
924            is_focused,
925        );
926
927        sync_mobile_keyboard(
928            started_focused,
929            is_focused,
930            activation.request_mobile_keyboard,
931            input_anchor,
932        );
933
934        if started_focused != is_focused {
935            shell.invalidate_widgets();
936
937            if is_focused {
938                self.state.with_inner(|state| {
939                    menu.menu
940                        .start_open(state.filtered_options.options.len(), Instant::now());
941                });
942            }
943
944            if !published_message_to_shell {
945                if is_focused {
946                    if let Some(on_open) = self.on_open.take() {
947                        shell.publish(on_open);
948                    }
949                } else if let Some(on_close) = self.on_close.take() {
950                    shell.publish(on_close);
951                }
952            }
953        }
954    }
955
956    fn mouse_interaction(
957        &self,
958        tree: &widget::Tree,
959        layout: Layout<'_>,
960        cursor: mouse::Cursor,
961        viewport: &Rectangle,
962        renderer: &Renderer,
963    ) -> mouse::Interaction {
964        self.text_input
965            .mouse_interaction(&tree.children[0], layout, cursor, viewport, renderer)
966    }
967
968    fn draw(
969        &self,
970        tree: &widget::Tree,
971        renderer: &mut Renderer,
972        theme: &Theme,
973        _style: &renderer::Style,
974        layout: Layout<'_>,
975        cursor: mouse::Cursor,
976        viewport: &Rectangle,
977    ) {
978        register_mobile_text_region(true, layout.bounds(), viewport);
979
980        let is_focused = {
981            let text_input_state = tree.children[0]
982                .state
983                .downcast_ref::<text_input::State<Renderer::Paragraph>>();
984
985            text_input_state.is_focused()
986        };
987
988        let selection = if is_focused || self.selection.is_empty() {
989            None
990        } else {
991            Some(&self.selection)
992        };
993        let bounds = layout.bounds();
994        let is_hovered = cursor.is_over(bounds);
995        let label_x = bounds.x + tokens::component::text_field::LEADING_SPACE;
996        let label_width = if self.label.is_some() {
997            let state = tree
998                .state
999                .downcast_ref::<MenuState<T, Renderer::Paragraph>>();
1000
1001            state.label.min_width()
1002        } else {
1003            0.0
1004        };
1005        let label_notch = self.label.as_ref().and_then(|_| {
1006            text_field_floating_label_notch(bounds, label_x, label_width, label_width, 1.0)
1007        });
1008        let outline_clip_width = if is_focused {
1009            tokens::component::text_field::FOCUS_OUTLINE_WIDTH
1010        } else {
1011            tokens::component::text_field::OUTLINE_WIDTH
1012        };
1013
1014        draw_text_field_notched(
1015            renderer,
1016            bounds,
1017            outline_clip_width,
1018            label_notch,
1019            |renderer| {
1020                self.text_input.draw(
1021                    &tree.children[0],
1022                    renderer,
1023                    theme,
1024                    layout,
1025                    cursor,
1026                    selection,
1027                    viewport,
1028                );
1029            },
1030        );
1031
1032        if let Some(label) = &self.label {
1033            let label_size = Pixels(tokens::component::text_field::LABEL_TEXT_POPULATED_SIZE);
1034            let label_line_height = LineHeight::Absolute(Pixels(
1035                tokens::component::text_field::LABEL_TEXT_POPULATED_LINE_HEIGHT,
1036            ));
1037            let label_height = f32::from(label_line_height.to_absolute(label_size));
1038            let label_y = bounds.y;
1039
1040            renderer.fill_text(
1041                core_text::Text {
1042                    content: label.clone(),
1043                    size: label_size,
1044                    line_height: label_line_height,
1045                    font: self.font.unwrap_or_else(|| renderer.default_font()),
1046                    bounds: Size::new(label_width, label_height),
1047                    align_x: text::Alignment::Default,
1048                    align_y: iced_widget::core::alignment::Vertical::Center,
1049                    shaping: self.text_shaping,
1050                    wrapping: text::Wrapping::None,
1051                },
1052                Point::new(label_x, label_y),
1053                combobox_label_color(theme, is_focused, is_hovered),
1054                *viewport,
1055            );
1056        }
1057    }
1058
1059    fn overlay<'b>(
1060        &'b mut self,
1061        tree: &'b mut widget::Tree,
1062        layout: Layout<'_>,
1063        renderer: &Renderer,
1064        viewport: &Rectangle,
1065        translation: Vector,
1066    ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
1067        let is_focused = {
1068            let text_input_state = tree.children[0]
1069                .state
1070                .downcast_ref::<text_input::State<Renderer::Paragraph>>();
1071
1072            text_input_state.is_focused()
1073        };
1074
1075        if is_focused {
1076            let MenuState {
1077                menu,
1078                filtered_options,
1079                hovered_option,
1080                ..
1081            } = tree
1082                .state
1083                .downcast_mut::<MenuState<T, Renderer::Paragraph>>();
1084
1085            self.state.sync_filtered_options(filtered_options);
1086
1087            if filtered_options.options.is_empty() {
1088                None
1089            } else {
1090                let bounds = layout.bounds();
1091
1092                let mut menu = menu_overlay::Menu::new(
1093                    menu,
1094                    &filtered_options.options,
1095                    hovered_option,
1096                    |selection| {
1097                        self.state.with_inner_mut(|state| {
1098                            state.value = String::new();
1099                            state.filtered_options.update(self.state.options.clone());
1100                        });
1101
1102                        tree.children[0]
1103                            .state
1104                            .downcast_mut::<text_input::State<Renderer::Paragraph>>()
1105                            .unfocus();
1106
1107                        (self.on_selected)(selection)
1108                    },
1109                    self.on_option_hovered.as_deref(),
1110                    &self.menu_class,
1111                )
1112                .width(bounds.width)
1113                .padding(self.option_padding)
1114                .text_line_height(self.line_height)
1115                .text_shaping(self.text_shaping);
1116
1117                if let Some(font) = self.font {
1118                    menu = menu.font(font);
1119                }
1120
1121                if let Some(size) = self.size {
1122                    menu = menu.text_size(size);
1123                }
1124
1125                let anchor = select::prefer_down_when_menu_fits(
1126                    layout.position() + translation,
1127                    *viewport,
1128                    bounds.height,
1129                    select::resolved_menu_height(
1130                        self.menu_height,
1131                        self.intrinsic_menu_height(filtered_options.options.len(), renderer),
1132                        viewport.height,
1133                    ),
1134                );
1135
1136                Some(menu.overlay(
1137                    anchor.position,
1138                    *viewport,
1139                    anchor.target_height,
1140                    self.menu_height,
1141                ))
1142            }
1143        } else {
1144            None
1145        }
1146    }
1147}
1148
1149impl<'a, T, Message, Renderer> From<ComboboxCore<'a, T, Message, Renderer>>
1150    for Element<'a, Message, Theme, Renderer>
1151where
1152    T: Display + Clone + 'static,
1153    Message: Clone + 'a,
1154    Renderer: core_text::Renderer + 'a,
1155{
1156    fn from(combobox: ComboboxCore<'a, T, Message, Renderer>) -> Self {
1157        Self::new(combobox)
1158    }
1159}
1160
1161fn combobox_label_color(theme: &Theme, is_focused: bool, is_hovered: bool) -> Color {
1162    let colors = theme.colors();
1163
1164    if is_focused {
1165        colors.primary.color
1166    } else if is_hovered {
1167        colors.surface.text
1168    } else {
1169        colors.surface.text_variant
1170    }
1171}
1172
1173fn search<'a, T, A>(
1174    options: impl IntoIterator<Item = T> + 'a,
1175    option_matchers: impl IntoIterator<Item = &'a A> + 'a,
1176    query: &'a str,
1177) -> impl Iterator<Item = T> + 'a
1178where
1179    A: AsRef<str> + 'a,
1180{
1181    let query: Vec<String> = query
1182        .to_lowercase()
1183        .split(|c: char| !c.is_ascii_alphanumeric())
1184        .map(String::from)
1185        .collect();
1186
1187    options
1188        .into_iter()
1189        .zip(option_matchers)
1190        .filter_map(move |(option, matcher)| {
1191            if query.iter().all(|part| matcher.as_ref().contains(part)) {
1192                Some(option)
1193            } else {
1194                None
1195            }
1196        })
1197}
1198
1199fn build_matchers<'a, T>(options: impl IntoIterator<Item = T> + 'a) -> Vec<String>
1200where
1201    T: Display + 'a,
1202{
1203    options.into_iter().map(build_matcher).collect()
1204}
1205
1206fn build_matcher<T>(option: T) -> String
1207where
1208    T: Display,
1209{
1210    let mut matcher = option.to_string();
1211    matcher.retain(|c| c.is_ascii_alphanumeric());
1212    matcher.to_lowercase()
1213}
1214
1215fn inner_options<T>(options: &[T]) -> Vec<DisplayValue<T>>
1216where
1217    T: Clone,
1218{
1219    options.iter().cloned().map(DisplayValue::Option).collect()
1220}
1221
1222#[cfg(test)]
1223#[path = "../../../tests/widget/component/combobox.rs"]
1224mod tests;