Skip to content

Commit

Permalink
Merge branch 'main' into feat/drag-and-drop
Browse files Browse the repository at this point in the history
  • Loading branch information
r3tr0ananas authored Nov 12, 2024
2 parents 00793d9 + 429bbf8 commit d995d24
Show file tree
Hide file tree
Showing 8 changed files with 412 additions and 97 deletions.
10 changes: 6 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "roseate"
edition = "2021"
version = "0.1.0-alpha.11"
version = "0.1.0-alpha.14"
description = "A small and simple but fancy image viewer built with Rust that's cross platform."
authors = ["Goldy <[email protected]>"]
license = "GPL-3.0"
Expand All @@ -15,17 +15,19 @@ rfd = "0.15.0"
imagesize = "0.13.0"
eframe = { version = "0.29.1", features = ["default"] }
egui_extras = { version = "0.29.1", features = ["all_loaders"]}
image = {version = "0.25.4", features = ["jpeg", "png"]}
image = {version = "0.25.5", features = ["jpeg", "png"]}
egui_animation = "0.6.0"
simple-easing = "1.0.1"
log = "0.4.22"
env_logger = "0.11.5"
re_format = "0.19.0"
re_format = "0.19.1"
cap = "0.1.2"
clap = {version = "4.5.20", features = ["derive"]}
rand = "0.8.5"
display-info = "0.5.1"
display-info = "0.5.2"
egui-notify = "0.17.0"
svg_metadata = "0.5.1"
textwrap = "0.16.1"

[profile.dev.package."*"]
opt-level = 3
156 changes: 95 additions & 61 deletions src/app.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use std::{time::Duration, vec};

use cirrus_theming::Theme;
use eframe::egui::{self, Color32, CursorIcon, ImageSource, Margin, Rect, Stroke, Vec2};
use egui_notify::Toasts;
use cirrus_theming::Theme;
use eframe::egui::{self, Align, Color32, CursorIcon, Frame, ImageSource, Layout, Margin, Rect, Style, TextStyle, Vec2};

use crate::{error, files, image::{apply_image_optimizations, Image}, info_box::InfoBox, window_scaling::WindowScaling, zoom_pan::ZoomPan};
use crate::{error, files, image::Image, image_loader::ImageLoader, info_box::InfoBox, window_scaling::WindowScaling, zoom_pan::ZoomPan};

