Skip to main content

retroglyph_ui/
block.rs

1//! Compose [`Grid`] values before drawing them.
2//!
3//! [`join_h`] and [`join_v`] concatenate several `Grid`s into one,
4//! side-by-side or stacked, via [`Grid::blit`]. `Grid` is constructible
5//! without a [`Backend`](retroglyph_core::backend::Backend)/[`Terminal`](retroglyph_core::terminal::Terminal), so
6//! composing widget output ahead of drawing it means composing `Grid`s directly, with no
7//! separate cell/buffer type.
8use retroglyph_core::grid::Grid;
9
10/// Concatenate `grids` left-to-right into one [`Grid`] (layer 0 only).
11///
12/// The result's width is the sum of the input widths; its height is the
13/// tallest input. Each grid is placed top-aligned; cells below a shorter
14/// grid are left untouched (empty, per [`Grid::new`]'s default tiles). For
15/// an empty slice, returns a 1-wide, 0-tall grid: [`Grid::new`] panics on a
16/// width of zero (it divides by width internally), so a 1×0 grid is as
17/// close to "empty" as an actual `Grid` can represent.
18#[must_use]
19pub fn join_h(grids: &[Grid]) -> Grid {
20    if grids.is_empty() {
21        return Grid::new(1, 0);
22    }
23    let width = grids
24        .iter()
25        .fold(0u16, |acc, g| acc.saturating_add(g.width()));
26    let height = grids.iter().map(Grid::height).max().unwrap_or(0);
27    let mut out = Grid::new(width, height);
28
29    let mut x_offset = 0u16;
30    for g in grids {
31        out.blit(0, g, g.size().to_rect(), x_offset, 0);
32        x_offset = x_offset.saturating_add(g.width());
33    }
34    out
35}
36
37/// Stack `grids` top-to-bottom into one [`Grid`] (layer 0 only).
38///
39/// The result's height is the sum of the input heights; its width is the
40/// widest input. Each grid is placed left-aligned; cells past a narrower
41/// grid's width are left untouched (empty, per [`Grid::new`]'s default
42/// tiles). For an empty slice, returns a 1-wide, 0-tall grid: see [`join_h`]
43/// for why a zero-width grid isn't representable.
44#[must_use]
45pub fn join_v(grids: &[Grid]) -> Grid {
46    if grids.is_empty() {
47        return Grid::new(1, 0);
48    }
49    let width = grids.iter().map(Grid::width).max().unwrap_or(0);
50    let height = grids
51        .iter()
52        .fold(0u16, |acc, g| acc.saturating_add(g.height()));
53    let mut out = Grid::new(width, height);
54
55    let mut y_offset = 0u16;
56    for g in grids {
57        out.blit(0, g, g.size().to_rect(), 0, y_offset);
58        y_offset = y_offset.saturating_add(g.height());
59    }
60    out
61}
62
63#[cfg(test)]
64mod tests {
65    use retroglyph_core::color::Style;
66    use retroglyph_core::grid::Pos;
67    use retroglyph_core::tile::Tile;
68
69    use super::*;
70
71    #[test]
72    fn join_h_concatenates_and_pads_shorter_grids() {
73        let mut a = Grid::new(2, 3);
74        a.put_tile(0, (0, 0), Tile::new('a', Style::default()));
75        let mut b = Grid::new(2, 1);
76        b.put_tile(0, (0, 0), Tile::new('b', Style::default()));
77
78        let joined = join_h(&[a, b]);
79        assert_eq!((joined.width(), joined.height()), (4, 3));
80        assert_eq!(joined[Pos::new(0, 0)].glyph(), 'a');
81        assert_eq!(joined[Pos::new(2, 0)].glyph(), 'b');
82        // b is only 1 row tall; row 1 under it was never written.
83        assert_eq!(joined[Pos::new(2, 1)].glyph(), ' ');
84    }
85
86    #[test]
87    fn join_v_stacks_and_pads_narrower_grids() {
88        let mut a = Grid::new(3, 1);
89        a.put_tile(0, (0, 0), Tile::new('a', Style::default()));
90        let mut b = Grid::new(1, 1);
91        b.put_tile(0, (0, 0), Tile::new('b', Style::default()));
92
93        let joined = join_v(&[a, b]);
94        assert_eq!((joined.width(), joined.height()), (3, 2));
95        assert_eq!(joined[Pos::new(0, 0)].glyph(), 'a');
96        assert_eq!(joined[Pos::new(0, 1)].glyph(), 'b');
97        // b is only 1 column wide; the rest of its row was never written.
98        assert_eq!(joined[Pos::new(1, 1)].glyph(), ' ');
99    }
100
101    #[test]
102    fn join_empty_slice_is_essentially_empty() {
103        // Grid::new(0, _) always panics (it divides by width internally),
104        // so a 1-wide, 0-tall grid is the closest representable "empty".
105        let joined = join_h(&[]);
106        assert_eq!((joined.width(), joined.height()), (1, 0));
107        let joined = join_v(&[]);
108        assert_eq!((joined.width(), joined.height()), (1, 0));
109    }
110
111    #[test]
112    fn join_only_copies_layer_zero() {
113        let mut a = Grid::new(1, 1);
114        a.put_tile(0, (0, 0), Tile::new('a', Style::default())); // layer 0
115        a.put_tile(1, (0, 0), Tile::new('z', Style::default())); // layer 1
116
117        let joined = join_h(&[a]);
118        assert_eq!(joined[Pos::new(0, 0)].glyph(), 'a');
119        assert_eq!(joined.tile(1, (0, 0)), None); // layer 1 was never allocated
120    }
121}