bevy_ascii_terminal 0.12.3

A simple terminal for rendering ascii in bevy.
Documentation
use std::ops::Div;
use std::ops::RangeBounds;
use std::ops::Sub;

use bevy::math::IVec2;
use bevy::math::UVec2;
use bevy::prelude::Color;
use bevy::prelude::Component;
use bevy::prelude::Vec2;

use sark_grids::geometry::GridRect;
use sark_grids::grid::Side;
use sark_grids::Grid;
use sark_grids::GridPoint;
use sark_grids::Size2d;

use crate::border::Border;
use crate::fmt_tile::ColorFormat;
use crate::formatting::StringFormatter;
use crate::TileFormatter;

/// A simple terminal for writing text in a readable grid.
///
/// Contains various functions for drawing colorful text to a
/// terminal.
///
/// # Example
/// ```rust
/// use bevy_ascii_terminal::*;
/// use bevy::prelude::Color;
///
/// let mut term = Terminal::new([10,10]);
///
/// term.put_char([1,1], 'h'.fg(Color::RED));
/// term.put_string([2,1], "ello".bg(Color::BLUE));
///
/// let hello = term.get_string([1,1], 5);
/// ```
#[derive(Component, Clone, Debug, Default)]
pub struct Terminal {
    tiles: Grid<Tile>,
    size: UVec2,
    /// Tile to insert when a position is "cleared".
    ///
    /// The terminal will be filled with this tile when created.
    pub clear_tile: Tile,
    /// An optional border for the terminal.
    ///
    /// The terminal border is considered separate from the terminal itself,
    /// terminal positions and sizes do not include the border unless otherwise
    /// specified.
    border: Option<Border>,
}

/// A single tile of the terminal.
///
/// Defaults to a blank glyph with a black background and a white foreground.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Tile {
    /// The glyph for the tile. Glyphs are mapped to sprites via the
    /// terminal's `UvMapping`
    pub glyph: char,
    /// The forergound color for the tile.
    pub fg_color: Color,
    /// The background color for the tile.
    pub bg_color: Color,
}

impl Tile {
    pub const DEFAULT_FGCOL: Color = Color::WHITE;
    pub const DEFAULT_BGCOL: Color = Color::BLACK;

    /// Create an invisible tile.
    pub fn transparent() -> Tile {
        Tile {
            glyph: ' ',
            fg_color: Color::rgba_u8(0, 0, 0, 0),
            bg_color: Color::rgba_u8(0, 0, 0, 0),
        }
    }
}

impl Default for Tile {
    fn default() -> Self {
        Tile {
            glyph: ' ',
            fg_color: Tile::DEFAULT_FGCOL,
            bg_color: Tile::DEFAULT_BGCOL,
        }
    }
}

impl From<char> for Tile {
    fn from(c: char) -> Self {
        Tile {
            glyph: c,
            ..Default::default()
        }
    }
}

impl Terminal {
    /// Construct a terminal with the given size
    pub fn new(size: impl Size2d) -> Terminal {
        let clear_tile = Tile::default();
        Terminal {
            tiles: Grid::new(size),
            size: size.as_uvec2(),
            clear_tile,
            ..Default::default()
        }
    }

    /// Specify a border for the terminal.
    ///
    /// The terminal border is considered separate from the terminal itself,
    /// writes and sizes within the terminal will ignore the border unless
    /// otherwise specified.
    pub fn with_border(mut self, border: Border) -> Self {
        self.border = Some(border);
        self
    }

    pub fn with_clear_tile(mut self, clear_tile: impl Into<Tile>) -> Self {
        self.clear_tile = clear_tile.into();
        self.clear();
        self
    }

    pub fn set_border(&mut self, border: Border) {
        self.border = Some(border);
    }

    pub fn remove_border(&mut self) {
        self.border = None;
    }

    pub fn border(&self) -> Option<&Border> {
        self.border.as_ref()
    }

    pub fn border_mut(&mut self) -> Option<&mut Border> {
        self.border.as_mut()
    }

    /// Resize the terminal.
    ///
    /// This will clear the terminal.
    pub fn resize(&mut self, size: impl Size2d) {
        self.tiles = Grid::new(size);
        self.size = size.as_uvec2();
    }

