AkurAI Build
Menu

Public repository / master

AkurAI-Framework / crates/css/src/utilities/effects.rs

Latest change 3aae9f4e85241bcb775c31f5c03094becd3da56a - Pure-Rust UI layer: akurai-ui, akurai-crypto, Tailwind-grade akurai-css by Ólafur Búi Ólafsson

//! Effects: box shadows and rings (composed like Tailwind through
//! `--tw-shadow` / `--tw-ring-*` so `shadow-md ring-2` show both), opacity,
//! blur/backdrop-blur and transforms (`translate-*`, `scale-*`, `rotate-*`).
//!
//! The composed custom properties are registered with `@property … inherits:
//! false` (emitted once at the end of the stylesheet) so a parent's shadow or
//! ring never leaks into its children; every `var()` read has a fallback, so
//! no preflight is needed.

use super::value::{arbitrary, integer, length_or_fraction, negate, trim_float};
use super::{register, Utility};

/// Registrations for every property in the shadow/ring composition.
const BOX_SHADOW_PROPS: &[&str] = &[
    register!("--tw-shadow"),
    register!("--tw-ring-shadow"),
    register!("--tw-ring-offset-shadow"),
    register!("--tw-ring-color"),
    register!("--tw-ring-offset-color"),
    register!("--tw-ring-offset-width"),
    register!("--tw-ring-inset"),
];

const TRANSLATE_PROPS: &[&str] = &[register!("--tw-translate-x"), register!("--tw-translate-y")];

const SHADOWS: &[(&str, &str)] = &[
    ("sm", "0 1px 2px 0 rgba(0,0,0,0.05)"),
    (
        "",
        "0 1px 3px 0 rgba(0,0,0,0.1),0 1px 2px -1px rgba(0,0,0,0.1)",
    ),
    (
        "md",
        "0 4px 6px -1px rgba(0,0,0,0.1),0 2px 4px -2px rgba(0,0,0,0.1)",
    ),
    (
        "lg",
        "0 10px 15px -3px rgba(0,0,0,0.1),0 4px 6px -4px rgba(0,0,0,0.1)",
    ),
    (
        "xl",
        "0 20px 25px -5px rgba(0,0,0,0.1),0 8px 10px -6px rgba(0,0,0,0.1)",
    ),
    ("2xl", "0 25px 50px -12px rgba(0,0,0,0.25)"),
    ("inner", "inset 0 2px 4px 0 rgba(0,0,0,0.05)"),
    ("none", "0 0 #0000"),
];

/// Default `ring` width (Tailwind v3).
const DEFAULT_RING_PX: u32 = 3;

const BLURS: &[(&str, &str)] = &[
    ("none", "0"),
    ("sm", "4px"),
    ("", "8px"),
    ("md", "12px"),
    ("lg", "16px"),
    ("xl", "24px"),
    ("2xl", "40px"),
    ("3xl", "64px"),
];

pub fn resolve(u: &str) -> Option<Utility> {
    shadow(u)
        .or_else(|| ring(u))
        .or_else(|| opacity(u))
        .or_else(|| blur(u))
        .or_else(|| translate(u, false))
        .or_else(|| scale(u))
        .or_else(|| rotate(u, false))
        .or_else(|| (u == "transform-none").then(transform_none))
}

/// `-translate-x-1/2`, `-rotate-45`.
pub fn negative(u: &str) -> Option<Utility> {
    translate(u, true).or_else(|| rotate(u, true))
}

/// Strip `prefix` and return either the `-suffix` or `""` for the bare name
/// (`shadow` ⇒ `""`, `shadow-md` ⇒ `"md"`).
fn suffix<'a>(u: &'a str, prefix: &str) -> Option<&'a str> {
    let rest = u.strip_prefix(prefix)?;
    if rest.is_empty() {
        return Some("");
    }
    rest.strip_prefix('-').filter(|s| !s.is_empty())
}

fn shadow(u: &str) -> Option<Utility> {
    let size = suffix(u, "shadow")?;
    let (_, value) = SHADOWS.iter().find(|(k, _)| *k == size)?;
    Some(
        Utility::from_decls(vec![
            format!("--tw-shadow:{value}"),
            "box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),\
             var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)"
                .to_string(),
        ])
        .extras(BOX_SHADOW_PROPS),
    )
}

/// `ring`, `ring-N`, `ring-[3px]`, `ring-inset`, `ring-offset-N`.
fn ring(u: &str) -> Option<Utility> {
    if u == "ring-inset" {
        return Some(Utility::decl("--tw-ring-inset", "inset").extras(BOX_SHADOW_PROPS));
    }
    if let Some(v) = u.strip_prefix("ring-offset-") {
        let width = px_or_arbitrary(v)?;
        return Some(Utility::decl("--tw-ring-offset-width", &width).extras(BOX_SHADOW_PROPS));
    }
    let width = match suffix(u, "ring")? {
        "" => format!("{DEFAULT_RING_PX}px"),
        v => px_or_arbitrary(v)?,
    };
    Some(
        Utility::from_decls(vec![
            "--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 \
             var(--tw-ring-offset-width,0px) var(--tw-ring-offset-color,var(--bg))"
                .to_string(),
            format!(
                "--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 \
                 calc({width} + var(--tw-ring-offset-width,0px)) \
                 var(--tw-ring-color,var(--accent))"
            ),
            "box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),\
             var(--tw-shadow,0 0 #0000)"
                .to_string(),
        ])
        .extras(BOX_SHADOW_PROPS),
    )
}

