thndrs_lib/cli/renderer/
row.rs1use super::style::{CellStyle, Span};
7
8#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
12pub struct RowGroupId {
13 pub entry_index: usize,
16}
17
18#[derive(Clone, Debug, Default, Eq, PartialEq)]
20pub struct Row {
21 pub spans: Vec<Span>,
22 pub width: usize,
24 pub group_id: Option<RowGroupId>,
26}
27
28impl Row {
29 pub fn padded(spans: Vec<Span>, width: usize, pad_style: CellStyle) -> Self {
31 let spans = super::layout::pad_row(spans, width, pad_style);
32 Row { spans, width, group_id: None }
33 }
34
35 pub fn blank(width: usize, style: CellStyle) -> Self {
37 if width == 0 {
38 return Row { spans: Vec::new(), width: 0, group_id: None };
39 }
40 Row { spans: vec![Span::styled(" ".repeat(width), style)], width, group_id: None }
41 }
42
43 #[cfg(test)]
45 pub fn text_width(&self) -> usize {
46 super::layout::spans_width(&self.spans)
47 }
48
49 #[cfg(test)]
51 pub fn text(&self) -> String {
52 let mut out = String::new();
53 for span in &self.spans {
54 out.push_str(&span.text);
55 }
56 out
57 }
58}
59
60#[derive(Clone, Debug, Default, Eq, PartialEq)]
66pub struct Frame {
67 pub rows: Vec<Row>,
68 pub width: usize,
69 pub cursor: Option<CursorCoord>,
70 pub cursor_visible: bool,
72}
73
74#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
76pub struct CursorCoord {
77 pub row: usize,
78 pub col: usize,
79}
80
81impl CursorCoord {
82 pub fn new(row: usize, col: usize) -> Self {
83 CursorCoord { row, col }
84 }
85}
86
87impl Frame {
88 pub fn new(width: usize) -> Self {
90 Frame { rows: Vec::new(), width, cursor: None, cursor_visible: true }
91 }
92
93 pub fn push(&mut self, row: Row) {
95 self.rows.push(row);
96 }
97
98 pub fn set_cursor(&mut self, cursor: CursorCoord) {
100 self.cursor = Some(cursor);
101 }
102
103 pub fn len(&self) -> usize {
105 self.rows.len()
106 }
107
108 pub fn is_empty(&self) -> bool {
110 self.rows.is_empty()
111 }
112
113 #[cfg(test)]
115 pub fn render_text(&self) -> String {
116 let mut out = String::new();
117 for row in &self.rows {
118 out.push_str(&row.text());
119 out.push('\n');
120 }
121 out
122 }
123
124 #[cfg(test)]
128 pub fn render_styled(&self) -> String {
129 let mut out = String::new();
130 for (i, row) in self.rows.iter().enumerate() {
131 if i > 0 {
132 out.push('\n');
133 }
134 out.push_str("│ ");
135 for span in &row.spans {
136 out.push_str(&span.text);
137 }
138
139 out.push_str(" #");
140 for span in &row.spans {
141 if !span.text.is_empty() {
142 out.push_str(&format!(" [{}]={}", span.style, span.text.replace('\n', "\\n")));
143 }
144 }
145 }
146 out
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153 use crate::renderer::style::Color;
154
155 #[test]
156 fn row_padded_fills_width() {
157 let row = Row::padded(vec![Span::plain("hi")], 10, CellStyle::default());
158 assert_eq!(row.text_width(), 10);
159 assert_eq!(row.width, 10);
160 }
161
162 #[test]
163 fn frame_render_text_one_line_per_row() {
164 let mut frame = Frame::new(10);
165 frame.push(Row::padded(vec![Span::plain("a")], 5, CellStyle::default()));
166 frame.push(Row::padded(vec![Span::plain("b")], 5, CellStyle::default()));
167 let text = frame.render_text();
168 assert!(text.contains("a"));
169 assert!(text.contains("b"));
170 assert_eq!(text.matches('\n').count(), 2);
171 }
172
173 #[test]
174 fn frame_render_styled_includes_style_annotations() {
175 let mut frame = Frame::new(10);
176 frame.push(Row::padded(
177 vec![Span::styled("hi", CellStyle::new().fg(Color::Red))],
178 6,
179 CellStyle::default(),
180 ));
181 let styled = frame.render_styled();
182 assert!(styled.contains("fg=red"));
183 assert!(styled.contains("hi"));
184 }
185
186 #[test]
187 fn frame_set_cursor_stores_coord() {
188 let mut frame = Frame::new(10);
189 frame.set_cursor(CursorCoord::new(2, 3));
190 assert_eq!(frame.cursor, Some(CursorCoord { row: 2, col: 3 }));
191 }
192}