aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/app.rs447
-rw-r--r--src/consts.rs6
-rw-r--r--src/main.rs10
-rw-r--r--src/undo.rs133
4 files changed, 596 insertions, 0 deletions
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 @@
1use crate::undo::{ModifyRecord, OpKind, Operation, UndoStack};
2
3use sdl2::{
4 event::Event,
5 keyboard::Keycode,
6 mouse::MouseButton,
7 pixels::Color,
8 rect::{Point, Rect},
9 render::Canvas,
10 video::Window,
11 Sdl,
12};
13
14use crate::consts::{BLACK, GRID_COLOR, WHITE};
15
16pub struct AppState<'ctx> {
17 start: Point,
18 width: u32,
19 height: u32,
20 data: Vec<bool>,
21 zoom: u8,
22 brush_size: u8,
23 grid: Grid,
24 context: &'ctx Sdl,
25 canvas: Canvas<Window>,
26 last_point: Option<Point>,
27 active_color: bool,
28 undo_stack: UndoStack<Operation>,
29 current_operation: Operation,
30}
31
32struct Grid {
33 enabled: bool,
34 color: Color,
35}
36
37impl Grid {
38 fn new() -> Self {
39 Self {
40 enabled: true,
41 color: GRID_COLOR,
42 }
43 }
44}
45
46// private actions on appstate
47impl<'ctx> AppState<'ctx> {
48 fn pan<P: Into<Point>>(&mut self, direction: P) {
49 self.start += direction.into();
50 }
51
52 fn bounds(&self) -> (Point, Point) {
53 let x_min = self.start.x();
54 let y_min = self.start.y();
55 let x_max = self.start.x() + (self.width * self.zoom as u32) as i32;
56 let y_max = self.start.y() + (self.height * self.zoom as u32) as i32;
57 return (
58 Point::new(x_min, y_min),
59 Point::new(x_max as i32, y_max as i32),
60 );
61 }
62
63 fn change_active_color(&mut self) {
64 self.active_color = !self.active_color;
65 }
66
67 fn idx_at_coord<P: Into<Point>>(&self, p: P) -> Option<(u32, u32)> {
68 let p: Point = p.into();
69 if self.within_canvas(p) {
70 // convert p relative to start of drawing area
71 let rel_p = p - self.start;
72 // reduce p based on zoom and cell size
73 let (sx, sy) = (rel_p.x() / self.zoom as i32, rel_p.y() / self.zoom as i32);
74 return Some((sx as u32, sy as u32));
75 } else {
76 None
77 }
78 }
79
80 fn within_canvas<P: Into<Point>>(&self, p: P) -> bool {
81 let p: Point = p.into();
82 let (mini, maxi) = self.bounds();
83 p.x() < maxi.x() && p.y() < maxi.y() && p.x() >= mini.x() && p.y() >= mini.y()
84 }
85
86 fn within_grid<P: Into<Point>>(&self, p: P) -> bool {
87 let p = p.into();
88 let (x, y) = (p.x(), p.y());
89 x >= 0 && x < self.width as i32 && y >= 0 && y < self.height as i32
90 }
91
92 fn set_at<P: Into<Point>>(&mut self, p: P, val: bool) -> Result<ModifyRecord, ()> {
93 let p: Point = p.into();
94 if let Some((x, y)) = self.idx_at_coord(p) {
95 let old_val = self.data[(y * self.width + x) as usize];
96 self.data[(y * self.width + x) as usize] = val;
97 return Ok(ModifyRecord::new((x as i32, y as i32), old_val, val));
98 }
99 return Err(());
100 }
101
102 fn set_with_absolute<P: Into<Point>>(&mut self, p: P, val: bool) -> Result<ModifyRecord, ()> {
103 let p: Point = p.into();
104 let (x, y) = (p.x(), p.y());
105 if self.within_grid(p) {
106 let idx = y as u32 * self.width + x as u32;
107 let old_val = self.data[idx as usize];
108 self.data[idx as usize] = val;
109 return Ok(ModifyRecord::new((x as i32, y as i32), old_val, val));
110 }
111 return Err(());
112 }
113
114 fn toggle_grid(&mut self) {
115 self.grid.enabled = !self.grid.enabled;
116 }
117
118 fn paint_point<P: Into<Point>>(
119 &mut self,
120 center: P,
121 val: bool,
122 ) -> Result<Vec<ModifyRecord>, ()> {
123 let radius = self.brush_size;
124 if radius == 1 {
125 return Ok(self.set_at(center, val).map(|x| vec![x])?);
126 } else {
127 if let Some(center_on_grid) = self.idx_at_coord(center) {
128 // center_on_grid is now a coordinate on the drawing grid
129 let (x0, y0) = (center_on_grid.0 as i64, center_on_grid.1 as i64);
130 let (mut dx, mut dy, mut err) = (radius as i64, 0i64, 1 - radius as i64);
131 let mut circle = vec![];
132 let mut old_vals = vec![];
133 while dx >= dy {
134 circle.push((x0 + dx, y0 + dy));
135 circle.push((x0 - dx, y0 + dy));
136 circle.push((x0 + dx, y0 - dy));
137 circle.push((x0 - dx, y0 - dy));
138 circle.push((x0 + dy, y0 + dx));
139 circle.push((x0 - dy, y0 + dx));
140 circle.push((x0 + dy, y0 - dx));
141 circle.push((x0 - dy, y0 - dx));
142 dy = dy + 1;
143 if err < 0 {
144 err = err + 2 * dy + 1;
145 } else {
146 dx -= 1;
147 err += 2 * (dy - dx) + 1;
148 }
149 }
150 // circle's insides
151 for x in 0..radius as i64 {
152 for y in 0..radius as i64 {
153 if x.pow(2) + y.pow(2) < (radius as i64).pow(2) {
154 circle.push((x0 + x, y0 + y));
155 circle.push((x0 - x, y0 + y));
156 circle.push((x0 + x, y0 - y));
157 circle.push((x0 - x, y0 - y));
158 }
159 }
160 }
161 dbg!(&circle);
162 for (x, y) in circle {
163 if self.within_grid((x as i32, y as i32)) {
164 let idx = y as u32 * self.width + x as u32;
165 old_vals.push(ModifyRecord::new(
166 (x as i32, y as i32),
167 self.data[idx as usize],
168 val,
169 ));
170 self.data[idx as usize] = val;
171 }
172 }
173 return Ok(old_vals);
174 }
175 }
176 return Err(());
177 }
178
179 fn draw_line<P: Into<Point>>(&mut self, to: P) {
180 let to = to.into();
181 let from = self.last_point.unwrap_or(to.into());
182 }
183
184 fn apply_operation(&mut self, op: Operation, op_kind: OpKind) -> Result<(), ()> {
185 for ModifyRecord {
186 point,
187 old_val,
188 val,
189 } in op.into_iter()
190 {
191 self.set_with_absolute(
192 point,
193 match op_kind {
194 OpKind::Undo => old_val,
195 OpKind::Redo => val,
196 },
197 )?;
198 }
199 Ok(())
200 }
201
202 fn zoom_in(&mut self, p: (i32, i32)) {
203 // attempt to center around cursor
204 if let Some(p) = self.idx_at_coord(p) {
205 let (x1, y1) = (p.0 * (self.zoom as u32), p.1 * (self.zoom as u32));
206 let (x2, y2) = (p.0 * (1 + self.zoom as u32), p.1 * (1 + self.zoom as u32));
207 let diffx = x2 as i32 - x1 as i32;
208 let diffy = y2 as i32 - y1 as i32;
209 self.start = self.start - Point::from((diffx, diffy));
210 }
211 self.zoom += 1;
212 }
213
214 fn increase_brush_size(&mut self) {
215 self.brush_size += 1;
216 }
217
218 fn descrease_brush_size(&mut self) {
219 if self.brush_size > 1 {
220 self.brush_size -= 1;
221 }
222 }
223
224 fn zoom_out(&mut self, p: (i32, i32)) {
225 if self.zoom > 1 {
226 // attempt to center around cursor
227 if let Some(p) = self.idx_at_coord(p) {
228 let (x1, y1) = (p.0 * (self.zoom as u32), p.1 * (self.zoom as u32));
229 let (x2, y2) = (p.0 * (self.zoom as u32 - 1), p.1 * (self.zoom as u32 - 1));
230 let diffx = x2 as i32 - x1 as i32;
231 let diffy = y2 as i32 - y1 as i32;
232 self.start = self.start - Point::from((diffx, diffy));
233 }
234 self.zoom -= 1;
235 }
236 }
237
238 fn draw_grid(&mut self) {
239 let cs = self.zoom as u32;
240 let canvas = &mut self.canvas;
241 canvas.set_draw_color(self.grid.color);
242 for i in 0..=self.width {
243 let x = (i * cs) as i32;
244 let y = (self.height * cs) as i32;
245 let start = self.start + Point::new(x, 0);
246 let end = self.start + Point::new(x, y);
247 canvas.draw_line(start, end).unwrap();
248 }
249 for j in 0..=self.height {
250 let x = (self.width * cs) as i32;
251 let y = (j * cs) as i32;
252 let start = self.start + Point::new(0, y);
253 let end = self.start + Point::new(x, y);
254 canvas.draw_line(start, end).unwrap();
255 }
256 }
257
258 fn draw(&mut self) {
259 let cs = self.zoom as u32;
260 if self.grid.enabled {
261 self.draw_grid();
262 }
263 let canvas = &mut self.canvas;
264 for (idx, val) in self.data.iter().enumerate() {
265 if *val {
266 let idx = idx as i32;
267 let (x, y) = (idx % self.width as i32, idx / self.height as i32);
268 canvas.set_draw_color(WHITE);
269 canvas
270 .fill_rect(Rect::new(
271 // start drawing 1 pixel after the grid line
272 x * cs as i32 + self.start.x() + 1,
273 y * cs as i32 + self.start.y() + 1,
274 // stop drawing 1 pixel before the grid line
275 cs - 1,
276 cs - 1,
277 ))
278 .unwrap();
279 }
280 }
281 }
282
283 fn modify<F>(&mut self, func: F)
284 where
285 F: FnOnce(&mut Self),
286 {
287 func(self);
288 self.canvas.set_draw_color(Color::RGB(0, 0, 0));
289 self.canvas.clear();
290 self.canvas.set_draw_color(Color::RGB(64, 64, 64));
291 self.draw();
292 self.canvas.present();
293 }
294}
295
296// publicly available functions on appstate
297impl<'ctx> AppState<'ctx> {
298 pub fn init(width: u32, height: u32, context: &'ctx Sdl) -> Self {
299 let video_subsystem = context.video().unwrap();
300
301 let window = video_subsystem
302 .window("Pixel editor", 200, 200)
303 .position_centered()
304 .opengl()
305 .build()
306 .map_err(|e| e.to_string())
307 .unwrap();
308
309 let canvas = window
310 .into_canvas()
311 .build()
312 .map_err(|e| e.to_string())
313 .unwrap();
314
315 let data = vec![false; (width * height) as usize];
316 Self {
317 start: Point::new(60, 60),
318 width,
319 height,
320 data,
321 zoom: 5,
322 brush_size: 1,
323 grid: Grid::new(),
324 canvas,
325 context,
326 last_point: None,
327 active_color: true,
328 undo_stack: UndoStack::new(),
329 current_operation: Vec::new(),
330 }
331 }
332
333 pub fn run(&mut self) {
334 self.canvas.set_draw_color(BLACK);
335 self.canvas.clear();
336 self.draw();
337 self.canvas.present();
338
339 let mut event_pump = self.context.event_pump().unwrap();
340
341 'running: loop {
342 let mouse = event_pump.mouse_state();
343 for event in event_pump.poll_iter() {
344 match event {
345 Event::KeyDown {
346 keycode: Some(k), ..
347 } => {
348 match k {
349 // pan
350 Keycode::W => self.modify(|e| e.pan((0, 10))),
351 Keycode::A => self.modify(|e| e.pan((10, 0))),
352 Keycode::S => self.modify(|e| e.pan((0, -10))),
353 Keycode::D => self.modify(|e| e.pan((-10, 0))),
354 // zoom
355 Keycode::C => {
356 let cursor = (mouse.x(), mouse.y());
357 self.modify(|e| e.zoom_in(cursor));
358 }
359 Keycode::Z => {
360 let cursor = (mouse.x(), mouse.y());
361 self.modify(|e| e.zoom_out(cursor));
362 }
363 // brush ops
364 Keycode::Q => self.modify(|e| e.descrease_brush_size()),
365 Keycode::E => self.modify(|e| e.increase_brush_size()),
366 // flip color
367 Keycode::X => self.modify(|e| e.change_active_color()),
368 // toggle grid
369 Keycode::Tab => self.modify(|e| e.toggle_grid()),
370 // exit
371 Keycode::Escape => break 'running,
372 // undo & redo
373 Keycode::U => self.modify(|e| {
374 if let Some(op) = e.undo_stack.undo() {
375 e.apply_operation(op, OpKind::Undo);
376 }
377 }),
378 Keycode::R => self.modify(|e| {
379 if let Some(op) = e.undo_stack.redo() {
380 e.apply_operation(op, OpKind::Redo);
381 }
382 }),
383 _ => (),
384 }
385 }
386 // start of operation
387 Event::MouseButtonDown {
388 x, y, mouse_btn, ..
389 } => {
390 self.modify(|e| {
391 let pt = (x, y);
392 e.last_point = Some(pt.into());
393 let val = match mouse_btn {
394 MouseButton::Right => !e.active_color,
395 _ => e.active_color,
396 };
397 if let Ok(o) = e.paint_point(pt, val) {
398 e.current_operation.extend(o);
399 }
400 });
401 }
402 // click and drag
403 Event::MouseMotion {
404 x, y, mousestate, ..
405 } => {
406 let is_left = mousestate.is_mouse_button_pressed(MouseButton::Left);
407 let is_right = mousestate.is_mouse_button_pressed(MouseButton::Right);
408 if is_left {
409 self.modify(|e| {
410 let pt = (x, y);
411 let val = e.active_color;
412 if let Ok(o) = e.paint_point(pt, val) {
413 e.current_operation.extend(o);
414 }
415 });
416 } else if is_right {
417 self.modify(|e| {
418 let pt = (x, y);
419 let val = !e.active_color;
420 if let Ok(o) = e.paint_point(pt, val) {
421 e.current_operation.extend(o);
422 }
423 });
424 }
425 }
426 // end of operation
427 Event::MouseButtonUp { .. } => self.modify(|e| {
428 dbg!(&e.current_operation.len());
429 let op = e
430 .current_operation
431 .drain(..)
432 .filter(|v| !v.old_val == v.val)
433 .collect::<Vec<_>>();
434 e.undo_stack.push(op);
435 dbg!(&e.undo_stack);
436 }),
437 Event::Quit { .. } => {
438 break 'running;
439 }
440 _ => {
441 self.modify(|_| ());
442 }
443 }
444 }
445 }
446 }
447}
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 @@
1use sdl2::pixels::Color;
2
3pub const GRID_COLOR: Color = Color::RGB(64, 64, 64);
4pub const WHITE: Color = Color::RGB(255, 255, 255);
5pub const BLACK: Color = Color::RGB(0, 0, 0);
6
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 @@
1mod app;
2mod consts;
3mod undo;
4
5use app::AppState;
6
7pub fn main() {
8 let sdl_context = sdl2::init().unwrap();
9 AppState::init(100, 100, &sdl_context).run();
10}
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 @@
1#[derive(Copy, Clone, Debug)]
2pub struct ModifyRecord {
3 pub point: (i32, i32),
4 pub old_val: bool,
5 pub val: bool,
6}
7
8impl ModifyRecord {
9 pub fn new(point: (i32, i32), old_val: bool, val: bool) -> Self {
10 ModifyRecord {
11 point,
12 old_val,
13 val,
14 }
15 }
16}
17
18pub enum OpKind {
19 Undo,
20 Redo,
21}
22
23pub type Operation = Vec<ModifyRecord>;
24
25#[derive(Debug)]
26pub struct UndoStack<T> {
27 operations: Vec<T>,
28 position: Option<u32>,
29}
30
31impl<T> UndoStack<T>
32where
33 T: Clone,
34{
35 pub fn new() -> Self {
36 Self {
37 operations: Vec::with_capacity(64),
38 position: None,
39 }
40 }
41
42 pub fn push(&mut self, op: T) {
43 if let Some(p) = self.position {
44 // remove all operations past the newly pushed operation
45 for _ in 1 + (p as usize)..self.operations.len() {
46 self.operations.pop();
47 }
48 // advance position
49 self.position = Some(p + 1);
50 // add new operation
51 self.operations.push(op);
52 } else {
53 // empty ops list or undone till start of stack
54 // remove all operations past the newly pushed operation
55 self.operations.clear();
56 // advance position
57 self.position = Some(0);
58 // add new operation
59 self.operations.push(op);
60 }
61 }
62
63 pub fn undo(&mut self) -> Option<T> {
64 if let Some(p) = self.position {
65 self.position = p.checked_sub(1);
66 // we want to return a clone and not a reference because push deletes the item
67 return Some(self.operations[p as usize].clone());
68 }
69 return None;
70 }
71
72 pub fn redo(&mut self) -> Option<T> {
73 if let Some(p) = self.position {
74 if p < self.operations.len() as u32 - 1 {
75 self.position = Some(p + 1);
76 return Some(self.operations[1 + p as usize].clone());
77 }
78 } else if !self.operations.is_empty() {
79 self.position = Some(0);
80 return Some(self.operations[0].clone());
81 }
82 return None;
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 fn setup() -> UndoStack<u32> {
91 let mut stack = UndoStack::new();
92 stack.push(10);
93 stack.push(5);
94 stack.push(2);
95 stack
96 }
97
98 #[test]
99 fn undo_works() {
100 let mut stack = setup();
101 assert_eq!(stack.undo(), Some(2));
102 assert_eq!(stack.undo(), Some(5));
103 assert_eq!(stack.undo(), Some(10));
104 }
105 #[test]
106 fn redo_works() {
107 let mut stack = setup();
108 stack.undo();
109 stack.undo();
110 stack.undo();
111 assert_eq!(stack.redo(), Some(10));
112 assert_eq!(stack.redo(), Some(5));
113 assert_eq!(stack.redo(), Some(2));
114 }
115
116 #[test]
117 fn undo_push_redo() {
118 let mut stack = setup();
119 stack.undo();
120 stack.push(16);
121 assert_eq!(stack.redo(), None);
122 assert_eq!(stack.undo(), Some(16));
123 }
124
125 #[test]
126 fn stack_identity() {
127 let mut stack = setup();
128 stack.undo();
129 stack.redo();
130 stack.undo();
131 assert_eq!(stack.operations, setup().operations);
132 }
133}