use termion::{clear, cursor, raw::RawTerminal}; const ESTIMATED_FRAME_BIT_SIZE: usize = 11; use std::io::{Stdout, Write}; use super::theme::{ColorSet, Theme}; const FRAME_CHAR_HORIZONTAL: char = ' '; const FRAME_CHAR_VERTICAL: char = ' '; pub enum FrameSize { ByAbsolute(u16, u16), ByPercent(u16, u16), } #[derive(Debug, Clone, Copy)] pub struct Frame { // Both are (w, h) start: (u16, u16), end: (u16, u16), width: u16, theme: ColorSet, } impl Frame { #[inline(always)] pub fn sub( &self, theme: ColorSet, size: FrameSize, width: u16, ) -> Frame { let (p_width, p_height) = self.size(); size.abs_size((p_width - 1, p_height - 1), theme, width) } #[inline(always)] pub fn goto(&self, x: u16, y: u16) -> String { let cg = cursor::Goto( self.width.max( (self.width + self.start.0 + x) .min(self.end.0 - self.width), ), self.width.max( (self.width + self.start.1 + y) .min(self.end.1 - self.width), ), ); cg.into() } #[inline(always)] fn goto_internal(&self, x: u16, y: u16) -> String { cursor::Goto( (self.start.0 + x).min(self.end.0), (self.start.1 + y).min(self.end.1), ) .into() } #[inline(always)] fn write_clear_to_end(&self, s: &str) -> String { let clear_length = self.size().0 as usize - s.len() - (self.width * 2) as usize; format!("{}{}", s, " ".repeat(clear_length)) } pub fn writeln(&self, s: &str, x: u16, y: u16) -> String { let words = s.split('\n').collect::>(); let mut out_words = Vec::with_capacity(words.len()); for i in 0..words.len() { out_words.push(format!( "{ret}{str}", str = self.write_clear_to_end(words[i]), ret = self.goto(x, y + i as u16), )); } out_words.concat() } #[inline(always)] fn write_frame_line(&self, y: u16, length: u16) -> String { self.goto_internal(0, y) + &FRAME_CHAR_HORIZONTAL .to_string() .repeat(length as usize) } pub fn write_centered(&self, s: &str, y: u16) -> String { let (width, _) = self.size(); let words = s.split('\n').collect::>(); let mut out_words = Vec::with_capacity(words.len()); for i in 0..words.len() { let word = words[i]; let x = width - (word.len() as u16).min(width); out_words.push(format!( "{ret}{str}", str = word, ret = self.goto(x / 2, y + i as u16), )); } out_words.concat() } #[inline(always)] pub fn size(&self) -> (u16, u16) { (self.end.0 - self.start.0, self.end.1 - self.start.1) } #[inline(always)] pub fn from_terminal_size( theme: ColorSet, width: u16, ) -> Result { let term = termion::terminal_size()?; // Swap term dimensions to use x/y // let term = (term.1, term.0); Ok(Self::from_bottom_right(term, term, width, theme)) } #[inline(always)] fn from_bottom_right( (pos_w, pos_h): (u16, u16), (term_w, term_h): (u16, u16), width: u16, theme: ColorSet, ) -> Self { Self { start: (1.max(term_w - pos_w), 1.max(term_h - pos_h)), end: (pos_w, pos_h), width, theme, } } pub fn frame_str(&self, body_theme: String) -> String { if self.width == 0 { return body_theme + &clear::All.to_string(); } self.goto_internal(0, 0); let (w_len, h_len) = self.size(); let width_str = FRAME_CHAR_HORIZONTAL .to_string() .repeat(w_len as usize + 1); let mut frame = Vec::with_capacity(h_len as usize + 4); let frame_theme = self.theme.to_string(); let body_clear = body_theme.clone() + &" ".repeat(w_len as usize); frame.push(self.theme.to_string()); for y in 0..self.width { frame.push(self.write_frame_line(y, w_len)); } for y in self.width..h_len - self.width { frame.push(format!( "{left}{body_clear}{frame_theme}{left}{char}{right}{char}", body_clear = &body_clear, frame_theme = &frame_theme, left = self.goto_internal(0, y), right = self.goto_internal(w_len - self.width, y), char = FRAME_CHAR_VERTICAL .to_string() .repeat(self.width as usize), )); } for y in h_len - self.width..h_len { frame.push(self.write_frame_line(y, w_len)); } frame.push(self.goto(0, 0)); frame.push(body_theme); frame.concat() } } impl FrameSize { #[inline(always)] fn abs_size( &self, (term_width, term_height): (u16, u16), theme: ColorSet, width: u16, ) -> Frame { let pos = match self { FrameSize::ByAbsolute(h, w) => { ((*w).min(term_width), (*h).min(term_height)) } FrameSize::ByPercent(h, w) => { // term_height = 100% // x = h% // // term_height * h / 100 = x ( term_width * (*w).min(100) / 100, term_height * (*h).min(100) / 100, ) } }; Frame::from_bottom_right( pos, (term_width, term_height), width, theme, ) } }