1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
//! Static content handling for inline mode
//!
//! This module handles the extraction and rendering of `Static` elements,
//! which are elements that persist in the terminal history (like Ink's `<Static>`).
use crate::core::Element;
use crate::layout::LayoutEngine;
use crate::renderer::tree_renderer::render_element_tree;
use crate::renderer::{Output, Terminal};
/// Static content renderer for inline mode
///
/// Handles the extraction, rendering, and committing of static content
/// that should persist in terminal history.
pub(crate) struct StaticRenderer {
/// Lines of static content that have been committed
committed_lines: Vec<String>,
}
impl StaticRenderer {
/// Create a new static renderer
pub(crate) fn new() -> Self {
Self {
committed_lines: Vec::new(),
}
}
/// Extract static content from the element tree
///
/// Only extracts content from Static elements that have actual children
/// (new items to render). Empty Static elements are skipped.
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
}
/// Recursive helper for extracting static content
fn extract_recursive(&self, element: &Element, width: u16, lines: &mut Vec<String>) {
if element.style.is_static {
// Only render if the static element has children (new items)
// Empty Static elements mean all items have already been rendered
if !element.children.is_empty() {
// Render static element to get its content
let mut engine = LayoutEngine::new();
engine.compute(element, width, 100); // Use large height for static content
let layout = engine.get_layout(element.id).unwrap_or_default();
// Ensure we have valid dimensions
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);
let clip_depth_before = output.clip_depth();
render_element_tree(element, &engine, &mut output, 0.0, 0.0);
assert_eq!(
output.clip_depth(),
clip_depth_before,
"static content render left an unbalanced clip stack"
);
let rendered = output.render();
for line in rendered.lines() {
// Skip empty lines to avoid clutter
let trimmed = line.trim();
if !trimmed.is_empty() {
lines.push(line.to_string());
}
}
}
}
// Check children for static content (non-static elements might contain static children)
for child in &element.children {
self.extract_recursive(child, width, lines);
}
}
/// Commit static content to the terminal (write permanently)
///
/// This follows the Ink/Bubbletea pattern:
/// 1. Clear the current dynamic UI
/// 2. Write the static content (which will persist)
/// 3. The dynamic UI will be re-rendered below
pub(crate) fn commit_static_content(
&mut self,
new_lines: &[String],
terminal: &mut Terminal,
) -> std::io::Result<()> {
use std::io::{Write, stdout};
// Skip if no lines to commit
if new_lines.is_empty() {
return Ok(());
}
// Clear current dynamic UI first (like Ink's log.clear())
terminal.clear()?;
let mut stdout = stdout();
for line in new_lines {
// Write the line with erase-to-end-of-line to ensure clean output
writeln!(stdout, "{}\x1b[K", line)?;
self.committed_lines.push(line.clone());
}
stdout.flush()?;
// Force a full repaint of the dynamic UI
terminal.repaint();
Ok(())
}
/// Filter out static elements from the tree
///
/// Returns a new element tree with all static elements removed,
/// leaving only dynamic content for rendering.
pub(crate) fn filter_static_elements(&self, element: &Element) -> Element {
let mut new_element = element.clone();
// Remove static children
new_element.children = element
.children
.iter()
.filter(|child| !child.style.is_static)
.map(|child| self.filter_static_elements(child))
.collect();
new_element
}
}
#[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_lines.len(), 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()); // Non-static elements return empty
}
#[test]
fn test_filter_static_elements() {
let renderer = StaticRenderer::new();
// Create element with static child
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);
// Should only have the dynamic child
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();
// Create a static element with children
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();
// Create nested structure with static element
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);
// Outer should have 2 children, but inner should have 0 (static filtered out)
assert_eq!(filtered.children.len(), 2);
assert_eq!(filtered.children.get(0).unwrap().children.len(), 0);
}
}