    /// The width of the terminal, excluding the border.
    pub fn width(&self) -> usize {
        self.size.x as usize
    }

    /// The height of the terminal, excluding the border.
    pub fn height(&self) -> usize {
        self.size.y as usize
    }

    /// The size of the terminal, excluding the border.
    pub fn size(&self) -> UVec2 {
        self.size
    }

    /// The size of the terminal, including the border if it has one.
    pub fn size_with_border(&self) -> UVec2 {
        let border_size = if self.has_border() {
            UVec2::splat(2)
        } else {
            UVec2::ZERO
        };
        self.size + border_size
    }

    pub fn width_with_border(&self) -> usize {
        if self.has_border() {
            self.width() + 2
        } else {
            self.width()
        }
    }

    pub fn height_with_border(&self) -> usize {
        if self.has_border() {
            self.height() + 2
        } else {
            self.height()
        }
    }

    /// Whether or not the terminal has a border.
    pub fn has_border(&self) -> bool {
        self.border.is_some()
    }

    /// Convert a local 2d position to it's corresponding
    /// 1d index
    #[inline]
    pub fn transform_lti(&self, xy: impl GridPoint) -> usize {
        self.tiles.transform_lti(xy)
    }

    /// Convert 1D index to it's local terminal position.
    #[inline]
    pub fn transform_itl(&self, i: usize) -> IVec2 {
        self.tiles.transform_itl(i)
    }

    /// Insert a formatted character into the terminal.
    ///
    /// The [`TileModifier`] trait allows you to optionally specify a foreground
    /// and/or background color for the tile using the `fg` and `bg` functions.
    /// If you don't specify a color then the existing color in the terminal tile will
    /// be unaffected.
    ///
    /// # Example
    ///
    /// ```rust
    /// use bevy_ascii_terminal::prelude::*;
    /// use bevy::prelude::Color;
    ///
    /// let mut term = Terminal::new([10,10]);
    /// // Insert an 'a' with a blue foreground and a red background.
    /// term.put_char([2,3], 'a'.fg(Color::BLUE).bg(Color::RED));
    /// // Replace the 'a' with a 'q'. Foreground and background color will be
    /// // unaffected
    /// term.put_char([2,3], 'q');
    /// ```
    pub fn put_char(&mut self, xy: impl GridPoint, writer: impl TileFormatter) {
        let fmt = writer.format();
        fmt.draw(xy, self);
    }

    /// Change the foreground or background color for a single tile in the terminal.
    ///
    /// # Example
    ///
    /// ```rust
    /// use bevy::prelude::*;
    /// use bevy_ascii_terminal::prelude::*;
    /// let mut term = Terminal::new([10,10]);
    ///
    /// // Set the background color for the given tile to blue.
    /// term.put_color([3,3], Color::BLUE.bg());
    /// ```
    pub fn put_color(&mut self, xy: impl GridPoint, color: ColorFormat) {
        let tile = self.get_tile_mut(xy);
        match color {
            ColorFormat::FgColor(col) => tile.fg_color = col,
            ColorFormat::BgColor(col) => tile.bg_color = col,
        }
    }

    /// Insert a [Tile].
    pub fn put_tile(&mut self, xy: impl GridPoint, tile: Tile) {
        let t = self.get_tile_mut(xy);
        *t = tile;
    }

    /// Write a formatted string to the terminal.
    ///
    /// The [`StringFormatter`] trait allows you to optionally specify a foreground
    /// and/or background color for the string using the `fg` and `bg` functions.
    /// If you don't specify a color then the existing colors in the terminal
    /// will be unaffected.
    ///
    /// # Example
    ///
    /// ```rust
    /// use bevy_ascii_terminal::prelude::*;
    /// use bevy::prelude::Color;
    ///
    /// let mut term = Terminal::new([10,10]);
    /// // Write a blue "Hello" to the terminal
    /// term.put_string([1,2], "Hello".fg(Color::BLUE));
    /// // Write "Hello" with a green background
    /// term.put_string([2,1], "Hello".bg(Color::GREEN));
    /// ```
    ///
    /// You can also specify a `Pivot` for the string via the `pivot` function.
    /// This will align the string to the given pivot point. If the string
    /// is multiple lines, it will be adjusted appropriately to fit the given
    /// alignment.
    ///
    /// # Example
    ///
    /// ```rust
    /// use bevy_ascii_terminal::*;
    /// use bevy::prelude::Color;
    ///
    /// let mut term = Terminal::new([10,10]);
    /// // Write a mutli-line string to the center of the terminal
    /// term.put_string([0,0].pivot(Pivot::Center), "Hello\nHow are you?");
    /// ```
    pub fn put_string<'a>(&mut self, xy: impl GridPoint, writer: impl StringFormatter<'a> + 'a) {
        let pivot = if let Some(pivot) = xy.get_pivot() {
            Vec2::from(pivot)
        } else {
            Vec2::ZERO
        };
        let origin = self.tiles.pivoted_point(xy);
        let fmt = writer.formatted();
        let string = &fmt.string;

