use crate::console::{Console, ConsoleOptions, Renderable};
use crate::segment::{ControlCode, ControlType, Segment};
use crate::style::Style;
use crate::text::{JustifyMethod, OverflowMethod, Text};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum VerticalOverflowMethod {
Crop,
Ellipsis,
Visible,
}
pub struct LiveRender {
pub renderable: Text,
pub style: Style,
pub vertical_overflow: VerticalOverflowMethod,
shape: Option<(usize, usize)>,
}
impl LiveRender {
pub fn new(renderable: Text) -> Self {
LiveRender {
renderable,
style: Style::null(),
vertical_overflow: VerticalOverflowMethod::Ellipsis,
shape: None,
}
}
#[must_use]
pub fn with_style(mut self, style: Style) -> Self {
self.style = style;
self
}
#[must_use]
pub fn with_vertical_overflow(mut self, overflow: VerticalOverflowMethod) -> Self {
self.vertical_overflow = overflow;
self
}
pub fn last_render_height(&self) -> usize {
match self.shape {
Some((_, height)) => height,
None => 0,
}
}
pub fn set_renderable(&mut self, renderable: Text) {
self.renderable = renderable;
}
pub fn position_cursor(&self) -> Vec<Segment> {
let Some((_, height)) = self.shape else {
return Vec::new();
};
if height == 0 {
return Vec::new();
}
let mut codes: Vec<ControlCode> = Vec::new();
codes.push(ControlCode::Simple(ControlType::CarriageReturn));
codes.push(ControlCode::WithParam(ControlType::EraseInLine, 2));
for _ in 0..height.saturating_sub(1) {
codes.push(ControlCode::WithParam(ControlType::CursorUp, 1));
codes.push(ControlCode::WithParam(ControlType::EraseInLine, 2));
}
vec![Segment::new("", None, Some(codes))]
}
pub fn restore_cursor(&self) -> Vec<Segment> {
let Some((_, height)) = self.shape else {
return Vec::new();
};
if height == 0 {
return Vec::new();
}
let mut codes: Vec<ControlCode> = Vec::new();
codes.push(ControlCode::Simple(ControlType::CarriageReturn));
for _ in 0..height {
codes.push(ControlCode::WithParam(ControlType::CursorUp, 1));
codes.push(ControlCode::WithParam(ControlType::EraseInLine, 2));
}
vec![Segment::new("", None, Some(codes))]
}
}
impl Renderable for LiveRender {
fn rich_console(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
let style_ref = if self.style.is_null() {
None
} else {
Some(&self.style)
};
let mut lines =
console.render_lines(&self.renderable, Some(options), style_ref, false, false);
let (_, height) = Segment::get_shape(&lines);
let max_height = options.height.unwrap_or(options.size.height);
if height > max_height {
match self.vertical_overflow {
VerticalOverflowMethod::Crop => {
lines.truncate(max_height);
}
VerticalOverflowMethod::Ellipsis => {
let ellipsis_lines = if max_height > 0 { max_height - 1 } else { 0 };
lines.truncate(ellipsis_lines);
let mut overflow_text = Text::new("...", Style::null());
overflow_text.overflow = Some(OverflowMethod::Crop);
overflow_text.justify = Some(JustifyMethod::Center);
overflow_text.end = String::new();
let ellipsis_segments = console.render(&overflow_text, Some(options));
lines.push(ellipsis_segments);
}
VerticalOverflowMethod::Visible => {
}
}
}
let final_shape = Segment::get_shape(&lines);
#[allow(invalid_reference_casting)]
{
let self_mut = unsafe { &mut *(self as *const LiveRender as *mut LiveRender) };
self_mut.shape = Some(final_shape);
}
let mut segments = Vec::new();
let line_count = lines.len();
for (i, line) in lines.into_iter().enumerate() {
segments.extend(line);
if i + 1 < line_count {
segments.push(Segment::line());
}
}
segments
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_construction() {
let lr = LiveRender::new(Text::new("hello", Style::null()));
assert_eq!(lr.renderable.plain(), "hello");
assert!(lr.style.is_null());
assert_eq!(lr.vertical_overflow, VerticalOverflowMethod::Ellipsis);
assert!(lr.shape.is_none());
}
#[test]
fn test_with_style() {
let style = Style::parse("bold").unwrap();
let lr = LiveRender::new(Text::new("x", Style::null())).with_style(style.clone());
assert_eq!(lr.style, style);
}
#[test]
fn test_with_vertical_overflow() {
let lr = LiveRender::new(Text::new("x", Style::null()))
.with_vertical_overflow(VerticalOverflowMethod::Crop);
assert_eq!(lr.vertical_overflow, VerticalOverflowMethod::Crop);
}
#[test]
fn test_last_render_height_before_render() {
let lr = LiveRender::new(Text::new("hello", Style::null()));
assert_eq!(lr.last_render_height(), 0);
}
#[test]
fn test_last_render_height_after_render() {
let console = Console::builder().width(80).build();
let lr = LiveRender::new(Text::new("line1\nline2\nline3", Style::null()));
let opts = console.options();
let _ = lr.rich_console(&console, &opts);
assert_eq!(lr.last_render_height(), 3);
}
#[test]
fn test_set_renderable() {
let mut lr = LiveRender::new(Text::new("old", Style::null()));
lr.set_renderable(Text::new("new", Style::null()));
assert_eq!(lr.renderable.plain(), "new");
}
#[test]
fn test_renderable_basic() {
let console = Console::builder().width(80).markup(false).build();
let lr = LiveRender::new(Text::new("Hello, World!", Style::null()));
let opts = console.options();
let segments = lr.rich_console(&console, &opts);
let combined: String = segments.iter().map(|s| s.text.as_str()).collect();
assert!(combined.contains("Hello, World!"));
}
#[test]
fn test_renderable_multiline() {
let console = Console::builder().width(80).markup(false).build();
let lr = LiveRender::new(Text::new("Line1\nLine2", Style::null()));
let opts = console.options();
let segments = lr.rich_console(&console, &opts);
let has_newline = segments.iter().any(|s| s.text == "\n");
assert!(has_newline);
let combined: String = segments.iter().map(|s| s.text.as_str()).collect();
assert!(combined.contains("Line1"));
assert!(combined.contains("Line2"));
}
#[test]
fn test_vertical_overflow_crop() {
let console = Console::builder().width(80).height(3).build();
let lr = LiveRender::new(Text::new("L1\nL2\nL3\nL4\nL5", Style::null()))
.with_vertical_overflow(VerticalOverflowMethod::Crop);
let opts = console.options();
let segments = lr.rich_console(&console, &opts);
let combined: String = segments.iter().map(|s| s.text.as_str()).collect();
assert!(combined.contains("L1"));
assert!(combined.contains("L2"));
assert!(combined.contains("L3"));
assert!(!combined.contains("L4"));
assert!(!combined.contains("L5"));
assert_eq!(lr.last_render_height(), 3);
}
#[test]
fn test_vertical_overflow_ellipsis() {
let console = Console::builder().width(80).height(3).build();
let lr = LiveRender::new(Text::new("L1\nL2\nL3\nL4\nL5", Style::null()))
.with_vertical_overflow(VerticalOverflowMethod::Ellipsis);
let opts = console.options();
let segments = lr.rich_console(&console, &opts);
let combined: String = segments.iter().map(|s| s.text.as_str()).collect();
assert!(combined.contains("L1"));
assert!(combined.contains("L2"));
assert!(combined.contains("..."));
assert!(!combined.contains("L3\n"));
assert_eq!(lr.last_render_height(), 3);
}
#[test]
fn test_vertical_overflow_visible() {
let console = Console::builder().width(80).height(3).build();
let lr = LiveRender::new(Text::new("L1\nL2\nL3\nL4\nL5", Style::null()))
.with_vertical_overflow(VerticalOverflowMethod::Visible);
let opts = console.options();
let segments = lr.rich_console(&console, &opts);
let combined: String = segments.iter().map(|s| s.text.as_str()).collect();
assert!(combined.contains("L1"));
assert!(combined.contains("L5"));
assert_eq!(lr.last_render_height(), 5);
}
#[test]
fn test_position_cursor_no_render() {
let lr = LiveRender::new(Text::new("hello", Style::null()));
let segments = lr.position_cursor();
assert!(segments.is_empty());
}
#[test]
fn test_position_cursor_after_render() {
let console = Console::builder().width(80).build();
let lr = LiveRender::new(Text::new("L1\nL2\nL3", Style::null()));
let opts = console.options();
let _ = lr.rich_console(&console, &opts);
let segments = lr.position_cursor();
assert_eq!(segments.len(), 1);
let ctrl = segments[0].control.as_ref().unwrap();
assert_eq!(ctrl[0], ControlCode::Simple(ControlType::CarriageReturn));
assert_eq!(ctrl[1], ControlCode::WithParam(ControlType::EraseInLine, 2));
assert_eq!(ctrl.len(), 6);
assert_eq!(ctrl[2], ControlCode::WithParam(ControlType::CursorUp, 1));
assert_eq!(ctrl[3], ControlCode::WithParam(ControlType::EraseInLine, 2));
assert_eq!(ctrl[4], ControlCode::WithParam(ControlType::CursorUp, 1));
assert_eq!(ctrl[5], ControlCode::WithParam(ControlType::EraseInLine, 2));
}
#[test]
fn test_restore_cursor_no_render() {
let lr = LiveRender::new(Text::new("hello", Style::null()));
let segments = lr.restore_cursor();
assert!(segments.is_empty());
}
#[test]
fn test_restore_cursor_after_render() {
let console = Console::builder().width(80).build();
let lr = LiveRender::new(Text::new("L1\nL2\nL3", Style::null()));
let opts = console.options();
let _ = lr.rich_console(&console, &opts);
let segments = lr.restore_cursor();
assert_eq!(segments.len(), 1);
let ctrl = segments[0].control.as_ref().unwrap();
assert_eq!(ctrl[0], ControlCode::Simple(ControlType::CarriageReturn));
assert_eq!(ctrl.len(), 7);
for i in 0..3 {
assert_eq!(
ctrl[1 + i * 2],
ControlCode::WithParam(ControlType::CursorUp, 1)
);
assert_eq!(
ctrl[2 + i * 2],
ControlCode::WithParam(ControlType::EraseInLine, 2)
);
}
}
#[test]
fn test_shape_tracking() {
let console = Console::builder().width(40).build();
let lr = LiveRender::new(Text::new("Hello", Style::null()));
let opts = console.options();
assert!(lr.shape.is_none());
let _ = lr.rich_console(&console, &opts);
assert!(lr.shape.is_some());
let (w, h) = lr.shape.unwrap();
assert!(w > 0);
assert_eq!(h, 1);
}
#[test]
fn test_position_cursor_single_line() {
let console = Console::builder().width(80).build();
let lr = LiveRender::new(Text::new("Hello", Style::null()));
let opts = console.options();
let _ = lr.rich_console(&console, &opts);
let segments = lr.position_cursor();
assert_eq!(segments.len(), 1);
let ctrl = segments[0].control.as_ref().unwrap();
assert_eq!(ctrl.len(), 2);
assert_eq!(ctrl[0], ControlCode::Simple(ControlType::CarriageReturn));
assert_eq!(ctrl[1], ControlCode::WithParam(ControlType::EraseInLine, 2));
}
#[test]
fn test_restore_cursor_single_line() {
let console = Console::builder().width(80).build();
let lr = LiveRender::new(Text::new("Hello", Style::null()));
let opts = console.options();
let _ = lr.rich_console(&console, &opts);
let segments = lr.restore_cursor();
assert_eq!(segments.len(), 1);
let ctrl = segments[0].control.as_ref().unwrap();
assert_eq!(ctrl.len(), 3);
assert_eq!(ctrl[0], ControlCode::Simple(ControlType::CarriageReturn));
assert_eq!(ctrl[1], ControlCode::WithParam(ControlType::CursorUp, 1));
assert_eq!(ctrl[2], ControlCode::WithParam(ControlType::EraseInLine, 2));
}
#[test]
fn test_vertical_overflow_method_variants() {
assert_ne!(
VerticalOverflowMethod::Crop,
VerticalOverflowMethod::Ellipsis
);
assert_ne!(
VerticalOverflowMethod::Ellipsis,
VerticalOverflowMethod::Visible
);
assert_ne!(
VerticalOverflowMethod::Crop,
VerticalOverflowMethod::Visible
);
}
#[test]
fn test_render_with_style() {
let console = Console::builder().width(80).markup(false).build();
let style = Style::parse("bold").unwrap();
let lr = LiveRender::new(Text::new("styled", Style::null())).with_style(style);
let opts = console.options();
let segments = lr.rich_console(&console, &opts);
let combined: String = segments.iter().map(|s| s.text.as_str()).collect();
assert!(combined.contains("styled"));
}
#[test]
fn test_no_overflow_when_fits() {
let console = Console::builder().width(80).height(10).build();
let lr = LiveRender::new(Text::new("L1\nL2\nL3", Style::null()))
.with_vertical_overflow(VerticalOverflowMethod::Ellipsis);
let opts = console.options();
let segments = lr.rich_console(&console, &opts);
let combined: String = segments.iter().map(|s| s.text.as_str()).collect();
assert!(combined.contains("L1"));
assert!(combined.contains("L2"));
assert!(combined.contains("L3"));
assert!(!combined.contains("..."));
}
}