BREP_app 0.1.0

The BREP CAD application: an eframe (egui + wgpu) host that draws the brep-render 3D engine into an egui frame — native + wasm from one codebase.
Documentation
//! A reusable, custom-painted TREE-NODE widget — the shared building block for
//! the engine-native side panels (the history feature tree here; the Scene tree
//! and others reuse it verbatim). egui's default `CollapsingHeader` draws a
//! disclosure TRIANGLE and no connector rules; the design reference is a classic
//! file-tree with **`[+]`/`[-]` collapse boxes + connector lines**, so this
//! module paints those itself.
//!
//! # The node model (immutable-mode friendly)
//!
//! A node is ONE row: `⟨connectors⟩ [+/-] ⟨glyph⟩ ⟨label⟩ … ⟨right content⟩`.
//! Expansion state is OWNED BY THE CALLER (the panels need exclusive-expand +
//! roll-to side effects), so [`node`] just draws a row and returns what was
//! clicked; the caller decides whether to recurse into children.
//!
//! ## Connector geometry
//!
//! Indentation is one [`INDENT`] column per tree level. A node at connector
//! column `d = guides.len()` draws its `├`/`└` connector at column `d` and its
//! box slot at column `d + 1`. `guides[i]` (`i < d`) says whether an ANCESTOR's
//! sibling line passes vertically through this row at column `i`. Descending into
//! a node's children extends the guide stack by [`child_guides`] with
//! `!this_node_is_last` — the node's own connector line continues down past its
//! children only if it has following siblings. This is the standard file-tree
//! rule and yields the reference's exact rules.
//!
//! This module has NO engine dependency — it is pure egui + geometry, so a later
//! `brep-ui` crate (and the theme pass, #49) can reuse it unchanged.

use eframe::egui;

/// One tree indentation level, in egui points.
pub const INDENT: f32 = 16.0;
/// The collapse box's side length.
const BOX: f32 = 13.0;
/// Gap between the box slot, an optional glyph, and the label.
const GAP: f32 = 4.0;
/// Right-side gutter reserved before the right-aligned row content (timing,
/// delete X, field inputs, a Select button) so those controls clear the sidebar
/// scroll bar instead of underlapping it — an underlapping X eats the click as a
/// scroll drag rather than firing.
const RIGHT_PAD: f32 = 10.0;

/// The spec for one tree row. Borrows its strings; holds no state (expansion is
/// the caller's — see the module docs).
pub struct TreeRow<'a> {
    /// Ancestor vertical-guide flags, one per level strictly above this node
    /// (`true` = a sibling line passes through this row at that level). The
    /// node's own connector column index is `guides.len()`.
    pub guides: &'a [bool],
    /// This node is the LAST among its siblings (`└` vs `├`). Ignored when `root`.
    pub is_last: bool,
    /// Draw a `[+]`/`[-]` collapse box (an expandable node) instead of an empty
    /// box slot (a leaf). A leaf keeps the same label indent so labels align.
    pub expandable: bool,
    /// Current expand state — only meaningful when `expandable`.
    pub expanded: bool,
    /// The far-left ROOT row (e.g. `Features`): no connector, box at column 0.
    pub root: bool,
    /// An optional per-type glyph drawn between the box slot and the label.
    pub glyph: Option<&'a str>,
    /// The row's text label.
    pub label: &'a str,
    /// Emphasize the label (the rolled-to / selected node).
    pub selected: bool,
    /// Make the label a drag handle (`Sense::click_and_drag`) — drag-reorder.
    pub draggable: bool,
}

impl<'a> TreeRow<'a> {
    /// A plain expandable branch node.
    pub fn branch(guides: &'a [bool], is_last: bool, expanded: bool, label: &'a str) -> Self {
        Self {
            guides,
            is_last,
            expandable: true,
            expanded,
            root: false,
            glyph: None,
            label,
            selected: false,
            draggable: false,
        }
    }

    /// A plain leaf node (no collapse box).
    pub fn leaf(guides: &'a [bool], is_last: bool, label: &'a str) -> Self {
        Self {
            guides,
            is_last,
            expandable: false,
            expanded: false,
            root: false,
            glyph: None,
            label,
            selected: false,
            draggable: false,
        }
    }

