grux/
art.rs

1//! Configure and draw simple ASCII or ASCII-like* sprites to a 2D grid.
2//!
3//! [`grux::art`][`crate::art`] provides:
4//! - A uniform interface for drawing art to a 2D grid: [`Sprite`].
5//! - Built-in art types e.g. [`Line`], [`FilledRect`], [`BorderRect`].
6//!
7//! > ⓘ **NOTE**: The art types in this module are _not_ limited to ASCII characters.
8//! >
9//! > For example, you can use [`Line`] to draw a line of unicode characters, or even a line of a
10//! > more complex type; for example a custom `Cell` struct in your own data structure. ASCII is
11//! > just a way to understand the way the sprites are drawn.
12
13use std::fmt::Display;
14
15use crate::GridWriter;
16
17/// A trait for types that can be drawn to a 2D grid.
18///
19/// # Examples
20///
21/// A custom type that implements [`Sprite`], for example a pre-configured unicode box:
22///
23/// ```
24/// # use grux::art::Sprite;
25/// # use grux::{GridWriter};
26/// struct AsciiBoxExample;
27///
28/// // Not a super useful example, but it's a start.
29/// impl Sprite for AsciiBoxExample {
30///    type Element = char;
31///
32///    fn width(&self) -> usize {
33///        3
34///    }
35///
36///    fn height(&self) -> usize {
37///        3
38///    }
39///
40///    fn draw_to(&self, position: (usize, usize), to: &mut impl GridWriter<Element = Self::Element>) {
41///        to.set((position.0 + 0, position.1 + 0), '╔');
42///        to.set((position.0 + 1, position.1 + 0), '═');
43///        to.set((position.0 + 2, position.1 + 0), '╗');
44///        to.set((position.0 + 0, position.1 + 1), '║');
45///        to.set((position.0 + 2, position.1 + 1), '║');
46///        to.set((position.0 + 0, position.1 + 2), '╚');
47///        to.set((position.0 + 1, position.1 + 2), '═');
48///        to.set((position.0 + 2, position.1 + 2), '╝');
49///        to.set((position.0 + 1, position.1 + 1), ' ');
50///    }
51/// }
52///
53/// let mut grid = [[' '; 3]; 3];
54/// AsciiBoxExample.draw_to((0, 0), &mut grid);
55///
56/// assert_eq!(grid, [
57///    ['╔', '═', '╗'],
58///    ['║', ' ', '║'],
59///    ['╚', '═', '╝'],
60/// ]);
61/// ```
62pub trait Sprite {
63    type Element: Clone;
64
65    /// The width of the element.
66    #[must_use]
67    fn width(&self) -> usize;
68
69    /// The height of the element.
70    #[must_use]
71    fn height(&self) -> usize;
72
73    /// Draws the given element to the grid at the given `(x. y)` position.
74    fn draw_to(&self, position: (usize, usize), to: &mut impl GridWriter<Element = Self::Element>);
75}
76
77/// A structured way to draw a line to a 2D grid.
78///
79/// # Examples
80///
81/// ```
82/// # use grux::art::{Line, Sprite};
83/// # use grux::{GridWriter};
84/// let mut grid = [[' '; 3]; 4];
85///
86/// let line = Line::horizontal(3, '═');
87/// line.draw_to((0, 0), &mut grid);
88/// line.draw_to((0, 3), &mut grid);
89///
90/// let line = Line::vertical(2, '║');
91/// line.draw_to((0, 1), &mut grid);
92/// line.draw_to((2, 1), &mut grid);
93///
94/// assert_eq!(grid, [
95///     ['═', '═', '═'],
96///     ['║', ' ', '║'],
97///     ['║', ' ', '║'],
98///     ['═', '═', '═']
99/// ]);
100/// ```
101pub struct Line<T: Display> {
102    length: usize,
103    render: T,
104    orientation: Orientation,
105}
106
107/// Options for drawing a line to a 2D grid.
108enum Orientation {
109    /// Left to right.
110    Horizontal,
111
112    /// Top to bottom.
113    Vertical,
114}
115
116impl<T: Display> Line<T> {
117    /// Configures a horizontal line of the given length.
118    #[must_use]
119    pub fn horizontal(length: usize, render: T) -> Self {
120        Self {
121            length,
122            render,
123            orientation: Orientation::Horizontal,
124        }
125    }
126
127    /// Configures a vertical line of the given length.
128    #[must_use]
129    pub fn vertical(length: usize, render: T) -> Self {
130        Self {
131            length,
132            render,
133            orientation: Orientation::Vertical,
134        }
135    }
136}
137
138impl<T: Display + Clone> Sprite for Line<T> {
139    type Element = T;
140
141    fn width(&self) -> usize {
142        match self.orientation {
143            Orientation::Horizontal => self.length,
144            Orientation::Vertical => 1,
145        }
146    }
147
148    fn height(&self) -> usize {
149        match self.orientation {
150            Orientation::Horizontal => 1,
151            Orientation::Vertical => self.length,
152        }
153    }
154
155    fn draw_to(&self, position: (usize, usize), to: &mut impl GridWriter<Element = Self::Element>) {
156        let (x, y) = position;
157
158        match self.orientation {
159            Orientation::Horizontal => {
160                for i in 0..self.length {
161                    to.set((x + i, y), self.render.clone());
162                }
163            }
164            Orientation::Vertical => {
165                for i in 0..self.length {
166                    to.set((x, y + i), self.render.clone());
167                }
168            }
169        }
170    }
171}
172
173/// A structured way to draw a filled rectangle to a 2D grid.
174///
175/// If you want to draw a rectangle that is just a border, see [`BorderRect`].
176///
177/// # Examples
178///
179/// ```
180/// # use grux::art::{FillRect, Sprite};
181/// # use grux::{GridWriter};
182/// let mut grid = [[' '; 4]; 4];
183///
184/// let rect = FillRect::new(2, 2, '█');
185///
186/// rect.draw_to((1, 1), &mut grid);
187///
188/// assert_eq!(grid, [
189///     [' ', ' ', ' ', ' '],
190///     [' ', '█', '█', ' '],
191///     [' ', '█', '█', ' '],
192///     [' ', ' ', ' ', ' '],
193/// ]);
194/// ```
195pub struct FillRect<T: Display> {
196    width: usize,
197    height: usize,
198    render: T,
199}
200
201impl<T: Display> FillRect<T> {
202    /// Configures a filled rectangle of the given width and height.
203    #[must_use]
204    pub fn new(width: usize, height: usize, render: T) -> Self {
205        Self {
206            width,
207            height,
208            render,
209        }
210    }
211}
212
213impl<T: Display + Clone> Sprite for FillRect<T> {
214    type Element = T;
215
216    fn width(&self) -> usize {
217        self.width
218    }
219
220    fn height(&self) -> usize {
221        self.height
222    }
223
224    fn draw_to(&self, position: (usize, usize), to: &mut impl GridWriter<Element = Self::Element>) {
225        let (x, y) = position;
226
227        for i in 0..self.width {
228            for j in 0..self.height {
229                to.set((x + i, y + j), self.render.clone());
230            }
231        }
232    }
233}
234
235/// A structured way to draw a bordered rectangle to a 2D grid.
236///
237/// # Examples
238///
239/// ```
240/// # use grux::art::{BorderRect, Sprite};
241/// # use grux::{GridWriter};
242/// let mut grid = [[' '; 4]; 4];
243///
244/// let rect = BorderRect::new(4, 4, ['╔', '═', '╗', '║', '║', '╚', '═', '╝']);
245/// rect.draw_to((0, 0), &mut grid);
246///
247/// assert_eq!(grid, [
248///     ['╔', '═', '═', '╗'],
249///     ['║', ' ', ' ', '║'],
250///     ['║', ' ', ' ', '║'],
251///     ['╚', '═', '═', '╝'],
252/// ]);
253/// ```
254pub struct BorderRect<T: Display> {
255    width: usize,
256    height: usize,
257    render: [T; 8],
258}
259
260impl<T: Display> BorderRect<T> {
261    /// Configures a bordered rectangle of the given width and height.
262    ///
263    /// The render array should be in the following order:
264    /// - Top left corner
265    /// - Top border
266    /// - Top right corner
267    /// - Left border
268    /// - Right border
269    /// - Bottom left corner
270    /// - Bottom border
271    /// - Bottom right corner
272    ///
273    /// # Panics
274    ///
275    /// If the width or height is less than 2.
276    #[must_use]
277    pub fn new(width: usize, height: usize, render: [T; 8]) -> Self {
278        assert!(width >= 2, "Width must be at least 2");
279        assert!(height >= 2, "Height must be at least 2");
280        Self {
281            width,
282            height,
283            render,
284        }
285    }
286}
287impl<T: Display + Clone> BorderRect<T> {
288    fn top_left(&self) -> T {
289        self.render[0].clone()
290    }
291
292    fn top(&self) -> T {
293        self.render[1].clone()
294    }
295
296    fn top_right(&self) -> T {
297        self.render[2].clone()
298    }
299
300    fn left(&self) -> T {
301        self.render[3].clone()
302    }
303
304    fn right(&self) -> T {
305        self.render[4].clone()
306    }
307
308    fn bottom_left(&self) -> T {
309        self.render[5].clone()
310    }
311
312    fn bottom(&self) -> T {
313        self.render[6].clone()
314    }
315
316    fn bottom_right(&self) -> T {
317        self.render[7].clone()
318    }
319}
320
321impl<T: Display + Clone> Sprite for BorderRect<T> {
322    type Element = T;
323
324    fn width(&self) -> usize {
325        self.width
326    }
327
328    fn height(&self) -> usize {
329        self.height
330    }
331
332    fn draw_to(&self, position: (usize, usize), to: &mut impl GridWriter<Element = Self::Element>) {
333        let (x, y) = position;
334        let width = self.width();
335        let height = self.height();
336
337        // Top Middle and Bottom Middle
338        for i in 1..width - 1 {
339            to.set((x + i, y), self.top());
340            to.set((x + i, y + height - 1), self.bottom());
341        }
342
343        // Left Side and Right Side
344        for i in 1..height - 1 {
345            to.set((x, y + i), self.left());
346            to.set((x + width - 1, y + i), self.right());
347        }
348
349        // Corners
350        to.set((x, y), self.top_left());
351        to.set((x + width - 1, y), self.top_right());
352        to.set((x, y + height - 1), self.bottom_left());
353        to.set((x + width - 1, y + height - 1), self.bottom_right());
354    }
355}