Skip to main content

matchmaker/ui/
preview.rs

1use log::error;
2use ratatui::{
3    layout::Rect,
4    widgets::{Paragraph, Wrap},
5};
6
7use crate::{
8    config::{PreviewConfig, PreviewLayout},
9    preview::Preview,
10    utils::text::wrapped_line_height,
11};
12
13#[derive(Debug)]
14pub struct PreviewUI {
15    pub view: Preview,
16    pub config: PreviewConfig,
17    pub layout_idx: usize,
18    /// content area
19    pub area: Rect,
20    pub scroll: [u16; 2],
21    offset: u16,
22    target: Option<usize>,
23}
24
25impl PreviewUI {
26    pub fn new(view: Preview, config: PreviewConfig) -> Self {
27        Self {
28            view,
29            config,
30            layout_idx: 0,
31            scroll: Default::default(),
32            offset: 0,
33            area: Rect::default(),
34            target: None,
35        }
36    }
37    pub fn update_dimensions(&mut self, area: &Rect) {
38        let mut height = area.height;
39        height -= self.config.border.height().min(height);
40        self.area.height = height;
41
42        let mut width = area.width;
43        width -= self.config.border.width().min(width);
44        self.area.width = width;
45    }
46
47    // -------- Layout -----------
48    // None if not show
49    pub fn layout(&self) -> Option<&PreviewLayout> {
50        if !self.config.show || self.config.layout.is_empty() {
51            None
52        } else {
53            let ret = &self.config.layout[self.layout_idx].layout;
54            if ret.max == 0 { None } else { Some(ret) }
55        }
56    }
57    pub fn command(&self) -> &str {
58        if self.config.layout.is_empty() {
59            ""
60        } else {
61            self.config.layout[self.layout_idx].command.as_str()
62        }
63    }
64    pub fn cycle_layout(&mut self) {
65        self.layout_idx = (self.layout_idx + 1) % self.config.layout.len()
66    }
67    pub fn set_layout(&mut self, idx: u8) -> bool {
68        let idx = idx as usize;
69        if idx <= self.config.layout.len() {
70            let changed = self.layout_idx != idx;
71            self.layout_idx = idx;
72            changed
73        } else {
74            error!("Layout idx {idx} out of bounds, ignoring.");
75            false
76        }
77    }
78
79    // ----- config ---------
80    pub fn is_show(&self) -> bool {
81        self.layout().is_some()
82    }
83    // cheap show toggle + change tracking
84    pub fn show(&mut self, show: bool) -> bool {
85        let previous = self.config.show;
86        self.config.show = show;
87        previous != show
88    }
89    pub fn toggle_show(&mut self) {
90        self.config.show = !self.config.show;
91    }
92
93    pub fn wrap(&mut self, wrap: bool) {
94        self.config.wrap = wrap;
95    }
96    pub fn is_wrap(&self) -> bool {
97        self.config.wrap
98    }
99
100    // ----- actions --------
101    pub fn up(&mut self, n: u16) {
102        if self.offset >= n {
103            self.offset -= n;
104        } else if self.config.scroll_wrap {
105            let total_lines = self.view.len() as u16;
106            self.offset = total_lines.saturating_sub(n - self.offset);
107        } else {
108            self.offset = 0;
109        }
110    }
111    pub fn down(&mut self, n: u16) {
112        let total_lines = self.view.len() as u16;
113
114        if self.offset + n > total_lines {
115            if self.config.scroll_wrap {
116                self.offset = 0;
117            } else {
118                self.offset = total_lines;
119            }
120        } else {
121            self.offset += n;
122        }
123    }
124
125    pub fn scroll(&mut self, horizontal: bool, val: i8) {
126        let a = &mut self.scroll[horizontal as usize];
127
128        if val == 0 {
129            *a = 0;
130        } else {
131            let new = (*a as i8 + val).clamp(0, u16::MAX as i8);
132            *a = new as u16;
133        }
134    }
135
136    pub fn set_target(&mut self, mut target: isize) {
137        target += self.config.scroll.offset;
138        self.target = Some(if target < 0 {
139            self.view.len().saturating_sub(target.unsigned_abs())
140        } else {
141            self.view.len().min(target.unsigned_abs())
142        });
143        let mut index = self.target.unwrap();
144
145        // decrement the index to put the target lower on the page.
146        // The resulting height up to the top of target should >= p% of height.
147        let results = self.view.results().lines;
148        let mut lines_above =
149            self.config
150                .scroll
151                .percentage
152                .complement()
153                .compute_clamped(self.area.height, 0, 0);
154        // shoddy approximation to how Paragraph wraps lines
155        while index > 0 && lines_above > 0 {
156            let prev = wrapped_line_height(&results[index], self.area.width);
157            if prev > lines_above {
158                break;
159            } else {
160                index -= 1;
161                lines_above -= prev;
162            }
163        }
164        self.offset = index as u16;
165        log::trace!("offset: {}, index: {}", self.offset, self.target.unwrap());
166    }
167
168    // --------------------------
169
170    pub fn make_preview(&self) -> Paragraph<'_> {
171        let mut results = self.view.results().into_iter();
172        let height = self.area.height as usize;
173        if height == 0 {
174            return Paragraph::new(Vec::new());
175        }
176
177        let mut lines = Vec::with_capacity(height);
178
179        for _ in 0..self.config.scroll.header_lines.min(height) {
180            if let Some(line) = results.next() {
181                lines.push(line);
182            } else {
183                break;
184            };
185        }
186        let mut results = results.skip(self.offset as usize);
187        for _ in self.config.scroll.header_lines..height {
188            if let Some(line) = results.next() {
189                lines.push(line);
190            }
191        }
192
193        let mut preview = Paragraph::new(lines);
194        preview = preview.block(self.config.border.as_block());
195        if self.config.wrap {
196            preview = preview.wrap(Wrap { trim: true }).scroll(self.scroll.into());
197        }
198        preview
199    }
200}