tarsier 1.4.1

A simple image editor
Documentation
//! Top panel
use bladvak::eframe::egui::{
    self, Color32, ColorImage, Image, ImageSource, TextFormat, include_image, text::LayoutJob,
};
use bladvak::errors::ErrorManager;
use image::ImageFormat;
use std::path::Path;
use std::{io::Cursor, path::PathBuf};

use crate::{TarsierApp, side_panel::EditMode};

impl TarsierApp {
    /// Reset icon
    const RESET_ICON: ImageSource<'_> = include_image!("../assets/icon_x-circle.png");
    /// Save state icon
    const SAVE_STATE_ICON: ImageSource<'_> = include_image!("../assets/icon_check.png");
    /// Rotate icon
    const ROTATE_CCW_ICON: ImageSource<'_> = include_image!("../assets/icon_rotate_ccw.png");
    /// Rotate icon inverse
    const ROTATE_CW_ICON: ImageSource<'_> = include_image!("../assets/icon_rotate_cw.png");
    /// Flip horizontal icon
    const FLIP_H_ICON: ImageSource<'_> = include_image!("../assets/icon_flip_h.png");
    /// Flip vertical icon
    const FLIP_V_ICON: ImageSource<'_> = include_image!("../assets/icon_flip_v.png");

    /// Show the file menu
    pub(crate) fn app_menu_file(&mut self, ui: &mut egui::Ui, error_manager: &mut ErrorManager) {
        if ui.button("New").clicked() {
            self.new_image.is_open = true;
            ui.close();
        }
        self.menu_clipboard(ui, error_manager);
        ui.menu_button("Save", |ui| {
            if ui.button("PNG").clicked() {
                ui.close();
                self.save_image_with_format(
                    ImageFormat::Png.extensions_str()[0],
                    ImageFormat::Png,
                    error_manager,
                );
            }
            if ui.button("JPEG").clicked() {
                ui.close();
                self.save_image_with_format(
                    ImageFormat::Jpeg.extensions_str()[0],
                    ImageFormat::Jpeg,
                    error_manager,
                );
            }
            if ui.button("BMP").clicked() {
                ui.close();
                self.save_image_with_format(
                    ImageFormat::Bmp.extensions_str()[0],
                    ImageFormat::Bmp,
                    error_manager,
                );
            }
            if ui.button("GIF").clicked() {
                ui.close();
                self.save_image_with_format(
                    ImageFormat::Gif.extensions_str()[0],
                    ImageFormat::Gif,
                    error_manager,
                );
            }
        });
    }

    /// Show the clipboard menu
    pub fn menu_clipboard(&mut self, ui: &mut egui::Ui, error_manager: &mut ErrorManager) {
        ui.menu_button("Clipboard", |ui| {
            if ui.button("Copy").clicked()
                && let Err(e) = bladvak::utils::set_image_in_clipboard(
                    ui.ctx(),
                    self.img.width() as usize,
                    self.img.height() as usize,
                    self.img.to_rgba8().as_flat_samples().as_slice(),
                )
            {
                error_manager.add_error(e);
            }

            #[cfg(not(target_arch = "wasm32"))]
            if ui.button("Paste").clicked() {
                match bladvak::utils::get_image_from_clipboard() {
                    Ok((width, height, rgba_data)) => {
                        #[allow(clippy::cast_possible_truncation)]
                        if let Some(buffer) = image::ImageBuffer::from_raw(
                            width as u32,
                            height as u32,
                            rgba_data.clone(),
                        ) {
                            let img = image::DynamicImage::ImageRgba8(buffer);
                            self.update_image(img);
                        } else {
                            error_manager
                                .add_error("Invalid image data from clipboard".to_string());
                        }
                    }
                    Err(e) => {
                        error_manager.add_error(e);
                    }
                }
            }
        });
    }

    /// Save the current image with the specified format
    pub(crate) fn save_image_with_format(
        &mut self,
        extension: &str,
        image_format: ImageFormat,
        error_manager: &mut ErrorManager,
    ) {
        let current_save_path = self
            .save_path
            .clone()
            .unwrap_or(PathBuf::from(format!("image.{extension}")));
        let save_path = bladvak::utils::get_save_path(Some(&current_save_path));
        match save_path {
            Ok(save_p) => {
                self.save_path.clone_from(&save_p);
                if let Some(path_to_save) = save_p
                    && let Err(err) = self.save_image(image_format, &path_to_save)
                {
                    error_manager.add_error(err);
                }
            }
            Err(e) => {
                error_manager.add_error(e);
            }
        }
    }