    pub fn glyph(mut self, glyph: Option<&'a str>) -> Self {
        self.glyph = glyph;
        self
    }
    pub fn selected(mut self, selected: bool) -> Self {
        self.selected = selected;
        self
    }
    pub fn draggable(mut self, draggable: bool) -> Self {
        self.draggable = draggable;
        self
    }
}

/// What happened to a drawn tree row.
pub struct NodeResponse {
    /// The whole row rect (for hit publishing / drag-target hit-testing).
    pub row_rect: egui::Rect,
    /// The collapse-box rect (for hit publishing).
    pub box_rect: egui::Rect,
    /// The `[+]`/`[-]` collapse box was clicked (a pure toggle intent).
    pub toggled: bool,
    /// The label response — the click/drag handle. Use `.clicked()` to select,
    /// `.drag_started()` / `.drag_stopped()` to reorder.
    pub label: egui::Response,
}

/// Extend a guide stack for a node's children: the node's own connector column
/// keeps drawing a vertical line past the children ONLY if the node has more
/// siblings below it (`!is_last`).
pub fn child_guides(guides: &[bool], is_last: bool) -> Vec<bool> {
    let mut next = guides.to_vec();
    next.push(!is_last);
    next
}

/// Draw ONE tree row and return what was interacted with. `add_right` fills the
/// right-aligned content area (timing + delete, field inputs, a Select button…).
pub fn node(
    ui: &mut egui::Ui,
    row: TreeRow,
    add_right: impl FnOnce(&mut egui::Ui),
) -> NodeResponse {
    let connector_col = if row.root { 0 } else { row.guides.len() };
    // The box slot (and thus the label) begins one column right of the connector.
    let indent = if row.root {
        0.0
    } else {
        (connector_col as f32 + 1.0) * INDENT
    };

    let mut box_rect = egui::Rect::NOTHING;
    let mut toggled = false;
    let mut label_resp: Option<egui::Response> = None;

    let inner = ui.horizontal(|ui| {
        // Zero the inter-item spacing so the geometry math is exact; gaps are
        // added explicitly below.
        ui.spacing_mut().item_spacing.x = 0.0;
        if indent > 0.0 {
            ui.add_space(indent);
        }

        // --- collapse box (expandable) or an equally-wide empty slot (leaf) ----
        let (brect, bresp) = ui.allocate_exact_size(
            egui::vec2(BOX, BOX),
            if row.expandable {
                egui::Sense::click()
            } else {
                egui::Sense::hover()
            },
        );
        box_rect = brect;
        if row.expandable {
            paint_box(ui, brect, row.expanded);
            if bresp.clicked() {
                toggled = true;
            }
        }
        ui.add_space(GAP);

        // --- optional per-type glyph ------------------------------------------
        if let Some(glyph) = row.glyph {
            ui.label(egui::RichText::new(glyph).color(ui.visuals().text_color()));
            ui.add_space(GAP);
        }

        // --- label (the click / drag handle) ----------------------------------
        let mut text = egui::RichText::new(row.label);
        if row.selected {
            text = text.strong();
        }
        let sense = if row.draggable {
            egui::Sense::click_and_drag()
        } else {
            egui::Sense::click()
        };
        label_resp = Some(ui.add(egui::Label::new(text).sense(sense)));

        // --- right-aligned content --------------------------------------------
        ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
            ui.add_space(RIGHT_PAD);
            add_right(ui);
        });
    });

    let row_rect = inner.response.rect;
    if !row.root {
        paint_connectors(ui, row_rect, row.guides, connector_col, row.is_last);
    }

    NodeResponse {
        row_rect,
        box_rect,
        toggled,
        label: label_resp.expect("label always drawn"),
    }
}

