paintty 0.1.0

Painting app that runs in your terminal
pub enum PaintTool {
  Paintbrush,
  Bucket,
}

pub struct Canvas {
  pixels: Vec<Vec<Pixel>>,
  current_color: Pixel,
  current_tool: PaintTool,
}

impl Canvas {
  pub fn new(width: usize, height: usize) -> Self {
    let mut pixels = Vec::with_capacity(height);
    pixels.resize_with(height, || {
      let mut row = Vec::with_capacity(width);
      row.resize(width, Default::default());
      row
    });

    Self {
      pixels,
      current_color: Pixel::BLACK,
      current_tool: PaintTool::Paintbrush,
    }
  }

  pub fn pixels(&self) -> &Vec<Vec<Pixel>> {
    &self.pixels
  }

  fn get_pixel_mut(pixels: &mut Vec<Vec<Pixel>>, x: usize, y: usize) -> Option<&mut Pixel> {
    pixels.get_mut(y).and_then(|row| row.get_mut(x))
  }

  pub fn current_tool(&self) -> &PaintTool {
    &self.current_tool
  }

  pub fn set_tool(&mut self, tool: PaintTool) {
    self.current_tool = tool;
  }

  pub fn current_color(&self) -> &Pixel {
    &self.current_color
  }

  pub fn set_color(&mut self, color: Pixel) {
    self.current_color = color;
  }

  fn paint_pixel(&mut self, x: usize, y: usize) {
    let Some(pixel) = Self::get_pixel_mut(&mut self.pixels, x, y) else {
      return;
    };

    *pixel = self.current_color;
  }

  fn flood_fill_pixel(&mut self, origin_x: usize, origin_y: usize) {
    let Some(replace_color) = Self::get_pixel_mut(&mut self.pixels, origin_x, origin_y).copied()
    else {
      return;
    };

    let mut unseen_pixels = vec![(origin_x, origin_y)];

    while let Some((x, y)) = unseen_pixels.pop() {
      match Self::get_pixel_mut(&mut self.pixels, x, y).copied() {
        Some(color) if color == replace_color => (),
        _ => continue,
      }

      self.paint_pixel(x, y);

      let neighbors = vec![(0, 1), (1, 0), (0, -1), (-1, 0)]
        .into_iter()
        .flat_map(|pos| {
          x.checked_add_signed(pos.0)
            .and_then(|x| y.checked_add_signed(pos.1).map(|y| (x, y)))
        });

      unseen_pixels.extend(neighbors);
    }
  }

  pub fn interact_with_pixel(&mut self, x: usize, y: usize) {
    match self.current_tool {
      PaintTool::Paintbrush => self.paint_pixel(x, y),
      PaintTool::Bucket => self.flood_fill_pixel(x, y),
    }
  }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Pixel {
  r: u8,
  g: u8,
  b: u8,
  a: u8,
}

impl Pixel {
  pub const WHITE: Self = Self::from_rgb(255, 255, 255);
  pub const BLACK: Self = Self::from_rgb(0, 0, 0);

  pub const fn from_rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
    Self { r, g, b, a }
  }

  pub const fn from_rgb(r: u8, g: u8, b: u8) -> Self {
    Self::from_rgba(r, g, b, 255)
  }
}

impl Default for Pixel {
  fn default() -> Self {
    Self::WHITE
  }
}

impl From<Pixel> for crossterm::style::Color {
  fn from(value: Pixel) -> Self {
    use crossterm::style::Color;
    if value.a < 255 {
      return Color::Reset;
    }

    Color::Rgb {
      r: value.r,
      g: value.g,
      b: value.b,
    }
  }
}