grid_ui/
grid.rs

1use crate::process::DrawProcess;
2#[derive(Debug, Clone, PartialEq, Eq, Hash)]
3#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
4/// This is a frame. It stores the terminal's size in a convenient place.
5/// It isn't stored in a grid, as grids are altered when they're split.
6/// For examples, see the frame's methods.
7pub struct Frame {
8    grid: Grid,
9}
10impl Frame {
11    /**
12    Creates a new frame.
13    # Example
14    ``` rust
15    # use grid_ui::grid::Frame;
16    # fn main() {
17    let ten_by_ten: Frame = Frame::new(0, 0, 10, 10);
18    # }
19    ```
20    */
21    pub fn new(x_min: usize, y_min: usize, x_max: usize, y_max: usize) -> Frame {
22        Frame {
23            grid: Grid {
24                start_x: x_min,
25                start_y: y_min,
26                end_x: x_max,
27                end_y: y_max,
28            },
29        }
30    }
31    /**
32    Produces a fresh grid, which contains the entire frame.
33    # Example
34    ``` rust
35    # use grid_ui::grid::Frame;
36    # use grid_ui::grid::Grid;
37    # fn main() {
38    let ten_by_ten: Frame = Frame::new(0, 0, 10, 10);
39    let ten_by_ten_grid: Grid = ten_by_ten.next_frame();
40    assert_eq!(ten_by_ten_grid, Grid {start_x: 0, start_y: 0, end_x: 10, end_y: 10});
41    # }
42    ```
43    */
44    pub fn next_frame(&self) -> Grid {
45        self.grid.clone()
46    }
47    /**
48    Resizes the grid, changing its size.
49    # Example
50    ``` rust
51    # use grid_ui::grid::Frame;
52    # use grid_ui::grid::Grid;
53    # fn main() {
54    let mut ten_by_ten: Frame = Frame::new(0, 0, 10, 10);
55    let ten_by_ten_grid: Grid = ten_by_ten.next_frame();
56    assert_eq!(ten_by_ten_grid, Grid {start_x: 0, start_y: 0, end_x: 10, end_y: 10});
57    ten_by_ten.resize(5, 5, 10, 10);
58    let five_by_five_grid: Grid = ten_by_ten.next_frame();
59    assert_eq!(five_by_five_grid, Grid {start_x: 5, start_y: 5, end_x: 10, end_y: 10});
60    # }
61    ```
62    */
63    pub fn resize(&mut self, x_min: usize, y_min: usize, x_max: usize, y_max: usize) {
64        self.grid = Grid {
65            start_x: x_min,
66            start_y: y_min,
67            end_x: x_max,
68            end_y: y_max,
69        }
70    }
71}
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
73#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
74/// Whether the alignment is in the negative direction [up/left] or in the positive direction [down/right].
75/// Alignments will have different behaviors depending on where they're used.
76pub enum Alignment {
77    Minus,
78    Plus,
79}
80#[derive(Debug, Clone, PartialEq, Eq, Hash)]
81#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
82enum Maximum {
83    None,
84    X(usize, Alignment),
85    Y(usize, Alignment),
86}
87impl Default for Maximum {
88    fn default() -> Self {
89        Maximum::None
90    }
91}
92#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
93#[derive(Debug, Clone, PartialEq, Eq, Default, Hash)]
94/**
95Inputting this to a grid will give a GridData based on the specifications used in the function.
96# Examples
97Creating a grid
98``` rust
99# use grid_ui::out;
100# use grid_ui::trim::Ignore;
101# use grid_ui::grid::*;
102# fn main() -> Result<(), ()>{
103let mut grid = Frame::new(0, 0, 10, 10).next_frame();
104let chunk = grid.split(&SplitStrategy::new());
105assert_eq!(chunk, Some(Grid {start_x: 0, start_y: 0, end_x: 10, end_y: 10}));
106# Ok(())
107# }
108```
109*/
110pub struct SplitStrategy {
111    min_size_x: Option<usize>,
112    min_size_y: Option<usize>,
113    max_size: Maximum,
114}
115impl SplitStrategy {
116    /**
117    Creates an empty split strategy. Empty strategies will simply take up the entire grid when used. 
118    # Examples
119    The default grid:
120    ``` rust
121    # use grid_ui::out;
122    # use grid_ui::trim::Ignore;
123    # use grid_ui::grid::*;
124    # fn main() -> Result<(), ()>{
125    let mut grid = Frame::new(0, 0, 10, 10).next_frame();
126    let chunk = grid.split(&SplitStrategy::new());
127    assert_eq!(chunk, Some(Grid {start_x: 0, start_y: 0, end_x: 10, end_y: 10}));
128    # Ok(())
129    # }
130    ```
131    */
132    pub fn new() -> SplitStrategy {
133        SplitStrategy {
134            min_size_x: None,
135            min_size_y: None,
136            max_size: Maximum::None,
137        }
138    }
139    /**
140    Sets a maximum X value. The resulting grid will only be at most of length v.
141    It'll be either on the left or the right, depending on the alignment (left = minus).
142    # Panics
143    Only one maximum direction can be set. Otherwise, this function will panic.
144    This is intended. 
145    # Examples
146    Applying a grid with a maximum x value
147    ``` rust
148    # use grid_ui::out;
149    # use grid_ui::trim::Ignore;
150    # use grid_ui::grid::*;
151    # fn main() -> Result<(), ()>{
152    let mut grid = Frame::new(0, 0, 10, 10).next_frame();
153    let chunk = grid.split(&SplitStrategy::new().max_x(5, Alignment::Minus));
154    assert_eq!(chunk, Some(Grid {start_x: 0, start_y: 0, end_x: 5, end_y: 10}));
155    let chunk = grid.split(&SplitStrategy::new().max_x(2, Alignment::Plus));
156    assert_eq!(chunk, Some(Grid {start_x: 8, start_y: 0, end_x: 10, end_y: 10}));
157    # Ok(())
158    # }
159    ```
160    This function will panic - you can't set two maximums. 
161    ```should_panic
162    # use grid_ui::out;
163    # use grid_ui::trim::Ignore;
164    # use grid_ui::grid::*;
165    # fn main() -> Result<(), ()>{
166    let cannot_set_both_x_and_y = SplitStrategy::new().max_x(2, Alignment::Minus).max_y(1, Alignment::Plus);
167    # Ok(())
168    # }
169    ```
170    */
171    pub fn max_x(mut self, v: usize, a: Alignment) -> Self {
172        if matches!(self.max_size, Maximum::None) {
173            self.max_size = Maximum::X(v, a);
174            self
175        } else {
176            panic!("A maximum already exists!")
177        }
178    }
179    /**
180    Sets a maximum Y value. The resulting grid data will only be of height v.
181    It'll be either on the top or the bottom, depending on the alignment (top = minus).
182    # Panics
183    Only one maximum direction can be set. Otherwise, this function will panic.
184    This is intended. 
185    # Examples
186    Applying a grid with a maximum x value
187    ``` rust
188    # use grid_ui::out;
189    # use grid_ui::trim::Ignore;
190    # use grid_ui::grid::*;
191    # fn main() -> Result<(), ()>{
192    let mut grid = Frame::new(0, 0, 10, 10).next_frame();
193    let chunk = grid.split(&SplitStrategy::new().max_y(5, Alignment::Minus));
194    assert_eq!(chunk, Some(Grid {start_x: 0, start_y: 0, end_x: 10, end_y: 5}));
195    let chunk = grid.split(&SplitStrategy::new().max_y(2, Alignment::Plus));
196    assert_eq!(chunk, Some(Grid {start_x: 0, start_y: 8, end_x: 10, end_y: 10}));
197    # Ok(())
198    # }
199    ```
200    This function will panic - you can't set two maximums. 
201    ```should_panic
202    # use grid_ui::out;
203    # use grid_ui::trim::Ignore;
204    # use grid_ui::grid::*;
205    # fn main() -> Result<(), ()>{
206    let cannot_set_both_x_and_y = SplitStrategy::new().max_x(2, Alignment::Minus).max_y(1, Alignment::Plus);
207    # Ok(())
208    # }
209    ```
210    */
211    pub fn max_y(mut self, v: usize, a: Alignment) -> Self {
212        if matches!(self.max_size, Maximum::None) {
213            self.max_size = Maximum::Y(v, a);
214            self
215        } else {
216            panic!("A maximum already exists!")
217        }
218    }
219    /**
220    Sets a minimum X value. If the grid cannot give the grid data this amount of length,
221    no strategy will be returned.
222    # Examples
223    ``` rust
224    # use grid_ui::out;
225    # use grid_ui::trim::Ignore;
226    # use grid_ui::grid::*;
227    # fn main() -> Result<(), ()>{
228    let mut grid = Frame::new(0, 0, 10, 10).next_frame();
229    let chunk = grid.split(&SplitStrategy::new().min_x(15));
230    assert_eq!(chunk, None);
231    let chunk = grid.split(&SplitStrategy::new().min_x(5));
232    assert_eq!(chunk, Some(Grid {start_x: 0, start_y: 0, end_x: 10, end_y: 10}));
233    # Ok(())
234    # }
235    ```
236    */
237    pub fn min_x(mut self, v: usize) -> Self {
238        self.min_size_x = Some(v);
239        self
240    }
241    /**
242    Sets a minimum Y value. If the grid cannot give the grid data this amount of height,
243    no strategy will be returned.
244    # Examples
245    ``` rust
246    # use grid_ui::out;
247    # use grid_ui::trim::Ignore;
248    # use grid_ui::grid::*;
249    # fn main() -> Result<(), ()>{
250    let mut grid = Frame::new(0, 0, 10, 10).next_frame();
251    let chunk = grid.split(&SplitStrategy::new().min_y(15));
252    assert_eq!(chunk, None);
253    let chunk = grid.split(&SplitStrategy::new().min_y(5));
254    assert_eq!(chunk, Some(Grid {start_x: 0, start_y: 0, end_x: 10, end_y: 10}));
255    # Ok(())
256    # }
257    ```
258    */
259    pub fn min_y(mut self, v: usize) -> Self {
260        self.min_size_y = Some(v);
261        self
262    }
263    #[doc(hidden)]
264    /// Applies a split strategy. This is meant to be indirectly called.
265    fn apply(&self, grid: &mut Grid) -> Option<Grid> {
266        if grid.start_x == grid.end_x || grid.start_y == grid.end_y {
267            // no space left
268            return None;
269        }
270        if let Some(val) = self.min_size_y {
271            // below minimum size
272            if grid.end_y <= grid.start_y + val {
273                return None;
274            }
275        }
276        if let Some(val) = self.min_size_x {
277            // below minimum size
278            if grid.end_x <= grid.start_x + val {
279                return None;
280            }
281        }
282        match &self.max_size {
283            Maximum::None => {
284                // Takes up the entire grid
285                let return_value = Some(Grid::new(grid.start_x, grid.start_y, grid.end_x, grid.end_y));
286                grid.start_x = grid.end_x;
287                grid.start_y = grid.end_y;
288                return_value
289            }
290            Maximum::X(size, alignment) => {
291                let size = *size;
292                let size = size.min(grid.end_x - grid.start_x);
293                if matches!(alignment, Alignment::Minus) {
294                    // Takes up the entire grid, up to the maximum size from the left.
295                    let return_value = Some(Grid::new(grid.start_x, grid.start_y, grid.start_x + size, grid.end_y));
296                    grid.start_x += size;
297                    return_value
298                } else {
299                    // Takes up the entire grid, up to the maximum size from the right.
300                    let return_value = Some(Grid::new(grid.end_x - size, grid.start_y, grid.end_x, grid.end_y));
301                    grid.end_x -= size;
302                    return_value
303                }
304            }
305            Maximum::Y(size, alignment) => {
306                let size = *size;
307                let size = size.min(grid.end_y - grid.start_y);
308                if matches!(alignment, Alignment::Minus) {
309                    // Takes up the entire grid, up to the maximum size from the top.
310                    let return_value = Some(Grid::new(grid.start_x, grid.start_y, grid.end_x, grid.start_y + size));
311                    grid.start_y += size;
312                    return_value
313                } else {
314                    // Takes up the entire grid, up to the maximum size from the bottom.
315                    let return_value = Some(Grid::new(grid.start_x, grid.end_y - size, grid.end_x, grid.end_y));
316                    grid.end_y -= size;
317                    return_value
318                }
319            }
320        }
321    }
322}
323#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
324#[derive(Debug, Clone, PartialEq, Eq, Hash)]
325/// A grid - basically, a square meant to resemble a portion of a terminal. Can be split up into other grids.
326/// Cloning a grid is bad practice! Use it only if you must.
327pub struct Grid {
328    pub start_x: usize,
329    pub start_y: usize,
330    pub end_x: usize,
331    pub end_y: usize,
332}
333impl Grid {
334    fn new(start_x: usize, start_y: usize, end_x: usize, end_y: usize) -> Grid {
335        Grid {
336            start_x,
337            start_y,
338            end_x,
339            end_y,
340        }
341    }
342    /**
343    Splits the grid into two others based on a SplitStrategy.
344    With the default split strategy, the entire grid will go into the returned grid, leaving the first one empty.
345    Expect to use this function a lot.
346    # Return value
347    Returns None if no new grid can be created - either because the grid is already empty or because it's below the minimum size.
348    # Examples
349    ``` rust
350    # use grid_ui::out;
351    # use grid_ui::trim::Ignore;
352    # use grid_ui::grid::*;
353    # fn main() -> Result<(), ()>{
354    let mut grid = Frame::new(0, 0, 10, 10).next_frame();
355    let second = grid.split(&SplitStrategy::new().max_y(5, Alignment::Minus));
356    assert_eq!(second, Some(Grid {start_x: 0, start_y: 0, end_x: 10, end_y: 5}));
357    assert_eq!(grid, Grid {start_x: 0, start_y: 5, end_x: 10, end_y: 10});
358    let cant_be_made = grid.split(&SplitStrategy::new().min_y(6));
359    assert_eq!(cant_be_made, None);
360    let takes_up_all = grid.split(&SplitStrategy::new());
361    assert_eq!(takes_up_all, Some(Grid {start_x: 0, start_y: 5, end_x: 10, end_y: 10}));
362    assert_eq!(grid, Grid {start_x: 10, start_y: 10, end_x: 10, end_y: 10});
363    let cant_be_made = grid.split(&SplitStrategy::new());
364    assert_eq!(cant_be_made, None);
365    # Ok(())
366    # }
367    ```
368    */
369    pub fn split(&mut self, strategy: &SplitStrategy) -> Option<Grid> {
370        strategy.apply(self)
371    }
372    /**
373    Extends the grid in the either direction, either positive or negative, if the input is compatible
374    (ie grids are next to each other and of similar dimensions)
375    If the two grids are incompatible, it returns an error and gives the grid back. 
376    # Example
377    ``` rust
378    # use grid_ui::grid;
379    # use grid_ui::out;
380    # use grid_ui::trim::Ignore;
381    # fn main() -> Result<(), ()>{
382    let mut grid = grid::Frame::new(0, 0, 10, 10).next_frame();
383    let mut second_grid = grid.split(&grid::SplitStrategy::new().max_y(5, grid::Alignment::Plus)).ok_or(())?;
384    assert_eq!(grid.end_y, 5);
385    assert!(grid.extend(second_grid).is_ok());
386    assert_eq!(grid.end_y, 10);
387    let incompatible_grid = grid::Frame::new(4, 4, 8, 8).next_frame();
388    assert!(grid.extend(incompatible_grid).is_err());
389    # Ok(())
390    # }
391    ```
392    */
393    
394    pub fn extend(&mut self, grid: Grid) -> Result<(), Grid> {
395        if self.start_x == grid.start_x && self.end_x == grid.end_x {
396            if self.end_y == grid.start_y {
397                self.end_y = grid.end_y;
398                return Ok(())
399            }
400            if self.start_y == grid.end_y {
401                self.start_y = grid.start_y;
402                return Ok(())
403            }
404        }
405        if self.start_y == grid.start_y && self.end_y == grid.end_y {
406            if self.end_x == grid.start_x {
407                self.end_x = grid.end_x;
408                return Ok(())
409            }
410            if self.start_x == grid.end_x {
411                self.start_x = grid.start_x;
412                return Ok(())
413            }
414        }
415        Err(grid)
416    }
417    /**
418    Converts the grid into a DrawProcess. The draw process can then be used to draw onto the terminal.
419    # Examples
420    ``` rust
421    # use grid_ui::out;
422    # use grid_ui::trim::Truncate;
423    # use grid_ui::grid::*;
424    # fn main() -> Result<(), ()>{
425    let mut grid = Frame::new(0, 0, 10, 10).next_frame();
426    let mut process = grid.into_process(DividerStrategy::End);
427    process.add_to_section("Some text".to_string(), &mut Truncate, Alignment::Minus);
428    # Ok(())
429    # }
430    ```
431    */
432    pub fn into_process(self, strategy: DividerStrategy) -> DrawProcess {
433        DrawProcess::new(self, strategy)
434    }
435}
436
437#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
438#[derive(Debug, Clone, PartialEq, Eq, Hash)]
439/// Where the divider will be placed. The divider is between two sides: A plus side and a minus side.
440/// Content can be added on the plus or minus side if there's space available.
441/// For examples of divider behavior, see docs for DrawProcess.
442pub enum DividerStrategy {
443    Beginning,
444    End,
445    Halfway,
446    Pos(usize),
447}