realtime_termgraph/
canvas.rs

1#[allow(unused_imports)]
2use crossterm::{
3    cursor, terminal,
4    terminal::{Clear, ClearType},
5    ExecutableCommand, QueueableCommand,
6};
7use map_range::MapRange;
8use std::{
9    io::{stdout, Write},
10    ops::RangeInclusive,
11};
12
13use super::Point;
14
15#[derive(Clone)]
16pub struct Canvas {
17    dimensions: (u16, u16),
18    x_bounds: RangeInclusive<f32>,
19    y_bounds: RangeInclusive<f32>,
20    start_row: u16,
21    marker: char,
22    border_char: char,
23}
24
25impl Default for Canvas {
26    fn default() -> Self {
27        Self {
28            dimensions: (20, 10),
29            x_bounds: -10.0..=10.0,
30            y_bounds: -10.0..=10.0,
31            start_row: cursor::position().unwrap_or((0, 0)).1,
32            marker: '.',
33            border_char: '#',
34        }
35    }
36}
37
38impl Drop for Canvas {
39    fn drop(&mut self) {
40        self.go_to_exit_pos();
41        crossterm::execute!(stdout(), cursor::Show).unwrap();
42    }
43}
44
45impl Canvas {
46    pub fn new(rows: u16, columns: u16) -> Self {
47        Self {
48            dimensions: (rows, columns),
49            x_bounds: -10.0..=10.0,
50            y_bounds: -10.0..=10.0,
51            ..Self::default()
52        }
53        .init()
54    }
55
56    pub fn custom(
57        rows: u16,
58        columns: u16,
59        x_bounds: RangeInclusive<f32>,
60        y_bounds: RangeInclusive<f32>,
61        marker: char,
62        border_char: char,
63    ) -> Self {
64        Self {
65            dimensions: (rows, columns),
66            x_bounds,
67            y_bounds,
68            marker,
69            border_char,
70            ..Self::default()
71        }
72        .init()
73    }
74
75    pub fn init(mut self) -> Self {
76        Self::clear_rows(self.dimensions.1 + 4);
77        crossterm::execute!(stdout(), cursor::MoveUp(self.dimensions.1 + 4)).unwrap();
78        self.start_row = cursor::position().unwrap_or((0, 0)).1;
79        self.print_border();
80
81        self
82    }
83
84    pub fn style(mut self, marker: char, border_char: char) -> Self {
85        self.marker = marker;
86        self.border_char = border_char;
87        self
88    }
89
90    pub fn x_bounds(mut self, bounds: RangeInclusive<f32>) -> Self {
91        self.x_bounds = bounds;
92        self
93    }
94
95    pub fn y_bounds(mut self, bounds: RangeInclusive<f32>) -> Self {
96        self.y_bounds = bounds;
97        self
98    }
99
100    pub fn bounds(self, x_bounds: RangeInclusive<f32>, y_bounds: RangeInclusive<f32>) -> Self {
101        self.x_bounds(x_bounds).y_bounds(y_bounds)
102    }
103
104    pub fn plot_points(&self, points: &[Point]) {
105        for point in points {
106            self.plot_point(point);
107        }
108    }
109
110    pub fn plot_point(&self, point: &Point) {
111        if let Some((x, y)) = self.map_point(point) {
112            self.char_at_position(y, x, self.marker);
113        }
114    }
115
116    fn map_point(&self, point: &Point) -> Option<(u16, u16)> {
117        let (x, y) = point;
118        if self.x_bounds.contains(x) && self.y_bounds.contains(y) {
119            let (xs, xe) = (self.x_bounds.start(), self.x_bounds.end());
120            let (ys, ye) = (self.y_bounds.start(), self.y_bounds.end());
121
122            let nx = x.map_range(*xs..*xe, 0.0..(self.dimensions.0 as f32));
123            let ny = y.map_range(*ys..*ye, (self.dimensions.1 as f32)..0.0);
124
125            return Some((nx.round() as u16, ny.round() as u16));
126        }
127
128        None
129    }
130
131    fn final_row(&self) -> u16 {
132        self.start_row + self.dimensions.1 - 1
133    }
134
135    fn go_to_exit_pos(&self) {
136        crossterm::execute!(stdout(), cursor::MoveTo(0, self.final_row() + 4)).unwrap();
137    }
138
139    fn clear_rows(count: u16) {
140        (0..count).for_each(|_| println!());
141    }
142
143    pub fn print_border(&self) {
144        let border_text = (0..=self.dimensions.0 + 2).map(|_| '#').collect::<String>();
145
146        self.write_to_row(0, &border_text);
147        self.write_to_row(self.dimensions.1 + 2, &border_text);
148
149        for row in 1..=self.dimensions.1 + 1 {
150            char_at_position(self.start_row + row, 0, self.border_char).unwrap();
151            char_at_position(self.start_row + row, self.dimensions.0 + 2, '#').unwrap();
152        }
153        self.go_to_exit_pos();
154    }
155
156    fn write_to_row(&self, row: u16, text: &str) {
157        write_to_row(self.start_row + row, text).unwrap();
158        self.go_to_exit_pos();
159    }
160
161    fn char_at_position(&self, row: u16, column: u16, char: char) {
162        char_at_position(self.start_row + row + 1, column + 1, char).unwrap();
163        self.go_to_exit_pos();
164    }
165}
166
167fn write_to_row(row: u16, text: &str) -> Result<(), std::io::Error> {
168    let mut stdout = stdout();
169    stdout.execute(cursor::Hide)?;
170
171    stdout.queue(cursor::MoveTo(0, row))?;
172
173    stdout.queue(Clear(ClearType::CurrentLine))?;
174    stdout.write_all(text.as_bytes())?;
175
176    stdout.flush()?;
177
178    Ok(())
179}
180
181fn char_at_position(row: u16, column: u16, char: char) -> Result<(), std::io::Error> {
182    let mut stdout = stdout();
183    stdout.execute(cursor::Hide)?;
184
185    stdout.queue(cursor::MoveTo(column, row))?;
186
187    stdout.write_all(&[char as u8])?;
188
189    stdout.flush()?;
190
191    Ok(())
192}
193
194#[cfg(test)]
195mod tests {
196    #[allow(unused_imports)]
197    use super::*;
198
199    #[test]
200    fn test_write() -> Result<(), std::io::Error> {
201        let text: &str = "Hello from Grant!";
202        write_to_row(4, text)?;
203        char_at_position(3, 0, '.')?;
204
205        Ok(())
206    }
207
208    #[test]
209    fn test_map_point_default() {
210        let canvas: Canvas = Canvas::default();
211        let expected_maps = [
212            ((0.0, 0.0), (10, 5)),
213            ((-10.0, 0.0), (0, 5)),
214            ((10.0, 0.0), (20, 5)),
215            ((-10.0, 10.0), (0, 0)),
216            ((10.0, -10.0), (20, 10)),
217        ];
218
219        for (point, map) in expected_maps {
220            assert_eq!(canvas.map_point(&point), Some(map));
221        }
222    }
223
224    #[test]
225    fn test_map_point_dimensions() {
226        let canvas: Canvas = Canvas::new(40, 10);
227        let expected_maps = [
228            ((0.0, 0.0), (20, 5)),
229            ((-10.0, 0.0), (0, 5)),
230            ((10.0, 0.0), (40, 5)),
231            ((-10.0, 10.0), (0, 0)),
232            ((10.0, -10.0), (40, 10)),
233        ];
234
235        for (point, map) in expected_maps {
236            assert_eq!(canvas.map_point(&point), Some(map));
237        }
238    }
239
240    #[test]
241    fn test_map_point_range() {
242        let canvas = Canvas::custom(20, 10, 0.0..=40.0, 0.0..=20.0, '+', '#');
243        let expected_maps = [((0.0, 10.0), (0, 5)), ((10.0, 0.0), (5, 10))];
244
245        for (point, map) in expected_maps {
246            assert_eq!(canvas.map_point(&point), Some(map));
247        }
248    }
249}