ascii_canvas/
lib.rs

1//! An "ASCII Canvas" allows us to draw lines and write text into a
2//! fixed-sized canvas and then convert that canvas into ASCII
3//! characters. ANSI styling is supported.
4
5use crate::style::Style;
6use std::cmp;
7use std::iter::ExactSizeIterator;
8use std::ops::Range;
9use term::Terminal;
10
11mod row;
12#[cfg(test)]
13mod test;
14#[cfg(test)]
15mod test_util;
16
17pub mod style;
18
19pub use self::row::Row;
20
21///////////////////////////////////////////////////////////////////////////
22
23/// AsciiView is a view onto an `AsciiCanvas` which potentially
24/// applies transformations along the way (e.g., shifting, adding
25/// styling information). Most of the main drawing methods for
26/// `AsciiCanvas` are defined as inherent methods on an `AsciiView`
27/// trait object.
28pub trait AsciiView {
29    fn columns(&self) -> usize;
30    fn read_char(&mut self, row: usize, column: usize) -> char;
31    fn write_char(&mut self, row: usize, column: usize, ch: char, style: Style);
32}
33
34impl<'a> dyn AsciiView + 'a {
35    fn add_box_dirs(&mut self, row: usize, column: usize, dirs: u8) {
36        let old_ch = self.read_char(row, column);
37        let new_ch = add_dirs(old_ch, dirs);
38        self.write_char(row, column, new_ch, Style::new());
39    }
40
41    /// Draws a line for the given range of rows at the given column.
42    pub fn draw_vertical_line(&mut self, rows: Range<usize>, column: usize) {
43        let len = rows.len();
44        for (index, r) in rows.enumerate() {
45            let new_dirs = if index == 0 {
46                DOWN
47            } else if index == len - 1 {
48                UP
49            } else {
50                UP | DOWN
51            };
52            self.add_box_dirs(r, column, new_dirs);
53        }
54    }
55
56    /// Draws a horizontal line along a given row for the given range
57    /// of columns.
58    pub fn draw_horizontal_line(&mut self, row: usize, columns: Range<usize>) {
59        let len = columns.len();
60        for (index, c) in columns.enumerate() {
61            let new_dirs = if index == 0 {
62                RIGHT
63            } else if index == len - 1 {
64                LEFT
65            } else {
66                LEFT | RIGHT
67            };
68            self.add_box_dirs(row, c, new_dirs);
69        }
70    }
71
72    /// Writes characters in the given style at the given position.
73    pub fn write_chars<I>(&mut self, row: usize, column: usize, chars: I, style: Style)
74    where
75        I: Iterator<Item = char>,
76    {
77        for (i, ch) in chars.enumerate() {
78            self.write_char(row, column + i, ch, style);
79        }
80    }
81
82    /// Creates a new view onto the same canvas, but writing at an offset.
83    pub fn shift(&mut self, row: usize, column: usize) -> ShiftedView {
84        ShiftedView::new(self, row, column)
85    }
86
87    /// Creates a new view onto the same canvas, but applying a style
88    /// to all the characters written.
89    pub fn styled(&mut self, style: Style) -> StyleView {
90        StyleView::new(self, style)
91    }
92}
93
94pub struct AsciiCanvas {
95    columns: usize,
96    rows: usize,
97    characters: Vec<char>,
98    styles: Vec<Style>,
99}
100
101/// To use an `AsciiCanvas`, first create the canvas, then draw any
102/// lines, then write text labels. It is required to draw the lines
103/// first so that we can detect intersecting lines properly (we could
104/// track which characters belong to lines, I suppose).
105impl AsciiCanvas {
106    /// Create a canvas of the given size. We will automatically add
107    /// rows as needed, but the columns are fixed at creation.
108    pub fn new(rows: usize, columns: usize) -> Self {
109        AsciiCanvas {
110            rows,
111            columns,
112            characters: vec![' '; columns * rows],
113            styles: vec![Style::new(); columns * rows],
114        }
115    }
116
117    fn grow_rows_if_needed(&mut self, new_rows: usize) {
118        if new_rows >= self.rows {
119            let new_chars = (new_rows - self.rows) * self.columns;
120            self.characters.extend((0..new_chars).map(|_| ' '));
121            self.styles.extend((0..new_chars).map(|_| Style::new()));
122            self.rows = new_rows;
123        }
124    }
125
126    fn index(&mut self, r: usize, c: usize) -> usize {
127        self.grow_rows_if_needed(r + 1);
128        self.in_range_index(r, c)
129    }
130
131    fn in_range_index(&self, r: usize, c: usize) -> usize {
132        assert!(r < self.rows);
133        assert!(c <= self.columns);
134        r * self.columns + c
135    }
136
137    fn start_index(&self, r: usize) -> usize {
138        self.in_range_index(r, 0)
139    }
140
141    fn end_index(&self, r: usize) -> usize {
142        self.in_range_index(r, self.columns)
143    }
144
145    pub fn write_to<T: Terminal + ?Sized>(&self, term: &mut T) -> term::Result<()> {
146        for row in self.to_strings() {
147            row.write_to(term)?;
148            writeln!(term)?;
149        }
150        Ok(())
151    }
152
153    pub fn to_strings(&self) -> Vec<Row> {
154        (0..self.rows)
155            .map(|row| {
156                let start = self.start_index(row);
157                let end = self.end_index(row);
158                let chars = &self.characters[start..end];
159                let styles = &self.styles[start..end];
160                Row::new(chars, styles)
161            })
162            .collect()
163    }
164}
165
166impl AsciiView for AsciiCanvas {
167    fn columns(&self) -> usize {
168        self.columns
169    }
170
171    fn read_char(&mut self, row: usize, column: usize) -> char {
172        assert!(column < self.columns);
173        let index = self.index(row, column);
174        self.characters[index]
175    }
176
177    fn write_char(&mut self, row: usize, column: usize, ch: char, style: Style) {
178        assert!(column < self.columns);
179        let index = self.index(row, column);
180        self.characters[index] = ch;
181        self.styles[index] = style;
182    }
183}
184
185#[derive(Copy, Clone)]
186struct Point {
187    row: usize,
188    column: usize,
189}
190
191/// Gives a view onto an AsciiCanvas that has a fixed upper-left
192/// point. You can get one of these by calling the `shift()` method on
193/// any ASCII view.
194///
195/// Shifted views also track the extent of the characters which are
196/// written through them; the `close()` method can be used to read
197/// that out when you are finished.
198pub struct ShiftedView<'canvas> {
199    // either the base canvas or another view
200    base: &'canvas mut dyn AsciiView,
201
202    // fixed at creation: the content is always allowed to grow down,
203    // but cannot grow right more than `num_columns`
204    upper_left: Point,
205
206    // this is updated to track content that is emitted
207    lower_right: Point,
208}
209
210impl<'canvas> ShiftedView<'canvas> {
211    fn new(base: &'canvas mut dyn AsciiView, row: usize, column: usize) -> Self {
212        let upper_left = Point { row, column };
213        ShiftedView {
214            base,
215            upper_left,
216            lower_right: upper_left,
217        }
218    }
219
220    /// Finalize the view; returns the (maximal row, maximal column)
221    /// that was written (in the coordinates of the parent view, not
222    /// the shifted view). Note that these values are the actual last
223    /// places that were written, so if you wrote to that precise
224    /// location, you would overwrite some of the content that was
225    /// written.
226    pub fn close(self) -> (usize, usize) {
227        (self.lower_right.row, self.lower_right.column)
228    }
229
230    fn track_max(&mut self, row: usize, column: usize) {
231        self.lower_right.row = cmp::max(self.lower_right.row, row);
232        self.lower_right.column = cmp::max(self.lower_right.column, column);
233    }
234}
235
236impl<'canvas> AsciiView for ShiftedView<'canvas> {
237    fn columns(&self) -> usize {
238        self.base.columns() - self.upper_left.column
239    }
240
241    fn read_char(&mut self, row: usize, column: usize) -> char {
242        let row = self.upper_left.row + row;
243        let column = self.upper_left.column + column;
244        self.base.read_char(row, column)
245    }
246
247    fn write_char(&mut self, row: usize, column: usize, ch: char, style: Style) {
248        let row = self.upper_left.row + row;
249        let column = self.upper_left.column + column;
250        self.track_max(row, column);
251        self.base.write_char(row, column, ch, style)
252    }
253}
254
255/// Gives a view onto an AsciiCanvas that applies an additional style
256/// to things that are written. You can get one of these by calling
257/// the `styled()` method on any ASCII view.
258pub struct StyleView<'canvas> {
259    base: &'canvas mut dyn AsciiView,
260    style: Style,
261}
262
263impl<'canvas> StyleView<'canvas> {
264    fn new(base: &'canvas mut dyn AsciiView, style: Style) -> Self {
265        StyleView { base, style }
266    }
267}
268
269impl<'canvas> AsciiView for StyleView<'canvas> {
270    fn columns(&self) -> usize {
271        self.base.columns()
272    }
273
274    fn read_char(&mut self, row: usize, column: usize) -> char {
275        self.base.read_char(row, column)
276    }
277
278    fn write_char(&mut self, row: usize, column: usize, ch: char, style: Style) {
279        self.base
280            .write_char(row, column, ch, style.with(self.style))
281    }
282}
283
284///////////////////////////////////////////////////////////////////////////
285// Unicode box-drawing characters
286
287const UP: u8 = 0b0001;
288const DOWN: u8 = 0b0010;
289const LEFT: u8 = 0b0100;
290const RIGHT: u8 = 0b1000;
291
292const BOX_CHARS: &[(char, u8)] = &[
293    ('╵', UP),
294    ('│', UP | DOWN),
295    ('┤', UP | DOWN | LEFT),
296    ('├', UP | DOWN | RIGHT),
297    ('┼', UP | DOWN | LEFT | RIGHT),
298    ('┘', UP | LEFT),
299    ('└', UP | RIGHT),
300    ('┴', UP | LEFT | RIGHT),
301    // No UP:
302    ('╷', DOWN),
303    ('┐', DOWN | LEFT),
304    ('┌', DOWN | RIGHT),
305    ('┬', DOWN | LEFT | RIGHT),
306    // No UP|DOWN:
307    ('╶', LEFT),
308    ('─', LEFT | RIGHT),
309    // No LEFT:
310    ('╴', RIGHT),
311    // No RIGHT:
312    (' ', 0),
313];
314
315fn box_char_for_dirs(dirs: u8) -> char {
316    for &(c, d) in BOX_CHARS {
317        if dirs == d {
318            return c;
319        }
320    }
321    panic!("no box character for dirs: {:b}", dirs);
322}
323
324fn dirs_for_box_char(ch: char) -> Option<u8> {
325    for &(c, d) in BOX_CHARS {
326        if c == ch {
327            return Some(d);
328        }
329    }
330    None
331}
332
333fn add_dirs(old_ch: char, new_dirs: u8) -> char {
334    let old_dirs = dirs_for_box_char(old_ch).unwrap_or(0);
335    box_char_for_dirs(old_dirs | new_dirs)
336}