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 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 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 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 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 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 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 self.sizing.scroll_delta +=
478 ctx.input(|i| i.pointer.delta()) * ctx.pixels_per_point();
479 }
480 } else {
481 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}