avis-imgv 0.1.0

Image viewer based on egui. Makes use of modern RAM amounts by loading images ahead of time for very fast responsiveness. Minimal UI with heavy use of shortcuts.
Documentation
use crate::image::Image;
use eframe::egui::{self, Response};
use eframe::epaint::vec2;
use std::path::PathBuf;
use std::thread::JoinHandle;

pub struct ThumbnailImage {
    pub path: PathBuf,
    pub name: String,
    should_unload: bool,
    image: Option<Image>,
    load_image_handle: Option<JoinHandle<Option<Image>>>,
    output_profile: String,
}

impl ThumbnailImage {
    pub fn from_paths(paths: &[PathBuf], output_profile: &String) -> Vec<Self> {
        paths
            .iter()
            .map(|p| Self {
                path: p.clone(),
                name: p
                    .file_name()
                    .unwrap_or_default()
                    .to_string_lossy()
                    .to_string(),
                image: None,
                load_image_handle: None,
                should_unload: false,
                output_profile: output_profile.to_owned(),
            })
            .collect()
    }

    pub fn ui(
        &mut self,
        ui: &mut egui::Ui,
        mut size: [f32; 2],
        margin_size: &f32,
    ) -> Option<Response> {
        let mut outer_margin_size = *margin_size;
        size[1] -= outer_margin_size;
        size[0] -= outer_margin_size;
        outer_margin_size /= 2.;

        self.finish_img_loading();

        let image = match &mut self.image {
            Some(image) => image,
            None => {
                Self::display_empty_image_frame(ui, size[1], outer_margin_size);
                return None;
            }
        };

        let texture = match image.get_texture(&self.name, ui) {
            Some(t) => t,
            None => {
                Self::display_empty_image_frame(ui, size[1], outer_margin_size);
                return None;
            }
        };

        let outer_margin = egui::style::Margin {
            left: outer_margin_size,
            right: outer_margin_size,
            top: outer_margin_size,
            bottom: outer_margin_size,
        };

        let mut margin = egui::style::Margin {
            left: 0.,
            right: 0.,
            top: 0.,
            bottom: 0.,
        };

        let prev = [size[0], size[1]];

        if texture.aspect_ratio() > 1. {
            size[1] /= texture.aspect_ratio();
            let half_free_y = (prev[1] - size[1]) / 2.;
            margin.top = half_free_y;
            margin.bottom = half_free_y;
        } else {
            size[0] *= texture.aspect_ratio();
            let half_free_x = (prev[0] - size[0]) / 2.;
            margin.right = half_free_x;
            margin.left = half_free_x;
        }

        let mut response: Option<Response> = None;
        egui::Frame::none()
            .inner_margin(margin)
            .outer_margin(outer_margin)
            .fill(egui::Color32::from_rgb(119, 119, 119))
            .show(ui, |ui| {
                let img_response = ui
                    .add(
                        egui::Image::new(texture)
                            .fit_to_exact_size(vec2(size[0], size[1]))
                            .sense(egui::Sense {
                                click: (true),
                                drag: (true),
                                focusable: (true),
                            }),
                    )
                    .on_hover_text_at_pointer(&self.name);

                response = Some(img_response)
            });

        response
    }

    pub fn display_empty_image_frame(ui: &mut egui::Ui, size: f32, outer_margin: f32) {
        let spinner_size = size / 3.;
        let inner_margin = (size - spinner_size) / 2.;

        egui::Frame::none()
            .inner_margin(egui::style::Margin {
                left: inner_margin,
                right: inner_margin,
                top: inner_margin,
                bottom: inner_margin,
            })
            .outer_margin(egui::style::Margin {
                left: outer_margin,
                right: outer_margin,
                top: outer_margin,
                bottom: outer_margin,
            })
            .fill(egui::Color32::from_rgb(119, 119, 119))
            .show(ui, |ui| ui.add(egui::Spinner::new().size(spinner_size)));
    }

    pub fn finish_img_loading(&mut self) {
        if self.load_image_handle.is_none() {
            return;
        };

        let lih = self.load_image_handle.take().unwrap();
        if lih.is_finished() {
            match lih.join() {
                Ok(image) => self.image = image,
                Err(_) => println!("Failure joining load image thread"),
            }
        } else {
            self.load_image_handle = Some(lih);
        }
    }

    pub fn load(&mut self, image_size: u32) -> bool {
        if self.load_image_handle.is_none() && self.image.is_none() {
            println!("Loading image -> {}", self.path.display());
            self.should_unload = false;
            self.load_image_handle = Some(Image::load(
                self.path.clone(),
                Some(image_size),
                self.output_profile.clone(),
            ));
            true
        } else {
            false
        }
    }

    ///If image is marked for unloading, unload it
    pub fn unload_delayed(&mut self) {
        if self.should_unload {
            match &mut self.load_image_handle {
                Some(ih) => {
                    if ih.is_finished() {
                        self.load_image_handle = None;
                        self.image = None;
                        self.should_unload = false;
                    }
                }
                None => {}
            }
        }
    }

    ///If image is currently loading marks it for unload
    ///If image is loaded, unloads it
    pub fn unload(&mut self, image_nr: usize) {
        if self.load_image_handle.is_some() {
            self.should_unload = true;
            println!(
                "Marking Image Handle for delayed unload {} - {}",
                image_nr, self.name
            );
        } else {
            self.image = None;
            self.load_image_handle = None;
        }
    }

    pub fn is_loading(&self) -> bool {
        match &self.load_image_handle {
            Some(lih) => !lih.is_finished(),
            None => false,
        }
    }
}