use crate::{ bitmap::{positive_angle_with_x, Axis, MapPoint, Pixmap}, brush::{Brush, CircleBrush, LineBrush}, cache::Cache, command::CommandBox, consts::{colors::*, ANGLE, FONT_PATH, RC_PATH, STDLIB_PATH}, dither, error::AppError, grid::Grid, guide::Guide, lisp::{eval, lex::Lexer, parse::Parser, prelude, EnvList}, message::Message, rect, symmetry::Symmetry, undo::{ModifyRecord, OpKind, PaintRecord, UndoStack}, utils::{self, draw_text, handle_error, is_copy_event, is_paste_event, load_script}, widget::{Container, HorAlign, Offset, Size, VertAlign}, }; use std::{ cell::RefCell, collections::HashMap, 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 cache: RefCell>, pub context: &'ctx Sdl, pub current_operation: Vec, pub dither_level: u8, pub file_name: Option, pub guides: HashMap, 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, } // 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 } pub fn cache(&self) { let mut cache = self.cache.borrow_mut(); *cache = Some(Cache { last_brush_size: self.brush.size().unwrap_or(0), }); } 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; ( 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); 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.height() / 2), y: None, }, (_, None) => Symmetry { x: None, y: Some(self.width() / 2), }, (None, y) => Symmetry { x: Some(self.height() / 2), y, }, (Some(_), Some(_)) => Symmetry { x: None, y: None }, } } pub fn cycle_brush(&mut self) { self.brush = match self.brush { Brush::Circle(_) => Brush::line(0, false), Brush::Line(LineBrush { extend: false, .. }) => Brush::line(0, true), Brush::Line(LineBrush { extend: true, .. }) => Brush::Fill, _ => Brush::new(0), } } 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)) { if self.pixmap.contains(point) { 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)) { if self.pixmap.contains(c) { 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 -= 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 -= 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)); 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_statusline(&mut self) { let container = Container::new(Offset::Left(0), Offset::Bottom(40), &self.canvas) .width(Size::Max, &self.canvas) .height(Size::Absolute(20), &self.canvas); self.canvas.set_draw_color(WHITE); self.canvas.fill_rect(container.area()).unwrap(); let mut padding_box = Container::uninit() .width(Size::Absolute(18), &self.canvas) .height(Size::Absolute(18), &self.canvas); let mut primary = Container::uninit() .width(Size::Absolute(16), &self.canvas) .height(Size::Absolute(16), &self.canvas); container.place(&mut padding_box, HorAlign::Right, VertAlign::Center); padding_box.place(&mut primary, HorAlign::Center, VertAlign::Center); self.canvas .set_draw_color(if !self.active_color { WHITE } else { BLACK }); self.canvas.fill_rect(primary.area()).unwrap(); self.canvas .set_draw_color(if self.active_color { WHITE } else { BLACK }); let brush_box = (0..8) .map(|x| (0..8).map(|y| (x, y).into()).collect::>()) .flatten() .filter(|&pt| dither::bayer(self.dither_level, pt)) .collect::>(); for pt in brush_box { let canvas_pt = Point::from(primary.start) + Point::from((pt.x as i32 * 2, pt.y as i32 * 2)); self.canvas .fill_rect(rect!(canvas_pt.x(), canvas_pt.y(), 2, 2)) .unwrap(); } let mouse_coords = if let Some((x, y)) = self.idx_at_coord(self.mouse) { format!("{:3}, {:3}", x + 1, y + 1) } else { String::from("---, ---") }; let status_text = format!( "{} [PT {}][KIND {}]", if self.file_name.is_some() { format!("{}", self.file_name.as_ref().unwrap().display()) } else { "No Name".into() }, mouse_coords, self.brush ); draw_text( &mut self.canvas, self.ttf_context, status_text, BLACK, container.start, ); } fn draw_command_box(&mut self) { let container = Container::new(Offset::Left(0), Offset::Bottom(20), &self.canvas) .width(Size::Max, &self.canvas) .height(Size::Absolute(20), &self.canvas); self.canvas.set_draw_color(BLACK); self.canvas.fill_rect(container.area()).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(), container.start, ); return; } // show repl draw_text( &mut self.canvas, self.ttf_context, &self.command_box.text[..], WHITE, container.start, ); 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, container.start.1 + 2, 2, container.height - 4 ); self.canvas.fill_rect(cursor).unwrap(); } fn draw_brush(&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(); } } } if let Brush::Line(LineBrush { start, size, .. }) = self.brush { 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()); let angle = positive_angle_with_x(from, to.into()); draw_text( &mut self.canvas, self.ttf_context, format!( "{:.width$}°", angle, width = if (angle - ANGLE).abs() < 1e-3 { 3 } else { 0 } ), PINK, (self.mouse.0 + size as i32, self.mouse.1 + size as i32), ); 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_line_to_grid(&mut self, line: u32, axis: Axis, color: Color) { let (winsize_x, winsize_y) = self.canvas.window().size(); let cs = self.zoom as u32; let offset = (line * cs) + (cs / 2); self.canvas.set_draw_color(color); match axis { Axis::X => { let line_coord = offset as i32 + self.start.y(); self.canvas .draw_line((0, line_coord), (winsize_x as i32, line_coord)) .unwrap(); } Axis::Y => { let line_coord = offset as i32 + self.start.x(); self.canvas .draw_line((line_coord, 0), (line_coord, winsize_y as i32)) .unwrap(); } } } fn draw_symmetry(&mut self) { let Symmetry { x, y } = self.symmetry; if let Some(line) = x { self.draw_line_to_grid(line, Axis::X, CYAN) } if let Some(line) = y { self.draw_line_to_grid(line, Axis::Y, CYAN) } } fn draw_guides(&mut self) { // do this without clone for (Guide { offset, axis }, enabled) in self.guides.clone() { if enabled { self.draw_line_to_grid(offset, axis, PINK); } } } 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; self.canvas.set_draw_color(WHITE); for (line_nr, scan) in self.pixmap.data[..].chunks(width as usize).enumerate() { let mut pass = 0usize; for (color, length) in utils::compress(scan) { if color { self.canvas .fill_rect(rect!( (pass as u32 * cs) as i32 + start.x(), (line_nr as u32 * cs) as i32 + start.y(), cs * length as u32, cs )) .unwrap(); } pass += length; } } self.canvas.set_draw_color(GRID_COLOR); self.canvas .draw_rect(rect!( start.x(), start.y(), self.width() * cs, self.height() * cs )) .unwrap(); if grid_enabled { self.grid .draw(&mut self.canvas, self.zoom, &self.start, width, height); } self.draw_guides(); self.draw_symmetry(); self.draw_statusline(); self.draw_command_box(); self.draw_brush(); } 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_else(|| 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, cache: RefCell::new(None), guides: HashMap::new(), 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[..]).map_err(AppError::File)?; 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); } Keycode::F => self.cycle_brush(), // bucket fill tool 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(); } _ if keymod == Mod::LCTRLMOD || keymod == Mod::RCTRLMOD => { self.brush = Brush::line( self.cache .borrow() .as_ref() .map(|c| c.last_brush_size) .unwrap_or(0), true, ); } _ => (), } } Event::KeyUp { keycode: Some(k), .. } if k == Keycode::LCtrl || k == Keycode::RCtrl => { self.brush = Brush::new( self.cache .borrow() .as_ref() .map(|c| c.last_brush_size) .unwrap_or(0), ); } // 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 let Some(s) = start { if let Ok(o) = self.paint_line(s, pt, val, size) { self.current_operation.extend(o); self.brush = Brush::Line(LineBrush { size, start: if extend { contact } else { None }, extend, }); } } else { self.brush = Brush::Line(LineBrush { size, start: contact, extend, }); } } Brush::Fill => { if let Some(c) = contact { // this `get` is unchecked because contact is checked // to be within pixmap let target = self.pixmap.get(c); let replacement = self.active_color; let operation = self.pixmap.flood_fill(c, target, replacement); for o in operation.iter() { // this `set` is unchecked because the returned // value of flood_fill is checked to be within pixmap self.pixmap.set(*o, replacement); } self.current_operation.extend( operation .into_iter() .map(|point| PaintRecord { point, old: target, new: replacement, }) .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()); } } match event { Event::KeyDown { keycode: Some(k), keymod, .. } => 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(), Keycode::Return => self.eval_command(), Keycode::Escape => { self.command_box.clear(); self.message.text.clear(); self.mode = Mode::Draw; } _ 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(), _ => (), }, // how does one handle alt keys // _ if keymod == Mod::LALTMOD => match k { // Keycode::B => self.command_box.cursor_back_word(), // Keycode::F => self.command_box.cursor_forward_word(), // _ => (), // }, _ => (), }, Event::TextInput { text, .. } => { self.command_box.push_str(&text[..]); } _ => (), } } } } self.cache(); self.redraw(); } } }