use crate::{
components as cpn, container::Container, error::{FtuiError, FtuiResult},
list::List, util::ansi
};
use std::io::{self, Write};
use crossterm as ct;
#[derive(Clone, Debug, PartialEq, Eq)]
struct Line {
ansi: Vec<&'static str>,
width: usize,
data: String,
}
impl Line {
pub fn new(width: u16) -> Line {
Line {
ansi: vec![],
width: width as usize,
data: " ".repeat(width as usize),
}
}
#[inline]
pub fn add_ansi(&mut self, value: &'static str) {
self.ansi.push(value);
}
pub fn add_ansi_many(&mut self, value: &[&'static str]) {
self.ansi.reserve(value.len());
self.ansi.extend(value.iter().copied());
}
pub fn fill(&mut self, c: char) {
self.data.clear();
self.data.extend(std::iter::repeat(c).take(self.width));
}
pub fn fill_dotted(&mut self, c: char) {
let repeat_count = (self.width as f32 / 2.0).floor() as usize;
self.data.clear();
for _ in 0..repeat_count {
self.data.push(c);
self.data.push(' ');
}
}
#[inline]
pub fn edit(&mut self, data: &String, begin: u16) {
self.data.replace_range(begin as usize..data.len() + begin as usize, data);
}
pub fn clear(&mut self) {
self.fill(' ');
self.ansi.clear();
}
}
pub fn ready() -> FtuiResult<()> {
print!(
"{}{}{}",
ansi::ESC_CLEAR_TERM, ansi::ESC_CURSOR_HOME, ansi::ESC_CURSOR_HIDE);
io::stdout().flush()?;
Ok(())
}
pub fn unready() -> FtuiResult<()> {
print!(
"{}{}{}",
ansi::ESC_CLEAR_TERM, ansi::ESC_CURSOR_HOME, ansi::ESC_CURSOR_SHOW);
io::stdout().flush()?;
Ok(())
}
pub fn clear() -> FtuiResult<()> {
print!("{}", ansi::ESC_CLEAR_TERM);
io::stdout().flush()?;
Ok(())
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Renderer {
width: u16,
height: u16,
lines: Vec<Line>,
}
impl Renderer {
pub fn new(width: u16, height: u16) -> Renderer {
Renderer {
width,
height,
lines: Self::make_lines(width, height),
}
}
pub fn fullscreen() -> FtuiResult<Renderer> {
let (width, height) = ct::terminal::size()?;
Ok(Self::new(width, height))
}
fn make_lines(width: u16, height: u16) -> Vec<Line> {
(0..height).map(|_| Line::new(width)).collect()
}
#[inline]
fn calc_middle_align_pos(width: u16, len: usize) -> u16 {
((width as f32 - len as f32) / 2.0).round() as u16
}
#[inline]
fn calc_right_align_pos(width: u16, len: usize) -> u16 {
(width as usize - len) as u16
}
#[inline]
fn calc_left_align_pos() -> u16 {
0
}
#[inline]
fn calc_bottom_align_pos(height: u16) -> u16 {
height - 1
}
fn ensure_label_inbound(&self, len: usize) -> FtuiResult<()> {
if len > self.width as usize {
Err(FtuiError::RendererContainerTooBig)
} else {
Ok(())
}
}
fn render_header(&mut self, header: &cpn::Header) -> FtuiResult<()> {
self.ensure_label_inbound(header.len())?;
self.lines[0].edit(
header.label(),
Self::calc_middle_align_pos(self.width, header.len()));
self.lines[0].add_ansi(ansi::ESC_GREEN_B);
Ok(())
}
fn render_options(&mut self, options: &[cpn::Option]) -> FtuiResult<()> {
for option in options {
self.ensure_label_inbound(option.len())?;
let line = &mut self.lines[option.line() as usize];
line.edit(option.label(), 0);
if option.selc_on() {
line.add_ansi(ansi::ESC_BLUE_B);
}
}
Ok(())
}
#[inline]
fn apply_correct_separator(&mut self, separator: &cpn::Separator, c: char) {
if separator.is_dotted() {
self.lines[separator.line() as usize].fill_dotted(c);
} else {
self.lines[separator.line() as usize].fill(c);
}
}
fn render_separator(&mut self, separators: &[cpn::Separator]) {
for separator in separators {
match separator.style() {
cpn::SeparatorStyle::Solid =>
self.apply_correct_separator(separator, '█'),
cpn::SeparatorStyle::Medium =>
self.apply_correct_separator(separator, '━'),
cpn::SeparatorStyle::Thin =>
self.apply_correct_separator(separator, '─'),
cpn::SeparatorStyle::Double =>
self.apply_correct_separator(separator, '═'),
cpn::SeparatorStyle::Custom(c) =>
self.apply_correct_separator(separator, c),
}
}
}
fn resolve_text_pos_with_len(&self, text: &mut cpn::Text, len: usize) {
if text.flags().contains(cpn::TextFlags::ALIGN_MIDDLE) {
text.set_pos(Self::calc_middle_align_pos(self.width, len));
} else if text.flags().contains(cpn::TextFlags::ALIGN_RIGHT) {
text.set_pos(Self::calc_right_align_pos(self.width, len));
} else {
text.set_pos(Self::calc_left_align_pos());
}
if text.flags().contains(cpn::TextFlags::ALIGN_BOTTOM) {
text.set_line(Self::calc_bottom_align_pos(self.height));
}
}
#[inline]
fn resolve_text_pos(&self, text: &mut cpn::Text) {
self.resolve_text_pos_with_len(text, text.len());
}
fn render_text(&mut self, texts: &mut [cpn::Text]) -> FtuiResult<()> {
for text in texts.iter_mut() {
self.ensure_label_inbound(text.len())?;
self.resolve_text_pos(text);
let line = &mut self.lines[text.line() as usize];
line.edit(text.label(), text.pos());
line.add_ansi_many(text.styles());
}
Ok(())
}
pub fn render(&mut self, container: &mut Container) -> FtuiResult<()> {
if container.component_count() > self.height {
return Err(FtuiError::RendererContainerTooBig);
}
if let Some(header) = container.header().as_ref() {
self.render_header(header)?;
}
self.render_options(container.options())?;
self.render_text(container.texts_mut())?;
self.render_separator(container.separators());
Ok(())
}
pub fn render_list(&mut self, list: &mut List) -> FtuiResult<()> {
let avoid_header_offset = match list.header() {
Some(header) => {
self.render_header(header)?;
1
},
None => 0,
};
if list.len() == 0 {
return Ok(());
}
let offset = list.offset();
let is_number = list.is_number();
let element_len_offset = if list.is_number() { 3 } else { 0 };
for (i, element) in list
.elements_mut()
.iter_mut()
.skip(offset)
.take((self.height - 1) as usize)
.enumerate()
{
self.ensure_label_inbound(element.len())?;
self.resolve_text_pos_with_len(
element, element.len() + element_len_offset);
let line = &mut self.lines[i + avoid_header_offset];
if is_number {
line.edit(
&format!("{}. {}", i + 1 + offset, element.label()),
element.pos());
} else {
line.edit(element.label(), element.pos());
}
line.add_ansi_many(element.styles());
}
Ok(())
}
pub fn draw(&mut self) -> FtuiResult<()> {
for (i, line) in self.lines.iter().enumerate() {
let output = format!(
"{}{}{}{}",
line.ansi.concat(),
line.data, ansi::ESC_COLOR_RESET, ansi::ESC_STYLE_RESET);
if i == (self.height - 1) as usize {
print!("{}", output);
} else {
println!("{}", output);
}
}
print!("{}", ansi::ESC_CURSOR_HOME);
io::stdout().flush()?;
Ok(())
}
#[inline]
pub fn clear(&mut self) {
self.lines.iter_mut().for_each(|line| line.clear());
}
pub fn simple_draw(&mut self, container: &mut Container) -> FtuiResult<()> {
self.clear();
self.render(container)?;
self.draw()?;
Ok(())
}
}