gemini_engine/view/
mod.rs

1//! This module is home to the [`View`] struct, a [`Canvas`] that is able to draw to `stdout`.
2use crate::core::{CanDraw, Canvas, ColChar, Vec2D};
3use std::{
4    fmt::{self, Display, Formatter},
5    io::{self, Write},
6};
7
8mod scale_to_fit;
9mod term_utils;
10mod wrapping;
11
12pub use scale_to_fit::ScaleFitView;
13pub use wrapping::WrappingMode;
14
15/// The View struct implements [`Canvas`], and draws to stdout upon calling `display_render`.
16/// ```no_run
17/// use gemini_engine::{view::{WrappingMode, View}, core::{ColChar, Vec2D}, primitives::Pixel};
18///
19/// let mut view = View::new(9, 3, ColChar::BACKGROUND)
20///     .with_wrapping_mode(WrappingMode::Panic);
21/// let pixel = Pixel::new(view.center(), ColChar::SOLID);
22///
23/// view.draw(&pixel);
24///
25/// view.display_render().unwrap();
26/// ```
27#[derive(Debug, Clone)]
28pub struct View {
29    /// The width of the `View`. If modified, the View should be cleared to account for the new size
30    pub width: usize,
31    /// The height of the `View`. If modified, the View should be cleared to account for the new size
32    pub height: usize,
33    /// The character that the `View` will be filled with by default when [`View::clear`] is called
34    pub background_char: ColChar,
35    /// Determine how to handle pixels that are plotted outside the `View`
36    pub wrapping_mode: WrappingMode,
37    /// If true, [`View::display_render`] will block until the console window is resized to fit the `View`
38    pub block_until_resized: bool,
39    pixels: Vec<ColChar>,
40}
41
42impl View {
43    /// Create a new `View`
44    #[must_use]
45    pub fn new(width: usize, height: usize, background_char: ColChar) -> Self {
46        let mut view = Self {
47            width,
48            height,
49            background_char,
50            wrapping_mode: WrappingMode::Ignore,
51            block_until_resized: false,
52            pixels: Vec::with_capacity(width * height),
53        };
54        view.clear();
55
56        view
57    }
58
59    /// Return the `View` with an updated `wrapping_mode` property. Consumes the original `View`
60    ///
61    /// ## Example
62    /// ```
63    /// # use gemini_engine::{view::{View, WrappingMode}, core::{ColChar, Vec2D, Canvas}};
64    /// let mut view = View::new(20, 7, ColChar::BACKGROUND)
65    ///     .with_wrapping_mode(WrappingMode::Wrap);
66    /// // The pixel will be wrapped and drawn at `(0, 4)`
67    /// view.plot(Vec2D::new(20,4), ColChar::SOLID);
68    /// ```
69    #[must_use]
70    pub const fn with_wrapping_mode(mut self, wrapping_mode: WrappingMode) -> Self {
71        self.wrapping_mode = wrapping_mode;
72        self
73    }
74
75    /// Return the `View` with an updated `block_until_resized` property. Consumes the original `View`
76    ///
77    /// ## Example
78    /// ```no_run
79    /// # use gemini_engine::{view::{View, WrappingMode}, core::ColChar};
80    /// let mut view = View::new(20, 7, ColChar::BACKGROUND)
81    ///     .with_block_until_resized();
82    /// // If the terminal size is smaller than (20, 7), this will wait until the terminal has been resized
83    /// view.display_render().unwrap();
84    /// ```
85    #[must_use]
86    pub const fn with_block_until_resized(mut self) -> Self {
87        self.block_until_resized = true;
88        self
89    }
90
91    /// Return the width and height of the `View` as a [`Vec2D`]
92    #[must_use]
93    pub const fn size(&self) -> Vec2D {
94        Vec2D::new(self.width as i64, self.height as i64)
95    }
96
97    /// Return [`Vec2D`] coordinates of the centre of the `View`
98    #[must_use]
99    pub fn center(&self) -> Vec2D {
100        self.size() / 2
101    }
102
103    /// Clear the `View` of all pixels, overwriting them all with the set `background_char`
104    pub fn clear(&mut self) {
105        self.pixels = vec![self.background_char; self.width * self.height];
106    }
107
108    /// Draw a struct implementing [`CanDraw`] to the `View`
109    #[inline]
110    pub fn draw(&mut self, element: &impl CanDraw) {
111        element.draw_to(self);
112    }
113
114    /// Draw a struct implementing [`CanDraw`] to the `View` with a doubled width. Drawing a `Pixel` at `Vec2D(5,3)`, for example, will result in pixels at at `Vec2D(10,3)` and `Vec2D(11,3)` being plotted to. Useful when you want to work with more square pixels, as single text characters are much taller than they are wide
115    pub fn draw_double_width(&mut self, element: &impl CanDraw) {
116        struct DoubleWidthView<'v>(&'v mut View);
117        impl Canvas for DoubleWidthView<'_> {
118            fn plot(&mut self, pos: Vec2D, c: ColChar) {
119                let pos = pos * Vec2D::new(2, 1);
120                self.0.plot(pos, c);
121                self.0.plot(pos + Vec2D::new(1, 0), c);
122            }
123        }
124
125        // Wrap the `View` in a custom struct (defined above), replacing the plot function with one that plots at double width, and pass it to the element as usual. This should be much faster and more memory efficient than storing all of the element's draw calls in a `PixelContainer` before double-width plotting each of them.
126        element.draw_to(&mut DoubleWidthView(self));
127    }
128
129    /// Display the `View`. `View` implements the `Display` trait and so can be rendered in many ways (such as `println!("{view}");`), but this is intended to be the fastest way possible.
130    ///
131    /// # Errors
132    /// Returns the `Result` from writing to `io::stdout().lock()`. You can simply ignore it with `let _ =` or `.unwrap()` most of the time
133    pub fn display_render(&self) -> io::Result<()> {
134        let mut stdout = io::stdout().lock();
135        if self.block_until_resized {
136            let view_size = self.size();
137            term_utils::block_until_resized(view_size);
138        }
139
140        write!(stdout, "{self}")
141    }
142}
143
144impl Canvas for View {
145    /// Plot a pixel to the `View`. Accepts a [`Vec2D`] (the position of the pixel) and a [`ColChar`] (what the pixel should look like/what colour it should be)
146    ///
147    /// # Panics
148    /// Will panic if the position is out of bounds of the `View` and `wrapping_mode` is `WrappingMode::Panic`
149    fn plot(&mut self, pos: Vec2D, c: ColChar) {
150        if let Some(wrapped_pos) = self.wrapping_mode.handle_bounds(pos, self.size()) {
151            let i = self.width * wrapped_pos.y.unsigned_abs() as usize
152                + wrapped_pos.x.unsigned_abs() as usize;
153            self.pixels[i] = c;
154        }
155    }
156}
157
158impl Display for View {
159    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
160        term_utils::prepare_terminal(f).map_err(|_| fmt::Error)?;
161
162        f.write_str("\x1b[H\x1b[J")?;
163        for y in 0..self.height {
164            let row = &self.pixels[self.width * y..self.width * (y + 1)];
165
166            for x in 0..row.len() {
167                row[x].display_with_prev_and_next(
168                    f,
169                    row.get(x.wrapping_sub(1)).map(|c| c.modifier),
170                    row.get(x + 1).map(|c| c.modifier),
171                )?;
172            }
173            f.write_str("\r\n")?;
174        }
175        f.write_str("\x1b[J")?;
176
177        Ok(())
178    }
179}