1use std::collections::HashSet;
58use std::sync::Arc;
59
60use gpui::{
61 div, prelude::FluentBuilder as _, px, App, ElementId, InteractiveElement, IntoElement,
62 MouseButton, ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, Styled,
63 Window,
64};
65use herogpui_core::{element_id, SelectionMode};
66use herogpui_theme::ActiveTheme;
67
68use crate::a11y::{self, A11y as _};
69use crate::list_box::{extend_selection_range, ListBoxSelectionRange as SelectionRange};
70use crate::{icons, util};
71
72type KeysCallback = Arc<dyn Fn(&HashSet<SharedString>, &mut Window, &mut App) + 'static>;
73
74const ROW_HEIGHT: gpui::Pixels = px(32.);
76const ROW_GAP: gpui::Pixels = px(2.);
78const PADDING: gpui::Pixels = px(4.);
80const INDENT: f32 = 16.;
82
83#[must_use = "builder methods return a new value; pass it on to its component"]
85pub struct TreeItem {
86 key: SharedString,
87 label: SharedString,
88 icon: Option<SharedString>,
89 children: Vec<TreeItem>,
90 is_disabled: bool,
91}
92
93impl TreeItem {
94 pub fn new(key: impl Into<SharedString>, label: impl Into<SharedString>) -> Self {
96 Self {
97 key: key.into(),
98 label: label.into(),
99 icon: None,
100 children: Vec::new(),
101 is_disabled: false,
102 }
103 }
104
105 pub fn child(mut self, item: TreeItem) -> Self {
107 self.children.push(item);
108 self
109 }
110
111 pub fn children(mut self, items: impl IntoIterator<Item = TreeItem>) -> Self {
114 self.children.extend(items);
115 self
116 }
117
118 pub fn icon(mut self, path: impl Into<SharedString>) -> Self {
120 self.icon = Some(path.into());
121 self
122 }
123
124 pub fn is_disabled(mut self, disabled: bool) -> Self {
127 self.is_disabled = disabled;
128 self
129 }
130}
131
132#[must_use = "a component does nothing until it is rendered: add it as a child or return it from `render`"]
134#[derive(IntoElement)]
135pub struct TreeView {
136 id: ElementId,
137 items: Vec<TreeItem>,
138 selection_mode: SelectionMode,
139 selected_keys: HashSet<SharedString>,
140 is_selection_controlled: bool,
141 default_selected_keys: HashSet<SharedString>,
142 disallow_empty_selection: bool,
143 expanded_keys: HashSet<SharedString>,
144 is_expansion_controlled: bool,
145 default_expanded_keys: HashSet<SharedString>,
146 disabled_keys: HashSet<SharedString>,
147 is_disabled: bool,
148 on_selection_change: Option<KeysCallback>,
149 on_expanded_change: Option<KeysCallback>,
150 max_h: Option<gpui::Pixels>,
151}
152
153impl TreeView {
154 pub fn new(id: impl Into<ElementId>, items: Vec<TreeItem>) -> Self {
157 Self {
158 id: id.into(),
159 items,
160 selection_mode: SelectionMode::None,
163 selected_keys: HashSet::new(),
164 is_selection_controlled: false,
165 default_selected_keys: HashSet::new(),
166 disallow_empty_selection: false,
167 expanded_keys: HashSet::new(),
168 is_expansion_controlled: false,
169 default_expanded_keys: HashSet::new(),
170 disabled_keys: HashSet::new(),
171 is_disabled: false,
172 on_selection_change: None,
173 on_expanded_change: None,
174 max_h: None,
175 }
176 }
177
178 pub fn max_h(mut self, height: impl Into<gpui::Pixels>) -> Self {
182 self.max_h = Some(height.into());
183 self
184 }
185
186 pub fn selection_mode(mut self, mode: SelectionMode) -> Self {
188 self.selection_mode = mode;
189 self
190 }
191
192 pub fn selected_keys(mut self, keys: impl IntoIterator<Item = SharedString>) -> Self {
194 self.selected_keys = keys.into_iter().collect();
195 self.is_selection_controlled = true;
196 self
197 }
198
199 pub fn default_selected_keys(mut self, keys: impl IntoIterator<Item = SharedString>) -> Self {
201 self.default_selected_keys = keys.into_iter().collect();
202 self
203 }
204
205 pub fn disallow_empty_selection(mut self, v: bool) -> Self {
207 self.disallow_empty_selection = v;
208 self
209 }
210
211 pub fn expanded_keys(mut self, keys: impl IntoIterator<Item = SharedString>) -> Self {
213 self.expanded_keys = keys.into_iter().collect();
214 self.is_expansion_controlled = true;
215 self
216 }
217
218 pub fn default_expanded_keys(mut self, keys: impl IntoIterator<Item = SharedString>) -> Self {
220 self.default_expanded_keys = keys.into_iter().collect();
221 self
222 }
223
224 pub fn disabled_keys(mut self, keys: impl IntoIterator<Item = SharedString>) -> Self {
226 self.disabled_keys = keys.into_iter().collect();
227 self
228 }
229
230 pub fn is_disabled(mut self, disabled: bool) -> Self {
233 self.is_disabled = disabled;
234 self
235 }
236
237 pub fn on_selection_change(
239 mut self,
240 f: impl Fn(&HashSet<SharedString>, &mut Window, &mut App) + 'static,
241 ) -> Self {
242 self.on_selection_change = Some(Arc::new(f));
243 self
244 }
245
246 pub fn on_expanded_change(
248 mut self,
249 f: impl Fn(&HashSet<SharedString>, &mut Window, &mut App) + 'static,
250 ) -> Self {
251 self.on_expanded_change = Some(Arc::new(f));
252 self
253 }
254}
255
256#[derive(Clone)]
258struct Row {
259 key: SharedString,
260 label: SharedString,
261 icon: Option<SharedString>,
262 depth: usize,
263 has_children: bool,
264 expanded: bool,
265 parent: Option<usize>,
267 disabled: bool,
268 pos_in_set: usize,
270 set_size: usize,
272}
273
274fn flatten(
277 items: Vec<TreeItem>,
278 depth: usize,
279 parent: Option<usize>,
280 expanded: &HashSet<SharedString>,
281 disabled_keys: &HashSet<SharedString>,
282 out: &mut Vec<Row>,
283) {
284 let set_size = items.len();
285 for (pos_in_set, item) in items.into_iter().enumerate() {
286 let has_children = !item.children.is_empty();
287 let is_open = has_children && expanded.contains(&item.key);
288 let index = out.len();
289 out.push(Row {
290 disabled: item.is_disabled || disabled_keys.contains(&item.key),
291 key: item.key,
292 label: item.label,
293 icon: item.icon,
294 depth,
295 has_children,
296 expanded: is_open,
297 parent,
298 pos_in_set,
299 set_size,
300 });
301 if is_open {
302 flatten(
303 item.children,
304 depth + 1,
305 Some(index),
306 expanded,
307 disabled_keys,
308 out,
309 );
310 }
311 }
312}
313
314fn ancestors_of(items: &[TreeItem], key: &SharedString) -> Option<Vec<SharedString>> {
317 for item in items {
318 if &item.key == key {
319 return Some(Vec::new());
320 }
321 if let Some(mut path) = ancestors_of(&item.children, key) {
322 path.push(item.key.clone());
323 return Some(path);
324 }
325 }
326 None
327}
328
329fn toggled_selection(
331 current: &HashSet<SharedString>,
332 key: &SharedString,
333 mode: SelectionMode,
334 disallow_empty: bool,
335) -> HashSet<SharedString> {
336 let order: Vec<SharedString> = current.iter().cloned().collect();
337 crate::selection::next_selection(&order, key, mode, disallow_empty)
338 .into_iter()
339 .collect()
340}
341
342#[derive(Clone)]
344struct TreeState {
345 cursor: gpui::Entity<Option<SharedString>>,
346 selected: HashSet<SharedString>,
347 selected_own: Option<gpui::Entity<HashSet<SharedString>>>,
348 expanded: HashSet<SharedString>,
349 expanded_own: Option<gpui::Entity<HashSet<SharedString>>>,
350 mode: SelectionMode,
351 disallow_empty: bool,
352 on_selection_change: Option<KeysCallback>,
353 on_expanded_change: Option<KeysCallback>,
354 range: gpui::Entity<SelectionRange>,
356 order: std::rc::Rc<Vec<SharedString>>,
358 selectable: std::rc::Rc<HashSet<SharedString>>,
360}
361
362impl TreeState {
363 fn set_cursor(&self, key: &SharedString, cx: &mut App) {
364 self.cursor.update(cx, |value, cx| {
365 if value.as_ref() != Some(key) {
366 *value = Some(key.clone());
367 cx.notify();
368 }
369 });
370 }
371
372 fn set_expanded(&self, key: &SharedString, open: bool, window: &mut Window, cx: &mut App) {
373 if self.expanded.contains(key) == open {
374 return;
375 }
376 let mut next = self.expanded.clone();
377 if open {
378 next.insert(key.clone());
379 } else {
380 next.remove(key);
381 }
382 if let Some(own) = &self.expanded_own {
383 own.update(cx, |value, cx| {
384 value.clone_from(&next);
385 cx.notify();
386 });
387 }
388 if let Some(cb) = &self.on_expanded_change {
389 cb(&next, window, cx);
390 }
391 }
392
393 fn set_selected(&self, next: HashSet<SharedString>, window: &mut Window, cx: &mut App) {
394 if next == self.selected {
395 return;
396 }
397 if let Some(own) = &self.selected_own {
398 own.update(cx, |value, cx| {
399 value.clone_from(&next);
400 cx.notify();
401 });
402 }
403 if let Some(cb) = &self.on_selection_change {
404 cb(&next, window, cx);
405 }
406 }
407
408 fn extend_to(&self, key: &SharedString, window: &mut Window, cx: &mut App) {
412 let range = self.range.read(cx).clone();
413 let next =
414 extend_selection_range(&self.selected, &self.order, &self.selectable, &range, key);
415 self.range.update(cx, |range, _| {
416 if range.anchor.is_none() {
417 range.anchor = Some(key.clone());
418 }
419 range.current = Some(key.clone());
420 range.is_all = false;
421 });
422 self.set_selected(next, window, cx);
423 }
424
425 fn extends(&self, shift: bool) -> bool {
427 shift && self.mode == SelectionMode::Multiple
428 }
429
430 fn activate(&self, row: &Row, window: &mut Window, cx: &mut App) {
434 if row.disabled {
435 return;
436 }
437 if crate::selection::reports_changes(self.mode) {
438 let was_selected = self.selected.contains(&row.key);
439 let next = toggled_selection(&self.selected, &row.key, self.mode, self.disallow_empty);
440 if self.mode == SelectionMode::Multiple && !was_selected {
441 self.range.update(cx, |range, _| {
442 *range = SelectionRange {
443 anchor: Some(row.key.clone()),
444 current: Some(row.key.clone()),
445 is_all: false,
446 };
447 });
448 }
449 self.set_selected(next, window, cx);
450 } else if row.has_children {
451 self.set_expanded(&row.key, !row.expanded, window, cx);
452 }
453 }
454}
455
456fn selector_base(id: &ElementId) -> String {
458 match id {
459 ElementId::Name(name) => name.to_string(),
460 other => format!("{other:?}"),
461 }
462}
463
464impl RenderOnce for TreeView {
465 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
466 let base = self.id.clone();
467 let selector = selector_base(&base);
468 let focus_handle = window
469 .use_keyed_state(element_id::scoped(&base, "focus"), cx, |_, cx| {
470 cx.focus_handle()
471 })
472 .read(cx)
473 .clone()
474 .tab_stop(!self.is_disabled);
475 let cursor = window.use_keyed_state(element_id::scoped(&base, "cursor"), cx, |_, _| {
476 None::<SharedString>
477 });
478 let typed = window.use_keyed_state(element_id::scoped(&base, "typed"), cx, |_, _| {
479 crate::list_nav::Typeahead::default()
480 });
481 let range = window.use_keyed_state(element_id::scoped(&base, "range"), cx, |_, _| {
482 SelectionRange::default()
483 });
484 let list = window
485 .use_keyed_state(element_id::scoped(&base, "list"), cx, |_, _| {
486 crate::VirtualListHandle::uniform(0)
487 })
488 .read(cx)
489 .clone();
490 let (selected, selected_own) = util::controlled(
491 window,
492 cx,
493 element_id::scoped(&base, "selected"),
494 self.is_selection_controlled
495 .then(|| self.selected_keys.clone()),
496 self.default_selected_keys.clone(),
497 );
498 let (expanded, expanded_own) = util::controlled(
499 window,
500 cx,
501 element_id::scoped(&base, "expanded"),
502 self.is_expansion_controlled
503 .then(|| self.expanded_keys.clone()),
504 self.default_expanded_keys.clone(),
505 );
506
507 let cursor_ancestors = cursor
512 .read(cx)
513 .as_ref()
514 .and_then(|key| ancestors_of(&self.items, key))
515 .unwrap_or_default();
516 let mut rows = Vec::new();
517 flatten(
518 self.items,
519 0,
520 None,
521 &expanded,
522 &self.disabled_keys,
523 &mut rows,
524 );
525 let disabled_tree = self.is_disabled;
526 let stops: Vec<usize> = rows
527 .iter()
528 .enumerate()
529 .filter(|(_, row)| !row.disabled && !disabled_tree)
530 .map(|(ix, _)| ix)
531 .collect();
532
533 let has_focus = focus_handle.is_focused(window);
539 let held = cursor
540 .read(cx)
541 .as_ref()
542 .and_then(|key| rows.iter().position(|row| &row.key == key))
543 .filter(|ix| stops.contains(ix))
544 .or_else(|| {
545 let ancestor = cursor_ancestors.iter().find_map(|key| {
546 rows.iter()
547 .position(|row| &row.key == key)
548 .filter(|ix| stops.contains(ix))
549 })?;
550 let key = rows[ancestor].key.clone();
551 cursor.update(cx, |value, _| *value = Some(key));
552 Some(ancestor)
553 });
554 let cursor_at = held.or_else(|| {
555 has_focus
556 .then(|| {
557 stops
558 .iter()
559 .copied()
560 .find(|ix| selected.contains(&rows[*ix].key))
561 .or_else(|| stops.first().copied())
562 })
563 .flatten()
564 });
565 let focused_at = (has_focus && window.is_window_active())
566 .then_some(cursor_at)
567 .flatten();
568
569 let state = TreeState {
570 cursor,
571 selected: selected.clone(),
572 selected_own,
573 expanded,
574 expanded_own,
575 mode: self.selection_mode,
576 disallow_empty: self.disallow_empty_selection,
577 on_selection_change: self.on_selection_change.clone(),
578 on_expanded_change: self.on_expanded_change.clone(),
579 range,
580 order: std::rc::Rc::new(rows.iter().map(|row| row.key.clone()).collect()),
581 selectable: std::rc::Rc::new(stops.iter().map(|ix| rows[*ix].key.clone()).collect()),
582 };
583 let rows = std::rc::Rc::new(rows);
584 if list.item_count() != rows.len() {
585 list.splice(0..list.item_count(), rows.len());
586 }
587
588 let colors = cx.colors().clone();
589 let radius = util::soft_radius(cx);
590 let pointer = util::interactive_cursor(cx);
591 let disabled_opacity = cx.layout().disabled_opacity;
592 let selects = self.selection_mode != SelectionMode::None;
593
594 let mut tree = div()
595 .id(base.clone())
596 .a11y(a11y::Role::Tree)
597 .relative()
598 .flex()
599 .flex_col()
600 .text_size(util::FIELD_TEXT)
601 .text_color(colors.foreground)
602 .debug_selector({
603 let name = format!("{selector}-tree");
604 move || name
605 });
606
607 if disabled_tree {
608 tree = tree.opacity(disabled_opacity);
609 } else {
610 let keys_state = state.clone();
611 let keys_rows = rows.clone();
612 let keys_focus = focus_handle.clone();
613 let keys_list = list.clone();
614 let keys_stops = stops;
615 tree = tree
616 .track_focus(&focus_handle)
617 .key_context("TreeView")
618 .on_mouse_down(MouseButton::Left, {
619 let focus = focus_handle.clone();
620 move |_, window, cx| window.focus(&focus, cx)
621 })
622 .on_key_down(move |event, window, cx| {
623 if !keys_focus.is_focused(window) {
624 return;
625 }
626 let m = &event.keystroke.modifiers;
627 if m.control || m.alt || m.platform || m.function {
628 return;
629 }
630 let key = event.keystroke.key.as_str();
631 let rows = &keys_rows;
632 let state = &keys_state;
633 let from = cursor_at;
634 let moved = |to: usize, cx: &mut App| {
635 state.set_cursor(&rows[to].key, cx);
636 keys_list.scroll_to_item(to, crate::VirtualListScroll::Reveal);
637 util::set_focus_visible(true, cx);
638 };
639 match key {
640 "right" => {
641 let Some(ix) = from else { return };
642 let row = &rows[ix];
643 if row.has_children && !row.expanded {
644 state.set_expanded(&row.key, true, window, cx);
645 util::set_focus_visible(true, cx);
646 } else if row.has_children {
647 let child = ix + 1;
648 if rows.get(child).is_some_and(|c| c.parent == Some(ix))
649 && keys_stops.contains(&child)
650 {
651 moved(child, cx);
652 }
653 }
654 cx.stop_propagation();
655 }
656 "left" => {
657 let Some(ix) = from else { return };
658 let row = &rows[ix];
659 if row.has_children && row.expanded {
660 state.set_expanded(&row.key, false, window, cx);
661 util::set_focus_visible(true, cx);
662 } else if let Some(parent) =
663 row.parent.filter(|p| keys_stops.contains(p))
664 {
665 moved(parent, cx);
666 }
667 cx.stop_propagation();
668 }
669 "escape"
670 if !m.shift
671 && crate::selection::reports_changes(state.mode)
672 && !state.disallow_empty
673 && !state.selected.is_empty() =>
674 {
675 state
676 .range
677 .update(cx, |range, _| *range = SelectionRange::default());
678 state.set_selected(HashSet::new(), window, cx);
679 cx.stop_propagation();
680 }
681 _ => match crate::list_nav::resolve(&keys_stops, from, key, false) {
682 crate::list_nav::Move::To(to) => {
683 moved(to, cx);
684 if state.extends(m.shift)
688 && matches!(key, "up" | "down")
689 && Some(to) != from
690 {
691 state.extend_to(&rows[to].key, window, cx);
692 }
693 cx.stop_propagation();
694 }
695 crate::list_nav::Move::Activate => {
696 if let Some(ix) = from {
697 util::set_focus_visible(true, cx);
698 state.activate(&rows[ix], window, cx);
699 }
700 cx.stop_propagation();
701 }
702 crate::list_nav::Move::Ignore => {
703 if m.shift || !crate::list_nav::is_typeahead_key(key) {
704 return;
705 }
706 let labels: Vec<String> =
707 rows.iter().map(|row| row.label.to_string()).collect();
708 let now = web_time::Instant::now();
709 let (query, repeat) = typed.update(cx, |t, _| {
710 let query = t.push(key, now);
711 (query, t.is_repeat())
712 });
713 if let Some(found) = crate::list_nav::typeahead(
714 &labels,
715 &keys_stops,
716 from,
717 &query,
718 repeat,
719 ) {
720 moved(found, cx);
721 }
722 }
723 },
724 }
725 });
726 }
727
728 let content_height = PADDING * 2. + (ROW_HEIGHT + ROW_GAP) * rows.len() as f32 - ROW_GAP;
732 let rows_id = element_id::scoped(&base, "rows");
733 let render_row = {
734 move |ix: usize, _window: &mut Window, cx: &mut App| {
735 let row = &rows[ix];
736 let disabled = row.disabled || disabled_tree;
737 let is_selected = selects && selected.contains(&row.key);
738 let is_cursor = cursor_at == Some(ix);
739 let fg = if is_selected {
740 colors.accent.soft_foreground(colors.foreground)
741 } else {
742 colors.foreground
743 };
744 let hover_bg = colors.default.color;
745 let mut el = div()
746 .id(element_id::scoped(
747 &element_id::scoped(&base, "item"),
748 row.key.clone(),
749 ))
750 .a11y_named(
751 a11y::Role::TreeItem,
752 &a11y::Name::labelled(row.label.clone()),
753 )
754 .a11y_level(row.depth)
755 .a11y_set_position(row.pos_in_set, row.set_size)
756 .when(row.has_children, |el| el.a11y_expanded(row.expanded))
757 .when(selects, |el| el.a11y_selected(is_selected))
758 .when(is_cursor, |el| el.a11y_active_descendant())
762 .relative()
763 .flex()
764 .flex_row()
765 .items_center()
766 .gap(px(4.))
767 .h(ROW_HEIGHT)
768 .flex_shrink_0()
769 .pl(px(4. + INDENT * row.depth as f32))
770 .pr(px(8.))
771 .rounded(radius)
772 .text_color(fg)
773 .when(is_selected, |el| el.bg(colors.accent.soft()))
774 .debug_selector({
775 let name = format!("{selector}-row-{}", row.key);
776 move || name
777 });
778 if disabled {
779 el = el.opacity(disabled_opacity);
780 } else {
781 el = el
782 .cursor(pointer)
783 .when(!is_selected, |el| el.hover(move |s| s.bg(hover_bg)));
784 }
785 el = util::with_focus_ring_overlay(
786 el,
787 util::shows_focus_ring(focused_at == Some(ix), cx),
788 true,
789 radius,
790 Vec::new(),
791 cx,
792 );
793
794 let mut chevron = div()
797 .id(element_id::scoped(
798 &element_id::scoped(&base, "toggle"),
799 row.key.clone(),
800 ))
801 .flex()
802 .items_center()
803 .justify_center()
804 .size(px(16.))
805 .flex_shrink_0();
806 if row.has_children {
807 chevron = chevron
808 .debug_selector({
809 let name = format!("{selector}-toggle-{}", row.key);
810 move || name
811 })
812 .child(
813 gpui::svg()
814 .size(px(12.))
815 .path(if row.expanded {
816 icons::CHEVRON_DOWN
817 } else {
818 icons::CHEVRON_RIGHT
819 })
820 .text_color(colors.muted),
821 );
822 if !disabled {
823 let toggle = state.clone();
824 let focus = focus_handle.clone();
825 let key = row.key.clone();
826 let open = row.expanded;
827 chevron = chevron
828 .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
829 .on_click(move |_, window, cx| {
830 cx.stop_propagation();
831 util::set_focus_visible(false, cx);
832 window.focus(&focus, cx);
833 toggle.set_cursor(&key, cx);
834 toggle.set_expanded(&key, !open, window, cx);
835 });
836 }
837 }
838 el = el.child(chevron);
839 if let Some(path) = &row.icon {
840 el = el.child(
841 gpui::svg()
842 .size(util::FIELD_ICON)
843 .path(path.clone())
844 .flex_shrink_0()
845 .text_color(fg),
846 );
847 }
848 el = el.child(div().flex_1().min_w_0().truncate().child(row.label.clone()));
849
850 if !disabled {
851 let press = state.clone();
852 let press_rows = rows.clone();
853 el = el.on_click(move |event, window, cx| {
854 let row = &press_rows[ix];
855 util::set_focus_visible(false, cx);
856 press.set_cursor(&row.key, cx);
857 if press.extends(event.modifiers().shift) {
858 press.extend_to(&row.key, window, cx);
859 } else {
860 press.activate(row, window, cx);
861 }
862 });
863 }
864 if cursor_at == Some(ix) && focus_handle.is_focused(_window) {
865 el = util::record_focus_bounds(el, &focus_handle, _window, cx);
866 }
867 div()
868 .w_full()
869 .h(ROW_HEIGHT + ROW_GAP)
870 .child(el)
871 .into_any_element()
872 }
873 };
874 let rows_list = crate::VirtualList::new(rows_id, &list, render_row).padding(
875 PADDING,
876 PADDING,
877 PADDING - ROW_GAP,
878 );
879 let rows_list = match self.max_h {
880 Some(max_h) => rows_list.height(max_h.min(content_height)),
881 None => rows_list,
882 };
883 tree.child(rows_list)
884 }
885}
886
887#[cfg(test)]
888mod tests {
889 use super::*;
890
891 fn tree() -> Vec<TreeItem> {
892 vec![
893 TreeItem::new("a", "A").children(vec![
894 TreeItem::new("a1", "A1"),
895 TreeItem::new("a2", "A2").child(TreeItem::new("a2x", "A2x")),
896 ]),
897 TreeItem::new("b", "B").is_disabled(true),
898 ]
899 }
900
901 fn keys(rows: &[Row]) -> Vec<&str> {
902 rows.iter().map(|row| row.key.as_ref()).collect()
903 }
904
905 #[test]
906 fn flatten_walks_open_parents_only() {
907 let mut rows = Vec::new();
908 flatten(tree(), 0, None, &HashSet::new(), &HashSet::new(), &mut rows);
909 assert_eq!(keys(&rows), ["a", "b"]);
910
911 let open: HashSet<SharedString> = ["a".into(), "a2".into()].into_iter().collect();
912 let mut rows = Vec::new();
913 flatten(tree(), 0, None, &open, &HashSet::new(), &mut rows);
914 assert_eq!(keys(&rows), ["a", "a1", "a2", "a2x", "b"]);
915 assert_eq!(
916 rows.iter().map(|r| r.depth).collect::<Vec<_>>(),
917 [0, 1, 1, 2, 0]
918 );
919 assert_eq!(rows[3].parent, Some(2));
920 assert!(rows[4].disabled);
921 assert!(rows[0].expanded && !rows[1].has_children);
922 }
923
924 #[test]
925 fn rows_count_their_position_among_siblings() {
926 let open: HashSet<SharedString> = ["a".into(), "a2".into()].into_iter().collect();
927 let mut rows = Vec::new();
928 flatten(tree(), 0, None, &open, &HashSet::new(), &mut rows);
929 assert_eq!(
931 rows.iter()
932 .map(|r| (r.pos_in_set, r.set_size))
933 .collect::<Vec<_>>(),
934 [(0, 2), (0, 2), (1, 2), (0, 1), (1, 2)]
935 );
936 }
937
938 #[test]
939 fn an_open_key_on_a_leaf_is_not_expanded() {
940 let open: HashSet<SharedString> = ["a1".into()].into_iter().collect();
941 let mut rows = Vec::new();
942 flatten(tree(), 0, None, &open, &HashSet::new(), &mut rows);
943 assert!(rows.iter().all(|row| !row.expanded));
944 }
945}