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    // -------- Layout -----------
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    pub fn cycle_layout(&mut self) {
155        self.layout_idx = (self.layout_idx + 1) % self.config.layout.len()
156    }
157    pub fn set_layout(&mut self, idx: u8) -> bool {
158        let idx = idx as usize;
159        if idx < self.config.layout.len() {
160            let changed = self.layout_idx != idx;
161            self.layout_idx = idx;
162            changed
163        } else {
164            error!("Layout idx {idx} out of bounds, ignoring.");
165            false
166        }
167    }
168
169    // ----- config && getters ---------
170
171    pub fn show(&mut self, show: bool) -> bool {
172        log::trace!("toggle preview with: {show}");
173        let changed = self.show != show;
174        self.show = show;
175        changed
176    }
177
178    pub fn toggle_show(&mut self) {
179        self.show = !self.show;
180    }
181
182    pub fn wrap(&mut self, wrap: bool) {
183        self.config.wrap = wrap;
184    }
185    pub fn is_wrap(&self) -> bool {
186        self.config.wrap
187    }
188    pub fn offset(&self) -> usize {
189        self.config.scroll.header_lines + self.offset
190    }
191    pub fn target_line(&self) -> Option<usize> {
192        self.target
193    }
194
195    // ----- actions --------
196    pub fn up(&mut self, n: u16) {
197        let total_lines = self.view.len();
198        let n = n as usize;
199
200        if self.offset >= n {
201            self.offset -= n;
202        } else if self.config.scroll_wrap {
203            self.offset = total_lines.saturating_sub(n - self.offset);
204        } else {
205            self.offset = 0;
206        }
207    }
208    pub fn down(&mut self, n: u16) {
209        let total_lines = self.view.len();
210        let n = n as usize;
211
212        if self.offset + n > total_lines {
213            if self.config.scroll_wrap {
214                self.offset = 0;
215            } else {
216                self.offset = total_lines;
217            }
218        } else {
219            self.offset += n;
220        }
221    }
222
223    pub fn scroll(&mut self, horizontal: bool, val: i8) {
224        let a = &mut self.scroll[horizontal as usize];
225
226        if val == 0 {
227            *a = 0;
228        } else {
229            let new = (*a as i8 + val).clamp(0, u16::MAX as i8);
230            *a = new as u16;
231        }
232    }
233
234    pub fn set_target(&mut self, target: Option<isize>) {
235        let results = self.view.results().lines;
236        let line_count = results.len();
237
238        let Some(mut target) = target else {
239            self.target = None;
240            self.offset = 0;
241            return;
242        };
243
244        target += self.config.scroll.offset;
245
246        self.target = Some(if target < 0 {
247            line_count.saturating_sub(target.unsigned_abs())
248        } else {
249            target as usize
250        });
251
252        let index = self.target.unwrap();
253
254        self.offset = if index >= results.len() {
255            self.attained_target = false;
256            results.len().saturating_sub(self.area.height as usize / 2)
257        } else {
258            self.attained_target = true;
259            self.target_to_offset(index, &results)
260        };
261
262        log::trace!("Preview initial offset: {}, index: {}", self.offset, index);
263    }
264
265    fn target_to_offset(&self, mut target: usize, results: &Vec<Line>) -> usize {
266        // decrement the index to put the target lower on the page.
267        // The resulting height up to the top of target should >= p% of height.
268        let mut lines_above =
269            self.config
270                .scroll
271                .percentage
272                .complement()
273                .compute_clamped(self.area.height, 0, 0);
274
275        // shoddy approximation to how Paragraph wraps lines
276        while target > 0 && lines_above > 0 {
277            let prev = results
278                .get(target)
279                .map(|x| wrapped_line_height(x, self.area.width))
280                .unwrap_or(1);
281            if prev > lines_above {
282                break;
283            } else {
284                target -= 1;
285                lines_above -= prev;
286            }
287        }
288
289        target
290    }
291    // --------------------------
292
293    pub fn make_preview(&mut self) -> Paragraph<'_> {
294        let results = self.view.results();
295        let rl = results.lines.len();
296        let height = self.area.height as usize;
297
298        if let Some(target) = self.target
299            && !self.attained_target
300            && target < rl
301        {
302            self.offset = self.target_to_offset(target, &results.lines);
303            self.attained_target = true;
304        };
305
306        let mut results = results.into_iter();
307
308        if height == 0 {
309            return Paragraph::new(Vec::new());
310        }
311
312        let mut lines = Vec::with_capacity(height);
313
314        for _ in 0..self.config.scroll.header_lines.min(height) {
315            if let Some(line) = results.next() {
316                lines.push(line);
317            } else {
318                break;
319            };
320        }
321
322        let mut results = results.skip(self.offset);
323
324        for _ in self.config.scroll.header_lines..height {
325            if let Some(line) = results.next() {
326                lines.push(line);
327            }
328        }
329
330        let mut preview = Paragraph::new(lines);
331        preview = preview.block(self.border().as_block());
332        if self.config.wrap {
333            preview = preview
334                .wrap(Wrap { trim: false })
335                .scroll(self.scroll.into());
336        }
337        preview
338    }
339}