paintty 0.1.0

Painting app that runs in your terminal
use std::io::{self, Stdout};

use crossterm::{
  cursor, queue,
  style::{self, Color, Stylize},
  terminal::WindowSize,
};

use crate::canvas::{Canvas, PaintTool, Pixel};

pub struct Bound {
  top: u16,
  left: u16,

  bottom: u16,
  right: u16,
}

impl Bound {
  pub fn from_pos_size(pos: (u16, u16), size: (u16, u16)) -> Self {
    Self {
      top: pos.1,
      left: pos.0,

      bottom: pos.1 + size.1,
      right: pos.0 + size.0,
    }
  }

  pub fn contains(&self, pos: (u16, u16)) -> bool {
    return (self.left..self.right).contains(&pos.0) && (self.top..self.bottom).contains(&pos.1);
  }
}

pub struct DialogState {
  pub hidden: bool,
  toolbar_pos: (u16, u16),
  palette_pos: (u16, u16),
  bounds: Vec<Bound>,
}

impl DialogState {
  const TOOLBAR_SIZE: (u16, u16) = (4, 4);
  const PALETTE_SIZE: (u16, u16) = (24, 4);
  const COLORS: [Pixel; 20] = [
    Pixel::from_rgb(0, 0, 0),
    Pixel::from_rgb(120, 120, 120),
    Pixel::from_rgb(153, 0, 48),
    Pixel::from_rgb(237, 28, 36),
    Pixel::from_rgb(255, 126, 0),
    Pixel::from_rgb(255, 242, 0),
    Pixel::from_rgb(34, 177, 76),
    Pixel::from_rgb(0, 183, 239),
    Pixel::from_rgb(47, 54, 153),
    Pixel::from_rgb(111, 49, 152),
    Pixel::from_rgb(255, 255, 255),
    Pixel::from_rgb(180, 180, 180),
    Pixel::from_rgb(156, 90, 60),
    Pixel::from_rgb(255, 163, 177),
    Pixel::from_rgb(255, 194, 14),
    Pixel::from_rgb(245, 228, 156),
    Pixel::from_rgb(168, 230, 29),
    Pixel::from_rgb(153, 217, 234),
    Pixel::from_rgb(112, 154, 209),
    Pixel::from_rgb(181, 165, 213),
  ];

  pub fn new(terminal_size: &WindowSize) -> Self {
    let toolbar_pos = (2, terminal_size.rows - 5);
    let palette_pos = (toolbar_pos.0 + Self::TOOLBAR_SIZE.0, terminal_size.rows - 5);

    Self {
      hidden: true,
      toolbar_pos,
      palette_pos,
      bounds: Vec::new(),
    }
  }

  pub fn bounds(&self) -> &Vec<Bound> {
    &self.bounds
  }

  pub fn render_palette(&mut self, stdout: &mut Stdout) -> io::Result<()> {
    self
      .bounds
      .push(Bound::from_pos_size(self.palette_pos, Self::PALETTE_SIZE));
    draw_dialog(stdout, self.palette_pos, Self::PALETTE_SIZE)?;

    queue!(
      stdout,
      cursor::MoveTo(self.palette_pos.0 + 2, self.palette_pos.1 + 1),
    )?;

    for color in &Self::COLORS[0..10] {
      queue!(stdout, style::PrintStyledContent("  ".on((*color).into())))?;
    }

    queue!(
      stdout,
      cursor::MoveTo(self.palette_pos.0 + 2, self.palette_pos.1 + 2),
    )?;

    for color in &Self::COLORS[10..20] {
      queue!(stdout, style::PrintStyledContent("  ".on((*color).into())))?;
    }

    Ok(())
  }

  pub fn render(&mut self, stdout: &mut Stdout, canvas: &Canvas) -> io::Result<()> {
    self.bounds.clear();

    if self.hidden {
      return Ok(());
    }

    self
      .bounds
      .push(Bound::from_pos_size(self.toolbar_pos, Self::TOOLBAR_SIZE));
    draw_dialog(stdout, self.toolbar_pos, Self::TOOLBAR_SIZE)?;
    let mut brush_color = Color::Reset;
    let mut bucket_color = Color::Reset;
    match canvas.current_tool() {
      PaintTool::Paintbrush => brush_color = Color::White,
      PaintTool::Bucket => bucket_color = Color::White,
    }

    queue!(
      stdout,
      cursor::MoveTo(self.toolbar_pos.0 + 1, self.toolbar_pos.1 + 1),
      style::Print(" "),
      style::Print("🖌️ ".on(brush_color)),
      cursor::MoveTo(self.toolbar_pos.0 + 1, self.toolbar_pos.1 + 2),
      style::Print(" "),
      style::Print("🪣".on(bucket_color)),
    )?;

    self.render_palette(stdout)?;

    Ok(())
  }

  pub fn interact(&mut self, pos: (u16, u16), canvas: &mut Canvas) {
    if pos.0 >= self.toolbar_pos.0 + 2 && pos.0 < self.toolbar_pos.0 + Self::TOOLBAR_SIZE.0 {
      match pos.1 - self.toolbar_pos.1 {
        1 => canvas.set_tool(PaintTool::Paintbrush),
        2 => canvas.set_tool(PaintTool::Bucket),
        _ => (),
      }
    } else {
      let color_column_offset = (pos.0)
        .checked_sub(self.palette_pos.0 + 2)
        .map(|x| x / 2)
        .take_if(|x| *x < 10);

      let color_row_offset = (pos.1)
        .checked_sub(self.palette_pos.1 + 1)
        .take_if(|y| *y < 2)
        .map(|y| y * 10);

      let color_index = color_row_offset
        .and_then(|row_offset| color_column_offset.map(|column_offset| row_offset + column_offset));
      if let Some(color_index) = color_index {
        canvas.set_color(Self::COLORS[color_index as usize]);
      }
    }
  }
}

fn draw_dialog(stdout: &mut Stdout, position: (u16, u16), size: (u16, u16)) -> io::Result<()> {
  for y in position.1..(position.1 + size.1) {
    queue!(
      stdout,
      cursor::MoveTo(position.0, y as u16),
      style::PrintStyledContent(" ".repeat(size.0 as usize).reset())
    )?;
  }

  Ok(())
}