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}