glyph_ui 0.1.0

TUI library utilizing the Elm architecture
Documentation
use std::convert::TryInto;

use euclid::{Point2D, Rect, Size2D};
use thiserror::Error;
use unicode_segmentation::UnicodeSegmentation;

use crate::{unit::Cell, Am, Command, CommandBuf};

/// Draw on a subset of the screen
pub struct Printer<'a> {
    /// Area this printer is allowed to print on
    area: Rect<u16, Cell>,

    cmd_buf: Am<CommandBuf>,

    new_cmd_buf: NewCmdBuf<'a>,
}

type NewCmdBuf<'a> = &'a dyn Fn() -> Am<CommandBuf>;

impl<'a> Printer<'a> {
    /// Creates a new printer on the given window
    pub(crate) fn new<S>(
        size: S,
        cmd_buf: Am<CommandBuf>,
        new_cmd_buf: NewCmdBuf<'a>,
    ) -> Self
    where
        S: Into<Size2D<u16, Cell>>,
    {
        Self {
            area: Rect::new(Point2D::zero(), size.into()),
            cmd_buf,
            new_cmd_buf,
        }
    }

    /// Used on the root view when the screen size changes
    pub(crate) fn set_size<S>(&mut self, size: S)
    where
        S: Into<Size2D<u16, Cell>>,
    {
        self.area.size = size.into();
    }

    /// Get the size of this printer
    ///
    /// Note that the printable area will be `[(0, 0), (size.x, size.y))`, i.e.
    /// inclusive-exclusive.
    pub fn size(&self) -> Size2D<u16, Cell> {
        self.area.size
    }

    /// Prints text inside the printer's area
    pub fn print<P>(&self, text: &str, start: P) -> Result<(), OutOfBounds>
    where
        P: Into<Point2D<u16, Cell>>,
    {
        // Where we are asked to start printing relative to the printer
        let start = start.into();

        // Where printing starts relative to the origin of the screen (not the
        // printer). Note that crossterm represents the top-left-most cell as
        // (0, 0) and not (1, 1).
        let start = self.area.origin + start.to_vector();

        // Starting point must be inside the printer area
        if !&self.area.contains(start) {
            return Err(OutOfBounds::Start(self.area, start));
        }

        // The amount of columns the text requires
        let width =
            text.graphemes(true).count().try_into().expect("u16 overflow");

        // Where the printing will stop relative to the screen
        let end = (
            start
                .x
                .saturating_add(width)
                // One character only takes a single cell, which means the start
                // and end are the same value.
                .saturating_sub(1)
                // Disallow going before the beginning in case width is zero
                .max(start.x),
            start.y,
        )
            .into();

        // Ending point must be inside the printer area
        if !&self.area.contains(end) {
            return Err(OutOfBounds::End(self.area, end));
        }

        self.cmd_buf.lock().cmds.push(Command::MoveTo(start));
        self.cmd_buf.lock().cmds.push(Command::Print(text.to_string()));

        Ok(())
    }

    /// Shows the cursor at the given location
    pub fn show_cursor_at<P>(&self, point: P) -> Result<(), OutOfBounds>
    where
        P: Into<Point2D<u16, Cell>>,
    {
        let point = point.into();

        let point = self.area.origin + point.to_vector();

        if !self.area.contains(point) {
            return Err(OutOfBounds::Start(self.area, point));
        }

        let mut cmd_buf = self.cmd_buf.lock();
        cmd_buf.cmds.push(Command::MoveTo(point));
        cmd_buf.cmds.push(Command::ShowCursor);
        cmd_buf.changes_cursor = true;

        Ok(())
    }

    /// Hides the cursor
    pub fn hide_cursor<P>(&self) {
        self.cmd_buf.lock().cmds.push(Command::HideCursor);

        // Don't bother setting changes_cursor to true because we don't want to
        // potentially contest another view that wants the cursor shown
        // somewhere else
    }

    /// Returns a printer that's allowed to print on a subset of its parent
    pub fn to_sub_area<R>(&self, sub_area: R) -> Result<Self, OutOfBounds>
    where
        R: Into<Rect<u16, Cell>>,
    {
        let sub_area = sub_area.into();
        let new_area = Rect {
            origin: self.area.origin + sub_area.origin.to_vector(),
            size: sub_area.size,
        };

        if !self.area.contains_rect(&new_area) {
            return Err(OutOfBounds::Area(self.area, new_area));
        }

        Ok(Self {
            area: new_area,
            cmd_buf: (self.new_cmd_buf)(),
            new_cmd_buf: self.new_cmd_buf,
        })
    }
}

/// Error returned from some [`Printer`] methods
///
/// [`Printer`]: Printer
#[derive(Error, Debug)]
pub enum OutOfBounds {
    /// The starting point was out of bounds
    #[error("starting point {1:?} out of bounds {0:?}")]
    Start(Rect<u16, Cell>, Point2D<u16, Cell>),

    /// The ending point was out of bounds
    #[error("ending point {1:?} out of bounds {0:?}")]
    End(Rect<u16, Cell>, Point2D<u16, Cell>),

    /// A new rectangle does not fit inside the old rectangle
    #[error("inner area {1:?} out of outer rectangle {0:?}")]
    Area(Rect<u16, Cell>, Rect<u16, Cell>),
}