1use cba::bait::TransformExt;
2use ratatui::{
3 layout::Constraint,
4 text::{Line, Text},
5 widgets::{Cell, Paragraph, Row, Table},
6};
7
8use crate::{
9 config::{DisplayConfig, RowConnectionStyle},
10 utils::{
11 serde::StringOrVec,
12 text::{wrap_line, wrap_text, wrapping_indicator},
13 },
14};
15pub type HeaderTable = Vec<Vec<Line<'static>>>;
16#[derive(Debug)]
17pub struct DisplayUI {
18 width: u16,
19 height: u16,
20 text: Vec<Text<'static>>,
21 lines: HeaderTable, pub show: bool,
23 pub config: DisplayConfig,
24}
25
26impl DisplayUI {
27 pub fn new(config: DisplayConfig) -> Self {
28 let (text, height) = match &config.content {
29 Some(StringOrVec::String(s)) => {
30 let text = Text::from(s.clone());
31 let height = text.height() as u16;
32 (vec![text], height)
33 }
34 Some(StringOrVec::Vec(s)) => {
35 let text: Vec<_> = s.iter().map(|s| Text::from(s.clone())).collect();
36 let height = text.iter().map(|t| t.height()).max().unwrap_or_default() as u16;
37 (text, height)
38 }
39 _ => (vec![], 0),
40 };
41
42 Self {
43 height,
44 width: 0,
45 show: config.content.is_some() || config.header_lines > 0,
46 lines: Vec::new(),
47 text,
48 config,
49 }
50 }
51
52 pub fn update_width(&mut self, width: u16) {
53 let border_w = self.config.border.width();
54 let new_w = width.saturating_sub(border_w);
55 self.width = new_w;
56 }
57
58 pub fn height(&self) -> u16 {
59 if !self.show {
60 return 0;
61 }
62 let mut height = self.height;
63 height += self.config.border.height();
64
65 height
66 }
67
68 pub fn set(&mut self, text: impl Into<Text<'static>>) {
70 self.text = vec![text.into()];
71
72 self.show = true;
73 }
74
75 pub fn clear(&mut self, keep_header: bool) {
76 if !keep_header {
77 self.lines.clear();
78 self.show = false;
79 } else if self.lines.is_empty() {
80 self.show = false;
81 }
82
83 self.text.clear();
84 }
85
86 pub fn is_single_column(&self) -> bool {
88 self.text.len() == 1
89 }
90
91 pub fn header_table(&mut self, table: HeaderTable) {
92 self.lines = table
93 }
94
95 pub fn make_display(
97 &mut self,
98 result_indentation: u16,
99 mut widths: Vec<u16>,
100 col_spacing: u16,
101 ) -> Table<'_> {
102 if self.text.is_empty() && self.lines.is_empty() || widths.is_empty() {
103 return Table::default();
104 }
105
106 let block = {
107 let b = self.config.border.as_block();
108 if self.config.match_indent {
109 let mut padding = self.config.border.padding;
110
111 padding.left = result_indentation.saturating_sub(self.config.border.left());
112 widths[0] -= result_indentation;
113 b.padding(padding.0)
114 } else {
115 b
116 }
117 };
118
119 let (cells, height) = if self.is_single_column() {
120 let text = wrap_text(
122 self.text[0].clone(),
123 if self.config.wrap { self.width } else { 0 },
124 )
125 .0;
126 let cells = vec![Cell::from(text)];
127 let height = self.text[0].height() as u16;
128
129 (cells, height)
130 } else
131 {
133 let mut height = 0;
134 let cells = self
136 .text
137 .iter()
138 .cloned()
139 .zip(widths.iter().copied())
140 .map(|(text, width)| {
141 let ret = wrap_text(text, if self.config.wrap { width } else { 0 }).0;
142 height = height.max(ret.height() as u16);
143
144 Cell::from(ret.transform_if(
145 matches!(self.config.row_connection, RowConnectionStyle::Disjoint),
146 |t| t.style(self.config.style),
147 ))
148 })
149 .collect();
150
151 (cells, height)
152 };
153
154 let row = Row::new(cells).style(self.config.style).height(height);
155 let mut rows = vec![row];
156 self.height = height;
157
158 if !self.lines.is_empty() {
160 rows.extend(self.lines.iter().map(|row| {
162 let cells: Vec<Cell> = row
163 .iter()
164 .cloned()
165 .enumerate()
166 .map(|(i, l)| {
167 wrap_line(
168 l,
169 self.config
170 .wrap
171 .then_some(widths.get(i).cloned())
172 .flatten()
173 .unwrap_or_default(),
174 &wrapping_indicator(),
175 )
176 })
177 .map(Cell::from)
178 .collect();
179 Row::new(cells)
180 }));
181
182 self.height += self.lines.len() as u16;
183 }
184
185 let widths = if self.is_single_column() {
186 vec![Constraint::Percentage(100)]
187 } else {
188 widths.into_iter().map(Constraint::Length).collect()
189 };
190
191 Table::new(rows, widths)
192 .block(block)
193 .column_spacing(col_spacing)
194 .transform_if(
195 !matches!(self.config.row_connection, RowConnectionStyle::Disjoint),
196 |t| t.style(self.config.style),
197 )
198 }
199
200 pub fn make_full_width_row(&self, result_indentation: u16) -> Paragraph<'_> {
202 let left = if self.config.match_indent {
204 result_indentation.saturating_sub(self.config.border.left())
205 } else {
206 self.config.border.left()
207 };
208 let top = self.config.border.top();
209 let right = self.config.border.width().saturating_sub(left);
210 let bottom = self.config.border.height() - top;
211
212 let block = ratatui::widgets::Block::default().padding(ratatui::widgets::Padding {
213 left,
214 top,
215 right,
216 bottom,
217 });
218
219 Paragraph::new(self.text[0].clone())
220 .block(block)
221 .style(self.config.style)
222 }
223}