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::{
70 Element, Event, Font, Length, Padding, Pixels, Rectangle, Shell, Size, Theme, Vector,
71};
72use crate::overlay::menu;
73use crate::text::LineHeight;
74use crate::text_input;
75
76use std::fmt::Display;
77use std::sync::atomic::{self, AtomicU64};
78
79pub struct ComboBox<'a, T, Message, Theme = crate::Theme>
136where
137 Theme: Catalog,
138{
139 state: &'a State<T>,
140 id: Option<widget::Id>,
141 placeholder: text::Fragment<'a>,
142 selection: String,
143 width: Length,
144 line_height: Option<LineHeight>,
145 font: Option<Font>,
146 on_selected: Box<dyn Fn(T) -> Message + 'a>,
147 on_option_hovered: Option<Box<dyn Fn(T) -> Message + 'a>>,
148 on_open: Option<Message>,
149 on_close: Option<Message>,
150 on_input: Option<Box<dyn Fn(String) -> Message + 'a>>,
151 padding: Padding,
152 size: Option<Pixels>,
153 shaping: text::Shaping,
154 ellipsis: text::Ellipsis,
155 input_class: <Theme as text_input::Catalog>::Class<'a>,
156 menu_class: <Theme as menu::Catalog>::Class<'a>,
157 menu_height: Length,
158 last_status: Option<text_input::Status>,
159}
160
161impl<'a, T, Message, Theme> ComboBox<'a, T, Message, Theme>
162where
163 T: std::fmt::Display + Clone,
164 Theme: Catalog,
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: None,
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: 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 = Some(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 option_matchers: Vec<String>,
381 filtered_options: Vec<T>,
382 version: u64,
383}
384
385impl<T: Display + Clone, R: text::Renderer> Internal<T, R> {
386 fn hovered_option(&self) -> usize {
387 let index = self.hovered_option.unwrap_or_default();
388
389 index.min(self.filtered_options.len().saturating_sub(1))
390 }
391
392 fn filter(&mut self, options: &[T], value: &str) {
393 self.option_matchers = build_matchers(options);
394 self.filtered_options = search(options, &self.option_matchers, value)
395 .cloned()
396 .collect();
397 }
398}
399
400struct Editor<R: text::Renderer> {
401 input: text::Input<R>,
402 selection: Option<String>,
403}
404
405impl<T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
406 for ComboBox<'_, T, Message, Theme>
407where
408 T: Display + Clone + 'static,
409 Message: Clone,
410 Theme: Catalog,
411 Renderer: text::Renderer + 'static,
412{
413 fn size(&self) -> Size<Length> {
414 Size {
415 width: self.width,
416 height: Length::Fit,
417 }
418 }
419
420 fn layout(
421 &mut self,
422 tree: &mut widget::Tree,
423 renderer: &Renderer,
424 limits: &layout::Limits,
425 ) -> layout::Node {
426 let state = tree.state.downcast_mut::<Internal<T, Renderer>>();
427
428 state.editor.input.layout(
429 renderer,
430 limits,
431 input::Layout {
432 width: self.width,
433 height: Length::Fit,
434 padding: self.padding,
435 placeholder: &self.placeholder,
436 font: self.font,
437 size: self.size,
438 line_height: self.line_height,
439 alignment: text::Alignment::Default,
440 multiline: None,
441 is_secure: false,
442 },
443 )
444 }
445
446 fn tag(&self) -> widget::tree::Tag {
447 widget::tree::Tag::of::<Internal<T, Renderer>>()
448 }
449
450 fn state(&self) -> widget::tree::State {
451 widget::tree::State::new(Internal::<T, Renderer> {
452 editor: Editor {
453 input: text::Input::new(),
454 selection: None,
455 },
456 menu: menu::State::new(),
457 filtered_options: Vec::new(),
458 option_matchers: Vec::new(),
459 hovered_option: Some(0),
460 version: 0,
461 })
462 }
463
464 fn diff(&mut self, tree: &mut widget::Tree) {
465 let state = tree.state.downcast_mut::<Internal<T, Renderer>>();
466
467 if state.version != self.state.version
468 || state.editor.selection.as_deref() != Some(&self.selection)
469 {
470 state.editor.input.overwrite(&self.selection);
471 state.editor.selection = Some(self.selection.clone());
472 state.filter(&self.state.options, &self.selection);
473
474 state.version = self.state.version;
475 }
476 }
477
478 fn update(
479 &mut self,
480 tree: &mut widget::Tree,
481 event: &Event,
482 layout: Layout<'_>,
483 cursor: mouse::Cursor,
484 _renderer: &Renderer,
485 shell: &mut Shell<'_, Message>,
486 _viewport: &Rectangle,
487 ) {
488 let internal = tree.state.downcast_mut::<Internal<T, Renderer>>();
489
490 let was_focused = internal.editor.input.is_focused();
491
492 let edit = internal.editor.input.update::<Message>(
493 event,
494 layout.bounds(),
495 cursor,
496 shell,
497 editor::Binding::from_key_press,
498 );
499
500 if edit.is_some() {
501 let value = internal.editor.input.value();
502
503 if let Some(on_input) = &self.on_input {
504 shell.publish(on_input(value.clone()));
505 }
506
507 internal.filter(&self.state.options, &value);
508 }
509
510 let is_focused = internal.editor.input.is_focused();
511
512 if is_focused {
513 if !was_focused {
514 internal.editor.input.overwrite("");
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(option) = internal
535 .filtered_options
536 .get(internal.hovered_option())
537 .cloned()
538 {
539 internal.menu = menu::State::default();
540 internal.editor.selection = None;
541 internal.editor.input.overwrite("");
542 internal.editor.input.unfocus();
543
544 shell.publish((self.on_selected)(option));
545 }
546
547 shell.capture_event();
548 shell.request_redraw();
549 }
550 (key::Named::ArrowUp, _) | (key::Named::Tab, true) => {
551 let index = internal.hovered_option();
552
553 internal.hovered_option = Some(if index == 0 {
554 internal.filtered_options.len().saturating_sub(1)
555 } else {
556 index.saturating_sub(1)
557 });
558
559 if let Some(on_option_hovered) = &mut self.on_option_hovered
560 && let Some(option) = internal
561 .hovered_option
562 .and_then(|index| internal.filtered_options.get(index))
563 {
564 shell.publish((on_option_hovered)(option.clone()));
565 }
566
567 shell.capture_event();
568 shell.request_redraw();
569 }
570 (key::Named::ArrowDown, _) | (key::Named::Tab, false) => {
571 let index = internal.hovered_option();
572
573 internal.hovered_option = Some(
574 if index >= internal.filtered_options.len().saturating_sub(1) {
575 0
576 } else {
577 index
578 .saturating_add(1)
579 .min(internal.filtered_options.len().saturating_sub(1))
580 },
581 );
582
583 if let Some(on_option_hovered) = &mut self.on_option_hovered
584 && let Some(option) = internal
585 .hovered_option
586 .and_then(|index| internal.filtered_options.get(index))
587 {
588 shell.publish((on_option_hovered)(option.clone()));
589 }
590
591 shell.capture_event();
592 shell.request_redraw();
593 }
594 _ => {}
595 }
596 }
597 }
598
599 if was_focused != is_focused {
600 if is_focused {
601 if let Some(on_open) = self.on_open.take() {
602 shell.publish(on_open);
603 }
604 } else if let Some(on_close) = self.on_close.take() {
605 internal.editor.input.overwrite(&self.selection);
606 shell.publish(on_close);
607 }
608 }
609
610 let status = if internal.editor.input.is_focused() {
611 text_input::Status::Focused {
612 is_hovered: cursor.is_over(layout.bounds()),
613 }
614 } else if cursor.is_over(layout.bounds()) {
615 text_input::Status::Hovered
616 } else {
617 text_input::Status::Active
618 };
619
620 if let Event::Window(window::Event::RedrawRequested(_now)) = event {
621 self.last_status = Some(status);
622
623 shell.request_input_method(
624 &internal
625 .editor
626 .input
627 .input_method(layout.bounds().shrink(self.padding).position()),
628 );
629 } else if self
630 .last_status
631 .is_some_and(|last_status| status != last_status)
632 {
633 shell.request_redraw();
634 }
635 }
636
637 fn mouse_interaction(
638 &self,
639 _tree: &widget::Tree,
640 layout: Layout<'_>,
641 cursor: mouse::Cursor,
642 _viewport: &Rectangle,
643 _renderer: &Renderer,
644 ) -> mouse::Interaction {
645 if cursor.is_over(layout.bounds()) {
646 mouse::Interaction::Text
647 } else {
648 mouse::Interaction::default()
649 }
650 }
651
652 fn draw(
653 &self,
654 tree: &widget::Tree,
655 renderer: &mut Renderer,
656 theme: &Theme,
657 _style: &renderer::Style,
658 layout: Layout<'_>,
659 _cursor: mouse::Cursor,
660 viewport: &Rectangle,
661 ) {
662 let internal = tree.state.downcast_ref::<Internal<T, Renderer>>();
663
664 let bounds = layout.bounds();
665 let style = text_input::Catalog::style(
666 theme,
667 &self.input_class,
668 self.last_status.unwrap_or(text_input::Status::Disabled),
669 );
670
671 renderer.fill_quad(
672 renderer::Quad {
673 bounds,
674 border: style.border,
675 ..renderer::Quad::default()
676 },
677 style.background,
678 );
679
680 internal.editor.input.draw(
681 renderer,
682 bounds,
683 *viewport,
684 input::Style {
685 value: style.value,
686 selection: style.selection,
687 placeholder: style.placeholder,
688 },
689 );
690 }
691
692 fn overlay<'b>(
693 &'b mut self,
694 tree: &'b mut widget::Tree,
695 layout: Layout<'_>,
696 _renderer: &Renderer,
697 viewport: &Rectangle,
698 translation: Vector,
699 ) -> Vec<overlay::Element<'b, Message, Theme, Renderer>> {
700 let internal = tree.state.downcast_mut::<Internal<T, Renderer>>();
701 let is_focused = internal.editor.input.is_focused();
702
703 if is_focused {
704 let Internal {
705 menu,
706 filtered_options,
707 hovered_option,
708 editor,
709 ..
710 } = tree.state.downcast_mut::<Internal<T, Renderer>>();
711
712 if filtered_options.is_empty() {
713 Vec::new()
714 } else {
715 let bounds = layout.bounds();
716
717 let mut menu = menu::Menu::new(
718 menu,
719 filtered_options,
720 hovered_option,
721 &T::to_string,
722 |selection| {
723 editor.selection = None;
724 editor.input.overwrite("");
725 editor.input.unfocus();
726
727 (self.on_selected)(selection)
728 },
729 self.on_option_hovered.as_deref(),
730 &self.menu_class,
731 )
732 .width(bounds.width)
733 .padding(self.padding)
734 .shaping(self.shaping)
735 .ellipsis(self.ellipsis);
736
737 if let Some(font) = self.font {
738 menu = menu.font(font);
739 }
740
741 if let Some(size) = self.size {
742 menu = menu.text_size(size);
743 }
744
745 vec![menu.overlay(
746 layout.position() + translation,
747 *viewport,
748 bounds.height,
749 self.menu_height,
750 )]
751 }
752 } else {
753 Vec::new()
754 }
755 }
756
757 fn operate(
758 &mut self,
759 tree: &mut widget::Tree,
760 layout: Layout<'_>,
761 _renderer: &Renderer,
762 operation: &mut dyn widget::Operation,
763 ) {
764 let state = tree.state.downcast_mut::<Internal<T, Renderer>>();
765 let bounds = layout.bounds();
766
767 operation.focusable(self.id.as_ref(), bounds, &mut state.editor.input);
768 operation.text_input(self.id.as_ref(), bounds, &mut state.editor.input);
769 }
770}
771
772impl<'a, T, Message, Theme, Renderer> From<ComboBox<'a, T, Message, Theme>>
773 for Element<'a, Message, Theme, Renderer>
774where
775 T: Display + Clone + 'static,
776 Message: Clone + 'a,
777 Theme: Catalog + 'a,
778 Renderer: text::Renderer + 'static,
779{
780 fn from(combo_box: ComboBox<'a, T, Message, Theme>) -> Self {
781 Self::new(combo_box)
782 }
783}
784
785pub trait Catalog: text_input::Catalog + menu::Catalog {
787 fn default_input<'a>() -> <Self as text_input::Catalog>::Class<'a> {
789 <Self as text_input::Catalog>::default()
790 }
791
792 fn default_menu<'a>() -> <Self as menu::Catalog>::Class<'a> {
794 <Self as menu::Catalog>::default()
795 }
796}
797
798impl Catalog for Theme {}
799
800fn search<'a, T, A>(
801 options: impl IntoIterator<Item = T> + 'a,
802 option_matchers: impl IntoIterator<Item = &'a A> + 'a,
803 query: &'a str,
804) -> impl Iterator<Item = T> + 'a
805where
806 A: AsRef<str> + 'a,
807{
808 let query: Vec<String> = query
809 .to_lowercase()
810 .split(|c: char| !c.is_ascii_alphanumeric())
811 .map(String::from)
812 .collect();
813
814 options
815 .into_iter()
816 .zip(option_matchers)
817 .filter_map(move |(option, matcher)| {
819 if query.iter().all(|part| matcher.as_ref().contains(part)) {
820 Some(option)
821 } else {
822 None
823 }
824 })
825}
826
827fn build_matchers<'a, T>(options: impl IntoIterator<Item = T> + 'a) -> Vec<String>
828where
829 T: Display + 'a,
830{
831 options.into_iter().map(build_matcher).collect()
832}
833
834fn build_matcher<T>(option: T) -> String
835where
836 T: Display,
837{
838 let mut matcher = option.to_string();
839 matcher.retain(|c| c.is_ascii_alphanumeric());
840 matcher.to_lowercase()
841}