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 if min > max {
61 return Err(format!("min_width ({}) > max_width ({})", min, max));
62 }
63 }
64
65 if let (Some(min), Some(max)) = (self.min_height, self.max_height) {
66 if min > max {
67 return Err(format!("min_height ({}) > max_height ({})", min, max));
68 }
69 }
70
71 if let Some(spacing) = self.spacing {
72 if spacing < 0.0 {
73 return Err(format!("spacing must be non-negative, got {}", spacing));
74 }
75 }
76
77 if let Some(padding) = &self.padding {
78 if 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
87 if let Some(Length::FillPortion(n)) = self.width {
88 if n == 0 {
89 return Err(format!("fill_portion must be 1-255, got {}", n));
90 }
91 }
92
93 if let Some(Length::FillPortion(n)) = self.height {
94 if n == 0 {
95 return Err(format!("fill_portion must be 1-255, got {}", n));
96 }
97 }
98
99 if let Some(Length::Percentage(p)) = self.width {
100 if !(0.0..=100.0).contains(&p) {
101 return Err(format!("percentage must be 0.0-100.0, got {}", p));
102 }
103 }
104
105 if let Some(Length::Percentage(p)) = self.height {
106 if !(0.0..=100.0).contains(&p) {
107 return Err(format!("percentage must be 0.0-100.0, got {}", p));
108 }
109 }
110
111 if self.position.is_some() {
113 if self.top.is_none()
115 && self.right.is_none()
116 && self.bottom.is_none()
117 && self.left.is_none()
118 {
119 return Err(
120 "position requires at least one offset (top, right, bottom, or left)"
121 .to_string(),
122 );
123 }
124 }
125
126 Ok(())
127 }
128}
129
130#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
132pub enum Length {
133 Fixed(f32),
135 Fill,
137 Shrink,
139 FillPortion(u8),
141 Percentage(f32),
143}
144
145impl Length {
146 pub fn parse(s: &str) -> Result<Self, String> {
159 let s = s.trim();
160
161 if s.eq_ignore_ascii_case("fill") {
162 return Ok(Length::Fill);
163 }
164
165 if s.eq_ignore_ascii_case("shrink") {
166 return Ok(Length::Shrink);
167 }
168
169 if s.starts_with("fill_portion(") && s.ends_with(')') {
171 let inner = &s[13..s.len() - 1];
172 let n: u8 = inner
173 .parse()
174 .map_err(|_| format!("Invalid fill_portion: {}", s))?;
175 return Ok(Length::FillPortion(n));
176 }
177
178 if let Some(num) = s.strip_suffix('%') {
180 let p: f32 = num
181 .parse()
182 .map_err(|_| format!("Invalid percentage: {}", s))?;
183 return Ok(Length::Percentage(p));
184 }
185
186 let pixels: f32 = s
188 .parse()
189 .map_err(|_| format!("Invalid length value: {}", s))?;
190 Ok(Length::Fixed(pixels))
191 }
192}
193
194#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
196pub struct Padding {
197 pub top: f32,
198 pub right: f32,
199 pub bottom: f32,
200 pub left: f32,
201}
202
203impl Padding {
204 pub fn parse(s: &str) -> Result<Self, String> {
220 let parts: Vec<&str> = s.split_whitespace().collect();
221
222 match parts.len() {
223 1 => {
224 let all: f32 = parts[0]
225 .parse()
226 .map_err(|_| format!("Invalid padding: {}", s))?;
227 Ok(Padding {
228 top: all,
229 right: all,
230 bottom: all,
231 left: all,
232 })
233 }
234 2 => {
235 let v: f32 = parts[0]
236 .parse()
237 .map_err(|_| format!("Invalid vertical padding: {}", parts[0]))?;
238 let h: f32 = parts[1]
239 .parse()
240 .map_err(|_| format!("Invalid horizontal padding: {}", parts[1]))?;
241 Ok(Padding {
242 top: v,
243 right: h,
244 bottom: v,
245 left: h,
246 })
247 }
248 4 => {
249 let t: f32 = parts[0]
250 .parse()
251 .map_err(|_| format!("Invalid top padding: {}", parts[0]))?;
252 let r: f32 = parts[1]
253 .parse()
254 .map_err(|_| format!("Invalid right padding: {}", parts[1]))?;
255 let b: f32 = parts[2]
256 .parse()
257 .map_err(|_| format!("Invalid bottom padding: {}", parts[2]))?;
258 let l: f32 = parts[3]
259 .parse()
260 .map_err(|_| format!("Invalid left padding: {}", parts[3]))?;
261 Ok(Padding {
262 top: t,
263 right: r,
264 bottom: b,
265 left: l,
266 })
267 }
268 _ => Err(format!(
269 "Invalid padding format: '{}'. Expected 1, 2, or 4 values",
270 s
271 )),
272 }
273 }
274}
275
276#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
278pub enum Alignment {
279 Start,
281 Center,
283 End,
285 Stretch,
287}
288
289impl Alignment {
290 pub fn parse(s: &str) -> Result<Self, String> {
292 match s.trim().to_lowercase().as_str() {
293 "start" => Ok(Alignment::Start),
294 "center" => Ok(Alignment::Center),
295 "end" => Ok(Alignment::End),
296 "stretch" => Ok(Alignment::Stretch),
297 _ => Err(format!(
298 "Invalid alignment: '{}'. Expected start, center, end, or stretch",
299 s
300 )),
301 }
302 }
303}
304
305#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
307pub enum Justification {
308 Start,
310 Center,
312 End,
314 SpaceBetween,
316 SpaceAround,
318 SpaceEvenly,
320}
321
322impl Justification {
323 pub fn parse(s: &str) -> Result<Self, String> {
325 match s.trim().to_lowercase().as_str() {
326 "start" => Ok(Justification::Start),
327 "center" => Ok(Justification::Center),
328 "end" => Ok(Justification::End),
329 "space_between" => Ok(Justification::SpaceBetween),
330 "space_around" => Ok(Justification::SpaceAround),
331 "space_evenly" => Ok(Justification::SpaceEvenly),
332 _ => Err(format!(
333 "Invalid justification: '{}'. Expected start, center, end, space_between, space_around, or space_evenly",
334 s
335 )),
336 }
337 }
338}
339
340#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
342pub enum Direction {
343 Horizontal,
344 HorizontalReverse,
345 Vertical,
346 VerticalReverse,
347}
348
349impl Direction {
350 pub fn parse(s: &str) -> Result<Self, String> {
352 match s.trim().to_lowercase().as_str() {
353 "horizontal" => Ok(Direction::Horizontal),
354 "horizontal_reverse" => Ok(Direction::HorizontalReverse),
355 "vertical" => Ok(Direction::Vertical),
356 "vertical_reverse" => Ok(Direction::VerticalReverse),
357 _ => Err(format!(
358 "Invalid direction: '{}'. Expected horizontal, horizontal_reverse, vertical, or vertical_reverse",
359 s
360 )),
361 }
362 }
363}
364
365#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
367pub enum Position {
368 Relative,
370 Absolute,
372}
373
374impl Position {
375 pub fn parse(s: &str) -> Result<Self, String> {
377 match s.trim().to_lowercase().as_str() {
378 "relative" => Ok(Position::Relative),
379 "absolute" => Ok(Position::Absolute),
380 _ => Err(format!(
381 "Invalid position: '{}'. Expected relative or absolute",
382 s
383 )),
384 }
385 }
386}
387
388#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
390pub enum Breakpoint {
391 Mobile,
393 Tablet,
395 Desktop,
397}
398
399impl Breakpoint {
400 pub fn from_viewport_width(width: f32) -> Self {
402 match width {
403 w if w < 640.0 => Breakpoint::Mobile,
404 w if w < 1024.0 => Breakpoint::Tablet,
405 _ => Breakpoint::Desktop,
406 }
407 }
408
409 pub fn parse(s: &str) -> Result<Self, String> {
411 match s.trim().to_lowercase().as_str() {
412 "mobile" => Ok(Breakpoint::Mobile),
413 "tablet" => Ok(Breakpoint::Tablet),
414 "desktop" => Ok(Breakpoint::Desktop),
415 _ => Err(format!(
416 "Invalid breakpoint: '{}'. Expected mobile, tablet, or desktop",
417 s
418 )),
419 }
420 }
421}