        let h = string.lines().count() as i32;
        let y = (origin.y as f32 + (h - 1) as f32 * (1.0 - pivot.y)) as i32;

        let bounds = self.tiles.bounds();

        //println!("Origin {}, y {}", origin, y);

        for (i, line) in string.lines().enumerate() {
            let y = y - i as i32;
            //println!("Origin {}, Line {}. Bounds {}", origin, y, bounds);
            if y < bounds.min_i().y || y > bounds.max_i().y {
                break;
            }

            let len = line.chars().count().min(self.width());
            let x = origin.x - ((len - 1) as f32 * pivot.x) as i32;
            //println!("Getting index for {}, {}", x, y);
            let i = self.transform_lti([x, y]);
            //println!("X {}, I {}", x, i);
            let tiles = self.tiles.slice_mut()[i..].iter_mut().take(len);

            //println!("Writing string at {:?}", [x,y]);

            for (char, mut t) in line.chars().zip(tiles) {
                t.glyph = char;
                fmt.apply(t);
            }
        }
    }

    /// Clear a range of characters to the terminal's `clear_tile`.
    pub fn clear_string(&mut self, xy: impl GridPoint, len: usize) {
        let i = self.transform_lti(xy);
        for t in self.tiles.slice_mut()[i..].iter_mut().take(len) {
            *t = self.clear_tile;
        }
    }

    /// Retrieve the char from a tile.
    pub fn get_char(&self, xy: impl GridPoint) -> char {
        self.get_tile(xy).glyph
    }

    /// Retrieve a string from the terminal.
    pub fn get_string(&self, xy: impl GridPoint, len: usize) -> String {
        let i = self.transform_lti(xy);
        let iter = self.tiles.slice()[i..].iter().take(len).map(|t| t.glyph);

        String::from_iter(iter)
    }

    #[inline]
    /// Retrieve an immutable reference to a tile in the terminal.
    pub fn get_tile(&self, xy: impl GridPoint) -> &Tile {
        &self.tiles[self.transform_lti(xy)]
    }

    #[inline]
    /// Retrieve a mutable reference to a tile in the terminal.
    pub fn get_tile_mut(&mut self, xy: impl GridPoint) -> &mut Tile {
        let i = self.transform_lti(xy);
        &mut self.tiles[i]
    }

    /// Clear an area of the terminal to the terminal's `clear_tile`.
    pub fn clear_box(&mut self, xy: impl GridPoint, size: impl Size2d) {
        let [width, height] = size.as_array();
        let [x, y] = xy.as_array();
        for y in y..y + height as i32 {
            for x in x..x + width as i32 {
                self.put_tile([x, y], self.clear_tile);
            }
        }
    }

    /// Clear the terminal tiles to the terminal's `clear_tile`.
    pub fn clear(&mut self) {
        for t in self.tiles.iter_mut() {
            *t = self.clear_tile
        }
    }

    pub fn clear_line(&mut self, line: usize) {
        let tile = self.clear_tile;
        self.iter_row_mut(line).for_each(|t| *t = tile);
    }

    /// Returns true if the given position is inside the bounds of the terminal.
    #[inline]
    pub fn in_bounds(&self, xy: impl GridPoint) -> bool {
        self.tiles.in_bounds(xy)
    }

    /// An immutable iterator over the tiles of the terminal.
    pub fn iter(&self) -> impl DoubleEndedIterator<Item = &Tile> {
        self.tiles.iter()
    }

    /// A mutable iterator over the tiles of the terminal.
    pub fn iter_mut(&mut self) -> impl DoubleEndedIterator<Item = &mut Tile> {
        self.tiles.iter_mut()
    }

    /// An immutable iterator over an entire row of tiles in the terminal.
    pub fn iter_row(&self, y: usize) -> impl DoubleEndedIterator<Item = &Tile> {
        self.tiles.iter_row(y)
    }

    /// An immutable iterator over an entire row of tiles in the terminal.
    pub fn iter_row_mut(&mut self, y: usize) -> impl DoubleEndedIterator<Item = &mut Tile> {
        self.tiles.iter_row_mut(y)
    }

    /// An immutable iterator over a range of rows in the terminal.
    ///
    /// The iterator moves along each row from left to right, where 0 is the
    /// bottom row and `height - 1` is the top row.
    pub fn iter_rows(
        &self,
        range: impl RangeBounds<usize>,
    ) -> impl DoubleEndedIterator<Item = &[Tile]> {
        self.tiles.iter_rows(range)
    }

    /// A mutable iterator over a range of rows in the terminal.
    ///
    /// The iterator moves along each row from left to right, where 0 is the
    /// bottom row and `height - 1` is the top row.
    pub fn iter_rows_mut(
        &mut self,
        range: impl RangeBounds<usize>,
    ) -> impl DoubleEndedIterator<Item = &mut [Tile]> {
        self.tiles.iter_rows_mut(range)
    }

    /// An immutable iterator over an entire column of tiles in the terminal.
    ///
    /// The iterator moves from bottom to top.
    pub fn iter_column(&self, x: usize) -> impl DoubleEndedIterator<Item = &Tile> {
        self.tiles.iter_column(x)
    }

    /// A mutable iterator over an entire column of tiles in the terminal.
    ///
    /// The iterator moves from bottom to top.
    pub fn iter_column_mut(&mut self, x: usize) -> impl DoubleEndedIterator<Item = &mut Tile> {
        self.tiles.iter_column_mut(x)
    }

    /// Get the index for a given side on the terminal.
    pub fn side_index(&self, side: Side) -> usize {
        self.tiles.side_index(side)
    }

    /// Transform a position from terminal local space (origin bottom left) to
    /// world space (origin center).
    #[inline]
    pub fn transform_ltw(&self, pos: impl GridPoint) -> IVec2 {
        pos.as_ivec2() - self.size.as_ivec2().sub(1).div(2)
    }

    /// Transform a position from world space (origin center) to terminal local
    /// space (origin bottom left).
    #[inline]
    pub fn transform_wtl(&self, pos: impl GridPoint) -> IVec2 {
        //println!("P {}, Half size {}", pos.as_ivec2(),  self.size.as_ivec2().sub(1).div(2));
        pos.as_ivec2() + self.size.as_ivec2().div(2)
    }

    pub fn slice(&self) -> &[Tile] {
        self.tiles.slice()
    }

    pub fn slice_mut(&mut self) -> &mut [Tile] {
        self.tiles.slice_mut()
    }

    pub fn bounds_with_border(&self) -> GridRect {
        let bounds = self.bounds();
        if self.has_border() {
            bounds.resized([1, 1])
        } else {
            bounds
        }
    }

    pub fn bounds(&self) -> GridRect {
        let mut bounds = self.tiles.bounds();
        bounds.center -= self.size.as_ivec2() / 2;
        //println!("TERM BOUNDS {}", bounds);
        bounds
    }
}

#[cfg(test)]
mod tests {

    use super::*;

    #[test]
    fn put_char() {
        let mut term = Terminal::new([20, 20]);

        term.put_char([5, 5], 'h');

        assert_eq!('h', term.get_char([5, 5]));

        term.put_char([1, 2], 'q'.fg(Color::RED));

        let t = term.get_tile([1, 2]);
        assert_eq!('q', t.glyph);
        assert_eq!(Color::RED, t.fg_color);
    }

    #[test]
    fn put_string() {
        let mut term = Terminal::new([20, 20]);
        // term.put_string([0, 0], "Hello");
        // assert_eq!("Hello", term.get_string([0, 0], 5));

        term.put_string([1, 1], "Hello");
        assert_eq!("He", term.get_string([1, 1], 2));
    }
}