use crate::{ bitmap::{Axis, MapPoint, Pixmap}, undo::{ModifyRecord, OpKind, Operation, UndoStack}, }; use std::convert::From; use sdl2::{ event::Event, keyboard::Keycode, mouse::MouseButton, pixels::Color, rect::{Point, Rect}, render::Canvas, video::Window, Sdl, }; use crate::consts::{BLACK, GRID_COLOR, WHITE}; pub struct AppState<'ctx> { active_color: bool, brush_size: u8, canvas: Canvas, context: &'ctx Sdl, current_operation: Operation, grid: Grid, last_point: Option, pixmap: Pixmap, start: Point, symmetry: Symmetry, undo_stack: UndoStack, zoom: u8, } struct Grid { enabled: bool, color: Color, } #[derive(Debug, Default, Copy, Clone)] struct Symmetry { x: Option, y: Option, } 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(); } fn width(&self) -> u32 { self.pixmap.width } 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), ); } fn change_active_color(&mut self) { self.active_color = !self.active_color; } 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 } } 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() } fn toggle_grid(&mut self) { self.grid.enabled = !self.grid.enabled; } 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 }, } } fn apply_symmetry(&self, figure: &[MapPoint]) -> Vec { let Symmetry { x, y } = self.symmetry; match (x, y) { (None, None) => vec![], (Some(line), None) => self.pixmap.mirror_figure(figure, line, Axis::X), (None, Some(line)) => self.pixmap.mirror_figure(figure, line, Axis::Y), (Some(x), Some(y)) => { let along_x = self.pixmap.mirror_figure(figure, x, Axis::X); let along_y = self.pixmap.mirror_figure(figure, y, Axis::Y); let reflected = self.pixmap.reflect_figure(figure, (x, y).into()); along_x .into_iter() .chain(along_y) .chain(reflected) .collect() } } } 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 mut modify_record = vec![]; let circle = self.pixmap.get_circle(center, radius); let sym_circle = self.apply_symmetry(&circle); for point in circle.into_iter().chain(sym_circle) { let old_val = self.pixmap.set(point, val); modify_record.push(ModifyRecord::new(point, old_val, val)); } Ok(modify_record) } fn paint_line>(&mut self, start: P, end: P) -> Result, ()> { let start = self.idx_at_coord(start).ok_or(())?; let end = self.idx_at_coord(end).ok_or(())?; let line = self.pixmap.get_line(start, end); let sym_line = self.apply_symmetry(&line); let mut line_modify_record = vec![]; let val = self.active_color; for point in line.into_iter().chain(sym_line) { let circle_around_point = self.pixmap.get_circle(point, self.brush_size as u32); for c in circle_around_point { let old_val = self.pixmap.set(c, val); line_modify_record.push(ModifyRecord::new(c, old_val, val)); } } Ok(line_modify_record) } 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), }; } } } 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); } } 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; } fn increase_brush_size(&mut self) { self.brush_size += 1; } fn decrease_brush_size(&mut self) { if self.brush_size > 0 { self.brush_size -= 1; } } 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; } } 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(&mut self) { let cs = self.zoom as u32; let (width, height) = (self.width(), self.height()); let start = self.start; if self.grid.enabled { self.draw_grid(); } let canvas = &mut self.canvas; 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); canvas.set_draw_color(WHITE); canvas .fill_rect(Rect::new( // start drawing 1 pixel after the grid line x * cs as i32 + start.x() + 1, y * cs as i32 + start.y() + 1, // stop drawing 1 pixel before the grid line cs - 1, cs - 1, )) .unwrap(); } } } fn redraw(&mut self) { self.canvas.set_draw_color(BLACK); self.canvas.clear(); self.draw(); self.canvas.present(); } } // publicly available functions on appstate impl<'ctx> AppState<'ctx> { pub fn init(width: u32, height: u32, context: &'ctx Sdl) -> Self { let video_subsystem = context.video().unwrap(); let window = video_subsystem .window("Pixel editor", 200, 200) .position_centered() .opengl() .build() .map_err(|e| e.to_string()) .unwrap(); let canvas = window .into_canvas() .build() .map_err(|e| e.to_string()) .unwrap(); let pixmap = Pixmap::new_with(width, height, false); Self { active_color: true, brush_size: 0, canvas, context, current_operation: Vec::new(), grid: Grid::new(), last_point: None, pixmap, start: Point::new(60, 60), symmetry: Default::default(), undo_stack: UndoStack::new(), zoom: 5, } } 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(); for event in event_pump.poll_iter() { match event { Event::KeyDown { keycode: Some(k), .. } => { 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 => { 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(), // flip color Keycode::X => self.change_active_color(), // toggle grid Keycode::Tab => self.toggle_grid(), // line drawing Keycode::F => { let end = (mouse.x(), mouse.y()).into(); if let Some(start) = self.last_point { if let Ok(o) = self.paint_line(start, end) { self.commit_operation(); self.current_operation = o.into_iter().filter(|v| !v.old_val == v.val).collect(); self.commit_operation(); self.last_point = Some(end); } } } Keycode::V => self.cycle_symmetry(), // exit Keycode::Escape => break 'running, // 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); } } _ => (), } } // start of operation Event::MouseButtonDown { x, y, mouse_btn, .. } => { let pt = (x, y); self.last_point = Some(pt.into()); 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; } _ => {} } } self.redraw(); } } }