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}