Skip to main content

matchmaker/ui/
display.rs

1#![allow(unused)]
2use cli_boilerplate_automation::bait::{BoolExt, TransformExt};
3use log::debug;
4use ratatui::{
5    layout::{Constraint, Rect},
6    style::{Style, Stylize},
7    text::Text,
8    widgets::{Cell, Paragraph, Row, Table, Wrap},
9};
10
11use crate::{
12    config::{DisplayConfig, RowConnectionStyle},
13    utils::{
14        serde::StringOrVec,
15        text::{left_pad, prefix_text, wrap_text, wrapped_height},
16    },
17};
18
19#[derive(Debug)]
20pub struct DisplayUI {
21    width: u16,
22    height: u16,
23    text: Vec<Text<'static>>,
24    text_split_index: usize, // everything at and after this index is a header_line
25    pub show: bool,
26    pub config: DisplayConfig,
27}
28
29impl DisplayUI {
30    pub fn new(config: DisplayConfig) -> Self {
31        let (text, height) = match &config.content {
32            Some(StringOrVec::String(s)) => {
33                let text = Text::from(s.clone());
34                let height = text.height() as u16;
35                (vec![text], height)
36            }
37            Some(StringOrVec::Vec(s)) => {
38                let text: Vec<_> = s.iter().map(|s| Text::from(s.clone())).collect();
39                let height = text.iter().map(|t| t.height()).max().unwrap_or_default() as u16;
40                (text, height)
41            }
42            _ => (vec![], 0),
43        };
44
45        Self {
46            height,
47            width: 0,
48            show: config.content.is_some() || config.header_lines > 0,
49            text_split_index: text.len(),
50            text,
51            config,
52        }
53    }
54
55    pub fn update_width(&mut self, width: u16) {
56        let border_w = self.config.border.width();
57        let new_w = width.saturating_sub(border_w);
58        if new_w != self.width {
59            self.width = new_w;
60            // only rewrap of single cell is supported for now
61            if self.config.wrap && self.text_split_index == 1 {
62                let text = wrap_text(self.text[0].clone(), self.width).0;
63                self.text[0] = text;
64            }
65        }
66    }
67
68    pub fn height(&self) -> u16 {
69        if !self.show {
70            return 0;
71        }
72        let mut height = self.height;
73        height += self.config.border.height();
74
75        height
76    }
77
78    /// Set text and visibility. Compute wrapped height.
79    pub fn set(&mut self, text: impl Into<Text<'static>>, keep_header_columns: bool) {
80        let (text, _) = wrap_text(text.into(), self.config.wrap as u16 * self.width);
81
82        if keep_header_columns {
83            // Keep everything after text_split_index (header)
84            let header = self.text.split_off(self.text_split_index);
85
86            self.text = vec![text];
87            self.text.extend(header);
88        } else {
89            self.text = vec![text];
90        }
91
92        self.text_split_index = 1;
93        self.show = true;
94    }
95
96    pub fn clear(&mut self, keep_header_columns: bool) {
97        self.show = false;
98
99        if keep_header_columns {
100            let header = self.text.split_off(self.text_split_index);
101            self.text = header;
102        } else {
103            self.text.clear();
104        }
105
106        self.text_split_index = 0;
107    }
108
109    /// Whether this is table has just one column (ignoring header_lines)
110    pub fn single(&self) -> bool {
111        self.text_split_index == 1
112    }
113
114    pub fn header_columns(&mut self, columns: Vec<Text<'static>>) {
115        self.text.truncate(self.text_split_index);
116        self.text.extend(columns);
117    }
118
119    // todo: lowpri: cache texts to not have to always rewrap?
120    pub fn make_display(
121        &mut self,
122        result_indentation: u16,
123        mut widths: Vec<u16>,
124        col_spacing: u16,
125    ) -> Table<'_> {
126        if self.text.is_empty() || widths.is_empty() {
127            return Table::default();
128        }
129
130        let block = {
131            let b = self.config.border.as_block();
132            if self.config.match_indent {
133                let mut padding = self.config.border.padding;
134
135                padding.left = result_indentation.saturating_sub(self.config.border.left());
136                widths[0] -= result_indentation;
137                b.padding(padding)
138            } else {
139                b
140            }
141        };
142
143        let style = Style::default()
144            .fg(self.config.fg)
145            .add_modifier(self.config.modifier);
146
147        let (cells, height) = if self.text_split_index == 1 {
148            // Single Cell (Full Width)
149            // reflow is handled in update_width
150            let cells = if self.text.len() > 1 {
151                vec![]
152            } else {
153                vec![Cell::from(self.text[0].clone())]
154            };
155            let height = self.text[0].height() as u16;
156
157            (cells, height)
158        } else {
159            let mut height = 0;
160            // todo: for header, instead of reflowing on every render, the widths should be dynamically proportionate to the available width similar to results. Then results should take the max_widths from here instead of computing them.
161            let cells = self.text[..self.text_split_index]
162                .iter()
163                .cloned()
164                .zip(widths.iter().copied())
165                .map(|(text, width)| {
166                    let mut ret = wrap_text(text, width).0;
167                    height = height.max(ret.height() as u16);
168
169                    Cell::from(ret.transform_if(
170                        matches!(
171                            self.config.row_connection_style,
172                            RowConnectionStyle::Disjoint
173                        ),
174                        |r| r.style(style),
175                    ))
176                })
177                .collect();
178
179            (cells, height)
180        };
181
182        let row = Row::new(cells).style(style).height(height);
183        let mut rows = vec![row];
184        self.height = height;
185
186        // add header cells
187        if self.text_split_index < self.text.len() {
188            let mut height = 0;
189
190            let cells = self.text[self.text_split_index..].iter().map(|x| {
191                height = height.max(x.height() as u16);
192                Cell::from(x.clone())
193            });
194
195            rows.push(Row::new(cells).style(style).height(height));
196
197            self.height += height;
198        }
199
200        let widths = if self.single() && self.text.len() == 1 {
201            vec![Constraint::Percentage(100)]
202        } else {
203            widths.into_iter().map(Constraint::Length).collect()
204        };
205
206        Table::new(rows, widths)
207            .block(block)
208            .column_spacing(col_spacing)
209            .transform_if(
210                !matches!(
211                    self.config.row_connection_style,
212                    RowConnectionStyle::Disjoint
213                ),
214                |t| t.style(style),
215            )
216    }
217
218    /// Draw in the same area as display when self.single() to produce a full width row over the table area
219    pub fn make_full_width_row(&self, result_indentation: u16) -> Paragraph<'_> {
220        let style = Style::default()
221            .fg(self.config.fg)
222            .add_modifier(self.config.modifier);
223
224        // Compute padding
225        let left = if self.config.match_indent {
226            result_indentation.saturating_sub(self.config.border.left())
227        } else {
228            self.config.border.left()
229        };
230        let top = self.config.border.top();
231        let right = self.config.border.width().saturating_sub(left);
232        let bottom = self.config.border.height() - top;
233
234        let block = ratatui::widgets::Block::default().padding(ratatui::widgets::Padding {
235            left,
236            top,
237            right,
238            bottom,
239        });
240
241        Paragraph::new(self.text[0].clone())
242            .block(block)
243            .style(style)
244    }
245}