Skip to main content

dartboard_core/
ops.rs

1use serde::{Deserialize, Serialize};
2
3use crate::canvas::{Canvas, Pos};
4use crate::color::RgbColor;
5
6#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
7pub enum CanvasOp {
8    PaintCell {
9        pos: Pos,
10        ch: char,
11        fg: RgbColor,
12    },
13    ClearCell {
14        pos: Pos,
15    },
16    PaintRegion {
17        cells: Vec<CellWrite>,
18    },
19    ShiftRow {
20        y: usize,
21        kind: RowShift,
22    },
23    ShiftCol {
24        x: usize,
25        kind: ColShift,
26    },
27    /// Replace the entire canvas. Used for large structural edits (undo /
28    /// redo, paste of big regions) where itemizing per-cell writes would be
29    /// more expensive than just shipping a snapshot. Safe on SP; WS plan
30    /// will want to avoid this path for high-frequency edits.
31    Replace {
32        canvas: Canvas,
33    },
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
37pub enum CellWrite {
38    Paint { pos: Pos, ch: char, fg: RgbColor },
39    Clear { pos: Pos },
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
43pub enum RowShift {
44    PushLeft { to_x: usize },
45    PushRight { from_x: usize },
46    PullFromLeft { to_x: usize },
47    PullFromRight { from_x: usize },
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
51pub enum ColShift {
52    PushUp { to_y: usize },
53    PushDown { from_y: usize },
54    PullFromUp { to_y: usize },
55    PullFromDown { from_y: usize },
56}
57
58impl Canvas {
59    pub fn apply(&mut self, op: &CanvasOp) {
60        match op {
61            CanvasOp::PaintCell { pos, ch, fg } => {
62                let _ = self.put_glyph_colored(*pos, *ch, *fg);
63            }
64            CanvasOp::ClearCell { pos } => self.clear_cell(*pos),
65            CanvasOp::PaintRegion { cells } => {
66                for write in cells {
67                    match write {
68                        CellWrite::Paint { pos, ch, fg } => {
69                            let _ = self.put_glyph_colored(*pos, *ch, *fg);
70                        }
71                        CellWrite::Clear { pos } => self.clear_cell(*pos),
72                    }
73                }
74            }
75            CanvasOp::ShiftRow { y, kind } => match kind {
76                RowShift::PushLeft { to_x } => self.push_left(*y, *to_x),
77                RowShift::PushRight { from_x } => self.push_right(*y, *from_x),
78                RowShift::PullFromLeft { to_x } => self.pull_from_left(*y, *to_x),
79                RowShift::PullFromRight { from_x } => self.pull_from_right(*y, *from_x),
80            },
81            CanvasOp::ShiftCol { x, kind } => match kind {
82                ColShift::PushUp { to_y } => self.push_up(*x, *to_y),
83                ColShift::PushDown { from_y } => self.push_down(*x, *from_y),
84                ColShift::PullFromUp { to_y } => self.pull_from_up(*x, *to_y),
85                ColShift::PullFromDown { from_y } => self.pull_from_down(*x, *from_y),
86            },
87            CanvasOp::Replace { canvas } => *self = canvas.clone(),
88        }
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    fn red() -> RgbColor {
97        RgbColor::new(255, 0, 0)
98    }
99
100    #[test]
101    fn paint_cell_op_writes_colored_glyph() {
102        let mut canvas = Canvas::with_size(8, 4);
103        canvas.apply(&CanvasOp::PaintCell {
104            pos: Pos { x: 2, y: 1 },
105            ch: 'A',
106            fg: red(),
107        });
108        assert_eq!(canvas.get(Pos { x: 2, y: 1 }), 'A');
109        assert_eq!(canvas.fg(Pos { x: 2, y: 1 }), Some(red()));
110    }
111
112    #[test]
113    fn paint_region_applies_paint_and_clear_entries() {
114        let mut canvas = Canvas::with_size(8, 4);
115        canvas.set_colored(Pos { x: 0, y: 0 }, 'Q', red());
116        canvas.apply(&CanvasOp::PaintRegion {
117            cells: vec![
118                CellWrite::Clear {
119                    pos: Pos { x: 0, y: 0 },
120                },
121                CellWrite::Paint {
122                    pos: Pos { x: 1, y: 0 },
123                    ch: 'Z',
124                    fg: red(),
125                },
126            ],
127        });
128        assert_eq!(canvas.get(Pos { x: 0, y: 0 }), ' ');
129        assert_eq!(canvas.get(Pos { x: 1, y: 0 }), 'Z');
130    }
131
132    #[test]
133    fn shift_row_dispatches_to_push_left() {
134        let mut canvas = Canvas::with_size(8, 4);
135        canvas.set(Pos { x: 0, y: 0 }, 'A');
136        canvas.set(Pos { x: 1, y: 0 }, 'B');
137        canvas.set(Pos { x: 2, y: 0 }, 'C');
138        canvas.apply(&CanvasOp::ShiftRow {
139            y: 0,
140            kind: RowShift::PushLeft { to_x: 1 },
141        });
142        assert_eq!(canvas.get(Pos { x: 0, y: 0 }), 'B');
143        assert_eq!(canvas.get(Pos { x: 1, y: 0 }), ' ');
144        assert_eq!(canvas.get(Pos { x: 2, y: 0 }), 'C');
145    }
146
147    #[test]
148    fn canvas_op_serde_roundtrip() {
149        let op = CanvasOp::PaintRegion {
150            cells: vec![CellWrite::Paint {
151                pos: Pos { x: 1, y: 2 },
152                ch: '🌱',
153                fg: red(),
154            }],
155        };
156        let j = serde_json::to_string(&op).unwrap();
157        let back: CanvasOp = serde_json::from_str(&j).unwrap();
158        assert_eq!(op, back);
159    }
160}