use crate::core::Element;
use crate::layout::LayoutEngine;
use crate::renderer::tree_renderer::render_element_tree;
use crate::renderer::{Output, Terminal};
#[derive(Debug, Clone)]
pub struct RenderOptions {
pub trim: bool,
pub normalize_line_endings: bool,
}
impl Default for RenderOptions {
fn default() -> Self {
Self {
trim: true,
normalize_line_endings: true,
}
}
}
pub fn render_to_string_with_options(
element: &Element,
width: u16,
options: &RenderOptions,
) -> String {
let raw = RenderHelper.render_to_output(element, width);
if !options.normalize_line_endings {
return raw;
}
let normalized = raw.replace("\r\n", "\n");
if options.trim {
normalized
.lines()
.map(|line| line.trim_end())
.collect::<Vec<_>>()
.join("\n")
} else {
normalized
}
}
pub fn render_to_string(element: &Element, width: u16) -> String {
render_to_string_with_options(element, width, &RenderOptions::default())
}
pub fn render_to_string_no_trim(element: &Element, width: u16) -> String {
render_to_string_with_options(
element,
width,
&RenderOptions {
trim: false,
..Default::default()
},
)
}
pub fn render_to_string_raw(element: &Element, width: u16) -> String {
render_to_string_with_options(
element,
width,
&RenderOptions {
trim: false,
normalize_line_endings: false,
},
)
}
pub fn render_to_string_auto(element: &Element) -> String {
let (width, _) = Terminal::size().unwrap_or((80, 24));
render_to_string(element, width)
}
struct RenderHelper;
impl RenderHelper {
fn render_to_output(&self, element: &Element, width: u16) -> String {
let mut engine = LayoutEngine::new();
let layout_width = width;
let content_height = self.resolve_render_height(element, layout_width, &mut engine);
let render_width = layout_width;
let mut output = Output::new(render_width, content_height);
let clip_depth_before = output.clip_depth();
render_element_tree(element, &engine, &mut output, 0.0, 0.0);
debug_assert_eq!(
output.clip_depth(),
clip_depth_before,
"render_to_string left an unbalanced clip stack"
);
output.render()
}
fn resolve_render_height(
&self,
element: &Element,
width: u16,
engine: &mut LayoutEngine,
) -> u16 {
let initial_guess = self.calculate_element_height(element, width, engine).max(1);
let mut probe_height = initial_guess.max(64);
let mut measured_height = initial_guess;
for _ in 0..6 {
engine.compute(element, width, probe_height);
measured_height = engine
.get_layout(element.id)
.map(|layout| layout.height.ceil().max(1.0) as u16)
.unwrap_or(initial_guess.max(1));
if measured_height.saturating_add(1) < probe_height {
break;
}
if probe_height == u16::MAX {
break;
}
probe_height = probe_height
.saturating_mul(2)
.max(probe_height.saturating_add(1));
}
let resolved_height = measured_height.max(1);
engine.compute(element, width, resolved_height);
resolved_height
}
fn calculate_element_height(
&self,
element: &Element,
max_width: u16,
_engine: &mut LayoutEngine,
) -> u16 {
let mut height = 1u16;
let available_width = if element.style.has_border() {
max_width.saturating_sub(2)
} else {
max_width
};
let padding_h = (element.style.padding.left + element.style.padding.right) as u16;
let available_width = available_width.saturating_sub(padding_h).max(1);
if let Some(lines) = &element.spans {
let mut total_lines = 0usize;
for line in lines {
let line_text: String = line.spans.iter().map(|s| s.content.as_str()).collect();
total_lines += crate::layout::measure::count_wrapped_lines_by_width(
&line_text,
available_width as usize,
);
}
height = height.max(total_lines as u16);
}
if let Some(text) = &element.text_content {
let wrapped_lines = crate::layout::measure::count_wrapped_lines_by_width(
text,
available_width as usize,
);
height = height.max(wrapped_lines as u16);
}
if element.style.has_border() {
height = height.saturating_add(2);
}
let padding_v = (element.style.padding.top + element.style.padding.bottom) as u16;
height = height.saturating_add(padding_v);
if !element.children.is_empty() {
let mut child_height_sum = 0u16;
let mut child_height_max = 0u16;
for child in &element.children {
let child_height = self.calculate_element_height(child, max_width, _engine);
child_height_sum = child_height_sum.saturating_add(child_height);
child_height_max = child_height_max.max(child_height);
}
if element.style.flex_direction == crate::core::FlexDirection::Column {
height = height.saturating_add(child_height_sum);
} else {
height = height.max(child_height_max);
}
}
height
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::components::{Box, Text};
use crate::core::BorderStyle;
#[test]
fn test_render_to_string_simple() {
let element = Text::new("Hello").into_element();
let output = render_to_string(&element, 80);
assert!(output.contains("Hello"));
}
#[test]
fn test_render_to_string_with_border() {
let element = Box::new()
.border_style(BorderStyle::Single)
.child(Text::new("Test").into_element())
.into_element();
let output = render_to_string(&element, 80);
assert!(output.contains("Test"));
assert!(output.contains("─")); }
#[test]
fn test_render_to_string_no_trim() {
let element = Text::new("Hi").into_element();
let trimmed = render_to_string(&element, 80);
let not_trimmed = render_to_string_no_trim(&element, 80);
assert!(trimmed.contains("Hi"));
assert!(not_trimmed.contains("Hi"));
}
#[test]
fn test_render_to_string_applies_scroll_offset() {
let element = Box::new()
.padding_left(4.0)
.scroll_offset_x(2)
.child(Text::new("X").into_element())
.into_element();
let output = render_to_string(&element, 20);
let first_line = output.lines().next().unwrap_or_default();
let x_pos = first_line.find('X').unwrap_or(usize::MAX);
assert_eq!(x_pos, 2);
}
#[test]
fn test_render_to_string_handles_tall_content() {
let mut container = Box::new().flex_direction(crate::core::FlexDirection::Column);
for i in 0..1100 {
container = container.child(Text::new(format!("line-{i}")).into_element());
}
let element = container.into_element();
let output = render_to_string(&element, 40);
assert!(output.contains("line-0"));
assert!(output.contains("line-1099"));
}
}