tuigui 0.23.0

An easy-to-use, highly extensible, and speedy TUI library.
Documentation
use crate::preludes::widget_creation::*;
use crate::type_aliases::HashMap;

const FULL_CHAR: char = '';
const DUAL_CHAR_TOP: char = '';
const DUAL_CHAR_BOTTOM: char = '';

#[derive(Debug, Clone, Eq, PartialEq)]
/// A simple widget that just shows a grid of "pixels"
/// In this context, 2 pixels take up the same area as a character
pub struct PixelGrid {
    /// Filler color (background color)
    pub filler: Option<Color>,
    /// Character to use for differently colored top and bottom pixels (foreground top)
    pub dual_char_top: char,
    /// Character to use for differently colored top and bottom pixels (foreground bottom)
    pub dual_char_bottom: char,
    /// Character to use for samely colored top and bottom pixels
    pub single_char: char,
    /// Widget size information
    pub size_info: WidgetSizeInfo,
    /// Pixel position offset (offset all pixel positions)
    pub pixel_offset: Position,
    /// Pixel size
    pub pixel_size: Size,
    /// StyleGround to use for transparency when the filler is nothing
    pub transparent_style_ground: StyleGround,
    /// Pixels
    pixels: HashMap<Position, Color>,
    /// Widget data
    widget_data: WidgetData,
}

impl PixelGrid {
    pub fn new() -> Self {
        Self {
            filler: None,
            dual_char_top: DUAL_CHAR_TOP,
            dual_char_bottom: DUAL_CHAR_BOTTOM,
            single_char: FULL_CHAR,
            size_info: WidgetSizeInfo::Dynamic {
                min: None,
                max: None,
            },
            pixel_offset: Position::zero(),
            pixel_size: Size::same(1),
            transparent_style_ground: StyleGround::Transparent(StylePullLayer::Any),
            pixels: HashMap::new(),
            widget_data: WidgetData::new(),
        }
    }

    #[inline(always)]
    /// Returns either a pixel or the filler if there is
    /// no pixel at the specified position
    pub fn get_pixel(&self, position: Position) -> Option<Color> {
        return match self.pixels.get(&position) {
            Some(s) => Some(*s),
            None => self.filler,
        };
    }

    #[inline(always)]
    /// Set a pixel at a specific position
    ///
    /// Some(...) = Set pixel
    /// None = Clear pixel
    pub fn set_pixel(&mut self, position: Position, color: Option<impl Into<Color>>) {
        let color: Option<Color> = match color {
            Some(s) => Some(s.into()),
            None => None,
        };

        match color {
            Some(s) => self.pixels.insert(position, s),
            None => self.pixels.remove(&position),
        };
    }

    #[inline(always)]
    /// Erase everything
    pub fn clear_all(&mut self) {
        self.pixels = HashMap::new(); // Re-assigning a new HashMap seems to be faster than '.clear()'
    }

    #[inline(always)]
    /// Assemble pixel content when drawing on a canvas
    pub fn assemble_pixel_content(&self, top_pixel: Option<Color>, bottom_pixel: Option<Color>) -> Option<Content> {
        let content: Option<Content>;

        if top_pixel == bottom_pixel {
            content = Some(match top_pixel {
                Some(s) => Content::Styled(self.single_char, Style::new().fg(Some(s))),
                None => Content::Styled(' ', Style::new().bg(self.transparent_style_ground)),
            });
        } else {
            content = Some(match (top_pixel, bottom_pixel) {
                (Some(top), Some(bottom)) => Content::Styled(self.dual_char_top, Style::new().fg(Some(top)).bg(Some(bottom))),
                (Some(top), None) => Content::Styled(self.dual_char_top, Style::new().fg(Some(top)).bg(self.transparent_style_ground)),
                (None, Some(bottom)) => Content::Styled(self.dual_char_bottom, Style::new().fg(Some(bottom)).bg(self.transparent_style_ground)),
                _ => unreachable!(),
            });
        }

        return content;
    }
}

impl Widget for PixelGrid {
    fn draw(&mut self, canvas: &mut Canvas, _state_frame: Option<&EventStateFrame>) {
        let scale_col: f64 = self.pixel_size.cols.max(1) as f64;
        let scale_row: f64 = self.pixel_size.rows.max(1) as f64;

        for row in 0..canvas.transform.size.rows {
            for col in 0..canvas.transform.size.cols {
                let draw_position = Position::new(col as i16, row as i16);

                let mut top_position = Position::new(col as i16, (row as i16) << 1);
                let mut bottom_position = top_position + Position::new(0, 1);

                top_position = Position::new(
                    (top_position.col as f64 / scale_col).floor() as i16,
                    (top_position.row as f64 / scale_row).floor() as i16,
                );

                bottom_position = Position::new(
                    (bottom_position.col as f64 / scale_col).floor() as i16,
                    (bottom_position.row as f64 / scale_row).floor() as i16,
                );

                top_position = top_position - self.pixel_offset;
                bottom_position = bottom_position - self.pixel_offset;

                let top_pixel = self.get_pixel(top_position);
                let bottom_pixel = self.get_pixel(bottom_position);

                let content = self.assemble_pixel_content(top_pixel, bottom_pixel);

                canvas.set(draw_position, content);
            }
        }
    }

    fn widget_info(&self) -> WidgetInfo {
        WidgetInfo {
            size_info: self.size_info,
        }
    }

    fn widget_data(&mut self) -> &mut WidgetData {
        return &mut self.widget_data;
    }
}