use crate::components::text::Line;
use crate::core::Element;
use crate::layout::LayoutEngine;
use crate::renderer::{Output, Terminal};
pub fn render_to_string(element: &Element, width: u16) -> String {
render_to_string_impl(element, width, true)
}
pub fn render_to_string_no_trim(element: &Element, width: u16) -> String {
render_to_string_impl(element, width, false)
}
pub fn render_to_string_raw(element: &Element, width: u16) -> String {
let helper = RenderHelper;
helper.render_element_to_string_raw(element, width)
}
pub fn render_to_string_auto(element: &Element) -> String {
let (width, _) = Terminal::size().unwrap_or((80, 24));
render_to_string(element, width)
}
fn render_to_string_impl(element: &Element, width: u16, trim: bool) -> String {
let helper = RenderHelper;
helper.render_element_to_string_impl(element, width, trim)
}
struct RenderHelper;
impl RenderHelper {
fn render_element_to_string_impl(&self, element: &Element, width: u16, trim: bool) -> String {
let mut engine = LayoutEngine::new();
let layout_width = width;
let height = self.calculate_element_height(element, layout_width, &mut engine);
engine.compute(element, layout_width, height.max(1000));
let _layout = engine.get_layout(element.id).unwrap_or_default();
let render_width = layout_width;
let content_height = height.max(1);
let mut output = Output::new(render_width, content_height);
self.render_element_to_output(element, &engine, &mut output, 0.0, 0.0);
let rendered = output.render();
let normalized = rendered.replace("\r\n", "\n");
if trim {
normalized
.lines()
.map(|line| line.trim_end())
.collect::<Vec<_>>()
.join("\n")
} else {
normalized
}
}
#[allow(dead_code)]
fn calculate_content_width(&self, element: &Element) -> u16 {
use unicode_width::UnicodeWidthStr;
if let crate::core::Dimension::Points(w) = element.style.width {
return w as u16;
}
let mut width = 0u16;
if let Some(text) = &element.text_content {
width = width.max(text.width() as u16);
}
if let Some(lines) = &element.spans {
for line in lines {
let line_width: usize = line.spans.iter().map(|span| span.width()).sum();
width = width.max(line_width as u16);
}
}
if element.style.flex_direction == crate::core::FlexDirection::Row {
let mut child_width_sum = 0u16;
for child in &element.children {
let child_width = self.calculate_content_width(child);
child_width_sum = child_width_sum.saturating_add(child_width);
}
width = width.max(child_width_sum);
} else {
for child in &element.children {
let child_width = self.calculate_content_width(child);
width = width.max(child_width);
}
}
if element.style.has_border() {
width = width.saturating_add(2);
}
let padding_h = (element.style.padding.left + element.style.padding.right) as u16;
width = width.saturating_add(padding_h);
width.max(1)
}
fn calculate_element_height(
&self,
element: &Element,
max_width: u16,
_engine: &mut LayoutEngine,
) -> u16 {
use crate::layout::measure::wrap_text;
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();
let wrapped = wrap_text(&line_text, available_width as usize);
total_lines += wrapped.len();
}
height = height.max(total_lines as u16);
}
if let Some(text) = &element.text_content {
let wrapped = wrap_text(text, available_width as usize);
height = height.max(wrapped.len() 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
}
fn render_element_to_output(
&self,
element: &Element,
engine: &LayoutEngine,
output: &mut Output,
offset_x: f32,
offset_y: f32,
) {
if element.style.display == crate::core::Display::None {
return;
}
let layout = engine.get_layout(element.id).unwrap_or_default();
let x = (offset_x + layout.x) as u16;
let y = (offset_y + layout.y) as u16;
let width = layout.width as u16;
let height = layout.height as u16;
if element.style.background_color.is_some() {
output.fill_rect(x, y, width, height, ' ', &element.style);
}
if element.style.has_border() {
self.render_border(element, output, x, y, width, height);
}
let text_x =
x + if element.style.has_border() { 1 } else { 0 } + element.style.padding.left as u16;
let text_y =
y + if element.style.has_border() { 1 } else { 0 } + element.style.padding.top as u16;
if let Some(spans) = &element.spans {
Self::render_spans(spans, output, text_x, text_y);
} else if let Some(text) = &element.text_content {
output.write(text_x, text_y, text, &element.style);
}
let child_offset_x = offset_x + layout.x;
let child_offset_y = offset_y + layout.y;
for child in &element.children {
self.render_element_to_output(child, engine, output, child_offset_x, child_offset_y);
}
}
fn render_border(
&self,
element: &Element,
output: &mut Output,
x: u16,
y: u16,
width: u16,
height: u16,
) {
let (tl, tr, bl, br, h, v) = element.style.border_style.chars();
let mut base_style = element.style.clone();
base_style.dim = element.style.border_dim;
let mut top_style = base_style.clone();
top_style.color = element.style.get_border_top_color();
let mut right_style = base_style.clone();
right_style.color = element.style.get_border_right_color();
let mut bottom_style = base_style.clone();
bottom_style.color = element.style.get_border_bottom_color();
let mut left_style = base_style.clone();
left_style.color = element.style.get_border_left_color();
if element.style.border_top && height > 0 {
output.write_char(x, y, tl.chars().next().unwrap(), &top_style);
for col in (x + 1)..(x + width - 1) {
output.write_char(col, y, h.chars().next().unwrap(), &top_style);
}
if width > 1 {
output.write_char(x + width - 1, y, tr.chars().next().unwrap(), &top_style);
}
}
if element.style.border_bottom && height > 1 {
let bottom_y = y + height - 1;
output.write_char(x, bottom_y, bl.chars().next().unwrap(), &bottom_style);
for col in (x + 1)..(x + width - 1) {
output.write_char(col, bottom_y, h.chars().next().unwrap(), &bottom_style);
}
if width > 1 {
output.write_char(
x + width - 1,
bottom_y,
br.chars().next().unwrap(),
&bottom_style,
);
}
}
if element.style.border_left {
for row in (y + 1)..(y + height - 1) {
output.write_char(x, row, v.chars().next().unwrap(), &left_style);
}
}
if element.style.border_right && width > 1 {
for row in (y + 1)..(y + height - 1) {
output.write_char(x + width - 1, row, v.chars().next().unwrap(), &right_style);
}
}
}
fn render_spans(lines: &[Line], output: &mut Output, start_x: u16, start_y: u16) {
for (line_idx, line) in lines.iter().enumerate() {
let y = start_y + line_idx as u16;
let mut x = start_x;
for span in &line.spans {
output.write(x, y, &span.content, &span.style);
x += span.width() as u16;
}
}
}
fn render_element_to_string_raw(&self, element: &Element, width: u16) -> String {
let mut engine = LayoutEngine::new();
let layout_width = width;
let height = self.calculate_element_height(element, layout_width, &mut engine);
engine.compute(element, layout_width, height.max(1000));
let _layout = engine.get_layout(element.id).unwrap_or_default();
let render_width = layout_width;
let content_height = height.max(1);
let mut output = Output::new(render_width, content_height);
self.render_element_to_output(element, &engine, &mut output, 0.0, 0.0);
output.render()
}
}
#[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"));
}
}