AkurAI Build
Menu

Public repository / master

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

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

//! Interactivity, outlines, transitions and animation: cursor, pointer events,
//! user-select, resize, appearance, outline, transitions (duration/ease/delay),
//! `animate-spin|pulse|ping` (their `@keyframes` emitted once), will-change,
//! scroll behaviour/snap and touch-action.
//!
//! `transition-*` reads `--tw-duration`/`--tw-ease` with defaults, and
//! `duration-*`/`ease-*` set both the variable and the property, so
//! `duration-300 transition` works in either source order.

use super::value::{arbitrary, integer};
use super::{keyword, register, Utility};

const TIMING_PROPS: &[&str] = &[register!("--tw-duration"), register!("--tw-ease")];
const OUTLINE_STYLE_PROPS: &[&str] = &[register!("--tw-outline-style")];

const KEYWORDS: &[(&str, &str)] = &[
    ("cursor-auto", "cursor:auto"),
    ("cursor-default", "cursor:default"),
    ("cursor-pointer", "cursor:pointer"),
    ("cursor-wait", "cursor:wait"),
    ("cursor-text", "cursor:text"),
    ("cursor-move", "cursor:move"),
    ("cursor-help", "cursor:help"),
    ("cursor-not-allowed", "cursor:not-allowed"),
    ("cursor-grab", "cursor:grab"),
    ("cursor-grabbing", "cursor:grabbing"),
    ("cursor-col-resize", "cursor:col-resize"),
    ("cursor-row-resize", "cursor:row-resize"),
    ("pointer-events-none", "pointer-events:none"),
    ("pointer-events-auto", "pointer-events:auto"),
    ("select-none", "user-select:none"),
    ("select-text", "user-select:text"),
    ("select-all", "user-select:all"),
    ("select-auto", "user-select:auto"),
    ("resize", "resize:both"),
    ("resize-none", "resize:none"),
    ("resize-x", "resize:horizontal"),
    ("resize-y", "resize:vertical"),
    ("appearance-none", "appearance:none"),
    ("appearance-auto", "appearance:auto"),
    (
        "outline-none",
        "outline:2px solid transparent;outline-offset:2px",
    ),
    ("transition-none", "transition-property:none"),
    ("animate-none", "animation:none"),
    ("will-change-auto", "will-change:auto"),
    ("will-change-scroll", "will-change:scroll-position"),
    ("will-change-contents", "will-change:contents"),
    ("will-change-transform", "will-change:transform"),
    ("scroll-smooth", "scroll-behavior:smooth"),
    ("scroll-auto", "scroll-behavior:auto"),
    ("snap-none", "scroll-snap-type:none"),
    (
        "snap-x",
        "scroll-snap-type:x var(--tw-scroll-snap-strictness,proximity)",
    ),
    (
        "snap-y",
        "scroll-snap-type:y var(--tw-scroll-snap-strictness,proximity)",
    ),
    (
        "snap-both",
        "scroll-snap-type:both var(--tw-scroll-snap-strictness,proximity)",
    ),
    ("snap-mandatory", "--tw-scroll-snap-strictness:mandatory"),
    ("snap-proximity", "--tw-scroll-snap-strictness:proximity"),
    ("snap-start", "scroll-snap-align:start"),
    ("snap-end", "scroll-snap-align:end"),
    ("snap-center", "scroll-snap-align:center"),
    ("snap-align-none", "scroll-snap-align:none"),
    ("touch-auto", "touch-action:auto"),
    ("touch-none", "touch-action:none"),
    ("touch-pan-x", "touch-action:pan-x"),
    ("touch-pan-y", "touch-action:pan-y"),
    ("touch-manipulation", "touch-action:manipulation"),
];

const DEFAULT_EASE: &str = "cubic-bezier(0.4,0,0.2,1)";
const DEFAULT_DURATION: &str = "150ms";

/// `transition-*` → transitioned properties. The bare `transition` lists the
/// individual transform properties too, since `translate-*`/`scale-*`/
/// `rotate-*` use them.
const TRANSITIONS: &[(&str, &str)] = &[
    (
        "",
        "color,background-color,border-color,text-decoration-color,fill,stroke,opacity,\
         box-shadow,transform,translate,scale,rotate,filter,backdrop-filter",
    ),
    ("all", "all"),
    (
        "colors",
        "color,background-color,border-color,text-decoration-color,fill,stroke",
    ),
    ("opacity", "opacity"),
    ("shadow", "box-shadow"),
    ("transform", "transform,translate,scale,rotate"),
];

const EASES: &[(&str, &str)] = &[
    ("linear", "linear"),
    ("in", "cubic-bezier(0.4,0,1,1)"),
    ("out", "cubic-bezier(0,0,0.2,1)"),
    ("in-out", "cubic-bezier(0.4,0,0.2,1)"),
];

