#![forbid(unsafe_code)]
use makeover_layout::{Bevel, Depth, Fill, Intent};
use std::fmt::Write as _;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Emit {
pub border_width: &'static str,
pub class_prefix: &'static str,
}
impl Default for Emit {
fn default() -> Self {
Self {
border_width: "1px",
class_prefix: "",
}
}
}
#[must_use]
pub fn bevel_var(bevel: Bevel) -> &'static str {
match bevel {
Bevel::Raised => "--bevel-raised",
Bevel::Inset => "--bevel-inset",
}
}
#[must_use]
pub fn fill_var(fill: Fill) -> String {
match fill {
Fill::Well => format!("var(--{}, var(--{}))", fill.token(), Fill::Page.token()),
other => format!("var(--{})", other.token()),
}
}
#[must_use]
pub fn bevel_shadow(bevel: Bevel, opts: &Emit) -> String {
let (top_left, bottom_right) = bevel.edges();
let w = opts.border_width;
format!(
"inset {w} {w} 0 var(--{}), inset -{w} -{w} 0 var(--{})",
top_left.token(),
bottom_right.token()
)
}
#[must_use]
pub fn bevel_properties(opts: &Emit) -> String {
let mut css = String::new();
for bevel in [Bevel::Raised, Bevel::Inset] {
let _ = writeln!(
css,
" {}: {};",
bevel_var(bevel),
bevel_shadow(bevel, opts)
);
}
css
}
#[must_use]
pub fn depth_class(depth: Depth, opts: &Emit) -> Option<String> {
let name = match depth {
Depth::Flat => return None,
Depth::Raised => "raised",
Depth::Well => "well",
};
Some(format!("{}{name}", opts.class_prefix))
}
#[must_use]
pub fn depth_rules(opts: &Emit) -> String {
let mut css = String::new();
for depth in [Depth::Raised, Depth::Well] {
let Some(class) = depth_class(depth, opts) else {
continue;
};
let (Some(fill), Some(bevel)) = (depth.fill(), depth.bevel()) else {
continue;
};
let _ = writeln!(
css,
".{class} {{\n background: {};\n box-shadow: var({});\n}}",
fill_var(fill),
bevel_var(bevel)
);
}
if let (Some(raised), Some(pressed)) = (
depth_class(Depth::Raised, opts),
Depth::Raised.pressed().bevel(),
) {
let _ = writeln!(
css,
".{raised}:active {{\n box-shadow: var({});\n}}",
bevel_var(pressed)
);
}
css
}
#[must_use]
pub fn stylesheet(opts: &Emit) -> String {
format!(
"/* Generated by makeover-webview from makeover-layout. Do not edit.\n \
Depth is a fill and an edge together; naming them apart is what let\n \
them disagree. See the crate's README and wiki note makeover-layout. */\n\
:root {{\n{}}}\n\n{}",
bevel_properties(opts),
depth_rules(opts)
)
}
#[cfg(test)]
mod tests {
use super::*;
use makeover_layout::Edge;
#[test]
fn the_emitted_bevel_matches_what_the_apps_already_hand_write() {
let opts = Emit::default();
assert_eq!(
bevel_shadow(Bevel::Raised, &opts),
"inset 1px 1px 0 var(--bevel-light), inset -1px -1px 0 var(--bevel-dark)"
);
assert_eq!(
bevel_shadow(Bevel::Inset, &opts),
"inset 1px 1px 0 var(--bevel-dark), inset -1px -1px 0 var(--bevel-light)"
);
}
#[test]
fn no_colour_ever_reaches_the_output() {
let css = stylesheet(&Emit::default());
assert!(!css.contains('#'), "a hex literal escaped into the CSS");
assert!(
!css.contains("rgb"),
"a colour function escaped into the CSS"
);
assert!(css.contains("var(--surface-raised)"));
assert!(css.contains("var(--bevel-light)"));
}
#[test]
fn a_well_falls_back_through_css_rather_than_through_rust() {
assert_eq!(
fill_var(Fill::Well),
"var(--surface-well, var(--surface-page))"
);
assert_eq!(fill_var(Fill::Raised), "var(--surface-raised)");
assert_eq!(fill_var(Fill::Page), "var(--surface-page)");
}
#[test]
fn raised_and_well_do_not_collapse_onto_each_other() {
let css = depth_rules(&Emit::default());
assert!(css.contains(".raised {"));
assert!(css.contains(".well {"));
assert!(css.contains("var(--bevel-raised)"));
assert!(css.contains("var(--bevel-inset)"));
}
#[test]
fn the_cascade_carries_the_pressed_state() {
let css = depth_rules(&Emit::default());
assert!(css.contains(".raised:active {"));
}
#[test]
fn flat_emits_nothing_at_all() {
assert_eq!(depth_class(Depth::Flat, &Emit::default()), None);
assert!(!depth_rules(&Emit::default()).contains("flat"));
}
#[test]
fn a_prefix_namespaces_every_class() {
let opts = Emit {
class_prefix: "mo-",
..Emit::default()
};
let css = depth_rules(&opts);
assert!(css.contains(".mo-raised {"));
assert!(css.contains(".mo-well {"));
assert!(!css.contains(".raised {"));
}
#[test]
fn the_border_width_is_the_callers() {
let opts = Emit {
border_width: "2px",
..Emit::default()
};
assert!(bevel_shadow(Bevel::Raised, &opts).contains("inset 2px 2px 0"));
}
#[test]
fn edges_agree_with_the_description() {
let (tl, br) = Bevel::Raised.edges();
assert_eq!(tl.token(), Edge::Light.token());
assert_eq!(br.token(), Edge::Dark.token());
}
}