kk/src/display/compose.rs

212 lines
5.3 KiB
Rust

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<String> 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,
body_width: u16,
y: u16,
) -> Components {
match self {
Self::Left => {
vec![Component::Goto(0, y), Component::String(s)]
}
Self::Right => {
vec![
Component::Goto(s.len() as u16 - body_width, y),
Component::String(s),
]
}
Self::Center => {
vec![
Component::Goto((body_width - s.len()) / 2, y),
Component::String(s),
]
}
}
.into()
}
}
#[derive(Clone, Debug)]
pub enum Component {
String(Text),
Goto(u16, u16),
Theme(ColorSet),
Padding(u16),
// Pre-formatted
Internal(String),
Repeated(char, u16),
Clear,
}
impl Component {
pub fn make(self, frame: &Frame) -> String {
match self {
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(),
Component::Padding(len) => " ".repeat(len as usize),
Component::Internal(i) => i,
Component::Repeated(ch, len) => {
ch.to_string().repeat(len as usize)
}
}
}
}
#[derive(Clone, Debug)]
pub struct Components(pub Vec<Component>);
impl Deref for Components {
type Target = Vec<Component>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<Vec<Component>> for Components {
fn from(value: Vec<Component>) -> Self {
Self(value)
}
}
impl From<Component> for Components {
fn from(value: Component) -> Self {
Self(vec![value])
}
}
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,
body_width: u16,
) -> 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, body_width, 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::<Vec<Component>>()
.into()
}
pub fn str_len(&self) -> u16 {
(&self.0)
.into_iter()
.map(|comp| {
if let Component::String(s) = comp {
s.len()
} else {
0
}
})
.sum()
}
}