From 9f0dc65dabff2a1e443199ed68292398dcb390b1 Mon Sep 17 00:00:00 2001 From: Akshay Date: Fri, 12 Mar 2021 20:24:55 +0530 Subject: init --- src/app.rs | 447 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/consts.rs | 6 + src/main.rs | 10 ++ src/undo.rs | 133 +++++++++++++++++ 4 files changed, 596 insertions(+) create mode 100644 src/app.rs create mode 100644 src/consts.rs create mode 100644 src/main.rs create mode 100644 src/undo.rs (limited to 'src') diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..84d884f --- /dev/null +++ b/src/app.rs @@ -0,0 +1,447 @@ +use crate::undo::{ModifyRecord, OpKind, Operation, UndoStack}; + +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, + width: u32, + height: u32, + data: Vec, + 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 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 within_grid>(&self, p: P) -> bool { + let p = p.into(); + let (x, y) = (p.x(), p.y()); + x >= 0 && x < self.width as i32 && y >= 0 && y < self.height as i32 + } + + fn set_at>(&mut self, p: P, val: bool) -> Result { + let p: Point = p.into(); + if let Some((x, y)) = self.idx_at_coord(p) { + let old_val = self.data[(y * self.width + x) as usize]; + self.data[(y * self.width + x) as usize] = val; + return Ok(ModifyRecord::new((x as i32, y as i32), old_val, val)); + } + return Err(()); + } + + fn set_with_absolute>(&mut self, p: P, val: bool) -> Result { + let p: Point = p.into(); + let (x, y) = (p.x(), p.y()); + if self.within_grid(p) { + let idx = y as u32 * self.width + x as u32; + let old_val = self.data[idx as usize]; + self.data[idx as usize] = val; + return Ok(ModifyRecord::new((x as i32, y as i32), old_val, val)); + } + return Err(()); + } + + 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 { + return Ok(self.set_at(center, val).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)); + } + } + } + dbg!(&circle); + for (x, y) in circle { + if self.within_grid((x as i32, y as i32)) { + let idx = y as u32 * self.width + x as u32; + old_vals.push(ModifyRecord::new( + (x as i32, y as i32), + self.data[idx as usize], + val, + )); + self.data[idx as usize] = val; + } + } + return Ok(old_vals); + } + } + return Err(()); + } + + fn draw_line>(&mut self, to: P) { + let to = to.into(); + let from = self.last_point.unwrap_or(to.into()); + } + + fn apply_operation(&mut self, op: Operation, op_kind: OpKind) -> Result<(), ()> { + for ModifyRecord { + point, + old_val, + val, + } in op.into_iter() + { + self.set_with_absolute( + point, + match op_kind { + OpKind::Undo => old_val, + OpKind::Redo => val, + }, + )?; + } + Ok(()) + } + + 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 canvas = &mut self.canvas; + canvas.set_draw_color(self.grid.color); + for i in 0..=self.width { + let x = (i * cs) as i32; + let y = (self.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..=self.height { + let x = (self.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; + if self.grid.enabled { + self.draw_grid(); + } + let canvas = &mut self.canvas; + for (idx, val) in self.data.iter().enumerate() { + if *val { + let idx = idx as i32; + let (x, y) = (idx % self.width as i32, idx / self.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 + self.start.x() + 1, + y * cs as i32 + self.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 data = vec![false; (width * height) as usize]; + Self { + start: Point::new(60, 60), + width, + height, + data, + zoom: 5, + brush_size: 1, + 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| { + dbg!(&e.current_operation.len()); + let op = e + .current_operation + .drain(..) + .filter(|v| !v.old_val == v.val) + .collect::>(); + e.undo_stack.push(op); + dbg!(&e.undo_stack); + }), + Event::Quit { .. } => { + break 'running; + } + _ => { + self.modify(|_| ()); + } + } + } + } + } +} diff --git a/src/consts.rs b/src/consts.rs new file mode 100644 index 0000000..b5cea43 --- /dev/null +++ b/src/consts.rs @@ -0,0 +1,6 @@ +use sdl2::pixels::Color; + +pub const GRID_COLOR: Color = Color::RGB(64, 64, 64); +pub const WHITE: Color = Color::RGB(255, 255, 255); +pub const BLACK: Color = Color::RGB(0, 0, 0); + diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..bf89508 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,10 @@ +mod app; +mod consts; +mod undo; + +use app::AppState; + +pub fn main() { + let sdl_context = sdl2::init().unwrap(); + AppState::init(100, 100, &sdl_context).run(); +} diff --git a/src/undo.rs b/src/undo.rs new file mode 100644 index 0000000..2249fe7 --- /dev/null +++ b/src/undo.rs @@ -0,0 +1,133 @@ +#[derive(Copy, Clone, Debug)] +pub struct ModifyRecord { + pub point: (i32, i32), + pub old_val: bool, + pub val: bool, +} + +impl ModifyRecord { + pub fn new(point: (i32, i32), old_val: bool, val: bool) -> Self { + ModifyRecord { + point, + old_val, + val, + } + } +} + +pub enum OpKind { + Undo, + Redo, +} + +pub type Operation = Vec; + +#[derive(Debug)] +pub struct UndoStack { + operations: Vec, + position: Option, +} + +impl UndoStack +where + T: Clone, +{ + pub fn new() -> Self { + Self { + operations: Vec::with_capacity(64), + position: None, + } + } + + pub fn push(&mut self, op: T) { + if let Some(p) = self.position { + // remove all operations past the newly pushed operation + for _ in 1 + (p as usize)..self.operations.len() { + self.operations.pop(); + } + // advance position + self.position = Some(p + 1); + // add new operation + self.operations.push(op); + } else { + // empty ops list or undone till start of stack + // remove all operations past the newly pushed operation + self.operations.clear(); + // advance position + self.position = Some(0); + // add new operation + self.operations.push(op); + } + } + + pub fn undo(&mut self) -> Option { + if let Some(p) = self.position { + self.position = p.checked_sub(1); + // we want to return a clone and not a reference because push deletes the item + return Some(self.operations[p as usize].clone()); + } + return None; + } + + pub fn redo(&mut self) -> Option { + if let Some(p) = self.position { + if p < self.operations.len() as u32 - 1 { + self.position = Some(p + 1); + return Some(self.operations[1 + p as usize].clone()); + } + } else if !self.operations.is_empty() { + self.position = Some(0); + return Some(self.operations[0].clone()); + } + return None; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn setup() -> UndoStack { + let mut stack = UndoStack::new(); + stack.push(10); + stack.push(5); + stack.push(2); + stack + } + + #[test] + fn undo_works() { + let mut stack = setup(); + assert_eq!(stack.undo(), Some(2)); + assert_eq!(stack.undo(), Some(5)); + assert_eq!(stack.undo(), Some(10)); + } + #[test] + fn redo_works() { + let mut stack = setup(); + stack.undo(); + stack.undo(); + stack.undo(); + assert_eq!(stack.redo(), Some(10)); + assert_eq!(stack.redo(), Some(5)); + assert_eq!(stack.redo(), Some(2)); + } + + #[test] + fn undo_push_redo() { + let mut stack = setup(); + stack.undo(); + stack.push(16); + assert_eq!(stack.redo(), None); + assert_eq!(stack.undo(), Some(16)); + } + + #[test] + fn stack_identity() { + let mut stack = setup(); + stack.undo(); + stack.redo(); + stack.undo(); + assert_eq!(stack.operations, setup().operations); + } +} -- cgit v1.2.3