use super::{
RenderError, RenderResult, layout::Layout, operation::ImagePosition, properties::CursorPosition, text::TextDrawer,
};
use crate::{
config::{MaxColumnsAlignment, MaxRowsAlignment},
markdown::{text::WeightedLine, text_style::Colors},
render::{
operation::{
AsRenderOperations, BlockLine, ImageRenderProperties, ImageSize, LayoutGrid, MarginProperties, RenderAsync,
RenderOperation,
},
properties::WindowSize,
},
terminal::{
image::{
Image,
printer::{ImageProperties, PrintOptions},
scale::{ImageScaler, ScaleImage},
},
printer::{TerminalCommand, TerminalIo},
},
theme::Alignment,
};
use std::mem;
const MINIMUM_LINE_LENGTH: u16 = 10;
#[derive(Clone, Debug)]
pub(crate) struct MaxSize {
pub(crate) max_columns: u16,
pub(crate) max_columns_alignment: MaxColumnsAlignment,
pub(crate) max_rows: u16,
pub(crate) max_rows_alignment: MaxRowsAlignment,
}
impl Default for MaxSize {
fn default() -> Self {
Self {
max_columns: u16::MAX,
max_columns_alignment: Default::default(),
max_rows: u16::MAX,
max_rows_alignment: Default::default(),
}
}
}
#[derive(Clone, Debug)]
pub(crate) struct RenderEngineOptions {
pub(crate) validate_overflows: bool,
pub(crate) max_size: MaxSize,
pub(crate) column_layout_margin: u16,
}
impl Default for RenderEngineOptions {
fn default() -> Self {
Self { validate_overflows: false, max_size: Default::default(), column_layout_margin: 4 }
}
}
pub(crate) struct RenderEngine<'a, T>
where
T: TerminalIo,
{
terminal: &'a mut T,
window_rects: Vec<WindowRect>,
colors: Colors,
max_modified_row: u16,
layout: LayoutState,
options: RenderEngineOptions,
image_scaler: Box<dyn ScaleImage>,
}
impl<'a, T> RenderEngine<'a, T>
where
T: TerminalIo,
{
pub(crate) fn new(terminal: &'a mut T, window_dimensions: WindowSize, options: RenderEngineOptions) -> Self {
let max_modified_row = terminal.cursor_row();
let current_rect = Self::starting_rect(window_dimensions, &options);
let window_rects = vec![current_rect.clone()];
Self {
terminal,
window_rects,
colors: Default::default(),
max_modified_row,
layout: Default::default(),
options,
image_scaler: Box::<ImageScaler>::default(),
}
}
fn starting_rect(mut dimensions: WindowSize, options: &RenderEngineOptions) -> WindowRect {
let mut start_row = 0;
let mut start_column = 0;
if dimensions.columns > options.max_size.max_columns {
let extra_width = dimensions.columns - options.max_size.max_columns;
dimensions = dimensions.shrink_columns(extra_width);
start_column = match options.max_size.max_columns_alignment {
MaxColumnsAlignment::Left => 0,
MaxColumnsAlignment::Center => extra_width / 2,
MaxColumnsAlignment::Right => extra_width,
};
}
if dimensions.rows > options.max_size.max_rows {
let extra_height = dimensions.rows - options.max_size.max_rows;
dimensions = dimensions.shrink_rows(extra_height);
start_row = match options.max_size.max_rows_alignment {
MaxRowsAlignment::Top => 0,
MaxRowsAlignment::Center => extra_height / 2,
MaxRowsAlignment::Bottom => extra_height,
};
}
WindowRect { dimensions, start_column, start_row }
}
pub(crate) fn render<'b>(mut self, operations: impl Iterator<Item = &'b RenderOperation>) -> RenderResult {
let current_rect = self.current_rect().clone();
self.terminal.execute(&TerminalCommand::SetCursorBoundaries {
rows: current_rect.dimensions.rows.saturating_add(current_rect.start_row),
})?;
self.terminal.execute(&TerminalCommand::BeginUpdate)?;
if current_rect.start_row != 0 || current_rect.start_column != 0 {
self.terminal
.execute(&TerminalCommand::MoveTo { column: current_rect.start_column, row: current_rect.start_row })?;
}
for operation in operations {
self.render_one(operation)?;
}
self.terminal.execute(&TerminalCommand::EndUpdate)?;
self.terminal.execute(&TerminalCommand::Flush)?;
if self.options.validate_overflows && self.max_modified_row > self.window_rects[0].dimensions.rows {
return Err(RenderError::VerticalOverflow);
}
Ok(())
}
fn render_one(&mut self, operation: &RenderOperation) -> RenderResult {
match operation {
RenderOperation::ClearScreen => self.clear_screen(),
RenderOperation::ApplyMargin(properties) => self.apply_margin(properties),
RenderOperation::PopMargin => self.pop_margin(),
RenderOperation::SetColors(colors) => self.set_colors(colors),
RenderOperation::JumpToVerticalCenter => self.jump_to_vertical_center(),
RenderOperation::JumpToRow { index } => self.jump_to_row(*index),
RenderOperation::JumpToBottomRow { index } => self.jump_to_bottom(*index),
RenderOperation::JumpToColumn { index } => self.jump_to_column(*index),
RenderOperation::RenderText { line, alignment } => self.render_text(line, *alignment),
RenderOperation::RenderLineBreak => self.render_line_break(),
RenderOperation::RenderImage(image, properties) => self.render_image(image, properties),
RenderOperation::RenderBlockLine(operation) => self.render_block_line(operation),
RenderOperation::RenderDynamic(generator) => self.render_dynamic(generator.as_ref()),
RenderOperation::RenderDynamicTopLevel(generator) => self.render_dynamic_top_level(generator.as_ref()),
RenderOperation::RenderAsync(generator) => self.render_async(generator.as_ref()),
RenderOperation::InitColumnLayout { columns, grid } => self.init_column_layout(columns, *grid),
RenderOperation::EnterColumn { column } => self.enter_column(*column),
RenderOperation::ExitLayout => self.exit_layout(),
}?;
if let LayoutState::EnteredColumn { column, columns, .. } = &mut self.layout {
columns[*column].current_row = self.terminal.cursor_row();
};
self.max_modified_row = self.max_modified_row.max(self.terminal.cursor_row());
Ok(())
}
fn current_rect(&self) -> &WindowRect {
self.window_rects.last().expect("no rects")
}
fn current_dimensions(&self) -> &WindowSize {
&self.current_rect().dimensions
}
fn current_available_dimensions(&self) -> WindowSize {
let rect = self.current_rect();
rect.dimensions.shrink_rows(self.terminal.cursor_row())
}
fn clear_screen(&mut self) -> RenderResult {
let current = self.current_rect().clone();
self.terminal.execute(&TerminalCommand::ClearScreen)?;
self.terminal.execute(&TerminalCommand::MoveTo { column: current.start_column, row: current.start_row })?;
self.max_modified_row = 0;
Ok(())
}
fn apply_margin(&mut self, properties: &MarginProperties) -> RenderResult {
let MarginProperties { horizontal: horizontal_margin, top, bottom } = properties;
let current = self.current_rect();
let margin = horizontal_margin.as_characters(current.dimensions.columns);
let new_rect = current.shrink_horizontal(margin).shrink_bottom(*bottom).shrink_top(*top);
if new_rect.start_row != self.terminal.cursor_row() {
self.terminal.execute(&TerminalCommand::MoveToRow(new_rect.start_row))?;
}
self.window_rects.push(new_rect);
Ok(())
}
fn pop_margin(&mut self) -> RenderResult {
if self.window_rects.len() == 1 {
return Err(RenderError::PopDefaultScreen);
}
self.window_rects.pop();
Ok(())
}
fn set_colors(&mut self, colors: &Colors) -> RenderResult {
self.colors = *colors;
self.apply_colors()
}
fn apply_colors(&mut self) -> RenderResult {
self.terminal.execute(&TerminalCommand::SetColors(self.colors))?;
Ok(())
}
fn jump_to_vertical_center(&mut self) -> RenderResult {
let current = self.current_rect();
let center_row = current.dimensions.rows / 2;
let center_row = center_row.saturating_add(current.start_row);
self.terminal.execute(&TerminalCommand::MoveToRow(center_row))?;
Ok(())
}
fn jump_to_row(&mut self, row: u16) -> RenderResult {
let row = self.current_rect().start_row.saturating_add(row);
self.terminal.execute(&TerminalCommand::MoveToRow(row))?;
Ok(())
}
fn jump_to_bottom(&mut self, index: u16) -> RenderResult {
let current = self.current_rect();
let target_row = current.dimensions.rows.saturating_sub(index).saturating_sub(1);
let target_row = target_row.saturating_add(current.start_row);
self.terminal.execute(&TerminalCommand::MoveToRow(target_row))?;
Ok(())
}
fn jump_to_column(&mut self, column: u16) -> RenderResult {
let column = self.current_rect().start_column.saturating_add(column);
self.terminal.execute(&TerminalCommand::MoveToColumn(column))?;
Ok(())
}
fn render_text(&mut self, text: &WeightedLine, alignment: Alignment) -> RenderResult {
let layout = self.build_layout(alignment);
let dimensions = self.current_dimensions();
let positioning = layout.compute(dimensions, text.width() as u16);
let prefix = "".into();
let text_drawer = TextDrawer::new(&prefix, 0, text, positioning, &self.colors, MINIMUM_LINE_LENGTH)?;
let center_newlines = matches!(alignment, Alignment::Center { .. });
let text_drawer = text_drawer.center_newlines(center_newlines);
text_drawer.draw(self.terminal)?;
self.apply_colors()
}
fn render_line_break(&mut self) -> RenderResult {
self.terminal.execute(&TerminalCommand::MoveToNextLine)?;
Ok(())
}
fn render_image(&mut self, image: &Image, properties: &ImageRenderProperties) -> RenderResult {
let rect = self.current_rect().clone();
let starting_row = self.terminal.cursor_row();
let starting_cursor =
CursorPosition { row: starting_row.saturating_sub(rect.start_row), column: rect.start_column };
let (width, height) = image.image().dimensions();
let (columns, rows) = match properties.size {
ImageSize::ShrinkIfNeeded => {
let image_scale =
self.image_scaler.fit_image_to_rect(&rect.dimensions, width, height, &starting_cursor);
(image_scale.columns, image_scale.rows)
}
ImageSize::Specific(columns, rows) => (columns, rows),
ImageSize::WidthScaled { ratio } => {
let extra_columns = (rect.dimensions.columns as f64 * (1.0 - ratio)).ceil() as u16;
let dimensions = rect.dimensions.shrink_columns(extra_columns);
let image_scale =
self.image_scaler.scale_image(&dimensions, &rect.dimensions, width, height, &starting_cursor);
(image_scale.columns, image_scale.rows)
}
};
let cursor = match &properties.position {
ImagePosition::Cursor => starting_cursor.clone(),
ImagePosition::Center => Self::center_cursor(columns, &rect.dimensions, &starting_cursor),
ImagePosition::Right => Self::align_cursor_right(columns, &rect.dimensions, &starting_cursor),
};
self.terminal.execute(&TerminalCommand::MoveToColumn(cursor.column))?;
let options = PrintOptions {
columns,
rows,
z_index: properties.z_index,
column_width: rect.dimensions.pixels_per_column() as u16,
row_height: rect.dimensions.pixels_per_row() as u16,
background_color: properties.background_color,
};
self.terminal.execute(&TerminalCommand::PrintImage { image: image.clone(), options })?;
if properties.restore_cursor {
self.terminal.execute(&TerminalCommand::MoveTo { column: starting_cursor.column, row: starting_row })?;
} else {
self.terminal.execute(&TerminalCommand::MoveToRow(starting_row + rows))?;
}
self.apply_colors()
}
fn center_cursor(columns: u16, window: &WindowSize, cursor: &CursorPosition) -> CursorPosition {
let start_column = window.columns / 2 - (columns / 2);
let start_column = start_column + cursor.column;
CursorPosition { row: cursor.row, column: start_column }
}
fn align_cursor_right(columns: u16, window: &WindowSize, cursor: &CursorPosition) -> CursorPosition {
let start_column = window.columns.saturating_sub(columns).saturating_add(cursor.column);
CursorPosition { row: cursor.row, column: start_column }
}
fn render_block_line(&mut self, operation: &BlockLine) -> RenderResult {
let BlockLine {
text,
block_length,
alignment,
block_color,
prefix,
right_padding_length,
repeat_prefix_on_wrap,
} = operation;
let layout = self.build_layout(*alignment).with_font_size(text.font_size());
let dimensions = self.current_dimensions();
let positioning = layout.compute(dimensions, *block_length);
if self.options.validate_overflows && text.width() as u16 > positioning.max_line_length {
return Err(RenderError::HorizontalOverflow);
}
self.terminal.execute(&TerminalCommand::MoveToColumn(positioning.start_column))?;
let text_drawer =
TextDrawer::new(prefix, *right_padding_length, text, positioning, &self.colors, MINIMUM_LINE_LENGTH)?
.with_surrounding_block(*block_color)
.repeat_prefix_on_wrap(*repeat_prefix_on_wrap);
text_drawer.draw(self.terminal)?;
self.apply_colors()?;
Ok(())
}
fn render_dynamic(&mut self, generator: &dyn AsRenderOperations) -> RenderResult {
let operations = generator.as_render_operations(&self.current_available_dimensions());
for operation in operations {
self.render_one(&operation)?;
}
Ok(())
}
fn render_dynamic_top_level(&mut self, generator: &dyn AsRenderOperations) -> RenderResult {
let dimensions = self.window_rects.first().expect("no rects").dimensions;
let operations = generator.as_render_operations(&dimensions);
for operation in operations {
self.render_one(&operation)?;
}
Ok(())
}
fn render_async(&mut self, generator: &dyn RenderAsync) -> RenderResult {
let operations = generator.as_render_operations(&self.current_available_dimensions());
for operation in operations {
self.render_one(&operation)?;
}
Ok(())
}
fn init_column_layout(&mut self, widths: &[u8], grid: LayoutGrid) -> RenderResult {
if !matches!(self.layout, LayoutState::Default) {
self.exit_layout()?;
}
let current_row = self.terminal.cursor_row();
let rect = self.current_rect();
let total_units = widths.iter().copied().map(u16::from).sum::<u16>();
let total_columns = rect.dimensions.columns;
let unit_columns = total_columns as f64 / total_units as f64;
let mut columns = Vec::new();
let mut current_column = rect.start_column;
for width in widths {
let width = (f64::from(*width) * unit_columns) as u16;
let end_column = current_column + width;
columns.push(Column { start_column: current_column, end_column, current_row });
current_column = end_column;
}
self.layout = LayoutState::InitializedColumn { columns, grid, start_row: current_row };
Ok(())
}
fn enter_column(&mut self, column_index: usize) -> RenderResult {
let (columns, grid, start_row) = match mem::take(&mut self.layout) {
LayoutState::Default => return Err(RenderError::InvalidLayoutEnter),
LayoutState::InitializedColumn { columns, .. } | LayoutState::EnteredColumn { columns, .. }
if column_index >= columns.len() =>
{
return Err(RenderError::InvalidLayoutEnter);
}
LayoutState::InitializedColumn { columns, grid, start_row } => (columns, grid, start_row),
LayoutState::EnteredColumn { columns, grid, start_row, .. } => {
self.pop_margin()?;
(columns, grid, start_row)
}
};
let column = &columns[column_index];
let current_rect = self.current_rect();
let total_columns = column.end_column.saturating_sub(column.start_column);
let new_size = current_rect
.dimensions
.set_columns(total_columns)
.shrink_rows(column.current_row.saturating_sub(current_rect.start_row));
let mut dimensions =
WindowRect { dimensions: new_size, start_column: column.start_column, start_row: column.current_row };
if column_index == 0 {
dimensions = dimensions.shrink_right(self.options.column_layout_margin);
} else if column_index == columns.len() - 1 {
dimensions = dimensions.shrink_left(self.options.column_layout_margin);
} else {
let margin = self.options.column_layout_margin / 2;
dimensions = dimensions.shrink_left(margin).shrink_right(margin);
}
self.window_rects.push(dimensions);
self.terminal.execute(&TerminalCommand::MoveToRow(column.current_row))?;
self.layout = LayoutState::EnteredColumn { column: column_index, columns, grid, start_row };
Ok(())
}
fn exit_layout(&mut self) -> RenderResult {
match &self.layout {
LayoutState::Default => return Ok(()),
LayoutState::InitializedColumn { columns, grid, start_row }
| LayoutState::EnteredColumn { columns, grid, start_row, .. } => {
if let LayoutGrid::Draw(style) = grid {
let style = *style;
let max_row = columns.iter().map(|c| c.current_row).max().unwrap_or(0);
for row in *start_row..=max_row {
self.terminal.execute(&TerminalCommand::MoveToRow(row))?;
for column in columns.iter().skip(1) {
self.terminal.execute(&TerminalCommand::MoveToColumn(column.start_column))?;
self.terminal.execute(&TerminalCommand::PrintText { content: "│", style })?;
}
}
self.apply_colors()?;
}
}
};
match &self.layout {
LayoutState::Default | LayoutState::InitializedColumn { .. } => Ok(()),
LayoutState::EnteredColumn { .. } => {
self.terminal.execute(&TerminalCommand::MoveTo { column: 0, row: self.max_modified_row })?;
self.layout = LayoutState::Default;
self.pop_margin()?;
Ok(())
}
}
}
fn build_layout(&self, alignment: Alignment) -> Layout {
Layout::new(alignment).with_start_column(self.current_rect().start_column)
}
}
#[derive(Default)]
enum LayoutState {
#[default]
Default,
InitializedColumn {
start_row: u16,
columns: Vec<Column>,
grid: LayoutGrid,
},
EnteredColumn {
start_row: u16,
column: usize,
columns: Vec<Column>,
grid: LayoutGrid,
},
}
#[derive(Debug)]
struct Column {
start_column: u16,
end_column: u16,
current_row: u16,
}
#[derive(Clone, Debug)]
struct WindowRect {
dimensions: WindowSize,
start_column: u16,
start_row: u16,
}
impl WindowRect {
fn shrink_horizontal(&self, margin: u16) -> Self {
let dimensions = self.dimensions.shrink_columns(margin.saturating_mul(2));
let start_column = self.start_column + margin;
Self { dimensions, start_column, start_row: self.start_row }
}
fn shrink_left(&self, size: u16) -> Self {
let dimensions = self.dimensions.shrink_columns(size);
let start_column = self.start_column.saturating_add(size);
Self { dimensions, start_column, start_row: self.start_row }
}
fn shrink_right(&self, size: u16) -> Self {
let dimensions = self.dimensions.shrink_columns(size);
Self { dimensions, start_column: self.start_column, start_row: self.start_row }
}
fn shrink_top(&self, rows: u16) -> Self {
let dimensions = self.dimensions.shrink_rows(rows);
let start_row = self.start_row.saturating_add(rows);
Self { dimensions, start_column: self.start_column, start_row }
}
fn shrink_bottom(&self, rows: u16) -> Self {
let dimensions = self.dimensions.shrink_rows(rows);
Self { dimensions, start_column: self.start_column, start_row: self.start_row }
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
markdown::text_style::{Color, TextStyle},
terminal::{
image::{
ImageSource,
printer::{PrintImageError, TerminalImage},
scale::TerminalRect,
},
printer::TerminalError,
},
theme::Margin,
};
use ::image::{ColorType, DynamicImage};
use rstest::rstest;
use std::io;
use unicode_width::UnicodeWidthStr;
#[derive(Debug, PartialEq)]
enum Instruction {
MoveTo(u16, u16),
MoveToRow(u16),
MoveToColumn(u16),
MoveDown(u16),
MoveRight(u16),
MoveLeft(u16),
MoveToNextLine,
PrintText(String),
ClearScreen,
SetBackgroundColor(Color),
PrintImage(PrintOptions),
}
#[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(&mut self, column: u16, row: u16) -> io::Result<()> {
self.cursor_row = row;
self.push(Instruction::MoveTo(column, row))
}
fn move_to_row(&mut self, row: u16) -> io::Result<()> {
self.cursor_row = row;
self.push(Instruction::MoveToRow(row))
}
fn move_to_column(&mut self, column: u16) -> io::Result<()> {
self.push(Instruction::MoveToColumn(column))
}
fn move_down(&mut self, amount: u16) -> io::Result<()> {
self.push(Instruction::MoveDown(amount))
}
fn move_right(&mut self, amount: u16) -> io::Result<()> {
self.push(Instruction::MoveRight(amount))
}
fn move_left(&mut self, amount: u16) -> io::Result<()> {
self.push(Instruction::MoveLeft(amount))
}
fn move_to_next_line(&mut self) -> io::Result<()> {
self.push(Instruction::MoveToNextLine)
}
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))
}
fn clear_screen(&mut self) -> io::Result<()> {
self.cursor_row = 0;
self.push(Instruction::ClearScreen)
}
fn set_colors(&mut self, _colors: Colors) -> io::Result<()> {
Ok(())
}
fn set_background_color(&mut self, color: Color) -> io::Result<()> {
self.push(Instruction::SetBackgroundColor(color))
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
fn print_image(&mut self, _image: &Image, options: &PrintOptions) -> Result<(), PrintImageError> {
let _ = self.push(Instruction::PrintImage(options.clone()));
Ok(())
}
}
impl TerminalIo for TerminalBuf {
fn execute(&mut self, command: &TerminalCommand<'_>) -> Result<(), TerminalError> {
use TerminalCommand::*;
match command {
BeginUpdate => (),
EndUpdate => (),
MoveTo { column, row } => self.move_to(*column, *row)?,
MoveToRow(row) => self.move_to_row(*row)?,
MoveToColumn(column) => self.move_to_column(*column)?,
MoveDown(amount) => self.move_down(*amount)?,
MoveRight(amount) => self.move_right(*amount)?,
MoveLeft(amount) => self.move_left(*amount)?,
MoveToNextLine => self.move_to_next_line()?,
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()?,
PrintImage { image, options } => self.print_image(image, options)?,
SetCursorBoundaries { .. } => (),
};
Ok(())
}
fn cursor_row(&self) -> u16 {
self.cursor_row
}
}
struct DummyImageScaler;
impl ScaleImage for DummyImageScaler {
fn scale_image(
&self,
_scale_size: &WindowSize,
_window_dimensions: &WindowSize,
image_width: u32,
image_height: u32,
_position: &CursorPosition,
) -> TerminalRect {
TerminalRect { rows: image_width as u16, columns: image_height as u16 }
}
fn fit_image_to_rect(
&self,
_dimensions: &WindowSize,
image_width: u32,
image_height: u32,
_position: &CursorPosition,
) -> TerminalRect {
TerminalRect { rows: image_width as u16, columns: image_height as u16 }
}
}
fn do_render(max_size: MaxSize, operations: &[RenderOperation]) -> Vec<Instruction> {
let mut buf = TerminalBuf::default();
let dimensions = WindowSize { rows: 100, columns: 100, height: 200, width: 200 };
let options = RenderEngineOptions { validate_overflows: false, max_size, column_layout_margin: 0 };
let mut engine = RenderEngine::new(&mut buf, dimensions, options);
engine.image_scaler = Box::new(DummyImageScaler);
engine.render(operations.iter()).expect("render failed");
buf.instructions
}
fn render(operations: &[RenderOperation]) -> Vec<Instruction> {
do_render(Default::default(), operations)
}
fn render_with_max_size(operations: &[RenderOperation]) -> Vec<Instruction> {
let max_size = MaxSize {
max_rows: 10,
max_rows_alignment: MaxRowsAlignment::Center,
max_columns: 20,
max_columns_alignment: MaxColumnsAlignment::Center,
};
do_render(max_size, operations)
}
#[test]
fn columns() {
let ops = render(&[
RenderOperation::InitColumnLayout { columns: vec![1, 1], grid: LayoutGrid::None },
RenderOperation::EnterColumn { column: 0 },
RenderOperation::RenderText { line: "A".into(), alignment: Alignment::Left { margin: Margin::Fixed(0) } },
RenderOperation::EnterColumn { column: 1 },
RenderOperation::RenderText { line: "B".into(), alignment: Alignment::Left { margin: Margin::Fixed(0) } },
RenderOperation::EnterColumn { column: 0 },
RenderOperation::RenderText { line: "1".into(), alignment: Alignment::Left { margin: Margin::Fixed(0) } },
]);
let expected = [
Instruction::MoveToRow(0),
Instruction::MoveToColumn(0),
Instruction::PrintText("A".into()),
Instruction::MoveToRow(0),
Instruction::MoveToColumn(50),
Instruction::PrintText("B".into()),
Instruction::MoveToRow(1),
Instruction::MoveToColumn(0),
Instruction::PrintText("1".into()),
];
assert_eq!(ops, expected);
}
#[test]
fn bottom_margin() {
let ops = render(&[
RenderOperation::ApplyMargin(MarginProperties { horizontal: Margin::Fixed(1), top: 0, bottom: 10 }),
RenderOperation::RenderText { line: "A".into(), alignment: Alignment::Left { margin: Margin::Fixed(0) } },
RenderOperation::JumpToBottomRow { index: 0 },
RenderOperation::RenderText { line: "B".into(), alignment: Alignment::Left { margin: Margin::Fixed(0) } },
]);
let expected = [
Instruction::MoveToColumn(1),
Instruction::PrintText("A".into()),
Instruction::MoveToRow(89),
Instruction::MoveToColumn(1),
Instruction::PrintText("B".into()),
];
assert_eq!(ops, expected);
}
#[test]
fn top_margin() {
let ops = render(&[
RenderOperation::ApplyMargin(MarginProperties { horizontal: Margin::Fixed(1), top: 3, bottom: 0 }),
RenderOperation::RenderText { line: "A".into(), alignment: Alignment::Left { margin: Margin::Fixed(0) } },
]);
let expected = [Instruction::MoveToRow(3), Instruction::MoveToColumn(1), Instruction::PrintText("A".into())];
assert_eq!(ops, expected);
}
#[test]
fn margins() {
let ops = render(&[
RenderOperation::ApplyMargin(MarginProperties { horizontal: Margin::Fixed(1), top: 3, bottom: 10 }),
RenderOperation::JumpToRow { index: 0 },
RenderOperation::RenderText { line: "A".into(), alignment: Alignment::Left { margin: Margin::Fixed(0) } },
RenderOperation::JumpToBottomRow { index: 0 },
RenderOperation::RenderText { line: "B".into(), alignment: Alignment::Left { margin: Margin::Fixed(0) } },
]);
let expected = [
Instruction::MoveToRow(3),
Instruction::MoveToRow(3),
Instruction::MoveToColumn(1),
Instruction::PrintText("A".into()),
Instruction::MoveToRow(89),
Instruction::MoveToColumn(1),
Instruction::PrintText("B".into()),
];
assert_eq!(ops, expected);
}
#[test]
fn nested_margins() {
let ops = render(&[
RenderOperation::ApplyMargin(MarginProperties { horizontal: Margin::Fixed(1), top: 0, bottom: 10 }),
RenderOperation::ApplyMargin(MarginProperties { horizontal: Margin::Fixed(1), top: 0, bottom: 10 }),
RenderOperation::RenderText { line: "A".into(), alignment: Alignment::Left { margin: Margin::Fixed(0) } },
RenderOperation::JumpToBottomRow { index: 0 },
RenderOperation::RenderText { line: "B".into(), alignment: Alignment::Left { margin: Margin::Fixed(0) } },
RenderOperation::PopMargin,
RenderOperation::JumpToBottomRow { index: 0 },
RenderOperation::RenderText { line: "C".into(), alignment: Alignment::Left { margin: Margin::Fixed(0) } },
]);
let expected = [
Instruction::MoveToColumn(2),
Instruction::PrintText("A".into()),
Instruction::MoveToRow(79),
Instruction::MoveToColumn(2),
Instruction::PrintText("B".into()),
Instruction::MoveToRow(89),
Instruction::MoveToColumn(1),
Instruction::PrintText("C".into()),
];
assert_eq!(ops, expected);
}
#[test]
fn margin_with_max_size() {
let ops = render_with_max_size(&[
RenderOperation::RenderText { line: "A".into(), alignment: Alignment::Left { margin: Margin::Fixed(0) } },
RenderOperation::ApplyMargin(MarginProperties { horizontal: Margin::Fixed(1), top: 2, bottom: 1 }),
RenderOperation::RenderText { line: "B".into(), alignment: Alignment::Left { margin: Margin::Fixed(0) } },
RenderOperation::JumpToBottomRow { index: 0 },
RenderOperation::RenderText { line: "C".into(), alignment: Alignment::Left { margin: Margin::Fixed(0) } },
]);
let expected = [
Instruction::MoveTo(40, 45),
Instruction::MoveToColumn(40),
Instruction::PrintText("A".into()),
Instruction::MoveToRow(47),
Instruction::MoveToColumn(41),
Instruction::PrintText("B".into()),
Instruction::MoveToRow(53),
Instruction::MoveToColumn(41),
Instruction::PrintText("C".into()),
];
assert_eq!(ops, expected);
}
#[rstest]
#[case::shrink(ImageSize::ShrinkIfNeeded)]
#[case::specific(ImageSize::Specific(2, 2))]
#[case::width_scaled(ImageSize::WidthScaled { ratio: 1.0 })]
fn image(#[case] size: ImageSize) {
let image = DynamicImage::new(2, 2, ColorType::Rgba8);
let image = Image::new(TerminalImage::Ascii(image.into()), ImageSource::Generated);
let properties = ImageRenderProperties {
z_index: 0,
size,
restore_cursor: false,
background_color: None,
position: ImagePosition::Cursor,
};
let ops = render_with_max_size(&[RenderOperation::RenderImage(image, properties)]);
let expected = [
Instruction::MoveTo(40, 45),
Instruction::MoveToColumn(40),
Instruction::PrintImage(PrintOptions {
columns: 2,
rows: 2,
z_index: 0,
background_color: None,
column_width: 2,
row_height: 2,
}),
Instruction::MoveToRow(47),
];
assert_eq!(ops, expected);
}
#[rstest]
#[case::shrink(ImageSize::ShrinkIfNeeded)]
#[case::specific(ImageSize::Specific(2, 2))]
#[case::width_scaled(ImageSize::WidthScaled { ratio: 1.0 })]
fn centered_image(#[case] size: ImageSize) {
let image = DynamicImage::new(2, 2, ColorType::Rgba8);
let image = Image::new(TerminalImage::Ascii(image.into()), ImageSource::Generated);
let properties = ImageRenderProperties {
z_index: 0,
size,
restore_cursor: false,
background_color: None,
position: ImagePosition::Center,
};
let ops = render_with_max_size(&[RenderOperation::RenderImage(image, properties)]);
let expected = [
Instruction::MoveTo(40, 45),
Instruction::MoveToColumn(49),
Instruction::PrintImage(PrintOptions {
columns: 2,
rows: 2,
z_index: 0,
background_color: None,
column_width: 2,
row_height: 2,
}),
Instruction::MoveToRow(47),
];
assert_eq!(ops, expected);
}
#[rstest]
#[case::shrink(ImageSize::ShrinkIfNeeded)]
#[case::specific(ImageSize::Specific(2, 2))]
#[case::width_scaled(ImageSize::WidthScaled { ratio: 1.0 })]
fn right_aligned_image(#[case] size: ImageSize) {
let image = DynamicImage::new(2, 2, ColorType::Rgba8);
let image = Image::new(TerminalImage::Ascii(image.into()), ImageSource::Generated);
let properties = ImageRenderProperties {
z_index: 0,
size,
restore_cursor: false,
background_color: None,
position: ImagePosition::Right,
};
let ops = render_with_max_size(&[RenderOperation::RenderImage(image, properties)]);
let expected = [
Instruction::MoveTo(40, 45),
Instruction::MoveToColumn(58),
Instruction::PrintImage(PrintOptions {
columns: 2,
rows: 2,
z_index: 0,
background_color: None,
column_width: 2,
row_height: 2,
}),
Instruction::MoveToRow(47),
];
assert_eq!(ops, expected);
}
#[rstest]
fn restore_cursor_after_image() {
let image = DynamicImage::new(2, 2, ColorType::Rgba8);
let image = Image::new(TerminalImage::Ascii(image.into()), ImageSource::Generated);
let properties = ImageRenderProperties {
z_index: 0,
size: ImageSize::ShrinkIfNeeded,
restore_cursor: true,
background_color: None,
position: ImagePosition::Center,
};
let ops = render_with_max_size(&[RenderOperation::RenderImage(image, properties)]);
let expected = [
Instruction::MoveTo(40, 45),
Instruction::MoveToColumn(49),
Instruction::PrintImage(PrintOptions {
columns: 2,
rows: 2,
z_index: 0,
background_color: None,
column_width: 2,
row_height: 2,
}),
Instruction::MoveTo(40, 45),
];
assert_eq!(ops, expected);
}
}