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