Skip to main content

paint/
paint.rs

1//! A tiny paint program. Drag with the left mouse button to paint, drag with
2//! the right button to erase, press 1-8 or scroll to pick a colour, `c` to
3//! clear and `q` (or Ctrl+C) to quit.
4//!
5//! Run it in a terminal that reports the mouse (PowerShell, Windows Terminal,
6//! Linux, macOS, or over `ssh -t`): `cargo run --example paint`.
7
8use std::io;
9use std::time::Duration;
10use terminal_graphics_interactive::{
11    Colour, Display, Event, Graphic, KeyCode, MouseButton, MouseKind, Terminal, Text,
12};
13
14/// The colours on offer, picked with the keys 1 to 8.
15const 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
26/// The cells from `from` to `to` inclusive, in a straight line.
27///
28/// A fast mouse drag jumps several cells between events, so painting only the
29/// reported cells would leave gaps.
30fn line(from: (isize, isize), to: (isize, isize)) -> Vec<(isize, isize)> {
31    // Bresenham's algorithm
32    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    /// The canvas, with a status line on its last row.
59    display: Display,
60    /// Index into `PALETTE`.
61    brush: usize,
62    /// Where the pointer was at the last paint event, while a button is held.
63    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    /// Blanks the canvas and redraws the status line.
78    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    /// The last row of the display is the status line, not canvas.
89    fn canvas_height(&self) -> isize {
90        self.display.height() as isize - 1
91    }
92
93    /// One swatch per colour (the chosen one marked `#`), then the controls.
94    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    /// Paints (or erases, with black) from where the pointer last was to `(x, y)`.
117    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        // Keep the line walk short and free of overflow however wild the
122        // coordinates are: one cell outside the canvas is as good as a mile.
123        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    /// A button press starts a new stroke. The release that ended the last one
134    /// may never arrive (let go outside the window), and without this the next
135    /// click would draw a line from wherever the old stroke stopped.
136    fn begin_stroke_if_pressed(&mut self, kind: MouseKind) {
137        if matches!(kind, MouseKind::Down(_)) {
138            self.last = None;
139        }
140    }
141
142    /// Reacts to one event. Returns `false` when the program should quit.
143    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            // Pasted text is not typing: ignore it, so pasting `cargo run q` can
179            // neither clear the canvas nor quit. More kinds of event may come.
180            _ => {}
181        }
182        true
183    }
184}
185
186fn main() -> io::Result<()> {
187    let mut terminal = Terminal::start()?;
188
189    // one row shorter than the window, which `Terminal::draw` needs
190    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        // handle everything that has arrived before drawing the next frame
197        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        // the status line is the last row, y = 5
368        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        // a right click often pastes the clipboard, and `cargo run ...` contains
382        // a `c`, which would otherwise clear the canvas, and a `q`, which quits
383        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}