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);
}
}