use crate::resolve::NodeKind;
use crate::span::Span;
use crate::syntax::ast::{Decl, Value};
pub(crate) const SEQ_GAP_ROW: f64 = 32.0;
pub(crate) const SEQ_GAP_COL: f64 = 32.0;
fn decl(name: &str, values: Vec<Value>) -> Decl {
Decl {
name: name.into(),
groups: vec![values],
span: Span::empty(),
}
}
fn n(name: &str, v: f64) -> Decl {
decl(name, vec![Value::Number(v)])
}
fn id(name: &str, v: &str) -> Decl {
decl(name, vec![Value::Ident(v.into())])
}
fn var(name: &str, v: &str) -> Decl {
decl(name, vec![Value::Var(v.into())])
}
fn pair(name: &str, a: f64, b: f64) -> Decl {
decl(name, vec![Value::Number(a), Value::Number(b)])
}
pub(crate) fn root_layout_defaults(layout: Option<&str>) -> Vec<Decl> {
match layout {
Some("sequence") => vec![pair("gap", SEQ_GAP_ROW, SEQ_GAP_COL)],
Some("drawing") => vec![n("scale", 4.0)],
_ => Vec::new(),
}
}
pub fn primitive_bundle(kind: NodeKind) -> Vec<Decl> {
use NodeKind::*;
let sized = || {
vec![
var("fill", "fill"),
var("stroke", "stroke"),
n("stroke-width", 2.0),
n("padding", 20.0),
n("gap", 20.0),
]
};
match kind {
Block => vec![
id("fill", "none"),
id("stroke", "none"),
n("stroke-width", 2.0),
n("padding", 0.0),
n("gap", 20.0),
],
Oval | Hex | Cyl | Diamond => sized(),
Slant => {
let mut b = sized();
b.push(n("skew", 15.0));
b
}
Poly | Path => vec![
var("fill", "fill"),
var("stroke", "stroke"),
n("stroke-width", 2.0),
],
Sketch => vec![
var("fill", "fill"),
var("stroke", "stroke-dark"),
n("stroke-width", 2.0),
],
Line => vec![
id("fill", "none"),
var("stroke", "stroke"),
n("stroke-width", 2.0),
],
Icon => vec![
var("fill", "icon-fill"),
var("stroke", "stroke"),
n("stroke-width", 2.0),
n("width", 32.0),
n("height", 32.0),
],
Text | Image => Vec::new(),
}
}
pub fn template_bundle(name: &str) -> Vec<Decl> {
match name {
"box" => vec![
var("fill", "fill"),
var("stroke", "stroke"),
n("stroke-width", 2.0),
n("padding", 20.0),
n("radius", 8.0),
],
"rect" => vec![n("radius", 0.0)],
"group" => vec![
var("stroke", "group-stroke"),
id("stroke-style", "dashed"),
n("stroke-width", 1.0),
var("fill", "group-fill"),
n("radius", 8.0),
n("padding", 20.0),
],
"caption" => vec![
decl(
"pin",
vec![Value::Ident("top".into()), Value::Ident("left".into())],
),
pair("translate", 0.0, -18.0),
var("color", "caption-color"),
n("font-size", 12.0),
var("font-weight", "caption-font-weight"),
],
"footnote" => vec![
id("pin", "bottom"),
pair("translate", 0.0, 17.0),
n("font-size", 12.0),
var("color", "footer-color"),
],
"badge" => vec![
decl(
"pin",
vec![Value::Ident("top".into()), Value::Ident("right".into())],
),
pair("translate", 6.0, -6.0),
n("radius", 8.0),
pair("padding", 2.0, 6.0),
decl(
"shadow",
vec![Value::Number(2.0), Value::Number(3.0), Value::Number(3.0)],
),
var("fill", "accent"),
var("color", "accent-text"),
n("font-size", 11.0),
id("font-weight", "normal"),
],
"row" => vec![id("direction", "row")],
"column" => vec![id("direction", "column")],
"grid" => vec![id("layout", "grid")],
"chart" => vec![id("layout", "chart"), n("gap", 10.0)],
"pie" => vec![id("layout", "pie"), n("gap", 10.0)],
"sequence" => vec![
id("layout", "sequence"),
pair("gap", SEQ_GAP_ROW, SEQ_GAP_COL),
],
"note" => vec![
var("fill", "fill"),
var("stroke", "stroke"),
n("padding", 20.0),
n("scale", 1.0),
],
"balloon" => vec![
n("width", 16.0),
var("fill", "fill"),
var("stroke", "stroke"),
n("font-size", 11.0),
n("scale", 1.0),
],
"drawing" => vec![id("layout", "drawing"), n("padding", 0.0), n("scale", 4.0)],
"hole" => vec![var("fill", "bg"), var("stroke", "stroke-dark")],
"centerline" => vec![
id("stroke-style", "center"),
var("stroke", "stroke-light"),
n("stroke-width", 1.0),
id("fill", "none"),
],
"pitch-circle" => vec![
id("stroke-style", "center"),
var("stroke", "stroke-light"),
n("stroke-width", 1.0),
id("fill", "none"),
],
"breakline" => vec![
var("stroke", "stroke-light"),
n("stroke-width", 1.0),
id("fill", "none"),
],
"hidden" => vec![
id("stroke-style", "dashed"),
var("stroke", "stroke-dark"),
n("stroke-width", 1.0),
id("fill", "none"),
],
"shoulder" => vec![var("stroke", "stroke-dark")],
"threadline" => vec![
var("stroke", "stroke-light"),
n("stroke-width", 1.0),
id("fill", "none"),
],
"page" => vec![
n("scale", 4.0),
var("fill", "bg"),
n("stroke-width", 0.0),
n("width", 210.0),
n("height", 297.0),
],
"title-block" => vec![
n("font-size", 11.0),
n("stroke-width", 1.0),
n("radius", 0.0),
],
"frame" => vec![
id("fill", "none"),
var("stroke", "stroke"),
n("stroke-width", 2.0),
],
"zone" => vec![n("font-size", 9.0), var("color", "stroke-light")],
"tick" => vec![
var("stroke", "stroke"),
n("stroke-width", 1.0),
id("fill", "none"),
],
"loop" | "opt" | "alt" => vec![
id("fill", "none"),
var("stroke", "group-stroke"),
id("stroke-style", "dashed"),
n("stroke-width", 1.0),
n("radius", 4.0),
n("padding", 24.0),
n("font-size", 12.0),
],
"else" => vec![
id("fill", "none"),
var("stroke", "group-stroke"),
id("stroke-style", "dashed"),
n("stroke-width", 1.0),
n("font-size", 12.0),
],
"bars" => vec![n("radius", 2.0), id("stroke", "auto")],
"slice" => vec![id("stroke", "auto")],
"mark" => vec![id("marker", "dot")],
"sign" => vec![
n("width", 64.0),
n("height", 64.0),
n("padding", 4.0),
n("stroke-width", 2.0),
id("fit", "contain"),
],
"table" => vec![
id("layout", "grid"),
id("align", "stretch"),
id("justify", "stretch"),
n("gap", 1.0),
var("gap-fill", "stroke"),
n("padding", 0.0),
id("fill", "none"),
var("stroke", "stroke"),
n("stroke-width", 2.0),
id("stroke-style", "solid"),
n("font-size", 14.0),
id("font-weight", "normal"),
n("scale", 1.0),
],
"cell" => vec![pair("padding", 4.0, 8.0)],
"header" => vec![var("fill", "header-fill"), id("font-weight", "bold")],
"footer" => vec![var("color", "footer-color")],
"entity" => vec![decl(
"columns",
vec![Value::Ident("auto".into()), Value::Ident("auto".into())],
)],
_ => Vec::new(),
}
}
pub fn root_defaults() -> Vec<Decl> {
vec![
id("layout", "flow"),
n("padding", 20.0),
n("gap", 20.0),
n("font-size", 15.0),
]
}
pub fn link_defaults() -> Vec<Decl> {
vec![
n("stroke-width", 2.0),
n("clearance", 16.0),
n("font-size", 11.0),
]
}
#[cfg(test)]
mod tests {
use super::*;
use crate::resolve::NodeKind;
use crate::syntax::ast::Value;
fn has(decls: &[Decl], name: &str) -> bool {
decls.iter().any(|d| d.name == name)
}
fn num(decls: &[Decl], name: &str) -> Option<f64> {
decls
.iter()
.find(|d| d.name == name)
.and_then(|d| match d.groups.first()?.first()? {
Value::Number(n) => Some(*n),
_ => None,
})
}
fn ident(decls: &[Decl], name: &str) -> Option<String> {
decls
.iter()
.find(|d| d.name == name)
.and_then(|d| match d.groups.first()?.first()? {
Value::Ident(s) => Some(s.clone()),
_ => None,
})
}
fn var(decls: &[Decl], name: &str) -> Option<String> {
decls
.iter()
.find(|d| d.name == name)
.and_then(|d| match d.groups.first()?.first()? {
Value::Var(s) => Some(s.clone()),
_ => None,
})
}
#[test]
fn block_is_bare_and_box_template_carries_the_paint() {
let block = primitive_bundle(NodeKind::Block);
assert_eq!(num(&block, "padding"), Some(0.0));
assert_eq!(num(&block, "gap"), Some(20.0));
assert!(!has(&block, "radius"));
let boxt = template_bundle("box");
assert_eq!(num(&boxt, "radius"), Some(8.0));
assert_eq!(num(&boxt, "padding"), Some(20.0));
assert_eq!(num(&boxt, "stroke-width"), Some(2.0));
assert!(has(&boxt, "fill") && has(&boxt, "stroke"));
}
#[test]
fn slant_carries_skew_icon_carries_size() {
assert_eq!(num(&primitive_bundle(NodeKind::Slant), "skew"), Some(15.0));
let icon = primitive_bundle(NodeKind::Icon);
assert_eq!(num(&icon, "width"), Some(32.0));
assert_eq!(num(&icon, "height"), Some(32.0));
}
#[test]
fn group_template_is_a_dashed_frame() {
let g = template_bundle("group");
assert!(g.iter().any(|d| d.name == "stroke-style"));
assert_eq!(num(&g, "stroke-width"), Some(1.0));
assert!(template_bundle("oval").is_empty());
}
#[test]
fn chart_templates_set_the_gap_and_bars_round() {
assert_eq!(num(&template_bundle("chart"), "gap"), Some(10.0));
assert_eq!(num(&template_bundle("pie"), "gap"), Some(10.0));
assert_eq!(num(&template_bundle("bars"), "radius"), Some(2.0));
}
#[test]
fn flow_sugars_set_direction_and_grid_sets_layout() {
let dir = |t: &str| match template_bundle(t).iter().find(|d| d.name == "direction") {
Some(d) => match d.groups.first().and_then(|g| g.first()) {
Some(Value::Ident(s)) => Some(s.clone()),
_ => None,
},
None => None,
};
assert_eq!(dir("row").as_deref(), Some("row"));
assert_eq!(dir("column").as_deref(), Some("column"));
assert!(!has(&template_bundle("row"), "layout"));
assert_eq!(num(&template_bundle("grid"), "radius"), None);
assert!(template_bundle("grid")
.iter()
.any(|d| d.name == "layout"
&& matches!(d.groups.first().and_then(|g| g.first()), Some(Value::Ident(s)) if s == "grid")));
}
#[test]
fn root_default_layout_is_flow() {
assert!(root_defaults()
.iter()
.any(|d| d.name == "layout"
&& matches!(d.groups.first().and_then(|g| g.first()), Some(Value::Ident(s)) if s == "flow")));
}
#[test]
fn root_and_link_defaults_are_present() {
assert_eq!(num(&root_defaults(), "padding"), Some(20.0));
assert_eq!(num(&root_defaults(), "font-size"), Some(15.0));
assert_eq!(num(&link_defaults(), "clearance"), Some(16.0));
assert_eq!(num(&link_defaults(), "font-size"), Some(11.0));
}
#[test]
fn header_footer_entity_footnote_bundles() {
let h = template_bundle("header");
assert!(!has(&h, "justify") && !has(&h, "align"));
assert_eq!(var(&h, "fill").as_deref(), Some("header-fill"));
assert_eq!(ident(&h, "font-weight").as_deref(), Some("bold"));
let f = template_bundle("footer");
assert!(!has(&f, "justify") && !has(&f, "align"));
assert_eq!(var(&f, "color").as_deref(), Some("footer-color"));
assert!(!has(&f, "fill"));
let e = template_bundle("entity");
assert!(has(&e, "columns"));
assert!(!has(&e, "align"));
let foot = template_bundle("footnote");
assert_eq!(ident(&foot, "pin").as_deref(), Some("bottom"));
assert_eq!(num(&foot, "font-size"), Some(12.0));
}
}