/// `(name, animation, keyframes)`.
const ANIMATIONS: &[(&str, &str, &str)] = &[
    (
        "spin",
        "spin 1s linear infinite",
        "@keyframes spin{to{transform:rotate(360deg)}}",
    ),
    (
        "ping",
        "ping 1s cubic-bezier(0,0,0.2,1) infinite",
        "@keyframes ping{75%,100%{transform:scale(2);opacity:0}}",
    ),
    (
        "pulse",
        "pulse 2s cubic-bezier(0.4,0,0.6,1) infinite",
        "@keyframes pulse{50%{opacity:.5}}",
    ),
];

pub fn resolve(u: &str) -> Option<Utility> {
    keyword(KEYWORDS, u)
        .or_else(|| outline(u))
        .or_else(|| transition(u))
        .or_else(|| timing(u))
        .or_else(|| animation(u))
}

/// `outline`, `outline-2`, `outline-dashed`, `outline-offset-2`.
fn outline(u: &str) -> Option<Utility> {
    if let Some(v) = u.strip_prefix("outline-offset-") {
        let n = integer(v)?;
        return Some(Utility::decl("outline-offset", &format!("{n}px")));
    }
    let width = match u.strip_prefix("outline") {
        Some("") => "1px".to_string(),
        Some(rest) => {
            let v = rest.strip_prefix('-')?;
            if let s @ ("solid" | "dashed" | "dotted" | "double") = v {
                return Some(Utility::decl("--tw-outline-style", s).extras(OUTLINE_STYLE_PROPS));
            }
            format!("{}px", integer(v)?)
        }
        None => return None,
    };
    Some(Utility::from_decls(vec![
        "outline-style:var(--tw-outline-style,solid)".into(),
        format!("outline-width:{width}"),
    ]))
}

fn transition(u: &str) -> Option<Utility> {
    let kind = match u.strip_prefix("transition") {
        Some("") => "",
        Some(rest) => rest.strip_prefix('-')?,
        None => return None,
    };
    let (_, props) = TRANSITIONS.iter().find(|(k, _)| *k == kind)?;
    Some(
        Utility::from_decls(vec![
            format!("transition-property:{props}"),
            format!("transition-timing-function:var(--tw-ease,{DEFAULT_EASE})"),
            format!("transition-duration:var(--tw-duration,{DEFAULT_DURATION})"),
        ])
        .extras(TIMING_PROPS),
    )
}

/// `duration-200`, `ease-in-out`, `delay-75`.
fn timing(u: &str) -> Option<Utility> {
    if let Some(v) = u.strip_prefix("duration-") {
        let ms = ms_value(v)?;
        return Some(
            Utility::from_decls(vec![
                format!("--tw-duration:{ms}"),
                format!("transition-duration:{ms}"),
            ])
            .extras(TIMING_PROPS),
        );
    }
    if let Some(v) = u.strip_prefix("ease-") {
        let curve = match EASES.iter().find(|(k, _)| *k == v) {
            Some((_, c)) => (*c).to_string(),
            None => arbitrary(v)?,
        };
        return Some(
            Utility::from_decls(vec![
                format!("--tw-ease:{curve}"),
                format!("transition-timing-function:{curve}"),
            ])
            .extras(TIMING_PROPS),
        );
    }
    let ms = ms_value(u.strip_prefix("delay-")?)?;
    Some(Utility::decl("transition-delay", &ms))
}

fn ms_value(v: &str) -> Option<String> {
    integer(v)
        .map(|n| format!("{n}ms"))
        .or_else(|| arbitrary(v))
}

fn animation(u: &str) -> Option<Utility> {
    let name = u.strip_prefix("animate-")?;
    let (_, anim, keyframes) = ANIMATIONS.iter().find(|(k, _, _)| *k == name)?;
    Some(Utility::decl("animation", anim).extras(&[keyframes]))
}

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

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

    #[test]
    fn outline_forms() {
        assert_eq!(
            decl("outline").as_deref(),
            Some("outline-style:var(--tw-outline-style,solid);outline-width:1px")
        );
        assert_eq!(
            decl("outline-dashed").as_deref(),
            Some("--tw-outline-style:dashed")
        );
        assert_eq!(decl("outline-accent"), None);
        assert_eq!(decl("outline-"), None);
    }

    #[test]
    fn timing_values() {
        assert_eq!(decl("delay-[1s]").as_deref(), Some("transition-delay:1s"));
        assert_eq!(decl("duration-x"), None);
        assert_eq!(decl("ease-bounce"), None);
        assert_eq!(decl("transition-wat"), None);
    }

    #[test]
    fn animations_carry_keyframes() {
        let spin = resolve("animate-spin").unwrap();
        assert_eq!(
            spin.extras,
            vec!["@keyframes spin{to{transform:rotate(360deg)}}"]
        );
        assert_eq!(decl("animate-bounce"), None);
    }
}