1use std::fmt::{self, Display};
45use std::rc::Rc;
46
47use iced::advanced::layout::{self, Layout};
48use iced::advanced::widget::{Operation, Tree, Widget, operation, tree};
49use iced::advanced::{Clipboard, Shell, overlay, renderer};
50use iced::keyboard::key::Named;
51use iced::mouse::{self, ScrollDelta};
52use iced::widget::text::LineHeight;
53use iced::widget::text_input::{self, Status};
54use iced::widget::{self, Column, container, mouse_area, stack};
55use iced::{Alignment, Element, Length, Padding, Renderer, Theme};
56
57use crate::icon::{opacity, themed};
58use crate::keys::{self, Chord, Keymap};
59use crate::overlay::anchored::{Align, Placement, Side, anchored};
60use crate::overlay::menu::{self, Parts, RowStatus, Trailing};
61use crate::primitives::input::{self, Size};
62use crate::theme::{Tokens, space};
63
64pub const VISIBLE_ROWS: usize = 6;
66pub const DEFAULT_EMPTY: &str = "No results found.";
68
69const LINE_HEIGHT: f32 = 1.3;
71
72#[derive(Debug, Clone, PartialEq, Eq)]
74pub enum Event {
75 Input(String),
77 Open,
78 Close,
80 Next,
82 Previous,
84 ActivateHighlighted,
86 Highlight(usize),
88 Activate(usize),
90 Scroll(i32),
92}
93
94#[derive(Debug, Clone, PartialEq)]
96pub struct State<T> {
97 options: Vec<T>,
98 labels: Vec<String>,
99 query: String,
100 matches: Vec<usize>,
102 highlighted: usize,
104 offset: usize,
106 selected: Option<usize>,
107 open: bool,
108 rows: usize,
109}
110
111impl<T: Display> State<T> {
112 pub fn new(options: impl IntoIterator<Item = T>) -> Self {
115 let options: Vec<T> = options.into_iter().collect();
116 let labels = options
117 .iter()
118 .map(|option| option.to_string().to_lowercase())
119 .collect();
120 let matches = (0..options.len()).collect();
121 Self {
122 options,
123 labels,
124 query: String::new(),
125 matches,
126 highlighted: 0,
127 offset: 0,
128 selected: None,
129 open: false,
130 rows: VISIBLE_ROWS,
131 }
132 }
133}
134
135impl<T: PartialEq> State<T> {
136 pub fn with_selected(mut self, value: &T) -> Self {
138 self.selected = self.options.iter().position(|option| option == value);
139 self
140 }
141}
142
143impl<T> State<T> {
144 pub fn with_visible_rows(mut self, rows: usize) -> Self {
146 self.rows = rows.max(1);
147 self.reveal();
148 self
149 }
150
151 pub fn options(&self) -> &[T] {
152 &self.options
153 }
154
155 pub fn selected(&self) -> Option<&T> {
156 self.selected.and_then(|index| self.options.get(index))
157 }
158
159 pub fn query(&self) -> &str {
160 &self.query
161 }
162
163 pub fn is_open(&self) -> bool {
164 self.open
165 }
166
167 pub fn matches(&self) -> impl Iterator<Item = &T> {
169 self.matches
170 .iter()
171 .filter_map(|&index| self.options.get(index))
172 }
173
174 pub fn highlighted(&self) -> Option<&T> {
176 self.matches
177 .get(self.highlighted)
178 .and_then(|&index| self.options.get(index))
179 }
180
181 pub fn visible(&self) -> impl Iterator<Item = (usize, &T)> {
183 self.matches
184 .iter()
185 .skip(self.offset)
186 .take(self.rows)
187 .filter_map(|&index| self.options.get(index).map(|option| (index, option)))
188 }
189
190 fn is_highlighted(&self, index: usize) -> bool {
191 self.matches.get(self.highlighted) == Some(&index)
192 }
193
194 pub fn update(&mut self, event: Event) -> Option<T>
196 where
197 T: Clone,
198 {
199 match event {
200 Event::Input(query) => {
201 self.query = query;
202 self.open = true;
203 self.filter();
204 self.highlight(0);
205 }
206 Event::Open => self.open(),
207 Event::Close => self.close(),
208 Event::Next | Event::Previous if !self.open => self.open(),
209 Event::Next => self.step(1),
210 Event::Previous => self.step(-1),
211 Event::ActivateHighlighted => {
212 if !self.open {
213 return None;
214 }
215 let index = *self.matches.get(self.highlighted)?;
216 return self.choose(index);
217 }
218 Event::Activate(index) => return self.choose(index),
219 Event::Highlight(index) => {
220 if let Some(position) = self.matches.iter().position(|&i| i == index) {
221 self.highlight(position);
222 }
223 }
224 Event::Scroll(rows) => {
225 let last = self.matches.len().saturating_sub(self.rows);
226 let offset = (self.offset as i64).saturating_add(rows.into());
227 self.offset = offset.clamp(0, last as i64) as usize;
228 }
229 }
230 None
231 }
232
233 fn filter(&mut self) {
234 let query = self.query.trim().to_lowercase();
235 self.matches = self
236 .labels
237 .iter()
238 .enumerate()
239 .filter(|(_, label)| label.contains(&query))
240 .map(|(index, _)| index)
241 .collect();
242 }
243
244 fn open(&mut self) {
245 self.open = true;
246 self.query.clear();
247 self.filter();
248 let selected = self
249 .selected
250 .and_then(|index| self.matches.iter().position(|&i| i == index));
251 self.offset = 0;
252 self.highlight(selected.unwrap_or(0));
253 }
254
255 fn close(&mut self) {
256 self.open = false;
257 self.query.clear();
258 self.filter();
259 self.offset = 0;
260 }
261
262 fn choose(&mut self, index: usize) -> Option<T>
263 where
264 T: Clone,
265 {
266 let value = self.options.get(index)?.clone();
267 self.selected = Some(index);
268 self.close();
269 Some(value)
270 }
271
272 fn step(&mut self, delta: isize) {
273 let len = self.matches.len() as isize;
274 if len == 0 {
275 return;
276 }
277 let next = (self.highlighted as isize + delta).rem_euclid(len);
278 self.highlight(next as usize);
279 }
280
281 fn highlight(&mut self, position: usize) {
282 self.highlighted = position.min(self.matches.len().saturating_sub(1));
283 self.reveal();
284 }
285
286 fn reveal(&mut self) {
288 if self.highlighted < self.offset {
289 self.offset = self.highlighted;
290 } else if self.highlighted >= self.offset + self.rows {
291 self.offset = self.highlighted + 1 - self.rows;
292 }
293 self.offset = self
294 .offset
295 .min(self.matches.len().saturating_sub(self.rows));
296 }
297}
298
299#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
301pub enum Action {
302 Next,
303 Previous,
304 Activate,
305 Close,
306}
307
308impl Action {
309 pub fn event<T>(self, state: &State<T>) -> Option<Event> {
312 match self {
313 Action::Next => Some(Event::Next),
314 Action::Previous => Some(Event::Previous),
315 Action::Activate => state.open.then_some(Event::ActivateHighlighted),
316 Action::Close => state.open.then_some(Event::Close),
317 }
318 }
319}
320
321impl keys::Action for Action {
322 const ALL: &'static [Self] = &[
323 Action::Next,
324 Action::Previous,
325 Action::Activate,
326 Action::Close,
327 ];
328
329 fn defaults() -> Keymap<Self> {
330 default_keymap()
331 }
332
333 fn name(self) -> &'static str {
334 match self {
335 Action::Next => "Next",
336 Action::Previous => "Previous",
337 Action::Activate => "Activate",
338 Action::Close => "Close",
339 }
340 }
341
342 fn description(self) -> &'static str {
343 match self {
344 Action::Next => "Highlights the next suggestion, opening the list if it is closed.",
345 Action::Previous => {
346 "Highlights the previous suggestion, opening the list if it is closed."
347 }
348 Action::Activate => "Chooses the highlighted suggestion and closes the list.",
349 Action::Close => "Closes the list and clears the query.",
350 }
351 }
352}
353
354pub fn default_keymap() -> Keymap<Action> {
367 Keymap::new()
368 .bind(Chord::named(Named::ArrowDown), Action::Next)
369 .bind(Chord::named(Named::ArrowUp), Action::Previous)
370 .bind(Chord::named(Named::Enter), Action::Activate)
371 .bind(Chord::named(Named::Escape), Action::Close)
372}
373
374pub struct Combobox<'a, T, Message> {
378 state: &'a State<T>,
379 placeholder: String,
380 empty: String,
381 width: Length,
382 size: Size,
383 id: Option<widget::Id>,
384 invalid: bool,
385 keymap: Keymap<Action>,
386 on_event: Option<Box<dyn Fn(Event) -> Message + 'a>>,
387}
388
389pub fn combobox<'a, T, Message>(state: &'a State<T>) -> Combobox<'a, T, Message> {
391 Combobox {
392 state,
393 placeholder: String::new(),
394 empty: DEFAULT_EMPTY.to_owned(),
395 width: Length::Fixed(240.0),
396 size: Size::default(),
397 id: None,
398 invalid: false,
399 keymap: default_keymap(),
400 on_event: None,
401 }
402}
403
404impl<T: fmt::Debug, Message> fmt::Debug for Combobox<'_, T, Message> {
405 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
406 f.debug_struct("Combobox")
407 .field("state", self.state)
408 .field("placeholder", &self.placeholder)
409 .field("empty", &self.empty)
410 .field("width", &self.width)
411 .field("size", &self.size)
412 .field("invalid", &self.invalid)
413 .field("enabled", &self.on_event.is_some())
414 .finish_non_exhaustive()
415 }
416}
417
418impl<'a, T, Message> Combobox<'a, T, Message> {
419 pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
421 self.placeholder = placeholder.into();
422 self
423 }
424
425 pub fn empty(mut self, empty: impl Into<String>) -> Self {
427 self.empty = empty.into();
428 self
429 }
430
431 pub fn width(mut self, width: impl Into<Length>) -> Self {
433 self.width = width.into();
434 self
435 }
436
437 pub fn size(mut self, size: Size) -> Self {
440 self.size = size;
441 self
442 }
443
444 pub fn id(mut self, id: impl Into<widget::Id>) -> Self {
446 self.id = Some(id.into());
447 self
448 }
449
450 pub fn invalid(mut self, invalid: bool) -> Self {
454 self.invalid = invalid;
455 self
456 }
457
458 pub fn keymap(mut self, keymap: Keymap<Action>) -> Self {
460 self.keymap = keymap;
461 self
462 }
463
464 pub fn on_event(mut self, on_event: impl Fn(Event) -> Message + 'a) -> Self {
467 self.on_event = Some(Box::new(on_event));
468 self
469 }
470
471 pub fn is_enabled(&self) -> bool {
472 self.on_event.is_some()
473 }
474}
475
476impl<'a, T, Message> From<Combobox<'a, T, Message>> for Element<'a, Message>
477where
478 T: Display + 'a,
479 Message: Clone + 'a,
480{
481 fn from(combobox: Combobox<'a, T, Message>) -> Self {
482 let Combobox {
483 state,
484 placeholder,
485 empty,
486 width,
487 size,
488 id,
489 invalid,
490 keymap,
491 on_event,
492 } = combobox;
493 let selected = state.selected().map(ToString::to_string);
494
495 let Some(on_event) = on_event else {
496 let value = selected.unwrap_or_default();
497 return container(field(&placeholder, &value, size, id, None, false, invalid))
498 .width(width)
499 .into();
500 };
501 let on_event: Rc<dyn Fn(Event) -> Message + 'a> = Rc::from(on_event);
502
503 let open = state.open;
504 let (value, hint) = if open {
505 (state.query.clone(), selected.unwrap_or(placeholder))
506 } else {
507 (selected.unwrap_or_default(), placeholder)
508 };
509
510 let list = open.then(|| suggestions(state, &empty, &*on_event));
511 let on_anchor_press = (!open).then(|| on_event(Event::Open));
512 let on_dismiss = open.then(|| on_event(Event::Close));
513 let on_input: OnInput<'a, Message> = {
514 let on_event = Rc::clone(&on_event);
515 Box::new(move |query| on_event(Event::Input(query)))
516 };
517 let field = container(field(
518 &hint,
519 &value,
520 size,
521 id,
522 Some(on_input),
523 open,
524 invalid,
525 ))
526 .width(width);
527 let field = CaretToEnd {
528 content: field.into(),
529 watch: (open, state.selected),
530 };
531
532 anchored(Element::new(field))
533 .content(list)
534 .placement(Placement::new(Side::Bottom, Align::Start))
535 .match_width(true)
536 .dismiss_keys([])
537 .dismiss_on_blur(true)
538 .on_dismiss_maybe(on_dismiss)
539 .on_anchor_press_maybe(on_anchor_press)
540 .on_key(move |key| {
541 keymap
542 .resolve_event(key)
543 .and_then(|action| action.event(state))
544 .map(&*on_event)
545 })
546 .into()
547 }
548}
549
550type OnInput<'a, Message> = Box<dyn Fn(String) -> Message + 'a>;
551
552fn field<'a, Message: Clone + 'a>(
553 placeholder: &str,
554 value: &str,
555 size: Size,
556 id: Option<widget::Id>,
557 on_input: Option<OnInput<'a, Message>>,
558 open: bool,
559 invalid: bool,
560) -> Element<'a, Message> {
561 let enabled = on_input.is_some();
562 let metrics = size.metrics();
563 let mut padding = metrics.padding(false);
564 padding.right += metrics.icon + space::SM;
565
566 let mut input = widget::text_input(placeholder, value)
567 .size(metrics.text)
568 .line_height(LineHeight::Relative(LINE_HEIGHT))
569 .padding(padding)
570 .width(Length::Fill)
571 .style(move |theme, status| style(&Tokens::of(theme), status, open, invalid));
572 if let Some(id) = id {
573 input = input.id(id);
574 }
575 if let Some(on_input) = on_input {
576 input = input.on_input(on_input);
577 }
578
579 let chevron = themed(
580 crate::lucide!(ChevronsUpDown),
581 metrics.icon,
582 opacity(enabled),
583 |theme| Tokens::of(theme).muted_foreground,
584 );
585
586 stack![
587 input,
588 container(chevron)
589 .width(Length::Fill)
590 .height(Length::Fill)
591 .align_x(Alignment::End)
592 .align_y(Alignment::Center)
593 .padding(Padding::ZERO.right(metrics.padding_x)),
594 ]
595 .into()
596}
597
598fn suggestions<'a, T: Display, Message: Clone + 'a>(
599 state: &'a State<T>,
600 empty: &str,
601 on_event: &dyn Fn(Event) -> Message,
602) -> Element<'a, Message> {
603 let rows: Vec<Element<'a, Message>> = state
604 .visible()
605 .map(|(index, option)| {
606 let status = if state.is_highlighted(index) {
607 RowStatus::Highlighted
608 } else {
609 RowStatus::Idle
610 };
611 let label = option.to_string();
612 let trailing = if state.selected == Some(index) {
613 Trailing::Check
614 } else {
615 Trailing::None
616 };
617 let parts = Parts {
618 trailing,
619 ..Parts::label(&label)
620 };
621 let messages = (
622 on_event(Event::Highlight(index)),
623 on_event(Event::Activate(index)),
624 );
625 menu::item(parts, status, false, Some(messages))
626 })
627 .collect();
628
629 let body: Element<'a, Message> = if rows.is_empty() {
630 menu::empty(empty)
631 } else {
632 Column::from_vec(rows).width(Length::Fill).into()
633 };
634
635 let down = on_event(Event::Scroll(1));
636 let up = on_event(Event::Scroll(-1));
637 mouse_area(menu::surface(body).width(Length::Fill))
638 .on_scroll(move |delta| match delta {
639 ScrollDelta::Lines { y, .. } | ScrollDelta::Pixels { y, .. } if y < 0.0 => down.clone(),
640 _ => up.clone(),
641 })
642 .into()
643}
644
645pub fn style(tokens: &Tokens, status: Status, open: bool, invalid: bool) -> text_input::Style {
648 let status = match status {
649 Status::Active if open => Status::Focused { is_hovered: false },
650 Status::Hovered if open => Status::Focused { is_hovered: true },
651 status => status,
652 };
653 input::style(tokens, status, invalid)
654}
655
656struct CaretToEnd<'a, Message> {
660 content: Element<'a, Message>,
661 watch: (bool, Option<usize>),
663}
664
665fn moves_caret(before: Option<(bool, Option<usize>)>, now: (bool, Option<usize>)) -> bool {
668 let Some(before) = before else {
669 return false;
670 };
671 let (open, selected) = now;
672 !open && (before.0 || before.1 != selected)
673}
674
675struct MoveCaretToEnd;
676
677impl Operation for MoveCaretToEnd {
678 fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation)) {
679 operate(self);
680 }
681
682 fn text_input(
683 &mut self,
684 _id: Option<&widget::Id>,
685 _bounds: iced::Rectangle,
686 state: &mut dyn operation::TextInput,
687 ) {
688 state.move_cursor_to_end();
689 }
690}
691
692impl<Message> Widget<Message, Theme, Renderer> for CaretToEnd<'_, Message> {
693 fn size(&self) -> iced::Size<Length> {
694 self.content.as_widget().size()
695 }
696
697 fn size_hint(&self) -> iced::Size<Length> {
698 self.content.as_widget().size_hint()
699 }
700
701 fn tag(&self) -> tree::Tag {
702 tree::Tag::of::<Option<(bool, Option<usize>)>>()
703 }
704
705 fn state(&self) -> tree::State {
706 tree::State::new(None::<(bool, Option<usize>)>)
707 }
708
709 fn children(&self) -> Vec<Tree> {
710 vec![Tree::new(&self.content)]
711 }
712
713 fn diff(&self, tree: &mut Tree) {
714 tree.diff_children(std::slice::from_ref(&self.content));
715 }
716
717 fn layout(
720 &mut self,
721 tree: &mut Tree,
722 renderer: &Renderer,
723 limits: &layout::Limits,
724 ) -> layout::Node {
725 let node = self
726 .content
727 .as_widget_mut()
728 .layout(&mut tree.children[0], renderer, limits);
729 let seen = tree.state.downcast_mut::<Option<(bool, Option<usize>)>>();
730 let before = seen.replace(self.watch);
731 if moves_caret(before, self.watch) {
732 self.content.as_widget_mut().operate(
733 &mut tree.children[0],
734 Layout::new(&node),
735 renderer,
736 &mut MoveCaretToEnd,
737 );
738 }
739 node
740 }
741
742 fn draw(
743 &self,
744 tree: &Tree,
745 renderer: &mut Renderer,
746 theme: &Theme,
747 style: &renderer::Style,
748 layout: Layout<'_>,
749 cursor: mouse::Cursor,
750 viewport: &iced::Rectangle,
751 ) {
752 self.content.as_widget().draw(
753 &tree.children[0],
754 renderer,
755 theme,
756 style,
757 layout,
758 cursor,
759 viewport,
760 );
761 }
762
763 fn operate(
764 &mut self,
765 tree: &mut Tree,
766 layout: Layout<'_>,
767 renderer: &Renderer,
768 operation: &mut dyn Operation,
769 ) {
770 self.content
771 .as_widget_mut()
772 .operate(&mut tree.children[0], layout, renderer, operation);
773 }
774
775 fn update(
776 &mut self,
777 tree: &mut Tree,
778 event: &iced::Event,
779 layout: Layout<'_>,
780 cursor: mouse::Cursor,
781 renderer: &Renderer,
782 clipboard: &mut dyn Clipboard,
783 shell: &mut Shell<'_, Message>,
784 viewport: &iced::Rectangle,
785 ) {
786 self.content.as_widget_mut().update(
787 &mut tree.children[0],
788 event,
789 layout,
790 cursor,
791 renderer,
792 clipboard,
793 shell,
794 viewport,
795 );
796 }
797
798 fn mouse_interaction(
799 &self,
800 tree: &Tree,
801 layout: Layout<'_>,
802 cursor: mouse::Cursor,
803 viewport: &iced::Rectangle,
804 renderer: &Renderer,
805 ) -> mouse::Interaction {
806 self.content.as_widget().mouse_interaction(
807 &tree.children[0],
808 layout,
809 cursor,
810 viewport,
811 renderer,
812 )
813 }
814
815 fn overlay<'b>(
816 &'b mut self,
817 tree: &'b mut Tree,
818 layout: Layout<'b>,
819 renderer: &Renderer,
820 viewport: &iced::Rectangle,
821 translation: iced::Vector,
822 ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
823 self.content.as_widget_mut().overlay(
824 &mut tree.children[0],
825 layout,
826 renderer,
827 viewport,
828 translation,
829 )
830 }
831}
832
833#[cfg(test)]
834mod tests {
835 use super::*;
836 use crate::theme::{dark, light};
837
838 const FRUITS: [&str; 5] = ["Apple", "Banana", "Blueberry", "Cherry", "Grape"];
839
840 fn fruits() -> State<&'static str> {
841 State::new(FRUITS)
842 }
843
844 fn matches(state: &State<&'static str>) -> Vec<&'static str> {
845 state.matches().copied().collect()
846 }
847
848 fn press(keymap: &Keymap<Action>, chord: &str) -> Option<Action> {
849 let chord: Chord = chord.parse().unwrap();
850 keymap.resolve(chord.key(), chord.modifiers())
851 }
852
853 #[test]
854 fn new_state_is_closed_with_every_option() {
855 let state = fruits();
856 assert!(!state.is_open());
857 assert!(state.selected().is_none());
858 assert_eq!(state.query(), "");
859 assert_eq!(matches(&state), FRUITS);
860 assert_eq!(state.visible().count(), 5);
861 }
862
863 #[test]
864 fn with_selected_ignores_unknown_values() {
865 assert_eq!(
866 fruits().with_selected(&"Cherry").selected(),
867 Some(&"Cherry")
868 );
869 assert!(fruits().with_selected(&"Kiwi").selected().is_none());
870 }
871
872 #[test]
873 fn typing_filters_case_insensitively_and_keeps_order() {
874 let mut state = fruits();
875 assert!(state.update(Event::Input("B".into())).is_none());
876 assert!(state.is_open());
877 assert_eq!(matches(&state), ["Banana", "Blueberry"]);
878 assert_eq!(state.highlighted(), Some(&"Banana"));
879
880 let _ = state.update(Event::Input(" ERR ".into()));
881 assert_eq!(matches(&state), ["Blueberry", "Cherry"]);
882 }
883
884 #[test]
885 fn no_match_leaves_nothing_highlighted() {
886 let mut state = fruits();
887 let _ = state.update(Event::Input("kiwi".into()));
888 assert!(state.matches().next().is_none());
889 assert!(state.highlighted().is_none());
890 assert!(state.update(Event::ActivateHighlighted).is_none());
891 let _ = state.update(Event::Next);
892 assert!(state.highlighted().is_none());
893 }
894
895 #[test]
896 fn arrows_open_then_wrap_round() {
897 let mut state = fruits();
898 let _ = state.update(Event::Next);
899 assert!(state.is_open());
900 assert_eq!(state.highlighted(), Some(&"Apple"));
901
902 let _ = state.update(Event::Previous);
903 assert_eq!(state.highlighted(), Some(&"Grape"));
904 let _ = state.update(Event::Next);
905 assert_eq!(state.highlighted(), Some(&"Apple"));
906 }
907
908 #[test]
909 fn opening_highlights_the_selection() {
910 let mut state = fruits().with_selected(&"Cherry");
911 let _ = state.update(Event::Open);
912 assert_eq!(state.highlighted(), Some(&"Cherry"));
913 }
914
915 #[test]
916 fn confirm_chooses_closes_and_clears_the_query() {
917 let mut state = fruits();
918 let _ = state.update(Event::Input("an".into()));
919 assert_eq!(state.update(Event::ActivateHighlighted), Some("Banana"));
920 assert!(!state.is_open());
921 assert_eq!(state.query(), "");
922 assert_eq!(state.selected(), Some(&"Banana"));
923 assert_eq!(matches(&state).len(), FRUITS.len());
924 }
925
926 #[test]
927 fn confirm_does_nothing_while_closed() {
928 let mut state = fruits();
929 assert!(state.update(Event::ActivateHighlighted).is_none());
930 assert!(state.selected().is_none());
931 }
932
933 #[test]
934 fn pick_and_highlight_use_option_indices() {
935 let mut state = fruits();
936 let _ = state.update(Event::Input("berry".into()));
937 let _ = state.update(Event::Highlight(2));
938 assert_eq!(state.highlighted(), Some(&"Blueberry"));
939 let _ = state.update(Event::Highlight(0));
941 assert_eq!(state.highlighted(), Some(&"Blueberry"));
942
943 assert_eq!(state.update(Event::Activate(3)), Some("Cherry"));
944 assert!(state.update(Event::Activate(99)).is_none());
945 assert_eq!(state.selected(), Some(&"Cherry"));
946 }
947
948 #[test]
949 fn close_keeps_the_selection() {
950 let mut state = fruits().with_selected(&"Apple");
951 let _ = state.update(Event::Input("gr".into()));
952 let _ = state.update(Event::Close);
953 assert!(!state.is_open());
954 assert_eq!(state.query(), "");
955 assert_eq!(state.selected(), Some(&"Apple"));
956 }
957
958 #[test]
959 fn the_window_follows_the_highlight() {
960 let mut state = fruits().with_visible_rows(2);
961 let _ = state.update(Event::Open);
962 let visible = |state: &State<&'static str>| -> Vec<&str> {
963 state.visible().map(|(_, fruit)| *fruit).collect()
964 };
965 assert_eq!(visible(&state), ["Apple", "Banana"]);
966
967 let _ = state.update(Event::Next);
968 let _ = state.update(Event::Next);
969 assert_eq!(state.highlighted(), Some(&"Blueberry"));
970 assert_eq!(visible(&state), ["Banana", "Blueberry"]);
971
972 let _ = state.update(Event::Previous);
973 let _ = state.update(Event::Previous);
974 let _ = state.update(Event::Previous);
975 assert_eq!(state.highlighted(), Some(&"Grape"));
976 assert_eq!(visible(&state), ["Cherry", "Grape"]);
977 }
978
979 #[test]
980 fn scrolling_stays_within_the_matches() {
981 let mut state = fruits().with_visible_rows(2);
982 let _ = state.update(Event::Open);
983 let _ = state.update(Event::Scroll(10));
984 assert_eq!(state.visible().map(|(i, _)| i).collect::<Vec<_>>(), [3, 4]);
985 let _ = state.update(Event::Scroll(-10));
986 assert_eq!(state.visible().map(|(i, _)| i).collect::<Vec<_>>(), [0, 1]);
987 assert_eq!(State::<&str>::new([]).with_visible_rows(0).rows, 1);
988 }
989
990 #[test]
991 fn empty_options_are_safe() {
992 let mut state = State::<&str>::new([]);
993 let _ = state.update(Event::Next);
994 let _ = state.update(Event::Previous);
995 let _ = state.update(Event::Scroll(3));
996 assert!(state.update(Event::ActivateHighlighted).is_none());
997 assert!(state.highlighted().is_none());
998 }
999
1000 #[test]
1001 fn default_keymap_and_overrides() {
1002 let keymap = default_keymap();
1003 assert_eq!(press(&keymap, "ArrowDown"), Some(Action::Next));
1004 assert_eq!(press(&keymap, "ArrowUp"), Some(Action::Previous));
1005 assert_eq!(press(&keymap, "Enter"), Some(Action::Activate));
1006 assert_eq!(press(&keymap, "Escape"), Some(Action::Close));
1007
1008 let custom = keymap
1009 .bind("Ctrl+N".parse().unwrap(), Action::Next)
1010 .unbind(&Chord::named(Named::Escape));
1011 assert_eq!(press(&custom, "ctrl+n"), Some(Action::Next));
1012 assert_eq!(press(&custom, "Escape"), None);
1013 }
1014
1015 #[test]
1016 fn actions_map_to_events_only_when_they_apply() {
1017 let mut state = fruits();
1018 assert_eq!(Action::Next.event(&state), Some(Event::Next));
1019 assert_eq!(Action::Previous.event(&state), Some(Event::Previous));
1020 assert_eq!(Action::Activate.event(&state), None);
1021 assert_eq!(Action::Close.event(&state), None);
1022
1023 let _ = state.update(Event::Open);
1024 assert_eq!(
1025 Action::Activate.event(&state),
1026 Some(Event::ActivateHighlighted)
1027 );
1028 assert_eq!(Action::Close.event(&state), Some(Event::Close));
1029 }
1030
1031 #[test]
1032 fn default_builder_is_disabled_with_the_default_empty_text() {
1033 let state = fruits();
1034 let c: Combobox<'_, &str, Event> = combobox(&state);
1035 assert!(!c.is_enabled());
1036 assert_eq!(c.empty, DEFAULT_EMPTY);
1037 assert_eq!(c.width, Length::Fixed(240.0));
1038 assert_eq!(c.size, Size::Md);
1039 assert_eq!(c.keymap, default_keymap());
1040 assert!(!c.invalid);
1041 let c = c
1042 .placeholder("Pick")
1043 .empty("None")
1044 .size(Size::Sm)
1045 .invalid(true)
1046 .on_event(|e| e);
1047 assert_eq!(c.size, Size::Sm);
1048 assert!(c.invalid);
1049 assert!(c.is_enabled());
1050 assert_eq!((c.placeholder.as_str(), c.empty.as_str()), ("Pick", "None"));
1051 }
1052
1053 const STATES: [Status; 5] = [
1054 Status::Active,
1055 Status::Hovered,
1056 Status::Focused { is_hovered: false },
1057 Status::Focused { is_hovered: true },
1058 Status::Disabled,
1059 ];
1060
1061 #[test]
1062 fn open_field_draws_the_focused_border() {
1063 for theme in [light(), dark()] {
1064 let tokens = Tokens::of(&theme);
1065 let focused = input::style(&tokens, Status::Focused { is_hovered: false }, false);
1066 for status in STATES {
1067 let closed = style(&tokens, status, false, false);
1068 assert_eq!(closed, input::style(&tokens, status, false), "{status:?}");
1069 let open = style(&tokens, status, true, false);
1070 if status != Status::Disabled {
1071 assert_eq!(open.border.color, focused.border.color, "{status:?}");
1072 }
1073 }
1074 }
1075 }
1076
1077 #[test]
1078 fn disabled_field_uses_the_shared_disabled_background() {
1079 for theme in [light(), dark()] {
1080 let tokens = Tokens::of(&theme);
1081 for open in [false, true] {
1082 let disabled = style(&tokens, Status::Disabled, open, false);
1083 assert_eq!(
1084 disabled.background,
1085 iced::Background::Color(tokens.disabled_field())
1086 );
1087 }
1088 }
1089 }
1090
1091 #[test]
1092 fn invalid_field_uses_the_input_invalid_style() {
1093 for theme in [light(), dark()] {
1094 let tokens = Tokens::of(&theme);
1095 for status in STATES {
1096 assert_eq!(
1097 style(&tokens, status, false, true),
1098 input::style(&tokens, status, true),
1099 "{status:?}"
1100 );
1101 let valid = style(&tokens, status, false, false);
1102 if status != Status::Disabled {
1103 assert_ne!(
1104 style(&tokens, status, false, true).border.color,
1105 valid.border.color,
1106 "{status:?}"
1107 );
1108 }
1109 }
1110 }
1111 }
1112
1113 #[test]
1114 fn the_caret_moves_to_the_end_when_the_list_closes_or_the_pick_changes() {
1115 assert!(
1116 !moves_caret(None, (false, None)),
1117 "nothing to move on first layout"
1118 );
1119 assert!(moves_caret(Some((true, None)), (false, Some(2))), "a pick");
1120 assert!(
1121 moves_caret(Some((true, Some(2))), (false, Some(2))),
1122 "Escape"
1123 );
1124 assert!(
1125 moves_caret(Some((false, Some(1))), (false, Some(2))),
1126 "set by the app"
1127 );
1128 assert!(!moves_caret(Some((false, Some(2))), (false, Some(2))));
1129 assert!(!moves_caret(Some((false, None)), (true, None)), "opening");
1130 assert!(!moves_caret(Some((true, None)), (true, None)), "typing");
1131 }
1132}