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 diff(&self, _tree: &mut widget::Tree) {
513 }
515
516 fn update(
517 &mut self,
518 tree: &mut widget::Tree,
519 event: &Event,
520 layout: Layout<'_>,
521 cursor: mouse::Cursor,
522 renderer: &Renderer,
523 clipboard: &mut dyn Clipboard,
524 shell: &mut Shell<'_, Message>,
525 viewport: &Rectangle,
526 ) {
527 let menu = tree.state.downcast_mut::<Menu<T>>();
528
529 let started_focused = {
530 let text_input_state = tree.children[0]
531 .state
532 .downcast_ref::<text_input::State<Renderer::Paragraph>>();
533
534 text_input_state.is_focused()
535 };
536 let mut published_message_to_shell = false;
539
540 let mut local_messages = Vec::new();
542 let mut local_shell = Shell::new(&mut local_messages);
543
544 self.text_input.update(
546 &mut tree.children[0],
547 event,
548 layout,
549 cursor,
550 renderer,
551 clipboard,
552 &mut local_shell,
553 viewport,
554 );
555
556 if local_shell.is_event_captured() {
557 shell.capture_event();
558 }
559
560 shell.request_redraw_at(local_shell.redraw_request());
561 shell.request_input_method(local_shell.input_method());
562
563 for message in local_messages {
565 let TextInputEvent::TextChanged(new_value) = message;
566
567 if let Some(on_input) = &self.on_input {
568 shell.publish((on_input)(new_value.clone()));
569 }
570
571 self.state.with_inner_mut(|state| {
575 menu.hovered_option = Some(0);
576 state.value = new_value;
577
578 state.filtered_options.update(
579 search(
580 &self.state.options,
581 &state.option_matchers,
582 &state.value,
583 )
584 .cloned()
585 .collect(),
586 );
587 });
588 shell.invalidate_layout();
589 shell.request_redraw();
590 }
591
592 let is_focused = {
593 let text_input_state = tree.children[0]
594 .state
595 .downcast_ref::<text_input::State<Renderer::Paragraph>>();
596
597 text_input_state.is_focused()
598 };
599
600 if is_focused {
601 self.state.with_inner(|state| {
602 if !started_focused
603 && let Some(on_option_hovered) = &mut self.on_option_hovered
604 {
605 let hovered_option = menu.hovered_option.unwrap_or(0);
606
607 if let Some(option) =
608 state.filtered_options.options.get(hovered_option)
609 {
610 shell.publish(on_option_hovered(option.clone()));
611 published_message_to_shell = true;
612 }
613 }
614
615 if let Event::Keyboard(keyboard::Event::KeyPressed {
616 key: keyboard::Key::Named(named_key),
617 modifiers,
618 ..
619 }) = event
620 {
621 let shift_modifier = modifiers.shift();
622 match (named_key, shift_modifier) {
623 (key::Named::Enter, _) => {
624 if let Some(index) = &menu.hovered_option
625 && let Some(option) =
626 state.filtered_options.options.get(*index)
627 {
628 menu.new_selection = Some(option.clone());
629 }
630
631 shell.capture_event();
632 shell.request_redraw();
633 }
634 (key::Named::ArrowUp, _) | (key::Named::Tab, true) => {
635 if let Some(index) = &mut menu.hovered_option {
636 if *index == 0 {
637 *index = state
638 .filtered_options
639 .options
640 .len()
641 .saturating_sub(1);
642 } else {
643 *index = index.saturating_sub(1);
644 }
645 } else {
646 menu.hovered_option = Some(0);
647 }
648
649 if let Some(on_option_hovered) =
650 &mut self.on_option_hovered
651 && let Some(option) =
652 menu.hovered_option.and_then(|index| {
653 state
654 .filtered_options
655 .options
656 .get(index)
657 })
658 {
659 shell.publish((on_option_hovered)(
661 option.clone(),
662 ));
663 published_message_to_shell = true;
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 && let Some(option) =
698 menu.hovered_option.and_then(|index| {
699 state
700 .filtered_options
701 .options
702 .get(index)
703 })
704 {
705 shell.publish((on_option_hovered)(
707 option.clone(),
708 ));
709 published_message_to_shell = true;
710 }
711
712 shell.capture_event();
713 shell.request_redraw();
714 }
715 _ => {}
716 }
717 }
718 });
719 }
720
721 self.state.with_inner_mut(|state| {
723 if let Some(selection) = menu.new_selection.take() {
724 state.value = String::new();
726 state.filtered_options.update(self.state.options.clone());
727 menu.menu = menu::State::default();
728
729 shell.publish((self.on_selected)(selection));
731 published_message_to_shell = true;
732
733 let mut local_messages = Vec::new();
735 let mut local_shell = Shell::new(&mut local_messages);
736 self.text_input.update(
737 &mut tree.children[0],
738 &Event::Mouse(mouse::Event::ButtonPressed(
739 mouse::Button::Left,
740 )),
741 layout,
742 mouse::Cursor::Unavailable,
743 renderer,
744 clipboard,
745 &mut local_shell,
746 viewport,
747 );
748 shell.request_input_method(local_shell.input_method());
749 }
750 });
751
752 let is_focused = {
753 let text_input_state = tree.children[0]
754 .state
755 .downcast_ref::<text_input::State<Renderer::Paragraph>>();
756
757 text_input_state.is_focused()
758 };
759
760 if started_focused != is_focused {
761 shell.invalidate_widgets();
763
764 if !published_message_to_shell {
765 if is_focused {
766 if let Some(on_open) = self.on_open.take() {
767 shell.publish(on_open);
768 }
769 } else if let Some(on_close) = self.on_close.take() {
770 shell.publish(on_close);
771 }
772 }
773 }
774 }
775
776 fn mouse_interaction(
777 &self,
778 tree: &widget::Tree,
779 layout: Layout<'_>,
780 cursor: mouse::Cursor,
781 viewport: &Rectangle,
782 renderer: &Renderer,
783 ) -> mouse::Interaction {
784 self.text_input.mouse_interaction(
785 &tree.children[0],
786 layout,
787 cursor,
788 viewport,
789 renderer,
790 )
791 }
792
793 fn draw(
794 &self,
795 tree: &widget::Tree,
796 renderer: &mut Renderer,
797 theme: &Theme,
798 _style: &renderer::Style,
799 layout: Layout<'_>,
800 cursor: mouse::Cursor,
801 viewport: &Rectangle,
802 ) {
803 let is_focused = {
804 let text_input_state = tree.children[0]
805 .state
806 .downcast_ref::<text_input::State<Renderer::Paragraph>>();
807
808 text_input_state.is_focused()
809 };
810
811 let selection = if is_focused || self.selection.is_empty() {
812 None
813 } else {
814 Some(&self.selection)
815 };
816
817 self.text_input.draw(
818 &tree.children[0],
819 renderer,
820 theme,
821 layout,
822 cursor,
823 selection,
824 viewport,
825 );
826 }
827
828 fn overlay<'b>(
829 &'b mut self,
830 tree: &'b mut widget::Tree,
831 layout: Layout<'_>,
832 _renderer: &Renderer,
833 viewport: &Rectangle,
834 translation: Vector,
835 ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
836 let is_focused = {
837 let text_input_state = tree.children[0]
838 .state
839 .downcast_ref::<text_input::State<Renderer::Paragraph>>();
840
841 text_input_state.is_focused()
842 };
843
844 if is_focused {
845 let Menu {
846 menu,
847 filtered_options,
848 hovered_option,
849 ..
850 } = tree.state.downcast_mut::<Menu<T>>();
851
852 self.state.sync_filtered_options(filtered_options);
853
854 if filtered_options.options.is_empty() {
855 None
856 } else {
857 let bounds = layout.bounds();
858
859 let mut menu = menu::Menu::new(
860 menu,
861 &filtered_options.options,
862 hovered_option,
863 |x| {
864 tree.children[0]
865 .state
866 .downcast_mut::<text_input::State<Renderer::Paragraph>>(
867 )
868 .unfocus();
869
870 (self.on_selected)(x)
871 },
872 self.on_option_hovered.as_deref(),
873 &self.menu_class,
874 )
875 .width(bounds.width)
876 .padding(self.padding);
877
878 if let Some(font) = self.font {
879 menu = menu.font(font);
880 }
881
882 if let Some(size) = self.size {
883 menu = menu.text_size(size);
884 }
885
886 Some(menu.overlay(
887 layout.position() + translation,
888 *viewport,
889 bounds.height,
890 ))
891 }
892 } else {
893 None
894 }
895 }
896}
897
898impl<'a, T, Message, Theme, Renderer>
899 From<ComboBox<'a, T, Message, Theme, Renderer>>
900 for Element<'a, Message, Theme, Renderer>
901where
902 T: Display + Clone + 'static,
903 Message: Clone + 'a,
904 Theme: Catalog + 'a,
905 Renderer: text::Renderer + 'a,
906{
907 fn from(combo_box: ComboBox<'a, T, Message, Theme, Renderer>) -> Self {
908 Self::new(combo_box)
909 }
910}
911
912pub trait Catalog: text_input::Catalog + menu::Catalog {
914 fn default_input<'a>() -> <Self as text_input::Catalog>::Class<'a> {
916 <Self as text_input::Catalog>::default()
917 }
918
919 fn default_menu<'a>() -> <Self as menu::Catalog>::Class<'a> {
921 <Self as menu::Catalog>::default()
922 }
923}
924
925impl Catalog for Theme {}
926
927fn search<'a, T, A>(
928 options: impl IntoIterator<Item = T> + 'a,
929 option_matchers: impl IntoIterator<Item = &'a A> + 'a,
930 query: &'a str,
931) -> impl Iterator<Item = T> + 'a
932where
933 A: AsRef<str> + 'a,
934{
935 let query: Vec<String> = query
936 .to_lowercase()
937 .split(|c: char| !c.is_ascii_alphanumeric())
938 .map(String::from)
939 .collect();
940
941 options
942 .into_iter()
943 .zip(option_matchers)
944 .filter_map(move |(option, matcher)| {
946 if query.iter().all(|part| matcher.as_ref().contains(part)) {
947 Some(option)
948 } else {
949 None
950 }
951 })
952}
953
954fn build_matchers<'a, T>(
955 options: impl IntoIterator<Item = T> + 'a,
956) -> Vec<String>
957where
958 T: Display + 'a,
959{
960 options
961 .into_iter()
962 .map(|opt| {
963 let mut matcher = opt.to_string();
964 matcher.retain(|c| c.is_ascii_alphanumeric());
965 matcher.to_lowercase()
966 })
967 .collect()
968}