tui-additions 0.1.7

Additions to the Rust TUI crate
Documentation
use std::fmt::Display;

use tui::{
    style::{Color, Style},
    text::{Span, Spans},
    widgets::{Paragraph, Widget},
};

#[derive(Clone)]
pub struct TextField {
    pub content: String,
    pub scroll: usize,
    pub cursor: usize,
    pub style: Style,
    pub text_style: Style,
    pub cursor_style: Style,
    pub width: Option<u16>,
}

impl Widget for TextField {
    fn render(self, area: tui::layout::Rect, buf: &mut tui::buffer::Buffer) {
        if let Some(width) = self.width {
            if width != area.width {
                panic!("width mismatch");
            }
        } else {
            panic!("unknown width");
        }

        let cursor_at_end = self.cursor == self.content.len();
        let mut spans = vec![Span::styled(
            self.content[self.scroll..self.cursor].to_string(),
            self.text_style,
        )];

        if cursor_at_end {
            spans.push(Span::styled(String::from(' '), self.cursor_style));
        } else {
            spans.push(Span::styled(
                self.content[self.cursor..self.cursor + 1].to_string(),
                self.cursor_style,
            ));
            spans.push(Span::styled(
                self.content[self.cursor + 1..self.content.len()].to_string(),
                self.text_style,
            ));
        }

        let paragraph = Paragraph::new(Spans::from(spans)).style(self.style);
        paragraph.render(area, buf);
    }
}

impl Default for TextField {
    fn default() -> Self {
        Self {
            content: String::default(),
            scroll: 0,
            cursor: 0,
            style: Style::default(),
            text_style: Style::default(),
            cursor_style: Style::default().bg(Color::Gray),
            width: None,
        }
    }
}

impl TextField {
    pub fn insert(&mut self, index: usize, c: char) -> Result<(), TextFieldError> {
        self.content.insert(index, c);
        self.cursor += 1;
        self.update()?;
        Ok(())
    }

    pub fn remove(&mut self, index: usize) -> Result<Option<char>, TextFieldError> {
        if self.cursor == 0 {
            return Ok(None);
        }
        let c = self.content.remove(index - 1);
        self.cursor -= 1;
        self.update()?;
        Ok(Some(c))
    }

    pub fn push(&mut self, c: char) -> Result<(), TextFieldError> {
        self.insert(self.cursor, c)
    }

    pub fn pop(&mut self) -> Result<Option<char>, TextFieldError> {
        self.remove(self.cursor)
    }

    pub fn left(&mut self) -> Result<(), TextFieldError> {
        if self.cursor == 0 {
            return Ok(());
        }

        self.cursor -= 1;
        self.update()
    }

    pub fn right(&mut self) -> Result<(), TextFieldError> {
        if self.cursor == self.content.len() {
            return Ok(());
        }

        self.cursor += 1;
        self.update()
    }

    pub fn first(&mut self) -> Result<(), TextFieldError> {
        self.cursor = 0;
        self.update()
    }

    pub fn last(&mut self) -> Result<(), TextFieldError> {
        self.cursor = self.content.len();
        self.update()
    }
}

impl TextField {
    pub fn set_width(&mut self, width: u16) {
        self.width = Some(width)
    }

    pub fn update(&mut self) -> Result<(), TextFieldError> {
        let width = if let Some(width) = self.width {
            width
        } else {
            return Err(TextFieldError::UnknownWidth);
        };

        if self.scroll > self.cursor {
            self.scroll = self.cursor;
        } else if self.scroll + width as usize - 1 < self.cursor {
            self.scroll = self.cursor - width as usize + 1;
        }

        Ok(())
    }
}

#[derive(Debug)]
pub enum TextFieldError {
    UnknownWidth,
}

impl Display for TextFieldError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_fmt(format_args!("{:?}", self))
    }
}