use crate::{ bitmap::{MapPoint, Pixmap}, consts::{colors::*, FONT_PATH}, symmetry::Symmetry, 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, ttf::Sdl2TtfContext, video::Window, Sdl, }; macro_rules! quick_rect( ($x:expr, $y:expr, $w:expr, $h:expr) => ( Rect::new($x as i32, $y as i32, $w as u32, $h as u32) ) ); pub struct AppState<'ctx> { active_color: bool, brush_size: u8, canvas: Canvas, context: &'ctx Sdl, ttf_context: &'ctx Sdl2TtfContext, mouse: (i32, i32), 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, } 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 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 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) { 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, val: bool, ) -> 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.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 { 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_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(quick_rect!( 0, winsize_y - status_height, status_width, status_height )) .unwrap(); let mouse_coords = if let Some((x, y)) = self.idx_at_coord(self.mouse) { format!("{:3}, {:3}", x, y) } else { format!("---, ---") }; let status_text = format!( "[BRUSH {}] [SYM {}] {}", self.brush_size + 1, self.symmetry, mouse_coords ); draw_text( &mut self.canvas, self.ttf_context, status_text, BLACK, (0, winsize_y - status_height), ); } 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(GREY); 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_mouse(); } 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, ttf_context: &'ctx Sdl2TtfContext, ) -> 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 pixmap = Pixmap::new_with(width, height, false); Self { active_color: true, brush_size: 0, canvas, context, ttf_context, mouse: (0, 0), 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(); self.mouse = (mouse.x(), mouse.y()); 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.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(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(); } } } fn draw_text>( canvas: &mut Canvas, ttf_context: &Sdl2TtfContext, text: S, color: Color, (x, y): (u32, u32), ) { let text = text.as_ref(); let texture_creator = canvas.texture_creator(); let mut font = ttf_context.load_font(FONT_PATH, 17).unwrap(); font.set_style(sdl2::ttf::FontStyle::NORMAL); font.set_hinting(sdl2::ttf::Hinting::Mono); let surface = font.render(text.as_ref()).blended(color).unwrap(); let texture = texture_creator .create_texture_from_surface(&surface) .unwrap(); let (width, height) = font.size_of_latin1(text.as_bytes()).unwrap(); let area = quick_rect!(x, y, width, height); canvas.copy(&texture, None, area).unwrap(); }