termit-ui 0.0.1

Terminal UI with GUI-like layouts
Documentation
use crate::color::Color;
use crate::geometry::*;
use std::fmt::Display;
use unicode_segmentation::{Graphemes, UnicodeSegmentation};

#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
pub struct Spot {
    pub symbol: String,
    pub style: Style,
}

#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub struct Style {
    pub bold: Option<bool>,
    pub underline: Option<bool>,
    pub foreground: Option<Color>,
    pub background: Option<Color>,
}

impl Style {
    pub fn or(&self, other: &Style) -> Style {
        Style {
            bold: self.bold.or(other.bold),
            underline: self.underline.or(other.underline),
            foreground: self.foreground.or(other.foreground),
            background: self.background.or(other.background),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Drawing {
    pub text: String,
    pub scope: Window,
    pub style: Style,
}
impl Default for Drawing {
    fn default() -> Self {
        Drawing {
            text: Default::default(),
            style: Default::default(),
            scope: window(
                Point::default(),
                point(x(u16::max_value()), y(u16::max_value())),
            ),
        }
    }
}

pub trait Drawable {
    fn as_drawing(&self) -> Drawing;
}

impl<T: Display> Drawable for T {
    fn as_drawing(&self) -> Drawing {
        Drawing::new().with_text(self)
    }
}
impl Drawable for Spot {
    fn as_drawing(&self) -> Drawing {
        Drawing::new()
            .with_text(&self.symbol)
            .with_style(self.style)
    }
}

impl Drawing {
    pub fn new() -> Drawing {
        Drawing::default()
    }
    pub fn with_style(mut self, style: Style) -> Self {
        self.style = style;
        self
    }
    pub fn append(mut self, text: impl Display) -> Self {
        self.text.push_str(&format!("{}", text));
        self
    }
    pub fn with_text(mut self, text: impl Display) -> Self {
        self.text = format!("{}", text);
        self
    }
    pub fn with_pos(mut self, position: Point) -> Self {
        self.scope = window(position, self.scope.size);
        self
    }
    pub fn scope(mut self, scope: Window) -> Self {
        self.scope = scope;
        self
    }
    pub fn size(mut self, size: Point) -> Self {
        self.scope = window(self.scope.position, size);
        self
    }
    pub fn left(mut self, left: u16) -> Self {
        self.scope = window(point(x(left), self.scope.position.y), self.scope.size);
        self
    }
    pub fn top(mut self, top: u16) -> Self {
        self.scope = window(point(self.scope.position.x, y(top)), self.scope.size);
        self
    }
    pub fn width(mut self, width: u16) -> Self {
        self.scope = window(self.scope.position, point(x(width), self.scope.size.y));
        self
    }
    pub fn height(mut self, height: u16) -> Self {
        self.scope = window(self.scope.position, point(self.scope.size.x, y(height)));
        self
    }
    pub fn fg(mut self, foreground: impl Into<Option<Color>>) -> Self {
        self.style.foreground = foreground.into();
        self
    }
    pub fn bg(mut self, background: impl Into<Option<Color>>) -> Self {
        self.style.background = background.into();
        self
    }
    pub fn lines<'a>(&'a self) -> DrawingLines<'a> {
        DrawingLines(self)
    }
    pub fn spots<'a>(&'a self) -> DrawingSpots<'a> {
        DrawingSpots {
            style: self.style,
            size: self.scope.size,
            graphemes: self.text.graphemes(true),
            pos: point(x(0), y(0)),
        }
    }
    pub fn actual_width(&self) -> X {
        self.lines()
            .into_iter()
            .map(|l| x(l.graphemes(true).count() as u16))
            .max()
            .unwrap_or(x(0))
    }
}

pub struct DrawingSpots<'a> {
    size: Point,
    style: Style,
    graphemes: Graphemes<'a>,
    pos: Point,
}

impl<'a> Iterator for DrawingSpots<'a> {
    type Item = (Point, Spot);
    fn next(&mut self) -> Option<Self::Item> {
        while let (Some(g), true) = (self.graphemes.next(), self.size.contains(&self.pos)) {
            match g {
                "\r\n" | "\n\r" => {
                    self.pos.x = x(0);
                    self.pos.y += y(1);
                }
                "\n" => self.pos.y += y(1),
                "\r" => self.pos.x = x(0),
                g => {
                    let p = self.pos.clone();

                    if let Some(idx) = self.size.idx_at_point(p) {
                        if let Some(next) = self.size.x.point_at_idx(idx + 1) {
                            self.pos = next;
                        } else {
                            return None;
                        }
                    } else {
                        return None;
                    }

                    return Some((
                        p,
                        Spot {
                            symbol: g.to_owned(),
                            style: self.style,
                        },
                    ));
                }
            }
        }
        None
    }
}

pub struct DrawingLines<'a>(&'a Drawing);

pub struct DrawingLinesIterator<'a> {
    size: Point,
    graphemes: Graphemes<'a>,
    lines: u16,
}

impl<'a> IntoIterator for DrawingLines<'a> {
    type Item = String;
    type IntoIter = DrawingLinesIterator<'a>;
    fn into_iter(self) -> Self::IntoIter {
        DrawingLinesIterator {
            size: self.0.scope.size,
            graphemes: self.0.text.graphemes(true),
            lines: 0,
        }
    }
}

impl<'a> Iterator for DrawingLinesIterator<'a> {
    type Item = String;
    fn next(&mut self) -> Option<Self::Item> {
        let mut line = vec![];
        let mut can_be_empty = false;
        let max_lines = self.size.y.to_primitive();
        let max_cols = self.size.x.to_primitive();
        while self.lines < max_lines && line.len() < max_cols as usize {
            if let Some(c) = self.graphemes.next() {
                // a grapheme has been found, the line will be returned even if empty
                can_be_empty = true;
                match c {
                    // skip CR and LF and end the line
                    "\r\n" | "\n\r" | "\n" | "\r" => break,
                    // otherwise add grapheme to the line
                    c => line.push(c),
                }
            } else {
                // no more graphemes, end the line
                break;
            };
        }
        // return non-empty line or empty line if graphemes have been consumed
        if can_be_empty || !line.is_empty() {
            self.lines += 1;
            Some(line.concat())
        } else {
            None
        }
    }
}

#[test]
fn test_drawing_lines_break_on_size() {
    let lines: Vec<String> = "123456"
        .as_drawing()
        .width(2)
        .height(2)
        .lines()
        .into_iter()
        .collect();
    assert_eq!(lines, ["12", "34"])
}

#[test]
fn test_drawing_default_size() {
    let size = "".as_drawing().scope.size;
    assert_eq!(size.x.to_primitive(), u16::max_value());
    assert_eq!(size.y.to_primitive(), u16::max_value());
}

#[test]
fn test_drawing_actual_width() {
    let width = "12\n3".as_drawing().actual_width();
    assert_eq!(width, x(2));

    let width = "12\r3".as_drawing().actual_width();
    assert_eq!(width, x(2));

    let width = "12\r\n3".as_drawing().actual_width();
    assert_eq!(width, x(2));
}

#[test]
fn test_drawing_spots() {
    let count = "12\r\n4567".as_drawing().width(3).height(2).spots().count();
    assert_eq!(count, 5);
}