zi 0.3.2

A declarative library for building monospace user interfaces
Documentation
use unicode_width::UnicodeWidthStr;

use crate::{layout::Layout, Canvas, Component, ComponentLink, Rect, ShouldRender, Size, Style};

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum TextAlign {
    Left,
    Centre,
    Right,
}

impl Default for TextAlign {
    fn default() -> Self {
        Self::Left
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum TextWrap {
    None,
    Word,
}

impl Default for TextWrap {
    fn default() -> Self {
        Self::None
    }
}

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct TextProperties {
    pub style: Style,
    pub content: String,
    pub align: TextAlign,
    pub wrap: TextWrap,
}

impl TextProperties {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn style(mut self, style: Style) -> Self {
        self.style = style;
        self
    }

    pub fn content(mut self, content: impl Into<String>) -> Self {
        self.content = content.into();
        self
    }

    pub fn align(mut self, align: TextAlign) -> Self {
        self.align = align;
        self
    }

    pub fn wrap(mut self, wrap: TextWrap) -> Self {
        self.wrap = wrap;
        self
    }
}

#[derive(Debug)]
pub struct Text {
    frame: Rect,
    properties: <Self as Component>::Properties,
}

impl Component for Text {
    type Message = ();
    type Properties = TextProperties;

    fn create(properties: Self::Properties, frame: Rect, _link: ComponentLink<Self>) -> Self {
        Self { frame, properties }
    }

    fn change(&mut self, properties: Self::Properties) -> ShouldRender {
        if self.properties != properties {
            self.properties = properties;
            ShouldRender::Yes
        } else {
            ShouldRender::No
        }
    }

    fn resize(&mut self, frame: Rect) -> ShouldRender {
        if self.frame != frame {
            self.frame = frame;
            ShouldRender::Yes
        } else {
            ShouldRender::No
        }
    }

    fn view(&self) -> Layout {
        let Self {
            frame,
            properties:
                Self::Properties {
                    ref content,
                    align,
                    style,
                    wrap,
                },
        } = *self;

        let mut canvas = Canvas::new(frame.size);
        canvas.clear(style);

        let content_size = text_block_size(content);
        let position_x = match align {
            TextAlign::Left => 0,
            TextAlign::Centre => (frame.size.width / 2).saturating_sub(content_size.width / 2),
            TextAlign::Right => frame.size.width.saturating_sub(content_size.width),
        };

        let mut position_y = 0;
        for line in content.lines() {
            match wrap {
                TextWrap::None => {
                    canvas.draw_str(position_x, position_y, style, line);
                }
                TextWrap::Word => {
                    let mut cursor_x = position_x;
                    for word in line.split_whitespace() {
                        let word_width = UnicodeWidthStr::width(word);
                        if cursor_x > position_x {
                            if cursor_x >= frame.size.width
                                || word_width > frame.size.width.saturating_sub(cursor_x + 1)
                            {
                                position_y += 1;
                                cursor_x = position_x
                            } else {
                                canvas.draw_str(cursor_x, position_y, style, " ");
                                cursor_x += 1;
                            }
                        }
                        canvas.draw_str(cursor_x, position_y, style, word);
                        cursor_x += word_width;
                    }
                }
            }
            position_y += 1;
        }

        canvas.into()
    }
}

fn text_block_size(text: &str) -> Size {
    let width = text.lines().map(UnicodeWidthStr::width).max().unwrap_or(0);
    let height = text.lines().count();
    Size::new(width, height)
}