facett-core 0.1.6

facett — visual kernel: render a node/edge Scene into egui (wgpu fast path to come)
Documentation
//! **Policy sub-structs** — the smaller serialisable pieces of [`Theme`](super::Theme):
//! [`ThemeMode`], [`FocusSpec`] (§13), [`SurfaceSpec`] (§5/§20), [`Motion`],
//! [`EffectsPolicy`] (gates glass/blur/motion), and [`PerfConfig`] (§6).

use egui::Color32;
use serde::{Deserialize, Serialize};

/// Light / dark / follow the OS.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum ThemeMode {
    Light,
    Dark,
    FollowSystem,
}

/// How aggressively decorative effects (glass, blur, transparency, decorative
/// motion) are allowed. `None` is the Device preset (§23): GPU still allowed for
/// speed, just no eye-candy.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum EffectsPolicy {
    /// Full glass/blur/transparency/decorative motion.
    Full,
    /// Transparency + light motion, no blur (accessibility "reduce motion").
    Reduced,
    /// Nothing decorative — opaque, crisp, deterministic (Device / §23).
    None,
}

impl EffectsPolicy {
    /// May translucent fills / opacity be used?
    pub fn allows_transparency(self) -> bool {
        matches!(self, EffectsPolicy::Full | EffectsPolicy::Reduced)
    }
    /// May real backdrop blur (frosted glass) be used?
    pub fn allows_blur(self) -> bool {
        matches!(self, EffectsPolicy::Full)
    }
    /// May decorative (non-functional) motion play?
    pub fn allows_decorative_motion(self) -> bool {
        matches!(self, EffectsPolicy::Full)
    }
}

/// Focus, pane-nav, hints, revolver and form policy (§13). Off-by-default for the
/// optional surfaces so existing behaviour is unchanged until a host opts in.
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct FocusSpec {
    /// Show the which-key / Vimium-style hint overlay when triggered (FOC-3).
    pub hints_enabled: bool,
    /// The single character labels cycle through for hint badges.
    pub hint_alphabet: [char; 8],
    /// Bring a focused pane to centre/foreground (revolver, FOC-4). Disabled when
    /// `EffectsPolicy::None`.
    pub revolver_enabled: bool,
    /// Directional focus picks the nearest rect in that direction (FOC-2).
    pub spatial_nav: bool,
}

impl Default for FocusSpec {
    fn default() -> Self {
        Self {
            hints_enabled: false,
            hint_alphabet: ['a', 's', 'd', 'f', 'j', 'k', 'l', ';'],
            revolver_enabled: false,
            spatial_nav: true,
        }
    }
}

/// Surface / overlay defaults (§5, §20). A guest layered over a host can be
/// opaque (default), tinted (all backends), or frosted (wgpu-only, degrades to
/// tint). Effects here are gated by [`EffectsPolicy`].
#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
pub enum SurfaceSpec {
    /// Fully opaque (the default; Device forces this).
    #[default]
    Opaque,
    /// Per-subtree opacity multiplier in [0,1].
    Opacity(f32),
    /// Translucent fill of `(rgba)` over the host — works on all backends.
    Tint([u8; 4]),
    /// Real backdrop blur (wgpu-only); degrades to `Tint(tint)` off-wgpu.
    Frosted { blur_radius: f32, tint: [u8; 4] },
}

impl SurfaceSpec {
    /// Resolve this surface under an [`EffectsPolicy`]: when effects are off we
    /// always render opaque; frosted degrades to its tint when blur isn't allowed.
    pub fn resolve(self, policy: EffectsPolicy) -> SurfaceSpec {
        match self {
            SurfaceSpec::Opaque => SurfaceSpec::Opaque,
            _ if !policy.allows_transparency() => SurfaceSpec::Opaque,
            SurfaceSpec::Frosted { tint, .. } if !policy.allows_blur() => SurfaceSpec::Tint(tint),
            other => other,
        }
    }

    /// The tint colour to paint over the host, if this surface paints one.
    pub fn tint_color(self) -> Option<Color32> {
        match self {
            SurfaceSpec::Tint(c) | SurfaceSpec::Frosted { tint: c, .. } => {
                Some(Color32::from_rgba_unmultiplied(c[0], c[1], c[2], c[3]))
            }
            _ => None,
        }
    }
}

/// Animation timing (§3). `decorative` motion is gated by [`EffectsPolicy`];
/// functional motion (e.g. smooth scroll) reads these durations directly.
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct Motion {
    /// Standard transition duration (seconds).
    pub duration: f32,
    /// A faster duration for small/cheap transitions.
    pub fast: f32,
}

impl Default for Motion {
    fn default() -> Self {
        Self { duration: 0.18, fast: 0.10 }
    }
}

/// Performance budgets / toggles (§6). The grid + plots read these to bound work.
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct PerfConfig {
    /// Target frame budget in milliseconds (60 fps ≈ 16.7 ms).
    pub frame_budget_ms: f32,
    /// Extra rows rendered above/below the viewport to hide fast-scroll seams.
    pub overscan_rows: usize,
    /// Prefer the wgpu backend for custom GPU paths where available (§6/§23).
    pub prefer_wgpu: bool,
}

impl Default for PerfConfig {
    fn default() -> Self {
        Self { frame_budget_ms: 16.7, overscan_rows: 2, prefer_wgpu: true }
    }
}

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

    #[test]
    fn effects_none_disables_everything_decorative() {
        let p = EffectsPolicy::None;
        assert!(!p.allows_transparency());
        assert!(!p.allows_blur());
        assert!(!p.allows_decorative_motion());
    }

    #[test]
    fn frosted_degrades_to_tint_without_blur() {
        let frosted = SurfaceSpec::Frosted { blur_radius: 8.0, tint: [10, 12, 26, 180] };
        // Reduced allows transparency but not blur → falls back to its tint.
        assert_eq!(frosted.resolve(EffectsPolicy::Reduced), SurfaceSpec::Tint([10, 12, 26, 180]));
        // None forces opaque.
        assert_eq!(frosted.resolve(EffectsPolicy::None), SurfaceSpec::Opaque);
        // Full keeps it frosted.
        assert_eq!(frosted.resolve(EffectsPolicy::Full), frosted);
    }

    #[test]
    fn tint_forced_opaque_under_none() {
        let tint = SurfaceSpec::Tint([1, 2, 3, 200]);
        assert_eq!(tint.resolve(EffectsPolicy::None), SurfaceSpec::Opaque);
        assert_eq!(tint.resolve(EffectsPolicy::Full), tint);
    }
}