BREP_app 0.2.1

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
//! The SINGLE source of truth for toolbar-button styling — shared by the main
//! toolbar (`toolbar.rs`), the context bar (`context_bar.rs`), and the 2D sketcher
//! toolbar (`sketch.rs`). Every toolbar button goes through here, so sizing/style
//! changes land in ONE place. The human label is in the hover TOOLTIP, and every
//! button is SQUARE (min-width == height).
//!
//! Artwork is ALWAYS the catalogued SVG ([`crate::icons`]) — colour icons paint
//! their own colours, monochrome ones are tinted to the button's live text
//! colour. There is no font path: this app ships no icon font, and a glyph
//! character is only ever a KEY into the catalog.
//!
//! There used to be two other routes. Several private-use font glyphs painted at
//! one origin in different colours was the only way to get multi-colour artwork
//! out of a monochrome TrueType face; that went when each became a single colour
//! SVG. Then monochrome glyphs were left to `BrepIcons.ttf` on the reasoning that
//! a rasterised image "would buy nothing" — true in isolation, but it was the
//! last thing keeping a whole font in the binary, so it went too.
use eframe::egui;

/// The catalogued artwork for a button glyph — what lets a plain
/// `button`/`button_enabled`/`toggle` call site render its icon with no per-site
/// plumbing, so the toolbar, the workbench registry and the sketch tool row all
/// light up from the glyph alone. Shared with the tree and palette rows so all
/// three agree on what is an icon.
use crate::icons::artwork;

/// Square edge length of a single-glyph toolbar button (min-width == height).
pub const TOOLBAR_BTN: f32 = 28.0;

/// The size the button ARTWORK is drawn at inside that square — shared by the
/// font glyphs and the SVG icons so the two render identically sized.
const GLYPH_SIZE: f32 = 17.0;

fn square() -> egui::Vec2 {
    egui::vec2(TOOLBAR_BTN, TOOLBAR_BTN)
}

/// The button for one glyph: its catalogued artwork if it has any, else the
/// characters themselves as text.
///
/// Monochrome artwork is white in the catalog, so `image_tint_follows_text_color`
/// multiplies it by the button's LIVE text colour — which is what keeps hover,
/// pressed and disabled states looking exactly as they did when the font drew
/// the glyph. Colour artwork must never be tinted, so it opts out.
fn glyph_button<'a>(ui: &egui::Ui, glyph: &'a str) -> egui::Button<'a> {
    match artwork(glyph) {
        Some(icon) => {
            // Idempotent, and the only place a toolbar button needs it: a button
            // can be the first thing on screen to draw an SVG.
            egui_extras::install_image_loaders(ui.ctx());
            egui::Button::new(crate::icon_text::image(icon, GLYPH_SIZE))
                .image_tint_follows_text_color(icon.mono)
                .min_size(square())
        }
        None => egui::Button::new(glyph).min_size(square()),
    }
}

/// A plain action toolbar button: `glyph` shown, `tooltip` on hover. Square.
pub fn button(ui: &mut egui::Ui, glyph: &str, tooltip: &str) -> egui::Response {
    let button = glyph_button(ui, glyph);
    ui.add(button).on_hover_text(tooltip)
}

/// A toolbar button enabled only when `enabled` (undo/redo, selection actions).
pub fn button_enabled(
    ui: &mut egui::Ui,
    enabled: bool,
    glyph: &str,
    tooltip: &str,
) -> egui::Response {
    let button = glyph_button(ui, glyph);
    ui.add_enabled(enabled, button).on_hover_text(tooltip)
}

/// A toolbar TOGGLE button (draw-tool selection, wireframe): pressed when `selected`.
pub fn toggle(ui: &mut egui::Ui, selected: bool, glyph: &str, tooltip: &str) -> egui::Response {
    let button = glyph_button(ui, glyph).selected(selected);
    ui.add(button).on_hover_text(tooltip)
}

