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