ascii_forge/
layout.rs

1use crate::prelude::*;
2
3/// Defines a constraint for sizing elements within a layout.
4///
5/// Constraints determine how much space an element should occupy relative
6/// to the available space or other elements.
7#[derive(Debug, Clone)]
8pub enum Constraint {
9    /// Takes up a specified percentage of the total available space (0.0 to 100.0).
10    /// It will shrink if necessary to fit within the available space.
11    Percentage(f32),
12    /// Takes up a fixed amount of space in units (e.g., characters or rows).
13    /// If the available space is less than the fixed size, an error may occur.
14    Fixed(u16),
15    /// Takes up space within a specified minimum and maximum range.
16    /// It will try to fit its content but won't go below `min` or above `max`.
17    Range { min: u16, max: u16 },
18    /// Takes up at least the specified minimum space, but can grow beyond it.
19    Min(u16),
20    /// Takes up at most the specified maximum space, but can shrink below it.
21    Max(u16),
22    /// Takes up all the remaining available space after other constraints have been resolved.
23    /// Multiple flexible constraints will share the remaining space evenly.
24    Flexible,
25}
26
27/// The possible error results that can occur during layout calculation.
28#[derive(Debug, PartialEq, Eq)]
29pub enum LayoutError {
30    /// Indicates that at least one constraint (e.g., a `Fixed` or `Range` with too high `min`)
31    /// could not fit within the allocated space.
32    InsufficientSpace,
33
34    /// Occurs when `Percentage` constraints sum up to more than 100%, or a percentage
35    /// value is outside the 0.0-100.0 range.
36    InvalidPercentages,
37
38    /// Reserved for potential future conflicts where constraints are logically impossible
39    /// to satisfy simultaneously (currently not explicitly triggered by `resolve_constraints`).
40    ConstraintConflict,
41}
42
43/// An area that a layout element takes up.
44///
45/// Represents a rectangular region on the screen, defined by its top-left
46/// corner (x, y) and its dimensions (width, height).
47#[derive(Debug, PartialEq, Eq, Clone, Copy)]
48pub struct Rect {
49    /// The X-coordinate of the top-left corner.
50    pub x: u16,
51    /// The Y-coordinate of the top-left corner.
52    pub y: u16,
53    /// The width of the rectangle.
54    pub width: u16,
55    /// The height of the rectangle.
56    pub height: u16,
57}
58
59impl Rect {
60    /// Creates a new `Rect` with the specified position and dimensions.
61    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    /// Returns the top-left position as a Vec2.
71    pub fn position(&self) -> Vec2 {
72        vec2(self.x, self.y)
73    }
74
75    /// Returns the size as a Vec2.
76    pub fn size(&self) -> Vec2 {
77        vec2(self.width, self.height)
78    }
79
80    /// Returns the bottom-right corner as a Vec2.
81    pub fn bottom_right(&self) -> Vec2 {
82        vec2(self.x + self.width, self.y + self.height)
83    }
84
85    /// Returns the center point as a Vec2.
86    pub fn center(&self) -> Vec2 {
87        vec2(self.x + self.width / 2, self.y + self.height / 2)
88    }
89
90    /// Creates a Rect from two Vec2 points.
91    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    /// Creates a Rect from a position and size.
101    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    /// Creates a new Rect with padding applied inward.
111    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    /// Creates a new Rect with specific padding on each side.
121    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
137/// Creates a `Constraint::Percentage` variant.
138pub fn percent(value: f32) -> Constraint {
139    Constraint::Percentage(value)
140}
141
142/// Creates a `Constraint::Fixed` variant.
143pub fn fixed(value: u16) -> Constraint {
144    Constraint::Fixed(value)
145}
146
147/// Creates a `Constraint::Range` variant.
148pub fn range(min_val: u16, max_val: u16) -> Constraint {
149    Constraint::Range {
150        min: min_val,
151        max: max_val,
152    }
153}
154
155/// Creates a `Constraint::Min` variant.
156pub fn min(value: u16) -> Constraint {
157    Constraint::Min(value)
158}
159
160/// Creates a `Constraint::Max` variant.
161pub fn max(value: u16) -> Constraint {
162    Constraint::Max(value)
163}
164
165/// Creates a `Constraint::Flexible` variant.
166pub fn flexible() -> Constraint {
167    Constraint::Flexible
168}
169
170/// Defines a horizontal and vertical grid layout setup.
171///
172/// `Layout` is used for separating a given total space (e.g., the window size)
173/// into easy-to-manage rectangular chunks for rendering UI elements.
174#[derive(Default, Debug, Clone)]
175pub struct Layout {
176    /// A vector where each tuple represents a row: `(height_constraint, width_constraints_for_columns)`.
177    rows: Vec<(Constraint, Vec<Constraint>)>,
178}
179
180impl Layout {
181    /// Starts a new `Layout` definition.
182    pub fn new() -> Self {
183        Self::default()
184    }
185
186    /// Adds a new row to the layout with specified height and column width constraints.
187    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    /// Creates a row that takes up the full width of the available space with a single height constraint.
197    pub fn empty_row(self, constraint: Constraint) -> Self {
198        self.row(constraint, vec![flexible()])
199    }
200
201    /// Calculates the `Rect`s for all elements in the layout based on the total available space.
202    pub fn calculate(self, space: impl Into<Vec2>) -> Result<Vec<Vec<Rect>>, LayoutError> {
203        calculate_layout(space, self.rows)
204    }
205
206    /// Calculates the layout and renders elements to each rect area.
207    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    /// Calculates the layout and renders elements with clipping to fit within each rect.
229    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
251/// A helper for working with a single calculated layout.
252///
253/// This provides convenient methods for accessing and working with layout rects.
254pub struct CalculatedLayout {
255    rects: Vec<Vec<Rect>>,
256}
257
258impl CalculatedLayout {
259    /// Creates a new CalculatedLayout from calculated rects.
260    pub fn new(rects: Vec<Vec<Rect>>) -> Self {
261        Self { rects }
262    }
263
264    /// Gets a rect at the specified row and column.
265    pub fn get(&self, row: usize, col: usize) -> Option<&Rect> {
266        self.rects.get(row)?.get(col)
267    }
268
269    /// Gets all rects in a row.
270    pub fn row(&self, row: usize) -> Option<&[Rect]> {
271        self.rects.get(row).map(|r| r.as_slice())
272    }
273
274    /// Returns the total number of rows.
275    pub fn row_count(&self) -> usize {
276        self.rects.len()
277    }
278
279    /// Returns the number of columns in a specific row.
280    pub fn col_count(&self, row: usize) -> usize {
281        self.rects.get(row).map(|r| r.len()).unwrap_or(0)
282    }
283
284    /// Iterates over all rects with their row and column indices.
285    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    /// Renders an element at a specific layout position.
294    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    /// Renders an element clipped to a specific layout position.
306    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
318/// Calculates the layout of a grid, resolving constraints for rows and columns.
319pub 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    // Resolve heights for all rows
327    let row_heights = resolve_constraints(&height_constraints, total_space.y)?;
328    let mut result = Vec::new();
329    let mut current_y = 0u16;
330
331    // Iterate through rows to resolve column widths and create Rects
332    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
351/// Resolves a list of `Constraint`s for a single dimension (either width or height).
352pub 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    // Allocate fixed sizes first
377    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    // Allocate percentage sizes
390    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 combined fixed and percentage exceeds available, shrink percentages proportionally
400    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    // Ensure minimums are met for Range and Min constraints
410    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    // Identify indices of flexible, min, max, and range constraints for expansion
428    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    // Distribute remaining space to expandable constraints
445    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]); // Remaining space distributed
545    }
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}