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 if 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
616 if let Event::Keyboard(keyboard::Event::KeyPressed {
617 key: keyboard::Key::Named(named_key),
618 modifiers,
619 ..
620 }) = event
621 {
622 let shift_modifier = modifiers.shift();
623 match (named_key, shift_modifier) {
624 (key::Named::Enter, _) => {
625 if let Some(index) = &menu.hovered_option {
626 if let Some(option) =
627 state.filtered_options.options.get(*index)
628 {
629 menu.new_selection = Some(option.clone());
630 }
631 }
632
633 shell.capture_event();
634 shell.request_redraw();
635 }
636 (key::Named::ArrowUp, _) | (key::Named::Tab, true) => {
637 if let Some(index) = &mut menu.hovered_option {
638 if *index == 0 {
639 *index = state
640 .filtered_options
641 .options
642 .len()
643 .saturating_sub(1);
644 } else {
645 *index = index.saturating_sub(1);
646 }
647 } else {
648 menu.hovered_option = Some(0);
649 }
650
651 if let Some(on_option_hovered) =
652 &mut self.on_option_hovered
653 {
654 if let Some(option) =
655 menu.hovered_option.and_then(|index| {
656 state
657 .filtered_options
658 .options
659 .get(index)
660 })
661 {
662 shell.publish((on_option_hovered)(
664 option.clone(),
665 ));
666 published_message_to_shell = true;
667 }
668 }
669
670 shell.capture_event();
671 shell.request_redraw();
672 }
673 (key::Named::ArrowDown, _)
674 | (key::Named::Tab, false)
675 if !modifiers.shift() =>
676 {
677 if let Some(index) = &mut menu.hovered_option {
678 if *index
679 >= state
680 .filtered_options
681 .options
682 .len()
683 .saturating_sub(1)
684 {
685 *index = 0;
686 } else {
687 *index = index.saturating_add(1).min(
688 state
689 .filtered_options
690 .options
691 .len()
692 .saturating_sub(1),
693 );
694 }
695 } else {
696 menu.hovered_option = Some(0);
697 }
698
699 if let Some(on_option_hovered) =
700 &mut self.on_option_hovered
701 {
702 if let Some(option) =
703 menu.hovered_option.and_then(|index| {
704 state
705 .filtered_options
706 .options
707 .get(index)
708 })
709 {
710 shell.publish((on_option_hovered)(
712 option.clone(),
713 ));
714 published_message_to_shell = true;
715 }
716 }
717
718 shell.capture_event();
719 shell.request_redraw();
720 }
721 _ => {}
722 }
723 }
724 });
725 }
726
727 self.state.with_inner_mut(|state| {
729 if let Some(selection) = menu.new_selection.take() {
730 state.value = String::new();
732 state.filtered_options.update(self.state.options.clone());
733 menu.menu = menu::State::default();
734
735 shell.publish((self.on_selected)(selection));
737 published_message_to_shell = true;
738
739 let mut local_messages = Vec::new();
741 let mut local_shell = Shell::new(&mut local_messages);
742 self.text_input.update(
743 &mut tree.children[0],
744 &Event::Mouse(mouse::Event::ButtonPressed(
745 mouse::Button::Left,
746 )),
747 layout,
748 mouse::Cursor::Unavailable,
749 renderer,
750 clipboard,
751 &mut local_shell,
752 viewport,
753 );
754 shell.request_input_method(local_shell.input_method());
755 }
756 });
757
758 let is_focused = {
759 let text_input_state = tree.children[0]
760 .state
761 .downcast_ref::<text_input::State<Renderer::Paragraph>>();
762
763 text_input_state.is_focused()
764 };
765
766 if started_focused != is_focused {
767 shell.invalidate_widgets();
769
770 if !published_message_to_shell {
771 if is_focused {
772 if let Some(on_open) = self.on_open.take() {
773 shell.publish(on_open);
774 }
775 } else if let Some(on_close) = self.on_close.take() {
776 shell.publish(on_close);
777 }
778 }
779 }
780 }
781
782 fn mouse_interaction(
783 &self,
784 tree: &widget::Tree,
785 layout: Layout<'_>,
786 cursor: mouse::Cursor,
787 viewport: &Rectangle,
788 renderer: &Renderer,
789 ) -> mouse::Interaction {
790 self.text_input.mouse_interaction(
791 &tree.children[0],
792 layout,
793 cursor,
794 viewport,
795 renderer,
796 )
797 }
798
799 fn draw(
800 &self,
801 tree: &widget::Tree,
802 renderer: &mut Renderer,
803 theme: &Theme,
804 _style: &renderer::Style,
805 layout: Layout<'_>,
806 cursor: mouse::Cursor,
807 viewport: &Rectangle,
808 ) {
809 let is_focused = {
810 let text_input_state = tree.children[0]
811 .state
812 .downcast_ref::<text_input::State<Renderer::Paragraph>>();
813
814 text_input_state.is_focused()
815 };
816
817 let selection = if is_focused || self.selection.is_empty() {
818 None
819 } else {
820 Some(&self.selection)
821 };
822
823 self.text_input.draw(
824 &tree.children[0],
825 renderer,
826 theme,
827 layout,
828 cursor,
829 selection,
830 viewport,
831 );
832 }
833
834 fn overlay<'b>(
835 &'b mut self,
836 tree: &'b mut widget::Tree,
837 layout: Layout<'_>,
838 _renderer: &Renderer,
839 _viewport: &Rectangle,
840 translation: Vector,
841 ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
842 let is_focused = {
843 let text_input_state = tree.children[0]
844 .state
845 .downcast_ref::<text_input::State<Renderer::Paragraph>>();
846
847 text_input_state.is_focused()
848 };
849
850 if is_focused {
851 let Menu {
852 menu,
853 filtered_options,
854 hovered_option,
855 ..
856 } = tree.state.downcast_mut::<Menu<T>>();
857
858 self.state.sync_filtered_options(filtered_options);
859
860 if filtered_options.options.is_empty() {
861 None
862 } else {
863 let bounds = layout.bounds();
864
865 let mut menu = menu::Menu::new(
866 menu,
867 &filtered_options.options,
868 hovered_option,
869 |x| {
870 tree.children[0]
871 .state
872 .downcast_mut::<text_input::State<Renderer::Paragraph>>(
873 )
874 .unfocus();
875
876 (self.on_selected)(x)
877 },
878 self.on_option_hovered.as_deref(),
879 &self.menu_class,
880 )
881 .width(bounds.width)
882 .padding(self.padding);
883
884 if let Some(font) = self.font {
885 menu = menu.font(font);
886 }
887
888 if let Some(size) = self.size {
889 menu = menu.text_size(size);
890 }
891
892 Some(
893 menu.overlay(
894 layout.position() + translation,
895 bounds.height,
896 ),
897 )
898 }
899 } else {
900 None
901 }
902 }
903}
904
905impl<'a, T, Message, Theme, Renderer>
906 From<ComboBox<'a, T, Message, Theme, Renderer>>
907 for Element<'a, Message, Theme, Renderer>
908where
909 T: Display + Clone + 'static,
910 Message: Clone + 'a,
911 Theme: Catalog + 'a,
912 Renderer: text::Renderer + 'a,
913{
914 fn from(combo_box: ComboBox<'a, T, Message, Theme, Renderer>) -> Self {
915 Self::new(combo_box)
916 }
917}
918
919pub trait Catalog: text_input::Catalog + menu::Catalog {
921 fn default_input<'a>() -> <Self as text_input::Catalog>::Class<'a> {
923 <Self as text_input::Catalog>::default()
924 }
925
926 fn default_menu<'a>() -> <Self as menu::Catalog>::Class<'a> {
928 <Self as menu::Catalog>::default()
929 }
930}
931
932impl Catalog for Theme {}
933
934fn search<'a, T, A>(
935 options: impl IntoIterator<Item = T> + 'a,
936 option_matchers: impl IntoIterator<Item = &'a A> + 'a,
937 query: &'a str,
938) -> impl Iterator<Item = T> + 'a
939where
940 A: AsRef<str> + 'a,
941{
942 let query: Vec<String> = query
943 .to_lowercase()
944 .split(|c: char| !c.is_ascii_alphanumeric())
945 .map(String::from)
946 .collect();
947
948 options
949 .into_iter()
950 .zip(option_matchers)
951 .filter_map(move |(option, matcher)| {
953 if query.iter().all(|part| matcher.as_ref().contains(part)) {
954 Some(option)
955 } else {
956 None
957 }
958 })
959}
960
961fn build_matchers<'a, T>(
962 options: impl IntoIterator<Item = T> + 'a,
963) -> Vec<String>
964where
965 T: Display + 'a,
966{
967 options
968 .into_iter()
969 .map(|opt| {
970 let mut matcher = opt.to_string();
971 matcher.retain(|c| c.is_ascii_alphanumeric());
972 matcher.to_lowercase()
973 })
974 .collect()
975}