1use std::io;
9use std::time::Duration;
10use terminal_graphics_interactive::{
11 Colour, Display, Event, Graphic, KeyCode, MouseButton, MouseKind, Terminal, Text,
12};
13
14const PALETTE: [Colour; 8] = [
16 Colour::White,
17 Colour::Red,
18 Colour::Yellow,
19 Colour::Green,
20 Colour::Cyan,
21 Colour::Blue,
22 Colour::Magenta,
23 Colour::BrightBlack,
24];
25
26fn line(from: (isize, isize), to: (isize, isize)) -> Vec<(isize, isize)> {
31 let (mut x, mut y) = from;
33 let dx = (to.0 - x).abs();
34 let dy = -(to.1 - y).abs();
35 let step_x = if x < to.0 { 1 } else { -1 };
36 let step_y = if y < to.1 { 1 } else { -1 };
37 let mut error = dx + dy;
38
39 let mut cells = Vec::new();
40 loop {
41 cells.push((x, y));
42 if (x, y) == to {
43 return cells;
44 }
45 let doubled = 2 * error;
46 if doubled >= dy {
47 error += dy;
48 x += step_x;
49 }
50 if doubled <= dx {
51 error += dx;
52 y += step_y;
53 }
54 }
55}
56
57struct Paint {
58 display: Display,
60 brush: usize,
62 last: Option<(isize, isize)>,
64}
65
66impl Paint {
67 fn new(width: u32, height: u32) -> Paint {
68 let mut paint = Paint {
69 display: Display::new(width, height),
70 brush: 0,
71 last: None,
72 };
73 paint.clear();
74 paint
75 }
76
77 fn clear(&mut self) {
79 for y in 0..self.canvas_height() {
80 for x in 0..self.display.width() as isize {
81 self.display
82 .set_pixel(x, y, ' ', Colour::White, Colour::Black);
83 }
84 }
85 self.draw_status();
86 }
87
88 fn canvas_height(&self) -> isize {
90 self.display.height() as isize - 1
91 }
92
93 fn draw_status(&mut self) {
95 let y = self.canvas_height();
96 for x in 0..self.display.width() as isize {
97 self.display
98 .set_pixel(x, y, ' ', Colour::White, Colour::Black);
99 }
100 for (index, colour) in PALETTE.iter().enumerate() {
101 let mark = if index == self.brush { '#' } else { ' ' };
102 self.display
103 .set_pixel(index as isize, y, mark, Colour::Black, *colour);
104 }
105 let help = "1-8 or scroll: colour drag: paint right drag: erase c: clear q: quit";
106 Text::new(
107 PALETTE.len() as isize + 1,
108 y,
109 help,
110 Colour::White,
111 Colour::Black,
112 )
113 .draw(&mut self.display);
114 }
115
116 fn stroke(&mut self, x: isize, y: isize, colour: Colour) {
118 let width = self.display.width() as isize;
119 let height = self.canvas_height();
120
121 let clamp = |(x, y): (isize, isize)| (x.clamp(-1, width), y.clamp(-1, height));
124 let from = self.last.unwrap_or((x, y));
125 for (cx, cy) in line(clamp(from), clamp((x, y))) {
126 if (0..width).contains(&cx) && (0..height).contains(&cy) {
127 self.display.set_pixel(cx, cy, ' ', Colour::White, colour);
128 }
129 }
130 self.last = Some((x, y));
131 }
132
133 fn begin_stroke_if_pressed(&mut self, kind: MouseKind) {
137 if matches!(kind, MouseKind::Down(_)) {
138 self.last = None;
139 }
140 }
141
142 fn handle(&mut self, event: &Event) -> bool {
144 match event {
145 Event::Key(key) => {
146 if key.is_char('q') || key.is_ctrl('c') {
147 return false;
148 }
149 match key.code {
150 KeyCode::Char(c @ '1'..='8') if key.modifiers.is_none() => {
151 self.brush = c as usize - '1' as usize;
152 self.draw_status();
153 }
154 KeyCode::Char('c') if key.modifiers.is_none() => self.clear(),
155 _ => {}
156 }
157 }
158 Event::Mouse(mouse) => match mouse.kind {
159 MouseKind::Down(MouseButton::Left) | MouseKind::Drag(MouseButton::Left) => {
160 self.begin_stroke_if_pressed(mouse.kind);
161 self.stroke(mouse.x, mouse.y, PALETTE[self.brush]);
162 }
163 MouseKind::Down(MouseButton::Right) | MouseKind::Drag(MouseButton::Right) => {
164 self.begin_stroke_if_pressed(mouse.kind);
165 self.stroke(mouse.x, mouse.y, Colour::Black);
166 }
167 MouseKind::Up(_) => self.last = None,
168 MouseKind::ScrollUp => {
169 self.brush = (self.brush + PALETTE.len() - 1) % PALETTE.len();
170 self.draw_status();
171 }
172 MouseKind::ScrollDown => {
173 self.brush = (self.brush + 1) % PALETTE.len();
174 self.draw_status();
175 }
176 _ => {}
177 },
178 _ => {}
181 }
182 true
183 }
184}
185
186fn main() -> io::Result<()> {
187 let mut terminal = Terminal::start()?;
188
189 let (width, height) = terminal.size();
191 let mut paint = Paint::new(width, height.saturating_sub(1).max(2));
192
193 loop {
194 terminal.draw(&paint.display)?;
195
196 let mut event = terminal.wait()?;
198 loop {
199 if !paint.handle(&event) {
200 return Ok(());
201 }
202 match terminal.poll(Duration::ZERO)? {
203 Some(next) => event = next,
204 None => break,
205 }
206 }
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213 use terminal_graphics_interactive::{Key, Modifiers, Mouse};
214
215 fn key(c: char) -> Event {
216 Event::Key(Key::new(KeyCode::Char(c)))
217 }
218
219 fn mouse(kind: MouseKind, x: isize, y: isize) -> Event {
220 Event::Mouse(Mouse {
221 kind,
222 x,
223 y,
224 modifiers: Modifiers::NONE,
225 })
226 }
227
228 fn down(x: isize, y: isize) -> Event {
229 mouse(MouseKind::Down(MouseButton::Left), x, y)
230 }
231
232 fn drag(x: isize, y: isize) -> Event {
233 mouse(MouseKind::Drag(MouseButton::Left), x, y)
234 }
235
236 fn up(x: isize, y: isize) -> Event {
237 mouse(MouseKind::Up(MouseButton::Left), x, y)
238 }
239
240 fn background(paint: &Paint, x: isize, y: isize) -> Colour {
241 paint.display.pixel(x, y).unwrap().background()
242 }
243
244 #[test]
245 fn a_line_between_two_cells_includes_both_ends() {
246 assert_eq!(line((0, 0), (3, 0)), vec![(0, 0), (1, 0), (2, 0), (3, 0)]);
247 assert_eq!(line((3, 0), (0, 0)), vec![(3, 0), (2, 0), (1, 0), (0, 0)]);
248 assert_eq!(line((2, 2), (2, 4)), vec![(2, 2), (2, 3), (2, 4)]);
249 }
250
251 #[test]
252 fn a_line_from_a_cell_to_itself_is_that_cell() {
253 assert_eq!(line((5, 5), (5, 5)), vec![(5, 5)]);
254 }
255
256 #[test]
257 fn a_diagonal_line_has_one_cell_per_step() {
258 assert_eq!(line((0, 0), (3, 3)), vec![(0, 0), (1, 1), (2, 2), (3, 3)]);
259 }
260
261 #[test]
262 fn a_shallow_line_has_no_gaps() {
263 let cells = line((0, 0), (10, 3));
264 assert_eq!(cells.len(), 11);
265 assert_eq!(cells.first(), Some(&(0, 0)));
266 assert_eq!(cells.last(), Some(&(10, 3)));
267 for pair in cells.windows(2) {
268 assert!((pair[1].0 - pair[0].0).abs() <= 1 && (pair[1].1 - pair[0].1).abs() <= 1);
269 }
270 }
271
272 #[test]
273 fn the_canvas_starts_black() {
274 let paint = Paint::new(20, 6);
275 assert_eq!(background(&paint, 3, 2), Colour::Black);
276 }
277
278 #[test]
279 fn a_left_click_paints_the_cell_in_the_brush_colour() {
280 let mut paint = Paint::new(20, 6);
281 assert!(paint.handle(&down(4, 2)));
282 assert_eq!(background(&paint, 4, 2), PALETTE[0]);
283 assert_eq!(background(&paint, 5, 2), Colour::Black);
284 }
285
286 #[test]
287 fn number_keys_pick_the_brush() {
288 let mut paint = Paint::new(20, 6);
289 paint.handle(&key('2'));
290 paint.handle(&down(1, 1));
291 assert_eq!(background(&paint, 1, 1), PALETTE[1]);
292 paint.handle(&key('8'));
293 paint.handle(&down(2, 1));
294 assert_eq!(background(&paint, 2, 1), PALETTE[7]);
295 }
296
297 #[test]
298 fn scrolling_cycles_through_the_colours_both_ways() {
299 let mut paint = Paint::new(20, 6);
300 paint.handle(&mouse(MouseKind::ScrollDown, 0, 0));
301 assert_eq!(paint.brush, 1);
302 paint.handle(&mouse(MouseKind::ScrollUp, 0, 0));
303 paint.handle(&mouse(MouseKind::ScrollUp, 0, 0));
304 assert_eq!(paint.brush, PALETTE.len() - 1);
305 }
306
307 #[test]
308 fn dragging_fills_the_gap_between_reported_positions() {
309 let mut paint = Paint::new(20, 6);
310 paint.handle(&key('2'));
311 paint.handle(&down(0, 0));
312 paint.handle(&drag(6, 0));
313 for x in 0..=6 {
314 assert_eq!(background(&paint, x, 0), PALETTE[1], "x = {x}");
315 }
316 assert_eq!(background(&paint, 7, 0), Colour::Black);
317 }
318
319 #[test]
320 fn a_stroke_with_extreme_coordinates_neither_hangs_nor_panics() {
321 let mut paint = Paint::new(20, 6);
322 paint.handle(&down(2, 2));
323 paint.handle(&drag(isize::MAX, isize::MIN));
324 paint.handle(&drag(isize::MIN, isize::MAX));
325 paint.handle(&down(isize::MAX, isize::MAX));
326 }
327
328 #[test]
329 fn a_stroke_that_leaves_and_comes_back_paints_only_the_canvas_part() {
330 let mut paint = Paint::new(20, 6);
331 paint.handle(&key('2'));
332 paint.handle(&down(2, 2));
333 paint.handle(&drag(-40, 2));
334 assert_eq!(background(&paint, 0, 2), PALETTE[1]);
335 assert_eq!(background(&paint, 2, 2), PALETTE[1]);
336 }
337
338 #[test]
339 fn releasing_the_button_starts_a_new_stroke() {
340 let mut paint = Paint::new(20, 6);
341 paint.handle(&down(0, 0));
342 paint.handle(&up(0, 0));
343 paint.handle(&down(8, 0));
344 assert_eq!(
345 background(&paint, 4, 0),
346 Colour::Black,
347 "no line between strokes"
348 );
349 }
350
351 #[test]
352 fn the_right_button_erases() {
353 let mut paint = Paint::new(20, 6);
354 paint.handle(&down(3, 3));
355 paint.handle(&mouse(MouseKind::Down(MouseButton::Right), 3, 3));
356 assert_eq!(background(&paint, 3, 3), Colour::Black);
357 }
358
359 #[test]
360 fn painting_outside_the_canvas_or_on_the_status_line_does_nothing() {
361 let mut paint = Paint::new(20, 6);
362 let before = paint.display.clone();
363 paint.handle(&down(-1, 2));
364 paint.handle(&down(20, 2));
365 paint.handle(&down(2, 99));
366 assert_eq!(paint.display, before);
367 paint.handle(&down(15, 5));
369 assert_ne!(background(&paint, 15, 5), PALETTE[0]);
370 }
371
372 #[test]
373 fn a_stroke_that_leaves_the_canvas_is_clipped_without_panicking() {
374 let mut paint = Paint::new(20, 6);
375 paint.handle(&down(2, 2));
376 paint.handle(&drag(500, -500));
377 }
378
379 #[test]
380 fn pasted_text_is_ignored_so_it_cannot_trigger_commands() {
381 let mut paint = Paint::new(20, 6);
384 paint.handle(&key('3'));
385 paint.handle(&down(3, 3));
386 let before = paint.display.clone();
387
388 assert!(paint.handle(&Event::Paste("cargo run --example paint q 7 c".into())));
389
390 assert_eq!(paint.display, before);
391 assert_eq!(paint.brush, 2);
392 }
393
394 #[test]
395 fn c_clears_the_canvas() {
396 let mut paint = Paint::new(20, 6);
397 paint.handle(&down(3, 3));
398 paint.handle(&key('c'));
399 assert_eq!(background(&paint, 3, 3), Colour::Black);
400 }
401
402 #[test]
403 fn q_and_ctrl_c_quit_and_other_events_do_not() {
404 let mut paint = Paint::new(20, 6);
405 assert!(!paint.handle(&key('q')));
406 let ctrl_c = Event::Key(Key {
407 code: KeyCode::Char('c'),
408 modifiers: Modifiers {
409 ctrl: true,
410 ..Modifiers::NONE
411 },
412 });
413 assert!(!paint.handle(&ctrl_c));
414 assert!(paint.handle(&key('x')));
415 }
416
417 #[test]
418 fn the_status_line_shows_the_selected_colour() {
419 let mut paint = Paint::new(40, 6);
420 paint.handle(&key('3'));
421 let y = paint.display.height() as isize - 1;
422 assert_eq!(paint.display.pixel(2, y).unwrap().character(), '#');
423 assert_eq!(paint.display.pixel(1, y).unwrap().character(), ' ');
424 }
425
426 #[test]
427 fn a_tiny_display_does_not_panic() {
428 let mut paint = Paint::new(1, 2);
429 paint.handle(&down(0, 0));
430 paint.handle(&key('8'));
431 paint.handle(&key('c'));
432 let _ = Text::new(0, 0, "x", Colour::White, Colour::Black).contains(0, 0);
433 }
434}