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