Skip to main content

matchmaker/ui/
display.rs

1use cli_boilerplate_automation::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::{serde::StringOrVec, text::wrap_text},
12};
13pub type HeaderTable = Vec<Vec<Line<'static>>>;
14#[derive(Debug)]
15pub struct DisplayUI {
16    width: u16,
17    height: u16,
18    text: Vec<Text<'static>>,
19    header: HeaderTable, // lines from input
20    pub show: bool,
21    pub config: DisplayConfig,
22}
23
24impl DisplayUI {
25    pub fn new(config: DisplayConfig) -> Self {
26        let (text, height) = match &config.content {
27            Some(StringOrVec::String(s)) => {
28                let text = Text::from(s.clone());
29                let height = text.height() as u16;
30                (vec![text], height)
31            }
32            Some(StringOrVec::Vec(s)) => {
33                let text: Vec<_> = s.iter().map(|s| Text::from(s.clone())).collect();
34                let height = text.iter().map(|t| t.height()).max().unwrap_or_default() as u16;
35                (text, height)
36            }
37            _ => (vec![], 0),
38        };
39
40        Self {
41            height,
42            width: 0,
43            show: config.content.is_some() || config.header_lines > 0,
44            header: Vec::new(),
45            text,
46            config,
47        }
48    }
49
50    pub fn update_width(&mut self, width: u16) {
51        let border_w = self.config.border.width();
52        let new_w = width.saturating_sub(border_w);
53        if new_w != self.width {
54            self.width = new_w;
55            // only rewrap of single cell is supported for now
56            if self.config.wrap && self.single() {
57                let text = wrap_text(self.text[0].clone(), self.width).0;
58                self.text[0] = text;
59            }
60        }
61    }
62
63    pub fn height(&self) -> u16 {
64        if !self.show {
65            return 0;
66        }
67        let mut height = self.height;
68        height += self.config.border.height();
69
70        height
71    }
72
73    /// Set text and visibility. Compute wrapped height.
74    pub fn set(&mut self, text: impl Into<Text<'static>>) {
75        let (text, _) = wrap_text(text.into(), self.config.wrap as u16 * self.width);
76
77        self.text = vec![text];
78
79        self.show = true;
80    }
81
82    pub fn clear(&mut self, keep_header: bool) {
83        if !keep_header {
84            self.header.clear();
85            self.show = false;
86        } else if self.header.is_empty() {
87            self.show = false;
88        }
89
90        self.text.clear();
91    }
92
93    /// Whether this is table has just one column
94    pub fn single(&self) -> bool {
95        self.text.len() == 1
96    }
97
98    pub fn header_table(&mut self, table: HeaderTable) {
99        self.header = table
100    }
101
102    // todo: lowpri: cache texts to not have to always rewrap?
103    pub fn make_display(
104        &mut self,
105        result_indentation: u16,
106        mut widths: Vec<u16>,
107        col_spacing: u16,
108    ) -> Table<'_> {
109        if self.text.is_empty() || widths.is_empty() {
110            return Table::default();
111        }
112
113        let block = {
114            let b = self.config.border.as_block();
115            if self.config.match_indent {
116                let mut padding = self.config.border.padding;
117
118                padding.left = result_indentation.saturating_sub(self.config.border.left());
119                widths[0] -= result_indentation;
120                b.padding(padding)
121            } else {
122                b
123            }
124        };
125
126        let style = Style::default()
127            .fg(self.config.fg)
128            .add_modifier(self.config.modifier);
129
130        let (cells, height) = if self.single() {
131            // Single Cell (Full Width)
132            // reflow is handled in update_width
133            let cells = if self.text.len() > 1 {
134                vec![]
135            } else {
136                vec![Cell::from(self.text[0].clone())]
137            };
138            let height = self.text[0].height() as u16;
139
140            (cells, height)
141        } else {
142            let mut height = 0;
143            // todo: lowpri: is this wrapping behavior good enough?
144            let cells = self
145                .text
146                .iter()
147                .cloned()
148                .zip(widths.iter().copied())
149                .map(|(text, width)| {
150                    let ret = wrap_text(text, width).0;
151                    height = height.max(ret.height() as u16);
152
153                    Cell::from(ret.transform_if(
154                        matches!(
155                            self.config.row_connection_style,
156                            RowConnectionStyle::Disjoint
157                        ),
158                        |r| r.style(style),
159                    ))
160                })
161                .collect();
162
163            (cells, height)
164        };
165
166        let row = Row::new(cells).style(style).height(height);
167        let mut rows = vec![row];
168        self.height = height;
169
170        // add header cells
171        if !self.header.is_empty() {
172            // todo: support wrapping
173            rows.extend(self.header.iter().cloned().map(Row::new));
174
175            self.height += height;
176        }
177
178        let widths = if self.single() {
179            vec![Constraint::Percentage(100)]
180        } else {
181            widths.into_iter().map(Constraint::Length).collect()
182        };
183
184        Table::new(rows, widths)
185            .block(block)
186            .column_spacing(col_spacing)
187            .transform_if(
188                !matches!(
189                    self.config.row_connection_style,
190                    RowConnectionStyle::Disjoint
191                ),
192                |t| t.style(style),
193            )
194    }
195
196    /// Draw in the same area as display when self.single() to produce a full width row over the table area
197    pub fn make_full_width_row(&self, result_indentation: u16) -> Paragraph<'_> {
198        let style = Style::default()
199            .fg(self.config.fg)
200            .add_modifier(self.config.modifier);
201
202        // Compute padding
203        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(style)
222    }
223}