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<_>>() })
}
}