/// The outcome of a toolbar [`select`] combo for one frame.
pub struct SelectResult {
    /// The option `id` the user picked THIS frame — `Some` only on a real change
    /// (differs from `current`); `None` otherwise.
    pub changed: Option<String>,
    /// The combo HEADER's screen rect (the verifier clicks this to open the menu).
    pub header_rect: egui::Rect,
    /// `(id, rect)` per menu item while the menu is OPEN (empty when closed) — the
    /// verifier clicks an item rect to drive a SELECTION.
    pub item_rects: Vec<(String, egui::Rect)>,
}

/// A toolbar SELECT (combo box) matched to the toolbar-button height — the ONE
/// place a toolbar dropdown's style lives, so it reads as part of the button row.
/// `options` are `(label, id)` pairs; `current` is the selected id. Publishes the
/// header rect and (while open) per-item rects for the headed verifier.
pub fn select(
    ui: &mut egui::Ui,
    id_source: &str,
    current: &str,
    options: &[(&str, &str)],
) -> SelectResult {
    let current_label = options
        .iter()
        .find(|(_, id)| *id == current)
        .map(|(label, _)| *label)
        .unwrap_or(current);
    let mut changed = None;
    let mut item_rects = Vec::new();
    let inner = egui::ComboBox::from_id_salt(("toolbar-select", id_source))
        // Let the popup size to its content (all options visible) — a fixed
        // TOOLBAR_BTN height capped the menu to ~one row, hiding options past the
        // first when the list grew (e.g. the added placeholder workbenches).
        .selected_text(current_label)
        .show_ui(ui, |ui| {
            for (label, id) in options {
                let resp = ui.selectable_label(*id == current, *label);
                item_rects.push(((*id).to_string(), resp.rect));
                if resp.clicked() && *id != current {
                    changed = Some((*id).to_string());
                }
            }
        });
    SelectResult { changed, header_rect: inner.response.rect, item_rects }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// The rule that routes a button to artwork. Every toolbar/context/sketch
    /// button goes through it, and there is no longer a font to fall back to —
    /// a catalogued glyph that stopped resolving would render as a bare
    /// character with no icon at all.
    #[test]
    fn every_catalogued_glyph_takes_the_artwork_path() {
        // Colour artwork...
        for glyph in ["\u{1F41E}", "\u{1F4DA}", "\u{270D}", "\u{E032}"] {
            let icon = artwork(glyph).unwrap_or_else(|| panic!("{glyph:?} is catalogued"));
            assert!(!icon.mono, "{glyph:?} is colour artwork");
        }
        // ...and MONOCHROME artwork, which now takes the same path and is
        // tinted to the button's text colour instead of being drawn by a font.
        for glyph in ["\u{2699}", "\u{26F6}", "\u{2139}"] {
            let icon = artwork(glyph).unwrap_or_else(|| panic!("{glyph:?} is catalogued"));
            assert!(icon.mono, "{glyph:?} is monochrome artwork");
        }
        // Not catalogued at all — drawn as plain text.
        assert!(artwork("A").is_none());
        // A composite label is not an icon, even if it starts with one.
        assert!(artwork("\u{1F41E} Submit").is_none());
        assert!(artwork("").is_none());
    }

    /// Every retired stacked-layer glyph must be gone from the font, not merely
    /// unreferenced — leaving them would keep dead outlines in every binary.
    /// U+E010–E027 were the file/undo/redo/projection layers; U+E05A–E05C were
    /// the bug/book/pencil fills.
    #[test]
    fn the_retired_layer_glyphs_are_no_longer_catalogued() {
        // E010-E018 are now the COMPOSITES (they reclaimed the block their own
        // layers vacated); E019-E027 and E05A-E05C are the retired layers.
        let retired = (0xE019u32..=0xE027).chain(0xE05A..=0xE05C);
        for cp in retired.filter_map(char::from_u32) {
            assert!(
                !crate::icons::has(cp),
                "U+{:04X} was a stacked-layer glyph and should be deleted",
                cp as u32
            );
        }
        // ...and the composites that replaced them ARE there.
        for cp in (0xE010u32..=0xE018).filter_map(char::from_u32) {
            assert!(crate::icons::has(cp), "U+{:04X} composite is missing", cp as u32);
            let icon = artwork(&cp.to_string()).expect("composite is catalogued");
            assert!(!icon.mono, "U+{:04X} must be colour", cp as u32);
        }
    }
}