Box-Shadow Generator — Rust source
Design layered CSS box-shadows with offset, blur, spread, color, and inset. Live preview and copy-ready CSS.
This is the Rust implementation — the same logic the interactive tool runs, in a shareable, citable form.
// box-shadow-generator — Rust polyglot showcase port.
//
// Pure CSS box-shadow builder. Formats one or more shadow layers and joins
// them into a single CSS box-shadow value. Deterministic, dependency-free
// (Rust stdlib only — no `regex` crate), and never panics on bad input:
// invalid colors quietly fall back to a neutral translucent black, so a single
// bad color never breaks the whole stack.
//
// This is the Rust sibling of src/lib/boxShadow.ts (the canonical TypeScript
// that powers the live tool). The public surface mirrors the TS: a ShadowLayer
// struct plus parse_color, format_layer, and build_box_shadow. Because the
// stdlib ships no regex engine, the color-shape checks are written by hand —
// they accept exactly what the TS regexes do, nothing more.
//
// Ported from src/lib/boxShadow.ts.
// Display source — part of CosmoDev's polyglot tool pages.
/// A single layer in a CSS box-shadow stack.
#[derive(Debug, Clone, PartialEq)]
pub struct ShadowLayer {
pub inset: bool, // draw the shadow inside the box
pub offset_x: f64, // horizontal offset in px
pub offset_y: f64, // vertical offset in px
pub blur: f64, // blur radius in px
pub spread: f64, // spread distance in px
pub color: String, // any CSS color (named, hex, rgb(), hsl(), ...)
}
/// Outcome of validating a color string. `error` is `None` when `ok` is true,
/// mirroring the TypeScript `{ ok: boolean; error: string | null }` shape.
#[derive(Debug, Clone, PartialEq)]
pub struct ColorResult {
pub ok: bool,
pub error: Option<String>,
}
impl ColorResult {
fn ok_value() -> Self {
ColorResult { ok: true, error: None }
}
fn err_value(msg: String) -> Self {
ColorResult { ok: false, error: Some(msg) }
}
}
/// CSS named colors accepted without further inspection. parse_color()
/// lower-cases its input first, so the comparison is effectively
/// case-insensitive.
const NAMED_COLORS: &[&str] = &[
"transparent", "black", "white", "red", "green", "blue", "yellow",
"orange", "purple", "pink", "gray", "grey", "brown", "cyan", "magenta",
];
/// Matches the TS `^#[0-9a-f]{3}([0-9a-f]{3})?$` and `^#[0-9a-f]{8}$` rules:
/// a leading '#' followed by exactly 3, 6, or 8 lower-case hex digits.
/// `s` must already be lower-cased, as in the TS original.
fn is_hex_color(s: &str) -> bool {
let chars: Vec<char> = s.chars().collect();
if chars.len() < 2 || chars[0] != '#' {
return false;
}
let n = chars.len() - 1;
if n != 3 && n != 6 && n != 8 {
return false;
}
chars[1..]
.iter()
.all(|&c| c.is_ascii_digit() || ('a'..='f').contains(&c))
}
/// Matches the TS `^rgba?\([^)]+\)$` / `^hsla?\([^)]+\)$` rules: a function
/// name, an opening paren, one or more characters that are not ')', and a
/// closing paren at the very end. The inner contents are otherwise unchecked,
/// matching the live tool's permissive behavior. `prefixes` should list the
/// longer form first (e.g. "rgba" before "rgb") so each is tried independently.
fn is_function_color(s: &str, prefixes: &[&str]) -> bool {
for &prefix in prefixes {
if let Some(rest) = s.strip_prefix(prefix) {
let chars: Vec<char> = rest.chars().collect();
// Need '(' ... ')' with at least one char strictly between. The TS
// `[^)]+` forbids any ')' inside; combined with the trailing `)$`
// anchor, the closing paren must be the final character.
if chars.len() >= 3 && chars[0] == '(' && chars[chars.len() - 1] == ')' {
let inside: String = chars[1..chars.len() - 1].iter().collect();
if !inside.is_empty() && !inside.contains(')') {
return true;
}
}
}
}
false
}
/// Validate a CSS color string. Accepts the curated named-color set plus hex
/// (#rgb, #rrggbb, #rrggbbaa), rgb()/rgba(), and hsl()/hsla() forms. Input is
/// trimmed and lower-cased before matching, exactly like the TS original.
pub fn parse_color(color: &str) -> ColorResult {
let c = color.trim().to_lowercase();
if c.is_empty() {
return ColorResult::err_value("empty color".to_string());
}
if NAMED_COLORS.contains(&c.as_str()) {
return ColorResult::ok_value();
}
if is_hex_color(&c) {
return ColorResult::ok_value();
}
if is_function_color(&c, &["rgba", "rgb"]) || is_function_color(&c, &["hsla", "hsl"]) {
return ColorResult::ok_value();
}
ColorResult::err_value(format!("invalid color: {}", color))
}
/// Keep a color when it parses and otherwise substitute a neutral translucent
/// black. This is what makes `build_box_shadow` total over arbitrary input.
fn normalize_color(color: &str) -> String {
if parse_color(color).ok {
color.trim().to_string()
} else {
"rgba(0,0,0,0.5)".to_string()
}
}
/// Render an f64 the way JavaScript's template literals would: whole numbers
/// drop the decimal point (5, not 5.0). Rust's default `{}` Display for floats
/// already does this, so we lean on it.
fn format_number(v: f64) -> String {
format!("{}", v)
}
/// Render one shadow layer as its CSS fragment, e.g.
/// "inset 4px 8px 16px 0px #1a2b3c" or "0px 2px 4px 0px rgba(0,0,0,0.5)".
pub fn format_layer(layer: &ShadowLayer) -> String {
format!(
"{}{}px {}px {}px {}px {}",
if layer.inset { "inset " } else { "" },
format_number(layer.offset_x),
format_number(layer.offset_y),
format_number(layer.blur),
format_number(layer.spread),
normalize_color(&layer.color),
)
}
/// Compose a full CSS box-shadow declaration from an ordered list of layers
/// (the first layer renders on top). An empty slice yields the CSS keyword
/// "none".
pub fn build_box_shadow(layers: &[ShadowLayer]) -> String {
if layers.is_empty() {
return "none".to_string();
}
layers
.iter()
.map(format_layer)
.collect::<Vec<_>>()
.join(", ")
}
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →