diff options
author | Akshay <[email protected]> | 2021-03-12 14:54:55 +0000 |
---|---|---|
committer | Akshay <[email protected]> | 2021-03-12 14:54:55 +0000 |
commit | 9f0dc65dabff2a1e443199ed68292398dcb390b1 (patch) | |
tree | 17b29a4315df3d5caaa967b079c3d459063c0de9 /src |
init
Diffstat (limited to 'src')
-rw-r--r-- | src/app.rs | 447 | ||||
-rw-r--r-- | src/consts.rs | 6 | ||||
-rw-r--r-- | src/main.rs | 10 | ||||
-rw-r--r-- | src/undo.rs | 133 |
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 @@ | |||
1 | use crate::undo::{ModifyRecord, OpKind, Operation, UndoStack}; | ||
2 | |||
3 | use 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 | |||
14 | use crate::consts::{BLACK, GRID_COLOR, WHITE}; | ||
15 | |||
16 | pub 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 | |||
32 | struct Grid { | ||
33 | enabled: bool, | ||
34 | color: Color, | ||
35 | } | ||
36 | |||
37 | impl Grid { | ||
38 | fn new() -> Self { | ||
39 | Self { | ||
40 | enabled: true, | ||
41 | color: GRID_COLOR, | ||
42 | } | ||
43 | } | ||
44 | } | ||
45 | |||
46 | // private actions on appstate | ||
47 | impl<'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 | ||
297 | impl<'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 @@ | |||
1 | use sdl2::pixels::Color; | ||
2 | |||
3 | pub const GRID_COLOR: Color = Color::RGB(64, 64, 64); | ||
4 | pub const WHITE: Color = Color::RGB(255, 255, 255); | ||
5 | pub 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 @@ | |||
1 | mod app; | ||
2 | mod consts; | ||
3 | mod undo; | ||
4 | |||
5 | use app::AppState; | ||
6 | |||
7 | pub 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)] | ||
2 | pub struct ModifyRecord { | ||
3 | pub point: (i32, i32), | ||
4 | pub old_val: bool, | ||
5 | pub val: bool, | ||
6 | } | ||
7 | |||
8 | impl 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 | |||
18 | pub enum OpKind { | ||
19 | Undo, | ||
20 | Redo, | ||
21 | } | ||
22 | |||
23 | pub type Operation = Vec<ModifyRecord>; | ||
24 | |||
25 | #[derive(Debug)] | ||
26 | pub struct UndoStack<T> { | ||
27 | operations: Vec<T>, | ||
28 | position: Option<u32>, | ||
29 | } | ||
30 | |||
31 | impl<T> UndoStack<T> | ||
32 | where | ||
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)] | ||
87 | mod 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 | } | ||