facett-core 0.1.10

facett — visual kernel: render a node/edge Scene into egui (wgpu fast path to come)
Documentation
//! **Platform-adaptive "native feel" layer** — make facett feel *at home* on the
//! host OS. A [`Platform`] auto-detects the running OS (`cfg!(target_os)`, with an
//! explicit override) and selects a preset; a [`NativeFeel`] spec captures the
//! per-platform *cues* that map onto facett's **existing** primitives (it adds no
//! new render engine — it parameterises the ones we already have).
//!
//! # The cue → primitive map (the adaptive layer)
//!
//! | cue                         | macOS                                  | Windows 11                                   | facett primitive |
//! |-----------------------------|----------------------------------------|----------------------------------------------|------------------|
//! | translucent material        | **vibrancy** — high blur, low tint α   | **Mica** — lower blur, higher tint α         | [`SurfaceSpec::Frosted`](super::SurfaceSpec) (dual-Kawase blur in `render/gpu/blur.rs`) |
//! | corner radius               | soft (8 ctrl / 12 win)                 | moderate (6 ctrl / 8 win)                    | [`Metrics`](super::Metrics) |
//! | density                     | generous (44×28, 12pt margin)          | denser (40×24, 8pt margin)                   | [`Metrics`](super::Metrics) |
//! | scrollbars                  | floating, fade-when-idle               | solid, always-visible                        | [`ScrollSpec`](super::ScrollSpec) |
//! | overscroll                  | **rubber-band** spring overshoot       | hard edge                                    | [`rubber_band`](NativeFeel::rubber_band) → `SmoothScroll` |
//! | motion personality          | gentle spring **overshoot** (`EaseOutBack`), ~0.30s | snappy "connected" (`EaseInOutCubic`), ~0.15s | [`Motion`](super::Motion) curve + duration |
//! | hover signature             | subtle highlight                       | **reveal highlight** (cursor-follow edge glow) | [`reveal_highlight`](NativeFeel::reveal_highlight) → [`effects::RevealHighlight`](crate::effects::RevealHighlight) |
//! | focus                       | soft accent **ring** (~3px glow)       | crisp focus **rectangle**                    | [`FocusRing`] |
//! | accent usage                | tints selection + focus ring           | tints reveal + focus rect                    | [`accent_tint`](NativeFeel::accent_tint) |
//! | elevation                   | restrained shadow                      | pronounced drop-shadow elevation             | [`elevation`](NativeFeel::elevation) |
//! | window controls             | **traffic lights** (top-left)          | **caption buttons** (top-right)              | [`WindowControls`] (host-drawn; data hint here) |
//! | typography                  | SF Pro                                 | Segoe UI Variable                            | [`Typography`](super::Typography) `UiFont` |
//!
//! All of this is `serde` (a host can author/override it in TOML/JSON) and carries
//! **no egui-memory state and no wall-clock** (FC-1/FC-7): the reveal glow + spring
//! overshoot are driven by the caller's own clock through the existing
//! [`effects`](crate::effects) / [`scroll_engine`](crate::scroll_engine) primitives.

use serde::{Deserialize, Serialize};

/// The desktop platform we present as. `Neutral` is the cross-platform / Linux
/// default: premium but without OS-specific chrome cues (no traffic lights, no
/// reveal highlight).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum Platform {
    Mac,
    Windows,
    /// Linux / unknown / explicit "no OS-specific chrome".
    Neutral,
}

impl Platform {
    /// **Auto-detect** the host platform from `cfg!(target_os)` at compile time:
    /// macOS → [`Mac`](Platform::Mac), Windows → [`Windows`](Platform::Windows),
    /// everything else (Linux/BSD/unknown) → [`Neutral`](Platform::Neutral).
    ///
    /// This is the *default* — a host may override it explicitly (the demo's
    /// runtime toggle does exactly that, so the look is demonstrable on any OS).
    pub const fn detect() -> Platform {
        if cfg!(target_os = "macos") {
            Platform::Mac
        } else if cfg!(target_os = "windows") {
            Platform::Windows
        } else {
            Platform::Neutral
        }
    }

