1use std::rc::Rc;
17
18use iced::Element;
19use iced::keyboard::{Key, key::Named};
20use iced::widget::column;
21
22use crate::icon::Glyph;
23use crate::keys::{self, Chord, Keymap};
24use crate::overlay::anchored::{Align, Placement, Side, anchored};
25use crate::overlay::menu::{self, Leading, Parts, RowStatus, Trailing};
26use crate::theme::space;
27
28pub const WIDTH: f32 = 224.0;
30
31#[derive(Debug, Clone, PartialEq)]
33pub enum Kind<Id> {
34 Action,
36 Checkbox(bool),
38 Radio(bool),
41 Submenu(Vec<Entry<Id>>),
43}
44
45#[derive(Debug, Clone, PartialEq)]
47pub struct Item<Id> {
48 pub id: Id,
49 pub label: String,
50 pub icon: Option<Glyph>,
51 pub shortcut: Option<String>,
53 pub disabled: bool,
54 pub destructive: bool,
55 pub kind: Kind<Id>,
56}
57
58impl<Id> Item<Id> {
59 fn enabled(&self) -> bool {
60 !self.disabled
61 }
62
63 fn submenu(&self) -> Option<&[Entry<Id>]> {
64 match &self.kind {
65 Kind::Submenu(entries) => Some(entries),
66 _ => None,
67 }
68 }
69}
70
71#[derive(Debug, Clone, PartialEq)]
73pub enum Entry<Id> {
74 Item(Item<Id>),
75 Label(String),
77 Separator,
78}
79
80fn new_item<Id>(id: Id, label: impl Into<String>, kind: Kind<Id>) -> Entry<Id> {
81 Entry::Item(Item {
82 id,
83 label: label.into(),
84 icon: None,
85 shortcut: None,
86 disabled: false,
87 destructive: false,
88 kind,
89 })
90}
91
92pub fn item<Id>(id: Id, label: impl Into<String>) -> Entry<Id> {
94 new_item(id, label, Kind::Action)
95}
96
97pub fn checkbox_item<Id>(id: Id, label: impl Into<String>, checked: bool) -> Entry<Id> {
99 new_item(id, label, Kind::Checkbox(checked))
100}
101
102pub fn radio_item<Id>(id: Id, label: impl Into<String>, checked: bool) -> Entry<Id> {
104 new_item(id, label, Kind::Radio(checked))
105}
106
107pub fn submenu<Id>(
109 id: Id,
110 label: impl Into<String>,
111 entries: impl IntoIterator<Item = Entry<Id>>,
112) -> Entry<Id> {
113 new_item(id, label, Kind::Submenu(entries.into_iter().collect()))
114}
115
116pub fn group_label<Id>(label: impl Into<String>) -> Entry<Id> {
118 Entry::Label(label.into())
119}
120
121pub fn separator<Id>() -> Entry<Id> {
123 Entry::Separator
124}
125
126impl<Id> Entry<Id> {
127 pub fn icon(self, glyph: Glyph) -> Self {
130 self.with_item(|item| item.icon = Some(glyph))
131 }
132
133 pub fn shortcut(self, hint: impl Into<String>) -> Self {
134 let hint = hint.into();
135 self.with_item(|item| item.shortcut = Some(hint))
136 }
137
138 pub fn disabled(self, disabled: bool) -> Self {
139 self.with_item(|item| item.disabled = disabled)
140 }
141
142 pub fn destructive(self, destructive: bool) -> Self {
144 self.with_item(|item| item.destructive = destructive)
145 }
146
147 pub fn as_item(&self) -> Option<&Item<Id>> {
148 match self {
149 Entry::Item(item) => Some(item),
150 _ => None,
151 }
152 }
153
154 fn with_item(mut self, change: impl FnOnce(&mut Item<Id>)) -> Self {
155 if let Entry::Item(item) = &mut self {
156 change(item);
157 }
158 self
159 }
160
161 fn enabled_item(&self) -> Option<&Item<Id>> {
162 self.as_item().filter(|item| item.enabled())
163 }
164}
165
166#[derive(Debug, Clone, Copy, PartialEq, Eq)]
168pub enum Event<Id> {
169 Open,
171 Close,
172 Toggle,
173 Highlight(Id),
175 Next,
178 Previous,
181 First,
182 Last,
183 OpenSubmenu,
185 CloseSubmenu,
187 Activate(Id),
189 ActivateHighlighted,
192 Typeahead(char),
194}
195
196#[derive(Debug, Clone, Copy, PartialEq, Eq)]
198pub enum Output<Id> {
199 Activated(Id),
201 Toggled(Id, bool),
203 Selected(Id),
205}
206
207#[derive(Debug, Clone, PartialEq)]
209pub struct State<Id> {
210 entries: Vec<Entry<Id>>,
211 levels: Vec<Option<usize>>,
213}
214
215impl<Id: Copy + PartialEq> State<Id> {
216 pub fn new(entries: impl IntoIterator<Item = Entry<Id>>) -> Self {
218 Self {
219 entries: entries.into_iter().collect(),
220 levels: Vec::new(),
221 }
222 }
223
224 pub fn entries(&self) -> &[Entry<Id>] {
225 &self.entries
226 }
227
228 pub fn is_open(&self) -> bool {
229 !self.levels.is_empty()
230 }
231
232 pub fn depth(&self) -> usize {
235 self.levels.len()
236 }
237
238 pub fn highlighted(&self) -> Option<Id> {
240 let path = self.active_path()?;
241 self.item_at(&path).map(|item| item.id)
242 }
243
244 pub fn highlighted_at(&self, depth: usize) -> Option<usize> {
246 self.levels.get(depth).copied().flatten()
247 }
248
249 pub fn item(&self, id: Id) -> Option<&Item<Id>> {
250 let path = path_of(&self.entries, id)?;
251 self.item_at(&path)
252 }
253
254 pub fn is_checked(&self, id: Id) -> bool {
256 self.item(id)
257 .is_some_and(|item| matches!(item.kind, Kind::Checkbox(true) | Kind::Radio(true)))
258 }
259
260 pub fn set_checked(&mut self, id: Id, checked: bool) {
262 let Some(path) = path_of(&self.entries, id) else {
263 return;
264 };
265 let Some((&index, parents)) = path.split_last() else {
266 return;
267 };
268 let Some(siblings) = entries_mut(&mut self.entries, parents) else {
269 return;
270 };
271 let is_radio = matches!(
272 siblings.get(index),
273 Some(Entry::Item(Item {
274 kind: Kind::Radio(_),
275 ..
276 }))
277 );
278 if is_radio && checked {
279 for sibling in radio_group(siblings, index) {
280 if let Some(Entry::Item(item)) = siblings.get_mut(sibling) {
281 item.kind = Kind::Radio(false);
282 }
283 }
284 }
285 if let Some(Entry::Item(item)) = siblings.get_mut(index) {
286 match &mut item.kind {
287 Kind::Checkbox(value) | Kind::Radio(value) => *value = checked,
288 Kind::Action | Kind::Submenu(_) => {}
289 }
290 }
291 }
292
293 pub fn set_disabled(&mut self, id: Id, disabled: bool) {
295 let Some(path) = path_of(&self.entries, id) else {
296 return;
297 };
298 let Some((&index, parents)) = path.split_last() else {
299 return;
300 };
301 if let Some(Entry::Item(item)) =
302 entries_mut(&mut self.entries, parents).and_then(|entries| entries.get_mut(index))
303 {
304 item.disabled = disabled;
305 }
306 if disabled && self.levels.get(parents.len()).copied().flatten() == Some(index) {
307 self.levels.truncate(parents.len() + 1);
308 if let Some(level) = self.levels.get_mut(parents.len()) {
309 *level = None;
310 }
311 }
312 }
313
314 pub fn update(&mut self, event: Event<Id>) -> Option<Output<Id>> {
316 match event {
317 Event::Open => self.open(),
318 Event::Close => self.levels.clear(),
319 Event::Toggle if self.is_open() => self.levels.clear(),
320 Event::Toggle => self.open(),
321 Event::Highlight(id) => self.highlight(id),
322 Event::Next | Event::First if !self.is_open() => self.open_on(Edge::First),
323 Event::Previous | Event::Last if !self.is_open() => self.open_on(Edge::Last),
324 Event::Next => self.step(Step::Next),
325 Event::Previous => self.step(Step::Previous),
326 Event::First => self.step(Step::Edge(Edge::First)),
327 Event::Last => self.step(Step::Edge(Edge::Last)),
328 Event::OpenSubmenu => self.open_submenu(),
329 Event::CloseSubmenu => self.close_submenu(),
330 Event::Typeahead(character) => self.typeahead(character),
331 Event::Activate(id) => return self.activate(id),
332 Event::ActivateHighlighted if !self.is_open() => self.open_on(Edge::First),
333 Event::ActivateHighlighted => return self.activate_highlighted(),
334 }
335 None
336 }
337
338 pub fn key_event(&self, keymap: &Keymap<Action>, key: &keys::Event) -> Option<Event<Id>> {
343 if let Some(action) = keymap.resolve_event(key) {
344 return action.event(self);
345 }
346 if !self.is_open() {
347 return None;
348 }
349 typeahead(key).map(Event::Typeahead)
350 }
351
352 fn open(&mut self) {
353 if !self.is_open() {
354 self.levels.push(None);
355 }
356 }
357
358 fn open_on(&mut self, edge: Edge) {
359 self.levels = vec![edge.find(&self.entries)];
360 }
361
362 fn active_depth(&self) -> usize {
364 self.levels.iter().rposition(Option::is_some).unwrap_or(0)
365 }
366
367 fn active_path(&self) -> Option<Vec<usize>> {
368 let depth = self.active_depth();
369 self.levels.get(..=depth)?.iter().copied().collect()
370 }
371
372 fn entries_at(&self, depth: usize) -> Option<&[Entry<Id>]> {
373 let mut entries = self.entries.as_slice();
374 for level in self.levels.get(..depth)? {
375 entries = entries.get((*level)?)?.as_item()?.submenu()?;
376 }
377 Some(entries)
378 }
379
380 fn item_at(&self, path: &[usize]) -> Option<&Item<Id>> {
381 let (&index, parents) = path.split_last()?;
382 let mut entries = self.entries.as_slice();
383 for &parent in parents {
384 entries = entries.get(parent)?.as_item()?.submenu()?;
385 }
386 entries.get(index)?.as_item()
387 }
388
389 fn step(&mut self, step: Step) {
391 let depth = self.active_depth();
392 let Some(entries) = self.entries_at(depth) else {
393 return;
394 };
395 let current = self.highlighted_at(depth);
396 let target = match step {
397 Step::Next => next_enabled(entries, current, true, |_| true),
398 Step::Previous => next_enabled(entries, current, false, |_| true),
399 Step::Edge(edge) => edge.find(entries),
400 };
401 self.set_highlight(depth, target);
402 }
403
404 fn typeahead(&mut self, character: char) {
405 let depth = self.active_depth();
406 let Some(entries) = self.entries_at(depth) else {
407 return;
408 };
409 let wanted = character.to_lowercase().collect::<String>();
410 let target = next_enabled(entries, self.highlighted_at(depth), true, |item| {
411 item.label.to_lowercase().starts_with(&wanted)
412 });
413 self.set_highlight(depth, target);
414 }
415
416 fn set_highlight(&mut self, depth: usize, target: Option<usize>) {
417 if target.is_none() {
418 return;
419 }
420 self.levels.truncate(depth + 1);
421 if let Some(level) = self.levels.get_mut(depth) {
422 *level = target;
423 }
424 }
425
426 fn highlight(&mut self, id: Id) {
427 let Some(path) = path_of(&self.entries, id) else {
428 return;
429 };
430 let Some(item) = self.item_at(&path).filter(|item| item.enabled()) else {
431 return;
432 };
433 let opens = item.submenu().is_some();
434 let target = some(&path);
435 if self.levels.len() > target.len() && self.levels.starts_with(&target) {
436 return;
437 }
438 self.levels = target;
439 if opens {
440 self.levels.push(None);
441 }
442 }
443
444 fn open_submenu(&mut self) {
445 let depth = self.active_depth();
446 let Some(path) = self.active_path() else {
447 return;
448 };
449 let Some(children) = self
450 .item_at(&path)
451 .filter(|item| item.enabled())
452 .and_then(Item::submenu)
453 else {
454 return;
455 };
456 let first = Edge::First.find(children);
457 self.levels.truncate(depth + 1);
458 self.levels.push(first);
459 }
460
461 fn close_submenu(&mut self) {
462 if self.levels.len() <= 1 {
463 return;
464 }
465 let keep = self.active_depth().max(1);
466 self.levels.truncate(keep);
467 }
468
469 fn activate(&mut self, id: Id) -> Option<Output<Id>> {
470 let path = path_of(&self.entries, id)?;
471 let item = self.item_at(&path).filter(|item| item.enabled())?;
472 if item.submenu().is_some() {
473 self.highlight(id);
474 return None;
475 }
476 self.choose(id)
477 }
478
479 fn activate_highlighted(&mut self) -> Option<Output<Id>> {
480 let path = self.active_path()?;
481 let item = self.item_at(&path).filter(|item| item.enabled())?;
482 if item.submenu().is_some() {
483 self.open_submenu();
484 return None;
485 }
486 let id = item.id;
487 self.choose(id)
488 }
489
490 fn choose(&mut self, id: Id) -> Option<Output<Id>> {
492 let output = match self.item(id)?.kind {
493 Kind::Action => Output::Activated(id),
494 Kind::Checkbox(checked) => {
495 self.set_checked(id, !checked);
496 Output::Toggled(id, !checked)
497 }
498 Kind::Radio(_) => {
499 self.set_checked(id, true);
500 Output::Selected(id)
501 }
502 Kind::Submenu(_) => return None,
503 };
504 self.levels.clear();
505 Some(output)
506 }
507}
508
509fn some(path: &[usize]) -> Vec<Option<usize>> {
510 path.iter().copied().map(Some).collect()
511}
512
513#[derive(Debug, Clone, Copy)]
514enum Edge {
515 First,
516 Last,
517}
518
519impl Edge {
520 fn find<Id>(self, entries: &[Entry<Id>]) -> Option<usize> {
521 let enabled = |(_, entry): &(usize, &Entry<Id>)| entry.enabled_item().is_some();
522 match self {
523 Edge::First => entries.iter().enumerate().find(enabled),
524 Edge::Last => entries.iter().enumerate().rev().find(enabled),
525 }
526 .map(|(index, _)| index)
527 }
528}
529
530#[derive(Debug, Clone, Copy)]
531enum Step {
532 Next,
533 Previous,
534 Edge(Edge),
535}
536
537fn next_enabled<Id>(
540 entries: &[Entry<Id>],
541 from: Option<usize>,
542 forward: bool,
543 accept: impl Fn(&Item<Id>) -> bool,
544) -> Option<usize> {
545 let len = entries.len();
546 if len == 0 {
547 return None;
548 }
549 let start = from.unwrap_or(if forward { len - 1 } else { 0 });
550 let offset = if forward { 1 } else { len - 1 };
551 (1..=len)
552 .map(|n| (start + offset * n) % len)
553 .find(|&index| {
554 entries
555 .get(index)
556 .and_then(Entry::enabled_item)
557 .is_some_and(&accept)
558 })
559}
560
561fn path_of<Id: Copy + PartialEq>(entries: &[Entry<Id>], id: Id) -> Option<Vec<usize>> {
563 entries.iter().enumerate().find_map(|(index, entry)| {
564 let item = entry.as_item()?;
565 if item.id == id {
566 return Some(vec![index]);
567 }
568 let mut path = path_of(item.submenu()?, id)?;
569 path.insert(0, index);
570 Some(path)
571 })
572}
573
574fn entries_mut<'a, Id>(
575 entries: &'a mut Vec<Entry<Id>>,
576 parents: &[usize],
577) -> Option<&'a mut Vec<Entry<Id>>> {
578 let Some((&first, rest)) = parents.split_first() else {
579 return Some(entries);
580 };
581 match entries.get_mut(first)? {
582 Entry::Item(Item {
583 kind: Kind::Submenu(children),
584 ..
585 }) => entries_mut(children, rest),
586 _ => None,
587 }
588}
589
590fn radio_group<Id>(entries: &[Entry<Id>], index: usize) -> std::ops::Range<usize> {
592 let is_radio = |entry: &Entry<Id>| {
593 matches!(
594 entry,
595 Entry::Item(Item {
596 kind: Kind::Radio(_),
597 ..
598 })
599 )
600 };
601 let start = entries
602 .get(..index)
603 .and_then(|before| before.iter().rposition(|entry| !is_radio(entry)))
604 .map_or(0, |position| position + 1);
605 let end = entries
606 .get(index..)
607 .and_then(|after| after.iter().position(|entry| !is_radio(entry)))
608 .map_or(entries.len(), |position| index + position);
609 start..end
610}
611
612pub fn typeahead(key: &keys::Event) -> Option<char> {
615 let modifiers = key.modifiers;
616 if modifiers.control() || modifiers.alt() || modifiers.logo() {
617 return None;
618 }
619 let Key::Character(text) = &key.key else {
620 return None;
621 };
622 let mut chars = text.chars();
623 let character = chars.next()?;
624 if chars.next().is_some() || !character.is_alphanumeric() {
625 return None;
626 }
627 Some(character)
628}
629
630#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
632pub enum Action {
633 Open,
635 Next,
636 Previous,
637 First,
638 Last,
639 Activate,
640 Close,
641 OpenSubmenu,
642 CloseSubmenu,
643}
644
645impl Action {
646 pub fn event<Id: Copy + PartialEq>(self, state: &State<Id>) -> Option<Event<Id>> {
651 if !state.is_open() {
652 return (self == Action::Open).then_some(Event::First);
653 }
654 match self {
655 Action::Open => None,
656 Action::Next => Some(Event::Next),
657 Action::Previous => Some(Event::Previous),
658 Action::First => Some(Event::First),
659 Action::Last => Some(Event::Last),
660 Action::Activate => Some(Event::ActivateHighlighted),
661 Action::Close if state.depth() > 1 => Some(Event::CloseSubmenu),
662 Action::Close => Some(Event::Close),
663 Action::OpenSubmenu => Some(Event::OpenSubmenu),
664 Action::CloseSubmenu => Some(Event::CloseSubmenu),
665 }
666 }
667}
668
669impl keys::Action for Action {
670 const ALL: &'static [Self] = &[
671 Action::Open,
672 Action::Next,
673 Action::Previous,
674 Action::First,
675 Action::Last,
676 Action::Activate,
677 Action::Close,
678 Action::OpenSubmenu,
679 Action::CloseSubmenu,
680 ];
681
682 fn defaults() -> Keymap<Self> {
683 default_keymap()
684 }
685
686 fn name(self) -> &'static str {
687 match self {
688 Action::Open => "Open",
689 Action::Next => "Next",
690 Action::Previous => "Previous",
691 Action::First => "First",
692 Action::Last => "Last",
693 Action::Activate => "Activate",
694 Action::Close => "Close",
695 Action::OpenSubmenu => "OpenSubmenu",
696 Action::CloseSubmenu => "CloseSubmenu",
697 }
698 }
699
700 fn description(self) -> &'static str {
701 match self {
702 Action::Open => "Opens the menu on its first item.",
703 Action::Next => "Highlights the next enabled item, wrapping.",
704 Action::Previous => "Highlights the previous enabled item, wrapping.",
705 Action::First => "Highlights the first enabled item.",
706 Action::Last => "Highlights the last enabled item.",
707 Action::Activate => "Chooses the highlighted item or opens its submenu.",
708 Action::Close => "Closes the innermost submenu, or the menu.",
709 Action::OpenSubmenu => "Opens the highlighted submenu.",
710 Action::CloseSubmenu => "Closes the innermost submenu.",
711 }
712 }
713}
714
715pub fn default_keymap() -> Keymap<Action> {
735 Keymap::new()
736 .bind(Chord::named(Named::ArrowDown), Action::Next)
737 .bind(Chord::named(Named::ArrowUp), Action::Previous)
738 .bind(Chord::named(Named::Home), Action::First)
739 .bind(Chord::named(Named::End), Action::Last)
740 .bind(Chord::named(Named::Enter), Action::Activate)
741 .bind(Chord::named(Named::Space), Action::Activate)
742 .bind(Chord::named(Named::Escape), Action::Close)
743 .bind(Chord::named(Named::ArrowRight), Action::OpenSubmenu)
744 .bind(Chord::named(Named::ArrowLeft), Action::CloseSubmenu)
745}
746
747pub struct DropdownMenu<'a, Id, Message> {
749 state: &'a State<Id>,
750 trigger: Element<'a, Message>,
751 placement: Placement,
752 width: f32,
753 keymap: Keymap<Action>,
754 on_event: Option<Box<dyn Fn(Event<Id>) -> Message + 'a>>,
755}
756
757impl<Id: std::fmt::Debug, Message> std::fmt::Debug for DropdownMenu<'_, Id, Message> {
758 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
759 f.debug_struct("DropdownMenu")
760 .field("state", self.state)
761 .field("placement", &self.placement)
762 .field("width", &self.width)
763 .field("keymap", &self.keymap)
764 .finish_non_exhaustive()
765 }
766}
767
768pub fn dropdown_menu<'a, Id, Message>(
774 state: &'a State<Id>,
775 trigger: impl Into<Element<'a, Message>>,
776) -> DropdownMenu<'a, Id, Message> {
777 DropdownMenu {
778 state,
779 trigger: trigger.into(),
780 placement: Placement::new(Side::Bottom, Align::Start),
781 width: WIDTH,
782 keymap: default_keymap(),
783 on_event: None,
784 }
785}
786
787impl<'a, Id, Message> DropdownMenu<'a, Id, Message> {
788 pub fn side(mut self, side: Side) -> Self {
790 self.placement.side = side;
791 self
792 }
793
794 pub fn align(mut self, align: Align) -> Self {
796 self.placement.align = align;
797 self
798 }
799
800 pub fn gap(mut self, gap: f32) -> Self {
802 self.placement.gap = gap;
803 self
804 }
805
806 pub fn width(mut self, width: f32) -> Self {
808 self.width = width;
809 self
810 }
811
812 pub fn keymap(mut self, keymap: Keymap<Action>) -> Self {
814 self.keymap = keymap;
815 self
816 }
817
818 pub fn on_event(mut self, on_event: impl Fn(Event<Id>) -> Message + 'a) -> Self {
819 self.on_event = Some(Box::new(on_event));
820 self
821 }
822}
823
824impl<'a, Id, Message> From<DropdownMenu<'a, Id, Message>> for Element<'a, Message>
825where
826 Id: Copy + PartialEq + 'a,
827 Message: Clone + 'a,
828{
829 fn from(menu: DropdownMenu<'a, Id, Message>) -> Self {
830 let DropdownMenu {
831 state,
832 trigger,
833 placement,
834 width,
835 keymap,
836 on_event,
837 } = menu;
838 let on_event: Option<Rc<dyn Fn(Event<Id>) -> Message + 'a>> = on_event.map(Rc::from);
839 let panel = state
840 .is_open()
841 .then(|| panel(state, on_event.as_deref(), width));
842 let anchored = anchored(trigger)
843 .content(panel)
844 .placement(placement)
845 .dismiss_keys([]);
846 let Some(on_event) = on_event.filter(|_| state.is_open()) else {
847 return anchored.into();
848 };
849 anchored
850 .on_dismiss(on_event(Event::Close))
851 .on_key(move |key| state.key_event(&keymap, key).map(&*on_event))
852 .into()
853 }
854}
855
856pub(crate) fn panel<'a, Id, Message>(
859 state: &'a State<Id>,
860 on_event: Option<&dyn Fn(Event<Id>) -> Message>,
861 width: f32,
862) -> Element<'a, Message>
863where
864 Id: Copy + PartialEq + 'a,
865 Message: Clone + 'a,
866{
867 level(state, &state.entries, 0, on_event, width)
868}
869
870fn level<'a, Id, Message>(
871 state: &'a State<Id>,
872 entries: &'a [Entry<Id>],
873 depth: usize,
874 on_event: Option<&dyn Fn(Event<Id>) -> Message>,
875 width: f32,
876) -> Element<'a, Message>
877where
878 Id: Copy + PartialEq + 'a,
879 Message: Clone + 'a,
880{
881 let highlighted = state.highlighted_at(depth);
882 let submenu_open = state.depth() > depth + 1;
883 let indented = has_marks(entries);
884 let slot = reserves_slot(entries);
885
886 let rows = entries
887 .iter()
888 .enumerate()
889 .map(|(index, entry)| match entry {
890 Entry::Separator => menu::separator(),
891 Entry::Label(label) => menu::group_label(label, indented),
892 Entry::Item(item) => {
893 let is_highlighted = highlighted == Some(index);
894 let row = item_row(item, is_highlighted, slot, on_event);
895 let Some(children) = item.submenu().filter(|_| is_highlighted && submenu_open)
896 else {
897 return row;
898 };
899 anchored(row)
902 .content(Some(level(state, children, depth + 1, on_event, width)))
903 .placement(
904 Placement::new(Side::Right, Align::Start)
905 .gap(space::XS)
906 .offset(-(space::XS + 1.0)),
907 )
908 .pass_through(true)
909 .into()
910 }
911 });
912
913 menu::surface(column(rows).width(width)).into()
914}
915
916fn has_marks<Id>(entries: &[Entry<Id>]) -> bool {
919 entries.iter().any(|entry| {
920 matches!(
921 entry.as_item().map(|item| &item.kind),
922 Some(Kind::Checkbox(_) | Kind::Radio(_))
923 )
924 })
925}
926
927fn reserves_slot<Id>(entries: &[Entry<Id>]) -> bool {
930 has_marks(entries)
931 || entries
932 .iter()
933 .any(|entry| entry.as_item().is_some_and(|item| item.icon.is_some()))
934}
935
936fn leading<Id>(item: &Item<Id>, slot: bool) -> Leading {
939 match (&item.kind, slot) {
940 (_, false) => Leading::None,
941 (Kind::Checkbox(true), true) => Leading::Check,
942 (Kind::Radio(true), true) => Leading::Dot,
943 (_, true) => Leading::Empty,
944 }
945}
946
947fn item_row<'a, Id, Message>(
948 item: &'a Item<Id>,
949 highlighted: bool,
950 slot: bool,
951 on_event: Option<&dyn Fn(Event<Id>) -> Message>,
952) -> Element<'a, Message>
953where
954 Id: Copy + 'a,
955 Message: Clone + 'a,
956{
957 let enabled = on_event.filter(|_| item.enabled());
958 let status = match (enabled.is_some(), highlighted) {
959 (false, _) => RowStatus::Disabled,
960 (true, true) => RowStatus::Highlighted,
961 (true, false) => RowStatus::Idle,
962 };
963 let leading = leading(item, slot);
964 let trailing = if item.submenu().is_some() {
965 Trailing::Chevron
966 } else {
967 Trailing::None
968 };
969 let parts = Parts {
970 icon: item.icon,
971 hint: item.shortcut.as_deref(),
972 leading,
973 trailing,
974 ..Parts::label(&item.label)
975 };
976 let messages = enabled.map(|on_event| {
977 (
978 on_event(Event::Highlight(item.id)),
979 on_event(Event::Activate(item.id)),
980 )
981 });
982 menu::item(parts, status, item.destructive, messages)
983}
984
985#[cfg(test)]
986mod tests {
987 use super::*;
988 use iced::keyboard::Modifiers;
989 use iced::widget::text;
990
991 const COPY: u8 = 1;
992 const PASTE: u8 = 2;
993 const DELETE: u8 = 3;
994 const SHARE: u8 = 4;
995 const EMAIL: u8 = 5;
996 const LINK: u8 = 6;
997 const GRID: u8 = 7;
998 const SMALL: u8 = 8;
999 const LARGE: u8 = 9;
1000 const PREVIEW: u8 = 10;
1001
1002 fn state() -> State<u8> {
1003 State::new([
1004 group_label("Edit"),
1005 item(COPY, "Copy").shortcut("Ctrl+C"),
1006 item(PASTE, "Paste").disabled(true),
1007 item(DELETE, "Delete").destructive(true),
1008 separator(),
1009 submenu(
1010 SHARE,
1011 "Share",
1012 [item(EMAIL, "Email"), item(LINK, "Copy link")],
1013 ),
1014 checkbox_item(GRID, "Show grid", true),
1015 separator(),
1016 radio_item(SMALL, "Small", true),
1017 radio_item(LARGE, "Large", false),
1018 item(PREVIEW, "Preview"),
1019 ])
1020 }
1021
1022 fn open() -> State<u8> {
1023 let mut state = state();
1024 let _ = state.update(Event::Open);
1025 state
1026 }
1027
1028 #[test]
1029 fn builders_set_item_fields_and_ignore_labels() {
1030 let entry: Entry<u8> = item(1, "Save")
1031 .icon(crate::lucide!(Save))
1032 .shortcut("Ctrl+S")
1033 .disabled(true)
1034 .destructive(true);
1035 let item = entry.as_item().unwrap();
1036 assert_eq!(item.label, "Save");
1037 assert_eq!(item.icon, Some(crate::lucide!(Save)));
1038 assert_eq!(item.shortcut.as_deref(), Some("Ctrl+S"));
1039 assert!(item.disabled && item.destructive);
1040 assert_eq!(item.kind, Kind::Action);
1041
1042 let label: Entry<u8> = group_label("Group")
1043 .icon(crate::lucide!(Save))
1044 .disabled(true);
1045 assert_eq!(label, Entry::Label("Group".into()));
1046 assert_eq!(separator::<u8>().shortcut("X"), Entry::Separator);
1047 }
1048
1049 #[test]
1050 fn starts_closed_and_opens_without_a_highlight() {
1051 let mut state = state();
1052 assert!(!state.is_open());
1053 assert_eq!(state.depth(), 0);
1054 assert_eq!(state.update(Event::Open), None);
1055 assert!(state.is_open());
1056 assert_eq!(state.highlighted(), None);
1057 let _ = state.update(Event::Open);
1058 assert_eq!(state.depth(), 1);
1059 }
1060
1061 #[test]
1062 fn toggle_and_close() {
1063 let mut state = state();
1064 let _ = state.update(Event::Toggle);
1065 assert!(state.is_open());
1066 let _ = state.update(Event::Toggle);
1067 assert!(!state.is_open());
1068 let _ = state.update(Event::Next);
1069 let _ = state.update(Event::Close);
1070 assert!(!state.is_open());
1071 assert_eq!(state.highlighted(), None);
1072 }
1073
1074 #[test]
1075 fn next_skips_labels_separators_and_disabled_items_and_wraps() {
1076 let mut state = open();
1077 let mut seen = Vec::new();
1078 for _ in 0..8 {
1079 let _ = state.update(Event::Next);
1080 seen.push(state.highlighted().unwrap());
1081 }
1082 assert_eq!(
1083 seen,
1084 [COPY, DELETE, SHARE, GRID, SMALL, LARGE, PREVIEW, COPY]
1085 );
1086 }
1087
1088 #[test]
1089 fn previous_wraps_backwards() {
1090 let mut state = open();
1091 let _ = state.update(Event::Previous);
1092 assert_eq!(state.highlighted(), Some(PREVIEW));
1093 let _ = state.update(Event::First);
1094 let _ = state.update(Event::Previous);
1095 assert_eq!(state.highlighted(), Some(PREVIEW));
1096 }
1097
1098 #[test]
1099 fn first_and_last() {
1100 let mut state = open();
1101 let _ = state.update(Event::Last);
1102 assert_eq!(state.highlighted(), Some(PREVIEW));
1103 let _ = state.update(Event::First);
1104 assert_eq!(state.highlighted(), Some(COPY));
1105 }
1106
1107 #[test]
1108 fn moving_opens_a_closed_menu_on_an_end() {
1109 let mut down = state();
1110 let _ = down.update(Event::Next);
1111 assert!(down.is_open());
1112 assert_eq!(down.highlighted(), Some(COPY));
1113
1114 let mut up = state();
1115 let _ = up.update(Event::Previous);
1116 assert_eq!(up.highlighted(), Some(PREVIEW));
1117
1118 let mut enter = state();
1119 assert_eq!(enter.update(Event::ActivateHighlighted), None);
1120 assert_eq!(enter.highlighted(), Some(COPY));
1121 }
1122
1123 #[test]
1124 fn empty_and_all_disabled_menus_never_highlight() {
1125 let mut empty: State<u8> = State::new([]);
1126 let _ = empty.update(Event::Next);
1127 assert!(empty.is_open());
1128 assert_eq!(empty.highlighted(), None);
1129 assert_eq!(empty.update(Event::ActivateHighlighted), None);
1130
1131 let mut disabled = State::new([item(1u8, "A").disabled(true), separator()]);
1132 let _ = disabled.update(Event::Next);
1133 let _ = disabled.update(Event::Last);
1134 assert_eq!(disabled.highlighted(), None);
1135 assert_eq!(disabled.update(Event::Activate(1)), None);
1136 }
1137
1138 #[test]
1139 fn activating_an_action_returns_it_and_closes() {
1140 let mut state = open();
1141 assert_eq!(
1142 state.update(Event::Activate(COPY)),
1143 Some(Output::Activated(COPY))
1144 );
1145 assert!(!state.is_open());
1146 }
1147
1148 #[test]
1149 fn disabled_and_unknown_items_do_nothing() {
1150 let mut state = open();
1151 assert_eq!(state.update(Event::Activate(PASTE)), None);
1152 assert_eq!(state.update(Event::Activate(99)), None);
1153 let _ = state.update(Event::Highlight(PASTE));
1154 assert_eq!(state.highlighted(), None);
1155 assert!(state.is_open());
1156 }
1157
1158 #[test]
1159 fn enter_activates_the_highlighted_item() {
1160 let mut state = open();
1161 let _ = state.update(Event::Next);
1162 let _ = state.update(Event::Next);
1163 assert_eq!(
1164 state.update(Event::ActivateHighlighted),
1165 Some(Output::Activated(DELETE))
1166 );
1167 let mut nothing = open();
1168 assert_eq!(nothing.update(Event::ActivateHighlighted), None);
1169 assert!(nothing.is_open());
1170 }
1171
1172 #[test]
1173 fn checkbox_items_toggle() {
1174 let mut state = open();
1175 assert!(state.is_checked(GRID));
1176 assert_eq!(
1177 state.update(Event::Activate(GRID)),
1178 Some(Output::Toggled(GRID, false))
1179 );
1180 assert!(!state.is_checked(GRID));
1181 let _ = state.update(Event::Open);
1182 assert_eq!(
1183 state.update(Event::Activate(GRID)),
1184 Some(Output::Toggled(GRID, true))
1185 );
1186 }
1187
1188 #[test]
1189 fn radio_items_check_one_of_their_run() {
1190 let mut state = open();
1191 assert_eq!(
1192 state.update(Event::Activate(LARGE)),
1193 Some(Output::Selected(LARGE))
1194 );
1195 assert!(state.is_checked(LARGE));
1196 assert!(!state.is_checked(SMALL));
1197 state.set_checked(SMALL, true);
1198 assert!(state.is_checked(SMALL));
1199 assert!(!state.is_checked(LARGE));
1200 state.set_checked(SMALL, false);
1201 assert!(!state.is_checked(SMALL));
1202 }
1203
1204 #[test]
1205 fn radio_group_is_the_adjacent_run() {
1206 let entries: Vec<Entry<u8>> = vec![
1207 radio_item(1, "A", false),
1208 separator(),
1209 radio_item(2, "B", false),
1210 radio_item(3, "C", false),
1211 item(4, "D"),
1212 radio_item(5, "E", false),
1213 ];
1214 assert_eq!(radio_group(&entries, 0), 0..1);
1215 assert_eq!(radio_group(&entries, 2), 2..4);
1216 assert_eq!(radio_group(&entries, 3), 2..4);
1217 assert_eq!(radio_group(&entries, 5), 5..6);
1218 }
1219
1220 #[test]
1221 fn set_checked_ignores_actions_and_unknown_ids() {
1222 let mut state = state();
1223 state.set_checked(COPY, true);
1224 state.set_checked(99, true);
1225 assert!(!state.is_checked(COPY));
1226 assert_eq!(state, self::state());
1227 }
1228
1229 #[test]
1230 fn set_disabled_drops_the_highlight() {
1231 let mut state = open();
1232 let _ = state.update(Event::Highlight(DELETE));
1233 state.set_disabled(DELETE, true);
1234 assert_eq!(state.highlighted(), None);
1235 assert!(state.item(DELETE).unwrap().disabled);
1236 state.set_disabled(PASTE, false);
1237 let _ = state.update(Event::First);
1238 let _ = state.update(Event::Next);
1239 assert_eq!(state.highlighted(), Some(PASTE));
1240 }
1241
1242 #[test]
1243 fn pointer_on_a_submenu_opens_it_without_a_highlight() {
1244 let mut state = open();
1245 let _ = state.update(Event::Highlight(SHARE));
1246 assert_eq!(state.depth(), 2);
1247 assert_eq!(state.highlighted(), Some(SHARE));
1248 assert_eq!(state.highlighted_at(1), None);
1249
1250 let _ = state.update(Event::Highlight(LINK));
1251 assert_eq!(state.highlighted(), Some(LINK));
1252 let _ = state.update(Event::Highlight(SHARE));
1253 assert_eq!(
1254 state.highlighted(),
1255 Some(LINK),
1256 "returning to the parent keeps the submenu"
1257 );
1258
1259 let _ = state.update(Event::Highlight(COPY));
1260 assert_eq!(state.depth(), 1, "another item closes the submenu");
1261 }
1262
1263 #[test]
1264 fn keyboard_opens_and_closes_submenus() {
1265 let mut state = open();
1266 let _ = state.update(Event::Highlight(SHARE));
1267 let _ = state.update(Event::CloseSubmenu);
1268 assert_eq!(state.depth(), 1);
1269 assert_eq!(state.highlighted(), Some(SHARE));
1270
1271 let _ = state.update(Event::OpenSubmenu);
1272 assert_eq!(state.highlighted(), Some(EMAIL));
1273 let _ = state.update(Event::Next);
1274 assert_eq!(state.highlighted(), Some(LINK));
1275 let _ = state.update(Event::Next);
1276 assert_eq!(state.highlighted(), Some(EMAIL), "wraps inside the submenu");
1277
1278 let _ = state.update(Event::CloseSubmenu);
1279 assert_eq!(state.depth(), 1);
1280 assert_eq!(state.highlighted(), Some(SHARE));
1281 let _ = state.update(Event::CloseSubmenu);
1282 assert!(state.is_open(), "the root menu stays open");
1283 }
1284
1285 #[test]
1286 fn enter_on_a_submenu_opens_it_and_items_inside_activate() {
1287 let mut state = open();
1288 let _ = state.update(Event::Highlight(SHARE));
1289 let _ = state.update(Event::CloseSubmenu);
1290 assert_eq!(state.update(Event::ActivateHighlighted), None);
1291 assert_eq!(state.highlighted(), Some(EMAIL));
1292 assert_eq!(
1293 state.update(Event::ActivateHighlighted),
1294 Some(Output::Activated(EMAIL))
1295 );
1296 assert!(!state.is_open());
1297 }
1298
1299 #[test]
1300 fn clicking_a_submenu_item_opens_it() {
1301 let mut state = open();
1302 assert_eq!(state.update(Event::Activate(SHARE)), None);
1303 assert_eq!(state.depth(), 2);
1304 assert_eq!(
1305 state.update(Event::Activate(LINK)),
1306 Some(Output::Activated(LINK))
1307 );
1308 }
1309
1310 #[test]
1311 fn open_submenu_ignores_plain_items() {
1312 let mut state = open();
1313 let _ = state.update(Event::First);
1314 let _ = state.update(Event::OpenSubmenu);
1315 assert_eq!(state.depth(), 1);
1316 }
1317
1318 #[test]
1319 fn moving_in_the_parent_closes_the_submenu() {
1320 let mut state = open();
1321 let _ = state.update(Event::Highlight(SHARE));
1322 let _ = state.update(Event::Next);
1323 assert_eq!(state.depth(), 1);
1324 assert_eq!(state.highlighted(), Some(GRID));
1325 }
1326
1327 #[test]
1328 fn typeahead_jumps_to_matching_labels_and_cycles() {
1329 let mut state = open();
1330 let _ = state.update(Event::Typeahead('s'));
1331 assert_eq!(state.highlighted(), Some(SHARE));
1332 let _ = state.update(Event::Typeahead('S'));
1333 assert_eq!(state.highlighted(), Some(GRID));
1334 let _ = state.update(Event::Typeahead('s'));
1335 assert_eq!(state.highlighted(), Some(SMALL));
1336 let _ = state.update(Event::Typeahead('s'));
1337 assert_eq!(state.highlighted(), Some(SHARE));
1338 let _ = state.update(Event::Typeahead('p'));
1339 assert_eq!(
1340 state.highlighted(),
1341 Some(PREVIEW),
1342 "skips the disabled Paste"
1343 );
1344 let _ = state.update(Event::Typeahead('z'));
1345 assert_eq!(state.highlighted(), Some(PREVIEW));
1346 }
1347
1348 #[test]
1349 fn typeahead_starts_at_the_first_match() {
1350 let mut state = open();
1351 let _ = state.update(Event::Typeahead('c'));
1352 assert_eq!(state.highlighted(), Some(COPY));
1353 }
1354
1355 fn key(key: Key, modifiers: Modifiers) -> keys::Event {
1356 keys::Event { key, modifiers }
1357 }
1358
1359 fn character(c: &str) -> Key {
1360 Key::Character(c.into())
1361 }
1362
1363 #[test]
1364 fn typeahead_accepts_plain_letters_and_digits_only() {
1365 let none = Modifiers::empty();
1366 assert_eq!(typeahead(&key(character("a"), none)), Some('a'));
1367 assert_eq!(typeahead(&key(character("A"), Modifiers::SHIFT)), Some('A'));
1368 assert_eq!(typeahead(&key(character("7"), none)), Some('7'));
1369 assert_eq!(typeahead(&key(character("a"), Modifiers::CTRL)), None);
1370 assert_eq!(typeahead(&key(character("a"), Modifiers::ALT)), None);
1371 assert_eq!(typeahead(&key(character(" "), none)), None);
1372 assert_eq!(typeahead(&key(character("/"), none)), None);
1373 assert_eq!(typeahead(&key(character("ab"), none)), None);
1374 assert_eq!(typeahead(&key(Key::Named(Named::Enter), none)), None);
1375 }
1376
1377 #[test]
1378 fn key_event_uses_the_keymap_then_typeahead() {
1379 let keymap = default_keymap();
1380 let closed = state();
1381 let down = key(Key::Named(Named::ArrowDown), Modifiers::empty());
1382 let enter = key(Key::Named(Named::Enter), Modifiers::empty());
1383 let letter = key(character("d"), Modifiers::empty());
1384 assert_eq!(
1385 closed.key_event(&keymap, &down),
1386 None,
1387 "closed menus claim no keys"
1388 );
1389 assert_eq!(closed.key_event(&keymap, &enter), None);
1390 assert_eq!(closed.key_event(&keymap, &letter), None);
1391
1392 let bound = default_keymap().bind(Chord::named(Named::F2), Action::Open);
1393 let f2 = key(Key::Named(Named::F2), Modifiers::empty());
1394 assert_eq!(closed.key_event(&bound, &f2), Some(Event::First));
1395
1396 let open = open();
1397 assert_eq!(
1398 open.key_event(&keymap, &letter),
1399 Some(Event::Typeahead('d'))
1400 );
1401 let escape = key(Key::Named(Named::Escape), Modifiers::empty());
1402 assert_eq!(open.key_event(&keymap, &escape), Some(Event::Close));
1403 }
1404
1405 fn press(keymap: &Keymap<Action>, chord: &str) -> Option<Action> {
1406 let chord: Chord = chord.parse().unwrap();
1407 keymap.resolve(chord.key(), chord.modifiers())
1408 }
1409
1410 #[test]
1411 fn default_keymap_covers_every_action() {
1412 let keymap = default_keymap();
1413 assert_eq!(press(&keymap, "ArrowDown"), Some(Action::Next));
1414 assert_eq!(press(&keymap, "ArrowUp"), Some(Action::Previous));
1415 assert_eq!(press(&keymap, "Home"), Some(Action::First));
1416 assert_eq!(press(&keymap, "End"), Some(Action::Last));
1417 assert_eq!(press(&keymap, "Enter"), Some(Action::Activate));
1418 assert_eq!(press(&keymap, "Space"), Some(Action::Activate));
1419 assert_eq!(press(&keymap, "Escape"), Some(Action::Close));
1420 assert_eq!(press(&keymap, "ArrowRight"), Some(Action::OpenSubmenu));
1421 assert_eq!(press(&keymap, "ArrowLeft"), Some(Action::CloseSubmenu));
1422 assert_eq!(press(&keymap, "Tab"), None);
1423 for action in <Action as keys::Action>::ALL {
1424 let bound = !keymap.chords(action).is_empty();
1425 assert_eq!(bound, *action != Action::Open, "{action:?}");
1426 }
1427 assert_eq!(Keymap::<Action>::defaults(), keymap);
1428 }
1429
1430 #[test]
1431 fn keymap_overrides_and_unbinding() {
1432 let keymap = default_keymap()
1433 .unbind(&"Space".parse().unwrap())
1434 .bind("J".parse().unwrap(), Action::Next);
1435 assert_eq!(press(&keymap, "Space"), None);
1436 assert_eq!(press(&keymap, "j"), Some(Action::Next));
1437 assert_eq!(press(&keymap, "Enter"), Some(Action::Activate));
1438 }
1439
1440 #[test]
1441 fn a_closed_menu_only_answers_open() {
1442 let closed = state();
1443 assert_eq!(Action::Open.event(&closed), Some(Event::First));
1444 for &action in <Action as keys::Action>::ALL {
1445 if action != Action::Open {
1446 assert_eq!(action.event(&closed), None, "{action:?}");
1447 }
1448 }
1449 }
1450
1451 #[test]
1452 fn an_open_menu_maps_every_action_but_open() {
1453 let open = open();
1454 assert_eq!(Action::Open.event(&open), None);
1455 assert_eq!(Action::Next.event(&open), Some(Event::Next));
1456 assert_eq!(Action::Previous.event(&open), Some(Event::Previous));
1457 assert_eq!(Action::First.event(&open), Some(Event::First));
1458 assert_eq!(Action::Last.event(&open), Some(Event::Last));
1459 assert_eq!(
1460 Action::Activate.event(&open),
1461 Some(Event::ActivateHighlighted)
1462 );
1463 assert_eq!(Action::Close.event(&open), Some(Event::Close));
1464 assert_eq!(Action::OpenSubmenu.event(&open), Some(Event::OpenSubmenu));
1465 assert_eq!(Action::CloseSubmenu.event(&open), Some(Event::CloseSubmenu));
1466 }
1467
1468 #[test]
1469 fn close_in_a_submenu_closes_only_the_submenu() {
1470 let mut state = open();
1471 let _ = state.update(Event::Highlight(SHARE));
1472 assert_eq!(Action::Close.event(&state), Some(Event::CloseSubmenu));
1473 let _ = state.update(Event::CloseSubmenu);
1474 assert_eq!(Action::Close.event(&state), Some(Event::Close));
1475 }
1476
1477 #[test]
1478 fn every_action_has_a_name_and_a_sentence() {
1479 for &action in <Action as keys::Action>::ALL {
1480 assert!(!keys::Action::name(action).is_empty());
1481 assert!(keys::Action::description(action).ends_with('.'));
1482 }
1483 }
1484
1485 #[test]
1486 fn builder_defaults() {
1487 let state = state();
1488 let menu: DropdownMenu<'_, u8, ()> = dropdown_menu(&state, text("Open"));
1489 assert_eq!(menu.placement, Placement::new(Side::Bottom, Align::Start));
1490 assert_eq!(menu.width, WIDTH);
1491 assert_eq!(menu.keymap, default_keymap());
1492 assert!(menu.on_event.is_none());
1493
1494 let menu: DropdownMenu<'_, u8, ()> = dropdown_menu(&state, text("Open"))
1495 .side(Side::Top)
1496 .align(Align::End)
1497 .gap(8.0)
1498 .width(300.0)
1499 .keymap(Keymap::new())
1500 .on_event(|_| ());
1501 assert_eq!(menu.placement.side, Side::Top);
1502 assert_eq!(menu.placement.align, Align::End);
1503 assert_eq!(menu.placement.gap, 8.0);
1504 assert_eq!(menu.width, 300.0);
1505 assert!(menu.keymap.is_empty());
1506 assert!(menu.on_event.is_some());
1507 }
1508
1509 #[test]
1510 fn a_slot_is_reserved_for_marks_or_icons() {
1511 let plain: Vec<Entry<u8>> = vec![item(COPY, "Copy"), separator()];
1512 assert!(!reserves_slot(&plain) && !has_marks(&plain));
1513
1514 let icons: Vec<Entry<u8>> = vec![
1515 item(COPY, "Copy").icon(crate::lucide!(Copy)),
1516 item(PASTE, "Paste"),
1517 ];
1518 assert!(reserves_slot(&icons) && !has_marks(&icons));
1519
1520 let marks: Vec<Entry<u8>> = vec![item(COPY, "Copy"), radio_item(SMALL, "Small", false)];
1521 assert!(reserves_slot(&marks) && has_marks(&marks));
1522 }
1523
1524 #[test]
1525 fn the_slot_holds_the_mark_while_checked_and_the_icon_otherwise() {
1526 let entries: Vec<Entry<u8>> = vec![
1527 checkbox_item(GRID, "Grid", true).icon(crate::lucide!(Grid3x3)),
1528 checkbox_item(GRID, "Grid", false).icon(crate::lucide!(Grid3x3)),
1529 radio_item(SMALL, "Small", true),
1530 radio_item(LARGE, "Large", false),
1531 item(COPY, "Copy").icon(crate::lucide!(Copy)),
1532 ];
1533 let slots: Vec<Leading> = entries
1534 .iter()
1535 .filter_map(Entry::as_item)
1536 .map(|item| leading(item, true))
1537 .collect();
1538 assert_eq!(
1539 slots,
1540 [
1541 Leading::Check,
1542 Leading::Empty,
1543 Leading::Dot,
1544 Leading::Empty,
1545 Leading::Empty
1546 ]
1547 );
1548 let unslotted = entries.iter().filter_map(Entry::as_item);
1549 assert!(
1550 unslotted
1551 .map(|item| leading(item, false))
1552 .all(|l| l == Leading::None)
1553 );
1554 }
1555}