AkurAI Build
Menu

Public repository / master

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

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

//! Layout: display, position, inset/offsets, z-index, overflow, visibility,
//! box model, aspect ratio, object fit and table layout.

use super::value::{arbitrary, integer, length_or_fraction, negate};
use super::{keyword, prefixed, Utility};

const KEYWORDS: &[(&str, &str)] = &[
    // display
    ("block", "display:block"),
    ("inline-block", "display:inline-block"),
    ("inline", "display:inline"),
    ("flex", "display:flex"),
    ("inline-flex", "display:inline-flex"),
    ("grid", "display:grid"),
    ("inline-grid", "display:inline-grid"),
    ("hidden", "display:none"),
    ("contents", "display:contents"),
    ("flow-root", "display:flow-root"),
    ("list-item", "display:list-item"),
    ("table", "display:table"),
    ("table-row", "display:table-row"),
    ("table-cell", "display:table-cell"),
    (
        "sr-only",
        "position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;\
         clip:rect(0,0,0,0);white-space:nowrap;border-width:0",
    ),
    (
        "not-sr-only",
        "position:static;width:auto;height:auto;padding:0;margin:0;overflow:visible;\
         clip:auto;white-space:normal",
    ),
    // position
    ("static", "position:static"),
    ("relative", "position:relative"),
    ("absolute", "position:absolute"),
    ("fixed", "position:fixed"),
    ("sticky", "position:sticky"),
    // visibility / isolation / box
    ("visible", "visibility:visible"),
    ("invisible", "visibility:hidden"),
    ("collapse", "visibility:collapse"),
    ("isolate", "isolation:isolate"),
    ("isolation-auto", "isolation:auto"),
    ("box-border", "box-sizing:border-box"),
    ("box-content", "box-sizing:content-box"),
    // aspect / object
    ("aspect-auto", "aspect-ratio:auto"),
    ("aspect-square", "aspect-ratio:1 / 1"),
    ("aspect-video", "aspect-ratio:16 / 9"),
    ("object-contain", "object-fit:contain"),
    ("object-cover", "object-fit:cover"),
    ("object-fill", "object-fit:fill"),
    ("object-none", "object-fit:none"),
    ("object-scale-down", "object-fit:scale-down"),
    ("object-center", "object-position:center"),
    ("object-top", "object-position:top"),
    ("object-bottom", "object-position:bottom"),
    // tables
    ("border-collapse", "border-collapse:collapse"),
    ("border-separate", "border-collapse:separate"),
    ("table-auto", "table-layout:auto"),
    ("table-fixed", "table-layout:fixed"),
    // z
    ("z-auto", "z-index:auto"),
];

const OVERFLOW_VALUES: &[&str] = &["auto", "hidden", "clip", "visible", "scroll"];

/// Offsets: `(prefix, properties, tier)`.
const INSET: &[(&str, &[&str], u8)] = &[
    ("inset-x", &["left", "right"], 1),
    ("inset-y", &["top", "bottom"], 1),
    ("inset", &["inset"], 0),
    ("top", &["top"], 2),
    ("right", &["right"], 2),
    ("bottom", &["bottom"], 2),
    ("left", &["left"], 2),
];

pub fn resolve(u: &str) -> Option<Utility> {
    keyword(KEYWORDS, u)
        .or_else(|| overflow(u))
        .or_else(|| prefixed(u, INSET, inset_value))
        .or_else(|| z_index(u, false))
}

/// `-top-1/2`, `-inset-x-2`, `-z-10`.
pub fn negative(u: &str) -> Option<Utility> {
    prefixed(u, INSET, |v| negate(&length_or_fraction(v)?)).or_else(|| z_index(u, true))
}

fn inset_value(v: &str) -> Option<String> {
    if v == "auto" {
        return Some("auto".into());
    }
    length_or_fraction(v)
}

/// `overflow-hidden`, `overflow-x-auto`, …
fn overflow(u: &str) -> Option<Utility> {
    let rest = u.strip_prefix("overflow-")?;
    let (prop, v, tier) = match rest.split_once('-') {
        Some(("x", v)) => ("overflow-x", v, 1),
        Some(("y", v)) => ("overflow-y", v, 1),
        _ => ("overflow", rest, 0),
    };
    OVERFLOW_VALUES
        .contains(&v)
        .then(|| Utility::decl(prop, v).tier(tier))
}

/// `z-10`, `z-[999]`, and `-z-10` when `neg`.
fn z_index(u: &str, neg: bool) -> Option<Utility> {
    let v = u.strip_prefix("z-")?;
    let value = match integer(v) {
        Some(n) if neg => format!("-{n}"),
        Some(n) => n.to_string(),
        None if !neg => arbitrary(v)?,
        None => return None,
    };
    Some(Utility::decl("z-index", &value))
}

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

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

    #[test]
    fn overflow_axes() {
        assert_eq!(decl("overflow-auto").as_deref(), Some("overflow:auto"));
        assert_eq!(
            decl("overflow-y-scroll").as_deref(),
            Some("overflow-y:scroll")
        );
        assert_eq!(decl("overflow-z-auto"), None);
        assert_eq!(decl("overflow-x-wat"), None);
    }

    #[test]
    fn insets() {
        assert_eq!(decl("inset-1/2").as_deref(), Some("inset:50%"));
        assert_eq!(decl("left-px").as_deref(), Some("left:1px"));
        let n = |u: &str| negative(u).map(|x| x.declaration());
        assert_eq!(n("inset-x-2").as_deref(), Some("left:-8px;right:-8px"));
        assert_eq!(n("top-auto"), None);
    }

    #[test]
    fn z_values() {
        assert_eq!(decl("z-50").as_deref(), Some("z-index:50"));
        assert_eq!(decl("z-x"), None);
        assert_eq!(negative("z-[3]"), None);
    }
}