    /// A short stable label (also the serde discriminant for the override setting).
    pub fn label(self) -> &'static str {
        match self {
            Platform::Mac => "mac",
            Platform::Windows => "windows",
            Platform::Neutral => "neutral",
        }
    }

    /// Parse a label back (fuzzy: case-insensitive, `macos`/`osx` ≡ mac,
    /// `win` ≡ windows, anything else → `None`). The override-setting reader.
    pub fn from_label(s: &str) -> Option<Platform> {
        match s.trim().to_ascii_lowercase().as_str() {
            "mac" | "macos" | "osx" | "darwin" | "apple" => Some(Platform::Mac),
            "windows" | "win" | "win11" | "win32" => Some(Platform::Windows),
            "neutral" | "linux" | "nix" | "default" | "other" => Some(Platform::Neutral),
            _ => None,
        }
    }

    /// Cycle Mac → Windows → Neutral → Mac (the runtime toggle order).
    pub fn cycle(self) -> Platform {
        match self {
            Platform::Mac => Platform::Windows,
            Platform::Windows => Platform::Neutral,
            Platform::Neutral => Platform::Mac,
        }
    }

    /// The default-mode [`NativeFeel`] spec for this platform.
    pub fn native_feel(self) -> NativeFeel {
        match self {
            Platform::Mac => NativeFeel::macos(),
            Platform::Windows => NativeFeel::windows(),
            Platform::Neutral => NativeFeel::neutral(),
        }
    }
}

/// Where the window-management controls live + their shape. Real per-OS chrome is
/// *host-drawn* (a follow-up); this is the **data hint** so a host (or the demo's
/// faux title-bar) knows which side to render and the look the preset wants.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum WindowControls {
    /// macOS: three coloured "traffic-light" circles, top-**left**.
    TrafficLights,
    /// Windows: minimise/maximise/close caption buttons, top-**right**.
    Caption,
    /// No app-drawn window controls (host/borderless).
    None,
}

impl WindowControls {
    /// Are the controls on the left edge (macOS) vs the right (Windows)?
    pub fn on_left(self) -> bool {
        matches!(self, WindowControls::TrafficLights)
    }
}

/// The keyboard-focus indicator style — macOS draws a soft accent **ring** (a
/// glow halo), Windows 11 a crisp focus **rectangle**. Both read the palette's
/// `accent`; the difference is width/softness/expansion.
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct FocusRing {
    /// Stroke width of the ring/rect, px.
    pub width: f32,
    /// How far the ring sits *outside* the focused rect, px (macOS halo expands;
    /// Windows hugs the control).
    pub expansion: f32,
    /// Soft glow halo (macOS) vs a single crisp stroke (Windows focus rectangle).
    pub glow: bool,
    /// Tint the ring with the palette `accent` (both platforms do; Device uses a
    /// plain high-contrast outline instead).
    pub accent_tinted: bool,
}

impl FocusRing {
    /// macOS: a soft, accent-tinted ~3px ring that expands slightly (a halo).
    pub fn macos() -> Self {
        Self { width: 3.0, expansion: 2.5, glow: true, accent_tinted: true }
    }

    /// Windows 11: a crisp 2px accent focus rectangle hugging the control.
    pub fn windows() -> Self {
        Self { width: 2.0, expansion: 1.0, glow: false, accent_tinted: true }
    }

    /// Neutral: a modest 2px accent ring, no glow.
    pub fn neutral() -> Self {
        Self { width: 2.0, expansion: 1.5, glow: false, accent_tinted: true }
    }
}

/// The **native-feel spec** — the per-platform cues that aren't already captured
/// by [`Metrics`](super::Metrics)/[`ScrollSpec`](super::ScrollSpec)/
/// [`Typography`](super::Typography)/[`SurfaceSpec`](super::SurfaceSpec). Carried
/// on every [`Theme`](super::Theme) so a component (or the demo's chrome) can ask
/// "what does this platform want here?" and drive the existing primitives.
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct NativeFeel {
    /// Which platform this feel models.
    pub platform: Platform,
    /// Windows **reveal highlight**: a cursor-follow edge glow on hover. Drives
    /// [`effects::RevealHighlight`](crate::effects::RevealHighlight). Off on macOS
    /// (it uses a quieter highlight) and Neutral/Device.
    pub reveal_highlight: bool,
    /// macOS **rubber-band** overscroll: the scroll spring is allowed to overshoot
    /// past the edge and settle back. Read by the smooth-scroll glue.
    pub rubber_band: bool,
    /// Window-control chrome hint (host-drawn; see [`WindowControls`]).
    pub window_controls: WindowControls,
    /// Keyboard-focus indicator style (ring vs rectangle).
    pub focus_ring: FocusRing,
    /// How strongly the palette `accent` tints selection/material `∈[0,1]` —
    /// macOS leans on accent more than the more neutral Fluent surfaces.
    pub accent_tint: f32,
    /// Drop-shadow elevation strength `∈[0,1]` — Windows 11 uses pronounced
    /// elevation shadows; macOS keeps them restrained.
    pub elevation: f32,
}

