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, Theme, Vector,
68};
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>,
142 on_option_hovered: Option<Box<dyn Fn(T) -> Message>>,
143 on_open: Option<Message>,
144 on_close: Option<Message>,
145 on_input: Option<Box<dyn Fn(String) -> Message>>,
146 padding: Padding,
147 size: Option<f32>,
148 text_shaping: text::Shaping,
149 menu_class: <Theme as menu::Catalog>::Class<'a>,
150 menu_height: Length,
151}
152
153impl<'a, T, Message, Theme, Renderer> ComboBox<'a, T, Message, Theme, Renderer>
154where
155 T: std::fmt::Display + Clone,
156 Theme: Catalog,
157 Renderer: text::Renderer,
158{
159 pub fn new(
163 state: &'a State<T>,
164 placeholder: &str,
165 selection: Option<&T>,
166 on_selected: impl Fn(T) -> Message + 'static,
167 ) -> Self {
168 let text_input = TextInput::new(placeholder, &state.value())
169 .on_input(TextInputEvent::TextChanged)
170 .class(Theme::default_input());
171
172 let selection = selection.map(T::to_string).unwrap_or_default();
173
174 Self {
175 state,
176 text_input,
177 font: None,
178 selection: text_input::Value::new(&selection),
179 on_selected: Box::new(on_selected),
180 on_option_hovered: None,
181 on_input: None,
182 on_open: None,
183 on_close: None,
184 padding: text_input::DEFAULT_PADDING,
185 size: None,
186 text_shaping: text::Shaping::default(),
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 + 'static) -> 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 + 'static) -> 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 text_shaping(mut self, shaping: text::Shaping) -> Self {
276 self.text_shaping = shaping;
277 self
278 }
279
280 #[must_use]
282 pub fn input_style(
283 mut self,
284 style: impl Fn(&Theme, text_input::Status) -> text_input::Style + 'a,
285 ) -> Self
286 where
287 <Theme as text_input::Catalog>::Class<'a>: From<text_input::StyleFn<'a, Theme>>,
288 {
289 self.text_input = self.text_input.style(style);
290 self
291 }
292
293 #[must_use]
295 pub fn menu_style(mut self, style: impl Fn(&Theme) -> menu::Style + 'a) -> 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(mut self, class: impl Into<<Theme as menu::Catalog>::Class<'a>>) -> Self {
318 self.menu_class = class.into();
319 self
320 }
321}
322
323#[derive(Debug, Clone)]
325pub struct State<T> {
326 options: Vec<T>,
327 inner: RefCell<Inner<T>>,
328}
329
330#[derive(Debug, Clone)]
331struct Inner<T> {
332 value: String,
333 option_matchers: Vec<String>,
334 filtered_options: Filtered<T>,
335}
336
337#[derive(Debug, Clone)]
338struct Filtered<T> {
339 options: Vec<T>,
340 updated: Instant,
341}
342
343impl<T> State<T>
344where
345 T: Display + Clone,
346{
347 pub fn new(options: Vec<T>) -> Self {
349 Self::with_selection(options, None)
350 }
351
352 pub fn with_selection(options: Vec<T>, selection: Option<&T>) -> Self {
355 let value = selection.map(T::to_string).unwrap_or_default();
356
357 let option_matchers = build_matchers(&options);
359
360 let filtered_options = Filtered::new(
361 search(&options, &option_matchers, &value)
362 .cloned()
363 .collect(),
364 );
365
366 Self {
367 options,
368 inner: RefCell::new(Inner {
369 value,
370 option_matchers,
371 filtered_options,
372 }),
373 }
374 }
375
376 pub fn options(&self) -> &[T] {
381 &self.options
382 }
383
384 pub fn push(&mut self, new_option: T) {
386 let mut inner = self.inner.borrow_mut();
387
388 inner.option_matchers.push(build_matcher(&new_option));
389 self.options.push(new_option);
390
391 inner.filtered_options = Filtered::new(
392 search(&self.options, &inner.option_matchers, &inner.value)
393 .cloned()
394 .collect(),
395 );
396 }
397
398 pub fn into_options(self) -> Vec<T> {
400 self.options
401 }
402
403 fn value(&self) -> String {
404 let inner = self.inner.borrow();
405
406 inner.value.clone()
407 }
408
409 fn with_inner<O>(&self, f: impl FnOnce(&Inner<T>) -> O) -> O {
410 let inner = self.inner.borrow();
411
412 f(&inner)
413 }
414
415 fn with_inner_mut(&self, f: impl FnOnce(&mut Inner<T>)) {
416 let mut inner = self.inner.borrow_mut();
417
418 f(&mut inner);
419 }
420
421 fn sync_filtered_options(&self, options: &mut Filtered<T>) {
422 let inner = self.inner.borrow();
423
424 inner.filtered_options.sync(options);
425 }
426}
427
428impl<T> Default for State<T>
429where
430 T: Display + Clone,
431{
432 fn default() -> Self {
433 Self::new(Vec::new())
434 }
435}
436
437impl<T> Filtered<T>
438where
439 T: Clone,
440{
441 fn new(options: Vec<T>) -> Self {
442 Self {
443 options,
444 updated: Instant::now(),
445 }
446 }
447
448 fn empty() -> Self {
449 Self {
450 options: vec![],
451 updated: Instant::now(),
452 }
453 }
454
455 fn update(&mut self, options: Vec<T>) {
456 self.options = options;
457 self.updated = Instant::now();
458 }
459
460 fn sync(&self, other: &mut Filtered<T>) {
461 if other.updated != self.updated {
462 *other = self.clone();
463 }
464 }
465}
466
467struct Menu<T> {
468 menu: menu::State,
469 hovered_option: Option<usize>,
470 new_selection: Option<T>,
471 filtered_options: Filtered<T>,
472}
473
474#[derive(Debug, Clone)]
475enum TextInputEvent {
476 TextChanged(String),
477}
478
479impl<T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
480 for ComboBox<'_, T, Message, Theme, Renderer>
481where
482 T: Display + Clone + 'static,
483 Message: Clone,
484 Theme: Catalog,
485 Renderer: text::Renderer,
486{
487 fn size(&self) -> Size<Length> {
488 Widget::<TextInputEvent, Theme, Renderer>::size(&self.text_input)
489 }
490
491 fn layout(
492 &mut self,
493 tree: &mut widget::Tree,
494 renderer: &Renderer,
495 limits: &layout::Limits,
496 ) -> layout::Node {
497 let is_focused = {
498 let text_input_state = tree.children[0]
499 .state
500 .downcast_ref::<text_input::State<Renderer::Paragraph>>();
501
502 text_input_state.is_focused()
503 };
504
505 self.text_input.layout(
506 &mut tree.children[0],
507 renderer,
508 limits,
509 (!is_focused).then_some(&self.selection),
510 )
511 }
512
513 fn tag(&self) -> widget::tree::Tag {
514 widget::tree::Tag::of::<Menu<T>>()
515 }
516
517 fn state(&self) -> widget::tree::State {
518 widget::tree::State::new(Menu::<T> {
519 menu: menu::State::new(),
520 filtered_options: Filtered::empty(),
521 hovered_option: Some(0),
522 new_selection: None,
523 })
524 }
525
526 fn children(&self) -> Vec<widget::Tree> {
527 vec![widget::Tree::new(&self.text_input as &dyn Widget<_, _, _>)]
528 }
529
530 fn diff(&self, _tree: &mut widget::Tree) {
531 }
533
534 fn update(
535 &mut self,
536 tree: &mut widget::Tree,
537 event: &Event,
538 layout: Layout<'_>,
539 cursor: mouse::Cursor,
540 renderer: &Renderer,
541 clipboard: &mut dyn Clipboard,
542 shell: &mut Shell<'_, Message>,
543 viewport: &Rectangle,
544 ) {
545 let menu = tree.state.downcast_mut::<Menu<T>>();
546
547 let started_focused = {
548 let text_input_state = tree.children[0]
549 .state
550 .downcast_ref::<text_input::State<Renderer::Paragraph>>();
551
552 text_input_state.is_focused()
553 };
554 let mut published_message_to_shell = false;
557
558 let mut local_messages = Vec::new();
560 let mut local_shell = Shell::new(&mut local_messages);
561
562 self.text_input.update(
564 &mut tree.children[0],
565 event,
566 layout,
567 cursor,
568 renderer,
569 clipboard,
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
581 for message in local_messages {
583 let TextInputEvent::TextChanged(new_value) = message;
584
585 if let Some(on_input) = &self.on_input {
586 shell.publish((on_input)(new_value.clone()));
587 }
588
589 self.state.with_inner_mut(|state| {
593 menu.hovered_option = Some(0);
594 state.value = new_value;
595
596 state.filtered_options.update(
597 search(&self.state.options, &state.option_matchers, &state.value)
598 .cloned()
599 .collect(),
600 );
601 });
602 shell.invalidate_layout();
603 shell.request_redraw();
604 }
605
606 let is_focused = {
607 let text_input_state = tree.children[0]
608 .state
609 .downcast_ref::<text_input::State<Renderer::Paragraph>>();
610
611 text_input_state.is_focused()
612 };
613
614 if is_focused {
615 self.state.with_inner(|state| {
616 if !started_focused && let Some(on_option_hovered) = &mut self.on_option_hovered {
617 let hovered_option = menu.hovered_option.unwrap_or(0);
618
619 if let Some(option) = state.filtered_options.options.get(hovered_option) {
620 shell.publish(on_option_hovered(option.clone()));
621 published_message_to_shell = true;
622 }
623 }
624
625 if let Event::Keyboard(keyboard::Event::KeyPressed {
626 key: keyboard::Key::Named(named_key),
627 modifiers,
628 ..
629 }) = event
630 {
631 let shift_modifier = modifiers.shift();
632 match (named_key, shift_modifier) {
633 (key::Named::Enter, _) => {
634 if let Some(index) = &menu.hovered_option
635 && let Some(option) = state.filtered_options.options.get(*index)
636 {
637 menu.new_selection = Some(option.clone());
638 }
639
640 shell.capture_event();
641 shell.request_redraw();
642 }
643 (key::Named::ArrowUp, _) | (key::Named::Tab, true) => {
644 if let Some(index) = &mut menu.hovered_option {
645 if *index == 0 {
646 *index = state.filtered_options.options.len().saturating_sub(1);
647 } else {
648 *index = index.saturating_sub(1);
649 }
650 } else {
651 menu.hovered_option = Some(0);
652 }
653
654 if let Some(on_option_hovered) = &mut self.on_option_hovered
655 && let Some(option) = menu
656 .hovered_option
657 .and_then(|index| state.filtered_options.options.get(index))
658 {
659 shell.publish((on_option_hovered)(option.clone()));
661 published_message_to_shell = true;
662 }
663
664 shell.capture_event();
665 shell.request_redraw();
666 }
667 (key::Named::ArrowDown, _) | (key::Named::Tab, false)
668 if !modifiers.shift() =>
669 {
670 if let Some(index) = &mut menu.hovered_option {
671 if *index >= state.filtered_options.options.len().saturating_sub(1)
672 {
673 *index = 0;
674 } else {
675 *index = index.saturating_add(1).min(
676 state.filtered_options.options.len().saturating_sub(1),
677 );
678 }
679 } else {
680 menu.hovered_option = Some(0);
681 }
682
683 if let Some(on_option_hovered) = &mut self.on_option_hovered
684 && let Some(option) = menu
685 .hovered_option
686 .and_then(|index| state.filtered_options.options.get(index))
687 {
688 shell.publish((on_option_hovered)(option.clone()));
690 published_message_to_shell = true;
691 }
692
693 shell.capture_event();
694 shell.request_redraw();
695 }
696 _ => {}
697 }
698 }
699 });
700 }
701
702 self.state.with_inner_mut(|state| {
704 if let Some(selection) = menu.new_selection.take() {
705 state.value = String::new();
707 state.filtered_options.update(self.state.options.clone());
708 menu.menu = menu::State::default();
709
710 shell.publish((self.on_selected)(selection));
712 published_message_to_shell = true;
713
714 let mut local_messages = Vec::new();
716 let mut local_shell = Shell::new(&mut local_messages);
717 self.text_input.update(
718 &mut tree.children[0],
719 &Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
720 layout,
721 mouse::Cursor::Unavailable,
722 renderer,
723 clipboard,
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 |selection| {
838 self.state.with_inner_mut(|state| {
839 state.value = String::new();
840 state.filtered_options.update(self.state.options.clone());
841 });
842
843 tree.children[0]
844 .state
845 .downcast_mut::<text_input::State<Renderer::Paragraph>>()
846 .unfocus();
847
848 (self.on_selected)(selection)
849 },
850 self.on_option_hovered.as_deref(),
851 &self.menu_class,
852 )
853 .width(bounds.width)
854 .padding(self.padding)
855 .text_shaping(self.text_shaping);
856
857 if let Some(font) = self.font {
858 menu = menu.font(font);
859 }
860
861 if let Some(size) = self.size {
862 menu = menu.text_size(size);
863 }
864
865 Some(menu.overlay(
866 layout.position() + translation,
867 *viewport,
868 bounds.height,
869 self.menu_height,
870 ))
871 }
872 } else {
873 None
874 }
875 }
876}
877
878impl<'a, T, Message, Theme, Renderer> From<ComboBox<'a, T, Message, Theme, Renderer>>
879 for Element<'a, Message, Theme, Renderer>
880where
881 T: Display + Clone + 'static,
882 Message: Clone + 'a,
883 Theme: Catalog + 'a,
884 Renderer: text::Renderer + 'a,
885{
886 fn from(combo_box: ComboBox<'a, T, Message, Theme, Renderer>) -> Self {
887 Self::new(combo_box)
888 }
889}
890
891pub trait Catalog: text_input::Catalog + menu::Catalog {
893 fn default_input<'a>() -> <Self as text_input::Catalog>::Class<'a> {
895 <Self as text_input::Catalog>::default()
896 }
897
898 fn default_menu<'a>() -> <Self as menu::Catalog>::Class<'a> {
900 <Self as menu::Catalog>::default()
901 }
902}
903
904impl Catalog for Theme {}
905
906fn search<'a, T, A>(
907 options: impl IntoIterator<Item = T> + 'a,
908 option_matchers: impl IntoIterator<Item = &'a A> + 'a,
909 query: &'a str,
910) -> impl Iterator<Item = T> + 'a
911where
912 A: AsRef<str> + 'a,
913{
914 let query: Vec<String> = query
915 .to_lowercase()
916 .split(|c: char| !c.is_ascii_alphanumeric())
917 .map(String::from)
918 .collect();
919
920 options
921 .into_iter()
922 .zip(option_matchers)
923 .filter_map(move |(option, matcher)| {
925 if query.iter().all(|part| matcher.as_ref().contains(part)) {
926 Some(option)
927 } else {
928 None
929 }
930 })
931}
932
933fn build_matchers<'a, T>(options: impl IntoIterator<Item = T> + 'a) -> Vec<String>
934where
935 T: Display + 'a,
936{
937 options.into_iter().map(build_matcher).collect()
938}
939
940fn build_matcher<T>(option: T) -> String
941where
942 T: Display,
943{
944 let mut matcher = option.to_string();
945 matcher.retain(|c| c.is_ascii_alphanumeric());
946 matcher.to_lowercase()
947}