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, TextInputUpdateContext, absolute_line_height, draw_text_field_notched,
22 mobile_text_input_activation, register_mobile_text_region, select, sync_mobile_keyboard,
23 text_field_floating_label_notch, update_mobile_text_input,
24};
25use crate::style::{menu as menu_style, text_input as text_input_style};
26use crate::{Theme, tokens};
27
28#[derive(Clone)]
29enum DisplayValue<T> {
30 Option(T),
31 Input(String),
32}
33
34impl<T> fmt::Display for DisplayValue<T>
35where
36 T: fmt::Display,
37{
38 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39 match self {
40 Self::Option(option) => option.fmt(f),
41 Self::Input(input) => input.fmt(f),
42 }
43 }
44}
45
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>;
502 self
503 }
504
505 fn intrinsic_menu_height(&self, option_count: usize, renderer: &Renderer) -> f32 {
506 let text_size = self.size.unwrap_or_else(|| renderer.default_size());
507 let option_height =
508 f32::from(self.line_height.to_absolute(text_size)) + self.option_padding.y();
509
510 option_height * option_count as f32
511 }
512}
513
514#[derive(Debug, Clone)]
515struct SearchState<T> {
516 options: Vec<T>,
517 inner: RefCell<Inner<T>>,
518}
519
520#[derive(Debug, Clone)]
521struct Inner<T> {
522 value: String,
523 option_matchers: Vec<String>,
524 filtered_options: Filtered<T>,
525}
526
527#[derive(Debug, Clone)]
528struct Filtered<T> {
529 options: Vec<T>,
530 updated: Instant,
531}
532
533impl<T> SearchState<T>
534where
535 T: Display + Clone,
536{
537 fn with_selection(options: Vec<T>, selection: Option<&T>) -> Self {
538 let value = selection.map(T::to_string).unwrap_or_default();
539 let option_matchers = build_matchers(&options);
540 let filtered_options = Filtered::new(
541 search(&options, &option_matchers, &value)
542 .cloned()
543 .collect(),
544 );
545
546 Self {
547 options,
548 inner: RefCell::new(Inner {
549 value,
550 option_matchers,
551 filtered_options,
552 }),
553 }
554 }
555
556 fn push(&mut self, new_option: T) {
557 let mut inner = self.inner.borrow_mut();
558
559 inner.option_matchers.push(build_matcher(&new_option));
560 self.options.push(new_option);
561
562 inner.filtered_options = Filtered::new(
563 search(&self.options, &inner.option_matchers, &inner.value)
564 .cloned()
565 .collect(),
566 );
567 }
568
569 fn value(&self) -> String {
570 let inner = self.inner.borrow();
571
572 inner.value.clone()
573 }
574
575 fn with_inner<O>(&self, f: impl FnOnce(&Inner<T>) -> O) -> O {
576 let inner = self.inner.borrow();
577
578 f(&inner)
579 }
580
581 fn with_inner_mut(&self, f: impl FnOnce(&mut Inner<T>)) {
582 let mut inner = self.inner.borrow_mut();
583
584 f(&mut inner);
585 }
586
587 fn sync_filtered_options(&self, options: &mut Filtered<T>) {
588 let inner = self.inner.borrow();
589
590 inner.filtered_options.sync(options);
591 }
592}
593
594impl<T> Filtered<T>
595where
596 T: Clone,
597{
598 fn new(options: Vec<T>) -> Self {
599 Self {
600 options,
601 updated: Instant::now(),
602 }
603 }
604
605 fn empty() -> Self {
606 Self {
607 options: Vec::new(),
608 updated: Instant::now(),
609 }
610 }
611
612 fn update(&mut self, options: Vec<T>) {
613 self.options = options;
614 self.updated = Instant::now();
615 }
616
617 fn sync(&self, other: &mut Filtered<T>) {
618 if other.updated != self.updated {
619 *other = self.clone();
620 }
621 }
622}
623
624struct MenuState<T, P: core_text::Paragraph> {
625 menu: menu_overlay::State,
626 mobile_input: MobileTextInputState,
627 hovered_option: Option<usize>,
628 new_selection: Option<T>,
629 filtered_options: Filtered<T>,
630 label: paragraph::Plain<P>,
631}
632
633#[derive(Debug, Clone)]
634enum TextInputEvent {
635 TextChanged(String),
636}
637
638impl<T, Message, Renderer> Widget<Message, Theme, Renderer>
639 for ComboboxCore<'_, T, Message, Renderer>
640where
641 T: Display + Clone + 'static,
642 Message: Clone,
643 Renderer: core_text::Renderer,
644{
645 fn size(&self) -> Size<Length> {
646 Widget::<TextInputEvent, Theme, Renderer>::size(&self.text_input)
647 }
648
649 fn layout(
650 &mut self,
651 tree: &mut widget::Tree,
652 renderer: &Renderer,
653 limits: &layout::Limits,
654 ) -> layout::Node {
655 if let Some(label) = &self.label {
656 let state = tree
657 .state
658 .downcast_mut::<MenuState<T, Renderer::Paragraph>>();
659 let label_size = Pixels(tokens::component::text_field::LABEL_TEXT_POPULATED_SIZE);
660 let label_line_height = LineHeight::Absolute(Pixels(
661 tokens::component::text_field::LABEL_TEXT_POPULATED_LINE_HEIGHT,
662 ));
663
664 let _ = state.label.update(core_text::Text {
665 content: label,
666 bounds: Size::new(
667 f32::INFINITY,
668 f32::from(label_line_height.to_absolute(label_size)),
669 ),
670 size: label_size,
671 line_height: label_line_height,
672 font: self.font.unwrap_or_else(|| renderer.default_font()),
673 align_x: text::Alignment::Default,
674 align_y: iced_widget::core::alignment::Vertical::Center,
675 shaping: self.text_shaping,
676 wrapping: text::Wrapping::None,
677 });
678 }
679
680 let is_focused = {
681 let text_input_state = tree.children[0]
682 .state
683 .downcast_ref::<text_input::State<Renderer::Paragraph>>();
684
685 text_input_state.is_focused()
686 };
687
688 self.text_input.layout(
689 &mut tree.children[0],
690 renderer,
691 limits,
692 (!is_focused).then_some(&self.selection),
693 )
694 }
695
696 fn tag(&self) -> widget::tree::Tag {
697 widget::tree::Tag::of::<MenuState<T, Renderer::Paragraph>>()
698 }
699
700 fn state(&self) -> widget::tree::State {
701 widget::tree::State::new(MenuState::<T, Renderer::Paragraph> {
702 menu: menu_overlay::State::new(),
703 mobile_input: MobileTextInputState::default(),
704 filtered_options: Filtered::empty(),
705 hovered_option: Some(0),
706 new_selection: None,
707 label: paragraph::Plain::default(),
708 })
709 }
710
711 fn children(&self) -> Vec<widget::Tree> {
712 vec![widget::Tree::new(&self.text_input as &dyn Widget<_, _, _>)]
713 }
714
715 fn diff(&self, _tree: &mut widget::Tree) {}
716
717 fn update(
718 &mut self,
719 tree: &mut widget::Tree,
720 event: &Event,
721 layout: Layout<'_>,
722 cursor: mouse::Cursor,
723 renderer: &Renderer,
724 clipboard: &mut dyn Clipboard,
725 shell: &mut Shell<'_, Message>,
726 viewport: &Rectangle,
727 ) {
728 let menu = tree
729 .state
730 .downcast_mut::<MenuState<T, Renderer::Paragraph>>();
731 let activation = mobile_text_input_activation(
732 true,
733 &mut menu.mobile_input,
734 event,
735 layout.bounds().intersection(viewport),
736 cursor,
737 );
738
739 let started_focused = {
740 let text_input_state = tree.children[0]
741 .state
742 .downcast_ref::<text_input::State<Renderer::Paragraph>>();
743
744 text_input_state.is_focused()
745 };
746 let mut published_message_to_shell = false;
747
748 let mut local_messages = Vec::new();
749 let mut local_shell = Shell::new(&mut local_messages);
750
751 update_mobile_text_input(
752 &mut self.text_input,
753 &mut tree.children[0],
754 event,
755 layout,
756 activation,
757 TextInputUpdateContext {
758 renderer,
759 clipboard,
760 shell: &mut local_shell,
761 viewport,
762 },
763 );
764
765 if local_shell.is_event_captured() {
766 shell.capture_event();
767 }
768
769 shell.request_redraw_at(local_shell.redraw_request());
770 shell.request_input_method(local_shell.input_method());
771
772 for message in local_messages {
773 let TextInputEvent::TextChanged(new_value) = message;
774
775 if let Some(on_input) = &self.on_input {
776 shell.publish((on_input)(new_value.clone()));
777 }
778
779 self.state.with_inner_mut(|state| {
780 menu.hovered_option = Some(0);
781 state.value = new_value;
782
783 state.filtered_options.update(
784 search(&self.state.options, &state.option_matchers, &state.value)
785 .cloned()
786 .collect(),
787 );
788 });
789 shell.invalidate_layout();
790 shell.request_redraw();
791 }
792
793 let is_focused = {
794 let text_input_state = tree.children[0]
795 .state
796 .downcast_ref::<text_input::State<Renderer::Paragraph>>();
797
798 text_input_state.is_focused()
799 };
800
801 if is_focused {
802 self.state.with_inner(|state| {
803 if !started_focused && let Some(on_option_hovered) = &mut self.on_option_hovered {
804 let hovered_option = menu.hovered_option.unwrap_or(0);
805
806 if let Some(option) = state.filtered_options.options.get(hovered_option) {
807 shell.publish(on_option_hovered(option.clone()));
808 published_message_to_shell = true;
809 }
810 }
811
812 if let Event::Keyboard(keyboard::Event::KeyPressed {
813 key: keyboard::Key::Named(named_key),
814 modifiers,
815 ..
816 }) = event
817 {
818 match (named_key, modifiers.shift()) {
819 (key::Named::Enter, _) => {
820 if let Some(index) = &menu.hovered_option
821 && let Some(option) = state.filtered_options.options.get(*index)
822 {
823 menu.new_selection = Some(option.clone());
824 }
825
826 shell.capture_event();
827 shell.request_redraw();
828 }
829 (key::Named::ArrowUp, _) | (key::Named::Tab, true) => {
830 if let Some(index) = &mut menu.hovered_option {
831 if *index == 0 {
832 *index = state.filtered_options.options.len().saturating_sub(1);
833 } else {
834 *index = index.saturating_sub(1);
835 }
836 } else {
837 menu.hovered_option = Some(0);
838 }
839
840 if let Some(on_option_hovered) = &mut self.on_option_hovered
841 && let Some(option) = menu
842 .hovered_option
843 .and_then(|index| state.filtered_options.options.get(index))
844 {
845 shell.publish((on_option_hovered)(option.clone()));
846 published_message_to_shell = true;
847 }
848
849 shell.capture_event();
850 shell.request_redraw();
851 }
852 (key::Named::ArrowDown, _) | (key::Named::Tab, false)
853 if !modifiers.shift() =>
854 {
855 if let Some(index) = &mut menu.hovered_option {
856 if *index >= state.filtered_options.options.len().saturating_sub(1)
857 {
858 *index = 0;
859 } else {
860 *index = index.saturating_add(1).min(
861 state.filtered_options.options.len().saturating_sub(1),
862 );
863 }
864 } else {
865 menu.hovered_option = Some(0);
866 }
867
868 if let Some(on_option_hovered) = &mut self.on_option_hovered
869 && let Some(option) = menu
870 .hovered_option
871 .and_then(|index| state.filtered_options.options.get(index))
872 {
873 shell.publish((on_option_hovered)(option.clone()));
874 published_message_to_shell = true;
875 }
876
877 shell.capture_event();
878 shell.request_redraw();
879 }
880 _ => {}
881 }
882 }
883 });
884 }
885
886 self.state.with_inner_mut(|state| {
887 if let Some(selection) = menu.new_selection.take() {
888 state.value = String::new();
889 state.filtered_options.update(self.state.options.clone());
890 menu.menu = menu_overlay::State::default();
891
892 shell.publish((self.on_selected)(selection));
893 published_message_to_shell = true;
894
895 let mut local_messages = Vec::new();
896 let mut local_shell = Shell::new(&mut local_messages);
897 self.text_input.update(
898 &mut tree.children[0],
899 &Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
900 layout,
901 mouse::Cursor::Unavailable,
902 renderer,
903 clipboard,
904 &mut local_shell,
905 viewport,
906 );
907 shell.request_input_method(local_shell.input_method());
908 }
909 });
910
911 let is_focused = {
912 let text_input_state = tree.children[0]
913 .state
914 .downcast_ref::<text_input::State<Renderer::Paragraph>>();
915
916 text_input_state.is_focused()
917 };
918
919 sync_mobile_keyboard(
920 started_focused,
921 is_focused,
922 activation.request_mobile_keyboard,
923 );
924
925 if started_focused != is_focused {
926 shell.invalidate_widgets();
927
928 if is_focused {
929 self.state.with_inner(|state| {
930 menu.menu
931 .start_open(state.filtered_options.options.len(), Instant::now());
932 });
933 }
934
935 if !published_message_to_shell {
936 if is_focused {
937 if let Some(on_open) = self.on_open.take() {
938 shell.publish(on_open);
939 }
940 } else if let Some(on_close) = self.on_close.take() {
941 shell.publish(on_close);
942 }
943 }
944 }
945 }
946
947 fn mouse_interaction(
948 &self,
949 tree: &widget::Tree,
950 layout: Layout<'_>,
951 cursor: mouse::Cursor,
952 viewport: &Rectangle,
953 renderer: &Renderer,
954 ) -> mouse::Interaction {
955 self.text_input
956 .mouse_interaction(&tree.children[0], layout, cursor, viewport, renderer)
957 }
958
959 fn draw(
960 &self,
961 tree: &widget::Tree,
962 renderer: &mut Renderer,
963 theme: &Theme,
964 _style: &renderer::Style,
965 layout: Layout<'_>,
966 cursor: mouse::Cursor,
967 viewport: &Rectangle,
968 ) {
969 register_mobile_text_region(true, layout.bounds(), viewport);
970
971 let is_focused = {
972 let text_input_state = tree.children[0]
973 .state
974 .downcast_ref::<text_input::State<Renderer::Paragraph>>();
975
976 text_input_state.is_focused()
977 };
978
979 let selection = if is_focused || self.selection.is_empty() {
980 None
981 } else {
982 Some(&self.selection)
983 };
984 let bounds = layout.bounds();
985 let is_hovered = cursor.is_over(bounds);
986 let label_x = bounds.x + tokens::component::text_field::LEADING_SPACE;
987 let label_width = if self.label.is_some() {
988 let state = tree
989 .state
990 .downcast_ref::<MenuState<T, Renderer::Paragraph>>();
991
992 state.label.min_width()
993 } else {
994 0.0
995 };
996 let label_notch = self.label.as_ref().and_then(|_| {
997 text_field_floating_label_notch(bounds, label_x, label_width, label_width, 1.0)
998 });
999 let outline_clip_width = if is_focused {
1000 tokens::component::text_field::FOCUS_OUTLINE_WIDTH
1001 } else {
1002 tokens::component::text_field::OUTLINE_WIDTH
1003 };
1004
1005 draw_text_field_notched(
1006 renderer,
1007 bounds,
1008 outline_clip_width,
1009 label_notch,
1010 |renderer| {
1011 self.text_input.draw(
1012 &tree.children[0],
1013 renderer,
1014 theme,
1015 layout,
1016 cursor,
1017 selection,
1018 viewport,
1019 );
1020 },
1021 );
1022
1023 if let Some(label) = &self.label {
1024 let label_size = Pixels(tokens::component::text_field::LABEL_TEXT_POPULATED_SIZE);
1025 let label_line_height = LineHeight::Absolute(Pixels(
1026 tokens::component::text_field::LABEL_TEXT_POPULATED_LINE_HEIGHT,
1027 ));
1028 let label_height = f32::from(label_line_height.to_absolute(label_size));
1029 let label_y = bounds.y;
1030
1031 renderer.fill_text(
1032 core_text::Text {
1033 content: label.clone(),
1034 size: label_size,
1035 line_height: label_line_height,
1036 font: self.font.unwrap_or_else(|| renderer.default_font()),
1037 bounds: Size::new(label_width, label_height),
1038 align_x: text::Alignment::Default,
1039 align_y: iced_widget::core::alignment::Vertical::Center,
1040 shaping: self.text_shaping,
1041 wrapping: text::Wrapping::None,
1042 },
1043 Point::new(label_x, label_y),
1044 combobox_label_color(theme, is_focused, is_hovered),
1045 *viewport,
1046 );
1047 }
1048 }
1049
1050 fn overlay<'b>(
1051 &'b mut self,
1052 tree: &'b mut widget::Tree,
1053 layout: Layout<'_>,
1054 renderer: &Renderer,
1055 viewport: &Rectangle,
1056 translation: Vector,
1057 ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
1058 let is_focused = {
1059 let text_input_state = tree.children[0]
1060 .state
1061 .downcast_ref::<text_input::State<Renderer::Paragraph>>();
1062
1063 text_input_state.is_focused()
1064 };
1065
1066 if is_focused {
1067 let MenuState {
1068 menu,
1069 filtered_options,
1070 hovered_option,
1071 ..
1072 } = tree
1073 .state
1074 .downcast_mut::<MenuState<T, Renderer::Paragraph>>();
1075
1076 self.state.sync_filtered_options(filtered_options);
1077
1078 if filtered_options.options.is_empty() {
1079 None
1080 } else {
1081 let bounds = layout.bounds();
1082
1083 let mut menu = menu_overlay::Menu::new(
1084 menu,
1085 &filtered_options.options,
1086 hovered_option,
1087 |selection| {
1088 self.state.with_inner_mut(|state| {
1089 state.value = String::new();
1090 state.filtered_options.update(self.state.options.clone());
1091 });
1092
1093 tree.children[0]
1094 .state
1095 .downcast_mut::<text_input::State<Renderer::Paragraph>>()
1096 .unfocus();
1097
1098 (self.on_selected)(selection)
1099 },
1100 self.on_option_hovered.as_deref(),
1101 &self.menu_class,
1102 )
1103 .width(bounds.width)
1104 .padding(self.option_padding)
1105 .text_line_height(self.line_height)
1106 .text_shaping(self.text_shaping);
1107
1108 if let Some(font) = self.font {
1109 menu = menu.font(font);
1110 }
1111
1112 if let Some(size) = self.size {
1113 menu = menu.text_size(size);
1114 }
1115
1116 let anchor = select::prefer_down_when_menu_fits(
1117 layout.position() + translation,
1118 *viewport,
1119 bounds.height,
1120 select::resolved_menu_height(
1121 self.menu_height,
1122 self.intrinsic_menu_height(filtered_options.options.len(), renderer),
1123 viewport.height,
1124 ),
1125 );
1126
1127 Some(menu.overlay(
1128 anchor.position,
1129 *viewport,
1130 anchor.target_height,
1131 self.menu_height,
1132 ))
1133 }
1134 } else {
1135 None
1136 }
1137 }
1138}
1139
1140impl<'a, T, Message, Renderer> From<ComboboxCore<'a, T, Message, Renderer>>
1141 for Element<'a, Message, Theme, Renderer>
1142where
1143 T: Display + Clone + 'static,
1144 Message: Clone + 'a,
1145 Renderer: core_text::Renderer + 'a,
1146{
1147 fn from(combobox: ComboboxCore<'a, T, Message, Renderer>) -> Self {
1148 Self::new(combobox)
1149 }
1150}
1151
1152fn combobox_label_color(theme: &Theme, is_focused: bool, is_hovered: bool) -> Color {
1153 let colors = theme.colors();
1154
1155 if is_focused {
1156 colors.primary.color
1157 } else if is_hovered {
1158 colors.surface.text
1159 } else {
1160 colors.surface.text_variant
1161 }
1162}
1163
1164fn search<'a, T, A>(
1165 options: impl IntoIterator<Item = T> + 'a,
1166 option_matchers: impl IntoIterator<Item = &'a A> + 'a,
1167 query: &'a str,
1168) -> impl Iterator<Item = T> + 'a
1169where
1170 A: AsRef<str> + 'a,
1171{
1172 let query: Vec<String> = query
1173 .to_lowercase()
1174 .split(|c: char| !c.is_ascii_alphanumeric())
1175 .map(String::from)
1176 .collect();
1177
1178 options
1179 .into_iter()
1180 .zip(option_matchers)
1181 .filter_map(move |(option, matcher)| {
1182 if query.iter().all(|part| matcher.as_ref().contains(part)) {
1183 Some(option)
1184 } else {
1185 None
1186 }
1187 })
1188}
1189
1190fn build_matchers<'a, T>(options: impl IntoIterator<Item = T> + 'a) -> Vec<String>
1191where
1192 T: Display + 'a,
1193{
1194 options.into_iter().map(build_matcher).collect()
1195}
1196
1197fn build_matcher<T>(option: T) -> String
1198where
1199 T: Display,
1200{
1201 let mut matcher = option.to_string();
1202 matcher.retain(|c| c.is_ascii_alphanumeric());
1203 matcher.to_lowercase()
1204}
1205
1206fn inner_options<T>(options: &[T]) -> Vec<DisplayValue<T>>
1207where
1208 T: Clone,
1209{
1210 options.iter().cloned().map(DisplayValue::Option).collect()
1211}
1212
1213#[cfg(test)]
1214#[path = "../../../tests/widget/component/combobox.rs"]
1215mod tests;