use bladvak::{
File,
app::BladvakApp,
errors::{AppError, ErrorManager},
};
use bladvak::{
eframe::{
CreationContext,
egui::{self, Color32, Image, ImageSource, Pos2},
},
utils::is_native,
};
use bladvak::{egui_extras, log};
use image::{ColorType, DynamicImage, ImageReader};
use std::{fmt::Debug, io::Cursor, path::PathBuf, sync::Arc};
use crate::{
panels::{CursorInfo, ImageInfo, ImageOperationsPanel},
side_panel::{EditMode, ImageOperations},
};
#[derive(serde::Deserialize, serde::Serialize, Debug)]
pub struct NewImage {
pub(crate) is_open: bool,
pub(crate) width: u32,
pub(crate) height: u32,
pub(crate) color_type: ColorType,
}
impl Default for NewImage {
fn default() -> Self {
Self {
is_open: false,
height: 1024,
width: 1024,
color_type: ColorType::Rgba16,
}
}
}
#[derive(serde::Deserialize, serde::Serialize, Debug)]
pub struct CursorState {
#[serde(skip)]
pub selection: Option<egui::Rect>,
#[serde(skip)]
pub start_selection: Pos2,
#[serde(skip)]
pub last_drawing_point: Option<Pos2>,
#[serde(skip)]
pub is_selecting: bool,
pub cursor_op_as_window: bool,
pub remove_selection_after_op: bool,
}
impl Default for CursorState {
fn default() -> Self {
Self {
selection: None,
cursor_op_as_window: false,
start_selection: Pos2::ZERO,
last_drawing_point: None,
is_selecting: false,
remove_selection_after_op: false,
}
}
}
#[derive(serde::Deserialize, serde::Serialize)]
#[serde(default)]
pub struct TarsierApp {
#[serde(skip)]
pub(crate) img: DynamicImage,
#[serde(skip)]
pub(crate) saved_img: DynamicImage,
#[serde(skip)]
pub(crate) texture: Option<egui::TextureHandle>,
#[serde(skip)]
pub(crate) exif: Option<exif::Exif>,
pub(crate) cursor_info: CursorState,
pub(crate) image_info_as_window: bool,
pub(crate) image_operations: ImageOperations,
pub(crate) save_path: Option<PathBuf>,
#[serde(skip)]
pub(crate) new_image: NewImage,
}
impl Debug for TarsierApp {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut debug_fmt = f.debug_struct("TarsierApp");
debug_fmt.finish()
}
}
const ASSET: &[u8] = include_bytes!("../assets/icon-1024.png");
impl Default for TarsierApp {
fn default() -> Self {
let (img, _) = Self::load_default_image();
Self {
saved_img: img.clone(),
img,
texture: None,
exif: None,
cursor_info: CursorState::default(),
image_info_as_window: false,
image_operations: ImageOperations::default(),
save_path: None,
new_image: NewImage::default(),
}
}
}
impl TarsierApp {
pub(crate) fn load_default_image() -> (DynamicImage, Cursor<&'static [u8]>) {
let cursor = Cursor::new(ASSET);
#[allow(clippy::unwrap_used)]
let img = ImageReader::new(cursor.clone())
.with_guessed_format()
.unwrap()
.decode()
.unwrap();
(img, cursor)
}
const CROP_ICON: ImageSource<'_> = egui::include_image!("../assets/icon_crop.png");
pub(crate) fn cursor_ui(&mut self, ui: &mut egui::Ui) {
if self.image_operations.mode == EditMode::Drawing {
self.button_drawing(ui);
} else {
match self.cursor_info.selection {
Some(rect) => {
let width = rect.width().abs();
let height = rect.height().abs();
ui.label(format!("Width: {width:.0}"));
ui.label(format!("Height: {height:.0}"));
ui.label(format!("Min: {:?}", rect.left_top()));
ui.label(format!("Max: {:?}", rect.right_bottom()));
}
None => {
ui.label("No selection");
}
}
if let Some(selection) = self.cursor_info.selection {
let icon_image = Image::new(Self::CROP_ICON);
let icon = if ui.ctx().global_style().visuals.dark_mode {
icon_image
} else {
icon_image.tint(Color32::BLACK)
};
#[allow(clippy::cast_sign_loss)]
#[allow(clippy::cast_possible_truncation)]
if ui
.add(egui::Button::image_and_text(icon, "Crop"))
.on_hover_text("Crop the image")
.clicked()
{
let min_pos = selection.min;
let max_pos = selection.max;
let min_x = min_pos.x as u32;
let min_y = min_pos.y as u32;
let max_x = max_pos.x as u32;
let max_y = max_pos.y as u32;
let cropped_img = self
.img
.crop_imm(min_x, min_y, max_x - min_x, max_y - min_y);
self.update_image(cropped_img);
self.cursor_info.selection = None;
}
}
}
}
pub(crate) fn update_file(&mut self, new_img: DynamicImage, opt_cursor: Option<Cursor<&[u8]>>) {
self.saved_img.clone_from(&new_img);
self.update_image(new_img);
let exifreader = exif::Reader::new();
if let Some(bytes) = opt_cursor {
let mut bufreader = std::io::BufReader::new(bytes);
match exifreader.read_from_container(&mut bufreader) {
Ok(exif) => self.exif = Some(exif),
Err(e) => {
self.exif = None;
log::info!("Cannot get exif of image: {e}");
}
}
} else {
self.exif = None;
}
}
pub(crate) fn update_image(&mut self, new_img: DynamicImage) {
self.img = new_img;
self.updated_image();
}
pub(crate) fn updated_image(&mut self) {
self.texture = None;
if self.cursor_info.remove_selection_after_op {
self.cursor_info.selection = None;
}
}
}
impl BladvakApp<'_> for TarsierApp {
fn side_panel(
&mut self,
ui: &mut egui::Ui,
func_ui: impl FnOnce(&mut egui::Ui, &mut TarsierApp),
) {
egui::Frame::central_panel(&ui.ctx().global_style())
.show(ui, |panel_ui| func_ui(panel_ui, self));
}
fn panel_list(&self) -> Vec<Box<dyn bladvak::app::BladvakPanel<App = Self>>> {
vec![
Box::new(ImageInfo),
Box::new(ImageOperationsPanel),
Box::new(CursorInfo),
]
}
fn is_side_panel(&self) -> bool {
true
}
fn is_open_button(&self) -> bool {
true
}
fn handle_file(&mut self, file: File) -> Result<(), AppError> {
let img_reader = ImageReader::new(Cursor::new(&file.data)).with_guessed_format()?;
let img = match img_reader.decode() {
Ok(img) => img,
Err(e) => {
return Err(AppError::new_with_source(
"Cannot decode image",
Arc::new(e),
));
}
};
let cursor = Cursor::new(file.data.as_ref());
self.update_file(img, Some(cursor));
Ok(())
}
fn top_panel(&mut self, ui: &mut egui::Ui, error_manager: &mut ErrorManager) {
self.app_top_panel(ui, error_manager);
}
fn menu_file(&mut self, ui: &mut egui::Ui, error_manager: &mut ErrorManager) {
self.app_menu_file(ui, error_manager);
}
fn central_panel(&mut self, ui: &mut egui::Ui, error_manager: &mut ErrorManager) {
self.app_central_panel(ui, error_manager);
}
fn name() -> String {
env!("CARGO_PKG_NAME").to_string()
}
fn version() -> String {
env!("CARGO_PKG_VERSION").to_string()
}
fn repo_url() -> String {
"https://github.com/Its-Just-Nans/tarsier".to_string()
}
fn icon() -> &'static [u8] {
&include_bytes!("../assets/icon-256.png")[..]
}
fn try_new_with_args(
saved_state: Self,
cc: &CreationContext<'_>,
args: &[String],
) -> Result<Self, AppError> {
egui_extras::install_image_loaders(&cc.egui_ctx);
if is_native() && args.len() > 1 {
use std::fs;
let path = &args[1];
let absolute_path = fs::canonicalize(path)?;
let bytes = fs::read(&absolute_path)?;
let cursor: Cursor<&[u8]> = Cursor::new(bytes.as_ref());
let img_reader = ImageReader::new(cursor);
match img_reader.with_guessed_format()?.decode() {
Ok(img) => {
let mut app = saved_state;
let cursor_data = Cursor::new(bytes.as_ref());
app.update_file(img, Some(cursor_data));
app.save_path = Some(absolute_path);
Ok(app)
}
Err(e) => {
log::error!("Failed to load image '{}': {e}", absolute_path.display());
Err(AppError::new_with_source(
format!("Failed to load image '{}'", absolute_path.display()),
Arc::new(e),
))
}
}
} else {
Ok(saved_state)
}
}
}