use crate::{
markdown::{
elements::Text,
text::{WeightedLine, WeightedText},
text_style::{Color, Colors, TextStyle},
},
render::{RenderError, RenderResult, layout::Positioning},
terminal::printer::{TerminalCommand, TerminalIo},
};
pub(crate) struct TextDrawer<'a> {
prefix: &'a WeightedText,
right_padding_length: u16,
line: &'a WeightedLine,
positioning: Positioning,
prefix_width: u16,
default_colors: &'a Colors,
draw_block: bool,
block_color: Option<Color>,
repeat_prefix: bool,
center_newlines: bool,
}
impl<'a> TextDrawer<'a> {
pub(crate) fn new(
prefix: &'a WeightedText,
right_padding_length: u16,
line: &'a WeightedLine,
positioning: Positioning,
default_colors: &'a Colors,
minimum_line_length: u16,
) -> Result<Self, RenderError> {
let text_length = (line.width() + prefix.width() + right_padding_length as usize) as u16;
if text_length > positioning.max_line_length && positioning.max_line_length <= minimum_line_length {
return Err(RenderError::TerminalTooSmall);
}
let prefix_width = prefix.width() as u16;
let positioning = Positioning {
max_line_length: positioning
.max_line_length
.saturating_sub(prefix_width)
.saturating_sub(right_padding_length),
start_column: positioning.start_column,
};
Ok(Self {
prefix,
right_padding_length,
line,
positioning,
prefix_width,
default_colors,
draw_block: false,
block_color: None,
repeat_prefix: false,
center_newlines: false,
})
}
pub(crate) fn with_surrounding_block(mut self, block_color: Option<Color>) -> Self {
self.draw_block = true;
self.block_color = block_color;
self
}
pub(crate) fn repeat_prefix_on_wrap(mut self, value: bool) -> Self {
self.repeat_prefix = value;
self
}
pub(crate) fn center_newlines(mut self, value: bool) -> Self {
self.center_newlines = value;
self
}
pub(crate) fn draw<T>(self, terminal: &mut T) -> RenderResult
where
T: TerminalIo,
{
let mut line_length: u16 = 0;
terminal.execute(&TerminalCommand::MoveToColumn(self.positioning.start_column))?;
let font_size = self.line.font_size();
if self.prefix_width > 0 {
let Text { content, style } = self.prefix.text();
terminal.execute(&TerminalCommand::PrintText { content, style: *style })?;
}
for (line_index, line) in self.line.split(self.positioning.max_line_length as usize).enumerate() {
if line_index > 0 {
self.print_block_background(line_length, terminal)?;
terminal.execute(&TerminalCommand::MoveDown(font_size as u16))?;
let start_column = match self.center_newlines {
true => {
let line_width = line.iter().map(|l| l.width()).sum::<usize>() as u16;
let extra_space = self.positioning.max_line_length.saturating_sub(line_width);
self.positioning.start_column + extra_space / 2
}
false => self.positioning.start_column,
};
terminal.execute(&TerminalCommand::MoveToColumn(start_column))?;
line_length = 0;
if self.prefix_width > 0 {
if self.repeat_prefix {
let Text { content, style } = self.prefix.text();
terminal.execute(&TerminalCommand::PrintText { content, style: *style })?;
} else {
if let Some(color) = self.block_color {
terminal.execute(&TerminalCommand::SetBackgroundColor(color))?;
}
let text = " ".repeat(self.prefix_width as usize / font_size as usize);
let style = TextStyle::default().size(font_size);
terminal.execute(&TerminalCommand::PrintText { content: &text, style })?;
}
}
}
for chunk in line {
line_length = line_length.saturating_add(chunk.width() as u16);
let (text, style) = chunk.into_parts();
terminal.execute(&TerminalCommand::PrintText { content: text, style })?;
if style != Default::default() {
terminal.execute(&TerminalCommand::SetColors(*self.default_colors))?;
}
}
}
self.print_block_background(line_length, terminal)?;
Ok(())
}
fn print_block_background<T>(&self, line_length: u16, terminal: &mut T) -> RenderResult
where
T: TerminalIo,
{
if self.draw_block {
let remaining =
self.positioning.max_line_length.saturating_sub(line_length).saturating_add(self.right_padding_length);
if remaining > 0 {
let font_size = self.line.font_size();
if let Some(color) = self.block_color {
terminal.execute(&TerminalCommand::SetBackgroundColor(color))?;
}
let text = " ".repeat(remaining as usize / font_size as usize);
let style = TextStyle::default().size(font_size);
terminal.execute(&TerminalCommand::PrintText { content: &text, style })?;
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::terminal::printer::TerminalError;
use std::io;
use unicode_width::UnicodeWidthStr;
#[derive(Debug, PartialEq)]
enum Instruction {
MoveDown(u16),
MoveToColumn(u16),
PrintText { content: String, font_size: u8 },
}
#[derive(Default)]
struct TerminalBuf {
instructions: Vec<Instruction>,
cursor_row: u16,
}
impl TerminalBuf {
fn push(&mut self, instruction: Instruction) -> io::Result<()> {
self.instructions.push(instruction);
Ok(())
}
fn move_to_column(&mut self, column: u16) -> std::io::Result<()> {
self.push(Instruction::MoveToColumn(column))
}
fn move_down(&mut self, amount: u16) -> std::io::Result<()> {
self.push(Instruction::MoveDown(amount))
}
fn print_text(&mut self, content: &str, style: &TextStyle) -> io::Result<()> {
let content = content.to_string();
if content.is_empty() {
return Ok(());
}
self.cursor_row = content.width() as u16;
self.push(Instruction::PrintText { content, font_size: style.size })?;
Ok(())
}
fn clear_screen(&mut self) -> std::io::Result<()> {
unimplemented!()
}
fn set_colors(&mut self, _colors: Colors) -> std::io::Result<()> {
Ok(())
}
fn set_background_color(&mut self, _color: Color) -> std::io::Result<()> {
Ok(())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
impl TerminalIo for TerminalBuf {
fn execute(&mut self, command: &TerminalCommand<'_>) -> Result<(), TerminalError> {
use TerminalCommand::*;
match command {
BeginUpdate
| EndUpdate
| MoveToRow(_)
| MoveToNextLine
| MoveTo { .. }
| MoveRight(_)
| MoveLeft(_)
| PrintImage { .. }
| SetCursorBoundaries { .. } => {
unimplemented!()
}
MoveToColumn(column) => self.move_to_column(*column)?,
MoveDown(amount) => self.move_down(*amount)?,
PrintText { content, style } => self.print_text(content, style)?,
ClearScreen => self.clear_screen()?,
SetColors(colors) => self.set_colors(*colors)?,
SetBackgroundColor(color) => self.set_background_color(*color)?,
Flush => self.flush()?,
};
Ok(())
}
fn cursor_row(&self) -> u16 {
self.cursor_row
}
}
struct TestDrawer {
prefix: WeightedText,
positioning: Positioning,
right_padding_length: u16,
repeat_prefix_on_wrap: bool,
center_newlines: bool,
}
impl TestDrawer {
fn prefix<T: Into<WeightedText>>(mut self, prefix: T) -> Self {
self.prefix = prefix.into();
self
}
fn start_column(mut self, column: u16) -> Self {
self.positioning.start_column = column;
self
}
fn max_line_length(mut self, length: u16) -> Self {
self.positioning.max_line_length = length;
self
}
fn repeat_prefix_on_wrap(mut self) -> Self {
self.repeat_prefix_on_wrap = true;
self
}
fn center_newlines(mut self) -> Self {
self.center_newlines = true;
self
}
fn draw<L: Into<WeightedLine>>(self, line: L) -> Vec<Instruction> {
let line = line.into();
let colors = Default::default();
let drawer = TextDrawer::new(&self.prefix, self.right_padding_length, &line, self.positioning, &colors, 0)
.expect("failed to create drawer")
.repeat_prefix_on_wrap(self.repeat_prefix_on_wrap)
.center_newlines(self.center_newlines);
let mut buf = TerminalBuf::default();
drawer.draw(&mut buf).expect("drawing failed");
buf.instructions
}
}
impl Default for TestDrawer {
fn default() -> Self {
Self {
prefix: WeightedText::from(""),
positioning: Positioning { max_line_length: 100, start_column: 0 },
right_padding_length: 0,
repeat_prefix_on_wrap: false,
center_newlines: false,
}
}
}
#[test]
fn prefix_on_long_line() {
let instructions = TestDrawer::default().prefix("P").max_line_length(3).start_column(1).draw("AAAA");
let expected = &[
Instruction::MoveToColumn(1),
Instruction::PrintText { content: "P".into(), font_size: 1 },
Instruction::PrintText { content: "AA".into(), font_size: 1 },
Instruction::MoveDown(1),
Instruction::MoveToColumn(1),
Instruction::PrintText { content: " ".into(), font_size: 1 },
Instruction::PrintText { content: "AA".into(), font_size: 1 },
];
assert_eq!(instructions, expected);
}
#[test]
fn prefix_on_long_line_with_font_size() {
let text = WeightedLine::from(vec![Text::new("AAAA", TextStyle::default().size(2))]);
let prefix = WeightedText::from(Text::new("P", TextStyle::default().size(2)));
let instructions = TestDrawer::default().prefix(prefix).max_line_length(6).start_column(1).draw(text);
let expected = &[
Instruction::MoveToColumn(1),
Instruction::PrintText { content: "P".into(), font_size: 2 },
Instruction::PrintText { content: "AA".into(), font_size: 2 },
Instruction::MoveDown(2),
Instruction::MoveToColumn(1),
Instruction::PrintText { content: " ".into(), font_size: 2 },
Instruction::PrintText { content: "AA".into(), font_size: 2 },
];
assert_eq!(instructions, expected);
}
#[test]
fn prefix_on_long_line_with_font_size_and_repeat_prefix() {
let text = WeightedLine::from(vec![Text::new("AAAA", TextStyle::default().size(2))]);
let prefix = WeightedText::from(Text::new("P", TextStyle::default().size(2)));
let instructions =
TestDrawer::default().prefix(prefix).max_line_length(6).start_column(1).repeat_prefix_on_wrap().draw(text);
let expected = &[
Instruction::MoveToColumn(1),
Instruction::PrintText { content: "P".into(), font_size: 2 },
Instruction::PrintText { content: "AA".into(), font_size: 2 },
Instruction::MoveDown(2),
Instruction::MoveToColumn(1),
Instruction::PrintText { content: "P".into(), font_size: 2 },
Instruction::PrintText { content: "AA".into(), font_size: 2 },
];
assert_eq!(instructions, expected);
}
#[test]
fn center_newlines() {
let text = WeightedLine::from(vec![Text::from("hello world foo")]);
let instructions = TestDrawer::default().center_newlines().max_line_length(11).draw(text);
let expected = &[
Instruction::MoveToColumn(0),
Instruction::PrintText { content: "hello world".into(), font_size: 1 },
Instruction::MoveDown(1),
Instruction::MoveToColumn(4),
Instruction::PrintText { content: "foo".into(), font_size: 1 },
];
assert_eq!(instructions, expected);
}
}