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