Skip to main content

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}