/// Draw a LEAF whose content is a multi-line, WRAPPING colored message — e.g. a
/// feature's error text shown under its header. Unlike [`node`] (a single
/// non-wrapping row), the label WRAPS to the available width and the row grows as
/// tall as it needs; the `└`/`├` connector anchors to the FIRST line so it still
/// reads as a normal child leaf under its parent. Returns the full row rect.
pub fn message_leaf(
    ui: &mut egui::Ui,
    guides: &[bool],
    is_last: bool,
    text: &str,
    color: egui::Color32,
) -> egui::Rect {
    let connector_col = guides.len();
    // Align the wrapped text with where a LEAF's label starts: indent to the box
    // slot column, then clear the (empty) box slot + gap.
    let indent = (connector_col as f32 + 1.0) * INDENT + BOX + GAP;
    let line_h = ui.text_style_height(&egui::TextStyle::Body);
    let inner = ui.horizontal(|ui| {
        ui.spacing_mut().item_spacing.x = 0.0;
        ui.add_space(indent);
        // A vertical child claims the remaining row; the wrapping label wraps to its
        // width (minus the right gutter so it clears the sidebar scroll bar).
        ui.vertical(|ui| {
            ui.set_max_width((ui.available_width() - RIGHT_PAD).max(40.0));
            ui.add(
                egui::Label::new(egui::RichText::new(text).color(color))
                    .wrap_mode(egui::TextWrapMode::Wrap),
            );
        });
    });
    let row_rect = inner.response.rect;
    let tick_y = row_rect.top() + line_h * 0.5;
    paint_connectors_at(ui, row_rect, guides, connector_col, is_last, tick_y);
    row_rect
}

/// Paint a `[+]`/`[-]` collapse box: a rounded stroked square with a minus bar
/// (expanded) plus a vertical bar (collapsed → a plus). Theme-aware.
fn paint_box(ui: &egui::Ui, rect: egui::Rect, expanded: bool) {
    let painter = ui.painter();
    let stroke = egui::Stroke::new(1.0, line_color(ui));
    painter.rect(
        rect,
        egui::CornerRadius::same(2),
        egui::Color32::TRANSPARENT,
        stroke,
        egui::StrokeKind::Inside,
    );
    let c = rect.center();
    let arm = rect.width() * 0.28;
    let sign = egui::Stroke::new(1.4, ui.visuals().text_color());
    // horizontal bar (always → the minus of a `[-]`)
    painter.line_segment(
        [egui::pos2(c.x - arm, c.y), egui::pos2(c.x + arm, c.y)],
        sign,
    );
    // vertical bar (only when collapsed → completes the plus of a `[+]`)
    if !expanded {
        painter.line_segment(
            [egui::pos2(c.x, c.y - arm), egui::pos2(c.x, c.y + arm)],
            sign,
        );
    }
}

/// Paint the ancestor guide lines + this node's `├`/`└` connector into the row's
/// left gutter (over-drawn by the inter-row spacing so verticals join seamlessly
/// across rows). The `├`/`└` tick lands at the row's vertical center.
fn paint_connectors(
    ui: &egui::Ui,
    row: egui::Rect,
    guides: &[bool],
    connector_col: usize,
    is_last: bool,
) {
    paint_connectors_at(ui, row, guides, connector_col, is_last, row.center().y);
}

/// Like [`paint_connectors`] but with an explicit `tick_y` for the `├`/`└` join —
/// so a MULTI-LINE row (a wrapping message leaf) can anchor its connector to its
/// FIRST line instead of the block's vertical center.
fn paint_connectors_at(
    ui: &egui::Ui,
    row: egui::Rect,
    guides: &[bool],
    connector_col: usize,
    is_last: bool,
    mid: f32,
) {
    let painter = ui.painter();
    let stroke = egui::Stroke::new(1.0, line_color(ui));
    let sp = ui.spacing().item_spacing.y + 1.0;
    let left = row.left();
    let top = row.top() - sp;
    let bottom = row.bottom() + sp;
    let col_x = |c: usize| left + c as f32 * INDENT + INDENT * 0.5;

    // Ancestor sibling lines passing through this row.
    for (i, on) in guides.iter().enumerate() {
        if *on {
            let x = col_x(i);
            painter.line_segment([egui::pos2(x, top), egui::pos2(x, bottom)], stroke);
        }
    }
    // This node's own connector: vertical down to mid (└) or through (├), plus a
    // horizontal tick reaching the box slot.
    let x = col_x(connector_col);
    let v_bottom = if is_last { mid } else { bottom };
    painter.line_segment([egui::pos2(x, top), egui::pos2(x, v_bottom)], stroke);
    painter.line_segment(
        [egui::pos2(x, mid), egui::pos2(x + INDENT * 0.5, mid)],
        stroke,
    );
}

/// The subtle connector / box stroke color — the theme's non-interactive
/// foreground, dimmed. Theme-aware (light + dark) and the seam the later theme
/// pass tunes in one place.
fn line_color(ui: &egui::Ui) -> egui::Color32 {
    ui.visuals()
        .widgets
        .noninteractive
        .fg_stroke
        .color
        .gamma_multiply(0.7)
}