1use 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
46pub 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 pub fn new(options: Vec<T>) -> Self {
74 Self::with_selection(options, None)
75 }
76
77 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 pub fn options(&self) -> &[T] {
91 &self.options
92 }
93
94 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 pub fn into_options(self) -> Vec<T> {
102 self.options
103 }
104
105 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 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
140pub 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 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 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 pub fn on_open(mut self, message: Message) -> Self {
183 self.inner = self.inner.on_open(message);
184 self
185 }
186
187 pub fn on_close(mut self, message: Message) -> Self {
189 self.inner = self.inner.on_close(message);
190 self
191 }
192
193 pub fn label(mut self, label: impl Into<String>) -> Self {
195 self.inner = self.inner.label(label);
196 self
197 }
198
199 pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
201 self.inner = self.inner.padding(padding);
202 self
203 }
204
205 pub fn font(mut self, font: Renderer::Font) -> Self {
207 self.inner = self.inner.font(font);
208 self
209 }
210
211 pub fn icon(mut self, icon: Icon<Renderer::Font>) -> Self {
213 self.inner = self.inner.icon(icon);
214 self
215 }
216
217 pub fn size(mut self, size: impl Into<Pixels>) -> Self {
219 self.inner = self.inner.size(size);
220 self
221 }
222
223 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 pub fn width(mut self, width: impl Into<Length>) -> Self {
231 self.inner = self.inner.width(width);
232 self
233 }
234
235 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 pub fn text_shaping(mut self, shaping: text::Shaping) -> Self {
243 self.inner = self.inner.text_shaping(shaping);
244 self
245 }
246
247 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 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;