use crate::components::text::Line;
use crate::core::Element;
use crate::layout::LayoutEngine;
use crate::renderer::{Output, Terminal};
pub(crate) struct StaticRenderer {
committed_lines: Vec<String>,
}
impl StaticRenderer {
pub(crate) fn new() -> Self {
Self {
committed_lines: Vec::new(),
}
}
pub(crate) fn extract_static_content(&self, element: &Element, width: u16) -> Vec<String> {
let mut lines = Vec::new();
self.extract_recursive(element, width, &mut lines);
lines
}
fn extract_recursive(&self, element: &Element, width: u16, lines: &mut Vec<String>) {
if element.style.is_static {
if !element.children.is_empty() {
let mut engine = LayoutEngine::new();
engine.compute(element, width, 100);
let layout = engine.get_layout(element.id).unwrap_or_default();
let render_width = (layout.width as u16).max(1);
let render_height = (layout.height as u16).max(1);
let mut output = Output::new(render_width, render_height);
Self::render_element_to_output(element, &engine, &mut output, 0.0, 0.0);
let rendered = output.render();
for line in rendered.lines() {
let trimmed = line.trim();
if !trimmed.is_empty() {
lines.push(line.to_string());
}
}
}
}
for child in &element.children {
self.extract_recursive(child, width, lines);
}
}
pub(crate) fn commit_static_content(
&mut self,
new_lines: &[String],
terminal: &mut Terminal,
) -> std::io::Result<()> {
use std::io::{Write, stdout};
if new_lines.is_empty() {
return Ok(());
}
terminal.clear()?;
let mut stdout = stdout();
for line in new_lines {
writeln!(stdout, "{}\x1b[K", line)?;
self.committed_lines.push(line.clone());
}
stdout.flush()?;
terminal.repaint();
Ok(())
}
pub(crate) fn filter_static_elements(&self, element: &Element) -> Element {
let mut new_element = element.clone();
new_element.children = element
.children
.iter()
.filter(|child| !child.style.is_static)
.map(|child| self.filter_static_elements(child))
.collect();
new_element
}
#[allow(dead_code)]
pub(crate) fn committed_line_count(&self) -> usize {
self.committed_lines.len()
}
fn render_element_to_output(
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(
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;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::components::{Box, Text};
#[test]
fn test_static_renderer_creation() {
let renderer = StaticRenderer::new();
assert_eq!(renderer.committed_line_count(), 0);
}
#[test]
fn test_extract_empty_element() {
let renderer = StaticRenderer::new();
let element = Text::new("Hello").into_element();
let lines = renderer.extract_static_content(&element, 80);
assert!(lines.is_empty()); }
#[test]
fn test_filter_static_elements() {
let renderer = StaticRenderer::new();
let mut static_child = Text::new("Static").into_element();
static_child.style.is_static = true;
let dynamic_child = Text::new("Dynamic").into_element();
let parent = Box::new()
.child(static_child)
.child(dynamic_child)
.into_element();
let filtered = renderer.filter_static_elements(&parent);
assert_eq!(filtered.children.len(), 1);
assert!(!filtered.children.get(0).unwrap().style.is_static);
}
#[test]
fn test_extract_static_with_children() {
let renderer = StaticRenderer::new();
let mut static_element = Box::new()
.child(Text::new("Line 1").into_element())
.into_element();
static_element.style.is_static = true;
let lines = renderer.extract_static_content(&static_element, 80);
assert!(!lines.is_empty());
}
#[test]
fn test_filter_nested_static() {
let renderer = StaticRenderer::new();
let mut static_child = Text::new("Static").into_element();
static_child.style.is_static = true;
let inner_box = Box::new().child(static_child).into_element();
let outer_box = Box::new()
.child(inner_box)
.child(Text::new("Dynamic").into_element())
.into_element();
let filtered = renderer.filter_static_elements(&outer_box);
assert_eq!(filtered.children.len(), 2);
assert_eq!(filtered.children.get(0).unwrap().children.len(), 0);
}
}