diff --git a/src/display/body.rs b/src/display/body.rs index 44b23cc..d71189d 100644 --- a/src/display/body.rs +++ b/src/display/body.rs @@ -3,7 +3,7 @@ use std::io::{Stdout, Write}; use termion::{clear, cursor, raw::RawTerminal}; use super::{ - compose::{Component, Components}, + compose::{Component, Components, HorizontalAlignment, Text}, frame::Frame, theme::ColorSet, Environment, Event, Page, @@ -23,16 +23,14 @@ impl Body { screen: &mut RawTerminal, event: Event, ) -> Result<()> { - let event = format!("{}", event); - write!( - screen, - "{theme}{content}", - theme = env.theme.primary(), - content = env - .frame - .writeln(&format!("Event: {}", event), 0, 0) - .concat_for(&env.frame), - )?; + let echo_comps = + Components::break_into(format!("Event: {}", event)) + .nextline_after_text( + 0, + HorizontalAlignment::Left, + &env.frame, + ); + write!(screen, "{}", env.frame.make(echo_comps))?; screen.flush()?; Ok(()) } @@ -112,43 +110,76 @@ impl SigninPage { const HOSTNAME_Y: u16 = 3; const OK_Y: u16 = 5; let frame = self.frame.unwrap(); - let fr_width = frame.size().0; - let total_width = fr_width / 3; + let (w, _) = frame.inner_size(); + let exp_w = w / 3; - todo!() - // frame - // .write_centered("login kk", HEADER_Y) - // .into_iter() - // .chain( - // frame - // .write_centered( - // &self.field_str( - // highlight, - // total_width, - // "hostname", - // "", - // ), - // HOSTNAME_Y, - // ) - // .into_iter(), - // ) - // .chain(vec![Component::Theme(normal)].into_iter()) - // .chain(frame.write_centered("[ok]", OK_Y).into_iter()) - // .collect::>() - // .into() + vec![ + frame + .prefix_centered_goto( + vec![Component::String(Text::Normal( + "login kk".into(), + ))] + .into(), + HEADER_Y, + ) + .0, + frame + .prefix_centered_goto( + self.field( + highlight, + normal, + exp_w, + "hostname", + &self.hostname, + ), + HOSTNAME_Y, + ) + .0, + frame + .prefix_centered_goto( + self.field( + highlight, + normal, + exp_w, + "username", + &self.username, + ), + OK_Y, + ) + .0, + ] + .into_iter() + .flatten() + .collect::>() + .into() } #[inline] - fn field_str( + fn field( &self, color: ColorSet, - width: u16, - field: &str, - field_content: &str, - ) -> String { - let unpadded = format!("{field}: {field_content}",); - let len = field.len() + field_content.len() + 2; - unpadded + &"_".repeat(width as usize - len) + normal: ColorSet, + exp_width: u16, + field: C, + field_content: C, + ) -> Components + where + C: Into, + { + let (field, field_content) = + (field.into(), field_content.into()); + let budget_for_content = exp_width - field.len() as u16 - 3; + vec![ + Component::String((field + &": ").into()), + Component::Theme(color), + Component::String(Text::LimitedOrPadded( + field_content, + '_', + budget_for_content, + )), + Component::Theme(normal), + ] + .into() } } @@ -197,11 +228,10 @@ impl Page for SigninPage { .unwrap() .frame_str(env.theme.colors.subwin), hide_cursor = cursor::Hide, - content = "", - // content = self.draw( - // env.theme.colors.highlight, - // env.theme.colors.subwin - // ), + content = self.frame.unwrap().make(self.draw( + env.theme.colors.highlight, + env.theme.colors.subwin + )) )?; screen.flush()?; diff --git a/src/display/compose.rs b/src/display/compose.rs index 0044b44..bd50c0e 100644 --- a/src/display/compose.rs +++ b/src/display/compose.rs @@ -1,14 +1,121 @@ -use std::{ - iter::Map, - ops::{Deref, Range}, - vec::IntoIter, -}; - use super::{frame::Frame, theme::ColorSet}; +use std::ops::Deref; + +const ELLIPSES: char = '…'; + +#[derive(Clone, Debug)] +pub enum Text { + // Unrestricted + Normal(String), + // To length + Limited(String, u16), + // Limit or pad to length + LimitedOrPadded(String, char, u16), + PaddedBothSides(u16, String, u16), +} + +impl From for Text { + fn from(value: String) -> Self { + Self::Normal(value) + } +} + +impl Text { + #[inline] + pub fn limit(self, limit: u16) -> Text { + match self { + Self::Normal(s) => Self::Limited(s, limit), + Self::Limited(s, _) => Self::Limited(s, limit), + Self::LimitedOrPadded(s, c, _) => { + Self::LimitedOrPadded(s, c, limit) + } + Self::PaddedBothSides(left, s, right) => todo!(), + } + } + #[inline] + pub fn len(&self) -> u16 { + match self { + Text::Normal(s) => s.len() as u16, + Text::Limited(s, max) => (*max).min(s.len() as u16), + Text::LimitedOrPadded(_, _, len) => *len, + Text::PaddedBothSides(left, s, right) => { + left + s.len() as u16 + right + } + } + } + #[inline] + fn conditional_truncate(s: String, len: u16) -> String { + let mut s = s; + if s.len() > len as usize { + s.truncate(len as usize - 1); + s.push(ELLIPSES); + } + s + } + + #[inline] + pub fn make(self, frame: &Frame) -> String { + match self { + Text::Normal(s) => s, + Text::Limited(s, len) => { + Self::conditional_truncate(s, len) + } + Text::LimitedOrPadded(s, pad, len) => { + let s = Self::conditional_truncate(s, len); + let sln = s.len(); + s + &pad.to_string().repeat(len as usize - sln) + } + Text::PaddedBothSides(left, s, right) => { + format!( + "{}{}{}", + " ".repeat(left as usize), + s, + " ".repeat(right as usize) + ) + } + } + } +} + +#[derive(Clone, Copy)] +pub enum HorizontalAlignment { + Left, + Right, + Center, +} + +impl HorizontalAlignment { + pub fn align( + &self, + s: Text, + frame: &Frame, + y: u16, + ) -> Components { + let (width, _) = frame.size(); + match self { + Self::Left => { + vec![Component::Goto(0, y), Component::String(s)] + } + Self::Right => { + vec![ + Component::Goto(s.len() as u16 - width, y), + Component::String(s), + ] + } + Self::Center => { + vec![ + Component::Goto((width - s.len()) / 2, y), + Component::String(s), + ] + } + } + .into() + } +} #[derive(Clone, Debug)] pub enum Component { - String(String), + String(Text), Goto(u16, u16), Theme(ColorSet), Padding(u16), @@ -19,9 +126,9 @@ pub enum Component { } impl Component { - pub fn prepare_for(self, frame: &Frame) -> String { + pub fn make(self, frame: &Frame) -> String { match self { - Component::String(s) => s, + Component::String(s) => s.make(frame), Component::Goto(x, y) => frame.goto(x, y), Component::Theme(c) => c.to_string(), Component::Clear => termion::clear::All.to_string(), @@ -58,27 +165,48 @@ impl From for Components { } impl Components { + // Consumes itself, returns a set of new components + // with gotos for the following line, starting at y (not y+1) + // if there's a goto, the goto is used as the last y. + // also applies alignment, relative to the given frame + pub fn nextline_after_text( + self, + y: u16, + align: HorizontalAlignment, + frame: &Frame, + ) -> Self { + let mut last_y = y; + let xxx = self.0.into_iter().map(|comp| match comp { + Component::String(s) => { + last_y += 1; + align.align(s, frame, last_y).0 + } + Component::Goto(_, y) => { + last_y = y; + vec![comp] + } + _ => vec![comp], + }); + todo!() + } + + pub fn break_into(s: String) -> Components { + s.split('\n') + .map(|pt| Component::String(Text::Normal(pt.into()))) + .collect::>() + .into() + } + pub fn str_len(&self) -> u16 { (&self.0) .into_iter() .map(|comp| { if let Component::String(s) = comp { - s.len() as u16 + s.len() } else { 0 } }) .sum() } - - pub fn concat_for(self, frame: &Frame) -> String { - self.0 - .into_iter() - .map(|component| component.prepare_for(frame)) - .collect::() - } - - pub fn push(&mut self, comp: Component) { - self.0.push(comp) - } } diff --git a/src/display/frame.rs b/src/display/frame.rs index e40cf7b..c6eaab2 100644 --- a/src/display/frame.rs +++ b/src/display/frame.rs @@ -1,15 +1,12 @@ -use termion::{clear, cursor, raw::RawTerminal}; +use termion::cursor; const ESTIMATED_FRAME_BIT_SIZE: usize = 11; -use std::{ - io::{Stdout, Write}, - slice::Iter, - vec::IntoIter, -}; + +use crate::display::compose::Text; use super::{ compose::{Component, Components}, - theme::{ColorSet, Theme}, + theme::ColorSet, }; const FRAME_CHAR_HORIZONTAL: char = ' '; @@ -60,6 +57,20 @@ impl Frame { .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( @@ -73,7 +84,7 @@ impl Frame { } #[inline(always)] - fn write_centered_clear(&self, s: &str) -> Components { + fn write_centered_clear(&self, s: &str) -> Component { let width = self.size().0; let limit = width - self.border * 2; let s = if s.len() > limit as usize { @@ -83,12 +94,11 @@ impl Frame { }; let len = s.len() as u16; let base_size = ((width - len) / 2) - self.border; - vec![ - Component::Padding(base_size + ((width - len) % 2)), - Component::String(s.into()), - Component::Padding(base_size), - ] - .into() + Component::String(super::compose::Text::PaddedBothSides( + base_size + ((width - len) % 2), + s.into(), + base_size, + )) } #[inline(always)] @@ -100,58 +110,39 @@ impl Frame { .into() } - #[inline(always)] - fn write_clear_to_end(&self, s: &str) -> Components { - let limit = (self.size().0 - self.border * 2) as usize; - let s = if s.len() > limit { &s[..limit] } else { s }; + #[inline] + pub fn to_end(&self, s: &str, x: u16, y: u16) -> Components { + // FIXME: handle the fucking newlines grrrr i hate this + let width = self.inner_size().0; vec![ - Component::String(s.into()), - Component::Padding( - self.size().0 - s.len() as u16 - (self.border * 2), - ), + Component::Goto(x, y), + Component::String(Text::LimitedOrPadded( + s.into(), + ' ', + width - x, + )), ] .into() } - pub fn writeln(&self, s: &str, x: u16, y: u16) -> Components { - let mut index: u16 = 0; - s.split('\n') - .flat_map(|wrd| { - index += 1; - vec![ - vec![Component::Goto(x, y + index - 1)], - self.write_clear_to_end(wrd).0, - ] - }) - .flatten() - .collect::>() - .into() - } - - pub fn write_centered(&self, s: &str, y: u16) -> Components { - let mut index: u16 = 0; - s.split('\n') - .flat_map(|wrd| { - index += 1; - vec![ - vec![Component::Goto(0, y + index - 1)], - self.write_centered_clear(wrd).0, - ] - }) - .flatten() - .collect::>() - .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) { - eprintln!( - "end.0: {}, start.0: {}, end.1: {}, start.1: {}", - self.end.0, self.start.0, self.end.1, self.start.1 - ); (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.border * 2, + self.end.1 - self.start.1 - self.border * 2, + ) + } + #[inline(always)] pub fn from_terminal_size( theme: ColorSet, @@ -225,8 +216,9 @@ impl Frame { frame } + #[inline(always)] pub fn frame_str(&self, body_theme: ColorSet) -> String { - self.compose(body_theme).concat_for(self) + self.make(self.compose(body_theme)) } // compose generates a collection of components for drawing @@ -240,13 +232,10 @@ impl Frame { let (w_len, h_len) = self.size(); let frame_theme = Component::Internal(self.theme.to_string()); - let body_clear = Component::Internal( - Into::::into(vec![ - body_theme.clone(), - Component::Padding(w_len), - ]) - .concat_for(self), - ); + let body_clear = + Component::Internal(self.make(Into::::into( + vec![body_theme.clone(), Component::Padding(w_len)], + ))); let border = Component::Repeated(FRAME_CHAR_VERTICAL, self.border);