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}