zenith_core/ast/value.rs
1//! Dimension, unit, and property-value types.
2
3/// A unit of measurement used in `.zen` documents.
4#[derive(Debug, Clone, PartialEq)]
5pub enum Unit {
6 /// Document pixel units — `(px)`.
7 Px,
8 /// Point units — `(pt)`.
9 Pt,
10 /// Percentage — `(pct)`.
11 Pct,
12 /// Degrees — `(deg)`.
13 Deg,
14 /// An unrecognized unit annotation (forward-compat).
15 Unknown(String),
16}
17
18impl Unit {
19 /// Parse a unit annotation string (without the enclosing parentheses).
20 pub fn from_annotation(s: &str) -> Self {
21 match s {
22 "px" => Self::Px,
23 "pt" => Self::Pt,
24 "pct" => Self::Pct,
25 "deg" => Self::Deg,
26 other => Self::Unknown(other.to_owned()),
27 }
28 }
29
30 /// The canonical annotation string (without parentheses) — the inverse of
31 /// [`Unit::from_annotation`].
32 pub fn as_annotation(&self) -> &str {
33 match self {
34 Self::Px => "px",
35 Self::Pt => "pt",
36 Self::Pct => "pct",
37 Self::Deg => "deg",
38 Self::Unknown(s) => s.as_str(),
39 }
40 }
41}
42
43/// A value that carries a numeric magnitude and a measurement unit.
44#[derive(Debug, Clone, PartialEq)]
45pub struct Dimension {
46 /// The numeric magnitude.
47 pub value: f64,
48 /// The unit of the magnitude.
49 pub unit: Unit,
50}
51
52impl Dimension {
53 /// Format as canonical KDL value syntax, e.g. `(px)640` or `(pt)10.5`.
54 ///
55 /// An integral, finite magnitude renders without a fractional part. This is
56 /// the single source of the dimension string used by the formatter and by
57 /// the CLI's transaction/inspect output, so all three agree byte-for-byte.
58 pub fn to_kdl_string(&self) -> String {
59 let value = if self.value.fract() == 0.0 && self.value.is_finite() {
60 format!("{}", self.value as i64)
61 } else {
62 format!("{}", self.value)
63 };
64 format!("({}){value}", self.unit.as_annotation())
65 }
66}
67
68/// Convert a dimension value + unit to pixels.
69///
70/// Returns `Some(px)` for `Px` (identity) and `Pt` (×96/72).
71/// Returns `None` for `Pct`, `Deg`, and `Unknown` — the caller decides
72/// whether to resolve against an axis basis or emit an advisory.
73///
74/// This is the canonical conversion used by both the scene compiler and the
75/// validator; keeping it here ensures both agree on the arithmetic.
76pub fn dim_to_px(value: f64, unit: &Unit) -> Option<f64> {
77 match unit {
78 Unit::Px => Some(value),
79 Unit::Pt => Some(value * 96.0 / 72.0),
80 Unit::Pct | Unit::Deg | Unit::Unknown(_) => None,
81 }
82}
83
84/// A property value that is either a token reference or a raw literal string.
85#[derive(Debug, Clone, PartialEq)]
86pub enum PropertyValue {
87 /// A reference to a design token, e.g. `(token)"color.text.primary"`.
88 TokenRef(String),
89 /// A raw literal value stored as a string (e.g. a hex color `"#ff0000"`).
90 Literal(String),
91 /// A literal dimension with an explicit unit, e.g. `(px)24` or `(pt)13`.
92 Dimension(Dimension),
93 /// A typed reference to a runtime data field, e.g. `(data)"revenue.total"`.
94 DataRef(String),
95}