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::{Line, 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};
18pub type HeaderTable = Vec<Vec<Line<'static>>>;
19#[derive(Debug)]
20pub struct DisplayUI {
21    width: u16,
22    height: u16,
23    text: Vec<Text<'static>>,
24    header: HeaderTable,
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            header: Vec::new(),
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.single() {
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: bool) {
80        let (text, _) = wrap_text(text.into(), self.config.wrap as u16 * self.width);
81
82        self.text = vec![text];
83
84        if !keep_header {
85            self.header.clear();
86        }
87
88        self.show = true;
89    }
90
91    pub fn clear(&mut self, keep_header: bool) {
92        self.show = false;
93
94        if !keep_header {
95            self.header.clear();
96        }
97
98        self.text.clear();
99    }
100
101    /// Whether this is table has just one column
102    pub fn single(&self) -> bool {
103        self.text.len() == 1
104    }
105
106    pub fn header_table(&mut self, table: HeaderTable) {
107        self.header = table
108    }
109
110    // todo: lowpri: cache texts to not have to always rewrap?
111    pub fn make_display(
112        &mut self,
113        result_indentation: u16,
114        mut widths: Vec<u16>,
115        col_spacing: u16,
116    ) -> Table<'_> {
117        if self.text.is_empty() || widths.is_empty() {
118            return Table::default();
119        }
120
121        let block = {
122            let b = self.config.border.as_block();
123            if self.config.match_indent {
124                let mut padding = self.config.border.padding;
125
126                padding.left = result_indentation.saturating_sub(self.config.border.left());
127                widths[0] -= result_indentation;
128                b.padding(padding)
129            } else {
130                b
131            }
132        };
133
134        let style = Style::default()
135            .fg(self.config.fg)
136            .add_modifier(self.config.modifier);
137
138        let (cells, height) = if self.single() {
139            // Single Cell (Full Width)
140            // reflow is handled in update_width
141            let cells = if self.text.len() > 1 {
142                vec![]
143            } else {
144                vec![Cell::from(self.text[0].clone())]
145            };
146            let height = self.text[0].height() as u16;
147
148            (cells, height)
149        } else {
150            let mut height = 0;
151            // todo: lowpri: is this wrapping behavior good enough?
152            let cells = self
153                .text
154                .iter()
155                .cloned()
156                .zip(widths.iter().copied())
157                .map(|(text, width)| {
158                    let mut ret = wrap_text(text, width).0;
159                    height = height.max(ret.height() as u16);
160
161                    Cell::from(ret.transform_if(
162                        matches!(
163                            self.config.row_connection_style,
164                            RowConnectionStyle::Disjoint
165                        ),
166                        |r| r.style(style),
167                    ))
168                })
169                .collect();
170
171            (cells, height)
172        };
173
174        let row = Row::new(cells).style(style).height(height);
175        let mut rows = vec![row];
176        self.height = height;
177
178        // add header cells
179        if !self.header.is_empty() {
180            let mut height = self.header.len() as u16;
181            // todo: support wrapping
182            rows.extend(self.header.iter().cloned().map(Row::new));
183
184            self.height += height;
185        }
186
187        let widths = if self.single() {
188            vec![Constraint::Percentage(100)]
189        } else {
190            widths.into_iter().map(Constraint::Length).collect()
191        };
192
193        Table::new(rows, widths)
194            .block(block)
195            .column_spacing(col_spacing)
196            .transform_if(
197                !matches!(
198                    self.config.row_connection_style,
199                    RowConnectionStyle::Disjoint
200                ),
201                |t| t.style(style),
202            )
203    }
204
205    /// Draw in the same area as display when self.single() to produce a full width row over the table area
206    pub fn make_full_width_row(&self, result_indentation: u16) -> Paragraph<'_> {
207        let style = Style::default()
208            .fg(self.config.fg)
209            .add_modifier(self.config.modifier);
210
211        // Compute padding
212        let left = if self.config.match_indent {
213            result_indentation.saturating_sub(self.config.border.left())
214        } else {
215            self.config.border.left()
216        };
217        let top = self.config.border.top();
218        let right = self.config.border.width().saturating_sub(left);
219        let bottom = self.config.border.height() - top;
220
221        let block = ratatui::widgets::Block::default().padding(ratatui::widgets::Padding {
222            left,
223            top,
224            right,
225            bottom,
226        });
227
228        Paragraph::new(self.text[0].clone())
229            .block(block)
230            .style(style)
231    }
232}