/// `N` ⇒ `Npx`, or an arbitrary length.
fn px_or_arbitrary(v: &str) -> Option<String> {
    integer(v).map(|n| format!("{n}px")).or_else(|| {
        let a = arbitrary(v)?;
        super::value::looks_like_length(&a).then_some(a)
    })
}

/// `opacity-0`…`opacity-100` ⇒ `0`…`1`.
fn opacity(u: &str) -> Option<Utility> {
    let v = u.strip_prefix("opacity-")?;
    let value = match integer(v) {
        Some(n) if n <= 100 => trim_float(f64::from(n) / 100.0),
        Some(_) => return None,
        None => arbitrary(v)?,
    };
    Some(Utility::decl("opacity", &value))
}

/// `blur-*` and `backdrop-blur-*`.
fn blur(u: &str) -> Option<Utility> {
    let (backdrop, size) = match suffix(u, "backdrop-blur") {
        Some(size) => (true, size),
        None => (false, suffix(u, "blur")?),
    };
    let radius = match BLURS.iter().find(|(k, _)| *k == size) {
        Some((_, r)) => (*r).to_string(),
        None => arbitrary(size)?,
    };
    let filter = format!("blur({radius})");
    Some(if backdrop {
        Utility::props(&["-webkit-backdrop-filter", "backdrop-filter"], &filter)
    } else {
        Utility::decl("filter", &filter)
    })
}

/// `translate-x-4` ⇒ `--tw-translate-x:16px;translate:var(--tw-translate-x)
/// var(--tw-translate-y,0)`; the axes compose in any order.
fn translate(u: &str, neg: bool) -> Option<Utility> {
    let rest = u.strip_prefix("translate-")?;
    let (axis, v) = rest.split_once('-')?;
    if axis != "x" && axis != "y" {
        return None;
    }
    let mut value = length_or_fraction(v)?;
    if neg {
        value = negate(&value)?;
    }
    let pair = if axis == "x" {
        "var(--tw-translate-x) var(--tw-translate-y,0)"
    } else {
        "var(--tw-translate-x,0) var(--tw-translate-y)"
    };
    Some(
        Utility::from_decls(vec![
            format!("--tw-translate-{axis}:{value}"),
            format!("translate:{pair}"),
        ])
        .extras(TRANSLATE_PROPS),
    )
}

/// `scale-95` ⇒ `scale:95%`.
fn scale(u: &str) -> Option<Utility> {
    let v = u.strip_prefix("scale-")?;
    let value = match integer(v) {
        Some(n) => format!("{n}%"),
        None => arbitrary(v)?,
    };
    Some(Utility::decl("scale", &value))
}

/// `rotate-45` ⇒ `rotate:45deg`.
fn rotate(u: &str, neg: bool) -> Option<Utility> {
    let v = u.strip_prefix("rotate-")?;
    let value = match integer(v) {
        Some(0) => "0deg".to_string(),
        Some(n) if neg => format!("-{n}deg"),
        Some(n) => format!("{n}deg"),
        None if !neg => arbitrary(v)?,
        None => return None,
    };
    Some(Utility::decl("rotate", &value))
}

fn transform_none() -> Utility {
    Utility::body("transform:none;translate:none;scale:none;rotate:none")
}

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

    fn decl(u: &str) -> Option<String> {
        resolve(u).map(|x| x.declaration())
    }

    #[test]
    fn shadow_sizes() {
        assert!(decl("shadow-2xl").unwrap().contains("rgba(0,0,0,0.25)"));
        assert!(decl("shadow-inner")
            .unwrap()
            .starts_with("--tw-shadow:inset"));
        assert_eq!(decl("shadow-huge"), None);
        assert_eq!(decl("shadowy"), None);
        assert_eq!(decl("shadow-"), None);
    }

    #[test]
    fn ring_parsing() {
        assert!(decl("ring-[5px]").unwrap().contains("calc(5px + "));
        assert_eq!(decl("ring-[#fff]"), None);
        assert_eq!(decl("ring-accent"), None);
        assert_eq!(
            decl("ring-offset-4").as_deref(),
            Some("--tw-ring-offset-width:4px")
        );
    }

    #[test]
    fn blur_and_transforms() {
        assert_eq!(decl("blur-none").as_deref(), Some("filter:blur(0)"));
        assert_eq!(decl("blur-[2px]").as_deref(), Some("filter:blur(2px)"));
        assert_eq!(decl("translate-z-4"), None);
        assert_eq!(decl("rotate-[1turn]").as_deref(), Some("rotate:1turn"));
        assert!(negative("translate-y-[3px]")
            .unwrap()
            .declaration()
            .contains("-3px"));
        assert_eq!(
            negative("rotate-0").map(|x| x.declaration()).as_deref(),
            Some("rotate:0deg")
        );
    }
}