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