use termion::cursor; use super::{ compose::{Component, Components}, theme::{self, ColorSet}, }; 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), theme: theme::Frame, } impl Frame { #[inline(always)] pub fn sub(&self, theme: theme::Frame, size: FrameSize) -> Frame { let (p_width, p_height) = self.size(); let sub_size = size.abs_size(( p_width - self.theme.width * 2, p_height - self.theme.width * 2, )); self.child_centered(sub_size, theme) } #[inline(always)] pub fn goto(&self, x: u16, y: u16) -> String { cursor::Goto( self.theme.width.max( (self.theme.width + self.start.0 + x) .min(self.end.0 - self.theme.width), ), self.theme.width.max( (self.theme.width + self.start.1 + y) .min(self.end.1 - self.theme.width), ), ) .into() } #[inline(always)] pub fn prefix_centered_goto( &self, comp: Components, line_num: u16, ) -> Components { let x = (self.inner_size().0 - comp.str_len()) / 2; vec![Component::Goto(x, line_num)] .into_iter() .chain(comp.0.into_iter()) .collect::>() .into() } #[inline(always)] fn goto_internal(&self, x: u16, y: u16) -> Component { Component::Internal( cursor::Goto( (self.start.0 + x).min(self.end.0), (self.start.1 + y).min(self.end.1), ) .into(), ) .into() } #[inline(always)] fn write_centered_clear(&self, s: &str) -> Component { let width = self.size().0; let limit = width - self.theme.width * 2; let s = if s.len() > limit as usize { &s[..limit as usize] } else { s }; let len = s.len() as u16; let base_size = ((width - len) / 2) - self.theme.width; Component::String(super::compose::Text::PaddedBothSides( base_size + ((width - len) % 2), s.into(), base_size, )) } #[inline(always)] fn write_frame_line( &self, y: u16, length: u16, c: char, ) -> Components { vec![self.goto_internal(0, y), Component::Repeated(c, length)] .into() } pub fn make(&self, comp: Components) -> String { comp.0.into_iter().map(|c| c.make(self)).collect() } #[inline(always)] pub fn size(&self) -> (u16, u16) { (self.end.0 - self.start.0, self.end.1 - self.start.1) } // Size within the borders #[inline(always)] pub fn inner_size(&self) -> (u16, u16) { ( self.end.0 - self.start.0 - self.theme.width * 2, self.end.1 - self.start.1 - self.theme.width * 2, ) } #[inline(always)] pub fn from_terminal_size( theme: theme::Frame, ) -> Result { let term = termion::terminal_size()?; Ok(Self::from_bottom_right(term, term, theme)) } #[inline(always)] fn child_centered( &self, (pos_w, pos_h): (u16, u16), theme: theme::Frame, ) -> Frame { let parent_offset = self.theme.width / 2; let (parent_w, parent_h) = self.size(); let (parent_w, parent_h) = ( parent_w - self.theme.width, parent_h - self.theme.width, ); let start = ( 1.max( (parent_w - pos_w) / 2 + ((parent_w - pos_w) % 2) + parent_offset, ), 1.max( (parent_h - pos_h) / 2 + ((parent_h - pos_h) % 2) + parent_offset, ), ); let frame = Self { end: (start.0 + pos_w, start.1 + pos_h), start, theme, }; frame } #[inline(always)] fn from_bottom_right( (pos_w, pos_h): (u16, u16), (term_w, term_h): (u16, u16), theme: theme::Frame, ) -> Self { let start = (1.max(term_w - pos_w), 1.max(term_h - pos_h)); let frame = Self { end: (start.0 + pos_w, start.1 + pos_h), start, theme, }; if frame.start.0 > frame.end.0 || frame.start.1 > frame.end.1 { eprintln!( "pos_w {}, pos_h {}, term_w {}, term_h {}, border {}", pos_w, pos_h, term_w, term_h, theme.width, ); panic!( "start.0: {} end.0: {}, start.1: {}, end.1: {}", frame.start.0, frame.end.0, frame.start.1, frame.end.1 ); } frame } #[inline(always)] pub fn frame_str(&self, body_theme: ColorSet) -> String { self.make(self.compose(body_theme)) } // compose generates a collection of components for drawing // the frame within a screen pub fn compose(&self, body_theme: ColorSet) -> Components { let body_theme = Component::Internal(body_theme.to_string()); let (w_len, h_len) = self.size(); let frame_theme = Component::Internal(self.theme.color.to_string()); let body_clear = Component::Internal(self.make(Into::::into( vec![body_theme.clone(), Component::Padding(w_len)], ))); let border_left = Component::Repeated( self.theme.frame_chars.left, self.theme.width, ); let border_right = Component::Repeated( self.theme.frame_chars.right, self.theme.width, ); // This seems cursed but, I assure you, it's fine vec![vec![frame_theme.clone()]] .into_iter() // Top horizontal borders .chain((0..self.theme.width).into_iter().map(|y: u16| { self.write_frame_line( y, w_len, self.theme.frame_chars.top, ) .0 })) // Frame body + vertical frame borders .chain( (self.theme.width..h_len - self.theme.width) .into_iter() .map(|y: u16| { let left = self.goto_internal(0, y); let right = self.goto_internal( w_len - self.theme.width, y, ); vec![ left.clone(), body_clear.clone(), frame_theme.clone(), left, border_left.clone(), right, border_right.clone(), ] }), ) // Bottom horizontal border .chain((h_len - self.theme.width..h_len).into_iter().map( |y: u16| { self.write_frame_line( y, w_len, self.theme.frame_chars.bottom, ) .0 }, )) // Theme reset + move to beginning of inside frame .chain( vec![vec![Component::Goto(0, 0), body_theme]] .into_iter(), ) .flatten() .collect::>() .into() } } impl FrameSize { #[inline(always)] fn abs_size( &self, (term_width, term_height): (u16, u16), ) -> (u16, u16) { match self { FrameSize::ByAbsolute(w, h) => { ((*w).min(term_width), (*h).min(term_height)) } FrameSize::ByPercent(w, h) => { // term_height = 100% // x = h% // // term_height * h / 100 = x ( term_width * (*w).min(100) / 100, term_height * (*h).min(100) / 100, ) } } } }