Skip to main content

matchmaker/ui/
preview.rs

1use log::error;
2use ratatui::{
3    layout::Rect,
4    text::Line,
5    widgets::{Paragraph, Wrap},
6};
7
8use crate::{
9    config::{BorderSetting, PreviewConfig, PreviewSetting, ShowCondition, Side},
10    preview::Preview,
11    utils::text::wrapped_line_height,
12};
13
14#[derive(Debug)]
15pub struct PreviewUI {
16    pub view: Preview,
17    pub config: PreviewConfig,
18    layout_idx: usize,
19    /// content area
20    pub(crate) area: Rect,
21    pub scroll: [u16; 2],
22    offset: usize,
23    target: Option<usize>,
24    attained_target: bool,
25
26    show: bool,
27}
28
29impl PreviewUI {
30    pub fn new(view: Preview, mut config: PreviewConfig, [ui_width, ui_height]: [u16; 2]) -> Self {
31        for x in &mut config.layout {
32            if let Some(b) = &mut x.border
33                && b.sides.is_none()
34                && !b.is_empty()
35            {
36                b.sides = Some(x.layout.side.opposite())
37            }
38        }
39
40        let show = match config.show {
41            ShowCondition::Free(x) => {
42                if let Some(l) = config.layout.first() {
43                    match l.layout.side {
44                        Side::Bottom | Side::Top => ui_height >= x,
45                        _ => ui_width >= x,
46                    }
47                } else {
48                    false
49                }
50            }
51            ShowCondition::Bool(x) => x,
52        };
53
54        // enforce invariant of valid index
55        if config.layout.is_empty() {
56            let mut s = PreviewSetting::default();
57            s.layout.max = 0;
58            config.layout.push(s);
59        }
60
61        Self {
62            view,
63            config,
64            layout_idx: 0,
65            scroll: Default::default(),
66            offset: 0,
67            area: Rect::default(),
68            target: None,
69            attained_target: false,
70            show,
71        }
72    }
73
74    pub fn update_dimensions(&mut self, area: &Rect) {
75        let mut height = area.height;
76        height -= self.config.border.height().min(height);
77        self.area.height = height;
78
79        let mut width = area.width;
80        width -= self.config.border.width().min(width);
81        self.area.width = width;
82    }
83
84    pub fn reevaluate_show_condition(&mut self, [ui_width, ui_height]: [u16; 2], hide: bool) {
85        match self.config.show {
86            ShowCondition::Free(x) => {
87                if let Some(setting) = self.setting() {
88                    let l = &setting.layout;
89
90                    let show = match l.side {
91                        Side::Bottom | Side::Top => ui_height >= x,
92                        _ => ui_width >= x,
93                    };
94                    log::debug!(
95                        "Evaluated ShowCondition(Free({x})) against {ui_width}x{ui_height} => {show}"
96                    );
97                    if !hide && !show {
98                        return;
99                    }
100
101                    self.show(show);
102                };
103            }
104            ShowCondition::Bool(show) => {
105                if !hide && !show {
106                    return;
107                }
108                self.show(show);
109            }
110        };
111    }
112
113    // -------- Setting getters -----------
114    /// None if not show OR if max = 0 (disabled layour)
115    pub fn setting(&self) -> Option<&PreviewSetting> {
116        // if let Some(ret) = self.config.layout.get(self.layout_idx)
117        if let ret = &self.config.layout[self.layout_idx]
118            && ret.layout.max != 0
119        {
120            Some(&ret)
121        } else {
122            None
123        }
124    }
125
126    pub fn visible(&self) -> bool {
127        self.setting().is_some() && self.show
128    }
129
130    pub fn command(&self) -> &str {
131        self.setting().map(|x| x.command.as_str()).unwrap_or("")
132    }
133
134    pub fn border(&self) -> &BorderSetting {
135        self.setting()
136            .and_then(|s| s.border.as_ref())
137            .unwrap_or(&self.config.border)
138    }
139
140    pub fn get_initial_command(&self) -> &str {
141        let x = self.command();
142        if !x.is_empty() {
143            return x;
144        }
145
146        self.config
147            .layout
148            .iter()
149            .map(|l| l.command.as_str())
150            .find(|cmd| !cmd.is_empty())
151            .unwrap_or("")
152    }
153
154    // -------- Layout -----------
155    pub fn cycle_layout(&mut self) {
156        self.layout_idx = (self.layout_idx + 1) % self.config.layout.len()
157    }
158    pub fn set_layout(&mut self, idx: u8) -> bool {
159        let idx = idx as usize;
160        if idx < self.config.layout.len() {
161            let changed = self.layout_idx != idx;
162            self.layout_idx = idx;
163            changed
164        } else {
165            error!("Layout idx {idx} out of bounds, ignoring.");
166            false
167        }
168    }
169
170    // ----- config && getters ---------
171
172    pub fn show(&mut self, show: bool) -> bool {
173        log::trace!("toggle preview with: {show}");
174        let changed = self.show != show;
175        self.show = show;
176        changed
177    }
178
179    pub fn toggle_show(&mut self) {
180        self.show = !self.show;
181    }
182
183    pub fn wrap(&mut self, wrap: bool) {
184        self.config.wrap = wrap;
185    }
186    pub fn is_wrap(&self) -> bool {
187        self.config.wrap
188    }
189    pub fn offset(&self) -> usize {
190        self.config.initial.header_lines + self.offset
191    }
192    pub fn target_line(&self) -> Option<usize> {
193        self.target
194    }
195
196    // ----- actions --------
197    pub fn up(&mut self, n: u16) {
198        let total_lines = self.view.len();
199        let n = n as usize;
200
201        if self.offset >= n {
202            self.offset -= n;
203        } else if self.config.scroll_wrap {
204            self.offset = total_lines.saturating_sub(n - self.offset);
205        } else {
206            self.offset = 0;
207        }
208    }
209    pub fn down(&mut self, n: u16) {
210        let total_lines = self.view.len();
211        let n = n as usize;
212
213        if self.offset + n > total_lines {
214            if self.config.scroll_wrap {
215                self.offset = 0;
216            } else {
217                self.offset = total_lines;
218            }
219        } else {
220            self.offset += n;
221        }
222    }
223
224    pub fn scroll(&mut self, horizontal: bool, val: i8) {
225        let a = &mut self.scroll[horizontal as usize];
226
227        if val == 0 {
228            *a = 0;
229        } else {
230            let new = (*a as i8 + val).clamp(0, u16::MAX as i8);
231            *a = new as u16;
232        }
233    }
234
235    pub fn set_target(&mut self, target: Option<isize>) {
236        let results = self.view.results().lines;
237        let line_count = results.len();
238
239        let Some(mut target) = target else {
240            self.target = None;
241            self.offset = 0;
242            return;
243        };
244
245        target += self.config.initial.offset;
246
247        self.target = Some(if target < 0 {
248            line_count.saturating_sub(target.unsigned_abs())
249        } else {
250            target as usize
251        });
252
253        let index = self.target.unwrap();
254
255        self.offset = if index >= results.len() {
256            self.attained_target = false;
257            results.len().saturating_sub(self.area.height as usize / 2)
258        } else {
259            self.attained_target = true;
260            self.target_to_offset(index, &results)
261        };
262
263        log::trace!("Preview initial offset: {}, index: {}", self.offset, index);
264    }
265
266    fn target_to_offset(&self, mut target: usize, results: &Vec<Line>) -> usize {
267        // decrement the index to put the target lower on the page.
268        // The resulting height up to the top of target should >= p% of height.
269        let mut lines_above =
270            self.config
271                .initial
272                .percentage
273                .complement()
274                .compute_clamped(self.area.height, 0, 0);
275
276        // shoddy approximation to how Paragraph wraps lines
277        while target > 0 && lines_above > 0 {
278            let prev = results
279                .get(target)
280                .map(|x| wrapped_line_height(x, self.area.width))
281                .unwrap_or(1);
282            if prev > lines_above {
283                break;
284            } else {
285                target -= 1;
286                lines_above -= prev;
287            }
288        }
289
290        target
291    }
292    // --------------------------
293
294    pub fn make_preview(&mut self) -> Paragraph<'_> {
295        let results = self.view.results();
296        let rl = results.lines.len();
297        let height = self.area.height as usize;
298
299        if let Some(target) = self.target
300            && !self.attained_target
301            && target < rl
302        {
303            self.offset = self.target_to_offset(target, &results.lines);
304            self.attained_target = true;
305        };
306
307        let mut results = results.into_iter();
308
309        if height == 0 {
310            return Paragraph::new(Vec::new());
311        }
312
313        let mut lines = Vec::with_capacity(height);
314
315        for _ in 0..self.config.initial.header_lines.min(height) {
316            if let Some(line) = results.next() {
317                lines.push(line);
318            } else {
319                break;
320            };
321        }
322
323        let mut results = results.skip(self.offset);
324
325        for _ in self.config.initial.header_lines..height {
326            if let Some(line) = results.next() {
327                lines.push(line);
328            }
329        }
330
331        let mut preview = Paragraph::new(lines);
332        preview = preview.block(self.border().as_block());
333        if self.config.wrap {
334            preview = preview
335                .wrap(Wrap { trim: false })
336                .scroll(self.scroll.into());
337        }
338        preview
339    }
340}