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 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 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 pub fn setting(&self) -> Option<&PreviewSetting> {
116 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 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 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 let mut lines_above =
269 self.config
270 .scroll
271 .percentage
272 .complement()
273 .compute_clamped(self.area.height, 0, 0);
274
275 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 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}