use crate::{ bitmap::{MapPoint, Pixmap}, command::CommandBox, consts::{colors::*, FONT_PATH}, dither, lisp::{eval, lex::Lexer, parse::Parser, Environment}, message::Message, rect, symmetry::Symmetry, undo::{ModifyRecord, OpKind, Operation, UndoStack}, utils::{draw_text, is_copy_event, is_paste_event}, }; use std::{convert::From, fs::File, io::prelude::*}; 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_size: u8, pub canvas: Canvas, pub command_box: CommandBox, pub context: &'ctx Sdl, pub current_operation: Operation, pub dither_level: u8, pub grid: Grid, pub last_point: Option, pub lisp_env: Environment, 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, ) -> Result, ()> { let radius = self.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(ModifyRecord::new(point, old_val, val)); } Ok(modify_record) } pub fn paint_line>( &mut self, start: MapPoint, end: P, val: bool, ) -> 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, self.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(ModifyRecord::new(c, old_val, val)); } } Ok(line_modify_record) } pub fn apply_operation(&mut self, op: Operation, op_kind: OpKind) { for ModifyRecord { point, old_val, val, } in op.into_iter() { if self.pixmap.is_inside(point) { match op_kind { OpKind::Undo => self.pixmap.set(point, old_val), OpKind::Redo => self.pixmap.set(point, val), }; } } } pub fn commit_operation(&mut self) { if !self.current_operation.is_empty() { let op = self .current_operation .drain(..) .filter(|v| !v.old_val == v.val) .collect::>(); self.undo_stack.push(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 increase_brush_size(&mut self) { self.brush_size += 1; } pub fn decrease_brush_size(&mut self) { if self.brush_size > 0 { self.brush_size -= 1; } } 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 eval::eval(&res.unwrap(), self, None) { Ok(val) => { self.message.text = format!("{}", val); } Err(_) => { self.message.text = format!("Lisp Error!"); } } if let Some(path) = self.command_box.text.strip_prefix("(save ") { let image = self.export(); let encoded = image.encode().unwrap(); let mut buffer = File::create(path).unwrap(); buffer.write_all(&encoded[..]).unwrap(); 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 {}][BRUSH {}][SYM {}][PT {}][ACTIVE {}]", self.dither_level, self.brush_size + 1, self.symmetry, mouse_coords, if self.active_color { "WHT" } else { "BLK" } ); 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[..], WHITE, (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 brush_size = self.brush_size; let cs = self.zoom as u32; let pt = self.idx_at_coord(self.mouse); if let Some(center) = pt { let circle = self.pixmap.get_circle(center, brush_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(); } } } 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; let (winsize_x, winsize_y) = self.canvas.window().size(); let Symmetry { x: sym_x, y: sym_y } = self.symmetry; 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(); } 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(); } 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 }); } } // publicly available functions on appstate impl<'ctx> AppState<'ctx> { pub fn init( width: u32, height: u32, context: &'ctx Sdl, ttf_context: &'ctx Sdl2TtfContext, start_data: Option>, ) -> Self { let video_subsystem = context.video().unwrap(); let window = video_subsystem .window("Pixel editor", 200, 200) .position_centered() .resizable() .opengl() .build() .map_err(|e| e.to_string()) .unwrap(); let canvas = window .into_canvas() .build() .map_err(|e| e.to_string()) .unwrap(); let data = start_data.unwrap_or(vec![false; (width * height) as usize]); let pixmap = Pixmap::new_with(width, height, data); Self { active_color: true, brush_size: 0, canvas, context, command_box: CommandBox::new(), current_operation: Vec::new(), dither_level: 16, grid: Grid::new(), last_point: None, mode: Mode::Draw, message: Message::new().text(" "), mouse: (0, 0), pixmap, start: Point::new(60, 60), symmetry: Default::default(), ttf_context, undo_stack: UndoStack::new(), lisp_env: eval::with_prelude(), zoom: 5, } } pub fn export(&self) -> Image { let mut image = Image::new(self.width(), self.height()); image.data = self.pixmap.data.clone(); image } 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.decrease_brush_size(), Keycode::E => self.increase_brush_size(), 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(), // line drawing Keycode::F => { let end: Point = (mouse.x(), mouse.y()).into(); if let Some(start) = self.last_point { if let Ok(o) = self.paint_line(start, end, self.active_color) { self.commit_operation(); self.current_operation = o .into_iter() .filter(|v| !v.old_val == v.val) .collect(); self.commit_operation(); self.last_point = Some(self.idx_at_coord(end).unwrap().into()); } } } Keycode::V => self.cycle_symmetry(), // undo & redo Keycode::U => { if let Some(op) = self.undo_stack.undo() { self.apply_operation(op, OpKind::Undo); } } // 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(); } Keycode::R => { if let Some(op) = self.undo_stack.redo() { self.apply_operation(op, OpKind::Redo); } } _ => (), } } // start of operation Event::MouseButtonDown { x, y, mouse_btn, .. } => { let pt = (x, y); self.last_point = self.idx_at_coord(pt).map(MapPoint::from); let val = match mouse_btn { MouseButton::Right => !self.active_color, _ => self.active_color, }; if let Ok(o) = self.paint_point(pt, val) { self.current_operation.extend(o); } } // click and drag Event::MouseMotion { x, y, mousestate, .. } => { 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) { 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) { self.current_operation.extend(o); } } } // end of operation Event::MouseButtonUp { .. } => { let op = self .current_operation .drain(..) .filter(|v| !v.old_val == v.val) .collect::>(); self.undo_stack.push(op); } 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(); } } }