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