use crate::{ bitmap::{MapPoint, Pixmap}, undo::{ModifyRecord, OpKind, Operation, UndoStack}, }; use std::convert::{From, TryFrom}; 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> { start: Point, pixmap: Pixmap, zoom: u8, brush_size: u8, grid: Grid, context: &'ctx Sdl, canvas: Canvas, last_point: Option, active_color: bool, undo_stack: UndoStack, current_operation: Operation, } struct Grid { enabled: bool, 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(); } 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 paint_point>( &mut self, center: P, val: bool, ) -> Result, ()> { let radius = self.brush_size; if radius == 1 { let center_point: Point = center.into(); return Ok(if let Some(pt) = self.idx_at_coord(center_point) { let old_val = self.pixmap.set(pt, val); Ok(ModifyRecord::new(pt, old_val, val)) } else { Err(()) } .map(|x| vec![x])?); } else { if let Some(center_on_grid) = self.idx_at_coord(center) { // center_on_grid is now a coordinate on the drawing grid let (x0, y0) = (center_on_grid.0 as i64, center_on_grid.1 as i64); let (mut dx, mut dy, mut err) = (radius as i64, 0i64, 1 - radius as i64); let mut circle = vec![]; let mut old_vals = vec![]; while dx >= dy { circle.push((x0 + dx, y0 + dy)); circle.push((x0 - dx, y0 + dy)); circle.push((x0 + dx, y0 - dy)); circle.push((x0 - dx, y0 - dy)); circle.push((x0 + dy, y0 + dx)); circle.push((x0 - dy, y0 + dx)); circle.push((x0 + dy, y0 - dx)); circle.push((x0 - dy, y0 - dx)); dy = dy + 1; if err < 0 { err = err + 2 * dy + 1; } else { dx -= 1; err += 2 * (dy - dx) + 1; } } // circle's insides for x in 0..radius as i64 { for y in 0..radius as i64 { if x.pow(2) + y.pow(2) < (radius as i64).pow(2) { circle.push((x0 + x, y0 + y)); circle.push((x0 - x, y0 + y)); circle.push((x0 + x, y0 - y)); circle.push((x0 - x, y0 - y)); } } } for circumference_pt in circle { if let Ok(mp) = MapPoint::try_from(circumference_pt) { let old_val = self.pixmap.set(mp, val); old_vals.push(ModifyRecord::new((mp.x, mp.y), old_val, val)); } } return Ok(old_vals); } } return Err(()); } 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 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 descrease_brush_size(&mut self) { if self.brush_size > 1 { 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 modify(&mut self, func: F) where F: FnOnce(&mut Self), { func(self); self.canvas.set_draw_color(Color::RGB(0, 0, 0)); self.canvas.clear(); self.canvas.set_draw_color(Color::RGB(64, 64, 64)); 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 { start: Point::new(60, 60), zoom: 5, brush_size: 1, pixmap, grid: Grid::new(), canvas, context, last_point: None, active_color: true, undo_stack: UndoStack::new(), current_operation: Vec::new(), } } 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.modify(|e| e.pan((0, 10))), Keycode::A => self.modify(|e| e.pan((10, 0))), Keycode::S => self.modify(|e| e.pan((0, -10))), Keycode::D => self.modify(|e| e.pan((-10, 0))), // zoom Keycode::C => { let cursor = (mouse.x(), mouse.y()); self.modify(|e| e.zoom_in(cursor)); } Keycode::Z => { let cursor = (mouse.x(), mouse.y()); self.modify(|e| e.zoom_out(cursor)); } // brush ops Keycode::Q => self.modify(|e| e.descrease_brush_size()), Keycode::E => self.modify(|e| e.increase_brush_size()), // flip color Keycode::X => self.modify(|e| e.change_active_color()), // toggle grid Keycode::Tab => self.modify(|e| e.toggle_grid()), // exit Keycode::Escape => break 'running, // undo & redo Keycode::U => self.modify(|e| { if let Some(op) = e.undo_stack.undo() { e.apply_operation(op, OpKind::Undo); } }), Keycode::R => self.modify(|e| { if let Some(op) = e.undo_stack.redo() { e.apply_operation(op, OpKind::Redo); } }), _ => (), } } // start of operation Event::MouseButtonDown { x, y, mouse_btn, .. } => { self.modify(|e| { let pt = (x, y); e.last_point = Some(pt.into()); let val = match mouse_btn { MouseButton::Right => !e.active_color, _ => e.active_color, }; if let Ok(o) = e.paint_point(pt, val) { e.current_operation.extend(o); } }); } // click and drag Event::MouseMotion { x, y, mousestate, .. } => { let is_left = mousestate.is_mouse_button_pressed(MouseButton::Left); let is_right = mousestate.is_mouse_button_pressed(MouseButton::Right); if is_left { self.modify(|e| { let pt = (x, y); let val = e.active_color; if let Ok(o) = e.paint_point(pt, val) { e.current_operation.extend(o); } }); } else if is_right { self.modify(|e| { let pt = (x, y); let val = !e.active_color; if let Ok(o) = e.paint_point(pt, val) { e.current_operation.extend(o); } }); } } // end of operation Event::MouseButtonUp { .. } => self.modify(|e| { let op = e .current_operation .drain(..) .filter(|v| !v.old_val == v.val) .collect::>(); e.undo_stack.push(op); }), Event::Quit { .. } => { break 'running; } _ => { self.modify(|_| ()); } } } } } }