Skip to main content

blizz_ui/
layout.rs

1use std::io::Write;
2
3use crossterm::{
4  cursor::{Hide, Show},
5  execute,
6  style::ResetColor,
7  terminal::{self, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen},
8};
9use unicode_width::UnicodeWidthStr;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub struct Size {
13  pub width: u16,
14  pub height: u16,
15}
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub struct Position {
19  pub column: u16,
20  pub row: u16,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub struct Rect {
25  pub origin: Position,
26  pub size: Size,
27}
28
29pub fn size(width: u16, height: u16) -> Size {
30  Size { width, height }
31}
32
33pub fn position(column: u16, row: u16) -> Position {
34  Position { column, row }
35}
36
37pub fn rect(origin: Position, size: Size) -> Rect {
38  Rect { origin, size }
39}
40
41#[cfg(not(tarpaulin_include))]
42pub fn terminal_size() -> std::io::Result<Size> {
43  let (width, height) = terminal::size()?;
44  Ok(size(width, height))
45}
46
47pub fn enter_alternate_screen<W: Write>(writer: &mut W) -> std::io::Result<()> {
48  execute!(writer, EnterAlternateScreen, Clear(ClearType::All), Hide)
49}
50
51pub fn leave_alternate_screen<W: Write>(writer: &mut W) -> std::io::Result<()> {
52  execute!(writer, Show, ResetColor, LeaveAlternateScreen)
53}
54
55#[cfg(not(tarpaulin_include))]
56pub fn enable_raw_mode() -> std::io::Result<()> {
57  terminal::enable_raw_mode()
58}
59
60#[cfg(not(tarpaulin_include))]
61pub fn disable_raw_mode() -> std::io::Result<()> {
62  terminal::disable_raw_mode()
63}
64
65pub fn top_two_thirds(terminal_size: Size) -> Rect {
66  let height = terminal_size.height.saturating_mul(2) / 3;
67  rect(position(0, 0), size(terminal_size.width, height.max(1)))
68}
69
70pub fn bottom_third(terminal_size: Size) -> Rect {
71  let top = top_two_thirds(terminal_size);
72  let row = top.size.height.min(terminal_size.height);
73  let height = terminal_size.height.saturating_sub(row);
74  rect(position(0, row), size(terminal_size.width, height))
75}
76
77pub fn centered_block_origin(region: Rect, block_size: Size) -> Position {
78  let column = center_axis(region.origin.column, region.size.width, block_size.width);
79  let row = center_axis(region.origin.row, region.size.height, block_size.height);
80  position(column, row)
81}
82
83pub fn text_block_size(lines: &[&str]) -> Size {
84  let width = lines
85    .iter()
86    .map(|line| UnicodeWidthStr::width(*line))
87    .max()
88    .unwrap_or(0);
89
90  size(width as u16, lines.len() as u16)
91}
92
93pub fn centered_column(terminal_width: u16, content_width: u16) -> u16 {
94  terminal_width.saturating_sub(content_width) / 2
95}
96
97pub fn lerp_usize(from: usize, to: usize, progress: f64) -> usize {
98  let from_f = from as f64;
99  let to_f = to as f64;
100  (from_f + (to_f - from_f) * progress).round() as usize
101}
102
103fn center_axis(origin: u16, region: u16, content: u16) -> u16 {
104  origin.saturating_add(region.saturating_sub(content) / 2)
105}
106
107#[cfg(test)]
108mod tests {
109  use super::*;
110
111  #[test]
112  fn top_two_thirds_uses_upper_region() {
113    let region = top_two_thirds(size(120, 30));
114
115    assert_eq!(region, rect(position(0, 0), size(120, 20)));
116  }
117
118  #[test]
119  fn bottom_third_starts_after_upper_region() {
120    let region = bottom_third(size(120, 30));
121
122    assert_eq!(region, rect(position(0, 20), size(120, 10)));
123  }
124
125  #[test]
126  fn centered_block_origin_centers_within_region() {
127    let region = rect(position(0, 0), size(100, 30));
128
129    assert_eq!(
130      centered_block_origin(region, size(20, 10)),
131      position(40, 10)
132    );
133  }
134
135  #[test]
136  fn centered_block_origin_keeps_large_blocks_at_region_origin() {
137    let region = rect(position(5, 7), size(10, 3));
138
139    assert_eq!(centered_block_origin(region, size(20, 10)), position(5, 7));
140  }
141
142  #[test]
143  fn text_block_size_uses_display_width() {
144    let lines = ["hi", "▓▓"];
145
146    assert_eq!(text_block_size(&lines), size(2, 2));
147  }
148
149  #[test]
150  fn centered_column_centers_content() {
151    assert_eq!(centered_column(80, 20), 30);
152    assert_eq!(centered_column(80, 80), 0);
153    assert_eq!(centered_column(80, 100), 0);
154  }
155
156  #[test]
157  fn lerp_usize_interpolates_between_values() {
158    assert_eq!(lerp_usize(10, 20, 0.0), 10);
159    assert_eq!(lerp_usize(10, 20, 0.5), 15);
160    assert_eq!(lerp_usize(10, 20, 1.0), 20);
161    assert_eq!(lerp_usize(20, 10, 0.5), 15);
162  }
163
164  #[test]
165  fn alternate_screen_helpers_write_terminal_sequences() {
166    let mut buffer = Vec::new();
167
168    enter_alternate_screen(&mut buffer).unwrap();
169    leave_alternate_screen(&mut buffer).unwrap();
170
171    assert!(!buffer.is_empty());
172  }
173
174  #[test]
175  fn rect_constructor_creates_expected_value() {
176    let r = rect(position(3, 7), size(40, 20));
177
178    assert_eq!(r.origin.column, 3);
179    assert_eq!(r.origin.row, 7);
180    assert_eq!(r.size.width, 40);
181    assert_eq!(r.size.height, 20);
182  }
183
184  #[test]
185  fn size_and_position_constructors() {
186    let s = size(80, 24);
187    assert_eq!(s.width, 80);
188    assert_eq!(s.height, 24);
189
190    let p = position(10, 5);
191    assert_eq!(p.column, 10);
192    assert_eq!(p.row, 5);
193  }
194
195  #[test]
196  fn bottom_third_handles_small_terminal() {
197    let region = bottom_third(size(80, 3));
198    assert!(region.size.height <= 3);
199  }
200
201  #[test]
202  fn top_two_thirds_min_height_is_one() {
203    let region = top_two_thirds(size(80, 1));
204    assert_eq!(region.size.height, 1);
205  }
206}