Skip to main content

anathema_default_widgets/
canvas.rs

1use anathema_geometry::{LocalPos, Pos, Size};
2use anathema_value_resolver::AttributeStorage;
3use anathema_widgets::error::Result;
4use anathema_widgets::layout::{Constraints, LayoutCtx, PositionCtx};
5use anathema_widgets::paint::{Glyph, PaintCtx, SizePos};
6use anathema_widgets::{LayoutForEach, PaintChildren, PositionChildren, Style, Widget, WidgetId};
7use unicode_width::UnicodeWidthChar;
8
9use crate::{HEIGHT, WIDTH};
10
11#[derive(Debug, Default, Clone, Copy)]
12enum Cell {
13    #[default]
14    Empty,
15    Occupied(char, Style),
16}
17
18#[derive(Debug, Default)]
19pub struct CanvasBuffer {
20    positions: Box<[Cell]>,
21    size: Size,
22}
23
24impl CanvasBuffer {
25    pub fn new(size: Size) -> Self {
26        Self {
27            positions: vec![Cell::Empty; size.area()].into_boxed_slice(),
28            size,
29        }
30    }
31
32    fn put(&mut self, c: char, style: Style, pos: impl Into<LocalPos>) {
33        let pos = pos.into();
34
35        if pos.x >= self.size.width || pos.y >= self.size.height {
36            return;
37        }
38        let index = pos.to_index(self.size.width);
39
40        let mut cell = Cell::Occupied(c, style);
41        std::mem::swap(&mut self.positions[index], &mut cell);
42    }
43
44    fn get(&self, pos: impl Into<LocalPos>) -> Option<&Cell> {
45        let pos = pos.into();
46        if pos.x >= self.size.width || pos.y >= self.size.height {
47            return None;
48        }
49
50        let index = pos.to_index(self.size.width);
51        match self.positions.get(index)? {
52            cell @ Cell::Occupied(..) => Some(cell),
53            Cell::Empty => None,
54        }
55    }
56
57    fn get_mut(&mut self, pos: impl Into<LocalPos>) -> Option<&mut Cell> {
58        let pos = pos.into();
59        if pos.x >= self.size.width || pos.y >= self.size.height {
60            return None;
61        }
62
63        let index = pos.to_index(self.size.width);
64        match self.positions.get_mut(index)? {
65            cell @ Cell::Occupied(..) => Some(cell),
66            Cell::Empty => None,
67        }
68    }
69
70    fn remove(&mut self, pos: impl Into<LocalPos>) {
71        let pos = pos.into();
72        if pos.x >= self.size.width || pos.y >= self.size.height {
73            return;
74        }
75
76        let index = pos.to_index(self.size.width);
77        if index < self.positions.len() {
78            let mut cell = Cell::Empty;
79            std::mem::swap(&mut self.positions[index], &mut cell);
80        }
81    }
82
83    fn copy_from(other: &mut CanvasBuffer, size: Size) -> Self {
84        let mut new_buffer = CanvasBuffer::new(size);
85
86        for (pos, c, attrs) in other.drain() {
87            if pos.x >= size.width || pos.y >= size.height {
88                continue;
89            }
90            new_buffer.put(c, attrs, pos);
91        }
92
93        new_buffer
94    }
95
96    fn drain(&mut self) -> impl Iterator<Item = (LocalPos, char, Style)> + '_ {
97        self.positions.iter_mut().enumerate().filter_map(|(index, cell)| {
98            let mut old = Cell::Empty;
99            std::mem::swap(&mut old, cell);
100            //
101            match old {
102                Cell::Empty => None,
103                Cell::Occupied(c, attribs) => {
104                    let y = index as u16 / self.size.width;
105                    let x = index as u16 % self.size.width;
106                    let pos = LocalPos::new(x, y);
107                    Some((pos, c, attribs))
108                }
109            }
110        })
111    }
112
113    fn iter(&self) -> impl Iterator<Item = (LocalPos, char, &Style)> + '_ {
114        self.positions.iter().enumerate().filter_map(|(index, cell)| {
115            let x = index as u16 % self.size.width;
116            let y = index as u16 / self.size.width;
117            let pos = LocalPos::new(x, y);
118            //
119            match cell {
120                Cell::Empty => None,
121                Cell::Occupied(c, attribs) => Some((pos, *c, attribs)),
122            }
123        })
124    }
125
126    pub fn clear(&mut self) {
127        *self = Self::new(self.size);
128    }
129}
130
131#[derive(Debug)]
132pub struct Canvas {
133    buffer: CanvasBuffer,
134    pos: Pos,
135    is_dirty: bool,
136}
137
138impl Canvas {
139    pub fn restore_buffer(&mut self, buffer: &mut CanvasBuffer) {
140        self.buffer = std::mem::take(buffer);
141    }
142
143    pub fn take_buffer(&mut self) -> CanvasBuffer {
144        std::mem::take(&mut self.buffer)
145    }
146
147    pub fn translate(&self, pos: Pos) -> LocalPos {
148        let offset = pos - self.pos;
149        LocalPos::new(offset.x as u16, offset.y as u16)
150    }
151
152    pub fn put(&mut self, c: char, style: Style, pos: impl Into<LocalPos>) {
153        self.is_dirty = true;
154        self.buffer.put(c, style, pos);
155    }
156
157    pub fn get(&mut self, pos: impl Into<LocalPos>) -> Option<(char, Style)> {
158        match self.buffer.get(pos).copied()? {
159            Cell::Occupied(c, style) => Some((c, style)),
160            Cell::Empty => None,
161        }
162    }
163
164    pub fn get_mut(&mut self, pos: impl Into<LocalPos>) -> Option<(&mut char, &mut Style)> {
165        match self.buffer.get_mut(pos)? {
166            Cell::Occupied(c, style) => {
167                self.is_dirty = true;
168                Some((c, style))
169            }
170            Cell::Empty => None,
171        }
172    }
173
174    pub fn erase(&mut self, pos: impl Into<LocalPos>) {
175        self.is_dirty = true;
176        self.buffer.remove(pos)
177    }
178
179    pub fn clear(&mut self) {
180        self.buffer.clear();
181    }
182}
183
184impl Default for Canvas {
185    fn default() -> Self {
186        Self {
187            buffer: CanvasBuffer::new((32, 32).into()),
188            pos: Pos::ZERO,
189            is_dirty: true,
190        }
191    }
192}
193
194impl Widget for Canvas {
195    fn layout<'bp>(
196        &mut self,
197        _: LayoutForEach<'_, 'bp>,
198        mut constraints: Constraints,
199        id: WidgetId,
200        ctx: &mut LayoutCtx<'_, 'bp>,
201    ) -> Result<Size> {
202        let attribs = ctx.attribute_storage.get(id);
203
204        if let Some(width) = attribs.get_as::<u16>(WIDTH) {
205            constraints.set_max_width(width);
206        }
207
208        if let Some(height) = attribs.get_as::<u16>(HEIGHT) {
209            constraints.set_max_height(height);
210        }
211
212        let size = constraints.max_size();
213
214        if self.buffer.size != size {
215            self.buffer = CanvasBuffer::copy_from(&mut self.buffer, size);
216        }
217
218        Ok(size)
219    }
220
221    fn position<'bp>(
222        &mut self,
223        _: PositionChildren<'_, 'bp>,
224        _id: WidgetId,
225        _attribute_storage: &AttributeStorage<'bp>,
226        ctx: PositionCtx,
227    ) {
228        self.pos = ctx.pos;
229    }
230
231    fn paint<'bp>(
232        &mut self,
233        _children: PaintChildren<'_, 'bp>,
234        _id: WidgetId,
235        _attribute_storage: &AttributeStorage<'bp>,
236        mut ctx: PaintCtx<'_, SizePos>,
237    ) {
238        for (pos, c, style) in self.buffer.iter() {
239            ctx.set_style(*style, pos);
240            let glyph = Glyph::from_char(c, c.width().unwrap_or(0) as u8);
241            ctx.place_glyph(glyph, pos);
242        }
243    }
244
245    fn needs_reflow(&mut self) -> bool {
246        let needs_reflow = self.is_dirty;
247        self.is_dirty = false;
248        needs_reflow
249    }
250}
251
252#[cfg(test)]
253mod test {
254    use super::*;
255    use crate::testing::TestRunner;
256
257    #[test]
258    fn resize_canvas() {
259        let expected = "
260            ╔══╗
261            ║  ║
262            ║  ║
263            ╚══╝
264        ";
265        TestRunner::new("canvas", (2, 2)).instance().render_assert(expected);
266    }
267
268    #[test]
269    fn get_set_glyph() {
270        let mut canvas = Canvas::default();
271        canvas.put('a', Style::reset(), (0, 0));
272        let (c, _) = canvas.get((0, 0)).unwrap();
273        assert_eq!(c, 'a');
274    }
275
276    #[test]
277    fn remove_glyph() {
278        let mut canvas = Canvas::default();
279        canvas.put('a', Style::reset(), (0, 0));
280        assert!(canvas.get((0, 0)).is_some());
281        canvas.erase((0, 0));
282        assert!(canvas.get((0, 0)).is_none());
283    }
284
285    #[test]
286    fn put_buffer_out_of_range() {
287        let mut under_test = Canvas {
288            buffer: CanvasBuffer::new(Size::new(1, 2)),
289            ..Default::default()
290        };
291
292        under_test.put('x', Style::reset(), LocalPos::new(0, 0));
293        under_test.put('x', Style::reset(), LocalPos::new(0, 1));
294        under_test.put('o', Style::reset(), LocalPos::new(1, 0));
295
296        for cell in under_test.buffer.positions {
297            match cell {
298                Cell::Empty => panic!("Should not be empty"),
299                Cell::Occupied(c, _) => assert_eq!(c, 'x'),
300            }
301        }
302    }
303
304    #[test]
305    fn get_buffer_out_of_range() {
306        let mut under_test = Canvas {
307            buffer: CanvasBuffer::new(Size::new(1, 2)),
308            ..Default::default()
309        };
310
311        under_test.put('x', Style::reset(), LocalPos::new(0, 0));
312        under_test.put('x', Style::reset(), LocalPos::new(0, 1));
313
314        assert!(under_test.get(LocalPos::new(1, 0)).is_none());
315    }
316
317    #[test]
318    fn get_mut_buffer_out_of_range() {
319        let mut under_test = Canvas {
320            buffer: CanvasBuffer::new(Size::new(1, 2)),
321            ..Default::default()
322        };
323
324        under_test.put('x', Style::reset(), LocalPos::new(0, 0));
325        under_test.put('x', Style::reset(), LocalPos::new(0, 1));
326
327        assert!(under_test.get_mut(LocalPos::new(1, 0)).is_none());
328    }
329
330    #[test]
331    fn remove_buffer_out_of_range() {
332        let mut under_test = Canvas {
333            buffer: CanvasBuffer::new(Size::new(1, 2)),
334            ..Default::default()
335        };
336
337        under_test.put('x', Style::reset(), LocalPos::new(0, 0));
338        under_test.put('x', Style::reset(), LocalPos::new(0, 1));
339        under_test.erase(LocalPos::new(1, 0));
340
341        for cell in under_test.buffer.positions {
342            match cell {
343                Cell::Empty => panic!("Should not be empty"),
344                Cell::Occupied(c, _) => assert_eq!(c, 'x'),
345            }
346        }
347    }
348}