sequoia-style 0.1.1

Sequoia GUI framework — styles and themes
Documentation
#![forbid(unsafe_code)]

use sequoia_core::SpanMask;

use crate::effect::Backdrop;

#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub enum Mask {
    LinearFade {
        from_alpha: f32,
        to_alpha: f32,
        angle_rad: f32,
    },

    Uniform(f32),
}

impl Mask {
    pub fn strength_at(&self, t: f32) -> f32 {
        let t = t.clamp(0.0, 1.0);
        match *self {
            Mask::LinearFade {
                from_alpha,
                to_alpha,
                ..
            } => from_alpha + (to_alpha - from_alpha) * t,
            Mask::Uniform(v) => v.clamp(0.0, 1.0),
        }
    }

    pub fn span_mask(&self) -> SpanMask {
        match *self {
            Mask::LinearFade {
                from_alpha,
                to_alpha,
                angle_rad,
            } => SpanMask::Linear {
                from: from_alpha,
                to: to_alpha,
                angle_rad,
            },
            Mask::Uniform(v) => SpanMask::Uniform(v),
        }
    }
}

pub const VERTICAL: f32 = std::f32::consts::FRAC_PI_2;

pub fn gradient_mask<G>(gradient: G) -> Mask
where
    G: GradientAlphas,
{
    let (a, b) = gradient.endpoint_alphas();
    let (dx, dy) = gradient.direction();
    let angle = if dx == 0.0 && dy == 0.0 {
        VERTICAL
    } else {
        dy.atan2(dx)
    };
    Mask::LinearFade {
        from_alpha: a.clamp(0.0, 1.0),
        to_alpha: b.clamp(0.0, 1.0),
        angle_rad: angle,
    }
}

pub trait GradientAlphas {
    fn endpoint_alphas(&self) -> (f32, f32);

    fn direction(&self) -> (f32, f32) {
        (0.0, 1.0)
    }
}

#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Filter {
    pub blur: f32,
    pub mask: Option<Mask>,
}

impl Filter {
    pub const NONE: Self = Self {
        blur: 0.0,
        mask: None,
    };

    pub const fn blur(radius: f32) -> Self {
        Self {
            blur: if radius > 0.0 { radius } else { 0.0 },
            mask: None,
        }
    }

    pub fn blur_radius(mut self, radius: f32) -> Self {
        self.blur = radius.max(0.0);
        self
    }

    pub fn mask(mut self, mask: Mask) -> Self {
        self.mask = Some(mask);
        self
    }

    pub fn is_empty(&self) -> bool {
        !(self.blur > 0.0 && self.blur.is_finite())
    }

    pub fn blur_px(&self) -> f32 {
        if self.blur.is_finite() {
            self.blur.max(0.0)
        } else {
            0.0
        }
    }

    pub fn span_mask(&self) -> SpanMask {
        self.mask.map(|m| m.span_mask()).unwrap_or(SpanMask::None)
    }
}

#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub enum Effect {
    Blur(Filter),
    Shadow(crate::ShadowSpec),
    Backdrop(Backdrop),
}

impl Effect {
    pub fn is_empty(&self) -> bool {
        match self {
            Effect::Blur(b) => b.is_empty(),
            Effect::Shadow(s) => !s.is_visible(),
            Effect::Backdrop(b) => b.is_empty(),
        }
    }
}

pub fn blur(radius: f32) -> Filter {
    Filter::blur(radius.max(0.0))
}

pub fn gradient(from_alpha: f32, to_alpha: f32) -> Mask {
    gradient_angled(from_alpha, to_alpha, VERTICAL)
}

pub fn gradient_angled(from_alpha: f32, to_alpha: f32, angle_rad: f32) -> Mask {
    Mask::LinearFade {
        from_alpha: from_alpha.clamp(0.0, 1.0),
        to_alpha: to_alpha.clamp(0.0, 1.0),
        angle_rad,
    }
}

pub fn backdrop_blur(radius: f32) -> Backdrop {
    Backdrop::blur(radius)
}

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

    #[test]
    fn filter_vs_backdrop_semantics() {
        assert!(Filter::NONE.is_empty());
        assert!(!Filter::blur(12.0).is_empty());
        assert!(Backdrop::NONE.is_empty());
        assert!(!Backdrop::blur(4.0).is_empty());

        assert!(Backdrop::NONE.radius(12.0).is_empty());
    }

    #[test]
    fn blur_mask_builder() {
        let b = blur(30.0).mask(gradient(1.0, 0.0));
        assert_eq!(b.blur, 30.0);
        assert!(b.mask.is_some());
        let m = b.mask.as_ref().unwrap();
        assert_eq!(m.strength_at(0.0), 1.0, "the mask holds the ramp");
        assert_eq!(m.strength_at(1.0), 0.0);
        assert_eq!(
            m.span_mask(),
            SpanMask::Linear {
                from: 1.0,
                to: 0.0,
                angle_rad: VERTICAL,
            },
            "pionowa rampa do GPU"
        );
    }

    #[test]
    fn angled_mask_encodes_direction() {
        let m = gradient_angled(0.0, 1.0, 0.0);
        assert_eq!(m.span_mask().mask_vec(), [1.0, 0.0, 1.0, 0.0]);
        assert_eq!(Filter::NONE.span_mask(), SpanMask::None);
        assert_eq!(
            Backdrop::blur(4.0).span_mask(),
            SpanMask::None,
            "no mask, full strength"
        );
    }

    #[test]
    fn backdrop_builder_with_mask() {
        let f = Backdrop::blur(16.0)
            .dim(Color::BLACK)
            .noise(0.06)
            .radius(12.0)
            .mask(gradient(1.0, 0.0));
        assert_eq!(f.blur, 16.0);
        assert_eq!(f.radius, 12.0);
        assert!(!f.is_empty());
        assert!(f.mask.is_some());
        assert!(f.span_mask() != SpanMask::None);
    }
}