impl Default for NativeFeel {
    fn default() -> Self {
        Platform::detect().native_feel()
    }
}

impl NativeFeel {
    /// **macOS** — vibrancy-forward, accent-tinted soft focus ring, rubber-band
    /// scroll, traffic-light controls, restrained shadow, no reveal highlight.
    pub fn macos() -> Self {
        Self {
            platform: Platform::Mac,
            reveal_highlight: false,
            rubber_band: true,
            window_controls: WindowControls::TrafficLights,
            focus_ring: FocusRing::macos(),
            accent_tint: 0.85,
            elevation: 0.30,
        }
    }

    /// **Windows 11** — Mica/Acrylic, reveal highlight on hover, crisp focus
    /// rectangle, caption buttons, pronounced elevation, no rubber-band.
    pub fn windows() -> Self {
        Self {
            platform: Platform::Windows,
            reveal_highlight: true,
            rubber_band: false,
            window_controls: WindowControls::Caption,
            focus_ring: FocusRing::windows(),
            accent_tint: 0.65,
            elevation: 0.70,
        }
    }

    /// **Neutral** (Linux / cross-platform) — premium but OS-agnostic: no reveal,
    /// no rubber-band, no app-drawn window controls, moderate shadow.
    pub fn neutral() -> Self {
        Self {
            platform: Platform::Neutral,
            reveal_highlight: false,
            rubber_band: false,
            window_controls: WindowControls::None,
            focus_ring: FocusRing::neutral(),
            accent_tint: 0.55,
            elevation: 0.45,
        }
    }
}

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

    #[test]
    fn detect_is_a_valid_platform() {
        // Whatever we compiled on, detect() yields one of the three.
        let p = Platform::detect();
        assert!(matches!(p, Platform::Mac | Platform::Windows | Platform::Neutral));
        // …and the cfg branch agrees with the target we built for.
        #[cfg(target_os = "macos")]
        assert_eq!(p, Platform::Mac);
        #[cfg(target_os = "windows")]
        assert_eq!(p, Platform::Windows);
        #[cfg(not(any(target_os = "macos", target_os = "windows")))]
        assert_eq!(p, Platform::Neutral);
    }

    #[test]
    fn label_round_trips_and_is_fuzzy() {
        for p in [Platform::Mac, Platform::Windows, Platform::Neutral] {
            assert_eq!(Platform::from_label(p.label()), Some(p));
        }
        assert_eq!(Platform::from_label("macOS"), Some(Platform::Mac));
        assert_eq!(Platform::from_label("WIN"), Some(Platform::Windows));
        assert_eq!(Platform::from_label("linux"), Some(Platform::Neutral));
        assert_eq!(Platform::from_label("plan9"), None);
    }

    #[test]
    fn cycle_visits_all_three() {
        let mut seen = std::collections::HashSet::new();
        let mut p = Platform::Mac;
        for _ in 0..3 {
            seen.insert(p.label());
            p = p.cycle();
        }
        assert_eq!(seen.len(), 3, "cycle must visit Mac, Windows, Neutral");
        assert_eq!(p, Platform::Mac, "cycle returns to start after 3 steps");
    }

    #[test]
    fn native_feel_cues_differ_per_platform() {
        let m = NativeFeel::macos();
        let w = NativeFeel::windows();
        // The deltas that ARE the adaptive layer (asserted as data):
        assert!(w.reveal_highlight && !m.reveal_highlight, "reveal is a Windows cue");
        assert!(m.rubber_band && !w.rubber_band, "rubber-band is a macOS cue");
        assert_ne!(m.window_controls, w.window_controls, "controls live on opposite sides");
        assert!(m.window_controls.on_left() && !w.window_controls.on_left());
        assert!(m.focus_ring.glow && !w.focus_ring.glow, "mac ring glows; win is a crisp rect");
        assert!(w.elevation > m.elevation, "Windows elevation shadow is heavier");
        assert!(m.accent_tint > w.accent_tint, "macOS leans on accent more");
    }

    #[test]
    fn native_feel_serde_round_trips() {
        let f = NativeFeel::macos();
        let json = serde_json::to_string(&f).unwrap();
        let back: NativeFeel = serde_json::from_str(&json).unwrap();
        assert_eq!(f, back);
    }
}