1use crate::core::keyboard;
58use crate::core::keyboard::key;
59use crate::core::layout::{self, Layout};
60use crate::core::mouse;
61use crate::core::overlay;
62use crate::core::renderer;
63use crate::core::text;
64use crate::core::time::Instant;
65use crate::core::widget::{self, Widget};
66use crate::core::{
67 Clipboard, Element, Event, Length, Padding, Rectangle, Shell, Size, Theme,
68 Vector,
69};
70use crate::overlay::menu;
71use crate::text::LineHeight;
72use crate::text_input::{self, TextInput};
73
74use std::cell::RefCell;
75use std::fmt::Display;
76
77#[allow(missing_debug_implementations)]
134pub struct ComboBox<
135 'a,
136 T,
137 Message,
138 Theme = crate::Theme,
139 Renderer = crate::Renderer,
140> where
141 Theme: Catalog,
142 Renderer: text::Renderer,
143{
144 state: &'a State<T>,
145 text_input: TextInput<'a, TextInputEvent, Theme, Renderer>,
146 font: Option<Renderer::Font>,
147 selection: text_input::Value,
148 on_selected: Box<dyn Fn(T) -> Message>,
149 on_option_hovered: Option<Box<dyn Fn(T) -> Message>>,
150 on_open: Option<Message>,
151 on_close: Option<Message>,
152 on_input: Option<Box<dyn Fn(String) -> Message>>,
153 menu_class: <Theme as menu::Catalog>::Class<'a>,
154 padding: Padding,
155 size: Option<f32>,
156}
157
158impl<'a, T, Message, Theme, Renderer> ComboBox<'a, T, Message, Theme, Renderer>
159where
160 T: std::fmt::Display + Clone,
161 Theme: Catalog,
162 Renderer: text::Renderer,
163{
164 pub fn new(
168 state: &'a State<T>,
169 placeholder: &str,
170 selection: Option<&T>,
171 on_selected: impl Fn(T) -> Message + 'static,
172 ) -> Self {
173 let text_input = TextInput::new(placeholder, &state.value())
174 .on_input(TextInputEvent::TextChanged)
175 .class(Theme::default_input());
176
177 let selection = selection.map(T::to_string).unwrap_or_default();
178
179 Self {
180 state,
181 text_input,
182 font: None,
183 selection: text_input::Value::new(&selection),
184 on_selected: Box::new(on_selected),
185 on_option_hovered: None,
186 on_input: None,
187 on_open: None,
188 on_close: None,
189 menu_class: <Theme as Catalog>::default_menu(),
190 padding: text_input::DEFAULT_PADDING,
191 size: None,
192 }
193 }
194
195 pub fn on_input(
198 mut self,
199 on_input: impl Fn(String) -> Message + 'static,
200 ) -> Self {
201 self.on_input = Some(Box::new(on_input));
202 self
203 }
204
205 pub fn on_option_hovered(
208 mut self,
209 on_option_hovered: impl Fn(T) -> Message + 'static,
210 ) -> Self {
211 self.on_option_hovered = Some(Box::new(on_option_hovered));
212 self
213 }
214
215 pub fn on_open(mut self, message: Message) -> Self {
218 self.on_open = Some(message);
219 self
220 }
221
222 pub fn on_close(mut self, message: Message) -> Self {
225 self.on_close = Some(message);
226 self
227 }
228
229 pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
231 self.padding = padding.into();
232 self.text_input = self.text_input.padding(self.padding);
233 self
234 }
235
236 pub fn font(mut self, font: Renderer::Font) -> Self {
240 self.text_input = self.text_input.font(font);
241 self.font = Some(font);
242 self
243 }
244
245 pub fn icon(mut self, icon: text_input::Icon<Renderer::Font>) -> Self {
247 self.text_input = self.text_input.icon(icon);
248 self
249 }
250
251 pub fn size(mut self, size: f32) -> Self {
253 self.text_input = self.text_input.size(size);
254 self.size = Some(size);
255 self
256 }
257
258 pub fn line_height(self, line_height: impl Into<LineHeight>) -> Self {
260 Self {
261 text_input: self.text_input.line_height(line_height),
262 ..self
263 }
264 }
265
266 pub fn width(self, width: impl Into<Length>) -> Self {
268 Self {
269 text_input: self.text_input.width(width),
270 ..self
271 }
272 }
273
274 #[must_use]
276 pub fn input_style(
277 mut self,
278 style: impl Fn(&Theme, text_input::Status) -> text_input::Style + 'a,
279 ) -> Self
280 where
281 <Theme as text_input::Catalog>::Class<'a>:
282 From<text_input::StyleFn<'a, Theme>>,
283 {
284 self.text_input = self.text_input.style(style);
285 self
286 }
287
288 #[must_use]
290 pub fn menu_style(
291 mut self,
292 style: impl Fn(&Theme) -> menu::Style + 'a,
293 ) -> Self
294 where
295 <Theme as menu::Catalog>::Class<'a>: From<menu::StyleFn<'a, Theme>>,
296 {
297 self.menu_class = (Box::new(style) as menu::StyleFn<'a, Theme>).into();
298 self
299 }
300
301 #[cfg(feature = "advanced")]
303 #[must_use]
304 pub fn input_class(
305 mut self,
306 class: impl Into<<Theme as text_input::Catalog>::Class<'a>>,
307 ) -> Self {
308 self.text_input = self.text_input.class(class);
309 self
310 }
311
312 #[cfg(feature = "advanced")]
314 #[must_use]
315 pub fn menu_class(
316 mut self,
317 class: impl Into<<Theme as menu::Catalog>::Class<'a>>,
318 ) -> Self {
319 self.menu_class = class.into();
320 self
321 }
322}
323
324#[derive(Debug, Clone)]
326pub struct State<T> {
327 options: Vec<T>,
328 inner: RefCell<Inner<T>>,
329}
330
331#[derive(Debug, Clone)]
332struct Inner<T> {
333 value: String,
334 option_matchers: Vec<String>,
335 filtered_options: Filtered<T>,
336}
337
338#[derive(Debug, Clone)]
339struct Filtered<T> {
340 options: Vec<T>,
341 updated: Instant,
342}
343
344impl<T> State<T>
345where
346 T: Display + Clone,
347{
348 pub fn new(options: Vec<T>) -> Self {
350 Self::with_selection(options, None)
351 }
352
353 pub fn with_selection(options: Vec<T>, selection: Option<&T>) -> Self {
356 let value = selection.map(T::to_string).unwrap_or_default();
357
358 let option_matchers = build_matchers(&options);
360
361 let filtered_options = Filtered::new(
362 search(&options, &option_matchers, &value)
363 .cloned()
364 .collect(),
365 );
366
367 Self {
368 options,
369 inner: RefCell::new(Inner {
370 value,
371 option_matchers,
372 filtered_options,
373 }),
374 }
375 }
376
377 pub fn options(&self) -> &[T] {
382 &self.options
383 }
384
385 fn value(&self) -> String {
386 let inner = self.inner.borrow();
387
388 inner.value.clone()
389 }
390
391 fn with_inner<O>(&self, f: impl FnOnce(&Inner<T>) -> O) -> O {
392 let inner = self.inner.borrow();
393
394 f(&inner)
395 }
396
397 fn with_inner_mut(&self, f: impl FnOnce(&mut Inner<T>)) {
398 let mut inner = self.inner.borrow_mut();
399
400 f(&mut inner);
401 }
402
403 fn sync_filtered_options(&self, options: &mut Filtered<T>) {
404 let inner = self.inner.borrow();
405
406 inner.filtered_options.sync(options);
407 }
408}
409
410impl<T> Default for State<T>
411where
412 T: Display + Clone,
413{
414 fn default() -> Self {
415 Self::new(Vec::new())
416 }
417}
418
419impl<T> Filtered<T>
420where
421 T: Clone,
422{
423 fn new(options: Vec<T>) -> Self {
424 Self {
425 options,
426 updated: Instant::now(),
427 }
428 }
429
430 fn empty() -> Self {
431 Self {
432 options: vec![],
433 updated: Instant::now(),
434 }
435 }
436
437 fn update(&mut self, options: Vec<T>) {
438 self.options = options;
439 self.updated = Instant::now();
440 }
441
442 fn sync(&self, other: &mut Filtered<T>) {
443 if other.updated != self.updated {
444 *other = self.clone();
445 }
446 }
447}
448
449struct Menu<T> {
450 menu: menu::State,
451 hovered_option: Option<usize>,
452 new_selection: Option<T>,
453 filtered_options: Filtered<T>,
454}
455
456#[derive(Debug, Clone)]
457enum TextInputEvent {
458 TextChanged(String),
459}
460
461impl<T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
462 for ComboBox<'_, T, Message, Theme, Renderer>
463where
464 T: Display + Clone + 'static,
465 Message: Clone,
466 Theme: Catalog,
467 Renderer: text::Renderer,
468{
469 fn size(&self) -> Size<Length> {
470 Widget::<TextInputEvent, Theme, Renderer>::size(&self.text_input)
471 }
472
473 fn layout(
474 &self,
475 tree: &mut widget::Tree,
476 renderer: &Renderer,
477 limits: &layout::Limits,
478 ) -> layout::Node {
479 let is_focused = {
480 let text_input_state = tree.children[0]
481 .state
482 .downcast_ref::<text_input::State<Renderer::Paragraph>>();
483
484 text_input_state.is_focused()
485 };
486
487 self.text_input.layout(
488 &mut tree.children[0],
489 renderer,
490 limits,
491 (!is_focused).then_some(&self.selection),
492 )
493 }
494
495 fn tag(&self) -> widget::tree::Tag {
496 widget::tree::Tag::of::<Menu<T>>()
497 }
498
499 fn state(&self) -> widget::tree::State {
500 widget::tree::State::new(Menu::<T> {
501 menu: menu::State::new(),
502 filtered_options: Filtered::empty(),
503 hovered_option: Some(0),
504 new_selection: None,
505 })
506 }
507
508 fn children(&self) -> Vec<widget::Tree> {
509 vec![widget::Tree::new(&self.text_input as &dyn Widget<_, _, _>)]
510 }
511
512 fn update(
513 &mut self,
514 tree: &mut widget::Tree,
515 event: &Event,
516 layout: Layout<'_>,
517 cursor: mouse::Cursor,
518 renderer: &Renderer,
519 clipboard: &mut dyn Clipboard,
520 shell: &mut Shell<'_, Message>,
521 viewport: &Rectangle,
522 ) {
523 let menu = tree.state.downcast_mut::<Menu<T>>();
524
525 let started_focused = {
526 let text_input_state = tree.children[0]
527 .state
528 .downcast_ref::<text_input::State<Renderer::Paragraph>>();
529
530 text_input_state.is_focused()
531 };
532 let mut published_message_to_shell = false;
535
536 let mut local_messages = Vec::new();
538 let mut local_shell = Shell::new(&mut local_messages);
539
540 self.text_input.update(
542 &mut tree.children[0],
543 event,
544 layout,
545 cursor,
546 renderer,
547 clipboard,
548 &mut local_shell,
549 viewport,
550 );
551
552 if local_shell.is_event_captured() {
553 shell.capture_event();
554 }
555
556 shell.request_redraw_at(local_shell.redraw_request());
557 shell.request_input_method(local_shell.input_method());
558
559 for message in local_messages {
561 let TextInputEvent::TextChanged(new_value) = message;
562
563 if let Some(on_input) = &self.on_input {
564 shell.publish((on_input)(new_value.clone()));
565 }
566
567 self.state.with_inner_mut(|state| {
571 menu.hovered_option = Some(0);
572 state.value = new_value;
573
574 state.filtered_options.update(
575 search(
576 &self.state.options,
577 &state.option_matchers,
578 &state.value,
579 )
580 .cloned()
581 .collect(),
582 );
583 });
584 shell.invalidate_layout();
585 shell.request_redraw();
586 }
587
588 let is_focused = {
589 let text_input_state = tree.children[0]
590 .state
591 .downcast_ref::<text_input::State<Renderer::Paragraph>>();
592
593 text_input_state.is_focused()
594 };
595
596 if is_focused {
597 self.state.with_inner(|state| {
598 if !started_focused {
599 if let Some(on_option_hovered) = &mut self.on_option_hovered
600 {
601 let hovered_option = menu.hovered_option.unwrap_or(0);
602
603 if let Some(option) =
604 state.filtered_options.options.get(hovered_option)
605 {
606 shell.publish(on_option_hovered(option.clone()));
607 published_message_to_shell = true;
608 }
609 }
610 }
611
612 if let Event::Keyboard(keyboard::Event::KeyPressed {
613 key: keyboard::Key::Named(named_key),
614 modifiers,
615 ..
616 }) = event
617 {
618 let shift_modifier = modifiers.shift();
619 match (named_key, shift_modifier) {
620 (key::Named::Enter, _) => {
621 if let Some(index) = &menu.hovered_option {
622 if let Some(option) =
623 state.filtered_options.options.get(*index)
624 {
625 menu.new_selection = Some(option.clone());
626 }
627 }
628
629 shell.capture_event();
630 shell.request_redraw();
631 }
632 (key::Named::ArrowUp, _) | (key::Named::Tab, true) => {
633 if let Some(index) = &mut menu.hovered_option {
634 if *index == 0 {
635 *index = state
636 .filtered_options
637 .options
638 .len()
639 .saturating_sub(1);
640 } else {
641 *index = index.saturating_sub(1);
642 }
643 } else {
644 menu.hovered_option = Some(0);
645 }
646
647 if let Some(on_option_hovered) =
648 &mut self.on_option_hovered
649 {
650 if let Some(option) =
651 menu.hovered_option.and_then(|index| {
652 state
653 .filtered_options
654 .options
655 .get(index)
656 })
657 {
658 shell.publish((on_option_hovered)(
660 option.clone(),
661 ));
662 published_message_to_shell = true;
663 }
664 }
665
666 shell.capture_event();
667 shell.request_redraw();
668 }
669 (key::Named::ArrowDown, _)
670 | (key::Named::Tab, false)
671 if !modifiers.shift() =>
672 {
673 if let Some(index) = &mut menu.hovered_option {
674 if *index
675 >= state
676 .filtered_options
677 .options
678 .len()
679 .saturating_sub(1)
680 {
681 *index = 0;
682 } else {
683 *index = index.saturating_add(1).min(
684 state
685 .filtered_options
686 .options
687 .len()
688 .saturating_sub(1),
689 );
690 }
691 } else {
692 menu.hovered_option = Some(0);
693 }
694
695 if let Some(on_option_hovered) =
696 &mut self.on_option_hovered
697 {
698 if let Some(option) =
699 menu.hovered_option.and_then(|index| {
700 state
701 .filtered_options
702 .options
703 .get(index)
704 })
705 {
706 shell.publish((on_option_hovered)(
708 option.clone(),
709 ));
710 published_message_to_shell = true;
711 }
712 }
713
714 shell.capture_event();
715 shell.request_redraw();
716 }
717 _ => {}
718 }
719 }
720 });
721 }
722
723 self.state.with_inner_mut(|state| {
725 if let Some(selection) = menu.new_selection.take() {
726 state.value = String::new();
728 state.filtered_options.update(self.state.options.clone());
729 menu.menu = menu::State::default();
730
731 shell.publish((self.on_selected)(selection));
733 published_message_to_shell = true;
734
735 let mut local_messages = Vec::new();
737 let mut local_shell = Shell::new(&mut local_messages);
738 self.text_input.update(
739 &mut tree.children[0],
740 &Event::Mouse(mouse::Event::ButtonPressed(
741 mouse::Button::Left,
742 )),
743 layout,
744 mouse::Cursor::Unavailable,
745 renderer,
746 clipboard,
747 &mut local_shell,
748 viewport,
749 );
750 shell.request_input_method(local_shell.input_method());
751 }
752 });
753
754 let is_focused = {
755 let text_input_state = tree.children[0]
756 .state
757 .downcast_ref::<text_input::State<Renderer::Paragraph>>();
758
759 text_input_state.is_focused()
760 };
761
762 if started_focused != is_focused {
763 shell.invalidate_widgets();
765
766 if !published_message_to_shell {
767 if is_focused {
768 if let Some(on_open) = self.on_open.take() {
769 shell.publish(on_open);
770 }
771 } else if let Some(on_close) = self.on_close.take() {
772 shell.publish(on_close);
773 }
774 }
775 }
776 }
777
778 fn mouse_interaction(
779 &self,
780 tree: &widget::Tree,
781 layout: Layout<'_>,
782 cursor: mouse::Cursor,
783 viewport: &Rectangle,
784 renderer: &Renderer,
785 ) -> mouse::Interaction {
786 self.text_input.mouse_interaction(
787 &tree.children[0],
788 layout,
789 cursor,
790 viewport,
791 renderer,
792 )
793 }
794
795 fn draw(
796 &self,
797 tree: &widget::Tree,
798 renderer: &mut Renderer,
799 theme: &Theme,
800 _style: &renderer::Style,
801 layout: Layout<'_>,
802 cursor: mouse::Cursor,
803 viewport: &Rectangle,
804 ) {
805 let is_focused = {
806 let text_input_state = tree.children[0]
807 .state
808 .downcast_ref::<text_input::State<Renderer::Paragraph>>();
809
810 text_input_state.is_focused()
811 };
812
813 let selection = if is_focused || self.selection.is_empty() {
814 None
815 } else {
816 Some(&self.selection)
817 };
818
819 self.text_input.draw(
820 &tree.children[0],
821 renderer,
822 theme,
823 layout,
824 cursor,
825 selection,
826 viewport,
827 );
828 }
829
830 fn overlay<'b>(
831 &'b mut self,
832 tree: &'b mut widget::Tree,
833 layout: Layout<'_>,
834 _renderer: &Renderer,
835 translation: Vector,
836 ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
837 let is_focused = {
838 let text_input_state = tree.children[0]
839 .state
840 .downcast_ref::<text_input::State<Renderer::Paragraph>>();
841
842 text_input_state.is_focused()
843 };
844
845 if is_focused {
846 let Menu {
847 menu,
848 filtered_options,
849 hovered_option,
850 ..
851 } = tree.state.downcast_mut::<Menu<T>>();
852
853 self.state.sync_filtered_options(filtered_options);
854
855 if filtered_options.options.is_empty() {
856 None
857 } else {
858 let bounds = layout.bounds();
859
860 let mut menu = menu::Menu::new(
861 menu,
862 &filtered_options.options,
863 hovered_option,
864 |x| {
865 tree.children[0]
866 .state
867 .downcast_mut::<text_input::State<Renderer::Paragraph>>(
868 )
869 .unfocus();
870
871 (self.on_selected)(x)
872 },
873 self.on_option_hovered.as_deref(),
874 &self.menu_class,
875 )
876 .width(bounds.width)
877 .padding(self.padding);
878
879 if let Some(font) = self.font {
880 menu = menu.font(font);
881 }
882
883 if let Some(size) = self.size {
884 menu = menu.text_size(size);
885 }
886
887 Some(
888 menu.overlay(
889 layout.position() + translation,
890 bounds.height,
891 ),
892 )
893 }
894 } else {
895 None
896 }
897 }
898}
899
900impl<'a, T, Message, Theme, Renderer>
901 From<ComboBox<'a, T, Message, Theme, Renderer>>
902 for Element<'a, Message, Theme, Renderer>
903where
904 T: Display + Clone + 'static,
905 Message: Clone + 'a,
906 Theme: Catalog + 'a,
907 Renderer: text::Renderer + 'a,
908{
909 fn from(combo_box: ComboBox<'a, T, Message, Theme, Renderer>) -> Self {
910 Self::new(combo_box)
911 }
912}
913
914pub trait Catalog: text_input::Catalog + menu::Catalog {
916 fn default_input<'a>() -> <Self as text_input::Catalog>::Class<'a> {
918 <Self as text_input::Catalog>::default()
919 }
920
921 fn default_menu<'a>() -> <Self as menu::Catalog>::Class<'a> {
923 <Self as menu::Catalog>::default()
924 }
925}
926
927impl Catalog for Theme {}
928
929fn search<'a, T, A>(
930 options: impl IntoIterator<Item = T> + 'a,
931 option_matchers: impl IntoIterator<Item = &'a A> + 'a,
932 query: &'a str,
933) -> impl Iterator<Item = T> + 'a
934where
935 A: AsRef<str> + 'a,
936{
937 let query: Vec<String> = query
938 .to_lowercase()
939 .split(|c: char| !c.is_ascii_alphanumeric())
940 .map(String::from)
941 .collect();
942
943 options
944 .into_iter()
945 .zip(option_matchers)
946 .filter_map(move |(option, matcher)| {
948 if query.iter().all(|part| matcher.as_ref().contains(part)) {
949 Some(option)
950 } else {
951 None
952 }
953 })
954}
955
956fn build_matchers<'a, T>(
957 options: impl IntoIterator<Item = T> + 'a,
958) -> Vec<String>
959where
960 T: Display + 'a,
961{
962 options
963 .into_iter()
964 .map(|opt| {
965 let mut matcher = opt.to_string();
966 matcher.retain(|c| c.is_ascii_alphanumeric());
967 matcher.to_lowercase()
968 })
969 .collect()
970}