avis_imgv/
grid_view.rs

1use crate::{
2    callback::Callback, config::GridViewConfig, thumbnail_image::ThumbnailImage,
3    user_action::show_context_menu, utils,
4};
5use eframe::{
6    egui::{self, Ui},
7    epaint::Vec2,
8};
9use std::path::{Path, PathBuf};
10
11pub struct GridView {
12    imgs: Vec<ThumbnailImage>,
13    config: GridViewConfig,
14    output_profile: String,
15    selected_image_name: Option<String>,
16    prev_img_size: f32,
17    prev_scroll_offset: f32,
18    total_rows: usize,
19    images_per_row: usize,
20    prev_images_per_row: usize,
21    prev_row_range_start: usize,
22    reset_scroll: bool,
23    callback: Option<Callback>,
24}
25
26impl GridView {
27    pub fn new(
28        image_paths: &[PathBuf],
29        config: GridViewConfig,
30        output_profile: &String,
31    ) -> GridView {
32        let imgs = ThumbnailImage::from_paths(image_paths, output_profile);
33        let mut mg = GridView {
34            total_rows: 0,
35            imgs,
36            selected_image_name: None,
37            images_per_row: config.images_per_row,
38            prev_images_per_row: config.images_per_row,
39            config,
40            prev_img_size: 0.,
41            prev_scroll_offset: 0.,
42            prev_row_range_start: 0,
43            output_profile: output_profile.to_owned(),
44            reset_scroll: false,
45            callback: None,
46        };
47
48        mg.set_total_rows();
49
50        mg
51    }
52
53    pub fn set_images(&mut self, img_paths: &[PathBuf]) {
54        self.imgs = ThumbnailImage::from_paths(img_paths, &self.output_profile);
55        self.reset_scroll = true;
56        self.set_total_rows();
57    }
58
59    pub fn ui(&mut self, ctx: &egui::Context, jump_to_index: &mut Option<usize>) {
60        self.handle_input(ctx);
61
62        egui::CentralPanel::default().show(ctx, |ui| {
63            ui.spacing_mut().item_spacing = Vec2::new(0., 0.);
64            ui.set_min_width(ui.available_width());
65
66            let mut loading_imgs = self.imgs.iter().filter(|i| i.is_loading()).count();
67            let mut img_size = ui.available_width() / self.images_per_row as f32;
68            let prev_img_size = img_size;
69
70            if img_size % 6. != 0. {
71                img_size -= img_size % 6.; // Truncate to nearest multiple of 6
72            }
73
74            let remainder = (prev_img_size - img_size) * self.images_per_row as f32;
75
76            let mut scroll_area = egui::ScrollArea::vertical().drag_to_scroll(true);
77
78            //Since image size changes when we resize the window, we need to compensate the scroll
79            //offset as show_rows assumes fixed widget sizes
80            if img_size != self.prev_img_size {
81                scroll_area = scroll_area.scroll_offset(Vec2 {
82                    x: 0.,
83                    y: img_size * self.prev_scroll_offset / self.prev_img_size,
84                });
85            }
86
87            if self.images_per_row != self.prev_images_per_row {
88                let target_row =
89                    (self.prev_row_range_start * self.prev_images_per_row) / self.images_per_row;
90
91                scroll_area = scroll_area.scroll_offset(Vec2 {
92                    x: 0.,
93                    y: img_size * target_row as f32,
94                });
95            }
96
97            if let Some(mut i) = jump_to_index.take() {
98                //Get start of the row index so it's easier to calculate the offset
99                i = i - (i % self.images_per_row);
100                let scroll_offset = ((i as f32) / self.images_per_row as f32) * img_size;
101                scroll_area = scroll_area.scroll_offset(Vec2 {
102                    x: 0.,
103                    y: scroll_offset,
104                })
105            };
106
107            if self.reset_scroll {
108                scroll_area = scroll_area.scroll_offset(Vec2 { x: 0., y: 0. });
109                self.reset_scroll = false;
110            }
111
112            let scroll_area_response =
113                scroll_area.show_rows(ui, img_size, self.total_rows, |ui, row_range| {
114                    ui.spacing_mut().item_spacing = Vec2::new(0., 0.);
115
116                    let preload_from = row_range.start.saturating_sub(self.config.preloaded_rows);
117
118                    let preload_to = if row_range.end + self.config.preloaded_rows > self.total_rows
119                    {
120                        self.total_rows
121                    } else {
122                        row_range.end + self.config.preloaded_rows
123                    };
124
125                    //first we go over the visible ones
126                    for r in row_range.start..row_range.end {
127                        for i in r * self.images_per_row..(r + 1) * self.images_per_row {
128                            self.load_unload_image(
129                                i,
130                                row_range.start,
131                                row_range.end,
132                                &mut loading_imgs,
133                                img_size,
134                            );
135                        }
136                    }
137
138                    //then in the down direction as the user is most likely to scroll down
139                    for r in row_range.end..self.total_rows {
140                        for i in r * self.images_per_row..(r + 1) * self.images_per_row {
141                            self.load_unload_image(
142                                i,
143                                preload_from,
144                                preload_to,
145                                &mut loading_imgs,
146                                img_size,
147                            );
148                        }
149                    }
150
151                    //then up
152                    for r in 0..row_range.start {
153                        for i in r * self.images_per_row..(r + 1) * self.images_per_row {
154                            self.load_unload_image(
155                                i,
156                                preload_from,
157                                preload_to,
158                                &mut loading_imgs,
159                                img_size,
160                            );
161                        }
162                    }
163
164                    for r in row_range.clone() {
165                        ui.horizontal(|ui| {
166                            ui.spacing_mut().item_spacing = Vec2::new(0., 0.);
167                            ui.add_space(remainder / 2.0);
168
169                            for j in r * self.images_per_row..(r + 1) * self.images_per_row {
170                                if let Some(img) = &mut self.imgs.get_mut(j) {
171                                    Self::show_image(
172                                        img,
173                                        ui,
174                                        ctx,
175                                        img_size,
176                                        &mut self.selected_image_name,
177                                        &self.config,
178                                        &mut self.callback,
179                                    );
180                                }
181                            }
182                        });
183                    }
184
185                    if !utils::are_inputs_muted(ctx)
186                        && ui.input_mut(|i| i.consume_shortcut(&self.config.sc_scroll.kbd_shortcut))
187                    {
188                        ui.scroll_with_delta(Vec2::new(0., -(img_size * 0.5)));
189                    }
190
191                    self.prev_row_range_start = row_range.start;
192                });
193
194            self.prev_scroll_offset = scroll_area_response.state.offset.y;
195            self.prev_img_size = img_size;
196            self.prev_images_per_row = self.images_per_row;
197        });
198    }
199
200    fn load_unload_image(
201        &mut self,
202        i: usize,
203        preload_from: usize,
204        preload_to: usize,
205        loading_imgs: &mut usize,
206        image_size: f32,
207    ) {
208        let img = &mut match self.imgs.get_mut(i) {
209            Some(img) => img,
210            None => return,
211        };
212
213        if i >= preload_from * self.images_per_row && i <= preload_to * self.images_per_row {
214            if loading_imgs != &self.config.simultaneous_load {
215                //Double the square size so we have a little downscale going on
216                //Looks better than without and won't impact speed much. Possibly add as a config
217                if img.load((image_size * 2.) as u32) {
218                    *loading_imgs += 1;
219                }
220            }
221        } else {
222            img.unload_delayed();
223            img.unload(i);
224        }
225    }
226
227    fn show_image(
228        image: &mut ThumbnailImage,
229        ui: &mut Ui,
230        ctx: &egui::Context,
231        max_size: f32,
232        select_image_name: &mut Option<String>,
233        config: &GridViewConfig,
234        callback: &mut Option<Callback>,
235    ) {
236        if let Some(resp) = image.ui(ui, [max_size, max_size]) {
237            if resp.clicked() {
238                *select_image_name = Some(image.name.clone());
239            }
240            if resp.hovered() {
241                ctx.set_cursor_icon(egui::CursorIcon::PointingHand);
242            }
243
244            let return_callback = show_context_menu(&config.context_menu, resp, &image.path);
245
246            if let Some(return_callback) = return_callback {
247                *callback = Some(Callback::from_callback(
248                    return_callback,
249                    Some(image.path.clone()),
250                ));
251
252                println!("{callback:?}");
253            }
254        }
255    }
256
257    pub fn handle_input(&mut self, ctx: &egui::Context) {
258        if utils::are_inputs_muted(ctx) {
259            return;
260        }
261
262        if (ctx.input_mut(|i| i.consume_shortcut(&self.config.sc_more_per_row.kbd_shortcut))
263            || (ctx.input(|i| i.raw_scroll_delta.y) < 0. && ctx.input(|i| i.zoom_delta() != 1.)))
264            && self.images_per_row <= 15
265        {
266            self.images_per_row += 1;
267            self.set_total_rows();
268        }
269
270        if (ctx.input_mut(|i| i.consume_shortcut(&self.config.sc_less_per_row.kbd_shortcut))
271            || (ctx.input(|i| i.raw_scroll_delta.y) > 0. && ctx.input(|i| i.zoom_delta() != 1.)))
272            && self.images_per_row != 1
273        {
274            self.images_per_row -= 1;
275            self.set_total_rows();
276        }
277    }
278
279    pub fn selected_image_name(&mut self) -> Option<String> {
280        //We want it to be consumed
281        self.selected_image_name.take()
282    }
283
284    pub fn set_total_rows(&mut self) {
285        //div_ceil will be available in the next release. Avoids conversions..
286        self.total_rows = (self.imgs.len() as f32 / self.images_per_row as f32).ceil() as usize
287    }
288
289    pub fn pop(&mut self, path: &Path) {
290        if let Some(pos) = self.imgs.iter().position(|x| x.path == path) {
291            self.imgs.remove(pos);
292            self.set_total_rows();
293        }
294    }
295
296    pub fn take_callback(&mut self) -> Option<Callback> {
297        self.callback.take()
298    }
299
300    pub fn reload_at(&mut self, path: &Path) {
301        if let Some(pos) = self.imgs.iter().position(|x| x.path == path) {
302            if let Some(img) = self.imgs.get_mut(pos) {
303                img.unload_delayed();
304                img.unload(pos);
305            }
306        }
307    }
308}