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 menu_class: <Theme as menu::Catalog>::Class<'a>,
153 padding: Padding,
154 size: Option<f32>,
155}
156
157impl<'a, T, Message, Theme, Renderer> ComboBox<'a, T, Message, Theme, Renderer>
158where
159 T: std::fmt::Display + Clone,
160 Theme: Catalog,
161 Renderer: text::Renderer,
162{
163 pub fn new(
167 state: &'a State<T>,
168 placeholder: &str,
169 selection: Option<&T>,
170 on_selected: impl Fn(T) -> Message + 'static,
171 ) -> Self {
172 let text_input = TextInput::new(placeholder, &state.value())
173 .on_input(TextInputEvent::TextChanged)
174 .class(Theme::default_input());
175
176 let selection = selection.map(T::to_string).unwrap_or_default();
177
178 Self {
179 state,
180 text_input,
181 font: None,
182 selection: text_input::Value::new(&selection),
183 on_selected: Box::new(on_selected),
184 on_option_hovered: None,
185 on_input: None,
186 on_open: None,
187 on_close: None,
188 menu_class: <Theme as Catalog>::default_menu(),
189 padding: text_input::DEFAULT_PADDING,
190 size: None,
191 }
192 }
193
194 pub fn on_input(
197 mut self,
198 on_input: impl Fn(String) -> Message + 'static,
199 ) -> Self {
200 self.on_input = Some(Box::new(on_input));
201 self
202 }
203
204 pub fn on_option_hovered(
207 mut self,
208 on_option_hovered: impl Fn(T) -> Message + 'static,
209 ) -> Self {
210 self.on_option_hovered = Some(Box::new(on_option_hovered));
211 self
212 }
213
214 pub fn on_open(mut self, message: Message) -> Self {
217 self.on_open = Some(message);
218 self
219 }
220
221 pub fn on_close(mut self, message: Message) -> Self {
224 self.on_close = Some(message);
225 self
226 }
227
228 pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
230 self.padding = padding.into();
231 self.text_input = self.text_input.padding(self.padding);
232 self
233 }
234
235 pub fn font(mut self, font: Renderer::Font) -> Self {
239 self.text_input = self.text_input.font(font);
240 self.font = Some(font);
241 self
242 }
243
244 pub fn icon(mut self, icon: text_input::Icon<Renderer::Font>) -> Self {
246 self.text_input = self.text_input.icon(icon);
247 self
248 }
249
250 pub fn size(mut self, size: impl Into<Pixels>) -> Self {
252 let size = size.into();
253
254 self.text_input = self.text_input.size(size);
255 self.size = Some(size.0);
256
257 self
258 }
259
260 pub fn line_height(self, line_height: impl Into<LineHeight>) -> Self {
262 Self {
263 text_input: self.text_input.line_height(line_height),
264 ..self
265 }
266 }
267
268 pub fn width(self, width: impl Into<Length>) -> Self {
270 Self {
271 text_input: self.text_input.width(width),
272 ..self
273 }
274 }
275
276 #[must_use]
278 pub fn input_style(
279 mut self,
280 style: impl Fn(&Theme, text_input::Status) -> text_input::Style + 'a,
281 ) -> Self
282 where
283 <Theme as text_input::Catalog>::Class<'a>:
284 From<text_input::StyleFn<'a, Theme>>,
285 {
286 self.text_input = self.text_input.style(style);
287 self
288 }
289
290 #[must_use]
292 pub fn menu_style(
293 mut self,
294 style: impl Fn(&Theme) -> menu::Style + 'a,
295 ) -> Self
296 where
297 <Theme as menu::Catalog>::Class<'a>: From<menu::StyleFn<'a, Theme>>,
298 {
299 self.menu_class = (Box::new(style) as menu::StyleFn<'a, Theme>).into();
300 self
301 }
302
303 #[cfg(feature = "advanced")]
305 #[must_use]
306 pub fn input_class(
307 mut self,
308 class: impl Into<<Theme as text_input::Catalog>::Class<'a>>,
309 ) -> Self {
310 self.text_input = self.text_input.class(class);
311 self
312 }
313
314 #[cfg(feature = "advanced")]
316 #[must_use]
317 pub fn menu_class(
318 mut self,
319 class: impl Into<<Theme as menu::Catalog>::Class<'a>>,
320 ) -> Self {
321 self.menu_class = class.into();
322 self
323 }
324}
325
326#[derive(Debug, Clone)]
328pub struct State<T> {
329 options: Vec<T>,
330 inner: RefCell<Inner<T>>,
331}
332
333#[derive(Debug, Clone)]
334struct Inner<T> {
335 value: String,
336 option_matchers: Vec<String>,
337 filtered_options: Filtered<T>,
338}
339
340#[derive(Debug, Clone)]
341struct Filtered<T> {
342 options: Vec<T>,
343 updated: Instant,
344}
345
346impl<T> State<T>
347where
348 T: Display + Clone,
349{
350 pub fn new(options: Vec<T>) -> Self {
352 Self::with_selection(options, None)
353 }
354
355 pub fn with_selection(options: Vec<T>, selection: Option<&T>) -> Self {
358 let value = selection.map(T::to_string).unwrap_or_default();
359
360 let option_matchers = build_matchers(&options);
362
363 let filtered_options = Filtered::new(
364 search(&options, &option_matchers, &value)
365 .cloned()
366 .collect(),
367 );
368
369 Self {
370 options,
371 inner: RefCell::new(Inner {
372 value,
373 option_matchers,
374 filtered_options,
375 }),
376 }
377 }
378
379 pub fn options(&self) -> &[T] {
384 &self.options
385 }
386
387 fn value(&self) -> String {
388 let inner = self.inner.borrow();
389
390 inner.value.clone()
391 }
392
393 fn with_inner<O>(&self, f: impl FnOnce(&Inner<T>) -> O) -> O {
394 let inner = self.inner.borrow();
395
396 f(&inner)
397 }
398
399 fn with_inner_mut(&self, f: impl FnOnce(&mut Inner<T>)) {
400 let mut inner = self.inner.borrow_mut();
401
402 f(&mut inner);
403 }
404
405 fn sync_filtered_options(&self, options: &mut Filtered<T>) {
406 let inner = self.inner.borrow();
407
408 inner.filtered_options.sync(options);
409 }
410}
411
412impl<T> Default for State<T>
413where
414 T: Display + Clone,
415{
416 fn default() -> Self {
417 Self::new(Vec::new())
418 }
419}
420
421impl<T> Filtered<T>
422where
423 T: Clone,
424{
425 fn new(options: Vec<T>) -> Self {
426 Self {
427 options,
428 updated: Instant::now(),
429 }
430 }
431
432 fn empty() -> Self {
433 Self {
434 options: vec![],
435 updated: Instant::now(),
436 }
437 }
438
439 fn update(&mut self, options: Vec<T>) {
440 self.options = options;
441 self.updated = Instant::now();
442 }
443
444 fn sync(&self, other: &mut Filtered<T>) {
445 if other.updated != self.updated {
446 *other = self.clone();
447 }
448 }
449}
450
451struct Menu<T> {
452 menu: menu::State,
453 hovered_option: Option<usize>,
454 new_selection: Option<T>,
455 filtered_options: Filtered<T>,
456}
457
458#[derive(Debug, Clone)]
459enum TextInputEvent {
460 TextChanged(String),
461}
462
463impl<T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
464 for ComboBox<'_, T, Message, Theme, Renderer>
465where
466 T: Display + Clone + 'static,
467 Message: Clone,
468 Theme: Catalog,
469 Renderer: text::Renderer,
470{
471 fn size(&self) -> Size<Length> {
472 Widget::<TextInputEvent, Theme, Renderer>::size(&self.text_input)
473 }
474
475 fn layout(
476 &mut self,
477 tree: &mut widget::Tree,
478 renderer: &Renderer,
479 limits: &layout::Limits,
480 ) -> layout::Node {
481 let is_focused = {
482 let text_input_state = tree.children[0]
483 .state
484 .downcast_ref::<text_input::State<Renderer::Paragraph>>();
485
486 text_input_state.is_focused()
487 };
488
489 self.text_input.layout(
490 &mut tree.children[0],
491 renderer,
492 limits,
493 (!is_focused).then_some(&self.selection),
494 )
495 }
496
497 fn tag(&self) -> widget::tree::Tag {
498 widget::tree::Tag::of::<Menu<T>>()
499 }
500
501 fn state(&self) -> widget::tree::State {
502 widget::tree::State::new(Menu::<T> {
503 menu: menu::State::new(),
504 filtered_options: Filtered::empty(),
505 hovered_option: Some(0),
506 new_selection: None,
507 })
508 }
509
510 fn children(&self) -> Vec<widget::Tree> {
511 vec![widget::Tree::new(&self.text_input as &dyn Widget<_, _, _>)]
512 }
513
514 fn diff(&self, _tree: &mut widget::Tree) {
515 }
517
518 fn update(
519 &mut self,
520 tree: &mut widget::Tree,
521 event: &Event,
522 layout: Layout<'_>,
523 cursor: mouse::Cursor,
524 renderer: &Renderer,
525 clipboard: &mut dyn Clipboard,
526 shell: &mut Shell<'_, Message>,
527 viewport: &Rectangle,
528 ) {
529 let menu = tree.state.downcast_mut::<Menu<T>>();
530
531 let started_focused = {
532 let text_input_state = tree.children[0]
533 .state
534 .downcast_ref::<text_input::State<Renderer::Paragraph>>();
535
536 text_input_state.is_focused()
537 };
538 let mut published_message_to_shell = false;
541
542 let mut local_messages = Vec::new();
544 let mut local_shell = Shell::new(&mut local_messages);
545
546 self.text_input.update(
548 &mut tree.children[0],
549 event,
550 layout,
551 cursor,
552 renderer,
553 clipboard,
554 &mut local_shell,
555 viewport,
556 );
557
558 if local_shell.is_event_captured() {
559 shell.capture_event();
560 }
561
562 shell.request_redraw_at(local_shell.redraw_request());
563 shell.request_input_method(local_shell.input_method());
564
565 for message in local_messages {
567 let TextInputEvent::TextChanged(new_value) = message;
568
569 if let Some(on_input) = &self.on_input {
570 shell.publish((on_input)(new_value.clone()));
571 }
572
573 self.state.with_inner_mut(|state| {
577 menu.hovered_option = Some(0);
578 state.value = new_value;
579
580 state.filtered_options.update(
581 search(
582 &self.state.options,
583 &state.option_matchers,
584 &state.value,
585 )
586 .cloned()
587 .collect(),
588 );
589 });
590 shell.invalidate_layout();
591 shell.request_redraw();
592 }
593
594 let is_focused = {
595 let text_input_state = tree.children[0]
596 .state
597 .downcast_ref::<text_input::State<Renderer::Paragraph>>();
598
599 text_input_state.is_focused()
600 };
601
602 if is_focused {
603 self.state.with_inner(|state| {
604 if !started_focused
605 && let Some(on_option_hovered) = &mut self.on_option_hovered
606 {
607 let hovered_option = menu.hovered_option.unwrap_or(0);
608
609 if let Some(option) =
610 state.filtered_options.options.get(hovered_option)
611 {
612 shell.publish(on_option_hovered(option.clone()));
613 published_message_to_shell = true;
614 }
615 }
616
617 if let Event::Keyboard(keyboard::Event::KeyPressed {
618 key: keyboard::Key::Named(named_key),
619 modifiers,
620 ..
621 }) = event
622 {
623 let shift_modifier = modifiers.shift();
624 match (named_key, shift_modifier) {
625 (key::Named::Enter, _) => {
626 if let Some(index) = &menu.hovered_option
627 && let Some(option) =
628 state.filtered_options.options.get(*index)
629 {
630 menu.new_selection = Some(option.clone());
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 && let Some(option) =
654 menu.hovered_option.and_then(|index| {
655 state
656 .filtered_options
657 .options
658 .get(index)
659 })
660 {
661 shell.publish((on_option_hovered)(
663 option.clone(),
664 ));
665 published_message_to_shell = true;
666 }
667
668 shell.capture_event();
669 shell.request_redraw();
670 }
671 (key::Named::ArrowDown, _)
672 | (key::Named::Tab, false)
673 if !modifiers.shift() =>
674 {
675 if let Some(index) = &mut menu.hovered_option {
676 if *index
677 >= state
678 .filtered_options
679 .options
680 .len()
681 .saturating_sub(1)
682 {
683 *index = 0;
684 } else {
685 *index = index.saturating_add(1).min(
686 state
687 .filtered_options
688 .options
689 .len()
690 .saturating_sub(1),
691 );
692 }
693 } else {
694 menu.hovered_option = Some(0);
695 }
696
697 if let Some(on_option_hovered) =
698 &mut self.on_option_hovered
699 && let Some(option) =
700 menu.hovered_option.and_then(|index| {
701 state
702 .filtered_options
703 .options
704 .get(index)
705 })
706 {
707 shell.publish((on_option_hovered)(
709 option.clone(),
710 ));
711 published_message_to_shell = true;
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 viewport: &Rectangle,
836 translation: Vector,
837 ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
838 let is_focused = {
839 let text_input_state = tree.children[0]
840 .state
841 .downcast_ref::<text_input::State<Renderer::Paragraph>>();
842
843 text_input_state.is_focused()
844 };
845
846 if is_focused {
847 let Menu {
848 menu,
849 filtered_options,
850 hovered_option,
851 ..
852 } = tree.state.downcast_mut::<Menu<T>>();
853
854 self.state.sync_filtered_options(filtered_options);
855
856 if filtered_options.options.is_empty() {
857 None
858 } else {
859 let bounds = layout.bounds();
860
861 let mut menu = menu::Menu::new(
862 menu,
863 &filtered_options.options,
864 hovered_option,
865 |x| {
866 tree.children[0]
867 .state
868 .downcast_mut::<text_input::State<Renderer::Paragraph>>(
869 )
870 .unfocus();
871
872 (self.on_selected)(x)
873 },
874 self.on_option_hovered.as_deref(),
875 &self.menu_class,
876 )
877 .width(bounds.width)
878 .padding(self.padding);
879
880 if let Some(font) = self.font {
881 menu = menu.font(font);
882 }
883
884 if let Some(size) = self.size {
885 menu = menu.text_size(size);
886 }
887
888 Some(menu.overlay(
889 layout.position() + translation,
890 *viewport,
891 bounds.height,
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}