avis_imgv/
image_view.rs

1use eframe::egui::Sense;
2use eframe::{egui, epaint::Vec2};
3use std::path::{Path, PathBuf};
4
5use crate::gallery_image::{GalleryImageFrame, GalleryImageSizing};
6use crate::{
7    callback::Callback,
8    config::ImageViewConfig,
9    gallery_image::GalleryImage,
10    user_action::{self, show_context_menu},
11    utils,
12};
13
14pub const PERCENTAGES: &[f32] = &[200., 100., 75., 50., 25.];
15
16pub struct ImageView {
17    imgs: Vec<GalleryImage>,
18    pub selected_img_index: usize,
19    preload_active: bool,
20    frame: GalleryImageFrame,
21    sizing: GalleryImageSizing,
22    config: ImageViewConfig,
23    jump_to: String,
24    output_profile: String,
25    callback: Option<Callback>,
26}
27
28impl ImageView {
29    pub fn new(
30        image_paths: &[PathBuf],
31        selected_image_path: &Option<PathBuf>,
32        config: ImageViewConfig,
33        output_profile: &String,
34    ) -> ImageView {
35        let mut sg = ImageView {
36            imgs: vec![],
37            selected_img_index: 0,
38            preload_active: true,
39            frame: GalleryImageFrame {
40                enabled: false,
41                size_r: config.frame_size_relative_to_image,
42            },
43            sizing: GalleryImageSizing {
44                zoom_factor: 1.0,
45                scroll_delta: Vec2::new(0., 0.),
46                should_maximize: false,
47                has_maximized: false,
48            },
49            jump_to: String::new(),
50            output_profile: output_profile.to_owned(),
51            callback: None,
52            config,
53        };
54
55        sg.set_images(image_paths, selected_image_path);
56
57        sg
58    }
59
60    pub fn set_images(&mut self, image_paths: &[PathBuf], selected_image_path: &Option<PathBuf>) {
61        let imgs = GalleryImage::from_paths(image_paths, &self.output_profile);
62
63        self.imgs = imgs;
64        self.selected_img_index = match selected_image_path {
65            Some(path) => self.imgs.iter().position(|x| &x.path == path).unwrap_or(0),
66            None => 0,
67        };
68        self.preload_active =
69            Self::is_valid_for_preload(self.config.nr_loaded_images, self.imgs.len());
70
71        println!(
72            "Starting gallery with {} images on image {}",
73            self.imgs.len(),
74            self.selected_img_index + 1
75        );
76
77        self.load();
78    }
79
80    pub fn load(&mut self) {
81        if self.imgs.is_empty() {
82            return;
83        }
84
85        if !self.preload_active {
86            for i in 0..self.imgs.len() {
87                self.imgs[i].load();
88            }
89
90            return;
91        }
92
93        //Not many entries in this vec so it's not worth to use a hasmap
94        let mut indexes_to_load: Vec<usize> = vec![self.selected_img_index];
95
96        for i in 1..self.config.nr_loaded_images + 1 {
97            indexes_to_load.push(get_vec_index_subtracted_by(
98                self.imgs.len(),
99                self.selected_img_index,
100                i,
101            ));
102            indexes_to_load.push(get_vec_index_sum_by(
103                self.imgs.len(),
104                self.selected_img_index,
105                i,
106            ));
107        }
108
109        for (i, img) in &mut self.imgs.iter_mut().enumerate() {
110            if indexes_to_load.contains(&i) {
111                img.load();
112            } else {
113                img.unload();
114            }
115        }
116    }
117
118    pub fn select_by_name(&mut self, img_name: String) {
119        self.selected_img_index = self
120            .imgs
121            .iter()
122            .position(|x| x.name == img_name)
123            .unwrap_or(0);
124
125        self.load();
126    }
127
128    pub fn next_image(&mut self) {
129        if self.imgs.is_empty() {
130            return;
131        }
132
133        if self.config.should_wait && self.active_img_is_loading() {
134            return;
135        }
136
137        if self.preload_active {
138            let index_to_clear = get_vec_index_subtracted_by(
139                self.imgs.len(),
140                self.selected_img_index,
141                self.config.nr_loaded_images,
142            );
143
144            let index_to_preload = get_vec_index_sum_by(
145                self.imgs.len(),
146                self.selected_img_index,
147                self.config.nr_loaded_images,
148            );
149
150            self.imgs[index_to_clear].unload();
151            self.imgs[index_to_preload].load();
152        }
153
154        if self.selected_img_index == self.imgs.len() - 1 {
155            self.selected_img_index = 0;
156        } else {
157            self.selected_img_index += 1;
158        }
159
160        self.sizing.has_maximized = false;
161    }
162
163    pub fn previous_image(&mut self) {
164        if self.imgs.is_empty() {
165            return;
166        }
167
168        if self.preload_active {
169            let index_to_clear = get_vec_index_sum_by(
170                self.imgs.len(),
171                self.selected_img_index,
172                self.config.nr_loaded_images,
173            );
174
175            let index_to_preload = get_vec_index_subtracted_by(
176                self.imgs.len(),
177                self.selected_img_index,
178                self.config.nr_loaded_images,
179            );
180
181            self.imgs[index_to_clear].unload();
182            self.imgs[index_to_preload].load();
183        }
184
185        if self.selected_img_index == 0 {
186            self.selected_img_index = self.imgs.len() - 1;
187        } else {
188            self.selected_img_index -= 1;
189        }
190
191        self.sizing.has_maximized = false;
192    }
193
194    pub fn toggle_frame(&mut self) {
195        self.frame.enabled = !self.frame.enabled;
196    }
197
198    pub fn reset_zoom(&mut self) {
199        self.sizing.zoom_factor = 1.0;
200    }
201
202    pub fn double_zoom(&mut self) {
203        if self.sizing.zoom_factor <= 7.0 {
204            //make limit a config
205            self.sizing.zoom_factor *= 2.0;
206        } else {
207            self.sizing.zoom_factor = 1.0;
208        }
209    }
210
211    pub fn multiply_zoom(&mut self, zoom_delta: f32) {
212        if zoom_delta != 1.0 {
213            self.sizing.zoom_factor *= zoom_delta;
214        }
215    }
216
217    ///Sets zoom factor based on percentage and opened image size
218    pub fn set_zoom_factor_from_percentage(&mut self, percentage: &f32) {
219        let img = match self.get_active_img() {
220            Some(img) => img,
221            None => return,
222        };
223
224        let original_size = match img.image_size() {
225            Some(org_size) => org_size,
226            None => return,
227        };
228
229        self.sizing.zoom_factor = ((original_size[0] * percentage / 100.)
230            * self.sizing.zoom_factor)
231            / (img.prev_target_size[0] * self.sizing.zoom_factor);
232    }
233
234    pub fn fit_vertical(&mut self) {
235        let img = match self.get_active_img() {
236            Some(img) => img,
237            None => return,
238        };
239
240        self.sizing.zoom_factor = img.prev_available_size.y / img.prev_target_size.y;
241    }
242
243    pub fn fit_horizontal(&mut self) {
244        let img = match self.get_active_img() {
245            Some(img) => img,
246            None => return,
247        };
248
249        self.sizing.zoom_factor = img.prev_available_size.x / img.prev_target_size.x;
250    }
251
252    pub fn fit_maximize(&mut self) {
253        let img = match self.get_active_img() {
254            Some(img) => img,
255            None => return,
256        };
257
258        if img.prev_available_size.x / img.prev_available_size.y
259            > img.prev_target_size.x / img.prev_target_size.y
260        {
261            self.sizing.zoom_factor = img.prev_available_size.y / img.prev_target_size.y;
262        } else {
263            self.sizing.zoom_factor = img.prev_available_size.x / img.prev_target_size.x;
264        }
265    }
266
267    pub fn latch_fit_maximize(&mut self) {
268        self.sizing.should_maximize = !self.sizing.should_maximize;
269    }
270
271    pub fn get_active_img_nr(&mut self) -> usize {
272        self.selected_img_index + 1
273    }
274
275    pub fn get_active_img_mut(&mut self) -> Option<&mut GalleryImage> {
276        if !self.imgs.is_empty() {
277            return Some(&mut self.imgs[self.selected_img_index]);
278        }
279
280        None
281    }
282
283    pub fn get_active_img(&self) -> Option<&GalleryImage> {
284        if !self.imgs.is_empty() {
285            return Some(&self.imgs[self.selected_img_index]);
286        }
287
288        None
289    }
290
291    pub fn get_active_img_name(&mut self) -> String {
292        let format = self.config.name_format.clone();
293        match self.get_active_img_mut() {
294            Some(img) => img.get_display_name(format),
295            None => "".to_string(),
296        }
297    }
298
299    pub fn get_active_img_path(&self) -> Option<PathBuf> {
300        self.get_active_img().map(|img| img.path.clone())
301    }
302
303    pub fn active_img_is_loading(&self) -> bool {
304        match self.get_active_img() {
305            Some(img) => img.is_loading(),
306            None => false,
307        }
308    }
309
310    pub fn jump_to_image(&mut self) {
311        self.selected_img_index = match self.jump_to.parse::<usize>() {
312            Ok(i) => {
313                if i > self.imgs.len() || i < 1 {
314                    self.selected_img_index
315                } else {
316                    i - 1
317                }
318            }
319            Err(_) => self.selected_img_index,
320        };
321
322        self.load();
323        self.jump_to.clear();
324    }
325
326    pub fn reload_at(&mut self, path: &Path) {
327        if let Some(index) = self.imgs.iter().position(|x| x.path == path) {
328            let img = &mut self.imgs[index];
329            img.unload();
330            img.load();
331        }
332    }
333
334    ///Pops image from the collection
335    pub fn pop(&mut self, path: &Path) {
336        if let Some(pos) = self.imgs.iter().position(|x| x.path == path) {
337            self.imgs.remove(pos);
338            self.preload_active =
339                Self::is_valid_for_preload(self.config.nr_loaded_images, self.imgs.len());
340
341            //Last image of the collection, we want to load backwards
342            if self.selected_img_index == self.imgs.len() {
343                self.selected_img_index = self.imgs.len() - 1;
344            }
345
346            self.load();
347        }
348    }
349
350    pub fn is_valid_for_preload(preload_nr: usize, image_count: usize) -> bool {
351        preload_nr * 2 <= image_count
352    }
353
354    pub fn ui(&mut self, ctx: &egui::Context, flattened: bool, watcher_enabled: bool) {
355        self.handle_input(ctx);
356
357        egui::TopBottomPanel::bottom("gallery_bottom")
358            .show_separator_line(false)
359            .show(ctx, |ui| {
360                ui.horizontal_centered(|ui| {
361                    let response = ui.add_sized(
362                        Vec2::new(65., ui.available_height()),
363                        egui::TextEdit::singleline(&mut self.jump_to),
364                    );
365
366                    if response.lost_focus()
367                        && response.ctx.input(|i| i.key_pressed(egui::Key::Enter))
368                    {
369                        self.jump_to_image();
370                    }
371
372                    ui.add_sized(
373                        Vec2::new(35., ui.available_height()),
374                        egui::Label::new(format!(
375                            "{}/{}",
376                            self.get_active_img_nr(),
377                            self.imgs.len()
378                        )),
379                    );
380
381                    if flattened {
382                        ui.label("Flattened");
383                    }
384
385                    if watcher_enabled {
386                        ui.label("Watching");
387                    }
388
389                    if self.sizing.should_maximize {
390                        ui.label("Maximizing");
391                    }
392
393                    let mut label = egui::Label::new(self.get_active_img_name());
394                    label = label.truncate();
395                    ui.add_sized(
396                        Vec2::new(ui.available_width() - 245., ui.available_height()),
397                        label,
398                    );
399
400                    ui.with_layout(
401                        egui::Layout::right_to_left(eframe::emath::Align::Max),
402                        |ui| {
403                            ui.add_sized(
404                                Vec2::new(200., ui.available_height()),
405                                egui::Slider::new(&mut self.sizing.zoom_factor, 0.5..=10.0)
406                                    .text("🔎"),
407                            );
408
409                            if let Some(img) = self.get_active_img() {
410                                let resp = ui.add_sized(
411                                    Vec2::new(45., ui.available_height()),
412                                    egui::Label::new(format!("{:.1}%", img.prev_percentage_zoom))
413                                        .sense(Sense::click()),
414                                );
415
416                                resp.context_menu(|ui| {
417                                    if ui.button("Fit to screen").clicked() {
418                                        self.reset_zoom();
419                                        ui.close_menu();
420                                    }
421
422                                    if ui.button("Fit horizontal").clicked() {
423                                        self.fit_horizontal();
424                                        ui.close_menu();
425                                    }
426
427                                    if ui.button("Fit vertical").clicked() {
428                                        self.fit_vertical();
429                                        ui.close_menu();
430                                    }
431
432                                    ui.separator();
433
434                                    for percentage in PERCENTAGES {
435                                        if ui.button(format!("{percentage:.0}%")).clicked() {
436                                            self.set_zoom_factor_from_percentage(percentage);
437                                            ui.close_menu();
438                                        }
439                                    }
440                                });
441                            }
442                        },
443                    )
444                });
445            });
446
447        let image_pannel_resp = egui::CentralPanel::default()
448            .frame(egui::Frame::NONE.fill(egui::Color32::from_rgb(119, 119, 119)))
449            .show(ctx, |ui| {
450                ui.centered_and_justified(|ui| {
451                    if !self.imgs.is_empty() {
452                        let img: &mut GalleryImage = &mut self.imgs[self.selected_img_index];
453                        img.ui(ui, &self.frame, &mut self.sizing);
454                    }
455                });
456            })
457            .response;
458
459        //unfortunately we'll always be one frame behind
460        //when advancing with the scroll wheel
461        if image_pannel_resp.contains_pointer() {
462            if self.config.scroll_navigation {
463                if ctx.input(|i| i.raw_scroll_delta.y) > 0.0 && ctx.input(|i| i.zoom_delta()) == 1.0
464                {
465                    self.next_image();
466                }
467
468                if ctx.input(|i| i.raw_scroll_delta.y) < 0.0 && ctx.input(|i| i.zoom_delta()) == 1.0
469                {
470                    self.previous_image();
471                }
472            }
473
474            self.sizing.scroll_delta = ctx.input(|i| i.smooth_scroll_delta);
475            if ctx.input(|i| i.pointer.is_decidedly_dragging()) {
476                //drag
477                self.sizing.scroll_delta +=
478                    ctx.input(|i| i.pointer.delta()) * ctx.pixels_per_point();
479            }
480        } else {
481            //lest we lose hover in the frame that there's a scroll
482            //delta and we get infinite zoom
483            self.sizing.scroll_delta.x = 0.;
484            self.sizing.scroll_delta.y = 0.;
485        }
486
487        if let Some(path) = self.get_active_img_path() {
488            let callback = show_context_menu(&self.config.context_menu, image_pannel_resp, &path);
489
490            if let Some(callback) = callback {
491                self.callback = Some(Callback::from_callback(callback, Some(path)));
492            }
493        }
494    }
495
496    pub fn handle_input(&mut self, ctx: &egui::Context) {
497        if utils::are_inputs_muted(ctx) {
498            return;
499        }
500
501        if ctx.input_mut(|i| i.consume_shortcut(&self.config.sc_fit.kbd_shortcut)) {
502            self.reset_zoom();
503        }
504        if ctx.input_mut(|i| i.consume_shortcut(&self.config.sc_frame.kbd_shortcut)) {
505            self.toggle_frame();
506        }
507        if ctx.input_mut(|i| i.consume_shortcut(&self.config.sc_zoom.kbd_shortcut)) {
508            self.double_zoom();
509        }
510        if ctx.input_mut(|i| i.consume_shortcut(&self.config.sc_next.kbd_shortcut)) {
511            self.next_image();
512        }
513        if ctx.input_mut(|i| i.consume_shortcut(&self.config.sc_prev.kbd_shortcut)) {
514            self.previous_image();
515        }
516        if ctx.input_mut(|i| i.consume_shortcut(&self.config.sc_one_to_one.kbd_shortcut)) {
517            self.set_zoom_factor_from_percentage(&100.);
518        }
519        if ctx.input_mut(|i| i.consume_shortcut(&self.config.sc_fit_horizontal.kbd_shortcut)) {
520            self.fit_horizontal();
521        }
522        if ctx.input_mut(|i| i.consume_shortcut(&self.config.sc_fit_vertical.kbd_shortcut)) {
523            self.fit_vertical();
524        }
525        if ctx.input_mut(|i| i.consume_shortcut(&self.config.sc_fit_maximize.kbd_shortcut)) {
526            self.fit_maximize();
527        }
528        if ctx.input_mut(|i| i.consume_shortcut(&self.config.sc_latch_fit_maximize.kbd_shortcut)) {
529            self.latch_fit_maximize();
530        }
531
532        self.multiply_zoom(ctx.input(|i| i.zoom_delta()));
533
534        for action in &self.config.user_actions {
535            if !ctx.input_mut(|i| i.consume_shortcut(&action.shortcut.kbd_shortcut)) {
536                continue;
537            }
538
539            if let Some(path) = self.get_active_img_path() {
540                if user_action::execute(&action.exec, &path) {
541                    if let Some(callback) = action.callback.to_owned() {
542                        self.callback =
543                            Some(Callback::from_callback(callback, Some(path.to_owned())));
544                    }
545                }
546            } else {
547                println!("Unable to get active image path for user action");
548            }
549        }
550    }
551
552    pub fn take_callback(&mut self) -> Option<Callback> {
553        self.callback.take()
554    }
555}
556
557fn get_vec_index_subtracted_by(vec_len: usize, current_index: usize, to_subtract: usize) -> usize {
558    if current_index < to_subtract {
559        vec_len - (to_subtract - current_index)
560    } else {
561        current_index - to_subtract
562    }
563}
564
565fn get_vec_index_sum_by(vec_len: usize, current_index: usize, to_sum: usize) -> usize {
566    let mut idx = current_index + to_sum;
567    if idx >= vec_len {
568        idx = to_sum - (vec_len - current_index);
569    }
570
571    idx
572}