Search code examples
imagerusteguieframe

Dynamically load and display images from disk using eframe/egui in rust


beforehand as a heads up I want to say that I am relatively new to rust, and have very limited experience.

I am trying to implement my own image viewer using eframe/egui in rust. However I can't seem to get the loading and displaying of images using a file path to work.

I want the programm to work something like this:

  1. Select a directory

  2. Get every file from the directory

  3. Display the newest image

  4. Use buttons to click/loop through the images

Currently I am stuck at point 3 XD

fn main() -> Result<(), eframe::Error>{
    env_logger::init();
    let options = eframe::NativeOptions {
        viewport: egui::ViewportBuilder::default().with_inner_size([320.0, 240.0]),
        ..Default::default()
    };
    eframe::run_native(
        "Image Viewer",
        options,
        Box::new(
            |cc| {
               egui_extras::install_image_loaders(&cc.egui_ctx);
                Box::<ImageViewer>::default()
            }
        )
    )
}

#[derive(Default)]
struct ImageViewer {
    directory_path: Option<String>,
    images: Vec<String>,
    has_images: bool,
    current_image: i32
}

impl eframe::App for ImageViewer {
    fn update(&mut self, ctx: &Context, frame: &mut Frame) {
        egui::CentralPanel::default().show(ctx, |ui| {
            ui.vertical_centered_justified(|ui| {
                ui.horizontal_top(|ui| {
                    let path_label = ui.label("Path:");
                    let window_width = ui.available_width();
                    if let Some(directory_path) = &self.directory_path {
                        if ui.add_sized(
                            [window_width, 10.0],
                            egui::Button::new(directory_path)
                        )
                            .labelled_by(path_label.id)
                            .clicked() {
                            if let Some(path) = FileDialog::new().set_directory("/").pick_folder() {
                                self.directory_path = Some(path.display().to_string());
                                self.images = get_image_paths_from_directory(path);
                                self.has_images = self.images.len() > 0;
                                self.current_image = 0;
                            }
                        }
                    } else {
                        if ui.add_sized(
                            [window_width, 10.0],
                            egui::Button::new("No image directory selected")
                        )
                            .labelled_by(path_label.id)
                            .clicked() {
                            if let Some(path) = FileDialog::new().set_directory("/").pick_folder() {
                                self.directory_path = Some(path.display().to_string());
                                self.images = get_image_paths_from_directory(path);
                                self.has_images = self.images.len() > 0;
                                self.current_image = 0;
                            }
                        };
                    }
                });
                ui.vertical_centered_justified(|ui| {
                    if self.has_images {
                        // Load and display image
                    } else {
                        // Show placeholder
                    }
                })
            })
        });
    }
}

// Gets the file paths from a given PathBuf
fn get_image_paths_from_directory(path_buf: PathBuf) -> Vec<String> {
    path_buf
        .read_dir()
        .unwrap()
        .map(|entry| {
            let entry = entry.unwrap();
            let entry_path = entry.path();
            let file_name = entry_path.as_os_str();
            let file_name_as_str = file_name.to_str().unwrap();
            let file_name_as_string = String::from(file_name_as_str);

            file_name_as_string
        })
        .collect::<Vec<String>>()
}

I know that you can implement images like this

ui.add(egui::Image::new(egui::include_image!(image_path)));

However you have to declare the image_path before running the programm and I can't seem to find a way to change the path mid execution (or if that is even possible). Also setting the varibable beforehand would not serve the purpose of selecting a directory and browsing its images.

I also tried some things converting the image into bytes and tied loading it that way, without positive results however. There is just the missing know how in rust I think.

Thanks for the help in advance!


Solution

  • Just pass an URI file:///path/to/your/file.png to your Image::new call, it takes anything that implements Into<ImageSource> and there is an implementation of From<String> for ImageSource that treats the String as an URI:

    let file_path = "/path/to/your/file.png";
    let image = Image::new(format!("file://{file_path}"));
    

    The examples on Ui::image do cover that case and should be preferable manual add(Image):

    ui.image("file://assets/ferris.png");