Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Tree

Group: Data

Code

fn fake_rows(prefix: &str, n: usize) -> Vec<TreeRow> {
    (0..n)
        .map(|i| TreeRow {
            path: vec![i as u16],
            indent: 0,
            icon: None,
            text: StyledText::plain(format!("{prefix}{i}")),
            badge: None,
            is_expanded: None,
            decoration: Decoration::Normal,
            edit: None,
        })
        .collect()
}

/// A tiny two-level file tree: `src/` expanded with two children,
/// `tests/` collapsed with one hidden child — the app (not the
/// primitive) owns which children are currently visible.
struct FileTree {
    src_expanded: bool,
    tests_expanded: bool,
}

impl FileTree {
    fn rows(&self) -> Vec<TreeRow> {
        let mut rows = vec![TreeRow {
            path: vec![0],
            indent: 0,
            icon: None,
            text: StyledText::plain("src/"),
            badge: None,
            is_expanded: Some(self.src_expanded),
            decoration: Decoration::Normal,
            edit: None,
        }];
        if self.src_expanded {
            rows.push(TreeRow {
                path: vec![0, 0],
                indent: 1,
                icon: None,
                text: StyledText::plain("main.rs"),
                badge: None,
                is_expanded: None,
                decoration: Decoration::Normal,
                edit: None,
            });
            rows.push(TreeRow {
                path: vec![0, 1],
                indent: 1,
                icon: None,
                text: StyledText::plain("lib.rs"),
                badge: None,
                is_expanded: None,
                decoration: Decoration::Normal,
                edit: None,
            });
        }
        rows.push(TreeRow {
            path: vec![1],
            indent: 0,
            icon: None,
            text: StyledText::plain("tests/"),
            badge: None,
            is_expanded: Some(self.tests_expanded),
            decoration: Decoration::Normal,
            edit: None,
        });
        if self.tests_expanded {
            rows.push(TreeRow {
                path: vec![1, 0],
                indent: 1,
                icon: None,
                text: StyledText::plain("smoke.rs"),
                badge: None,
                is_expanded: None,
                decoration: Decoration::Normal,
                edit: None,
            });
        }
        rows
    }

    /// Flip the expand state of the top-level row named by `path[0]`
    /// (`0` = `src/`, `1` = `tests/`) and return a label for the status
    /// line.
    fn toggle(&mut self, path: &[u16]) -> String {
        match path.first() {
            Some(0) => {
                self.src_expanded = !self.src_expanded;
                format!(
                    "src/ → {}",
                    if self.src_expanded {
                        "expanded"
                    } else {
                        "collapsed"
                    }
                )
            }
            Some(1) => {
                self.tests_expanded = !self.tests_expanded;
                format!(
                    "tests/ → {}",
                    if self.tests_expanded {
                        "expanded"
                    } else {
                        "collapsed"
                    }
                )
            }
            _ => "—".into(),
        }
    }
}

pub struct TreeDemo {
    // Variant 0: multi-section debug sidebar.
    debug: SidebarSystem,
    debug_last: String,
    // Variant 1: single expandable file tree.
    file_tree: FileTree,
    file_sidebar: SidebarSystem,
    file_last: String,
}

impl TreeDemo {
    pub fn new() -> Self {
        let mut debug = SidebarSystem::new(vec![
            SidebarSectionDef::new("variables", "VARIABLES"),
            SidebarSectionDef::new("watch", "WATCH"),
            SidebarSectionDef::new("call-stack", "CALL STACK"),
            SidebarSectionDef::new("breakpoints", "BREAKPOINTS"),
        ]);
        debug.set_rows(0, fake_rows("v", 8));
        debug.set_rows(1, fake_rows("w", 4));
        debug.set_rows(2, fake_rows("frame", 3));
        debug.set_rows(3, fake_rows("bp", 0));
        debug.set_navigation_mode(NavigationMode::Selection);
        debug.set_scroll_mode(ScrollMode::WholePanel);
        debug.set_active_section(Some(0));

        let file_tree = FileTree {
            src_expanded: true,
            tests_expanded: false,
        };
        let mut file_sidebar =
            SidebarSystem::new(vec![SidebarSectionDef::new("files", "EXPLORER")]);
        file_sidebar.set_rows(0, file_tree.rows());
        file_sidebar.set_navigation_mode(NavigationMode::Selection);
        file_sidebar.set_active_section(Some(0));

        Self {
            debug,
            debug_last: "—".into(),
            file_tree,
            file_sidebar,
            file_last: "—".into(),
        }
    }

