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/sidebar.rs

//! **`Sidebar` — a persistent nav rail rendered from a [`NavModel`](crate::NavModel).**
//! Sections fold/unfold, leaves highlight the active route, and a collapse toggle
//! narrows it to an icon-only rail. Theme-aware (egui Style installed by
//! `look::Theme`). Returns the clicked route so the host drives its [`Router`].

use crate::nav::{NavItem, NavModel};
use egui::Ui;
use std::collections::BTreeSet;

/// Stateful rail: which sections are expanded + collapsed (icon-only) mode.
#[derive(Default)]
pub struct Sidebar {
    pub collapsed: bool,
    expanded: BTreeSet<String>,
    seeded: bool,
}

impl Sidebar {
    pub fn new() -> Self {
        Self::default()
    }

    /// Builder: start collapsed (icon-only).
    pub fn collapsed(mut self, on: bool) -> Self {
        self.collapsed = on;
        self
    }

    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 rail. Returns `Some(route)` if a leaf was clicked this frame.
    pub fn show(&mut self, ui: &mut Ui, model: &NavModel, active_route: &str) -> Option<String> {
        self.seed(model);
        let mut clicked = None;

        // Collapse toggle.
        let toggle = if self.collapsed { "»" } else { "« collapse" };
        if ui.add(egui::Button::new(toggle).frame(false)).clicked() {
            self.collapsed = !self.collapsed;
        }
        ui.separator();

        let items = model.items.clone(); // small; avoids borrow tangle with &mut self recursion
        for it in &items {
            self.row(ui, it, active_route, 0, &mut clicked);
        }
        clicked
    }

    fn row(&mut self, ui: &mut Ui, it: &NavItem, active: &str, depth: usize, clicked: &mut Option<String>) {
        let indent = depth as f32 * 14.0;
        let glyph = it.icon.clone().unwrap_or_default();

        if it.is_section() {
            if self.collapsed {
                // Collapsed: skip the header, surface the children's icons directly.
                for c in &it.children {
                    self.row(ui, c, active, depth, clicked);
                }
                return;
            }
            let exp = self.expanded.contains(&it.id);
            let arrow = if exp { "▾" } else { "▸" };
            ui.horizontal(|ui| {
                ui.add_space(indent);
                if ui.add(egui::Button::new(format!("{arrow} {glyph} {}", it.label)).frame(false)).clicked() {
                    if exp {
                        self.expanded.remove(&it.id);
                    } else {
                        self.expanded.insert(it.id.clone());
                    }
                }
            });
            if self.expanded.contains(&it.id) {
                for c in &it.children {
                    self.row(ui, c, active, depth + 1, clicked);
                }
            }
            return;
        }

        // Leaf.
        let route = it.route_or_id().to_string();
        let selected = route == active;
        if self.collapsed {
            let face = if glyph.is_empty() { it.label.chars().next().map(|c| c.to_string()).unwrap_or_default() } else { glyph };
            let resp = ui.add_enabled(it.enabled, egui::Button::selectable(selected, face)).on_hover_text(&it.label);
            if resp.clicked() {
                *clicked = Some(route);
            }
            return;
        }
        ui.horizontal(|ui| {
            ui.add_space(indent);
            let badge = it.badge.as_deref().map(|b| format!("  ({b})")).unwrap_or_default();
            let text = if glyph.is_empty() { format!("{}{}", it.label, badge) } else { format!("{glyph}  {}{}", it.label, badge) };
            let resp = ui.add_enabled(it.enabled, egui::Button::selectable(selected, text));
            if resp.clicked() {
                *clicked = Some(route.clone());
            }
        });
    }
}