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::{BorderSetting, 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, mut config: PreviewConfig) -> Self {
27        // todo: lowpri: this is not strictly correct
28        for x in &mut config.layout {
29            if let Some(b) = &mut x.border
30                && b.sides.is_none()
31            {
32                b.sides = Some(x.layout.side.opposite())
33            }
34        }
35
36        Self {
37            view,
38            config,
39            layout_idx: 0,
40            scroll: Default::default(),
41            offset: 0,
42            area: Rect::default(),
43            target: None,
44        }
45    }
46    pub fn update_dimensions(&mut self, area: &Rect) {
47        let mut height = area.height;
48        height -= self.config.border.height().min(height);
49        self.area.height = height;
50
51        let mut width = area.width;
52        width -= self.config.border.width().min(width);
53        self.area.width = width;
54    }
55
56    // -------- Layout -----------
57    /// None if not show
58    pub fn layout(&self) -> Option<&PreviewLayout> {
59        if !self.config.show || self.config.layout.is_empty() {
60            None
61        } else {
62            let ret = &self.config.layout[self.layout_idx].layout;
63            if ret.max == 0 { None } else { Some(ret) }
64        }
65    }
66    pub fn command(&self) -> &str {
67        if self.config.layout.is_empty() {
68            ""
69        } else {
70            self.config.layout[self.layout_idx].command.as_str()
71        }
72    }
73
74    pub fn border(&self) -> &BorderSetting {
75        self.config.layout[self.layout_idx]
76            .border
77            .as_ref()
78            .unwrap_or(&self.config.border)
79    }
80
81    pub fn get_initial_command(&self) -> &str {
82        if let Some(current) = self.config.layout.get(self.layout_idx) {
83            if !current.command.is_empty() {
84                return current.command.as_str();
85            }
86        }
87
88        self.config
89            .layout
90            .iter()
91            .map(|l| l.command.as_str())
92            .find(|cmd| !cmd.is_empty())
93            .unwrap_or("")
94    }
95
96    pub fn cycle_layout(&mut self) {
97        self.layout_idx = (self.layout_idx + 1) % self.config.layout.len()
98    }
99    pub fn set_layout(&mut self, idx: u8) -> bool {
100        let idx = idx as usize;
101        if idx < self.config.layout.len() {
102            let changed = self.layout_idx != idx;
103            self.layout_idx = idx;
104            changed
105        } else {
106            error!("Layout idx {idx} out of bounds, ignoring.");
107            false
108        }
109    }
110
111    // ----- config ---------
112    pub fn is_show(&self) -> bool {
113        self.layout().is_some()
114    }
115    // cheap show toggle + change tracking
116    pub fn show(&mut self, show: bool) -> bool {
117        let previous = self.config.show;
118        self.config.show = show;
119        previous != show
120    }
121    pub fn toggle_show(&mut self) {
122        self.config.show = !self.config.show;
123    }
124
125    pub fn wrap(&mut self, wrap: bool) {
126        self.config.wrap = wrap;
127    }
128    pub fn is_wrap(&self) -> bool {
129        self.config.wrap
130    }
131
132    // ----- actions --------
133    pub fn up(&mut self, n: u16) {
134        if self.offset >= n {
135            self.offset -= n;
136        } else if self.config.scroll_wrap {
137            let total_lines = self.view.len() as u16;
138            self.offset = total_lines.saturating_sub(n - self.offset);
139        } else {
140            self.offset = 0;
141        }
142    }
143    pub fn down(&mut self, n: u16) {
144        let total_lines = self.view.len() as u16;
145
146        if self.offset + n > total_lines {
147            if self.config.scroll_wrap {
148                self.offset = 0;
149            } else {
150                self.offset = total_lines;
151            }
152        } else {
153            self.offset += n;
154        }
155    }
156
157    pub fn scroll(&mut self, horizontal: bool, val: i8) {
158        let a = &mut self.scroll[horizontal as usize];
159
160        if val == 0 {
161            *a = 0;
162        } else {
163            let new = (*a as i8 + val).clamp(0, u16::MAX as i8);
164            *a = new as u16;
165        }
166    }
167
168    pub fn set_target(&mut self, mut target: isize) {
169        let results = self.view.results().lines;
170        let line_count = results.len();
171
172        target += self.config.scroll.offset;
173        self.target = Some(if target < 0 {
174            line_count.saturating_sub(target.unsigned_abs())
175        } else {
176            line_count.saturating_sub(1).min(target.unsigned_abs())
177        });
178        let mut index = self.target.unwrap();
179
180        // decrement the index to put the target lower on the page.
181        // The resulting height up to the top of target should >= p% of height.
182        let mut lines_above =
183            self.config
184                .scroll
185                .percentage
186                .complement()
187                .compute_clamped(self.area.height, 0, 0);
188        // shoddy approximation to how Paragraph wraps lines
189        while index > 0 && lines_above > 0 {
190            let prev = wrapped_line_height(&results[index], self.area.width);
191            if prev > lines_above {
192                break;
193            } else {
194                index -= 1;
195                lines_above -= prev;
196            }
197        }
198        self.offset = u16::try_from(index).unwrap_or(u16::MAX);
199        log::trace!("offset: {}, index: {}", self.offset, self.target.unwrap());
200    }
201
202    // --------------------------
203
204    pub fn make_preview(&self) -> Paragraph<'_> {
205        assert!(self.is_show());
206
207        let mut results = self.view.results().into_iter();
208        let height = self.area.height as usize;
209        if height == 0 {
210            return Paragraph::new(Vec::new());
211        }
212
213        let mut lines = Vec::with_capacity(height);
214
215        for _ in 0..self.config.scroll.header_lines.min(height) {
216            if let Some(line) = results.next() {
217                lines.push(line);
218            } else {
219                break;
220            };
221        }
222        let mut results = results.skip(self.offset as usize);
223        for _ in self.config.scroll.header_lines..height {
224            if let Some(line) = results.next() {
225                lines.push(line);
226            }
227        }
228
229        let mut preview = Paragraph::new(lines);
230        preview = preview.block(self.border().as_block());
231        if self.config.wrap {
232            preview = preview.wrap(Wrap { trim: true }).scroll(self.scroll.into());
233        }
234        preview
235    }
236}