anathema-default-widgets 0.2.6

Default widget implementations for Anathema
Documentation
use anathema_geometry::{LocalPos, Pos, Size};
use anathema_value_resolver::AttributeStorage;
use anathema_widgets::error::Result;
use anathema_widgets::layout::{Constraints, LayoutCtx, PositionCtx};
use anathema_widgets::paint::{Glyph, PaintCtx, SizePos};
use anathema_widgets::{LayoutForEach, PaintChildren, PositionChildren, Style, Widget, WidgetId};
use unicode_width::UnicodeWidthChar;

use crate::{HEIGHT, WIDTH};

#[derive(Debug, Default, Clone, Copy)]
enum Cell {
    #[default]
    Empty,
    Occupied(char, Style),
}

#[derive(Debug, Default)]
pub struct CanvasBuffer {
    positions: Box<[Cell]>,
    size: Size,
}

impl CanvasBuffer {
    pub fn new(size: Size) -> Self {
        Self {
            positions: vec![Cell::Empty; size.area()].into_boxed_slice(),
            size,
        }
    }

    fn put(&mut self, c: char, style: Style, pos: impl Into<LocalPos>) {
        let pos = pos.into();

        if pos.x >= self.size.width || pos.y >= self.size.height {
            return;
        }
        let index = pos.to_index(self.size.width);

        let mut cell = Cell::Occupied(c, style);
        std::mem::swap(&mut self.positions[index], &mut cell);
    }

    fn get(&self, pos: impl Into<LocalPos>) -> Option<&Cell> {
        let pos = pos.into();
        if pos.x >= self.size.width || pos.y >= self.size.height {
            return None;
        }

        let index = pos.to_index(self.size.width);
        match self.positions.get(index)? {
            cell @ Cell::Occupied(..) => Some(cell),
            Cell::Empty => None,
        }
    }

    fn get_mut(&mut self, pos: impl Into<LocalPos>) -> Option<&mut Cell> {
        let pos = pos.into();
        if pos.x >= self.size.width || pos.y >= self.size.height {
            return None;
        }

        let index = pos.to_index(self.size.width);
        match self.positions.get_mut(index)? {
            cell @ Cell::Occupied(..) => Some(cell),
            Cell::Empty => None,
        }
    }

    fn remove(&mut self, pos: impl Into<LocalPos>) {
        let pos = pos.into();
        if pos.x >= self.size.width || pos.y >= self.size.height {
            return;
        }

        let index = pos.to_index(self.size.width);
        if index < self.positions.len() {
            let mut cell = Cell::Empty;
            std::mem::swap(&mut self.positions[index], &mut cell);
        }
    }

    fn copy_from(other: &mut CanvasBuffer, size: Size) -> Self {
        let mut new_buffer = CanvasBuffer::new(size);

        for (pos, c, attrs) in other.drain() {
            if pos.x >= size.width || pos.y >= size.height {
                continue;
            }
            new_buffer.put(c, attrs, pos);
        }

        new_buffer
    }

    fn drain(&mut self) -> impl Iterator<Item = (LocalPos, char, Style)> + '_ {
        self.positions.iter_mut().enumerate().filter_map(|(index, cell)| {
            let mut old = Cell::Empty;
            std::mem::swap(&mut old, cell);
            //
            match old {
                Cell::Empty => None,
                Cell::Occupied(c, attribs) => {
                    let y = index as u16 / self.size.width;
                    let x = index as u16 % self.size.width;
                    let pos = LocalPos::new(x, y);
                    Some((pos, c, attribs))
                }
            }
        })
    }

    fn iter(&self) -> impl Iterator<Item = (LocalPos, char, &Style)> + '_ {
        self.positions.iter().enumerate().filter_map(|(index, cell)| {
            let x = index as u16 % self.size.width;
            let y = index as u16 / self.size.width;
            let pos = LocalPos::new(x, y);
            //
            match cell {
                Cell::Empty => None,
                Cell::Occupied(c, attribs) => Some((pos, *c, attribs)),
            }
        })
    }

    pub fn clear(&mut self) {
        *self = Self::new(self.size);
    }
}

#[derive(Debug)]
pub struct Canvas {
    buffer: CanvasBuffer,
    pos: Pos,
    is_dirty: bool,
}

impl Canvas {
    pub fn restore_buffer(&mut self, buffer: &mut CanvasBuffer) {
        self.buffer = std::mem::take(buffer);
    }

    pub fn take_buffer(&mut self) -> CanvasBuffer {
        std::mem::take(&mut self.buffer)
    }

    pub fn translate(&self, pos: Pos) -> LocalPos {
        let offset = pos - self.pos;
        LocalPos::new(offset.x as u16, offset.y as u16)
    }

    pub fn put(&mut self, c: char, style: Style, pos: impl Into<LocalPos>) {
        self.is_dirty = true;
        self.buffer.put(c, style, pos);
    }

    pub fn get(&mut self, pos: impl Into<LocalPos>) -> Option<(char, Style)> {
        match self.buffer.get(pos).copied()? {
            Cell::Occupied(c, style) => Some((c, style)),
            Cell::Empty => None,
        }
    }

