1use std::sync::Arc;
4
5use crate::callback::{Callback, KeyHandler};
6use crate::core::element::Element;
7use crate::core::event::{KeyCode, KeyEvent};
8use crate::style::{BorderStyle, Length, Padding, ScrollbarConfig, Span, Style, StyleSlot};
9use crate::widgets::{List, ListConfig, ListEvent, ListItem, ListItemLine};
10
11#[derive(Clone, Debug, PartialEq, Eq)]
13pub struct MultiSelectItem {
14 pub label: Arc<str>,
16 pub description: Option<Arc<str>>,
18}
19
20impl MultiSelectItem {
21 pub fn new(label: impl Into<Arc<str>>) -> Self {
23 Self {
24 label: label.into(),
25 description: None,
26 }
27 }
28
29 pub fn description(mut self, description: impl Into<Arc<str>>) -> Self {
31 self.description = Some(description.into());
32 self
33 }
34}
35
36impl From<&'static str> for MultiSelectItem {
37 fn from(value: &'static str) -> Self {
38 Self::new(value)
39 }
40}
41
42impl From<String> for MultiSelectItem {
43 fn from(value: String) -> Self {
44 Self::new(value)
45 }
46}
47
48impl From<Arc<str>> for MultiSelectItem {
49 fn from(value: Arc<str>) -> Self {
50 Self::new(value)
51 }
52}
53
54#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
56pub enum MultiSelectDescriptionPlacement {
57 #[default]
59 Inline,
60 Right,
62 Above,
64 Below,
66}
67
68#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
70pub enum MultiSelectDescriptionOverflow {
71 #[default]
73 Truncate,
74 Wrap,
76}
77
78#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
80pub struct MultiSelectToggleEvent {
81 pub index: usize,
83 pub selected: bool,
85}
86
87#[derive(Clone, Debug, PartialEq, Eq)]
89pub struct MultiSelectChangeEvent {
90 pub selected_indices: Vec<usize>,
92}
93
94#[derive(Clone, Debug, PartialEq, Eq)]
96pub struct MultiSelectCommitEvent {
97 pub selected_indices: Vec<usize>,
99}
100
101#[derive(Clone)]
107pub struct MultiSelect {
108 items: Arc<[MultiSelectItem]>,
109 active_index: usize,
110 selected_indices: Vec<usize>,
111 max_selected: Option<usize>,
112 title: Option<Arc<str>>,
113 title_style: Style,
114 width: Length,
115 height: Length,
116 list_config: ListConfig,
117 active_symbol: Option<Arc<str>>,
119 active_symbol_style: Option<Style>,
120 active_style: StyleSlot,
121 unselected_symbol: Option<Arc<str>>,
123 description_style: Style,
124 description_placement: MultiSelectDescriptionPlacement,
125 description_overflow: MultiSelectDescriptionOverflow,
126 description_selection: bool,
127 disabled: bool,
128 disabled_style: Style,
129 empty_text: Option<Arc<str>>,
130 focusable: bool,
131 tab_stop: bool,
132 on_focus: Option<Callback<()>>,
133 on_blur: Option<Callback<()>>,
134 on_key: Option<KeyHandler>,
135 on_active_index_change: Option<Callback<usize>>,
136 on_toggle: Option<Callback<MultiSelectToggleEvent>>,
137 on_change: Option<Callback<MultiSelectChangeEvent>>,
138 on_commit: Option<Callback<MultiSelectCommitEvent>>,
139}
140
141impl Default for MultiSelect {
142 fn default() -> Self {
143 Self {
144 items: Arc::from([]),
145 active_index: 0,
146 selected_indices: Vec::new(),
147 max_selected: None,
148 title: None,
149 title_style: Style::default(),
150 width: Length::Flex(1),
151 height: Length::Flex(1),
152 list_config: ListConfig {
153 border: true,
154 border_style: BorderStyle::Plain,
155 padding: Padding::default(),
156 style: Style::default(),
157 selection_style: StyleSlot::Inherit,
158 unfocused_selection_style: StyleSlot::Inherit,
159 selection_full_width: false,
160 selection_symbol: Some("[ ] ".into()),
161 selection_symbol_right: None,
162 selection_symbol_style: None,
163 unfocused_selection_symbol_style: None,
164 symbol_column: true,
165 gutter_gap: 0,
166 gutter_for_non_selectable: false,
167 item_horizontal_padding: Padding::default(),
168 header_horizontal_padding: Padding::default(),
169 empty_text_style: Style::default(),
170 item_hover_style: None,
171 scrollbar: false,
172 scrollbar_config: ScrollbarConfig::default(),
173 },
174 active_symbol: Some("[x] ".into()),
175 active_symbol_style: None,
176 active_style: StyleSlot::Inherit,
177 unselected_symbol: Some("[ ] ".into()),
178 description_style: Style::default(),
179 description_placement: MultiSelectDescriptionPlacement::Inline,
180 description_overflow: MultiSelectDescriptionOverflow::Truncate,
181 description_selection: true,
182 disabled: false,
183 disabled_style: Style::default(),
184 empty_text: None,
185 focusable: true,
186 tab_stop: true,
187 on_focus: None,
188 on_blur: None,
189 on_key: None,
190 on_active_index_change: None,
191 on_toggle: None,
192 on_change: None,
193 on_commit: None,
194 }
195 }
196}
197
198impl MultiSelect {
199 pub fn new() -> Self {
201 Self::default()
202 }
203
204 pub fn items(mut self, items: impl IntoIterator<Item = impl Into<MultiSelectItem>>) -> Self {
206 self.items = items.into_iter().map(Into::into).collect::<Vec<_>>().into();
207 self
208 }
209
210 pub fn items_arc(mut self, items: Arc<[MultiSelectItem]>) -> Self {
212 self.items = items;
213 self
214 }
215
216 pub fn description_style(mut self, style: Style) -> Self {
218 self.description_style = style;
219 self
220 }
221
222 pub fn description_placement(mut self, placement: MultiSelectDescriptionPlacement) -> Self {
224 self.description_placement = placement;
225 self
226 }
227
228 pub fn description_overflow(mut self, overflow: MultiSelectDescriptionOverflow) -> Self {
236 self.description_overflow = overflow;
237 self
238 }
239
240 pub fn description_selection(mut self, highlight: bool) -> Self {
245 self.description_selection = highlight;
246 self
247 }
248
249 pub fn active_index(mut self, active_index: usize) -> Self {
251 self.active_index = active_index;
252 self
253 }
254
255 pub fn selected_indices(mut self, selected_indices: impl IntoIterator<Item = usize>) -> Self {
257 self.selected_indices = selected_indices.into_iter().collect();
258 self
259 }
260
261 pub fn max_selected(mut self, max_selected: usize) -> Self {
263 self.max_selected = Some(max_selected);
264 self
265 }
266
267 pub fn width(mut self, width: Length) -> Self {
269 self.width = width;
270 self
271 }
272
273 pub fn title(mut self, title: impl Into<Arc<str>>) -> Self {
275 self.title = Some(title.into());
276 self
277 }
278
279 pub fn title_style(mut self, style: Style) -> Self {
281 self.title_style = style;
282 self
283 }
284
285 pub fn height(mut self, height: Length) -> Self {
287 self.height = height;
288 self
289 }
290
291 pub fn list_config(mut self, config: ListConfig) -> Self {
293 self.list_config = config;
294 self
295 }
296
297 pub fn border(mut self, border: bool) -> Self {
299 self.list_config.border = border;
300 self
301 }
302
303 pub fn border_style(mut self, style: BorderStyle) -> Self {
305 self.list_config.border_style = style;
306 self
307 }
308
309 pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
311 self.list_config.padding = padding.into();
312 self
313 }
314
315 pub fn style(mut self, style: Style) -> Self {
317 self.list_config.style = style;
318 self
319 }
320
321 pub fn selection_style(mut self, style: Style) -> Self {
323 self.list_config.selection_style = StyleSlot::Replace(style);
324 self
325 }
326
327 pub fn extend_selection_style(mut self, style: Style) -> Self {
329 self.list_config.selection_style = StyleSlot::Extend(style);
330 self
331 }
332
333 pub fn inherit_selection_style(mut self) -> Self {
335 self.list_config.selection_style = StyleSlot::Inherit;
336 self
337 }
338
339 pub fn selection_style_slot(mut self, slot: StyleSlot) -> Self {
341 self.list_config.selection_style = slot;
342 self
343 }
344
345 pub fn unfocused_selection_style(mut self, style: Style) -> Self {
347 self.list_config.unfocused_selection_style = StyleSlot::Replace(style);
348 self
349 }
350
351 pub fn extend_unfocused_selection_style(mut self, style: Style) -> Self {
353 self.list_config.unfocused_selection_style = StyleSlot::Extend(style);
354 self
355 }
356
357 pub fn inherit_unfocused_selection_style(mut self) -> Self {
359 self.list_config.unfocused_selection_style = StyleSlot::Inherit;
360 self
361 }
362
363 pub fn unfocused_selection_style_slot(mut self, slot: StyleSlot) -> Self {
365 self.list_config.unfocused_selection_style = slot;
366 self
367 }
368
369 pub fn selection_full_width(mut self, selection_full_width: bool) -> Self {
371 self.list_config.selection_full_width = selection_full_width;
372 self
373 }
374
375 pub fn item_hover_style(mut self, style: Style) -> Self {
377 self.list_config.item_hover_style = Some(StyleSlot::Replace(style));
378 self
379 }
380
381 pub fn extend_item_hover_style(mut self, style: Style) -> Self {
383 self.list_config.item_hover_style = Some(StyleSlot::Extend(style));
384 self
385 }
386
387 pub fn inherit_item_hover_style(mut self) -> Self {
389 self.list_config.item_hover_style = Some(StyleSlot::Inherit);
390 self
391 }
392
393 pub fn item_hover_style_slot(mut self, slot: StyleSlot) -> Self {
395 self.list_config.item_hover_style = Some(slot);
396 self
397 }
398
399 pub fn selection_symbol(mut self, symbol: Option<impl Into<Arc<str>>>) -> Self {
406 self.list_config.selection_symbol = symbol.map(Into::into);
407 self
408 }
409
410 pub fn selection_symbol_right(mut self, symbol: Option<impl Into<Arc<str>>>) -> Self {
413 self.list_config.selection_symbol_right = symbol.map(Into::into);
414 self
415 }
416
417 pub fn selection_symbol_style(mut self, style: Style) -> Self {
419 self.list_config.selection_symbol_style = Some(style);
420 self
421 }
422
423 pub fn unfocused_selection_symbol_style(mut self, style: Style) -> Self {
425 self.list_config.unfocused_selection_symbol_style = Some(style);
426 self
427 }
428
429 pub fn active_symbol(mut self, symbol: Option<impl Into<Arc<str>>>) -> Self {
434 self.active_symbol = symbol.map(Into::into);
435 self
436 }
437
438 pub fn active_symbol_style(mut self, style: Style) -> Self {
440 self.active_symbol_style = Some(style);
441 self
442 }
443
444 pub fn active_style(mut self, style: Style) -> Self {
446 self.active_style = StyleSlot::Replace(style);
447 self
448 }
449
450 pub fn extend_active_style(mut self, style: Style) -> Self {
452 self.active_style = StyleSlot::Extend(style);
453 self
454 }
455
456 pub fn inherit_active_style(mut self) -> Self {
458 self.active_style = StyleSlot::Inherit;
459 self
460 }
461
462 pub fn active_style_slot(mut self, slot: StyleSlot) -> Self {
464 self.active_style = slot;
465 self
466 }
467
468 pub fn unselected_symbol(mut self, symbol: Option<impl Into<Arc<str>>>) -> Self {
473 self.unselected_symbol = symbol.map(Into::into);
474 self
475 }
476
477 pub fn disabled(mut self, disabled: bool) -> Self {
479 self.disabled = disabled;
480 self
481 }
482
483 pub fn disabled_style(mut self, style: Style) -> Self {
485 self.disabled_style = style;
486 self
487 }
488
489 pub fn focusable(mut self, focusable: bool) -> Self {
491 self.focusable = focusable;
492 self
493 }
494
495 pub fn tab_stop(mut self, tab_stop: bool) -> Self {
497 self.tab_stop = tab_stop;
498 self
499 }
500
501 pub fn on_focus(mut self, cb: Callback<()>) -> Self {
503 self.on_focus = Some(cb);
504 self
505 }
506
507 pub fn on_blur(mut self, cb: Callback<()>) -> Self {
509 self.on_blur = Some(cb);
510 self
511 }
512
513 pub fn on_key(mut self, handler: KeyHandler) -> Self {
515 self.on_key = Some(handler);
516 self
517 }
518
519 pub fn scrollbar(mut self, scrollbar: bool) -> Self {
521 self.list_config.scrollbar = scrollbar;
522 self
523 }
524
525 pub fn scrollbar_config(mut self, config: ScrollbarConfig) -> Self {
527 self.list_config.scrollbar_config = config;
528 self
529 }
530
531 pub fn empty_text(mut self, text: impl Into<Arc<str>>) -> Self {
533 self.empty_text = Some(text.into());
534 self
535 }
536
537 pub fn empty_text_style(mut self, style: Style) -> Self {
539 self.list_config.empty_text_style = style;
540 self
541 }
542
543 pub fn on_active_index_change(mut self, cb: Callback<usize>) -> Self {
545 self.on_active_index_change = Some(cb);
546 self
547 }
548
549 pub fn on_toggle(mut self, cb: Callback<MultiSelectToggleEvent>) -> Self {
551 self.on_toggle = Some(cb);
552 self
553 }
554
555 pub fn on_change(mut self, cb: Callback<MultiSelectChangeEvent>) -> Self {
557 self.on_change = Some(cb);
558 self
559 }
560
561 pub fn on_commit(mut self, cb: Callback<MultiSelectCommitEvent>) -> Self {
563 self.on_commit = Some(cb);
564 self
565 }
566}
567
568impl From<MultiSelect> for Element {
569 fn from(multi: MultiSelect) -> Self {
570 let selected_indices =
571 normalize_selected_indices(multi.selected_indices, multi.items.len());
572 let active_index = normalized_active_index(multi.active_index, multi.items.len());
573
574 let list_items = multi
575 .items
576 .iter()
577 .enumerate()
578 .map(|(index, item)| {
579 let is_checked = selected_indices.binary_search(&index).is_ok();
580 multi_select_list_item(
581 item,
582 is_checked,
583 multi.description_style,
584 multi.description_placement,
585 multi.description_overflow,
586 multi.description_selection,
587 )
588 })
589 .collect::<Vec<_>>();
590
591 let mut list = List::new()
592 .items(list_items)
593 .selected(active_index)
594 .activate_on_click(false)
595 .border(multi.list_config.border)
596 .border_style(multi.list_config.border_style)
597 .padding(multi.list_config.padding)
598 .style(multi.list_config.style)
599 .selection_full_width(multi.list_config.selection_full_width)
600 .selection_symbol(multi.list_config.selection_symbol)
601 .selection_symbol_right(multi.list_config.selection_symbol_right)
602 .symbol_column(multi.list_config.symbol_column)
603 .gutter_gap(multi.list_config.gutter_gap)
604 .gutter_for_non_selectable(multi.list_config.gutter_for_non_selectable)
605 .active_symbol(multi.active_symbol)
606 .active_style_slot(multi.active_style)
607 .unselected_symbol(multi.unselected_symbol)
608 .scrollbar(multi.list_config.scrollbar)
609 .scrollbar_config(multi.list_config.scrollbar_config)
610 .title_style(multi.title_style)
611 .width(multi.width)
612 .height(multi.height)
613 .disabled(multi.disabled)
614 .disabled_style(multi.disabled_style)
615 .focusable(multi.focusable)
616 .tab_stop(multi.tab_stop)
617 .item_horizontal_padding(multi.list_config.item_horizontal_padding)
618 .header_horizontal_padding(multi.list_config.header_horizontal_padding)
619 .empty_text_style(multi.list_config.empty_text_style);
620 list = list
621 .selection_style_slot(multi.list_config.selection_style)
622 .unfocused_selection_style_slot(multi.list_config.unfocused_selection_style)
623 .item_hover_style_slot(
624 multi
625 .list_config
626 .item_hover_style
627 .unwrap_or(multi.list_config.selection_style),
628 );
629
630 if let Some(cb) = multi.on_focus.clone() {
631 list = list.on_focus(cb);
632 }
633 if let Some(cb) = multi.on_blur.clone() {
634 list = list.on_blur(cb);
635 }
636
637 if let Some(style) = multi.list_config.selection_symbol_style {
638 list = list.selection_symbol_style(style);
639 }
640 if let Some(style) = multi.list_config.unfocused_selection_symbol_style {
641 list = list.unfocused_selection_symbol_style(style);
642 }
643 if let Some(style) = multi.active_symbol_style {
644 list = list.active_symbol_style(style);
645 }
646 if let Some(title) = multi.title {
647 list = list.title(title);
648 }
649 if let Some(empty_text) = multi.empty_text {
650 list = list.empty_text(empty_text);
651 }
652
653 if let Some(cb) = multi.on_active_index_change.clone() {
654 list = list.on_select(Callback::new(move |event: ListEvent| cb.emit(event.index)));
655 }
656
657 if let Some(cb) = multi.on_commit {
658 let selected_indices = selected_indices.clone();
659 list = list.on_activate(Callback::new(move |_event: ListEvent| {
660 cb.emit(MultiSelectCommitEvent {
661 selected_indices: selected_indices.clone(),
662 });
663 }));
664 }
665
666 if !multi.disabled && (multi.on_toggle.is_some() || multi.on_change.is_some()) {
667 let selected_indices = selected_indices.clone();
668 let max_selected = multi.max_selected;
669 let on_toggle = multi.on_toggle;
670 let on_change = multi.on_change;
671 let selected_indices_click = selected_indices.clone();
672 let on_toggle_click = on_toggle.clone();
673 let on_change_click = on_change.clone();
674
675 list = list.on_item_click(Callback::new(move |event: ListEvent| {
676 let (next_selected, toggled_to_selected) = match toggle_selection(
677 selected_indices_click.as_slice(),
678 event.index,
679 max_selected,
680 ) {
681 Some(result) => result,
682 None => return,
683 };
684
685 if let Some(cb) = on_toggle_click.as_ref() {
686 cb.emit(MultiSelectToggleEvent {
687 index: event.index,
688 selected: toggled_to_selected,
689 });
690 }
691 if let Some(cb) = on_change_click.as_ref() {
692 cb.emit(MultiSelectChangeEvent {
693 selected_indices: next_selected,
694 });
695 }
696 }));
697
698 let caller_on_key = multi.on_key.clone();
699 list = list.on_key(KeyHandler::new(move |key: KeyEvent| {
700 if caller_on_key
701 .as_ref()
702 .is_some_and(|handler| handler.handle(key))
703 {
704 return true;
705 }
706 if key.code != KeyCode::Char(' ') {
707 return false;
708 }
709
710 let (next_selected, toggled_to_selected) =
711 match toggle_selection(selected_indices.as_slice(), active_index, max_selected)
712 {
713 Some(result) => result,
714 None => return true,
715 };
716
717 if let Some(cb) = on_toggle.as_ref() {
718 cb.emit(MultiSelectToggleEvent {
719 index: active_index,
720 selected: toggled_to_selected,
721 });
722 }
723 if let Some(cb) = on_change.as_ref() {
724 cb.emit(MultiSelectChangeEvent {
725 selected_indices: next_selected,
726 });
727 }
728
729 true
730 }));
731 } else if let Some(handler) = multi.on_key {
732 list = list.on_key(handler);
733 }
734
735 list.into()
736 }
737}
738
739fn multi_select_list_item(
740 item: &MultiSelectItem,
741 checked: bool,
742 description_style: Style,
743 description_placement: MultiSelectDescriptionPlacement,
744 description_overflow: MultiSelectDescriptionOverflow,
745 description_selection: bool,
746) -> ListItem {
747 let overflow = effective_description_overflow(description_placement, description_overflow);
748
749 let mut list_item = if let Some(description) = &item.description {
750 match description_placement {
751 MultiSelectDescriptionPlacement::Inline => ListItem::from_spans([
752 Span::new(item.label.clone()),
753 Span::new(" - ").style(description_style),
754 Span::new(description.clone()).style(description_style),
755 ]),
756 MultiSelectDescriptionPlacement::Right => ListItem::new(item.label.clone())
757 .description_spans([
758 Span::new(" ").style(description_style),
759 Span::new(description.clone()).style(description_style),
760 ])
761 .primary_selection_description(true)
762 .primary_hover_description(true)
763 .primary_truncate_description_first(true),
764 MultiSelectDescriptionPlacement::Above => {
765 ListItem::from_spans([Span::new(description.clone()).style(description_style)])
766 .primary_selection_label(description_selection)
767 .primary_selection_description(description_selection)
768 .primary_hover_label(description_selection)
769 .primary_hover_description(description_selection)
770 .primary_wrap_label(matches!(overflow, MultiSelectDescriptionOverflow::Wrap))
771 .symbol_line(1)
772 .line(ListItemLine::new(item.label.clone()))
773 }
774 MultiSelectDescriptionPlacement::Below => ListItem::new(item.label.clone()).line(
775 ListItemLine::new(description.clone())
776 .style(description_style)
777 .selection_label(description_selection)
778 .selection_description(description_selection)
779 .hover_label(description_selection)
780 .hover_description(description_selection)
781 .wrap_label(matches!(overflow, MultiSelectDescriptionOverflow::Wrap)),
782 ),
783 }
784 } else {
785 ListItem::new(item.label.clone())
786 };
787
788 list_item = list_item.active(checked);
789 list_item
790}
791
792fn effective_description_overflow(
793 placement: MultiSelectDescriptionPlacement,
794 overflow: MultiSelectDescriptionOverflow,
795) -> MultiSelectDescriptionOverflow {
796 match placement {
797 MultiSelectDescriptionPlacement::Above | MultiSelectDescriptionPlacement::Below => overflow,
798 MultiSelectDescriptionPlacement::Inline | MultiSelectDescriptionPlacement::Right => {
799 MultiSelectDescriptionOverflow::Truncate
800 }
801 }
802}
803
804fn normalized_active_index(active_index: usize, len: usize) -> usize {
805 if len == 0 {
806 0
807 } else {
808 active_index.min(len.saturating_sub(1))
809 }
810}
811
812fn normalize_selected_indices(mut selected_indices: Vec<usize>, len: usize) -> Vec<usize> {
813 selected_indices.retain(|index| *index < len);
814 selected_indices.sort_unstable();
815 selected_indices.dedup();
816 selected_indices
817}
818
819fn toggle_selection(
820 selected_indices: &[usize],
821 index: usize,
822 max_selected: Option<usize>,
823) -> Option<(Vec<usize>, bool)> {
824 let mut next = selected_indices.to_vec();
825
826 match next.binary_search(&index) {
827 Ok(index) => {
828 next.remove(index);
829 Some((next, false))
830 }
831 Err(insert_index) => {
832 if let Some(limit) = max_selected
833 && next.len() >= limit
834 {
835 return None;
836 }
837 next.insert(insert_index, index);
838 Some((next, true))
839 }
840 }
841}
842
843#[cfg(test)]
844mod tests {
845 use super::{
846 MultiSelectDescriptionOverflow, MultiSelectDescriptionPlacement, MultiSelectItem,
847 multi_select_list_item, normalize_selected_indices, toggle_selection,
848 };
849 use crate::style::Style;
850
851 #[test]
852 fn normalize_selected_indices_sorts_and_dedups() {
853 let selected = normalize_selected_indices(vec![4, 2, 2, 9, 1], 5);
854 assert_eq!(selected, vec![1, 2, 4]);
855 }
856
857 #[test]
858 fn toggle_selection_adds_and_removes() {
859 let (selected, added) = toggle_selection(&[1, 3], 2, None).expect("should toggle");
860 assert!(added);
861 assert_eq!(selected, vec![1, 2, 3]);
862
863 let (selected, added) = toggle_selection(&[1, 2, 3], 2, None).expect("should toggle");
864 assert!(!added);
865 assert_eq!(selected, vec![1, 3]);
866 }
867
868 #[test]
869 fn toggle_selection_respects_limit() {
870 let toggled = toggle_selection(&[0, 1], 2, Some(2));
871 assert!(toggled.is_none());
872 }
873
874 #[test]
875 fn above_description_keeps_label_on_secondary_line() {
876 let item = MultiSelectItem::new("Label").description("Desc");
877 let rendered = multi_select_list_item(
878 &item,
879 false,
880 Style::default(),
881 MultiSelectDescriptionPlacement::Above,
882 MultiSelectDescriptionOverflow::Truncate,
883 false,
884 );
885
886 let primary: String = rendered
887 .spans
888 .iter()
889 .map(|span| span.content.as_ref())
890 .collect();
891 let secondary: String = rendered.extra_lines[0]
892 .spans
893 .iter()
894 .map(|span| span.content.as_ref())
895 .collect();
896
897 assert_eq!(primary, "Desc");
898 assert_eq!(secondary, "Label");
899 }
900
901 #[test]
902 fn right_description_always_highlights_and_hovers() {
903 let item = MultiSelectItem::new("Label").description("Desc");
904 let rendered = multi_select_list_item(
905 &item,
906 false,
907 Style::default(),
908 MultiSelectDescriptionPlacement::Right,
909 MultiSelectDescriptionOverflow::Wrap,
910 false,
911 );
912
913 assert!(rendered.primary_selection_description);
914 assert!(rendered.primary_hover_description);
915 assert!(!rendered.description_spans.is_empty());
916 assert_eq!(rendered.description_spans[0].content.as_ref(), " ");
917 }
918
919 #[test]
920 fn below_wrap_sets_wrap_label_flag() {
921 let item = MultiSelectItem::new("Label").description("Desc");
922 let rendered = multi_select_list_item(
923 &item,
924 false,
925 Style::default(),
926 MultiSelectDescriptionPlacement::Below,
927 MultiSelectDescriptionOverflow::Wrap,
928 true,
929 );
930
931 assert_eq!(rendered.extra_lines.len(), 1);
932 assert!(rendered.extra_lines[0].wrap_label);
933 }
934
935 #[test]
936 fn items_arc_preserves_shared_slice() {
937 use super::MultiSelect;
938 use std::sync::Arc;
939
940 let items: Arc<[MultiSelectItem]> =
941 Arc::from([MultiSelectItem::new("a"), MultiSelectItem::new("b")]);
942 let multi = MultiSelect::new().items_arc(Arc::clone(&items));
943 assert!(Arc::ptr_eq(&multi.items, &items));
944 }
945}