1use crate::prelude::*;
2
3#[derive(Debug, Clone)]
8pub enum Constraint {
9 Percentage(f32),
12 Fixed(u16),
15 Range { min: u16, max: u16 },
18 Min(u16),
20 Max(u16),
22 Flexible,
25}
26
27#[derive(Debug, PartialEq, Eq)]
29pub enum LayoutError {
30 InsufficientSpace,
33
34 InvalidPercentages,
37
38 ConstraintConflict,
41}
42
43#[derive(Debug, PartialEq, Eq, Clone, Copy)]
48pub struct Rect {
49 pub x: u16,
51 pub y: u16,
53 pub width: u16,
55 pub height: u16,
57}
58
59impl Rect {
60 pub fn new(x: u16, y: u16, width: u16, height: u16) -> Self {
62 Self {
63 x,
64 y,
65 width,
66 height,
67 }
68 }
69
70 pub fn position(&self) -> Vec2 {
72 vec2(self.x, self.y)
73 }
74
75 pub fn size(&self) -> Vec2 {
77 vec2(self.width, self.height)
78 }
79
80 pub fn bottom_right(&self) -> Vec2 {
82 vec2(self.x + self.width, self.y + self.height)
83 }
84
85 pub fn center(&self) -> Vec2 {
87 vec2(self.x + self.width / 2, self.y + self.height / 2)
88 }
89
90 pub fn from_corners(top_left: Vec2, bottom_right: Vec2) -> Self {
92 Self {
93 x: top_left.x,
94 y: top_left.y,
95 width: bottom_right.x.saturating_sub(top_left.x),
96 height: bottom_right.y.saturating_sub(top_left.y),
97 }
98 }
99
100 pub fn from_pos_size(pos: Vec2, size: Vec2) -> Self {
102 Self {
103 x: pos.x,
104 y: pos.y,
105 width: size.x,
106 height: size.y,
107 }
108 }
109
110 pub fn with_padding(&self, padding: u16) -> Self {
112 Self {
113 x: self.x + padding,
114 y: self.y + padding,
115 width: self.width.saturating_sub(padding * 2),
116 height: self.height.saturating_sub(padding * 2),
117 }
118 }
119
120 pub fn with_padding_sides(&self, top: u16, right: u16, bottom: u16, left: u16) -> Self {
122 Self {
123 x: self.x + left,
124 y: self.y + top,
125 width: self.width.saturating_sub(left + right),
126 height: self.height.saturating_sub(top + bottom),
127 }
128 }
129}
130
131impl From<Rect> for Vec2 {
132 fn from(rect: Rect) -> Self {
133 vec2(rect.x, rect.y)
134 }
135}
136
137pub fn percent(value: f32) -> Constraint {
139 Constraint::Percentage(value)
140}
141
142pub fn fixed(value: u16) -> Constraint {
144 Constraint::Fixed(value)
145}
146
147pub fn range(min_val: u16, max_val: u16) -> Constraint {
149 Constraint::Range {
150 min: min_val,
151 max: max_val,
152 }
153}
154
155pub fn min(value: u16) -> Constraint {
157 Constraint::Min(value)
158}
159
160pub fn max(value: u16) -> Constraint {
162 Constraint::Max(value)
163}
164
165pub fn flexible() -> Constraint {
167 Constraint::Flexible
168}
169
170#[derive(Default, Debug, Clone)]
175pub struct Layout {
176 rows: Vec<(Constraint, Vec<Constraint>)>,
178}
179
180impl Layout {
181 pub fn new() -> Self {
183 Self::default()
184 }
185
186 pub fn row(
188 mut self,
189 height_constraint: Constraint,
190 width_constraints: Vec<Constraint>,
191 ) -> Self {
192 self.rows.push((height_constraint, width_constraints));
193 self
194 }
195
196 pub fn empty_row(self, constraint: Constraint) -> Self {
198 self.row(constraint, vec![flexible()])
199 }
200
201 pub fn calculate(self, space: impl Into<Vec2>) -> Result<Vec<Vec<Rect>>, LayoutError> {
203 calculate_layout(space, self.rows)
204 }
205
206 pub fn render<R: Render>(
208 self,
209 space: impl Into<Vec2>,
210 buffer: &mut Buffer,
211 elements: Vec<Vec<R>>,
212 ) -> Result<Vec<Vec<Rect>>, LayoutError> {
213 let rects = self.calculate(space)?;
214
215 for (row_idx, row_rects) in rects.iter().enumerate() {
216 if let Some(row_elements) = elements.get(row_idx) {
217 for (col_idx, rect) in row_rects.iter().enumerate() {
218 if let Some(element) = row_elements.get(col_idx) {
219 element.render(rect.position(), buffer);
220 }
221 }
222 }
223 }
224
225 Ok(rects)
226 }
227
228 pub fn render_clipped<R: Render>(
230 self,
231 space: impl Into<Vec2>,
232 buffer: &mut Buffer,
233 elements: Vec<Vec<R>>,
234 ) -> Result<Vec<Vec<Rect>>, LayoutError> {
235 let rects = self.calculate(space)?;
236
237 for (row_idx, row_rects) in rects.iter().enumerate() {
238 if let Some(row_elements) = elements.get(row_idx) {
239 for (col_idx, rect) in row_rects.iter().enumerate() {
240 if let Some(element) = row_elements.get(col_idx) {
241 element.render_clipped(rect.position(), rect.size(), buffer);
242 }
243 }
244 }
245 }
246
247 Ok(rects)
248 }
249}
250
251pub struct CalculatedLayout {
255 rects: Vec<Vec<Rect>>,
256}
257
258impl CalculatedLayout {
259 pub fn new(rects: Vec<Vec<Rect>>) -> Self {
261 Self { rects }
262 }
263
264 pub fn get(&self, row: usize, col: usize) -> Option<&Rect> {
266 self.rects.get(row)?.get(col)
267 }
268
269 pub fn row(&self, row: usize) -> Option<&[Rect]> {
271 self.rects.get(row).map(|r| r.as_slice())
272 }
273
274 pub fn row_count(&self) -> usize {
276 self.rects.len()
277 }
278
279 pub fn col_count(&self, row: usize) -> usize {
281 self.rects.get(row).map(|r| r.len()).unwrap_or(0)
282 }
283
284 pub fn iter(&self) -> impl Iterator<Item = (usize, usize, &Rect)> {
286 self.rects.iter().enumerate().flat_map(|(row_idx, row)| {
287 row.iter()
288 .enumerate()
289 .map(move |(col_idx, rect)| (row_idx, col_idx, rect))
290 })
291 }
292
293 pub fn render_at<R: Render>(
295 &self,
296 row: usize,
297 col: usize,
298 element: R,
299 buffer: &mut Buffer,
300 ) -> Option<Vec2> {
301 let rect = self.get(row, col)?;
302 Some(element.render(rect.position(), buffer))
303 }
304
305 pub fn render_clipped_at<R: Render>(
307 &self,
308 row: usize,
309 col: usize,
310 element: R,
311 buffer: &mut Buffer,
312 ) -> Option<Vec2> {
313 let rect = self.get(row, col)?;
314 Some(element.render_clipped(rect.position(), rect.size(), buffer))
315 }
316}
317
318pub fn calculate_layout(
320 total_space: impl Into<Vec2>,
321 rows: Vec<(Constraint, Vec<Constraint>)>,
322) -> Result<Vec<Vec<Rect>>, LayoutError> {
323 let total_space = total_space.into();
324 let height_constraints: Vec<Constraint> = rows.iter().map(|(h, _)| h.clone()).collect();
325
326 let row_heights = resolve_constraints(&height_constraints, total_space.y)?;
328 let mut result = Vec::new();
329 let mut current_y = 0u16;
330
331 for (row_idx, (_, width_constraints)) in rows.iter().enumerate() {
333 let row_height = row_heights[row_idx];
334 let widths = resolve_constraints(width_constraints, total_space.x)?;
335
336 let mut row_elements = Vec::new();
337 let mut current_x = 0u16;
338
339 for width in widths {
340 row_elements.push(Rect::new(current_x, current_y, width, row_height));
341 current_x += width;
342 }
343
344 result.push(row_elements);
345 current_y += row_height;
346 }
347
348 Ok(result)
349}
350
351pub fn resolve_constraints(
353 constraints: &[Constraint],
354 available: u16,
355) -> Result<Vec<u16>, LayoutError> {
356 if constraints.is_empty() {
357 return Ok(vec![]);
358 }
359
360 let mut total_percentage = 0.0f32;
361 for constraint in constraints {
362 if let Constraint::Percentage(pct) = constraint {
363 if *pct < 0.0 || *pct > 100.0 {
364 return Err(LayoutError::InvalidPercentages);
365 }
366 total_percentage += pct;
367 }
368 }
369
370 if total_percentage > 100.0 {
371 return Err(LayoutError::InvalidPercentages);
372 }
373
374 let mut allocated_sizes = vec![0u16; constraints.len()];
375
376 let mut fixed_total = 0u32;
378 for (i, constraint) in constraints.iter().enumerate() {
379 if let Constraint::Fixed(size) = constraint {
380 allocated_sizes[i] = *size;
381 fixed_total += *size as u32;
382 }
383 }
384
385 if fixed_total > available as u32 {
386 return Err(LayoutError::InsufficientSpace);
387 }
388
389 let mut percentage_total = 0u32;
391 for (i, constraint) in constraints.iter().enumerate() {
392 if let Constraint::Percentage(pct) = constraint {
393 let ideal_size = ((available as f32 * pct) / 100.0).round() as u32;
394 allocated_sizes[i] = ideal_size as u16;
395 percentage_total += ideal_size;
396 }
397 }
398
399 if fixed_total + percentage_total > available as u32 {
401 let shrink_factor = (available as u32 - fixed_total) as f32 / percentage_total as f32;
402 for (i, constraint) in constraints.iter().enumerate() {
403 if let Constraint::Percentage(_) = constraint {
404 allocated_sizes[i] = (allocated_sizes[i] as f32 * shrink_factor).round() as u16;
405 }
406 }
407 }
408
409 for (i, constraint) in constraints.iter().enumerate() {
411 match constraint {
412 Constraint::Range { min: min_val, .. } | Constraint::Min(min_val) => {
413 allocated_sizes[i] = allocated_sizes[i].max(*min_val);
414 }
415 _ => {}
416 }
417 }
418
419 let used_space: u32 = allocated_sizes.iter().map(|&x| x as u32).sum();
420
421 if used_space > available as u32 {
422 return Err(LayoutError::InsufficientSpace);
423 }
424
425 let mut remaining_space = (available as u32) - used_space;
426
427 let mut expandable_indices: Vec<(usize, u16)> = Vec::new();
429
430 for (i, constraint) in constraints.iter().enumerate() {
431 let max_val = match constraint {
432 Constraint::Range { max: m, .. } => Some(*m),
433 Constraint::Max(m) => Some(*m),
434 Constraint::Min(_) => Some(u16::MAX),
435 Constraint::Flexible => Some(u16::MAX),
436 _ => None,
437 };
438
439 if let Some(max) = max_val {
440 expandable_indices.push((i, max));
441 }
442 }
443
444 if !expandable_indices.is_empty() && remaining_space > 0 {
446 while remaining_space > 0 {
447 let mut distributed = 0u32;
448 let eligible: Vec<_> = expandable_indices
449 .iter()
450 .filter(|(idx, max_val)| allocated_sizes[*idx] < *max_val)
451 .collect();
452
453 if eligible.is_empty() {
454 break;
455 }
456
457 let space_per_item = std::cmp::max(1, remaining_space / eligible.len() as u32);
458
459 for &&(idx, max_val) in &eligible {
460 if remaining_space == 0 {
461 break;
462 }
463
464 let can_add = std::cmp::min(
465 max_val.saturating_sub(allocated_sizes[idx]) as u32,
466 std::cmp::min(space_per_item, remaining_space),
467 );
468
469 allocated_sizes[idx] += can_add as u16;
470 distributed += can_add;
471 remaining_space -= can_add;
472 }
473
474 if distributed == 0 {
475 break;
476 }
477 }
478 }
479
480 Ok(allocated_sizes)
481}
482
483#[cfg(test)]
484mod tests {
485 use super::*;
486
487 #[test]
488 fn test_percent_plus_fixed_heights() {
489 let layout_result = Layout::new()
490 .row(percent(100.0), vec![percent(100.0)])
491 .row(fixed(5), vec![percent(100.0)])
492 .calculate((100, 100))
493 .unwrap();
494 assert_eq!(
495 layout_result,
496 vec![
497 vec![Rect::new(0, 0, 100, 95)],
498 vec![Rect::new(0, 95, 100, 5)]
499 ]
500 );
501 }
502
503 #[test]
504 fn test_even_flexible_split() {
505 let layout_result = Layout::new()
506 .row(flexible(), vec![flexible(), flexible()])
507 .row(flexible(), vec![flexible(), flexible()])
508 .calculate((100, 100))
509 .unwrap();
510 assert_eq!(
511 layout_result,
512 vec![
513 vec![Rect::new(0, 0, 50, 50), Rect::new(50, 0, 50, 50)],
514 vec![Rect::new(0, 50, 50, 50), Rect::new(50, 50, 50, 50)]
515 ]
516 );
517 }
518
519 #[test]
520 fn test_rect_helpers() {
521 let rect = Rect::new(10, 20, 30, 40);
522 assert_eq!(rect.position(), vec2(10, 20));
523 assert_eq!(rect.size(), vec2(30, 40));
524 assert_eq!(rect.bottom_right(), vec2(40, 60));
525 assert_eq!(rect.center(), vec2(25, 40));
526 }
527
528 #[test]
529 fn test_rect_padding() {
530 let rect = Rect::new(10, 10, 30, 30);
531 let padded = rect.with_padding(5);
532 assert_eq!(padded, Rect::new(15, 15, 20, 20));
533 }
534
535 #[test]
536 fn test_rect_from_corners() {
537 let rect = Rect::from_corners(vec2(10, 20), vec2(40, 60));
538 assert_eq!(rect, Rect::new(10, 20, 30, 40));
539 }
540
541 #[test]
542 fn test_min_constraint() {
543 let sizes = resolve_constraints(&[min(30), min(20)], 100).unwrap();
544 assert_eq!(sizes, vec![55, 45]); }
546
547 #[test]
548 fn test_max_constraint() {
549 let sizes = resolve_constraints(&[max(30), flexible()], 100).unwrap();
550 assert_eq!(sizes, vec![30, 70]);
551 }
552
553 #[test]
554 fn test_min_insufficient() {
555 let result = resolve_constraints(&[min(60), min(60)], 100);
556 assert_eq!(result, Err(LayoutError::InsufficientSpace));
557 }
558}