1use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
10pub struct LayoutConstraints {
11 pub width: Option<Length>,
13 pub height: Option<Length>,
14
15 pub min_width: Option<f32>,
17 pub max_width: Option<f32>,
18 pub min_height: Option<f32>,
19 pub max_height: Option<f32>,
20
21 pub padding: Option<Padding>,
23
24 pub spacing: Option<f32>,
26
27 pub align_items: Option<Alignment>,
29 pub justify_content: Option<Justification>,
30 pub align_self: Option<Alignment>,
31
32 pub align_x: Option<Alignment>,
34 pub align_y: Option<Alignment>,
35
36 pub direction: Option<Direction>,
38
39 pub position: Option<Position>,
41 pub top: Option<f32>,
42 pub right: Option<f32>,
43 pub bottom: Option<f32>,
44 pub left: Option<f32>,
45 pub z_index: Option<i32>,
46}
47
48impl LayoutConstraints {
49 pub fn validate(&self) -> Result<(), String> {
59 if let (Some(min), Some(max)) = (self.min_width, self.max_width)
60 && min > max
61 {
62 return Err(format!("min_width ({}) > max_width ({})", min, max));
63 }
64
65 if let (Some(min), Some(max)) = (self.min_height, self.max_height)
66 && min > max
67 {
68 return Err(format!("min_height ({}) > max_height ({})", min, max));
69 }
70
71 if let Some(spacing) = self.spacing
72 && spacing < 0.0
73 {
74 return Err(format!("spacing must be non-negative, got {}", spacing));
75 }
76
77 if let Some(padding) = &self.padding
78 && (padding.top < 0.0
79 || padding.right < 0.0
80 || padding.bottom < 0.0
81 || padding.left < 0.0)
82 {
83 return Err("padding values must be non-negative".to_string());
84 }
85
86 if let Some(Length::FillPortion(n)) = self.width
87 && n == 0
88 {
89 return Err(format!("fill_portion must be 1-255, got {}", n));
90 }
91
92 if let Some(Length::FillPortion(n)) = self.height
93 && n == 0
94 {
95 return Err(format!("fill_portion must be 1-255, got {}", n));
96 }
97
98 if let Some(Length::Percentage(p)) = self.width
99 && !(0.0..=100.0).contains(&p)
100 {
101 return Err(format!("percentage must be 0.0-100.0, got {}", p));
102 }
103
104 if let Some(Length::Percentage(p)) = self.height
105 && !(0.0..=100.0).contains(&p)
106 {
107 return Err(format!("percentage must be 0.0-100.0, got {}", p));
108 }
109
110 if self.position.is_some() {
112 if self.top.is_none()
114 && self.right.is_none()
115 && self.bottom.is_none()
116 && self.left.is_none()
117 {
118 return Err(
119 "position requires at least one offset (top, right, bottom, or left)"
120 .to_string(),
121 );
122 }
123 }
124
125 Ok(())
126 }
127}
128
129#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
131pub enum Length {
132 Fixed(f32),
134 Fill,
136 Shrink,
138 FillPortion(u8),
140 Percentage(f32),
142}
143
144impl Length {
145 pub fn parse(s: &str) -> Result<Self, String> {
158 let s = s.trim();
159
160 if s.eq_ignore_ascii_case("fill") {
161 return Ok(Length::Fill);
162 }
163
164 if s.eq_ignore_ascii_case("shrink") {
165 return Ok(Length::Shrink);
166 }
167
168 if s.starts_with("fill_portion(") && s.ends_with(')') {
170 let inner = &s[13..s.len() - 1];
171 let n: u8 = inner
172 .parse()
173 .map_err(|_| format!("Invalid fill_portion: {}", s))?;
174 return Ok(Length::FillPortion(n));
175 }
176
177 if let Some(num) = s.strip_suffix('%') {
179 let p: f32 = num
180 .parse()
181 .map_err(|_| format!("Invalid percentage: {}", s))?;
182 return Ok(Length::Percentage(p));
183 }
184
185 let pixels: f32 = s
187 .parse()
188 .map_err(|_| format!("Invalid length value: {}", s))?;
189 Ok(Length::Fixed(pixels))
190 }
191}
192
193#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
195pub struct Padding {
196 pub top: f32,
197 pub right: f32,
198 pub bottom: f32,
199 pub left: f32,
200}
201
202impl Padding {
203 pub fn parse(s: &str) -> Result<Self, String> {
219 let parts: Vec<&str> = s.split_whitespace().collect();
220
221 match parts.len() {
222 1 => {
223 let all: f32 = parts[0]
224 .parse()
225 .map_err(|_| format!("Invalid padding: {}", s))?;
226 Ok(Padding {
227 top: all,
228 right: all,
229 bottom: all,
230 left: all,
231 })
232 }
233 2 => {
234 let v: f32 = parts[0]
235 .parse()
236 .map_err(|_| format!("Invalid vertical padding: {}", parts[0]))?;
237 let h: f32 = parts[1]
238 .parse()
239 .map_err(|_| format!("Invalid horizontal padding: {}", parts[1]))?;
240 Ok(Padding {
241 top: v,
242 right: h,
243 bottom: v,
244 left: h,
245 })
246 }
247 4 => {
248 let t: f32 = parts[0]
249 .parse()
250 .map_err(|_| format!("Invalid top padding: {}", parts[0]))?;
251 let r: f32 = parts[1]
252 .parse()
253 .map_err(|_| format!("Invalid right padding: {}", parts[1]))?;
254 let b: f32 = parts[2]
255 .parse()
256 .map_err(|_| format!("Invalid bottom padding: {}", parts[2]))?;
257 let l: f32 = parts[3]
258 .parse()
259 .map_err(|_| format!("Invalid left padding: {}", parts[3]))?;
260 Ok(Padding {
261 top: t,
262 right: r,
263 bottom: b,
264 left: l,
265 })
266 }
267 _ => Err(format!(
268 "Invalid padding format: '{}'. Expected 1, 2, or 4 values",
269 s
270 )),
271 }
272 }
273}
274
275#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
277pub enum Alignment {
278 Start,
280 Center,
282 End,
284 Stretch,
286}
287
288impl Alignment {
289 pub fn parse(s: &str) -> Result<Self, String> {
291 match s.trim().to_lowercase().as_str() {
292 "start" => Ok(Alignment::Start),
293 "center" => Ok(Alignment::Center),
294 "end" => Ok(Alignment::End),
295 "stretch" => Ok(Alignment::Stretch),
296 _ => Err(format!(
297 "Invalid alignment: '{}'. Expected start, center, end, or stretch",
298 s
299 )),
300 }
301 }
302}
303
304#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
306pub enum Justification {
307 Start,
309 Center,
311 End,
313 SpaceBetween,
315 SpaceAround,
317 SpaceEvenly,
319}
320
321impl Justification {
322 pub fn parse(s: &str) -> Result<Self, String> {
324 match s.trim().to_lowercase().as_str() {
325 "start" => Ok(Justification::Start),
326 "center" => Ok(Justification::Center),
327 "end" => Ok(Justification::End),
328 "space_between" => Ok(Justification::SpaceBetween),
329 "space_around" => Ok(Justification::SpaceAround),
330 "space_evenly" => Ok(Justification::SpaceEvenly),
331 _ => Err(format!(
332 "Invalid justification: '{}'. Expected start, center, end, space_between, space_around, or space_evenly",
333 s
334 )),
335 }
336 }
337}
338
339#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
341pub enum Direction {
342 Horizontal,
343 HorizontalReverse,
344 Vertical,
345 VerticalReverse,
346}
347
348impl Direction {
349 pub fn parse(s: &str) -> Result<Self, String> {
351 match s.trim().to_lowercase().as_str() {
352 "horizontal" => Ok(Direction::Horizontal),
353 "horizontal_reverse" => Ok(Direction::HorizontalReverse),
354 "vertical" => Ok(Direction::Vertical),
355 "vertical_reverse" => Ok(Direction::VerticalReverse),
356 _ => Err(format!(
357 "Invalid direction: '{}'. Expected horizontal, horizontal_reverse, vertical, or vertical_reverse",
358 s
359 )),
360 }
361 }
362}
363
364#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
366pub enum Position {
367 Relative,
369 Absolute,
371}
372
373impl Position {
374 pub fn parse(s: &str) -> Result<Self, String> {
376 match s.trim().to_lowercase().as_str() {
377 "relative" => Ok(Position::Relative),
378 "absolute" => Ok(Position::Absolute),
379 _ => Err(format!(
380 "Invalid position: '{}'. Expected relative or absolute",
381 s
382 )),
383 }
384 }
385}
386
387#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
389pub enum Breakpoint {
390 Mobile,
392 Tablet,
394 Desktop,
396}
397
398impl Breakpoint {
399 pub fn from_viewport_width(width: f32) -> Self {
401 match width {
402 w if w < 640.0 => Breakpoint::Mobile,
403 w if w < 1024.0 => Breakpoint::Tablet,
404 _ => Breakpoint::Desktop,
405 }
406 }
407
408 pub fn parse(s: &str) -> Result<Self, String> {
410 match s.trim().to_lowercase().as_str() {
411 "mobile" => Ok(Breakpoint::Mobile),
412 "tablet" => Ok(Breakpoint::Tablet),
413 "desktop" => Ok(Breakpoint::Desktop),
414 _ => Err(format!(
415 "Invalid breakpoint: '{}'. Expected mobile, tablet, or desktop",
416 s
417 )),
418 }
419 }
420}