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::text::editor;
65use crate::core::text::input;
66use crate::core::widget::operation::Focusable as _;
67use crate::core::widget::{self, Widget};
68use crate::core::window;
69use crate::core::{Element, Event, Length, Padding, Pixels, Rectangle, Shell, Size, Theme, Vector};
70use crate::overlay::menu;
71use crate::text::LineHeight;
72use crate::text_input;
73
74use std::fmt::Display;
75use std::sync::atomic::{self, AtomicU64};
76
77pub struct ComboBox<'a, T, Message, Theme = crate::Theme, Renderer = crate::Renderer>
134where
135 Theme: Catalog,
136 Renderer: text::Renderer,
137{
138 state: &'a State<T>,
139 id: Option<widget::Id>,
140 placeholder: text::Fragment<'a>,
141 selection: String,
142 width: Length,
143 line_height: LineHeight,
144 font: Option<Renderer::Font>,
145 on_selected: Box<dyn Fn(T) -> Message + 'a>,
146 on_option_hovered: Option<Box<dyn Fn(T) -> Message + 'a>>,
147 on_open: Option<Message>,
148 on_close: Option<Message>,
149 on_input: Option<Box<dyn Fn(String) -> Message + 'a>>,
150 padding: Padding,
151 size: Option<Pixels>,
152 shaping: text::Shaping,
153 ellipsis: text::Ellipsis,
154 input_class: <Theme as text_input::Catalog>::Class<'a>,
155 menu_class: <Theme as menu::Catalog>::Class<'a>,
156 menu_height: Length,
157 last_status: Option<text_input::Status>,
158}
159
160impl<'a, T, Message, Theme, Renderer> ComboBox<'a, T, Message, Theme, Renderer>
161where
162 T: std::fmt::Display + Clone,
163 Theme: Catalog,
164 Renderer: text::Renderer,
165{
166 pub fn new(
170 state: &'a State<T>,
171 placeholder: impl text::IntoFragment<'a>,
172 selection: Option<&T>,
173 on_selected: impl Fn(T) -> Message + 'a,
174 ) -> Self {
175 Self {
176 state,
177 id: None,
178 placeholder: placeholder.into_fragment(),
179 selection: selection.map(T::to_string).unwrap_or_default(),
180 width: Length::Fill,
181 line_height: LineHeight::default(),
182 font: None,
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 padding: text_input::DEFAULT_PADDING,
189 size: None,
190 shaping: text::Shaping::default(),
191 ellipsis: text::Ellipsis::End,
192 input_class: <Theme as Catalog>::default_input(),
193 menu_class: <Theme as Catalog>::default_menu(),
194 menu_height: Length::Shrink,
195 last_status: None,
196 }
197 }
198
199 pub fn id(mut self, id: impl Into<widget::Id>) -> Self {
201 self.id = Some(id.into());
202 self
203 }
204
205 pub fn on_input(mut self, on_input: impl Fn(String) -> Message + 'a) -> Self {
208 self.on_input = Some(Box::new(on_input));
209 self
210 }
211
212 pub fn on_option_hovered(mut self, on_option_hovered: impl Fn(T) -> Message + 'a) -> Self {
215 self.on_option_hovered = Some(Box::new(on_option_hovered));
216 self
217 }
218
219 pub fn on_open(mut self, message: Message) -> Self {
222 self.on_open = Some(message);
223 self
224 }
225
226 pub fn on_close(mut self, message: Message) -> Self {
229 self.on_close = Some(message);
230 self
231 }
232
233 pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
235 self.padding = padding.into();
236 self
237 }
238
239 pub fn font(mut self, font: Renderer::Font) -> Self {
243 self.font = Some(font);
244 self
245 }
246
247 pub fn size(mut self, size: impl Into<Pixels>) -> Self {
249 self.size = Some(size.into());
250 self
251 }
252
253 pub fn width(mut self, width: impl Into<Length>) -> Self {
255 self.width = width.into();
256 self
257 }
258
259 pub fn line_height(mut self, line_height: impl Into<LineHeight>) -> Self {
261 self.line_height = line_height.into();
262 self
263 }
264
265 pub fn menu_height(mut self, menu_height: impl Into<Length>) -> Self {
267 self.menu_height = menu_height.into();
268 self
269 }
270
271 pub fn shaping(mut self, shaping: text::Shaping) -> Self {
273 self.shaping = shaping;
274 self
275 }
276
277 pub fn ellipsis(mut self, ellipsis: text::Ellipsis) -> Self {
279 self.ellipsis = ellipsis;
280 self
281 }
282
283 #[must_use]
285 pub fn input_style(
286 mut self,
287 style: impl Fn(&Theme, text_input::Status) -> text_input::Style + 'a,
288 ) -> Self
289 where
290 <Theme as text_input::Catalog>::Class<'a>: From<text_input::StyleFn<'a, Theme>>,
291 {
292 self.input_class = (Box::new(style) as text_input::StyleFn<'a, Theme>).into();
293 self
294 }
295
296 #[must_use]
298 pub fn menu_style(mut self, style: impl Fn(&Theme) -> menu::Style + 'a) -> Self
299 where
300 <Theme as menu::Catalog>::Class<'a>: From<menu::StyleFn<'a, Theme>>,
301 {
302 self.menu_class = (Box::new(style) as menu::StyleFn<'a, Theme>).into();
303 self
304 }
305
306 #[cfg(feature = "advanced")]
308 #[must_use]
309 pub fn input_class(
310 mut self,
311 class: impl Into<<Theme as text_input::Catalog>::Class<'a>>,
312 ) -> Self {
313 self.input_class = class.into();
314 self
315 }
316
317 #[cfg(feature = "advanced")]
319 #[must_use]
320 pub fn menu_class(mut self, class: impl Into<<Theme as menu::Catalog>::Class<'a>>) -> 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 version: u64,
331}
332
333static VERSION: AtomicU64 = AtomicU64::new(0);
334
335impl<T> State<T>
336where
337 T: Display + Clone,
338{
339 pub fn new(options: Vec<T>) -> Self {
341 Self {
342 options,
343 version: VERSION.fetch_add(1, atomic::Ordering::Relaxed),
344 }
345 }
346
347 pub fn options(&self) -> &[T] {
352 &self.options
353 }
354
355 pub fn push(&mut self, new_option: T) {
357 self.options.push(new_option);
358 self.version = VERSION.fetch_add(1, atomic::Ordering::Relaxed);
359 }
360
361 pub fn into_options(self) -> Vec<T> {
363 self.options
364 }
365}
366
367impl<T> Default for State<T>
368where
369 T: Display + Clone,
370{
371 fn default() -> Self {
372 Self::new(Vec::new())
373 }
374}
375
376struct Internal<T, R: text::Renderer> {
377 editor: Editor<R>,
378 menu: menu::State,
379 hovered_option: Option<usize>,
380 new_selection: Option<T>,
381 option_matchers: Vec<String>,
382 filtered_options: Vec<T>,
383 version: u64,
384}
385
386impl<T: Display + Clone, R: text::Renderer> Internal<T, R> {
387 fn filter(&mut self, options: &[T]) {
388 self.option_matchers = build_matchers(options);
389 self.filtered_options = search(options, &self.option_matchers, &self.editor.value)
390 .cloned()
391 .collect();
392 }
393}
394
395struct Editor<R: text::Renderer> {
396 input: text::Input<R>,
397 value: String,
398 selection: Option<String>,
399}
400
401impl<T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
402 for ComboBox<'_, T, Message, Theme, Renderer>
403where
404 T: Display + Clone + 'static,
405 Message: Clone,
406 Theme: Catalog,
407 Renderer: text::Renderer + 'static,
408{
409 fn size(&self) -> Size<Length> {
410 Size {
411 width: self.width,
412 height: Length::Fit,
413 }
414 }
415
416 fn layout(
417 &mut self,
418 tree: &mut widget::Tree,
419 renderer: &Renderer,
420 limits: &layout::Limits,
421 ) -> layout::Node {
422 let state = tree.state.downcast_mut::<Internal<T, Renderer>>();
423
424 state.editor.input.layout(
425 renderer,
426 limits,
427 input::Layout {
428 width: self.width,
429 height: Length::Fit,
430 padding: self.padding,
431 placeholder: &self.placeholder,
432 font: self.font,
433 size: self.size,
434 line_height: self.line_height,
435 alignment: text::Alignment::Default,
436 multiline: None,
437 },
438 )
439 }
440
441 fn tag(&self) -> widget::tree::Tag {
442 widget::tree::Tag::of::<Internal<T, Renderer>>()
443 }
444
445 fn state(&self) -> widget::tree::State {
446 widget::tree::State::new(Internal::<T, Renderer> {
447 editor: Editor {
448 input: text::Input::new(),
449 value: String::new(),
450 selection: None,
451 },
452 menu: menu::State::new(),
453 filtered_options: Vec::new(),
454 option_matchers: Vec::new(),
455 hovered_option: Some(0),
456 new_selection: None,
457 version: 0,
458 })
459 }
460
461 fn diff(&mut self, tree: &mut widget::Tree) {
462 let state = tree.state.downcast_mut::<Internal<T, Renderer>>();
463
464 if state.version != self.state.version
465 || state.editor.selection.as_deref() != Some(&self.selection)
466 {
467 state.editor.input.overwrite(&self.selection);
468 state.editor.selection = Some(self.selection.clone());
469 state.editor.value = self.selection.clone();
470 state.filter(&self.state.options);
471
472 state.version = self.state.version;
473 }
474 }
475
476 fn update(
477 &mut self,
478 tree: &mut widget::Tree,
479 event: &Event,
480 layout: Layout<'_>,
481 cursor: mouse::Cursor,
482 _renderer: &Renderer,
483 shell: &mut Shell<'_, Message>,
484 _viewport: &Rectangle,
485 ) {
486 let internal = tree.state.downcast_mut::<Internal<T, Renderer>>();
487
488 let was_focused = internal.editor.input.is_focused();
489
490 let edit = internal.editor.input.update::<Message>(
491 event,
492 layout.bounds(),
493 cursor,
494 shell,
495 editor::Binding::from_key_press,
496 );
497
498 if edit.is_some() {
499 let value = internal.editor.input.value();
500
501 if let Some(on_input) = &self.on_input {
502 shell.publish(on_input(value.clone()));
503 }
504
505 internal.editor.value = value;
506 internal.filter(&self.state.options);
507 }
508
509 let is_focused = internal.editor.input.is_focused();
510
511 if is_focused {
512 if !was_focused {
513 internal.editor.input.overwrite("");
514 internal.editor.value.clear();
515 internal.filtered_options = self.state.options.clone();
516
517 if let Some(on_option_hovered) = &mut self.on_option_hovered {
518 let hovered_option = internal.hovered_option.unwrap_or(0);
519
520 if let Some(option) = internal.filtered_options.get(hovered_option) {
521 shell.publish(on_option_hovered(option.clone()));
522 }
523 }
524 }
525
526 if let Event::Keyboard(keyboard::Event::KeyPressed {
527 key: keyboard::Key::Named(named_key),
528 modifiers,
529 ..
530 }) = event
531 {
532 match (named_key, modifiers.shift()) {
533 (key::Named::Enter, _) => {
534 if let Some(index) = &internal.hovered_option
535 && let Some(option) = internal.filtered_options.get(*index)
536 {
537 internal.new_selection = Some(option.clone());
538 }
539
540 shell.capture_event();
541 shell.request_redraw();
542 }
543 (key::Named::ArrowUp, _) | (key::Named::Tab, true) => {
544 if let Some(index) = &mut internal.hovered_option {
545 if *index == 0 {
546 *index = internal.filtered_options.len().saturating_sub(1);
547 } else {
548 *index = index.saturating_sub(1);
549 }
550 } else {
551 internal.hovered_option = Some(0);
552 }
553
554 if let Some(on_option_hovered) = &mut self.on_option_hovered
555 && let Some(option) = internal
556 .hovered_option
557 .and_then(|index| internal.filtered_options.get(index))
558 {
559 shell.publish((on_option_hovered)(option.clone()));
561 }
562
563 shell.capture_event();
564 shell.request_redraw();
565 }
566 (key::Named::ArrowDown, _) | (key::Named::Tab, false) => {
567 if let Some(index) = &mut internal.hovered_option {
568 if *index >= internal.filtered_options.len().saturating_sub(1) {
569 *index = 0;
570 } else {
571 *index = index
572 .saturating_add(1)
573 .min(internal.filtered_options.len().saturating_sub(1));
574 }
575 } else {
576 internal.hovered_option = Some(0);
577 }
578
579 if let Some(on_option_hovered) = &mut self.on_option_hovered
580 && let Some(option) = internal
581 .hovered_option
582 .and_then(|index| internal.filtered_options.get(index))
583 {
584 shell.publish((on_option_hovered)(option.clone()));
586 }
587
588 shell.capture_event();
589 shell.request_redraw();
590 }
591 _ => {}
592 }
593 }
594 }
595
596 if let Some(selection) = internal.new_selection.take() {
598 internal.menu = menu::State::default();
600
601 internal.editor.input.overwrite(&selection.to_string());
602 internal.editor.input.unfocus();
603 internal.editor.value = String::new();
604
605 internal.filter(&self.state.options);
606
607 shell.publish((self.on_selected)(selection));
609 }
610
611 if was_focused != is_focused {
612 if is_focused {
613 if let Some(on_open) = self.on_open.take() {
614 shell.publish(on_open);
615 }
616 } else if let Some(on_close) = self.on_close.take() {
617 internal.editor.input.overwrite(&self.selection);
618 shell.publish(on_close);
619 }
620 }
621
622 let status = if internal.editor.input.is_focused() {
623 text_input::Status::Focused {
624 is_hovered: cursor.is_over(layout.bounds()),
625 }
626 } else if cursor.is_over(layout.bounds()) {
627 text_input::Status::Hovered
628 } else {
629 text_input::Status::Active
630 };
631
632 if let Event::Window(window::Event::RedrawRequested(_now)) = event {
633 self.last_status = Some(status);
634
635 shell.request_input_method(
636 &internal
637 .editor
638 .input
639 .input_method(layout.bounds().shrink(self.padding).position()),
640 );
641 } else if self
642 .last_status
643 .is_some_and(|last_status| status != last_status)
644 {
645 shell.request_redraw();
646 }
647 }
648
649 fn mouse_interaction(
650 &self,
651 _tree: &widget::Tree,
652 layout: Layout<'_>,
653 cursor: mouse::Cursor,
654 _viewport: &Rectangle,
655 _renderer: &Renderer,
656 ) -> mouse::Interaction {
657 if cursor.is_over(layout.bounds()) {
658 mouse::Interaction::Text
659 } else {
660 mouse::Interaction::default()
661 }
662 }
663
664 fn draw(
665 &self,
666 tree: &widget::Tree,
667 renderer: &mut Renderer,
668 theme: &Theme,
669 _style: &renderer::Style,
670 layout: Layout<'_>,
671 _cursor: mouse::Cursor,
672 viewport: &Rectangle,
673 ) {
674 let internal = tree.state.downcast_ref::<Internal<T, Renderer>>();
675
676 let bounds = layout.bounds();
677 let style = text_input::Catalog::style(
678 theme,
679 &self.input_class,
680 self.last_status.unwrap_or(text_input::Status::Disabled),
681 );
682
683 renderer.fill_quad(
684 renderer::Quad {
685 bounds,
686 border: style.border,
687 ..renderer::Quad::default()
688 },
689 style.background,
690 );
691
692 internal.editor.input.draw(
693 renderer,
694 bounds,
695 *viewport,
696 input::Style {
697 value: style.value,
698 selection: style.selection,
699 placeholder: style.placeholder,
700 },
701 );
702 }
703
704 fn overlay<'b>(
705 &'b mut self,
706 tree: &'b mut widget::Tree,
707 layout: Layout<'_>,
708 _renderer: &Renderer,
709 viewport: &Rectangle,
710 translation: Vector,
711 ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
712 let internal = tree.state.downcast_mut::<Internal<T, Renderer>>();
713 let is_focused = internal.editor.input.is_focused();
714
715 if is_focused {
716 let Internal {
717 menu,
718 filtered_options,
719 hovered_option,
720 new_selection,
721 ..
722 } = tree.state.downcast_mut::<Internal<T, Renderer>>();
723
724 if filtered_options.is_empty() {
725 None
726 } else {
727 let bounds = layout.bounds();
728
729 let mut menu = menu::Menu::new(
730 menu,
731 filtered_options,
732 hovered_option,
733 &T::to_string,
734 |selection| {
735 *new_selection = Some(selection.clone());
736
737 (self.on_selected)(selection)
738 },
739 self.on_option_hovered.as_deref(),
740 &self.menu_class,
741 )
742 .width(bounds.width)
743 .padding(self.padding)
744 .shaping(self.shaping)
745 .ellipsis(self.ellipsis);
746
747 if let Some(font) = self.font {
748 menu = menu.font(font);
749 }
750
751 if let Some(size) = self.size {
752 menu = menu.text_size(size);
753 }
754
755 Some(menu.overlay(
756 layout.position() + translation,
757 *viewport,
758 bounds.height,
759 self.menu_height,
760 ))
761 }
762 } else {
763 None
764 }
765 }
766
767 fn operate(
768 &mut self,
769 tree: &mut widget::Tree,
770 layout: Layout<'_>,
771 _renderer: &Renderer,
772 operation: &mut dyn widget::Operation,
773 ) {
774 let state = tree.state.downcast_mut::<Internal<T, Renderer>>();
775 let bounds = layout.bounds();
776
777 operation.focusable(self.id.as_ref(), bounds, &mut state.editor.input);
778 operation.text_input(self.id.as_ref(), bounds, &mut state.editor.input);
779 }
780}
781
782impl<'a, T, Message, Theme, Renderer> From<ComboBox<'a, T, Message, Theme, Renderer>>
783 for Element<'a, Message, Theme, Renderer>
784where
785 T: Display + Clone + 'static,
786 Message: Clone + 'a,
787 Theme: Catalog + 'a,
788 Renderer: text::Renderer + 'static,
789{
790 fn from(combo_box: ComboBox<'a, T, Message, Theme, Renderer>) -> Self {
791 Self::new(combo_box)
792 }
793}
794
795pub trait Catalog: text_input::Catalog + menu::Catalog {
797 fn default_input<'a>() -> <Self as text_input::Catalog>::Class<'a> {
799 <Self as text_input::Catalog>::default()
800 }
801
802 fn default_menu<'a>() -> <Self as menu::Catalog>::Class<'a> {
804 <Self as menu::Catalog>::default()
805 }
806}
807
808impl Catalog for Theme {}
809
810fn search<'a, T, A>(
811 options: impl IntoIterator<Item = T> + 'a,
812 option_matchers: impl IntoIterator<Item = &'a A> + 'a,
813 query: &'a str,
814) -> impl Iterator<Item = T> + 'a
815where
816 A: AsRef<str> + 'a,
817{
818 let query: Vec<String> = query
819 .to_lowercase()
820 .split(|c: char| !c.is_ascii_alphanumeric())
821 .map(String::from)
822 .collect();
823
824 options
825 .into_iter()
826 .zip(option_matchers)
827 .filter_map(move |(option, matcher)| {
829 if query.iter().all(|part| matcher.as_ref().contains(part)) {
830 Some(option)
831 } else {
832 None
833 }
834 })
835}
836
837fn build_matchers<'a, T>(options: impl IntoIterator<Item = T> + 'a) -> Vec<String>
838where
839 T: Display + 'a,
840{
841 options.into_iter().map(build_matcher).collect()
842}
843
844fn build_matcher<T>(option: T) -> String
845where
846 T: Display,
847{
848 let mut matcher = option.to_string();
849 matcher.retain(|c| c.is_ascii_alphanumeric());
850 matcher.to_lowercase()
851}