Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

facett-app/src/tree.rs

//! **`Tree` — a hierarchical single-select navigator.** Renders a
//! [`NavModel`](crate::NavModel) as an expand/collapse tree where **any** node is
//! selectable (a data/object picker), distinct from [`Sidebar`](crate::Sidebar)
//! which drives routing. Selection + expansion live in the struct (FC-1),
//! headless (`select`), observable (`state_json`).

use crate::nav::{NavItem, NavModel};
use serde_json::{Value, json};
use std::collections::BTreeSet;

#[derive(Default)]
pub struct Tree {
    expanded: BTreeSet<String>,
    selected: Option<String>,
    seeded: bool,
}

impl Tree {
    pub fn new() -> Self {
        Self::default()
    }
    pub fn selected(&self) -> Option<&str> {
        self.selected.as_deref()
    }
    pub fn select(&mut self, id: impl Into<String>) {
        self.selected = Some(id.into());
    }
    pub fn is_expanded(&self, id: &str) -> bool {
        self.expanded.contains(id)
    }
    pub fn set_expanded(&mut self, id: &str, on: bool) {
        if on {
            self.expanded.insert(id.to_string());
        } else {
            self.expanded.remove(id);
        }
    }

    fn seed(&mut self, model: &NavModel) {
        if !self.seeded {
            for it in model.flat() {
                if it.is_section() {
                    self.expanded.insert(it.id.clone());
                }
            }
            self.seeded = true;
        }
    }

    /// Draw the tree; returns the id newly selected this frame.
    pub fn show(&mut self, ui: &mut egui::Ui, model: &NavModel) -> Option<String> {
        self.seed(model);
        let mut clicked = None;
        let items = model.items.clone();
        for it in &items {
            self.row(ui, it, 0, &mut clicked);
        }
        clicked
    }

    fn row(&mut self, ui: &mut egui::Ui, it: &NavItem, depth: usize, clicked: &mut Option<String>) {
        let indent = depth as f32 * 14.0;
        let selected = self.selected.as_deref() == Some(it.id.as_str());
        ui.horizontal(|ui| {
            ui.add_space(indent);
            if it.is_section() {
                let exp = self.is_expanded(&it.id);
                let arrow = if exp { "▾" } else { "▸" };
                if ui.add(egui::Button::new(arrow).frame(false)).clicked() {
                    self.set_expanded(&it.id, !exp);
                }
            } else {
                ui.add_space(16.0);
            }
            let glyph = it.icon.clone().unwrap_or_default();
            let label = if glyph.is_empty() { it.label.clone() } else { format!("{glyph} {}", it.label) };
            if ui.add_enabled(it.enabled, egui::Button::selectable(selected, label)).clicked() {
                *clicked = Some(it.id.clone());
            }
        });
        if it.is_section() && self.is_expanded(&it.id) {
            for c in &it.children {
                self.row(ui, c, depth + 1, clicked);
            }
        }
    }

    pub fn state_json(&self) -> Value {
        json!({ "selected": self.selected, "expanded": self.expanded.iter().collect::<Vec<_>>() })
    }
}