    fn content_rect(area: Rect, backend: &dyn Backend) -> Rect {
        let lh = backend.line_height();
        Rect::new(area.x, area.y, area.width, (area.height - lh).max(0.0))
    }

    fn status_rect(area: Rect, backend: &dyn Backend) -> Rect {
        let lh = backend.line_height();
        Rect::new(area.x, area.y + area.height - lh, area.width, lh)
    }

    fn status(&self, variant: usize) -> StatusBar {
        let text = if variant == 0 {
            format!(" Tab cycles sections — last: {} ", self.debug_last)
        } else {
            format!(
                " click a header (or Space on it) to expand/collapse — last: {} ",
                self.file_last
            )
        };
        StatusBar {
            id: WidgetId::new("gallery:tree:status"),
            left_segments: vec![StatusBarSegment {
                text,
                fg: Color::rgb(220, 220, 220),
                bg: Color::rgb(40, 40, 60),
                bold: false,
                action_id: None,
            }],
            right_segments: vec![],
        }
    }
}

impl Default for TreeDemo {
    fn default() -> Self {
        Self::new()
    }
}

impl Demo for TreeDemo {
    fn name(&self) -> &'static str {
        "Tree"
    }

    fn group(&self) -> &'static str {
        "Data"
    }

    fn variants(&self) -> &'static [&'static str] {
        &["Debug panel (4 sections)", "File tree (expand/collapse)"]
    }

    fn render(&self, variant: usize, backend: &mut dyn Backend, area: Rect) {
        let content = Self::content_rect(area, backend);
        if variant == 0 {
            self.debug.render(backend, content);
        } else {
            self.file_sidebar.render(backend, content);
        }
        let status_rect = Self::status_rect(area, backend);
        let _ = backend.draw_status_bar_interactive(
            status_rect,
            &self.status(variant),
            &InteractionState::new(),
        );
    }

    fn handle(
        &mut self,
        variant: usize,
        event: &UiEvent,
        backend: &mut dyn Backend,
        area: Rect,
    ) -> Reaction {
        let content = Self::content_rect(area, backend);
        if variant == 0 {
            match self.debug.handle(event, backend, content) {
                SidebarEvent::HeaderActivated { section } => {
                    self.debug_last = format!("header→{section}");
                    Reaction::Redraw
                }
                SidebarEvent::RowSelected { section, path } => {
                    self.debug_last = format!("sel→{section} {path:?}");
                    Reaction::Redraw
                }
                SidebarEvent::StateChanged
                | SidebarEvent::Consumed
                | SidebarEvent::ScrollChanged { .. } => Reaction::Redraw,
                SidebarEvent::Ignored => Reaction::Continue,
                _ => Reaction::Redraw,
            }
        } else {
            match self.file_sidebar.handle(event, backend, content) {
                SidebarEvent::RowToggleExpand { path, .. } => {
                    self.file_last = self.file_tree.toggle(&path);
                    self.file_sidebar.set_rows(0, self.file_tree.rows());
                    Reaction::Redraw
                }
                SidebarEvent::RowSelected { path, .. } => {
                    self.file_last = format!("sel {path:?}");
                    Reaction::Redraw
                }
                SidebarEvent::StateChanged
                | SidebarEvent::Consumed
                | SidebarEvent::ScrollChanged { .. } => Reaction::Redraw,
                SidebarEvent::Ignored => match event {
                    UiEvent::KeyPressed {
                        key: Key::Char(' '),
                        ..
                    } => {
                        let ev = self.file_sidebar.toggle_expand_selected();
                        if let SidebarEvent::RowToggleExpand { path, .. } = ev {
                            self.file_last = self.file_tree.toggle(&path);
                            self.file_sidebar.set_rows(0, self.file_tree.rows());
                            Reaction::Redraw
                        } else {
                            Reaction::Continue
                        }
                    }
                    _ => Reaction::Continue,
                },
                _ => Reaction::Redraw,
            }
        }
    }

    fn source(&self) -> &'static str {
        extract_region(SOURCE)
    }

    fn data(&self, variant: usize) -> serde_json::Value {
        if variant == 0 {
            serde_json::json!({
                "active_section": self.debug.active_section(),
                "last": self.debug_last,
            })
        } else {
            serde_json::json!({
                "src_expanded": self.file_tree.src_expanded,
                "tests_expanded": self.file_tree.tests_expanded,
                "last": self.file_last,
            })
        }
    }

    fn caps_note(&self, _variant: usize, _caps: &BackendCaps) -> Option<String> {
        None
    }
}

Backends

VariantTUIGTKmacOSWindows
Debug panel (4 sections)
File tree (expand/collapse)