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 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 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}