use crate::{ bitmap::{positive_angle_with_x, MapPoint, Pixmap}, brush::{Brush, CircleBrush, LineBrush}, command::CommandBox, consts::{colors::*, FONT_PATH, RC_PATH, STDLIB_PATH}, dither, error::AppError, lisp::{eval, lex::Lexer, parse::Parser, prelude, EnvList}, message::Message, rect, symmetry::Symmetry, undo::{ModifyRecord, OpKind, PaintRecord, UndoStack}, utils::{draw_text, handle_error, is_copy_event, is_paste_event, load_script}, }; use std::{ convert::From, fs::File, io::prelude::*, path::{Path, PathBuf}, }; use obi::Image; use sdl2::{ event::Event, keyboard::{Keycode, Mod}, mouse::MouseButton, pixels::Color, rect::{Point, Rect}, render::Canvas, ttf::Sdl2TtfContext, video::Window, Sdl, }; #[derive(Debug, Copy, Clone, PartialEq)] pub enum Mode { Draw, Command, } pub struct AppState<'ctx> { pub active_color: bool, pub brush: Brush, pub canvas: Canvas, pub command_box: CommandBox, pub context: &'ctx Sdl, pub current_operation: Vec, pub dither_level: u8, pub file_name: Option, pub grid: Grid, pub lisp_env: EnvList, pub message: Message, pub mode: Mode, pub mouse: (i32, i32), pub pixmap: Pixmap, pub start: Point, pub symmetry: Symmetry, pub ttf_context: &'ctx Sdl2TtfContext, pub undo_stack: UndoStack, pub zoom: u8, } pub struct Grid { pub enabled: bool, pub color: Color, } impl Grid { fn new() -> Self { Self { enabled: true, color: GRID_COLOR, } } } // private actions on appstate impl<'ctx> AppState<'ctx> { fn pan>(&mut self, direction: P) { self.start += direction.into(); } pub fn width(&self) -> u32 { self.pixmap.width } pub fn height(&self) -> u32 { self.pixmap.height } fn bounds(&self) -> (Point, Point) { let x_min = self.start.x(); let y_min = self.start.y(); let x_max = self.start.x() + (self.width() * self.zoom as u32) as i32; let y_max = self.start.y() + (self.height() * self.zoom as u32) as i32; return ( Point::new(x_min, y_min), Point::new(x_max as i32, y_max as i32), ); } pub fn change_active_color(&mut self) { self.active_color = !self.active_color; } pub fn idx_at_coord>(&self, p: P) -> Option<(u32, u32)> { let p: Point = p.into(); if self.within_canvas(p) { // convert p relative to start of drawing area let rel_p = p - self.start; // reduce p based on zoom and cell size let (sx, sy) = (rel_p.x() / self.zoom as i32, rel_p.y() / self.zoom as i32); return Some((sx as u32, sy as u32)); } else { None } } pub fn within_canvas>(&self, p: P) -> bool { let p: Point = p.into(); let (mini, maxi) = self.bounds(); p.x() < maxi.x() && p.y() < maxi.y() && p.x() >= mini.x() && p.y() >= mini.y() } pub fn toggle_grid(&mut self) { self.grid.enabled = !self.grid.enabled; } pub fn cycle_symmetry(&mut self) { let Symmetry { x, y } = self.symmetry; self.symmetry = match (x, y) { (None, None) => Symmetry { x: Some(self.width() / 2), y: None, }, (_, None) => Symmetry { x: None, y: Some(self.height() / 2), }, (None, y) => Symmetry { x: Some(self.width() / 2), y, }, (Some(_), Some(_)) => Symmetry { x: None, y: None }, } } pub fn paint_point>( &mut self, center: P, val: bool, brush_size: u8, ) -> Result, ()> { let radius = brush_size as u32; let center = self.idx_at_coord(center).ok_or(())?; let dither_level = self.dither_level; let circle = self.pixmap.get_circle(center, radius, true); let sym_circle = self.symmetry.apply(&circle); let mut modify_record = vec![]; for point in circle .into_iter() .chain(sym_circle) .filter(|&pt| dither::bayer(dither_level, pt)) { let old_val = self.pixmap.set(point, val); modify_record.push(PaintRecord::new(point, old_val, val)); } Ok(modify_record) } pub fn paint_line>( &mut self, start: MapPoint, end: P, val: bool, brush_size: u8, ) -> Result, ()> { let MapPoint { x, y } = start; let end = self.idx_at_coord(end).ok_or(())?; let dither_level = self.dither_level; let line = self.pixmap.get_line((x, y), end); let sym_line = self.symmetry.apply(&line); let mut line_modify_record = vec![]; for point in line.into_iter().chain(sym_line) { let circle_around_point = self.pixmap.get_circle(point, brush_size as u32, true); for c in circle_around_point .into_iter() .filter(|&pt| dither::bayer(dither_level, pt)) { let old_val = self.pixmap.set(c, val); line_modify_record.push(PaintRecord::new(c, old_val, val)); } } Ok(line_modify_record) } pub fn apply_operation(&mut self, operation: ModifyRecord, op_kind: OpKind) { match operation { ModifyRecord::Paint(paints) => { for PaintRecord { point, old, new } in paints { if self.pixmap.contains(point) { match op_kind { OpKind::Undo => self.pixmap.set(point, old), OpKind::Redo => self.pixmap.set(point, new), }; } } } ModifyRecord::Invert => self.pixmap.invert(), ModifyRecord::Brush { old, new } => { match op_kind { OpKind::Undo => self.brush = old, OpKind::Redo => self.brush = new, }; } } } pub fn commit_operation(&mut self) { if !self.current_operation.is_empty() { let op = self .current_operation .drain(..) .filter(|v| !v.old == v.new) .collect::>(); self.undo_stack.push(ModifyRecord::Paint(op)); } } pub fn zoom_in(&mut self, p: (i32, i32)) { // attempt to center around cursor if let Some(p) = self.idx_at_coord(p) { let (x1, y1) = (p.0 * (self.zoom as u32), p.1 * (self.zoom as u32)); let (x2, y2) = (p.0 * (1 + self.zoom as u32), p.1 * (1 + self.zoom as u32)); let diffx = x2 as i32 - x1 as i32; let diffy = y2 as i32 - y1 as i32; self.start = self.start - Point::from((diffx, diffy)); } self.zoom += 1; } pub fn zoom_out(&mut self, p: (i32, i32)) { if self.zoom > 1 { // attempt to center around cursor if let Some(p) = self.idx_at_coord(p) { let (x1, y1) = (p.0 * (self.zoom as u32), p.1 * (self.zoom as u32)); let (x2, y2) = (p.0 * (self.zoom as u32 - 1), p.1 * (self.zoom as u32 - 1)); let diffx = x2 as i32 - x1 as i32; let diffy = y2 as i32 - y1 as i32; self.start = self.start - Point::from((diffx, diffy)); } self.zoom -= 1; } } pub fn center_grid(&mut self) { let (winsize_x, winsize_y) = self.canvas.window().size(); let grid_width = self.width() * self.zoom as u32; let grid_height = self.height() * self.zoom as u32; self.start = Point::new( (winsize_x as i32 - grid_width as i32) / 2, (winsize_y as i32 - grid_height as i32) / 2, ); } pub fn reduce_intensity(&mut self) { if self.dither_level > 0 { self.dither_level -= 1; } } pub fn increase_intensity(&mut self) { if self.dither_level < 16 { self.dither_level += 1; } } pub fn eval_command(&mut self) { let lisp_expr = &self.command_box.text; let mut parser = Parser::new(Lexer::new(lisp_expr, 0)); let res = parser.parse_single_expr(); match res { Ok(expr) => { let mut evaluator = eval::Evaluator { app: self, context: Vec::new(), }; match evaluator.eval(&expr) { Ok(val) => self.message.set_info(format!("{}", val)), Err(eval_err) => self.message.set_error(format!("{}", eval_err)), } } Err(err) => self.message = handle_error(err, &lisp_expr, "repl"), } self.command_box.hist_append(); self.command_box.clear(); self.mode = Mode::Draw; } fn draw_grid(&mut self) { let cs = self.zoom as u32; let (width, height) = (self.width(), self.height()); let canvas = &mut self.canvas; canvas.set_draw_color(self.grid.color); for i in 0..=width { let x = (i * cs) as i32; let y = (height * cs) as i32; let start = self.start + Point::new(x, 0); let end = self.start + Point::new(x, y); canvas.draw_line(start, end).unwrap(); } for j in 0..=height { let x = (width * cs) as i32; let y = (j * cs) as i32; let start = self.start + Point::new(0, y); let end = self.start + Point::new(x, y); canvas.draw_line(start, end).unwrap(); } } fn draw_statusline(&mut self) { let (winsize_x, winsize_y) = self.canvas.window().size(); let status_height: u32 = 20; let status_width = winsize_x; self.canvas.set_draw_color(WHITE); self.canvas .fill_rect(rect!( 0, winsize_y - status_height - 20, status_width, status_height )) .unwrap(); let mouse_coords = if let Some((x, y)) = self.idx_at_coord(self.mouse) { format!("{:3}, {:3}", x + 1, y + 1) } else { format!("---, ---") }; let status_text = format!( "[DITHER {}][SYM {}][PT {}][ACTIVE {}][KIND {}]", self.dither_level, self.symmetry, mouse_coords, if self.active_color { "WHT" } else { "BLK" }, self.brush ); draw_text( &mut self.canvas, self.ttf_context, status_text, BLACK, (0, winsize_y - status_height - 20), ); } fn draw_command_box(&mut self) { let (winsize_x, winsize_y) = self.canvas.window().size(); let cmd_height: u32 = 20; let cmd_width = winsize_x; self.canvas.set_draw_color(BLACK); self.canvas .fill_rect(rect!(0, winsize_y - cmd_height, cmd_width, cmd_height)) .unwrap(); if self.command_box.is_empty() { self.mode = Mode::Draw; // show msg draw_text( &mut self.canvas, self.ttf_context, &self.message.text[..], self.message.kind.into(), (0, winsize_y - cmd_height), ); return; } // show repl draw_text( &mut self.canvas, self.ttf_context, &self.command_box.text[..], WHITE, (0, winsize_y - cmd_height), ); self.canvas.set_draw_color(PINK); let mut font = self.ttf_context.load_font(FONT_PATH, 17).unwrap(); font.set_style(sdl2::ttf::FontStyle::NORMAL); font.set_hinting(sdl2::ttf::Hinting::Mono); let prev_text = &self.command_box.text[..self.command_box.cursor]; let prev_text_dim = font.size_of_latin1(prev_text.as_bytes()).unwrap(); let cursor = rect!( prev_text_dim.0, winsize_y - cmd_height + 2, 2, cmd_height - 4 ); self.canvas.fill_rect(cursor).unwrap(); } fn draw_mouse(&mut self) { let cs = self.zoom as u32; let pt = self.idx_at_coord(self.mouse); if matches!(self.brush, Brush::Circle { .. } | Brush::Line { .. }) { let size = self.brush.size().unwrap(); if let Some(center) = pt { let circle = self.pixmap.get_circle(center, size as u32, false); for MapPoint { x, y } in circle.into_iter() { self.canvas.set_draw_color(PINK); self.canvas .fill_rect(Rect::new( x as i32 * cs as i32 + self.start.x(), y as i32 * cs as i32 + self.start.y(), cs, cs, )) .unwrap(); } } } match self.brush { Brush::Line(LineBrush { start, size, .. }) => { let size = self.zoom as u32 * (size as u32 + 5); if let (Some(from), Some(to)) = (start, pt) { let line = self.pixmap.get_line(from, to.into()); draw_text( &mut self.canvas, self.ttf_context, format!("{}°", positive_angle_with_x(from, to.into())), PINK, (self.mouse.0 as u32 + size, self.mouse.1 as u32 + size), ); for MapPoint { x, y } in line.into_iter() { self.canvas.set_draw_color(PINK); self.canvas .fill_rect(Rect::new( x as i32 * cs as i32 + self.start.x(), y as i32 * cs as i32 + self.start.y(), cs, cs, )) .unwrap(); } } } _ => {} } } fn draw_symmetry(&mut self) { let (winsize_x, winsize_y) = self.canvas.window().size(); let cs = self.zoom as u32; let Symmetry { x: sym_x, y: sym_y } = self.symmetry; if let Some(line) = sym_x { self.canvas.set_draw_color(CYAN); let line_coord = (line * cs) as i32 + self.start.y() + (cs / 2) as i32; self.canvas .draw_line((0, line_coord), (winsize_x as i32, line_coord)) .unwrap(); } if let Some(line) = sym_y { self.canvas.set_draw_color(CYAN); let line_coord = (line * cs) as i32 + self.start.x() + (cs / 2) as i32; self.canvas .draw_line((line_coord, 0), (line_coord, winsize_y as i32)) .unwrap(); } } fn draw(&mut self) { let cs = self.zoom as u32; let (width, height) = (self.width(), self.height()); let start = self.start; let grid_enabled = self.grid.enabled; for (idx, val) in self.pixmap.data.iter().enumerate() { if *val { let idx = idx as i32; let (x, y) = (idx % width as i32, idx / height as i32); self.canvas.set_draw_color(WHITE); self.canvas .fill_rect(Rect::new( x * cs as i32 + start.x(), y * cs as i32 + start.y(), cs, cs, )) .unwrap(); } } if grid_enabled { self.draw_grid(); } self.draw_symmetry(); self.draw_statusline(); self.draw_command_box(); self.draw_mouse(); } fn redraw(&mut self) { self.canvas.set_draw_color(BLACK); self.canvas.clear(); self.draw(); self.canvas.present(); } pub fn quit(&mut self) { let ev = self.context.event().unwrap(); ev.push_event(Event::Quit { timestamp: 0u32 }) .expect("ohno unable to quit"); } pub fn init( width: u32, height: u32, context: &'ctx Sdl, ttf_context: &'ctx Sdl2TtfContext, start_data: Option>, file_name: Option, ) -> Result { let video_subsystem = context.video().map_err(AppError::Sdl)?; let window = video_subsystem .window("Pixel editor", 500, 500) .position_centered() .resizable() .opengl() .build() .map_err(|e| AppError::Sdl(e.to_string()))?; let canvas = window .into_canvas() .build() .map_err(|e| AppError::Sdl(e.to_string()))?; let data = start_data.unwrap_or(vec![false; (width * height) as usize]); let pixmap = Pixmap::new_with(width, height, data); let mut app = Self { active_color: true, brush: Brush::new(0), canvas, command_box: CommandBox::new(), context, current_operation: Vec::new(), dither_level: 16, file_name, grid: Grid::new(), lisp_env: vec![prelude::new_env().map_err(AppError::Lisp)?], message: Message::new().text(" "), mode: Mode::Draw, mouse: (0, 0), pixmap, start: Point::new(60, 60), symmetry: Default::default(), ttf_context, undo_stack: UndoStack::new(), zoom: 5, }; load_script(STDLIB_PATH, &mut app).map_err(AppError::Lisp)?; std::fs::create_dir_all(RC_PATH).map_err(AppError::File)?; load_script([RC_PATH, "rc.lisp"].iter().collect::(), &mut app) .map_err(AppError::Lisp)?; Ok(app) } pub fn export(&self) -> Image { let mut image = Image::new(self.width(), self.height()); image.data = self.pixmap.data.clone(); image } pub fn save_as>(&self, file_name: P) -> Result<(), AppError> { let image = self.export().encode().unwrap(); let mut file = File::create(file_name).map_err(AppError::File)?; file.write_all(&image[..]).unwrap(); return Ok(()); } pub fn run(&mut self) { self.canvas.set_draw_color(BLACK); self.canvas.clear(); self.draw(); self.canvas.present(); let mut event_pump = self.context.event_pump().unwrap(); 'running: loop { let mouse = event_pump.mouse_state(); self.mouse = (mouse.x(), mouse.y()); for event in event_pump.poll_iter() { if let Event::KeyDown { keycode: Some(Keycode::Num9), keymod, .. } = event { if keymod == Mod::LSHIFTMOD || keymod == Mod::RSHIFTMOD { self.mode = Mode::Command; } } match self.mode { Mode::Draw => { match event { Event::KeyDown { keycode: Some(k), keymod, .. } => { match k { // pan Keycode::W => self.pan((0, 10)), Keycode::A => self.pan((10, 0)), Keycode::S => self.pan((0, -10)), Keycode::D => self.pan((-10, 0)), // zoom Keycode::C if keymod == Mod::LSHIFTMOD => { self.center_grid(); } Keycode::C => { let cursor = (mouse.x(), mouse.y()); self.zoom_in(cursor); } Keycode::Z => { let cursor = (mouse.x(), mouse.y()); self.zoom_out(cursor); } // brush ops Keycode::Q => self.brush.shrink(), Keycode::E => self.brush.grow(), Keycode::Num1 => self.reduce_intensity(), Keycode::Num3 => self.increase_intensity(), // flip color Keycode::X => self.change_active_color(), // toggle grid Keycode::Tab => self.toggle_grid(), // invert canvas Keycode::I => { self.pixmap.invert(); self.undo_stack.push(ModifyRecord::Invert); } // line drawing Keycode::F => { if matches!( self.brush, Brush::Line(LineBrush { extend: false, .. }) ) { self.brush = Brush::line(0, true); } else { self.brush = Brush::line(0, false); } } // bucket fill tool Keycode::B => self.brush = Brush::Fill, Keycode::V => self.cycle_symmetry(), // undo & redo Keycode::U => { if let Some(op) = self.undo_stack.undo() { self.apply_operation(op, OpKind::Undo); } } Keycode::R => { if let Some(op) = self.undo_stack.redo() { self.apply_operation(op, OpKind::Redo); } } // export to file Keycode::N => { let image = self.export(); let encoded = image.encode().unwrap(); let mut buffer = File::create("test.obi").unwrap(); eprintln!("writing to file"); buffer.write_all(&encoded[..]).unwrap(); } _ => (), } } // start of operation Event::MouseButtonDown { x, y, mouse_btn, .. } => { let pt = (x, y); let contact = self.idx_at_coord(pt).map(MapPoint::from); let val = match mouse_btn { MouseButton::Right => !self.active_color, _ => self.active_color, }; match self.brush { Brush::Circle(CircleBrush { size }) => { if let Ok(o) = self.paint_point(pt, val, size) { self.current_operation.extend(o); } } Brush::Line(LineBrush { size, start, extend, }) => { if start.is_none() { self.brush = Brush::Line(LineBrush { size, start: contact, extend, }); } else if let Ok(o) = self.paint_line(start.unwrap(), pt, val, size) { self.current_operation.extend(o); self.brush = Brush::Line(LineBrush { size, start: if extend { contact } else { None }, extend, }); } } Brush::Fill => { if let Some(c) = contact { let target = self.pixmap.get(c); let mut operation = vec![]; self.pixmap.flood_fill( c, target, self.active_color, &mut operation, ); self.current_operation.extend( operation .into_iter() .map(|point| PaintRecord { point, old: target, new: self.active_color, }) .collect::>(), ) } } _ => {} } } // click and drag Event::MouseMotion { x, y, mousestate, .. } => { let size = match self.brush { Brush::Circle(CircleBrush { size }) => size, Brush::Line(LineBrush { size, .. }) => size, _ => continue, }; if mousestate.is_mouse_button_pressed(MouseButton::Left) { let pt = (x, y); let val = self.active_color; if let Ok(o) = self.paint_point(pt, val, size) { self.current_operation.extend(o); } } else if mousestate.is_mouse_button_pressed(MouseButton::Right) { let pt = (x, y); let val = !self.active_color; if let Ok(o) = self.paint_point(pt, val, size) { self.current_operation.extend(o); } } } // end of operation Event::MouseButtonUp { .. } => self.commit_operation(), Event::Quit { .. } => { break 'running; } _ => {} } } Mode::Command => { if let Event::KeyDown { keycode, keymod, .. } = event { let video = self.context.video().unwrap(); let clipboard = video.clipboard(); if is_copy_event(keycode, keymod) { clipboard .set_clipboard_text(&self.command_box.text) .unwrap(); } else if is_paste_event(keycode, keymod) && clipboard.has_clipboard_text() { self.command_box .push_str(&clipboard.clipboard_text().unwrap()); } } if let Event::KeyDown { keycode: Some(k), keymod, .. } = event { match k { Keycode::Backspace => self.command_box.backspace(), Keycode::Delete => self.command_box.delete(), Keycode::Left => self.command_box.backward(), Keycode::Right => self.command_box.forward(), Keycode::Up => self.command_box.hist_prev(), Keycode::Down => self.command_box.hist_next(), _ if keymod == Mod::LCTRLMOD => match k { Keycode::A => self.command_box.cursor_start(), Keycode::E => self.command_box.cursor_end(), Keycode::F => self.command_box.forward(), Keycode::B => self.command_box.backward(), Keycode::K => self.command_box.delete_to_end(), Keycode::U => self.command_box.delete_to_start(), _ => (), }, Keycode::Return => self.eval_command(), Keycode::Escape => { self.command_box.clear(); self.message.text = format!(" "); self.mode = Mode::Draw; } _ => (), } } match event { Event::TextInput { text, .. } => { self.command_box.push_str(&text[..]); } _ => (), } } } } self.redraw(); } } }