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