paintty 0.1.0

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

use crossterm::{
  cursor,
  event::{
    self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseButton, MouseEvent,
    MouseEventKind,
  },
  execute, queue,
  style::{self, Stylize},
  terminal::{self, window_size},
};

use crate::{canvas::Canvas, dialog::DialogState};

pub struct App {
  stdout: Stdout,
  canvas: Canvas,
  dialogs: DialogState,
  should_quit: bool,
}

impl App {
  pub fn new() -> io::Result<Self> {
    let mut stdout = io::stdout();
    terminal::enable_raw_mode()?;
    execute!(stdout, event::EnableMouseCapture, cursor::Hide)?;

    let terminal_size = window_size()?;
    let canvas = Canvas::new(
      (terminal_size.columns / 2) as usize,
      terminal_size.rows as usize,
    );

    Ok(Self {
      stdout,
      canvas,
      dialogs: DialogState::new(&terminal_size),
      should_quit: false,
    })
  }

  fn redraw_screen(&mut self) -> io::Result<()> {
    queue!(
      self.stdout,
      terminal::BeginSynchronizedUpdate,
      terminal::Clear(terminal::ClearType::All)
    )?;

    for (y, row) in self.canvas.pixels().iter().enumerate() {
      queue!(self.stdout, cursor::MoveTo(0, y as u16))?;
      for pixel in row {
        let color = (*pixel).into();

        queue!(self.stdout, style::PrintStyledContent("  ".on(color)))?;
      }
    }

    self.dialogs.render(&mut self.stdout, &self.canvas)?;

    execute!(self.stdout, terminal::EndSynchronizedUpdate)?;

    Ok(())
  }

  pub fn handle_key_event(&mut self, event: KeyEvent) -> io::Result<()> {
    if !matches!(event.kind, KeyEventKind::Press) {
      return Ok(());
    }

    match event.code {
      KeyCode::Char('q') => self.should_quit = true,
      KeyCode::Char('c') if event.modifiers.contains(KeyModifiers::CONTROL) => {
        self.should_quit = true
      }

      _ => self.dialogs.hidden = !self.dialogs.hidden,
    }

    Ok(())
  }

  pub fn handle_mouse_event(&mut self, event: MouseEvent) -> io::Result<()> {
    if !matches!(
      event.kind,
      MouseEventKind::Down(MouseButton::Left) | MouseEventKind::Drag(MouseButton::Left)
    ) {
      return Ok(());
    }

    if self
      .dialogs
      .bounds()
      .iter()
      .any(|bound| bound.contains((event.column, event.row)))
    {
      self
        .dialogs
        .interact((event.column, event.row), &mut self.canvas);
    } else {
      self
        .canvas
        .interact_with_pixel((event.column / 2) as usize, event.row as usize);
    }

    Ok(())
  }

  pub fn handle_event(&mut self, event: Event) -> io::Result<()> {
    match event {
      Event::Key(event) => self.handle_key_event(event)?,
      Event::Mouse(event) => self.handle_mouse_event(event)?,
      _ => (),
    }

    Ok(())
  }

  pub fn run(&mut self) -> io::Result<()> {
    loop {
      self.redraw_screen()?;
      if self.should_quit {
        break;
      }

      self.handle_event(event::read()?)?;
      if self.should_quit {
        break;
      }
    }

    Ok(())
  }
}

impl Drop for App {
  fn drop(&mut self) {
    if let Ok((_, rows)) = terminal::size() {
      let _ = execute!(self.stdout, cursor::MoveTo(0, rows));
    }

    let _ = execute!(self.stdout, event::DisableMouseCapture, cursor::Show);
    let _ = terminal::disable_raw_mode();
  }
}