1use std::collections::{HashMap, HashSet};
20use std::hash::Hash;
21use std::ops::Range;
22use std::rc::Rc;
23
24use iced::futures::channel::mpsc;
25use iced::futures::{Stream, StreamExt, stream};
26use iced::keyboard::key::Named;
27use iced::keyboard::{Key, Modifiers};
28use iced::widget::{Space, column, container, mouse_area, row, text};
29use iced::{Alignment, Background, Border, Color, Element, Length, Subscription, Theme, mouse};
30
31use crate::data::ellipsis::ellipsis;
32use crate::data::interaction::{Look, RowMenu, pressable, ringed, row_menu, scope};
33use crate::data::rows::rows;
34use crate::feedback::badge::{self, badge};
35use crate::icon::{Glyph, opacity, themed};
36use crate::keys::{self, Chord, Keymap};
37use crate::overlay::context_menu;
38use crate::overlay::menu::{self, ICON_SIZE, ROW_PADDING, RowStatus};
39use crate::primitives::checkbox::{CheckState, checkbox};
40use crate::theme::{Tokens, fade, radius, space, text_size};
41
42pub const ROW_HEIGHT: f32 = 32.0;
44pub const INDENT: f32 = 20.0;
46pub const CHANNEL_CAPACITY: usize = 64;
48pub const BATCH_SIZE: usize = 32;
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
53pub enum Mode {
54 None,
56 #[default]
58 Single,
59 Multiple,
61 Checkbox,
64}
65
66impl Mode {
67 pub const ALL: [Mode; 4] = [Mode::None, Mode::Single, Mode::Multiple, Mode::Checkbox];
68}
69
70#[derive(Debug, Clone, PartialEq, Eq)]
72pub enum Trailing {
73 Badge(String),
75 Text(String),
77}
78
79#[derive(Debug, Clone, PartialEq)]
80enum Kids<Id> {
81 None,
82 Nodes(Vec<Node<Id>>),
83 Lazy,
84}
85
86#[derive(Debug, Clone, PartialEq)]
89pub struct Node<Id> {
90 pub id: Id,
91 pub label: String,
92 pub icon: Option<Glyph>,
93 pub open_icon: Option<Glyph>,
95 pub trailing: Option<Trailing>,
96 pub disabled: bool,
97 kids: Kids<Id>,
98}
99
100pub fn node<Id>(id: Id, label: impl Into<String>) -> Node<Id> {
102 Node {
103 id,
104 label: label.into(),
105 icon: None,
106 open_icon: None,
107 trailing: None,
108 disabled: false,
109 kids: Kids::None,
110 }
111}
112
113#[derive(Debug, Clone, PartialEq)]
115pub struct Loaded<Id> {
116 pub parent: Id,
117 pub children: Vec<Node<Id>>,
118}
119
120pub fn loaded<Id>(parent: Id, children: impl IntoIterator<Item = Node<Id>>) -> Loaded<Id> {
122 Loaded {
123 parent,
124 children: children.into_iter().collect(),
125 }
126}
127
128impl<Id> Node<Id> {
129 pub fn icon(mut self, glyph: Glyph) -> Self {
130 self.icon = Some(glyph);
131 self
132 }
133
134 pub fn icons(mut self, closed: Glyph, open: Glyph) -> Self {
136 self.icon = Some(closed);
137 self.open_icon = Some(open);
138 self
139 }
140
141 pub fn folder(self) -> Self {
143 self.icons(crate::lucide!(Folder), crate::lucide!(FolderOpen))
144 }
145
146 pub fn file(self) -> Self {
148 self.icon(crate::lucide!(File))
149 }
150
151 pub fn badge(mut self, label: impl Into<String>) -> Self {
153 self.trailing = Some(Trailing::Badge(label.into()));
154 self
155 }
156
157 pub fn trailing(mut self, label: impl Into<String>) -> Self {
159 self.trailing = Some(Trailing::Text(label.into()));
160 self
161 }
162
163 pub fn disabled(mut self, disabled: bool) -> Self {
166 self.disabled = disabled;
167 self
168 }
169
170 pub fn children(mut self, children: impl IntoIterator<Item = Node<Id>>) -> Self {
171 self.kids = Kids::Nodes(children.into_iter().collect());
172 self
173 }
174
175 pub fn lazy(mut self) -> Self {
177 self.kids = Kids::Lazy;
178 self
179 }
180}
181
182#[derive(Debug, Clone, PartialEq)]
184enum Children<Id> {
185 None,
186 Loaded(Vec<Id>),
187 Unloaded,
188 Loading,
189}
190
191#[derive(Debug, Clone)]
192struct Slot<Id> {
193 node: Node<Id>,
194 parent: Option<Id>,
195 children: Children<Id>,
196}
197
198#[derive(Debug, Clone, Copy, PartialEq, Eq)]
200enum Line<Id> {
201 Node {
202 id: Id,
203 depth: usize,
204 },
205 Loading {
207 depth: usize,
208 },
209}
210
211impl<Id: PartialEq> Line<Id> {
212 fn is(&self, id: &Id) -> bool {
213 matches!(self, Line::Node { id: line, .. } if line == id)
214 }
215}
216
217pub type Sender<Id> = mpsc::Sender<Loaded<Id>>;
220
221#[derive(Debug, Clone)]
223pub enum Event<Id> {
224 Ready(Sender<Id>),
226 Received(Vec<Loaded<Id>>),
228 Toggle(Id),
230 Expand(Id),
231 Collapse(Id),
232 Press(Id, Modifiers),
234 Activate(Id),
236 Check(Id, bool),
238 Next,
239 Previous,
240 First,
241 Last,
242 ExtendNext,
245 ExtendPrevious,
246 ExpandOrEnter,
249 CollapseOrLeave,
252 ExpandSiblings,
254 ToggleHighlighted,
256 ActivateHighlighted,
257 SelectAll,
259 Typeahead(char),
262 Focus(bool),
264}
265
266#[derive(Debug, Clone)]
268pub enum Output<Id> {
269 Ready(Sender<Id>),
270 Load(Vec<Id>),
273 Activated(Id),
275 Selected(Vec<Id>),
277 Checked(Vec<Id>),
279}
280
281#[derive(Debug, Clone)]
283pub struct State<Id> {
284 roots: Vec<Id>,
285 slots: HashMap<Id, Slot<Id>>,
286 mode: Mode,
287 expanded: HashSet<Id>,
288 selected: HashSet<Id>,
289 checked: HashSet<Id>,
290 partial: HashSet<Id>,
292 highlighted: Option<Id>,
293 anchor: Option<Id>,
295 focused: bool,
296}
297
298impl<Id: Copy + Eq + Hash> State<Id> {
299 pub fn new(roots: impl IntoIterator<Item = Node<Id>>) -> Self {
302 let mut state = Self {
303 roots: Vec::new(),
304 slots: HashMap::new(),
305 mode: Mode::default(),
306 expanded: HashSet::new(),
307 selected: HashSet::new(),
308 checked: HashSet::new(),
309 partial: HashSet::new(),
310 highlighted: None,
311 anchor: None,
312 focused: false,
313 };
314 state.roots = roots
315 .into_iter()
316 .map(|node| state.insert(node, None))
317 .collect();
318 state
319 }
320
321 pub fn with_mode(mut self, mode: Mode) -> Self {
322 self.mode = mode;
323 self
324 }
325
326 pub fn with_expanded(mut self, ids: impl IntoIterator<Item = Id>) -> Self {
329 for id in ids {
330 if self.is_loaded(id) && self.has_children(id) {
331 let _ = self.expanded.insert(id);
332 }
333 }
334 self
335 }
336
337 pub fn mode(&self) -> Mode {
338 self.mode
339 }
340
341 pub fn roots(&self) -> &[Id] {
342 &self.roots
343 }
344
345 pub fn node(&self, id: Id) -> Option<&Node<Id>> {
346 self.slots.get(&id).map(|slot| &slot.node)
347 }
348
349 pub fn parent(&self, id: Id) -> Option<Id> {
350 self.slots.get(&id).and_then(|slot| slot.parent)
351 }
352
353 pub fn children(&self, id: Id) -> &[Id] {
356 match self.slots.get(&id).map(|slot| &slot.children) {
357 Some(Children::Loaded(children)) => children,
358 _ => &[],
359 }
360 }
361
362 pub fn has_children(&self, id: Id) -> bool {
364 match self.slots.get(&id).map(|slot| &slot.children) {
365 Some(Children::Loaded(children)) => !children.is_empty(),
366 Some(Children::Unloaded | Children::Loading) => true,
367 Some(Children::None) | None => false,
368 }
369 }
370
371 pub fn is_loaded(&self, id: Id) -> bool {
373 matches!(
374 self.slots.get(&id).map(|slot| &slot.children),
375 Some(Children::None | Children::Loaded(_))
376 )
377 }
378
379 pub fn is_loading(&self, id: Id) -> bool {
380 matches!(
381 self.slots.get(&id).map(|slot| &slot.children),
382 Some(Children::Loading)
383 )
384 }
385
386 pub fn set_loading(&mut self, id: Id) {
389 if let Some(slot) = self.slots.get_mut(&id)
390 && !matches!(slot.children, Children::Loaded(_))
391 {
392 slot.children = Children::Loading;
393 }
394 }
395
396 pub fn is_expanded(&self, id: Id) -> bool {
397 self.expanded.contains(&id)
398 }
399
400 pub fn is_selected(&self, id: Id) -> bool {
401 self.selected.contains(&id)
402 }
403
404 pub fn is_focused(&self) -> bool {
405 self.focused
406 }
407
408 pub fn highlighted(&self) -> Option<Id> {
410 self.highlighted
411 }
412
413 pub fn selected(&self) -> Vec<Id> {
415 self.walk(false)
416 .into_iter()
417 .map(|(id, _)| id)
418 .filter(|id| self.selected.contains(id))
419 .collect()
420 }
421
422 pub fn checked(&self) -> Vec<Id> {
424 self.walk(false)
425 .into_iter()
426 .map(|(id, _)| id)
427 .filter(|id| self.checked.contains(id))
428 .collect()
429 }
430
431 pub fn check_state(&self, id: Id) -> CheckState {
434 if self.checked.contains(&id) {
435 CheckState::Checked
436 } else if self.partial.contains(&id) {
437 CheckState::Indeterminate
438 } else {
439 CheckState::Unchecked
440 }
441 }
442
443 pub fn visible(&self) -> Vec<Id> {
446 self.walk(true).into_iter().map(|(id, _)| id).collect()
447 }
448
449 pub fn len(&self) -> usize {
451 self.slots.len()
452 }
453
454 pub fn is_empty(&self) -> bool {
455 self.slots.is_empty()
456 }
457
458 pub fn update(&mut self, event: Event<Id>) -> Option<Output<Id>> {
460 match event {
461 Event::Ready(sender) => return Some(Output::Ready(sender)),
462 Event::Received(batch) => {
463 let mut changed = false;
464 for loaded in batch {
465 changed |= self.load(loaded);
466 }
467 return changed.then(|| Output::Checked(self.checked()));
468 }
469 Event::Toggle(id) if self.is_expanded(id) => self.collapse(id),
470 Event::Toggle(id) | Event::Expand(id) => return self.expand([id]),
471 Event::Collapse(id) => self.collapse(id),
472 Event::Press(id, modifiers) => return self.press(id, modifiers),
473 Event::Activate(id) if self.is_enabled(id) => {
474 self.highlighted = Some(id);
475 return Some(Output::Activated(id));
476 }
477 Event::Activate(_) => {}
478 Event::Check(id, checked) => return self.check(id, checked),
479 Event::Next => self.step(Step::Next),
480 Event::Previous => self.step(Step::Previous),
481 Event::First => self.step(Step::First),
482 Event::Last => self.step(Step::Last),
483 Event::ExtendNext => return self.extend(Step::Next),
484 Event::ExtendPrevious => return self.extend(Step::Previous),
485 Event::ExpandOrEnter => return self.expand_or_enter(),
486 Event::CollapseOrLeave => self.collapse_or_leave(),
487 Event::ExpandSiblings => return self.expand_siblings(),
488 Event::ToggleHighlighted => return self.toggle_highlighted(),
489 Event::ActivateHighlighted => {
490 return self.highlighted.map(Output::Activated);
491 }
492 Event::SelectAll => return self.select_all(),
493 Event::Typeahead(character) => self.typeahead(character),
494 Event::Focus(focused) => {
495 self.focused = focused;
496 if focused && self.highlighted.is_none() {
499 let nodes = self.navigable();
500 self.highlighted = nodes
501 .iter()
502 .copied()
503 .find(|id| self.selected.contains(id))
504 .or(nodes.first().copied());
505 }
506 }
507 }
508 None
509 }
510
511 pub fn key_event(&self, keymap: &Keymap<Action>, key: &keys::Event) -> Option<Event<Id>> {
515 let action = keymap.resolve_event(key).or_else(|| {
516 let symbol = matches!(&key.key, Key::Character(text) if !text.chars().any(char::is_alphanumeric));
517 (symbol && key.modifiers.shift())
518 .then(|| {
519 keymap.resolve(&key.key, key.modifiers.difference(Modifiers::SHIFT))
520 })
521 .flatten()
522 });
523 if let Some(action) = action {
524 return action.event(self);
525 }
526 typeahead(key).map(Event::Typeahead)
527 }
528
529 fn insert(&mut self, mut node: Node<Id>, parent: Option<Id>) -> Id {
530 let id = node.id;
531 let children = match std::mem::replace(&mut node.kids, Kids::None) {
532 Kids::None => Children::None,
533 Kids::Lazy => Children::Unloaded,
534 Kids::Nodes(nodes) => Children::Loaded(
535 nodes
536 .into_iter()
537 .map(|child| self.insert(child, Some(id)))
538 .collect(),
539 ),
540 };
541 let _ = self.slots.insert(
542 id,
543 Slot {
544 node,
545 parent,
546 children,
547 },
548 );
549 id
550 }
551
552 fn is_enabled(&self, id: Id) -> bool {
553 self.slots.get(&id).is_some_and(|slot| !slot.node.disabled)
554 }
555
556 fn walk(&self, visible: bool) -> Vec<(Id, usize)> {
559 let mut out = Vec::new();
560 let mut stack: Vec<(Id, usize)> = self.roots.iter().rev().map(|&id| (id, 0)).collect();
561 while let Some((id, depth)) = stack.pop() {
562 out.push((id, depth));
563 if visible && !self.expanded.contains(&id) {
564 continue;
565 }
566 stack.extend(
567 self.children(id)
568 .iter()
569 .rev()
570 .map(|&child| (child, depth + 1)),
571 );
572 }
573 out
574 }
575
576 fn lines(&self) -> Vec<Line<Id>> {
579 let mut lines = Vec::new();
580 for (id, depth) in self.walk(true) {
581 lines.push(Line::Node { id, depth });
582 if self.is_expanded(id) && self.is_loading(id) {
583 lines.push(Line::Loading { depth: depth + 1 });
584 }
585 }
586 lines
587 }
588
589 fn navigable(&self) -> Vec<Id> {
591 self.walk(true)
592 .into_iter()
593 .map(|(id, _)| id)
594 .filter(|&id| self.is_enabled(id))
595 .collect()
596 }
597
598 fn step(&mut self, step: Step) {
599 let nodes = self.navigable();
600 let current = self
601 .highlighted
602 .and_then(|id| nodes.iter().position(|&node| node == id));
603 let target = match (step, current) {
604 (Step::First, _) | (Step::Next, None) => nodes.first(),
605 (Step::Last, _) | (Step::Previous, None) => nodes.last(),
606 (Step::Next, Some(index)) => nodes.get(index + 1).or(nodes.get(index)),
607 (Step::Previous, Some(index)) => nodes.get(index.saturating_sub(1)),
608 };
609 if let Some(&target) = target {
610 self.highlighted = Some(target);
611 }
612 }
613
614 fn expand(&mut self, ids: impl IntoIterator<Item = Id>) -> Option<Output<Id>> {
615 let mut load = Vec::new();
616 for id in ids {
617 if !self.is_enabled(id) || !self.has_children(id) {
618 continue;
619 }
620 let _ = self.expanded.insert(id);
621 let Some(slot) = self.slots.get_mut(&id) else {
622 continue;
623 };
624 if slot.children == Children::Unloaded {
625 slot.children = Children::Loading;
626 load.push(id);
627 }
628 }
629 (!load.is_empty()).then_some(Output::Load(load))
630 }
631
632 fn collapse(&mut self, id: Id) {
633 if !self.expanded.remove(&id) {
634 return;
635 }
636 let hidden = self
638 .highlighted
639 .is_some_and(|highlighted| self.is_ancestor(id, highlighted));
640 if hidden {
641 self.highlighted = Some(id);
642 }
643 }
644
645 fn is_ancestor(&self, ancestor: Id, id: Id) -> bool {
646 let mut current = self.parent(id);
647 while let Some(parent) = current {
648 if parent == ancestor {
649 return true;
650 }
651 current = self.parent(parent);
652 }
653 false
654 }
655
656 fn expand_or_enter(&mut self) -> Option<Output<Id>> {
657 let id = self.highlighted?;
658 if !self.is_expanded(id) {
659 return self.expand([id]);
660 }
661 let first = self
662 .children(id)
663 .iter()
664 .copied()
665 .find(|&child| self.is_enabled(child));
666 if let Some(first) = first {
667 self.highlighted = Some(first);
668 }
669 None
670 }
671
672 fn collapse_or_leave(&mut self) {
673 let Some(id) = self.highlighted else {
674 return;
675 };
676 if self.is_expanded(id) {
677 self.collapse(id);
678 return;
679 }
680 if let Some(parent) = self.parent(id).filter(|&parent| self.is_enabled(parent)) {
681 self.highlighted = Some(parent);
682 }
683 }
684
685 fn expand_siblings(&mut self) -> Option<Output<Id>> {
686 let id = self.highlighted?;
687 let siblings = match self.parent(id) {
688 Some(parent) => self.children(parent).to_vec(),
689 None => self.roots.clone(),
690 };
691 self.expand(siblings)
692 }
693
694 fn selection_output(&self, before: &HashSet<Id>) -> Option<Output<Id>> {
695 (self.selected != *before).then(|| Output::Selected(self.selected()))
696 }
697
698 fn press(&mut self, id: Id, modifiers: Modifiers) -> Option<Output<Id>> {
699 if !self.is_enabled(id) {
700 return None;
701 }
702 self.highlighted = Some(id);
703 match self.mode {
704 Mode::None => None,
705 Mode::Single => {
706 let before = std::mem::replace(&mut self.selected, HashSet::from([id]));
707 self.anchor = Some(id);
708 self.selection_output(&before)
709 }
710 Mode::Checkbox => self.check(id, !self.checked.contains(&id)),
711 Mode::Multiple => {
712 let before = self.selected.clone();
713 let anchor = self
714 .anchor
715 .filter(|&anchor| self.navigable().contains(&anchor));
716 match (modifiers.shift(), modifiers.command(), anchor) {
717 (true, additive, Some(anchor)) => {
718 if !additive {
719 self.selected.clear();
720 }
721 self.selected.extend(self.range(anchor, id));
722 }
723 (_, true, _) => {
724 if !self.selected.remove(&id) {
725 let _ = self.selected.insert(id);
726 }
727 self.anchor = Some(id);
728 }
729 _ => {
730 self.selected = HashSet::from([id]);
731 self.anchor = Some(id);
732 }
733 }
734 self.selection_output(&before)
735 }
736 }
737 }
738
739 fn range(&self, a: Id, b: Id) -> Vec<Id> {
741 let nodes = self.navigable();
742 let (Some(start), Some(end)) = (
743 nodes.iter().position(|&id| id == a),
744 nodes.iter().position(|&id| id == b),
745 ) else {
746 return vec![b];
747 };
748 let span = if start <= end {
749 start..=end
750 } else {
751 end..=start
752 };
753 nodes.get(span).map(<[Id]>::to_vec).unwrap_or_default()
754 }
755
756 fn extend(&mut self, step: Step) -> Option<Output<Id>> {
757 if self.mode != Mode::Multiple {
758 self.step(step);
759 return None;
760 }
761 let anchor = self.anchor.or(self.highlighted);
762 self.step(step);
763 let (Some(anchor), Some(highlighted)) = (anchor, self.highlighted) else {
764 return None;
765 };
766 self.anchor = Some(anchor);
767 let before = self.selected.clone();
768 self.selected = self.range(anchor, highlighted).into_iter().collect();
769 self.selection_output(&before)
770 }
771
772 fn toggle_highlighted(&mut self) -> Option<Output<Id>> {
773 let id = self.highlighted?;
774 match self.mode {
775 Mode::None => None,
776 Mode::Checkbox => self.check(id, !self.checked.contains(&id)),
777 Mode::Single => self.press(id, Modifiers::empty()),
778 Mode::Multiple => self.press(id, Modifiers::COMMAND),
779 }
780 }
781
782 fn select_all(&mut self) -> Option<Output<Id>> {
783 if self.mode != Mode::Multiple {
784 return None;
785 }
786 let before = self.selected.clone();
787 self.selected = self.navigable().into_iter().collect();
788 self.selection_output(&before)
789 }
790
791 fn typeahead(&mut self, character: char) {
792 let wanted: String = character.to_lowercase().collect();
793 let nodes = self.navigable();
794 let start = self
795 .highlighted
796 .and_then(|id| nodes.iter().position(|&node| node == id))
797 .map_or(0, |index| index + 1);
798 let found = (0..nodes.len())
799 .filter_map(|offset| nodes.get((start + offset) % nodes.len()))
800 .copied()
801 .find(|&id| {
802 self.node(id)
803 .is_some_and(|node| node.label.to_lowercase().starts_with(&wanted))
804 });
805 if let Some(found) = found {
806 self.highlighted = Some(found);
807 }
808 }
809
810 fn check(&mut self, id: Id, checked: bool) -> Option<Output<Id>> {
811 if self.mode != Mode::Checkbox || !self.is_enabled(id) {
812 return None;
813 }
814 let before = self.checked.clone();
815 self.set_subtree(id, checked);
816 self.refresh(self.parent(id));
817 (self.checked != before).then(|| Output::Checked(self.checked()))
818 }
819
820 fn set_subtree(&mut self, id: Id, checked: bool) {
823 let mut stack = vec![id];
824 while let Some(current) = stack.pop() {
825 if current != id && !self.is_enabled(current) {
826 continue;
827 }
828 let _ = self.partial.remove(¤t);
829 if checked {
830 let _ = self.checked.insert(current);
831 } else {
832 let _ = self.checked.remove(¤t);
833 }
834 stack.extend(self.children(current).iter().copied());
835 }
836 }
837
838 fn refresh(&mut self, start: Option<Id>) {
841 let mut current = start;
842 while let Some(parent) = current {
843 let children: Vec<Id> = self
844 .children(parent)
845 .iter()
846 .copied()
847 .filter(|&child| self.is_enabled(child))
848 .collect();
849 if !children.is_empty() {
850 let all = children.iter().all(|child| self.checked.contains(child));
851 let some = children
852 .iter()
853 .any(|child| self.checked.contains(child) || self.partial.contains(child));
854 if all {
855 let _ = self.checked.insert(parent);
856 } else {
857 let _ = self.checked.remove(&parent);
858 }
859 if some && !all {
860 let _ = self.partial.insert(parent);
861 } else {
862 let _ = self.partial.remove(&parent);
863 }
864 } else {
865 let _ = self.partial.remove(&parent);
866 }
867 current = self.parent(parent);
868 }
869 }
870
871 fn load(&mut self, loaded: Loaded<Id>) -> bool {
874 let parent = loaded.parent;
875 if !self.slots.contains_key(&parent) {
876 return false;
877 }
878 let checked_before = self.checked.clone();
879 for child in self.children(parent).to_vec() {
880 self.remove(child);
881 }
882 let children: Vec<Id> = loaded
883 .children
884 .into_iter()
885 .map(|child| self.insert(child, Some(parent)))
886 .collect();
887 if let Some(slot) = self.slots.get_mut(&parent) {
888 slot.children = Children::Loaded(children.clone());
889 }
890 if children.is_empty() {
891 let _ = self.expanded.remove(&parent);
892 }
893 if self.mode != Mode::Checkbox {
894 return false;
895 }
896 if self.checked.contains(&parent) {
897 for child in children {
898 if self.is_enabled(child) {
899 self.set_subtree(child, true);
900 }
901 }
902 }
903 self.refresh(Some(parent));
904 self.checked != checked_before
905 }
906
907 fn remove(&mut self, id: Id) {
909 for child in self.children(id).to_vec() {
910 self.remove(child);
911 }
912 let _ = self.slots.remove(&id);
913 let _ = self.expanded.remove(&id);
914 let _ = self.selected.remove(&id);
915 let _ = self.checked.remove(&id);
916 let _ = self.partial.remove(&id);
917 if self.highlighted == Some(id) {
918 self.highlighted = None;
919 }
920 if self.anchor == Some(id) {
921 self.anchor = None;
922 }
923 }
924}
925
926#[derive(Debug, Clone, Copy, PartialEq, Eq)]
927enum Step {
928 Next,
929 Previous,
930 First,
931 Last,
932}
933
934fn typeahead(key: &keys::Event) -> Option<char> {
937 let modifiers = key.modifiers;
938 if modifiers.control() || modifiers.alt() || modifiers.logo() {
939 return None;
940 }
941 let Key::Character(text) = &key.key else {
942 return None;
943 };
944 let mut chars = text.chars();
945 let character = chars.next()?;
946 if chars.next().is_some() || !character.is_alphanumeric() {
947 return None;
948 }
949 Some(character)
950}
951
952#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
954pub enum Action {
955 Next,
956 Previous,
957 First,
958 Last,
959 Expand,
960 Collapse,
961 Activate,
962 Select,
963 ExpandSiblings,
964 ExtendNext,
965 ExtendPrevious,
966 SelectAll,
967}
968
969impl Action {
970 pub fn event<Id: Copy + Eq + Hash>(self, state: &State<Id>) -> Option<Event<Id>> {
973 let highlighted = state.highlighted().is_some();
974 let multiple = state.mode() == Mode::Multiple;
975 match self {
976 Action::Next => Some(Event::Next),
977 Action::Previous => Some(Event::Previous),
978 Action::First => Some(Event::First),
979 Action::Last => Some(Event::Last),
980 Action::Expand => highlighted.then_some(Event::ExpandOrEnter),
981 Action::Collapse => highlighted.then_some(Event::CollapseOrLeave),
982 Action::Activate => highlighted.then_some(Event::ActivateHighlighted),
983 Action::Select => {
984 (highlighted && state.mode() != Mode::None).then_some(Event::ToggleHighlighted)
985 }
986 Action::ExpandSiblings => highlighted.then_some(Event::ExpandSiblings),
987 Action::ExtendNext if multiple => Some(Event::ExtendNext),
988 Action::ExtendNext => Some(Event::Next),
989 Action::ExtendPrevious if multiple => Some(Event::ExtendPrevious),
990 Action::ExtendPrevious => Some(Event::Previous),
991 Action::SelectAll => multiple.then_some(Event::SelectAll),
992 }
993 }
994}
995
996impl keys::Action for Action {
997 const ALL: &'static [Self] = &[
998 Action::Next,
999 Action::Previous,
1000 Action::First,
1001 Action::Last,
1002 Action::Expand,
1003 Action::Collapse,
1004 Action::Activate,
1005 Action::Select,
1006 Action::ExpandSiblings,
1007 Action::ExtendNext,
1008 Action::ExtendPrevious,
1009 Action::SelectAll,
1010 ];
1011
1012 fn defaults() -> Keymap<Self> {
1013 default_keymap()
1014 }
1015
1016 fn name(self) -> &'static str {
1017 match self {
1018 Action::Next => "Next",
1019 Action::Previous => "Previous",
1020 Action::First => "First",
1021 Action::Last => "Last",
1022 Action::Expand => "Expand",
1023 Action::Collapse => "Collapse",
1024 Action::Activate => "Activate",
1025 Action::Select => "Select",
1026 Action::ExpandSiblings => "ExpandSiblings",
1027 Action::ExtendNext => "ExtendNext",
1028 Action::ExtendPrevious => "ExtendPrevious",
1029 Action::SelectAll => "SelectAll",
1030 }
1031 }
1032
1033 fn description(self) -> &'static str {
1034 match self {
1035 Action::Next => "Highlights the next visible node.",
1036 Action::Previous => "Highlights the previous visible node.",
1037 Action::First => "Highlights the first node.",
1038 Action::Last => "Highlights the last visible node.",
1039 Action::Expand => {
1040 "Expands the highlighted node, or moves to its first child when it is open."
1041 }
1042 Action::Collapse => {
1043 "Collapses the highlighted node, or moves to its parent when it is closed."
1044 }
1045 Action::Activate => "Activates the highlighted node.",
1046 Action::Select => "Selects, toggles or checks the highlighted node.",
1047 Action::ExpandSiblings => "Expands the highlighted node and all of its siblings.",
1048 Action::ExtendNext => "Moves down and extends the selection, in a multiple selection.",
1049 Action::ExtendPrevious => {
1050 "Moves up and extends the selection, in a multiple selection."
1051 }
1052 Action::SelectAll => "Selects every visible node, in a multiple selection.",
1053 }
1054 }
1055}
1056
1057pub fn default_keymap() -> Keymap<Action> {
1078 Keymap::new()
1079 .bind(Chord::named(Named::ArrowDown), Action::Next)
1080 .bind(Chord::named(Named::ArrowUp), Action::Previous)
1081 .bind(Chord::named(Named::Home), Action::First)
1082 .bind(Chord::named(Named::End), Action::Last)
1083 .bind(Chord::named(Named::ArrowRight), Action::Expand)
1084 .bind(Chord::named(Named::ArrowLeft), Action::Collapse)
1085 .bind(Chord::named(Named::Enter), Action::Activate)
1086 .bind(Chord::named(Named::Space), Action::Select)
1087 .bind(Chord::character('*'), Action::ExpandSiblings)
1088 .bind(Chord::named(Named::ArrowDown).shift(), Action::ExtendNext)
1089 .bind(Chord::named(Named::ArrowUp).shift(), Action::ExtendPrevious)
1090 .bind(Chord::character('a').command(), Action::SelectAll)
1091}
1092
1093pub fn subscription<Id: Send + 'static>() -> Subscription<Event<Id>> {
1096 Subscription::run(stream::<Id>)
1097}
1098
1099pub fn stream<Id: Send + 'static>() -> impl Stream<Item = Event<Id>> {
1101 let (sender, receiver) = mpsc::channel(CHANNEL_CAPACITY);
1102 stream::once(async move { Event::Ready(sender) })
1103 .chain(receiver.ready_chunks(BATCH_SIZE).map(Event::Received))
1104}
1105
1106#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
1108pub struct Status {
1109 pub selected: bool,
1110 pub hovered: bool,
1111 pub highlighted: bool,
1113 pub disabled: bool,
1114}
1115
1116#[derive(Debug, Clone, Copy, PartialEq)]
1118pub struct RowStyle {
1119 pub background: Option<Color>,
1120 pub text: Color,
1121 pub icon: Color,
1122 pub ring: Option<Color>,
1124}
1125
1126pub fn row_style(tokens: &Tokens, status: Status) -> RowStyle {
1129 let menu_status = match (status.disabled, status.selected) {
1130 (true, _) => RowStatus::Disabled,
1131 (false, true) => RowStatus::Highlighted,
1132 (false, false) => RowStatus::Idle,
1133 };
1134 let base = menu::row_style(tokens, menu_status, false);
1135 let hover = (status.hovered && !status.disabled).then(|| fade(tokens.accent, 0.6));
1136 RowStyle {
1137 background: base.background.or(hover),
1138 text: base.text,
1139 icon: base.icon,
1140 ring: (status.highlighted && !status.disabled).then_some(tokens.ring),
1141 }
1142}
1143
1144pub fn guide_colour(tokens: &Tokens) -> Color {
1146 tokens.border
1147}
1148
1149type OnEvent<'a, Id, Message> = dyn Fn(Event<Id>) -> Message + 'a;
1150type BuildMenu<'a, Id, Message> =
1151 dyn FnOnce(Keymap<context_menu::Action>) -> RowMenu<'a, Id, Message> + 'a;
1152
1153pub struct Tree<'a, Id, Message> {
1155 state: &'a State<Id>,
1156 guides: bool,
1157 width: Length,
1158 height: Length,
1159 keymap: Keymap<Action>,
1160 menu: Option<Box<BuildMenu<'a, Id, Message>>>,
1161 menu_keymap: Keymap<context_menu::Action>,
1162 on_event: Option<Box<OnEvent<'a, Id, Message>>>,
1163}
1164
1165pub fn tree<Id, Message>(state: &State<Id>) -> Tree<'_, Id, Message> {
1168 Tree {
1169 state,
1170 guides: false,
1171 width: Length::Fill,
1172 height: Length::Shrink,
1173 keymap: default_keymap(),
1174 menu: None,
1175 menu_keymap: context_menu::default_keymap(),
1176 on_event: None,
1177 }
1178}
1179
1180impl<Id: std::fmt::Debug, Message> std::fmt::Debug for Tree<'_, Id, Message> {
1181 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1182 f.debug_struct("Tree")
1183 .field("state", self.state)
1184 .field("guides", &self.guides)
1185 .field("width", &self.width)
1186 .field("height", &self.height)
1187 .field("keymap", &self.keymap)
1188 .finish_non_exhaustive()
1189 }
1190}
1191
1192impl<'a, Id, Message> Tree<'a, Id, Message> {
1193 pub fn guides(mut self, guides: bool) -> Self {
1195 self.guides = guides;
1196 self
1197 }
1198
1199 pub fn width(mut self, width: impl Into<Length>) -> Self {
1200 self.width = width.into();
1201 self
1202 }
1203
1204 pub fn height(mut self, height: impl Into<Length>) -> Self {
1207 self.height = height.into();
1208 self
1209 }
1210
1211 pub fn keymap(mut self, keymap: Keymap<Action>) -> Self {
1213 self.keymap = keymap;
1214 self
1215 }
1216
1217 pub fn on_event(mut self, on_event: impl Fn(Event<Id>) -> Message + 'a) -> Self {
1218 self.on_event = Some(Box::new(on_event));
1219 self
1220 }
1221
1222 pub fn context_menu<MenuId>(
1226 mut self,
1227 menu: &'a context_menu::State<MenuId, Id>,
1228 on_event: impl Fn(context_menu::Event<MenuId, Id>) -> Message + 'a,
1229 ) -> Self
1230 where
1231 MenuId: Copy + PartialEq + 'a,
1232 Id: Clone + PartialEq + 'a,
1233 Message: Clone + 'a,
1234 {
1235 let on_event: Rc<dyn Fn(context_menu::Event<MenuId, Id>) -> Message + 'a> =
1236 Rc::new(on_event);
1237 self.menu = Some(Box::new(move |keymap| row_menu(menu, keymap, on_event)));
1238 self
1239 }
1240
1241 pub fn context_menu_keymap(mut self, keymap: Keymap<context_menu::Action>) -> Self {
1243 self.menu_keymap = keymap;
1244 self
1245 }
1246
1247 pub fn is_enabled(&self) -> bool {
1248 self.on_event.is_some()
1249 }
1250}
1251
1252impl<'a, Id, Message> From<Tree<'a, Id, Message>> for Element<'a, Message>
1253where
1254 Id: Copy + Eq + Hash + 'a,
1255 Message: Clone + 'a,
1256{
1257 fn from(tree: Tree<'a, Id, Message>) -> Self {
1258 let Tree {
1259 state,
1260 guides,
1261 width,
1262 height,
1263 keymap,
1264 menu,
1265 menu_keymap,
1266 on_event,
1267 } = tree;
1268 let on_event: Option<Rc<OnEvent<'a, Id, Message>>> = on_event.map(Rc::from);
1269 let menu = menu
1270 .filter(|_| on_event.is_some())
1271 .map(|build| Rc::new(build(menu_keymap)));
1272 let lines = state.lines();
1273 let highlight = state
1274 .highlighted
1275 .and_then(|id| lines.iter().position(|line| line.is(&id)));
1276 let count = lines.len();
1277
1278 let build = {
1279 let on_event = on_event.clone();
1280 let menu = menu.clone();
1281 let menu_target = menu.as_ref().and_then(|menu| menu.target());
1282 move |range: Range<usize>| -> Element<'a, Message> {
1283 let lines = lines.get(range).unwrap_or_default();
1284 column(lines.iter().map(|line| match *line {
1285 Line::Node { id, depth } => {
1286 let ringed = ringed(
1287 menu_target.as_ref(),
1288 &id,
1289 state.focused && state.highlighted == Some(id),
1290 );
1291 let row = node_view(state, id, depth, guides, ringed, on_event.as_ref());
1292 match &menu {
1293 Some(menu) if state.is_enabled(id) => menu.wrap(id, row),
1294 _ => row,
1295 }
1296 }
1297 Line::Loading { depth } => loading_view(depth, guides),
1298 }))
1299 .into()
1300 }
1301 };
1302 let list = container(
1303 rows(count, ROW_HEIGHT, build)
1304 .highlight(highlight)
1305 .height(height),
1306 )
1307 .width(width);
1308
1309 let Some(on_event) = on_event else {
1310 return list.into();
1311 };
1312 let on_focus = on_event.clone();
1313 scope(list)
1314 .on_focus(move |focused| on_focus(Event::Focus(focused)))
1315 .on_key(move |key| {
1316 let opened = menu
1317 .as_ref()
1318 .zip(state.highlighted())
1319 .and_then(|(menu, target)| menu.key(key, target));
1320 opened.or_else(|| state.key_event(&keymap, key).map(&*on_event))
1321 })
1322 .into()
1323 }
1324}
1325
1326fn indent<'a, Message: 'a>(depth: usize, guides: bool) -> Element<'a, Message> {
1328 let levels =
1329 (0..depth).map(|_| {
1330 let guide: Element<'a, Message> =
1331 if guides {
1332 container(container(Space::new()).width(1).height(Length::Fill).style(
1333 |theme| container::Style {
1334 background: Some(Background::Color(guide_colour(&Tokens::of(theme)))),
1335 ..container::Style::default()
1336 },
1337 ))
1338 .padding(iced::Padding::ZERO.left(ICON_SIZE / 2.0))
1339 .into()
1340 } else {
1341 Space::new().into()
1342 };
1343 container(guide).width(INDENT).height(Length::Fill).into()
1344 });
1345 row(levels).height(Length::Fill).into()
1346}
1347
1348fn line_frame<'a, Message: 'a>(content: impl Into<Element<'a, Message>>) -> Element<'a, Message> {
1349 container(content)
1350 .padding(
1351 iced::Padding::ZERO
1352 .left(ROW_PADDING.left)
1353 .right(ROW_PADDING.right),
1354 )
1355 .width(Length::Fill)
1356 .height(ROW_HEIGHT)
1357 .into()
1358}
1359
1360fn loading_view<'a, Message: 'a>(depth: usize, guides: bool) -> Element<'a, Message> {
1361 let muted = |theme: &Theme| Tokens::of(theme).muted_foreground;
1362 line_frame(
1363 row![
1364 indent(depth, guides),
1365 Space::new().width(INDENT),
1366 row![
1367 themed(crate::lucide!(LoaderCircle), ICON_SIZE, 1.0, muted),
1368 text("Loading...")
1369 .size(text_size::SM)
1370 .wrapping(text::Wrapping::None)
1371 .style(move |theme| text::Style {
1372 color: Some(muted(theme)),
1373 }),
1374 ]
1375 .spacing(space::SM)
1376 .align_y(Alignment::Center)
1377 .height(Length::Fill),
1378 ]
1379 .height(Length::Fill),
1380 )
1381}
1382
1383fn node_view<'a, Id, Message>(
1384 state: &'a State<Id>,
1385 id: Id,
1386 depth: usize,
1387 guides: bool,
1388 ringed: bool,
1389 on_event: Option<&Rc<OnEvent<'a, Id, Message>>>,
1390) -> Element<'a, Message>
1391where
1392 Id: Copy + Eq + Hash + 'a,
1393 Message: Clone + 'a,
1394{
1395 let Some(node) = state.node(id) else {
1396 return Space::new().into();
1397 };
1398 let on_event = on_event.filter(|_| !node.disabled);
1399 let enabled = on_event.is_some();
1400 let expanded = state.is_expanded(id);
1401 let status = Status {
1402 selected: state.mode != Mode::Checkbox && state.is_selected(id),
1403 hovered: false,
1404 highlighted: ringed,
1405 disabled: !enabled,
1406 };
1407 let colours = move |theme: &Theme| row_style(&Tokens::of(theme), status);
1408
1409 let chevron: Element<'a, Message> = if state.has_children(id) {
1410 let glyph = if expanded {
1411 crate::lucide!(ChevronDown)
1412 } else {
1413 crate::lucide!(ChevronRight)
1414 };
1415 let icon = container(themed(glyph, ICON_SIZE, opacity(enabled), move |theme| {
1416 colours(theme).icon
1417 }))
1418 .width(INDENT)
1419 .height(Length::Fill)
1420 .center_y(Length::Fill);
1421 match on_event {
1422 Some(on_event) => mouse_area(icon)
1423 .on_press(on_event(Event::Toggle(id)))
1424 .interaction(mouse::Interaction::Pointer)
1425 .into(),
1426 None => icon.into(),
1427 }
1428 } else {
1429 Space::new().width(INDENT).into()
1430 };
1431
1432 let mut content = row![]
1433 .spacing(space::SM)
1434 .align_y(Alignment::Center)
1435 .height(Length::Fill);
1436 if state.mode == Mode::Checkbox {
1437 let check = on_event.map(|on_event| move |checked| on_event(Event::Check(id, checked)));
1438 content = content.push(checkbox(state.check_state(id)).on_toggle_maybe(check));
1439 }
1440 let glyph = if expanded {
1441 node.open_icon.or(node.icon)
1442 } else {
1443 node.icon
1444 };
1445 if let Some(glyph) = glyph {
1446 content = content.push(themed(glyph, ICON_SIZE, opacity(enabled), move |theme| {
1447 colours(theme).icon
1448 }));
1449 }
1450 content = content.push(ellipsis(node.label.as_str()).colour(move |theme| colours(theme).text));
1451 match &node.trailing {
1452 Some(Trailing::Badge(label)) => {
1453 content = content.push(badge(label.as_str()).variant(badge::Variant::Secondary));
1454 }
1455 Some(Trailing::Text(label)) => {
1456 content = content.push(
1457 text(label.as_str())
1458 .size(text_size::XS)
1459 .wrapping(text::Wrapping::None)
1460 .style(move |theme: &Theme| text::Style {
1461 color: Some(Tokens::of(theme).muted_foreground),
1462 }),
1463 );
1464 }
1465 None => {}
1466 }
1467
1468 let body = line_frame(row![indent(depth, guides), chevron, content].height(Length::Fill));
1469 let row = pressable(body, move |theme, hovered| {
1470 let style = row_style(&Tokens::of(theme), Status { hovered, ..status });
1471 Look {
1472 background: style.background,
1473 border: Border {
1474 color: style.ring.unwrap_or(Color::TRANSPARENT),
1475 width: if style.ring.is_some() { 1.0 } else { 0.0 },
1476 radius: radius::SM.into(),
1477 },
1478 divider: None,
1479 }
1480 });
1481 let Some(on_event) = on_event.cloned() else {
1482 return row.into();
1483 };
1484 let activate = on_event(Event::Activate(id));
1485 row.on_press(move |modifiers| on_event(Event::Press(id, modifiers)))
1486 .on_double_click(Some(activate))
1487 .into()
1488}
1489
1490#[cfg(test)]
1491mod tests {
1492 use super::*;
1493 use crate::theme::{dark, light};
1494
1495 fn sample() -> State<u32> {
1506 State::new([
1507 node(1, "src").folder().children([
1508 node(2, "main.rs").file(),
1509 node(3, "lib").folder().children([
1510 node(4, "mod.rs"),
1511 node(5, "old.rs").disabled(true),
1512 node(6, "util.rs"),
1513 ]),
1514 ]),
1515 node(7, "docs").folder().lazy(),
1516 node(8, "readme.md").file(),
1517 ])
1518 }
1519
1520 fn open() -> State<u32> {
1521 sample().with_expanded([1, 3])
1522 }
1523
1524 fn selected(output: Option<Output<u32>>) -> Vec<u32> {
1525 match output {
1526 Some(Output::Selected(ids)) => ids,
1527 other => panic!("expected a selection, got {other:?}"),
1528 }
1529 }
1530
1531 fn checked(output: Option<Output<u32>>) -> Vec<u32> {
1532 match output {
1533 Some(Output::Checked(ids)) => ids,
1534 other => panic!("expected checked nodes, got {other:?}"),
1535 }
1536 }
1537
1538 fn key(key: Key, modifiers: Modifiers) -> keys::Event {
1539 keys::Event { key, modifiers }
1540 }
1541
1542 #[test]
1543 fn builds_the_structure_collapsed() {
1544 let state = sample();
1545 assert_eq!(state.len(), 8);
1546 assert_eq!(state.roots(), &[1, 7, 8]);
1547 assert_eq!(state.children(1), &[2, 3]);
1548 assert_eq!(state.parent(4), Some(3));
1549 assert_eq!(state.parent(1), None);
1550 assert_eq!(state.visible(), vec![1, 7, 8]);
1551 assert!(state.has_children(7) && !state.is_loaded(7));
1552 assert!(!state.has_children(8) && state.is_loaded(8));
1553 assert_eq!(state.mode(), Mode::Single);
1554 assert!(State::<u8>::new([]).is_empty());
1555 }
1556
1557 #[test]
1558 fn with_expanded_skips_leaves_and_lazy_nodes() {
1559 let state = sample().with_expanded([1, 7, 8]);
1560 assert!(state.is_expanded(1));
1561 assert!(!state.is_expanded(7), "lazy nodes load only when asked");
1562 assert!(!state.is_expanded(8));
1563 assert_eq!(state.visible(), vec![1, 2, 3, 7, 8]);
1564 }
1565
1566 #[test]
1567 fn toggle_expands_and_collapses() {
1568 let mut state = sample();
1569 assert!(state.update(Event::Toggle(1)).is_none());
1570 assert_eq!(state.visible(), vec![1, 2, 3, 7, 8]);
1571 let _ = state.update(Event::Toggle(1));
1572 assert_eq!(state.visible(), vec![1, 7, 8]);
1573 let _ = state.update(Event::Expand(8));
1574 assert!(!state.is_expanded(8), "a leaf does not expand");
1575 }
1576
1577 #[test]
1578 fn collapsing_moves_a_hidden_highlight_to_the_parent() {
1579 let mut state = open();
1580 let _ = state.update(Event::Press(4, Modifiers::empty()));
1581 let _ = state.update(Event::Collapse(1));
1582 assert_eq!(state.highlighted(), Some(1));
1583 }
1584
1585 #[test]
1586 fn expanding_a_lazy_node_starts_loading() {
1587 let mut state = sample();
1588 let output = state.update(Event::Expand(7));
1589 assert!(matches!(output, Some(Output::Load(ids)) if ids == [7]));
1590 assert!(state.is_loading(7) && state.is_expanded(7));
1591 assert_eq!(
1592 state.lines(),
1593 vec![
1594 Line::Node { id: 1, depth: 0 },
1595 Line::Node { id: 7, depth: 0 },
1596 Line::Loading { depth: 1 },
1597 Line::Node { id: 8, depth: 0 },
1598 ]
1599 );
1600 assert!(state.update(Event::Expand(7)).is_none(), "already loading");
1601 }
1602
1603 #[test]
1604 fn loaded_children_replace_the_loading_line() {
1605 let mut state = sample();
1606 let _ = state.update(Event::Expand(7));
1607 let _ = state.update(Event::Received(vec![loaded(
1608 7,
1609 [node(70, "guide.md"), node(71, "api").lazy()],
1610 )]));
1611 assert!(!state.is_loading(7));
1612 assert_eq!(state.visible(), vec![1, 7, 70, 71, 8]);
1613 assert_eq!(state.parent(71), Some(7));
1614 assert!(state.has_children(71));
1615 }
1616
1617 #[test]
1618 fn reloading_replaces_old_children_and_forgets_them() {
1619 let mut state = open();
1620 let _ = state.update(Event::Press(4, Modifiers::empty()));
1621 let _ = state.update(Event::Received(vec![loaded(3, [node(9, "new.rs")])]));
1622 assert_eq!(state.children(3), &[9]);
1623 assert!(state.node(4).is_none());
1624 assert_eq!(state.highlighted(), None);
1625 assert!(state.selected().is_empty());
1626 }
1627
1628 #[test]
1629 fn loading_no_children_collapses_the_node() {
1630 let mut state = sample();
1631 let _ = state.update(Event::Expand(7));
1632 let _ = state.update(Event::Received(vec![loaded(7, [])]));
1633 assert!(!state.is_expanded(7) && !state.has_children(7));
1634 assert!(
1635 state
1636 .update(Event::Received(vec![loaded(99, [])]))
1637 .is_none()
1638 );
1639 }
1640
1641 #[test]
1642 fn set_loading_marks_unloaded_nodes_only() {
1643 let mut state = sample();
1644 state.set_loading(7);
1645 assert!(state.is_loading(7));
1646 state.set_loading(1);
1647 assert!(!state.is_loading(1), "loaded children stay");
1648 }
1649
1650 #[test]
1651 fn arrows_move_over_visible_enabled_nodes_without_wrapping() {
1652 let mut state = open();
1653 let _ = state.update(Event::Next);
1654 assert_eq!(state.highlighted(), Some(1));
1655 for _ in 0..4 {
1656 let _ = state.update(Event::Next);
1657 }
1658 assert_eq!(state.highlighted(), Some(6), "skips the disabled node");
1659 let _ = state.update(Event::Last);
1660 assert_eq!(state.highlighted(), Some(8));
1661 let _ = state.update(Event::Next);
1662 assert_eq!(state.highlighted(), Some(8), "no wrap");
1663 let _ = state.update(Event::First);
1664 let _ = state.update(Event::Previous);
1665 assert_eq!(state.highlighted(), Some(1));
1666
1667 let mut fresh = open();
1668 let _ = fresh.update(Event::Previous);
1669 assert_eq!(fresh.highlighted(), Some(8));
1670 }
1671
1672 #[test]
1673 fn right_expands_then_enters_and_left_collapses_then_leaves() {
1674 let mut state = sample();
1675 let _ = state.update(Event::First);
1676 let _ = state.update(Event::ExpandOrEnter);
1677 assert!(state.is_expanded(1));
1678 assert_eq!(state.highlighted(), Some(1));
1679 let _ = state.update(Event::ExpandOrEnter);
1680 assert_eq!(state.highlighted(), Some(2));
1681 let _ = state.update(Event::ExpandOrEnter);
1682 assert_eq!(state.highlighted(), Some(2), "a leaf goes nowhere");
1683 let _ = state.update(Event::CollapseOrLeave);
1684 assert_eq!(state.highlighted(), Some(1));
1685 let _ = state.update(Event::CollapseOrLeave);
1686 assert!(!state.is_expanded(1));
1687 let _ = state.update(Event::CollapseOrLeave);
1688 assert_eq!(state.highlighted(), Some(1), "a root has no parent");
1689 }
1690
1691 #[test]
1692 fn right_on_a_lazy_node_loads_it() {
1693 let mut state = sample();
1694 let _ = state.update(Event::Press(7, Modifiers::empty()));
1695 assert!(matches!(
1696 state.update(Event::ExpandOrEnter),
1697 Some(Output::Load(ids)) if ids == [7]
1698 ));
1699 }
1700
1701 #[test]
1702 fn star_expands_every_sibling_and_loads_lazy_ones() {
1703 let mut state = sample();
1704 let _ = state.update(Event::First);
1705 let output = state.update(Event::ExpandSiblings);
1706 assert!(matches!(output, Some(Output::Load(ids)) if ids == [7]));
1707 assert!(state.is_expanded(1) && state.is_expanded(7));
1708 assert!(!state.is_expanded(8));
1709 }
1710
1711 #[test]
1712 fn typeahead_cycles_through_matching_labels() {
1713 let mut state = open();
1714 let _ = state.update(Event::Typeahead('M'));
1715 assert_eq!(state.highlighted(), Some(2));
1716 let _ = state.update(Event::Typeahead('m'));
1717 assert_eq!(state.highlighted(), Some(4));
1718 let _ = state.update(Event::Typeahead('m'));
1719 assert_eq!(state.highlighted(), Some(2), "wraps round");
1720 let _ = state.update(Event::Typeahead('o'));
1721 assert_eq!(state.highlighted(), Some(2), "disabled old.rs is skipped");
1722 let _ = state.update(Event::Typeahead('z'));
1723 assert_eq!(state.highlighted(), Some(2));
1724 }
1725
1726 #[test]
1727 fn single_selection_follows_presses() {
1728 let mut state = open();
1729 assert_eq!(
1730 selected(state.update(Event::Press(2, Modifiers::empty()))),
1731 [2]
1732 );
1733 assert_eq!(
1734 selected(state.update(Event::Press(4, Modifiers::CTRL))),
1735 [4]
1736 );
1737 assert!(state.update(Event::Press(4, Modifiers::empty())).is_none());
1738 assert!(state.update(Event::Press(5, Modifiers::empty())).is_none());
1739 assert_eq!(state.selected(), [4], "disabled nodes are not selected");
1740 }
1741
1742 #[test]
1743 fn multiple_selection_with_command_and_shift() {
1744 let mut state = open().with_mode(Mode::Multiple);
1745 let _ = state.update(Event::Press(2, Modifiers::empty()));
1746 assert_eq!(
1747 selected(state.update(Event::Press(4, Modifiers::COMMAND))),
1748 [2, 4]
1749 );
1750 assert_eq!(
1751 selected(state.update(Event::Press(2, Modifiers::COMMAND))),
1752 [4]
1753 );
1754 assert_eq!(
1755 selected(state.update(Event::Press(7, Modifiers::SHIFT))),
1756 [2, 3, 4, 6, 7],
1757 "a range from the anchor, skipping the disabled node"
1758 );
1759 let _ = state.update(Event::Press(8, Modifiers::empty()));
1760 let _ = state.update(Event::Press(2, Modifiers::COMMAND));
1761 assert_eq!(
1762 selected(state.update(Event::Press(4, Modifiers::SHIFT | Modifiers::COMMAND))),
1763 [2, 3, 4, 8],
1764 "Ctrl+Shift adds the range"
1765 );
1766 assert_eq!(
1767 selected(state.update(Event::Press(3, Modifiers::empty()))),
1768 [3]
1769 );
1770 }
1771
1772 #[test]
1773 fn shift_arrows_extend_from_the_anchor() {
1774 let mut state = open().with_mode(Mode::Multiple);
1775 let _ = state.update(Event::Press(2, Modifiers::empty()));
1776 assert_eq!(selected(state.update(Event::ExtendNext)), [2, 3]);
1777 assert_eq!(selected(state.update(Event::ExtendNext)), [2, 3, 4]);
1778 assert_eq!(selected(state.update(Event::ExtendPrevious)), [2, 3]);
1779 let _ = state.update(Event::ExtendPrevious);
1780 assert_eq!(selected(state.update(Event::ExtendPrevious)), [1, 2]);
1781 }
1782
1783 #[test]
1784 fn extend_only_moves_outside_a_multiple_selection() {
1785 let mut state = open();
1786 let _ = state.update(Event::Press(2, Modifiers::empty()));
1787 assert!(state.update(Event::ExtendNext).is_none());
1788 assert_eq!(state.highlighted(), Some(3));
1789 assert_eq!(state.selected(), [2]);
1790 }
1791
1792 #[test]
1793 fn space_selects_by_mode() {
1794 let mut single = open();
1795 let _ = single.update(Event::First);
1796 assert_eq!(selected(single.update(Event::ToggleHighlighted)), [1]);
1797
1798 let mut multiple = open().with_mode(Mode::Multiple);
1799 let _ = multiple.update(Event::First);
1800 let _ = multiple.update(Event::ToggleHighlighted);
1801 let _ = multiple.update(Event::Next);
1802 assert_eq!(selected(multiple.update(Event::ToggleHighlighted)), [1, 2]);
1803 assert_eq!(selected(multiple.update(Event::ToggleHighlighted)), [1]);
1804
1805 let mut none = open().with_mode(Mode::None);
1806 let _ = none.update(Event::First);
1807 assert!(none.update(Event::ToggleHighlighted).is_none());
1808 assert!(none.update(Event::Press(2, Modifiers::empty())).is_none());
1809 assert_eq!(none.highlighted(), Some(2));
1810 }
1811
1812 #[test]
1813 fn select_all_takes_every_visible_enabled_node() {
1814 let mut state = open().with_mode(Mode::Multiple);
1815 assert_eq!(
1816 selected(state.update(Event::SelectAll)),
1817 [1, 2, 3, 4, 6, 7, 8]
1818 );
1819 assert!(open().update(Event::SelectAll).is_none());
1820 }
1821
1822 #[test]
1823 fn activation_reports_the_node() {
1824 let mut state = open();
1825 assert!(state.update(Event::ActivateHighlighted).is_none());
1826 assert!(matches!(
1827 state.update(Event::Activate(2)),
1828 Some(Output::Activated(2))
1829 ));
1830 assert_eq!(state.highlighted(), Some(2));
1831 assert!(matches!(
1832 state.update(Event::ActivateHighlighted),
1833 Some(Output::Activated(2))
1834 ));
1835 assert!(state.update(Event::Activate(5)).is_none());
1836 }
1837
1838 #[test]
1839 fn checking_a_parent_checks_its_enabled_children() {
1840 let mut state = open().with_mode(Mode::Checkbox);
1841 assert_eq!(checked(state.update(Event::Check(3, true))), [3, 4, 6]);
1842 assert_eq!(state.check_state(3), CheckState::Checked);
1843 assert_eq!(
1844 state.check_state(5),
1845 CheckState::Unchecked,
1846 "disabled stays"
1847 );
1848 assert_eq!(state.check_state(1), CheckState::Indeterminate);
1849
1850 assert_eq!(
1851 checked(state.update(Event::Check(2, true))),
1852 [1, 2, 3, 4, 6]
1853 );
1854 assert_eq!(state.check_state(1), CheckState::Checked);
1855
1856 assert_eq!(checked(state.update(Event::Check(4, false))), [2, 6]);
1857 assert_eq!(state.check_state(3), CheckState::Indeterminate);
1858 assert_eq!(state.check_state(1), CheckState::Indeterminate);
1859
1860 assert_eq!(
1861 checked(state.update(Event::Check(1, false))),
1862 Vec::<u32>::new()
1863 );
1864 assert_eq!(state.check_state(3), CheckState::Unchecked);
1865 }
1866
1867 #[test]
1868 fn an_indeterminate_parent_checks_everything_on_the_next_click() {
1869 let mut state = open().with_mode(Mode::Checkbox);
1870 let _ = state.update(Event::Check(4, true));
1871 assert_eq!(state.check_state(3), CheckState::Indeterminate);
1872 let next = state.check_state(3).toggled().is_checked();
1873 assert_eq!(checked(state.update(Event::Check(3, next))), [3, 4, 6]);
1874 }
1875
1876 #[test]
1877 fn presses_and_space_toggle_checks_in_checkbox_mode() {
1878 let mut state = open().with_mode(Mode::Checkbox);
1879 assert_eq!(
1880 checked(state.update(Event::Press(6, Modifiers::empty()))),
1881 [6]
1882 );
1883 assert_eq!(state.check_state(3), CheckState::Indeterminate);
1884 assert_eq!(
1885 checked(state.update(Event::ToggleHighlighted)),
1886 Vec::<u32>::new()
1887 );
1888 assert!(state.update(Event::Check(5, true)).is_none(), "disabled");
1889 assert!(state.selected().is_empty(), "checkbox mode never selects");
1890 assert!(
1891 open().update(Event::Check(2, true)).is_none(),
1892 "other modes"
1893 );
1894 }
1895
1896 #[test]
1897 fn children_loading_under_a_checked_parent_arrive_checked() {
1898 let mut state = sample().with_mode(Mode::Checkbox);
1899 let _ = state.update(Event::Check(7, true));
1900 let _ = state.update(Event::Expand(7));
1901 let output = state.update(Event::Received(vec![loaded(
1902 7,
1903 [node(70, "a"), node(71, "b").disabled(true)],
1904 )]));
1905 assert_eq!(checked(output), [7, 70]);
1906 assert_eq!(state.check_state(7), CheckState::Checked);
1907 }
1908
1909 #[test]
1910 fn replacing_partially_checked_children_refreshes_ancestors_and_reports_checks() {
1911 let mut state = State::new([node(1, "Root").children([
1912 node(2, "Folder").children([node(3, "Old"), node(4, "Other")]),
1913 node(5, "Sibling"),
1914 ])])
1915 .with_mode(Mode::Checkbox);
1916 let _ = state.update(Event::Check(3, true));
1917 assert_eq!(state.check_state(1), CheckState::Indeterminate);
1918 assert_eq!(state.check_state(2), CheckState::Indeterminate);
1919
1920 let output = state.update(Event::Received(vec![loaded(2, [node(6, "New")])]));
1921
1922 assert_eq!(state.check_state(1), CheckState::Unchecked);
1923 assert_eq!(state.check_state(2), CheckState::Unchecked);
1924 assert_eq!(checked(output), Vec::<u32>::new());
1925 }
1926
1927 #[test]
1928 fn replacing_partially_checked_children_with_an_empty_list_clears_the_partial_mark() {
1929 let mut state =
1930 State::new([node(1, "Folder").children([node(2, "Old"), node(3, "Other")])])
1931 .with_mode(Mode::Checkbox);
1932 let _ = state.update(Event::Check(2, true));
1933
1934 let output = state.update(Event::Received(vec![loaded(1, [])]));
1935
1936 assert_eq!(state.check_state(1), CheckState::Unchecked);
1937 assert_eq!(checked(output), Vec::<u32>::new());
1938 }
1939
1940 #[test]
1941 fn reloading_an_unchanged_checked_leaf_emits_no_checkbox_change() {
1942 let mut state = State::new([node(1, "Folder").lazy()]).with_mode(Mode::Checkbox);
1943 let _ = state.update(Event::Check(1, true));
1944 let output = state.update(Event::Received(vec![loaded(1, [])]));
1945
1946 assert_eq!(state.check_state(1), CheckState::Checked);
1947 assert!(output.is_none());
1948 }
1949
1950 #[test]
1951 fn disabled_nodes_ignore_everything() {
1952 let mut state = open();
1953 let _ = state.update(Event::Press(5, Modifiers::empty()));
1954 assert_eq!(state.highlighted(), None);
1955 let disabled = State::new([node(1, "a").disabled(true).children([node(2, "b")])]);
1956 let mut disabled = disabled;
1957 assert!(disabled.update(Event::Toggle(1)).is_none());
1958 assert!(!disabled.is_expanded(1));
1959 }
1960
1961 #[test]
1962 fn focus_is_tracked_and_lands_on_a_node() {
1963 let mut state = sample();
1964 let _ = state.update(Event::Focus(true));
1965 assert!(state.is_focused());
1966 assert_eq!(state.highlighted(), Some(1), "the first node");
1967 let _ = state.update(Event::Focus(false));
1968 assert!(!state.is_focused());
1969 assert_eq!(state.highlighted(), Some(1), "the highlight stays");
1970
1971 let mut selected = open();
1972 let _ = selected.update(Event::Press(6, Modifiers::empty()));
1973 let _ = selected.update(Event::Collapse(3));
1974 let _ = selected.update(Event::Focus(true));
1975 assert_eq!(
1976 selected.highlighted(),
1977 Some(3),
1978 "an existing highlight is kept"
1979 );
1980
1981 let mut fresh = open().with_mode(Mode::Multiple);
1982 let _ = fresh.update(Event::SelectAll);
1983 let _ = fresh.update(Event::Focus(true));
1984 assert_eq!(fresh.highlighted(), Some(1), "the first selected node");
1985 }
1986
1987 fn press(keymap: &Keymap<Action>, chord: &str) -> Option<Action> {
1988 let chord: Chord = chord.parse().unwrap();
1989 keymap.resolve(chord.key(), chord.modifiers())
1990 }
1991
1992 #[test]
1993 fn default_keymap_covers_every_action() {
1994 let keymap = default_keymap();
1995 assert_eq!(press(&keymap, "ArrowDown"), Some(Action::Next));
1996 assert_eq!(press(&keymap, "ArrowUp"), Some(Action::Previous));
1997 assert_eq!(press(&keymap, "Home"), Some(Action::First));
1998 assert_eq!(press(&keymap, "End"), Some(Action::Last));
1999 assert_eq!(press(&keymap, "ArrowRight"), Some(Action::Expand));
2000 assert_eq!(press(&keymap, "ArrowLeft"), Some(Action::Collapse));
2001 assert_eq!(press(&keymap, "Enter"), Some(Action::Activate));
2002 assert_eq!(press(&keymap, "Space"), Some(Action::Select));
2003 assert_eq!(press(&keymap, "*"), Some(Action::ExpandSiblings));
2004 assert_eq!(press(&keymap, "Shift+ArrowDown"), Some(Action::ExtendNext));
2005 assert_eq!(
2006 press(&keymap, "Shift+ArrowUp"),
2007 Some(Action::ExtendPrevious)
2008 );
2009 assert_eq!(press(&keymap, "Mod+A"), Some(Action::SelectAll));
2010 for action in <Action as keys::Action>::ALL {
2011 assert!(!keymap.chords(action).is_empty(), "{action:?}");
2012 assert!(keys::Action::description(*action).ends_with('.'));
2013 }
2014 assert_eq!(Keymap::<Action>::defaults(), keymap);
2015 }
2016
2017 #[test]
2018 fn keymap_overrides_and_unbinding() {
2019 let keymap = default_keymap()
2020 .unbind(&Chord::character('*'))
2021 .bind(Chord::character('l'), Action::Expand);
2022 let state = open();
2023 assert!(
2024 state
2025 .key_event(&keymap, &key(Key::Character("*".into()), Modifiers::SHIFT))
2026 .is_none()
2027 );
2028 let mut state = state;
2029 let _ = state.update(Event::First);
2030 assert!(matches!(
2031 state.key_event(
2032 &keymap,
2033 &key(Key::Character("l".into()), Modifiers::empty())
2034 ),
2035 Some(Event::ExpandOrEnter)
2036 ));
2037 assert!(default_keymap().clear().is_empty());
2038 }
2039
2040 #[test]
2041 fn key_events_use_the_keymap_then_typeahead() {
2042 let keymap = default_keymap();
2043 let mut state = open();
2044 let _ = state.update(Event::First);
2045 let star = key(Key::Character("*".into()), Modifiers::SHIFT);
2046 assert!(matches!(
2047 state.key_event(&keymap, &star),
2048 Some(Event::ExpandSiblings)
2049 ));
2050 let letter = key(Key::Character("r".into()), Modifiers::empty());
2051 assert!(matches!(
2052 state.key_event(&keymap, &letter),
2053 Some(Event::Typeahead('r'))
2054 ));
2055 let ctrl_letter = key(Key::Character("r".into()), Modifiers::CTRL);
2056 assert!(state.key_event(&keymap, &ctrl_letter).is_none());
2057 let shifted_letter = key(Key::Character("R".into()), Modifiers::SHIFT);
2058 assert!(matches!(
2059 state.key_event(&keymap, &shifted_letter),
2060 Some(Event::Typeahead('R'))
2061 ));
2062 }
2063
2064 #[test]
2065 fn actions_map_to_events_only_when_they_apply() {
2066 let mut state = open();
2067 assert!(Action::Next.event(&state).is_some());
2068 assert!(
2069 Action::Expand.event(&state).is_none(),
2070 "nothing highlighted"
2071 );
2072 assert!(Action::Activate.event(&state).is_none());
2073 assert!(
2074 Action::SelectAll.event(&state).is_none(),
2075 "single selection"
2076 );
2077 let _ = state.update(Event::First);
2078 assert!(matches!(
2079 Action::Expand.event(&state),
2080 Some(Event::ExpandOrEnter)
2081 ));
2082 assert!(matches!(
2083 Action::Collapse.event(&state),
2084 Some(Event::CollapseOrLeave)
2085 ));
2086 assert!(matches!(
2087 Action::Select.event(&state),
2088 Some(Event::ToggleHighlighted)
2089 ));
2090 assert!(matches!(
2091 Action::ExtendNext.event(&state),
2092 Some(Event::Next)
2093 ));
2094 assert!(matches!(
2095 Action::ExtendPrevious.event(&state),
2096 Some(Event::Previous)
2097 ));
2098
2099 let multiple = open().with_mode(Mode::Multiple);
2100 assert!(matches!(
2101 Action::SelectAll.event(&multiple),
2102 Some(Event::SelectAll)
2103 ));
2104 assert!(matches!(
2105 Action::ExtendNext.event(&multiple),
2106 Some(Event::ExtendNext)
2107 ));
2108
2109 let mut none = open().with_mode(Mode::None);
2110 let _ = none.update(Event::First);
2111 assert!(Action::Select.event(&none).is_none());
2112 }
2113
2114 #[test]
2115 fn stream_emits_ready_first_then_batches() {
2116 use iced::futures::SinkExt;
2117 use iced::futures::executor::block_on;
2118
2119 let mut events = Box::pin(stream::<u32>());
2120 let Some(Event::Ready(mut sender)) = block_on(events.next()) else {
2121 panic!("first event must be Ready");
2122 };
2123 block_on(async {
2124 for parent in [1, 2, 3] {
2125 sender.send(loaded(parent, [])).await.ok();
2126 }
2127 });
2128 let Some(Event::Received(batch)) = block_on(events.next()) else {
2129 panic!("expected a batch");
2130 };
2131 let parents: Vec<_> = batch.iter().map(|loaded| loaded.parent).collect();
2132 assert_eq!(parents, [1, 2, 3]);
2133 }
2134
2135 #[test]
2136 fn ready_hands_the_sender_back() {
2137 let (sender, _receiver) = mpsc::channel(1);
2138 assert!(matches!(
2139 sample().update(Event::Ready(sender)),
2140 Some(Output::Ready(_))
2141 ));
2142 }
2143
2144 #[test]
2145 fn rows_follow_the_menu_look_in_both_themes() {
2146 for theme in [light(), dark()] {
2147 let tokens = Tokens::of(&theme);
2148 let idle = row_style(&tokens, Status::default());
2149 assert_eq!(idle.background, None);
2150 assert_eq!(idle.text, tokens.foreground);
2151 assert_eq!(idle.ring, None);
2152
2153 let selected = row_style(
2154 &tokens,
2155 Status {
2156 selected: true,
2157 ..Status::default()
2158 },
2159 );
2160 assert_eq!(selected.background, Some(tokens.accent));
2161 assert_eq!(selected.text, tokens.accent_foreground);
2162
2163 let hovered = row_style(
2164 &tokens,
2165 Status {
2166 hovered: true,
2167 ..Status::default()
2168 },
2169 );
2170 assert!(
2171 hovered
2172 .background
2173 .is_some_and(|colour| colour.a < tokens.accent.a)
2174 );
2175
2176 let highlighted = row_style(
2177 &tokens,
2178 Status {
2179 highlighted: true,
2180 ..Status::default()
2181 },
2182 );
2183 assert_eq!(highlighted.ring, Some(tokens.ring));
2184
2185 let disabled = row_style(
2186 &tokens,
2187 Status {
2188 disabled: true,
2189 hovered: true,
2190 highlighted: true,
2191 selected: true,
2192 },
2193 );
2194 assert_eq!(disabled.background, None);
2195 assert_eq!(disabled.text, tokens.muted_foreground);
2196 assert_eq!(disabled.ring, None);
2197 assert_eq!(guide_colour(&tokens), tokens.border);
2198 }
2199 }
2200
2201 #[test]
2202 fn builder_defaults_and_options() {
2203 let state = sample();
2204 let view: Tree<'_, u32, ()> = tree(&state);
2205 assert!(!view.guides && !view.is_enabled());
2206 assert_eq!((view.width, view.height), (Length::Fill, Length::Shrink));
2207 assert_eq!(view.keymap, default_keymap());
2208 let view = view
2209 .guides(true)
2210 .width(240)
2211 .height(300)
2212 .keymap(Keymap::new())
2213 .on_event(|_| ());
2214 assert!(view.guides && view.is_enabled());
2215 assert_eq!(view.width, Length::Fixed(240.0));
2216 assert!(view.keymap.is_empty());
2217 }
2218
2219 #[test]
2220 fn node_builders() {
2221 let folder = node(1, "src").folder().badge("3");
2222 assert_eq!(folder.icon, Some(crate::lucide!(Folder)));
2223 assert_eq!(folder.open_icon, Some(crate::lucide!(FolderOpen)));
2224 assert_eq!(folder.trailing, Some(Trailing::Badge("3".into())));
2225 let file = node(2, "a.txt").file().trailing("4 KB").disabled(true);
2226 assert_eq!(file.icon, Some(crate::lucide!(File)));
2227 assert_eq!(file.trailing, Some(Trailing::Text("4 KB".into())));
2228 assert!(file.disabled);
2229 assert_eq!(Mode::ALL.len(), 4);
2230 }
2231}