pub struct Roseate {
theme: Theme,
Expand All @@ -15,23 +15,32 @@ pub struct Roseate {
window_scaling: WindowScaling,
last_window_rect: Rect,
image_loaded: bool,
image_loader: ImageLoader,
last_window_rect: Rect
dropped_files: Vec<egui::DroppedFile>,
}

impl Roseate {
pub fn new(image: Option<Image>, theme: Theme, toasts: Toasts) -> Self {
let (ib_image, ib_theme) = (image.clone(), theme.clone());
let mut image_loader = ImageLoader::new();

if image.is_some() {
image_loader.load_image(&mut image.clone().unwrap(), false);
}

Self {
image,
theme,
toasts: toasts,
zoom_pan: ZoomPan::new(),
info_box: InfoBox::new(ib_image, ib_theme),
info_box: InfoBox::new(),
window_scaling: WindowScaling::new(),
last_window_rect: Rect::NOTHING,
image_loaded: false,
dropped_files: vec![]
image_loader: image_loader,
last_window_rect: Rect::NOTHING,
dropped_files: Vec<egui::DroppedFile>,
}
}
}
Expand All @@ -40,11 +49,14 @@ impl eframe::App for Roseate {

fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
let central_panel_frame = egui::Frame {
inner_margin: Margin::same(5.0),
fill: Color32::from_hex(&self.theme.hex_code).unwrap(), // I mean... it should not fail... we know it's a valid hex colour...
..Default::default()
};

ctx.set_style(Style {override_text_style: Some(TextStyle::Monospace), ..Default::default()});

self.info_box.init(&self.image, &self.theme);

self.info_box.handle_input(ctx);
self.zoom_pan.handle_zoom_input(ctx);
self.zoom_pan.handle_reset_input(ctx);
Expand Down Expand Up @@ -105,7 +117,6 @@ impl eframe::App for Roseate {
let rose_width: f32 = 130.0;

egui::Frame::default()
.stroke(Stroke::default())
.outer_margin(
// I adjust the margin as it's the only way I know to
// narrow down the interactive part (clickable part) of the rose image.
Expand All @@ -127,13 +138,13 @@ impl eframe::App for Roseate {
let image_result = files::select_image();

match image_result {
Ok(image) => {
// TODO: Need to improve this. Possibly by introducing an init function for info box .
Ok(mut image) => {
self.image = Some(image.clone());
self.info_box = InfoBox::new(Some(image.clone()), self.theme.clone());
// TODO: Use config's value for lazy load instead.
self.image_loader.load_image(&mut image, true);
},
Err(error) => {
error::log_and_toast(error, &mut self.toasts)
error::log_and_toast(error.into(), &mut self.toasts)
.duration(Some(Duration::from_secs(5)));
},
}
Expand All @@ -142,67 +153,90 @@ impl eframe::App for Roseate {
);
});

return;
return; // don't do anything else, you know, like stop right there bitch
}

self.info_box.update(ctx);
self.zoom_pan.update(ctx);

if !self.image_loaded {
let mutable_image = self.image.as_mut().unwrap();

let mut optimizations = Vec::new();
optimizations = apply_image_optimizations(optimizations, &mutable_image.image_size);

mutable_image.load_image(&optimizations);

self.image_loaded = true;
}
self.image_loader.update(&mut self.toasts);

let image = self.image.clone().unwrap();

self.window_scaling.update(&window_rect, &image.image_size);

ui.centered_and_justified(|ui| {
let scaled_image_size = self.window_scaling.relative_image_size(
Vec2::new(image.image_size.width as f32, image.image_size.height as f32)
);

if self.zoom_pan.is_pan_out_of_bounds(scaled_image_size) {
self.zoom_pan.schedule_pan_reset(Duration::from_millis(300));
};

// NOTE: umm do we move this to window scaling... *probably* if we
// want to stay consistent with zoom_pan but this isn't important right now.
let scaled_image_width_animated = egui_animation::animate_eased(
ctx, "image_scale_width", scaled_image_size.x, 1.5, simple_easing::cubic_in_out
) as u32 as f32;
let scaled_image_height_animated = egui_animation::animate_eased(
ctx, "image_scale_height", scaled_image_size.y, 1.5, simple_easing::cubic_in_out
) as u32 as f32;

let scaled_image_size = Vec2::new(scaled_image_width_animated, scaled_image_height_animated);

let zoom_scaled_image_size = self.zoom_pan.relative_image_size(scaled_image_size);
let image_position = ui.max_rect().center() - zoom_scaled_image_size * 0.5 + self.zoom_pan.pan_offset;

let zoom_pan_rect = Rect::from_min_size(image_position, zoom_scaled_image_size);

let response = ui.allocate_rect(zoom_pan_rect, egui::Sense::hover());

egui::Image::from_bytes(
format!("bytes://{}", image.image_path.to_string_lossy()), image.image_bytes.unwrap()
).rounding(10.0)
.paint_at(ui, zoom_pan_rect);
if self.image_loader.image_loaded {
ui.centered_and_justified(|ui| {
let scaled_image_size = self.window_scaling.relative_image_size(
Vec2::new(image.image_size.width as f32, image.image_size.height as f32)
);

if self.zoom_pan.is_pan_out_of_bounds(scaled_image_size) {
self.zoom_pan.schedule_pan_reset(Duration::from_millis(300));
};

// NOTE: umm do we move this to window scaling... *probably* if we
// want to stay consistent with zoom_pan but this isn't important right now.
let scaled_image_width_animated = egui_animation::animate_eased(
ctx, "image_scale_width", scaled_image_size.x, 1.5, simple_easing::cubic_in_out
) as u32 as f32;
let scaled_image_height_animated = egui_animation::animate_eased(
ctx, "image_scale_height", scaled_image_size.y, 1.5, simple_easing::cubic_in_out
) as u32 as f32;

let scaled_image_size = Vec2::new(scaled_image_width_animated, scaled_image_height_animated);

let zoom_scaled_image_size = self.zoom_pan.relative_image_size(scaled_image_size);
let image_position = ui.max_rect().center() - zoom_scaled_image_size * 0.5 + self.zoom_pan.pan_offset;

let zoom_pan_rect = Rect::from_min_size(image_position, zoom_scaled_image_size);

let response = ui.allocate_rect(zoom_pan_rect, egui::Sense::hover());

egui::Image::from_bytes(
format!(
"bytes://{}", image.image_path.to_string_lossy()
),
// we can unwrap because we know the bytes exist thanks to 'self.image_loader.image_loaded'.
image.image_bytes.lock().unwrap().clone().unwrap()
).rounding(10.0)
.paint_at(ui, zoom_pan_rect);

self.zoom_pan.handle_pan_input(ctx, &response, self.info_box.response.as_ref());
});

self.zoom_pan.handle_pan_input(ctx, &response, self.info_box.response.as_ref());
});
// We must update the WindowScaling with the window size AFTER
// the image has loaded to maintain that smooth scaling animation.
self.window_scaling.update(&window_rect, &image.image_size);

ctx.request_repaint_after_secs(0.5); // We need to request repaints just in
// just in case one doesn't happen when the window is resized in a certain circumstance
// (i.e. the user maximizes the window and doesn't interact with it). I'm not sure how else we can fix it.
ctx.request_repaint_after_secs(0.5); // We need to request repaints just in
// just in case one doesn't happen when the window is resized in a certain circumstance
// (i.e. the user maximizes the window and doesn't interact with it). I'm not sure how else we can fix it.
}
});

// This is deliberately placed after the central panel so the central panel
// can take up all the space essentially ignoring the space this panel would otherwise take.
// Check out the egui docs for more clarification: https://docs.rs/egui/0.29.1/egui/containers/panel/struct.CentralPanel.html
egui::TopBottomPanel::bottom("status_bar")
.show_separator_line(false)
.frame(
Frame::none()
.outer_margin(Margin {left: 10.0, bottom: 7.0, ..Default::default()})
).show(ctx, |ui| {
if let Some(loading) = &self.image_loader.image_loading {
ui.with_layout(Layout::left_to_right(Align::Center), |ui| {
ui.add(
egui::Spinner::new()
.color(Color32::from_hex("#e05f78").unwrap()) // NOTE: This should be the default accent colour.
.size(20.0)
);

if let Some(message) = &loading.message {
ui.label(message);
}
});
}
}
);

}

}
Expand Down
51 changes: 46 additions & 5 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ use egui_notify::{Toast, Toasts};
#[derive(Debug)]
pub enum Error {
FileNotFound(PathBuf),
NoFileSelected
NoFileSelected,
FailedToApplyOptimizations(String),
ImageFormatNotSupported(String),
}

