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