    /// Top panel operations
    fn top_panel_operations(
        &mut self,
        ui: &mut egui::Ui,
        error_manager: &mut ErrorManager,
        is_dark_theme: bool,
    ) {
        let ico_image = Image::new(Self::RESET_ICON);
        if ui
            .add(egui::Button::image(if is_dark_theme {
                ico_image
            } else {
                ico_image.tint(Color32::BLACK)
            }))
            .on_hover_text("Reset the image to the saved state")
            .clicked()
        {
            let new_img = self.saved_img.clone();
            self.update_image(new_img);
        }
        let ico_image = Image::new(Self::SAVE_STATE_ICON);
        if ui
            .add(egui::Button::image(if is_dark_theme {
                ico_image
            } else {
                ico_image.tint(Color32::BLACK)
            }))
            .on_hover_text("Save the current image state")
            .clicked()
        {
            self.saved_img = self.img.clone();
        }
        ui.separator();

        let ico_image = Image::new(Self::ROTATE_CW_ICON);
        if ui
            .add(egui::Button::image(if is_dark_theme {
                ico_image
            } else {
                ico_image.tint(Color32::BLACK)
            }))
            .on_hover_text("Rotate 90 degrees clockwise")
            .clicked()
        {
            self.apply_op(image::DynamicImage::rotate90, error_manager);
        }

        let ico_image = Image::new(Self::ROTATE_CCW_ICON);
        if ui
            .add(egui::Button::image(if is_dark_theme {
                ico_image
            } else {
                ico_image.tint(Color32::BLACK)
            }))
            .on_hover_text("Rotate 90 degrees counter-clockwise")
            .clicked()
        {
            self.apply_op(image::DynamicImage::rotate270, error_manager);
        }

        let ico_image = Image::new(Self::FLIP_H_ICON);
        if ui
            .add(egui::Button::image(if is_dark_theme {
                ico_image
            } else {
                ico_image.tint(Color32::BLACK)
            }))
            .on_hover_text("Flip horizontally")
            .clicked()
        {
            self.apply_op(image::DynamicImage::fliph, error_manager);
        }
        let ico_image = Image::new(Self::FLIP_V_ICON);
        if ui
            .add(egui::Button::image(if is_dark_theme {
                ico_image
            } else {
                ico_image.tint(Color32::BLACK)
            }))
            .on_hover_text("Flip vertically")
            .clicked()
        {
            self.apply_op(image::DynamicImage::flipv, error_manager);
        }
    }

    /// Show the top panel
    pub(crate) fn app_top_panel(&mut self, ui: &mut egui::Ui, error_manager: &mut ErrorManager) {
        let is_dark_theme = ui.ctx().global_style().visuals.dark_mode;
        ui.separator();
        self.top_panel_operations(ui, error_manager, is_dark_theme);
        ui.separator();
        let (default_color, background_color) = if ui.visuals().dark_mode {
            (Color32::LIGHT_GRAY, Color32::DARK_BLUE)
        } else {
            (Color32::DARK_GRAY, Color32::LIGHT_BLUE)
        };
        let mut job = LayoutJob::default();
        job.append(
            "Cursor ",
            0.0,
            TextFormat {
                color: default_color,
                ..Default::default()
            },
        );
        job.append(
            &format!("{}", self.image_operations.mode),
            0.0,
            TextFormat {
                color: default_color,
                background: background_color,
                ..Default::default()
            },
        );
        ui.menu_button(job, |ui| {
            let previous_state = self.image_operations.mode;
            ui.selectable_value(
                &mut self.image_operations.mode,
                EditMode::Selection,
                "Selection",
            );
            ui.selectable_value(
                &mut self.image_operations.mode,
                EditMode::Drawing,
                "Drawing",
            );
            if self.image_operations.mode != previous_state {
                ui.close();
                if self.image_operations.mode == EditMode::Drawing {
                    self.cursor_info.selection = None;
                }
            }
        });
        if let Some(selection) = self.cursor_info.selection {
            ui.separator();
            if ui
                .label(format!(
                    "Selection: {:.0}x{:.0}",
                    (selection.max.x - selection.min.x).abs(),
                    (selection.max.y - selection.min.y).abs()
                ))
                .on_hover_text("Click to clear selection")
                .clicked()
            {
                self.cursor_info.selection = None;
            }
        }
        ui.separator();
        if ui.button("Copy").clicked() {
            let size = [self.img.width() as _, self.img.height() as _];
            let rgb_img = self.img.to_rgba8();
            let pixels = rgb_img.as_flat_samples();
            let color_image = ColorImage::from_rgba_unmultiplied(size, pixels.as_slice());
            ui.ctx().copy_image(color_image);
        }
    }
}

impl TarsierApp {
    /// Save the current image
    fn save_image(&mut self, format: ImageFormat, path_file: &Path) -> Result<(), String> {
        let mut bytes: Vec<u8> = Vec::new();
        self.img
            .write_to(&mut Cursor::new(&mut bytes), format)
            .map_err(|e| format!("Cannot write image: {e}"))?;
        bladvak::utils::save_file(&bytes, path_file)
    }
}