Skip to main content

matchmaker/ui/
display.rs

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