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