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,
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            if self.config.wrap && self.text_split_index == 1 {
61                let text = wrap_text(self.text.remove(0), self.width).0;
62                self.text[0] = text;
63            }
64        }
65    }
66
67    pub fn height(&self) -> u16 {
68        if !self.show {
69            return 0;
70        }
71        let mut height = self.height;
72        height += self.config.border.height();
73
74        height
75    }
76
77    /// Set text and visibility. Compute wrapped height.
78    pub fn set(&mut self, text: impl Into<Text<'static>>) {
79        let (text, _) = wrap_text(text.into(), self.config.wrap as u16 * self.width);
80        self.text = vec![text];
81        self.text_split_index = 1;
82        self.show = true;
83    }
84
85    pub fn clear(&mut self) {
86        self.show = false;
87        self.text.clear();
88        self.text_split_index = 0;
89    }
90
91    pub fn single(&self) -> bool {
92        self.text_split_index == 1
93    }
94
95    pub fn header_columns(&mut self, columns: Vec<Text<'static>>) {
96        self.text.truncate(self.text_split_index);
97        self.text.extend(columns);
98    }
99
100    // todo: lowpri: cache texts to not have to always rewrap?
101    pub fn make_display(
102        &mut self,
103        result_indentation: u16,
104        mut widths: Vec<u16>,
105        col_spacing: u16,
106    ) -> Table<'_> {
107        if self.text.is_empty() || widths.is_empty() {
108            return Table::default();
109        }
110
111        let block = {
112            let b = self.config.border.as_block();
113            if self.config.match_indent {
114                let mut padding = self.config.border.padding;
115
116                padding.left = result_indentation.saturating_sub(self.config.border.left());
117                widths[0] -= result_indentation;
118                b.padding(padding)
119            } else {
120                b
121            }
122        };
123
124        let style = Style::default()
125            .fg(self.config.fg)
126            .add_modifier(self.config.modifier);
127
128        let (cells, height) = if self.text_split_index == 1 {
129            // Single Cell (Full Width)
130            // reflow is handled in update_width
131            let cells = if self.text_split_index < self.text.len() {
132                vec![]
133            } else {
134                vec![Cell::from(self.text[0].clone())]
135            };
136            let height = self.text[0].height() as u16;
137
138            (cells, height)
139        } else {
140            let mut height = 0;
141            // 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.
142            let cells = self.text[..self.text_split_index]
143                .iter()
144                .cloned()
145                .enumerate()
146                .map(|(i, text)| {
147                    let mut ret = wrap_text(text, widths[i]).0;
148                    height = height.max(ret.height() as u16);
149
150                    Cell::from(ret.transform_if(
151                        matches!(
152                            self.config.row_connection_style,
153                            RowConnectionStyle::Disjoint
154                        ),
155                        |r| r.style(style),
156                    ))
157                })
158                .collect();
159
160            (cells, height)
161        };
162
163        let row = Row::new(cells).style(style).height(height);
164        let mut rows = vec![row];
165
166        if self.text_split_index < self.text.len() {
167            self.height = height;
168            let mut height = 0;
169
170            let cells = self.text[self.text_split_index..].iter().map(|x| {
171                height = height.max(x.height() as u16);
172                Cell::from(x.clone())
173            });
174
175            rows.push(Row::new(cells).style(style).height(height));
176
177            self.height += height;
178        }
179
180        Table::new(rows, widths.to_vec())
181            .block(block)
182            .column_spacing(col_spacing)
183            .transform_if(
184                !matches!(
185                    self.config.row_connection_style,
186                    RowConnectionStyle::Disjoint
187                ),
188                |t| t.style(style),
189            )
190    }
191
192    /// Draw in the same area as display when self.single() to produce a full width row over the table area
193    pub fn make_full_width_row(&self, result_indentation: u16) -> Paragraph<'_> {
194        let style = Style::default()
195            .fg(self.config.fg)
196            .add_modifier(self.config.modifier);
197
198        // Compute padding
199        let left = if self.config.match_indent {
200            result_indentation.saturating_sub(self.config.border.left())
201        } else {
202            self.config.border.left()
203        };
204        let top = self.config.border.top();
205        let right = self.config.border.width().saturating_sub(left);
206        let bottom = self.config.border.height() - top;
207
208        let block = ratatui::widgets::Block::default().padding(ratatui::widgets::Padding {
209            left,
210            top,
211            right,
212            bottom,
213        });
214
215        // Paragraph with the first text element and correct padding
216        Paragraph::new(self.text[0].clone())
217            .block(block)
218            .style(style)
219    }
220}