impl Error {
Expand All @@ -25,13 +27,52 @@ impl Display for Error {
Error::NoFileSelected => write!(
f, "No file was selected in the file dialogue!"
),
Error::FailedToApplyOptimizations(technical_reason) => write!(
f,
"Failed to apply optimizations to this image! \
Roseate will run slower than usual and use a lot more memory \
possibly leading to system crashes. BEWARE! \n\nTechnical Reason: {}",
technical_reason
),
Error::ImageFormatNotSupported(image_format) => write!(
f, "The image format '{}' is not supported!", image_format
),
}
}
}

pub fn log_and_toast(error: Error, toasts: &mut Toasts) -> &mut Toast {
log::error!("{}", error);
pub enum LogAndToastError {
Error(Error),
String(String)
}

impl Into<LogAndToastError> for Error {
fn into(self) -> LogAndToastError {
LogAndToastError::String(self.message())
}
}

impl Into<LogAndToastError> for String {
fn into(self) -> LogAndToastError {
LogAndToastError::String(self)
}
}

impl Into<LogAndToastError> for &str {
fn into(self) -> LogAndToastError {
LogAndToastError::String(self.to_string())
}
}

pub fn log_and_toast(error_or_string: LogAndToastError, toasts: &mut Toasts) -> &mut Toast {
let error_message = match error_or_string {
LogAndToastError::Error(error) => error.message(),
LogAndToastError::String(string) => string,
};

log::error!("{}", error_message);

toasts.error(error.message())
.duration(Some(Duration::from_secs(5)))
toasts.error(
textwrap::wrap(error_message.as_str(), 100).join("\n")
).duration(Some(Duration::from_secs(5)))
}
2 changes: 1 addition & 1 deletion src/files.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::image::Image;

pub fn select_image() -> Result<Image, Error> {
let image_path = FileDialog::new()
.add_filter("images", &["png", "jpeg", "jpg", "webp", "gif"])
.add_filter("images", &["png", "jpeg", "jpg", "webp", "gif", "svg"])
.pick_file();

let image_or_error = match image_path {
Expand Down
Loading

0 comments on commit d995d24

Please sign in to comment.