1use crate::model::common::Anchor;
2use crate::{Bounds, InputBounds};
3
4#[derive(Debug, Clone)]
15pub struct LayoutDimensions {
16 total_bounds: Bounds,
18 child_constraints: Vec<InputBounds>,
20 layout_strategy: LayoutStrategy,
22}
23
24impl LayoutDimensions {
25 pub fn new(total_bounds: Bounds) -> Self {
27 Self {
28 total_bounds,
29 child_constraints: Vec::new(),
30 layout_strategy: LayoutStrategy::Absolute,
31 }
32 }
33
34 pub fn add_child(&mut self, input_bounds: InputBounds) {
36 self.child_constraints.push(input_bounds);
37 }
38
39 pub fn with_strategy(mut self, strategy: LayoutStrategy) -> Self {
41 self.layout_strategy = strategy;
42 self
43 }
44
45 pub fn parse_percentage(percentage_str: &str) -> Result<f64, LayoutError> {
48 if let Some(percent_part) = percentage_str.strip_suffix('%') {
49 match percent_part.parse::<f64>() {
50 Ok(value) => Ok(value.clamp(0.0, 100.0) / 100.0),
51 Err(_) => Err(LayoutError::InvalidPercentage(percentage_str.to_string())),
52 }
53 } else {
54 Err(LayoutError::InvalidPercentage(percentage_str.to_string()))
55 }
56 }
57
58 pub fn percentage_to_absolute(&self, percentage: f64, axis: Axis) -> usize {
61 let total = match axis {
62 Axis::X => self.total_bounds.width(),
63 Axis::Y => self.total_bounds.height(),
64 };
65
66 if total <= 1 {
67 return 0;
68 }
69
70 let normalized = percentage.clamp(0.0, 1.0);
71 ((normalized * (total - 1) as f64).round() as usize).min(total - 1)
72 }
73
74 pub fn resolve_input_bounds(&self, input: &InputBounds) -> Result<Bounds, LayoutError> {
77 let base = &self.total_bounds;
78
79 let x1 = if input.x1.is_empty() {
80 base.x1
81 } else if input.x1.ends_with('%') {
82 let percent = Self::parse_percentage(&input.x1)?;
83 base.x1 + self.percentage_to_absolute(percent, Axis::X)
84 } else {
85 base.x1
86 + input
87 .x1
88 .parse::<usize>()
89 .map_err(|_| LayoutError::InvalidCoordinate(input.x1.clone()))?
90 };
91
92 let y1 = if input.y1.is_empty() {
93 base.y1
94 } else if input.y1.ends_with('%') {
95 let percent = Self::parse_percentage(&input.y1)?;
96 base.y1 + self.percentage_to_absolute(percent, Axis::Y)
97 } else {
98 base.y1
99 + input
100 .y1
101 .parse::<usize>()
102 .map_err(|_| LayoutError::InvalidCoordinate(input.y1.clone()))?
103 };
104
105 let x2 = if input.x2.is_empty() {
106 base.x2
107 } else if input.x2.ends_with('%') {
108 let percent = Self::parse_percentage(&input.x2)?;
109 base.x1 + self.percentage_to_absolute(percent, Axis::X)
110 } else {
111 base.x1
112 + input
113 .x2
114 .parse::<usize>()
115 .map_err(|_| LayoutError::InvalidCoordinate(input.x2.clone()))?
116 };
117
118 let y2 = if input.y2.is_empty() {
119 base.y2
120 } else if input.y2.ends_with('%') {
121 let percent = Self::parse_percentage(&input.y2)?;
122 base.y1 + self.percentage_to_absolute(percent, Axis::Y)
123 } else {
124 base.y1
125 + input
126 .y2
127 .parse::<usize>()
128 .map_err(|_| LayoutError::InvalidCoordinate(input.y2.clone()))?
129 };
130
131 Ok(Bounds::new(x1, y1, x2, y2))
132 }
133
134 pub fn resolve_anchor(
137 &self,
138 anchor: &Anchor,
139 target_size: (usize, usize),
140 container: &Bounds,
141 ) -> Bounds {
142 let (target_width, target_height) = target_size;
143 let container_width = container.width();
144 let container_height = container.height();
145
146 let (x1, y1) = match anchor {
148 Anchor::TopLeft => (container.x1, container.y1),
149 Anchor::CenterTop => (
150 container.x1 + container_width.saturating_sub(target_width) / 2,
151 container.y1,
152 ),
153 Anchor::TopRight => (
154 container.x1 + container_width.saturating_sub(target_width),
155 container.y1,
156 ),
157 Anchor::CenterLeft => (
158 container.x1,
159 container.y1 + container_height.saturating_sub(target_height) / 2,
160 ),
161 Anchor::Center => (
162 container.x1 + container_width.saturating_sub(target_width) / 2,
163 container.y1 + container_height.saturating_sub(target_height) / 2,
164 ),
165 Anchor::CenterRight => (
166 container.x1 + container_width.saturating_sub(target_width),
167 container.y1 + container_height.saturating_sub(target_height) / 2,
168 ),
169 Anchor::BottomLeft => (
170 container.x1,
171 container.y1 + container_height.saturating_sub(target_height),
172 ),
173 Anchor::CenterBottom => (
174 container.x1 + container_width.saturating_sub(target_width) / 2,
175 container.y1 + container_height.saturating_sub(target_height),
176 ),
177 Anchor::BottomRight => (
178 container.x1 + container_width.saturating_sub(target_width),
179 container.y1 + container_height.saturating_sub(target_height),
180 ),
181 };
182
183 Bounds::new(x1, y1, x1 + target_width - 1, y1 + target_height - 1)
184 }
185
186 pub fn calculate_all_child_bounds(&self) -> Result<Vec<Bounds>, LayoutError> {
188 match &self.layout_strategy {
189 LayoutStrategy::Absolute => self.calculate_absolute_layout(),
190 LayoutStrategy::Grid { columns, rows } => self.calculate_grid_layout(*columns, *rows),
191 LayoutStrategy::Flex { direction } => self.calculate_flex_layout(*direction),
192 LayoutStrategy::Stack => self.calculate_stack_layout(),
193 }
194 }
195
196 fn calculate_absolute_layout(&self) -> Result<Vec<Bounds>, LayoutError> {
198 let mut results = Vec::new();
199
200 for constraint in &self.child_constraints {
201 let bounds = self.resolve_input_bounds(constraint)?;
202 results.push(bounds);
203 }
204
205 Ok(results)
206 }
207
208 fn calculate_grid_layout(
210 &self,
211 columns: usize,
212 rows: usize,
213 ) -> Result<Vec<Bounds>, LayoutError> {
214 if columns == 0 || rows == 0 {
215 return Err(LayoutError::InvalidGridDimensions { columns, rows });
216 }
217
218 let cell_width = self.total_bounds.width() / columns;
219 let cell_height = self.total_bounds.height() / rows;
220 let mut results = Vec::new();
221
222 for (index, _) in self.child_constraints.iter().enumerate() {
223 let col = index % columns;
224 let row = index / columns;
225
226 if row >= rows {
227 break; }
229
230 let x1 = self.total_bounds.x1 + col * cell_width;
231 let y1 = self.total_bounds.y1 + row * cell_height;
232 let x2 = x1 + cell_width - 1;
233 let y2 = y1 + cell_height - 1;
234
235 results.push(Bounds::new(x1, y1, x2, y2));
236 }
237
238 Ok(results)
239 }
240
241 fn calculate_flex_layout(&self, direction: FlexDirection) -> Result<Vec<Bounds>, LayoutError> {
243 let child_count = self.child_constraints.len();
244 if child_count == 0 {
245 return Ok(Vec::new());
246 }
247
248 let mut results = Vec::new();
249
250 match direction {
251 FlexDirection::Row => {
252 let child_width = self.total_bounds.width() / child_count;
253
254 for (index, _) in self.child_constraints.iter().enumerate() {
255 let x1 = self.total_bounds.x1 + index * child_width;
256 let x2 = x1 + child_width - 1;
257 let bounds = Bounds::new(x1, self.total_bounds.y1, x2, self.total_bounds.y2);
258 results.push(bounds);
259 }
260 }
261 FlexDirection::Column => {
262 let child_height = self.total_bounds.height() / child_count;
263
264 for (index, _) in self.child_constraints.iter().enumerate() {
265 let y1 = self.total_bounds.y1 + index * child_height;
266 let y2 = y1 + child_height - 1;
267 let bounds = Bounds::new(self.total_bounds.x1, y1, self.total_bounds.x2, y2);
268 results.push(bounds);
269 }
270 }
271 }
272
273 Ok(results)
274 }
275
276 fn calculate_stack_layout(&self) -> Result<Vec<Bounds>, LayoutError> {
278 let mut results = Vec::new();
279
280 for _ in &self.child_constraints {
281 results.push(self.total_bounds);
282 }
283
284 Ok(results)
285 }
286
287 pub fn distribute_space(&self, percentages: &[f64]) -> Result<Vec<usize>, LayoutError> {
290 let total_percent: f64 = percentages.iter().sum();
291 if total_percent > 100.01 {
292 return Err(LayoutError::PercentageOverflow {
294 total: total_percent,
295 });
296 }
297
298 let available_space = self.total_bounds.width();
299 let mut results = Vec::new();
300
301 for &percentage in percentages {
302 let space = ((percentage / 100.0) * available_space as f64).round() as usize;
303 results.push(space);
304 }
305
306 Ok(results)
307 }
308
309 pub fn calculate_minimum_space(&self) -> (usize, usize) {
311 if self.child_constraints.is_empty() {
312 return (1, 1); }
314
315 match &self.layout_strategy {
316 LayoutStrategy::Absolute => {
317 let mut min_width = 0;
319 let mut min_height = 0;
320
321 for constraint in &self.child_constraints {
322 if let Ok(bounds) = self.resolve_input_bounds(constraint) {
323 min_width = min_width.max(bounds.x2 + 1);
324 min_height = min_height.max(bounds.y2 + 1);
325 }
326 }
327
328 (min_width, min_height)
329 }
330 LayoutStrategy::Grid { columns, rows } => {
331 let min_cell_size = (8, 3); (*columns * min_cell_size.0, *rows * min_cell_size.1)
334 }
335 LayoutStrategy::Flex { .. } | LayoutStrategy::Stack => {
336 let min_child_size = (8, 3);
338 (
339 min_child_size.0,
340 min_child_size.1 * self.child_constraints.len(),
341 )
342 }
343 }
344 }
345
346 pub fn validate_layout(&self) -> Result<(), LayoutError> {
348 let (min_width, min_height) = self.calculate_minimum_space();
349
350 if self.total_bounds.width() < min_width {
351 return Err(LayoutError::InsufficientWidth {
352 available: self.total_bounds.width(),
353 required: min_width,
354 });
355 }
356
357 if self.total_bounds.height() < min_height {
358 return Err(LayoutError::InsufficientHeight {
359 available: self.total_bounds.height(),
360 required: min_height,
361 });
362 }
363
364 Ok(())
365 }
366}
367
368#[derive(Debug, Clone, PartialEq)]
369pub enum LayoutStrategy {
370 Absolute,
372 Grid { columns: usize, rows: usize },
374 Flex { direction: FlexDirection },
376 Stack,
378}
379
380#[derive(Debug, Clone, Copy, PartialEq)]
381pub enum FlexDirection {
382 Row, Column, }
385
386#[derive(Debug, Clone, Copy, PartialEq)]
387pub enum Axis {
388 X,
389 Y,
390}
391
392#[derive(Debug, Clone, PartialEq)]
393pub enum LayoutError {
394 InvalidPercentage(String),
395 InvalidCoordinate(String),
396 InvalidGridDimensions { columns: usize, rows: usize },
397 PercentageOverflow { total: f64 },
398 InsufficientWidth { available: usize, required: usize },
399 InsufficientHeight { available: usize, required: usize },
400}
401
402#[cfg(test)]
403mod tests {
404 use super::*;
405
406 #[test]
407 fn test_percentage_parsing() {
408 assert_eq!(LayoutDimensions::parse_percentage("50%").unwrap(), 0.5);
409 assert_eq!(LayoutDimensions::parse_percentage("100%").unwrap(), 1.0);
410 assert_eq!(LayoutDimensions::parse_percentage("0%").unwrap(), 0.0);
411
412 assert_eq!(LayoutDimensions::parse_percentage("150%").unwrap(), 1.0);
414 assert_eq!(LayoutDimensions::parse_percentage("-10%").unwrap(), 0.0);
415
416 assert!(LayoutDimensions::parse_percentage("not_percent").is_err());
418 assert!(LayoutDimensions::parse_percentage("50").is_err());
419 }
420
421 #[test]
422 fn test_percentage_to_absolute() {
423 let total_bounds = Bounds::new(0, 0, 100, 50);
424 let layout = LayoutDimensions::new(total_bounds);
425
426 let x = layout.percentage_to_absolute(0.5, Axis::X);
428 assert_eq!(x, 50);
429
430 let y = layout.percentage_to_absolute(0.5, Axis::Y);
432 assert_eq!(y, 25);
433
434 let x_max = layout.percentage_to_absolute(1.0, Axis::X);
436 assert_eq!(x_max, 100); }
438
439 #[test]
440 fn test_input_bounds_resolution() {
441 let total_bounds = Bounds::new(0, 0, 100, 50);
442 let layout = LayoutDimensions::new(total_bounds);
443
444 let input = InputBounds {
445 x1: "25%".to_string(),
446 y1: "20%".to_string(),
447 x2: "75%".to_string(),
448 y2: "80%".to_string(),
449 };
450
451 let resolved = layout.resolve_input_bounds(&input).unwrap();
452
453 assert_eq!(resolved.x1, 25); assert_eq!(resolved.y1, 10); assert_eq!(resolved.x2, 75); assert_eq!(resolved.y2, 40); }
458
459 #[test]
460 fn test_anchor_resolution() {
461 let total_bounds = Bounds::new(0, 0, 100, 50);
462 let layout = LayoutDimensions::new(total_bounds);
463
464 let container = Bounds::new(10, 10, 90, 40);
465 let target_size = (20, 10);
466
467 let bounds = layout.resolve_anchor(&Anchor::Center, target_size, &container);
469
470 assert_eq!(bounds.x1, 40);
475 assert_eq!(bounds.y1, 20);
476 assert_eq!(bounds.width(), 20);
477 assert_eq!(bounds.height(), 10);
478 }
479
480 #[test]
481 fn test_grid_layout() {
482 let total_bounds = Bounds::new(0, 0, 19, 11); let mut layout = LayoutDimensions::new(total_bounds);
484 layout = layout.with_strategy(LayoutStrategy::Grid {
485 columns: 2,
486 rows: 2,
487 });
488
489 for i in 0..4 {
491 layout.add_child(InputBounds {
492 x1: format!("{}%", i * 25),
493 y1: "0%".to_string(),
494 x2: format!("{}%", (i + 1) * 25),
495 y2: "100%".to_string(),
496 });
497 }
498
499 let bounds = layout.calculate_all_child_bounds().unwrap();
500 assert_eq!(bounds.len(), 4);
501
502 assert_eq!(bounds[0], Bounds::new(0, 0, 9, 5)); assert_eq!(bounds[1], Bounds::new(10, 0, 19, 5)); assert_eq!(bounds[2], Bounds::new(0, 6, 9, 11)); assert_eq!(bounds[3], Bounds::new(10, 6, 19, 11)); }
508
509 #[test]
510 fn test_flex_layout() {
511 let total_bounds = Bounds::new(0, 0, 29, 9); let mut layout = LayoutDimensions::new(total_bounds);
513 layout = layout.with_strategy(LayoutStrategy::Flex {
514 direction: FlexDirection::Row,
515 });
516
517 for i in 0..3 {
519 layout.add_child(InputBounds {
520 x1: format!("{}%", i * 30),
521 y1: "0%".to_string(),
522 x2: format!("{}%", (i + 1) * 30),
523 y2: "100%".to_string(),
524 });
525 }
526
527 let bounds = layout.calculate_all_child_bounds().unwrap();
528 assert_eq!(bounds.len(), 3);
529
530 assert_eq!(bounds[0], Bounds::new(0, 0, 9, 9)); assert_eq!(bounds[1], Bounds::new(10, 0, 19, 9)); assert_eq!(bounds[2], Bounds::new(20, 0, 29, 9)); }
535
536 #[test]
537 fn test_space_distribution() {
538 let total_bounds = Bounds::new(0, 0, 99, 9); let layout = LayoutDimensions::new(total_bounds);
540
541 let percentages = vec![30.0, 50.0, 20.0];
542 let spaces = layout.distribute_space(&percentages).unwrap();
543
544 assert_eq!(spaces, vec![30, 50, 20]);
545
546 let overflow = vec![60.0, 50.0, 20.0]; assert!(layout.distribute_space(&overflow).is_err());
549 }
550}