1use core::fmt;
4
5use dotloom_geometry::Point;
6use serde::{Deserialize, Serialize};
7
8use crate::{DocError, EntityId};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
12pub struct RefValue {
13 #[serde(rename = "ref")]
15 pub entity: EntityId,
16}
17
18#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
20pub struct AnchorRef {
21 pub entity: EntityId,
23 pub anchor: String,
25}
26
27impl AnchorRef {
28 #[must_use]
30 pub fn new(entity: EntityId, anchor: impl Into<String>) -> Self {
31 Self { entity, anchor: anchor.into() }
32 }
33}
34
35#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
38#[serde(untagged)]
39pub enum PropValue {
40 Bool(bool),
42 Number(f64),
44 Text(String),
46 Point(Point),
48 Ref(RefValue),
50 Anchor(AnchorRef),
52}
53
54impl PropValue {
55 #[must_use]
57 pub fn as_number(&self) -> Option<f64> {
58 if let Self::Number(v) = self { Some(*v) } else { None }
59 }
60
61 #[must_use]
63 pub fn referenced_entity(&self) -> Option<EntityId> {
64 match self {
65 Self::Ref(r) => Some(r.entity),
66 Self::Anchor(a) => Some(a.entity),
67 _ => None,
68 }
69 }
70
71 #[must_use]
73 pub fn is_finite(&self) -> bool {
74 match self {
75 Self::Number(v) => v.is_finite(),
76 Self::Point(p) => p.is_finite(),
77 _ => true,
78 }
79 }
80
81 #[must_use]
83 pub const fn kind_name(&self) -> &'static str {
84 match self {
85 Self::Bool(_) => "bool",
86 Self::Number(_) => "number",
87 Self::Text(_) => "text",
88 Self::Point(_) => "point",
89 Self::Ref(_) => "ref",
90 Self::Anchor(_) => "anchor",
91 }
92 }
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
97#[serde(try_from = "String", into = "String")]
98pub struct Color(pub u32);
99
100impl Color {
101 pub const BLACK: Self = Self(0x0000_00ff);
103
104 #[must_use]
106 pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
107 Self(((r as u32) << 24) | ((g as u32) << 16) | ((b as u32) << 8) | a as u32)
108 }
109
110 #[must_use]
112 pub const fn components(self) -> [u8; 4] {
113 self.0.to_be_bytes()
114 }
115
116 pub fn parse(s: &str) -> Result<Self, DocError> {
118 let hex = s.strip_prefix('#').ok_or_else(|| DocError::InvalidValue(format!("color `{s}`")))?;
119 let bad = || DocError::InvalidValue(format!("color `{s}`"));
120 if !hex.chars().all(|c| c.is_ascii_hexdigit()) {
121 return Err(bad());
122 }
123 let v = u32::from_str_radix(hex, 16).map_err(|_| bad())?;
124 match hex.len() {
125 3 => {
126 let r = (v >> 8) & 0xf;
127 let g = (v >> 4) & 0xf;
128 let b = v & 0xf;
129 Ok(Self((r * 17) << 24 | (g * 17) << 16 | (b * 17) << 8 | 0xff))
130 }
131 6 => Ok(Self(v << 8 | 0xff)),
132 8 => Ok(Self(v)),
133 _ => Err(bad()),
134 }
135 }
136}
137
138impl fmt::Display for Color {
139 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140 let [r, g, b, a] = self.components();
141 if a == 0xff { write!(f, "#{r:02x}{g:02x}{b:02x}") } else { write!(f, "#{r:02x}{g:02x}{b:02x}{a:02x}") }
142 }
143}
144
145impl TryFrom<String> for Color {
146 type Error = DocError;
147 fn try_from(s: String) -> Result<Self, DocError> {
148 Self::parse(&s)
149 }
150}
151
152impl From<Color> for String {
153 fn from(c: Color) -> Self {
154 c.to_string()
155 }
156}
157
158#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
160#[serde(rename_all = "camelCase")]
161pub struct Style {
162 #[serde(default, skip_serializing_if = "Option::is_none")]
164 pub stroke: Option<Color>,
165 #[serde(default, skip_serializing_if = "Option::is_none")]
167 pub fill: Option<Color>,
168 #[serde(default, skip_serializing_if = "Option::is_none")]
170 pub stroke_width: Option<f64>,
171 #[serde(default, skip_serializing_if = "Option::is_none")]
173 pub dash: Option<Vec<f64>>,
174}
175
176impl Style {
177 #[must_use]
179 pub fn is_default(&self) -> bool {
180 *self == Self::default()
181 }
182
183 pub fn validate(&self) -> Result<(), DocError> {
185 if let Some(w) = self.stroke_width
186 && !(w.is_finite() && (0.0..=1000.0).contains(&w))
187 {
188 return Err(DocError::InvalidValue("stroke width must be within 0..=1000 px".into()));
189 }
190 if let Some(d) = &self.dash
191 && (d.len() > 16 || d.iter().any(|v| !(v.is_finite() && *v >= 0.0)))
192 {
193 return Err(DocError::InvalidValue("dash pattern".into()));
194 }
195 Ok(())
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202
203 #[test]
204 fn prop_values_roundtrip_untagged() {
205 let vals = vec![
206 PropValue::Bool(true),
207 PropValue::Number(600.0),
208 PropValue::Text("oak".into()),
209 PropValue::Point(Point::new(1.0, 2.0)),
210 PropValue::Ref(RefValue { entity: EntityId(4) }),
211 PropValue::Anchor(AnchorRef::new(EntityId(4), "start")),
212 ];
213 let j = serde_json::to_string(&vals).unwrap();
214 assert_eq!(j, r#"[true,600.0,"oak",[1.0,2.0],{"ref":4},{"entity":4,"anchor":"start"}]"#);
215 let back: Vec<PropValue> = serde_json::from_str(&j).unwrap();
216 assert_eq!(back, vals);
217 }
218
219 #[test]
220 fn colors() {
221 assert_eq!(Color::parse("#fff").unwrap(), Color(0xffff_ffff));
222 assert_eq!(Color::parse("#1a2b3c").unwrap().to_string(), "#1a2b3c");
223 assert_eq!(Color::parse("#1a2b3c80").unwrap().to_string(), "#1a2b3c80");
224 assert!(Color::parse("1a2b3c").is_err());
225 assert!(Color::parse("#12345").is_err());
226 assert!(Color::parse("#+12345").is_err());
227 }
228}