    pub fn get_mut(&mut self, pos: impl Into<LocalPos>) -> Option<(&mut char, &mut Style)> {
        match self.buffer.get_mut(pos)? {
            Cell::Occupied(c, style) => {
                self.is_dirty = true;
                Some((c, style))
            }
            Cell::Empty => None,
        }
    }

    pub fn erase(&mut self, pos: impl Into<LocalPos>) {
        self.is_dirty = true;
        self.buffer.remove(pos)
    }

    pub fn clear(&mut self) {
        self.buffer.clear();
    }
}

impl Default for Canvas {
    fn default() -> Self {
        Self {
            buffer: CanvasBuffer::new((32, 32).into()),
            pos: Pos::ZERO,
            is_dirty: true,
        }
    }
}

impl Widget for Canvas {
    fn layout<'bp>(
        &mut self,
        _: LayoutForEach<'_, 'bp>,
        mut constraints: Constraints,
        id: WidgetId,
        ctx: &mut LayoutCtx<'_, 'bp>,
    ) -> Result<Size> {
        let attribs = ctx.attribute_storage.get(id);

        if let Some(width) = attribs.get_as::<u16>(WIDTH) {
            constraints.set_max_width(width);
        }

        if let Some(height) = attribs.get_as::<u16>(HEIGHT) {
            constraints.set_max_height(height);
        }

        let size = constraints.max_size();

        if self.buffer.size != size {
            self.buffer = CanvasBuffer::copy_from(&mut self.buffer, size);
        }

        Ok(size)
    }

    fn position<'bp>(
        &mut self,
        _: PositionChildren<'_, 'bp>,
        _id: WidgetId,
        _attribute_storage: &AttributeStorage<'bp>,
        ctx: PositionCtx,
    ) {
        self.pos = ctx.pos;
    }

    fn paint<'bp>(
        &mut self,
        _children: PaintChildren<'_, 'bp>,
        _id: WidgetId,
        _attribute_storage: &AttributeStorage<'bp>,
        mut ctx: PaintCtx<'_, SizePos>,
    ) {
        for (pos, c, style) in self.buffer.iter() {
            ctx.set_style(*style, pos);
            let glyph = Glyph::from_char(c, c.width().unwrap_or(0) as u8);
            ctx.place_glyph(glyph, pos);
        }
    }

    fn needs_reflow(&mut self) -> bool {
        let needs_reflow = self.is_dirty;
        self.is_dirty = false;
        needs_reflow
    }
}

#[cfg(test)]
mod test {
    use super::*;
    use crate::testing::TestRunner;

    #[test]
    fn resize_canvas() {
        let expected = "
            ╔══╗
            ║  ║
            ║  ║
            ╚══╝
        ";
        TestRunner::new("canvas", (2, 2)).instance().render_assert(expected);
    }

    #[test]
    fn get_set_glyph() {
        let mut canvas = Canvas::default();
        canvas.put('a', Style::reset(), (0, 0));
        let (c, _) = canvas.get((0, 0)).unwrap();
        assert_eq!(c, 'a');
    }

    #[test]
    fn remove_glyph() {
        let mut canvas = Canvas::default();
        canvas.put('a', Style::reset(), (0, 0));
        assert!(canvas.get((0, 0)).is_some());
        canvas.erase((0, 0));
        assert!(canvas.get((0, 0)).is_none());
    }

    #[test]
    fn put_buffer_out_of_range() {
        let mut under_test = Canvas {
            buffer: CanvasBuffer::new(Size::new(1, 2)),
            ..Default::default()
        };

        under_test.put('x', Style::reset(), LocalPos::new(0, 0));
        under_test.put('x', Style::reset(), LocalPos::new(0, 1));
        under_test.put('o', Style::reset(), LocalPos::new(1, 0));

        for cell in under_test.buffer.positions {
            match cell {
                Cell::Empty => panic!("Should not be empty"),
                Cell::Occupied(c, _) => assert_eq!(c, 'x'),
            }
        }
    }

    #[test]
    fn get_buffer_out_of_range() {
        let mut under_test = Canvas {
            buffer: CanvasBuffer::new(Size::new(1, 2)),
            ..Default::default()
        };

        under_test.put('x', Style::reset(), LocalPos::new(0, 0));
        under_test.put('x', Style::reset(), LocalPos::new(0, 1));

        assert!(under_test.get(LocalPos::new(1, 0)).is_none());
    }

    #[test]
    fn get_mut_buffer_out_of_range() {
        let mut under_test = Canvas {
            buffer: CanvasBuffer::new(Size::new(1, 2)),
            ..Default::default()
        };

        under_test.put('x', Style::reset(), LocalPos::new(0, 0));
        under_test.put('x', Style::reset(), LocalPos::new(0, 1));

        assert!(under_test.get_mut(LocalPos::new(1, 0)).is_none());
    }

    #[test]
    fn remove_buffer_out_of_range() {
        let mut under_test = Canvas {
            buffer: CanvasBuffer::new(Size::new(1, 2)),
            ..Default::default()
        };

        under_test.put('x', Style::reset(), LocalPos::new(0, 0));
        under_test.put('x', Style::reset(), LocalPos::new(0, 1));
        under_test.erase(LocalPos::new(1, 0));

        for cell in under_test.buffer.positions {
            match cell {
                Cell::Empty => panic!("Should not be empty"),
                Cell::Occupied(c, _) => assert_eq!(c, 'x'),
            }
        }
    }
}