Skip to main content

matchmaker/ui/
display.rs

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