facett-core 0.1.7

facett — visual kernel: render a node/edge Scene into egui (wgpu fast path to come)
Documentation
//! **Metrics & density** (§9) — spacing, sizing, radii, stroke widths, all on a
//! 4-pt grid (8-pt major rhythm). Drives egui's `Spacing` + radii. `interact_size`
//! height ≥ 24 px (macOS more generous); radii ~6–8 (Windows moderate, macOS
//! soft, Device tight). The single source of every padding/radius/stroke a
//! component would otherwise hardcode (COH-1).

use egui::{Margin, Vec2, vec2};
use serde::{Deserialize, Serialize};

/// Layout metrics for a preset. Every value is a multiple of 4 (8 for major
/// rhythm). Components and `apply` read these instead of literals.
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct Metrics {
    /// Gap between items: `[x, y]` in points.
    pub item_spacing: [f32; 2],
    /// Inner button padding `[x, y]`.
    pub button_padding: [f32; 2],
    /// Window margin (uniform, points).
    pub window_margin: f32,
    /// Menu margin (uniform, points).
    pub menu_margin: f32,
    /// Minimum interactive widget size `[w, h]`; height ≥ 24.
    pub interact_size: [f32; 2],
    /// Indent step for trees/collapsibles.
    pub indent: f32,
    /// Slider track width.
    pub slider_width: f32,
    /// Toggle/checkbox icon width.
    pub icon_width: f32,
    /// Corner radius for widgets (points).
    pub corner_radius: u8,
    /// Corner radius for windows.
    pub window_corner_radius: u8,
    /// Corner radius for menus.
    pub menu_corner_radius: u8,
    /// Default stroke / hairline width.
    pub stroke_width: f32,
}

impl Default for Metrics {
    fn default() -> Self {
        Self::windows()
    }
}

impl Metrics {
    /// Windows / Fluent 2 — moderate radii, 24 px controls.
    pub fn windows() -> Self {
        Self {
            item_spacing: [8.0, 4.0],
            button_padding: [12.0, 6.0],
            window_margin: 8.0,
            menu_margin: 8.0,
            interact_size: [40.0, 24.0],
            indent: 16.0,
            slider_width: 100.0,
            icon_width: 16.0,
            corner_radius: 6,
            window_corner_radius: 8,
            menu_corner_radius: 6,
            stroke_width: 1.0,
        }
    }

    /// macOS / Apple HIG — softer radii, slightly more generous controls.
    pub fn macos() -> Self {
        Self {
            item_spacing: [8.0, 4.0],
            button_padding: [12.0, 8.0],
            window_margin: 12.0,
            menu_margin: 8.0,
            interact_size: [44.0, 28.0],
            indent: 16.0,
            slider_width: 120.0,
            icon_width: 16.0,
            corner_radius: 8,
            window_corner_radius: 12,
            menu_corner_radius: 8,
            stroke_width: 1.0,
        }
    }

    /// Device — tight, crisp, high-density, conservative radii.
    pub fn device() -> Self {
        Self {
            item_spacing: [8.0, 4.0],
            button_padding: [12.0, 8.0],
            window_margin: 8.0,
            menu_margin: 8.0,
            interact_size: [44.0, 28.0], // bigger touch/glove targets, still crisp
            indent: 16.0,
            slider_width: 120.0,
            icon_width: 20.0,
            corner_radius: 2,
            window_corner_radius: 2,
            menu_corner_radius: 2,
            stroke_width: 1.5, // crisp, high-contrast hairlines
        }
    }

    pub fn item_spacing_vec(&self) -> Vec2 {
        vec2(self.item_spacing[0], self.item_spacing[1])
    }
    pub fn button_padding_vec(&self) -> Vec2 {
        vec2(self.button_padding[0], self.button_padding[1])
    }
    pub fn interact_size_vec(&self) -> Vec2 {
        vec2(self.interact_size[0], self.interact_size[1])
    }
    pub fn window_margin_m(&self) -> Margin {
        Margin::same(self.window_margin as i8)
    }
    pub fn menu_margin_m(&self) -> Margin {
        Margin::same(self.menu_margin as i8)
    }

    /// True iff the **rhythm** values (item spacing, margins, indent) sit on the
    /// 4-pt grid (the §9 rule). Inner control padding (`button_padding`) may use
    /// finer 2-pt steps — it is sub-rhythm, not part of the layout grid.
    pub fn on_4pt_grid(&self) -> bool {
        let on4 = |x: f32| (x % 4.0).abs() < 1e-3;
        on4(self.item_spacing[0])
            && on4(self.item_spacing[1])
            && on4(self.window_margin)
            && on4(self.menu_margin)
            && on4(self.indent)
    }
}

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

    #[test]
    fn all_presets_are_on_the_4pt_grid() {
        assert!(Metrics::windows().on_4pt_grid(), "windows metrics off-grid");
        assert!(Metrics::macos().on_4pt_grid(), "macos metrics off-grid");
        assert!(Metrics::device().on_4pt_grid(), "device metrics off-grid");
    }

    #[test]
    fn interact_height_is_at_least_24() {
        for m in [Metrics::windows(), Metrics::macos(), Metrics::device()] {
            assert!(m.interact_size[1] >= 24.0, "interact height must be >= 24, got {}", m.interact_size[1]);
        }
    }

    #[test]
    fn device_radii_are_tight() {
        assert!(Metrics::device().corner_radius <= 2, "Device must be crisp/tight");
        assert!(Metrics::macos().corner_radius >= Metrics::windows().corner_radius, "macOS softer than Windows");
    }
}