facett-core 0.1.4

facett — visual kernel: render a node/edge Scene into egui (wgpu fast path to come)
Documentation
//! **Scrollbars** (§10) — a serialisable [`ScrollSpec`] mapping onto egui's
//! `ScrollStyle` (all 14 fields incl. the six opacity fields) plus a per-preset
//! visibility default. Windows: solid, ~14–16 px, visible. macOS: floating,
//! ~8–10 px, dormant ≈ 0, fade on hover. Device: solid, high-contrast, always
//! visible.

use egui::style::ScrollStyle;
use serde::{Deserialize, Serialize};

/// When the scrollbar shows — maps to egui `ScrollBarVisibility`, applied
/// per-`ScrollArea` (it is not a `ScrollStyle` field in egui 0.34).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum ScrollVisibility {
    AlwaysVisible,
    VisibleWhenNeeded,
    AlwaysHidden,
}

impl ScrollVisibility {
    pub fn to_egui(self) -> egui::scroll_area::ScrollBarVisibility {
        use egui::scroll_area::ScrollBarVisibility as V;
        match self {
            ScrollVisibility::AlwaysVisible => V::AlwaysVisible,
            ScrollVisibility::VisibleWhenNeeded => V::VisibleWhenNeeded,
            ScrollVisibility::AlwaysHidden => V::AlwaysHidden,
        }
    }
}

/// All 14 `ScrollStyle` knobs + the visibility default, serialisable. The six
/// opacity fields make a macOS bar dormant-invisible until hover.
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct ScrollSpec {
    pub floating: bool,
    pub bar_width: f32,
    pub handle_min_length: f32,
    pub bar_inner_margin: f32,
    pub bar_outer_margin: f32,
    pub floating_width: f32,
    pub floating_allocated_width: f32,
    pub foreground_color: bool,
    pub dormant_background_opacity: f32,
    pub active_background_opacity: f32,
    pub interact_background_opacity: f32,
    pub dormant_handle_opacity: f32,
    pub active_handle_opacity: f32,
    pub interact_handle_opacity: f32,
    pub visibility: ScrollVisibility,
}

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

impl ScrollSpec {
    /// Windows: solid, always-present, chunky bar.
    pub fn windows() -> Self {
        Self {
            floating: false,
            bar_width: 14.0,
            handle_min_length: 16.0,
            bar_inner_margin: 4.0,
            bar_outer_margin: 0.0,
            floating_width: 2.0,
            floating_allocated_width: 0.0,
            foreground_color: false,
            dormant_background_opacity: 0.6,
            active_background_opacity: 0.7,
            interact_background_opacity: 0.9,
            dormant_handle_opacity: 0.7,
            active_handle_opacity: 0.9,
            interact_handle_opacity: 1.0,
            visibility: ScrollVisibility::AlwaysVisible,
        }
    }

    /// macOS: floating, slim, dormant-invisible, fades in on hover/scroll.
    pub fn macos() -> Self {
        Self {
            floating: true,
            bar_width: 10.0,
            handle_min_length: 16.0,
            bar_inner_margin: 2.0,
            bar_outer_margin: 0.0,
            floating_width: 8.0,
            floating_allocated_width: 0.0,
            foreground_color: true,
            dormant_background_opacity: 0.0,
            active_background_opacity: 0.0,
            interact_background_opacity: 0.4,
            dormant_handle_opacity: 0.0,
            active_handle_opacity: 0.6,
            interact_handle_opacity: 1.0,
            visibility: ScrollVisibility::VisibleWhenNeeded,
        }
    }

    /// Device: solid, high-contrast, always visible (no fade to find in sun).
    pub fn device() -> Self {
        Self {
            floating: false,
            bar_width: 16.0,
            handle_min_length: 24.0,
            bar_inner_margin: 4.0,
            bar_outer_margin: 0.0,
            floating_width: 2.0,
            floating_allocated_width: 0.0,
            foreground_color: false,
            dormant_background_opacity: 1.0,
            active_background_opacity: 1.0,
            interact_background_opacity: 1.0,
            dormant_handle_opacity: 1.0,
            active_handle_opacity: 1.0,
            interact_handle_opacity: 1.0,
            visibility: ScrollVisibility::AlwaysVisible,
        }
    }

    /// Build an egui [`ScrollStyle`], modelling the 14 work-order fields and
    /// inheriting egui's defaults for the rest (`content_margin`, `fade`, which
    /// 0.34 added beyond the spec). Starting from `solid()`/`floating()` keeps us
    /// forward-compatible with future egui fields.
    pub fn to_scroll_style(&self) -> ScrollStyle {
        let mut s = if self.floating { ScrollStyle::floating() } else { ScrollStyle::solid() };
        s.floating = self.floating;
        s.bar_width = self.bar_width;
        s.handle_min_length = self.handle_min_length;
        s.bar_inner_margin = self.bar_inner_margin;
        s.bar_outer_margin = self.bar_outer_margin;
        s.floating_width = self.floating_width;
        s.floating_allocated_width = self.floating_allocated_width;
        s.foreground_color = self.foreground_color;
        s.dormant_background_opacity = self.dormant_background_opacity;
        s.active_background_opacity = self.active_background_opacity;
        s.interact_background_opacity = self.interact_background_opacity;
        s.dormant_handle_opacity = self.dormant_handle_opacity;
        s.active_handle_opacity = self.active_handle_opacity;
        s.interact_handle_opacity = self.interact_handle_opacity;
        s
    }
}

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

    #[test]
    fn windows_is_solid_and_always_visible() {
        let s = ScrollSpec::windows();
        assert!(!s.floating);
        assert_eq!(s.visibility, ScrollVisibility::AlwaysVisible);
        assert!(s.bar_width >= 14.0);
    }

    #[test]
    fn macos_is_floating_and_dormant_invisible() {
        let s = ScrollSpec::macos();
        assert!(s.floating);
        assert_eq!(s.dormant_handle_opacity, 0.0, "macOS bar fades away when idle");
    }

    #[test]
    fn device_is_always_opaque() {
        let s = ScrollSpec::device();
        assert_eq!(s.dormant_handle_opacity, 1.0);
        assert_eq!(s.visibility, ScrollVisibility::AlwaysVisible);
    }

    #[test]
    fn to_scroll_style_copies_all_fields() {
        let s = ScrollSpec::macos();
        let e = s.to_scroll_style();
        assert_eq!(e.bar_width, s.bar_width);
        assert_eq!(e.floating, s.floating);
        assert_eq!(e.interact_handle_opacity, s.interact_handle_opacity);
    }
}