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