#![cfg_attr(all(not(feature = "benchmark"), not(debug_assertions)), windows_subsystem = "windows")]
use std::cell::{Cell, RefCell};
use std::f32;
use std::fmt::Debug;
use std::path::PathBuf;
use std::rc::Rc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::thread::JoinHandle;
use std::time::{Duration, Instant};
use directories_next::ProjectDirs;
use gelatin::application::ApplicationHandler;
use gelatin::event_loop::{self, ActiveEventLoop};
use lazy_static::lazy_static;
use log::{debug, trace};
use gelatin::winit::{
dpi::{PhysicalPosition, PhysicalSize},
event::WindowEvent,
window::Icon,
};
use gelatin::{
application::*,
button::*,
image,
label::*,
line_layout_container::*,
misc::*,
picture::*,
window::{Window, WindowDescriptorBuilder},
NextUpdate, Widget,
};
use crate::configuration::Theme;
use crate::configuration::{Cache, ConfigWindowSection, Configuration};
use crate::version::Version;
use crate::widgets::{
bottom_bar::BottomBar, copy_notification::CopyNotifications, help_screen::*, picture_widget::*,
};
mod clipboard_handler;
mod cmd_line;
mod configuration;
mod handle_panic;
mod image_cache;
mod input_handling;
mod parallel_action;
mod playback_manager;
mod shaders;
mod utils;
mod version;
mod widgets;
lazy_static! {
pub static ref PROJECT_DIRS: Option<ProjectDirs> = ProjectDirs::from("", "", "Emulsion");
}
static NEW_VERSION: &[u8] = include_bytes!("../resource/new-version-available.png");
static NEW_VERSION_LIGHT: &[u8] = include_bytes!("../resource/new-version-available-light.png");
static VISIT_SITE: &[u8] = include_bytes!("../resource/visit-site.png");
static USAGE: &[u8] = include_bytes!("../resource/usage.png");
static LEFT_TO_PAN: &[u8] = include_bytes!("../resource/use-left-to-pan.png");
#[derive(Debug)]
pub enum EmulsionEvent {
ImageLoaded,
}
fn main() {
std::panic::set_hook(Box::new(handle_panic::handle_panic));
env_logger::init();
trace!("Starting up. Panic hook set, logger initialized.");
let (config_path, cache_path) = get_config_and_cache_paths();
let args = cmd_line::parse_args(&config_path, &cache_path);
let cache = Cache::load(&cache_path);
let config = Configuration::load(&config_path);
debug!("Read cache: {cache:#?}");
debug!("Read config: {config:#?}");
let first_launch = cache.is_err();
let cache = Arc::new(Mutex::new(cache.unwrap_or_default()));
let config = Rc::new(RefCell::new(config.unwrap_or_default()));
if args.displayed_folders.is_some() {
config.borrow_mut().title.get_or_insert_with(Default::default).displayed_folders =
args.displayed_folders;
}
let mut application = Application::new();
let app_handler = AppHandler {
cache_path,
args,
first_launch,
cache: cache.clone(),
config: config.clone(),
update_presented: false,
update_checker_join_handle: None,
update_available: Arc::new(AtomicBool::new(false)),
update_check_done: Arc::new(AtomicBool::new(false)),
ui_elements: None,
};
let event_loop = event_loop::EventLoop::<()>::new();
application.start_event_loop(app_handler, event_loop);
}
struct UiElements {
set_theme: Rc<dyn Fn()>,
update_notification: Rc<HorizontalLayoutContainer>,
help_screen: Rc<HelpScreen>,
}
struct AppHandler {
cache_path: PathBuf,
args: cmd_line::Args,
first_launch: bool,
cache: Arc<Mutex<Cache>>,
config: Rc<RefCell<Configuration>>,
update_available: Arc<AtomicBool>,
update_check_done: Arc<AtomicBool>,
update_presented: bool,
update_checker_join_handle: Option<JoinHandle<()>>,
ui_elements: Option<UiElements>,
}
impl AppHandler {
fn create_window(
&mut self,
event_loop: &mut ActiveEventLoop,
args: cmd_line::Args,
first_launch: bool,
cache: Arc<Mutex<Cache>>,
config: Rc<RefCell<Configuration>>,
) -> Option<JoinHandle<()>> {
let window: Rc<Window> = {
let window_cache = &mut cache.lock().unwrap().window;
let window_cfg = &config.borrow().window;
let window_defaults = configuration::CacheWindowSection::default();
if let Some(ConfigWindowSection {
use_last_window_area: Some(false),
win_x,
win_y,
win_w,
win_h,
..
}) = window_cfg
{
window_cache.win_x = if let Some(x) = win_x { *x } else { window_defaults.win_x };
window_cache.win_y = if let Some(y) = win_y { *y } else { window_defaults.win_y };
window_cache.win_w = if let Some(w) = win_w { *w } else { window_defaults.win_w };
window_cache.win_h = if let Some(h) = win_h { *h } else { window_defaults.win_h };
}
if let Some(window_cfg) = window_cfg {
if let Some(start_maximized) = window_cfg.start_maximized {
window_cache.maximized = start_maximized;
}
}
let pos = PhysicalPosition::new(window_cache.win_x, window_cache.win_y);
let size = PhysicalSize::new(window_cache.win_w, window_cache.win_h);
let window_desc = WindowDescriptorBuilder::default()
.icon(Some(make_icon()))
.maximized(window_cache.maximized)
.size(size)
.position(Some(pos))
.app_id(Some("Emulsion".into()))
.build()
.unwrap();
let window = event_loop.create_window(window_desc).unwrap();
if let Some(ConfigWindowSection { start_fullscreen: Some(true), .. }) = window_cfg {
window.set_fullscreen(true);
}
window
};
add_window_movement_listener(&window, cache.clone());
let update_label_image = Rc::new(Picture::from_encoded_bytes(NEW_VERSION));
let update_label_image_light = Rc::new(Picture::from_encoded_bytes(NEW_VERSION_LIGHT));
let update_label = make_update_label();
let update_notification = make_update_notification(update_label.clone());
let usage_img = Picture::from_encoded_bytes(USAGE);
let help_screen = Rc::new(HelpScreen::new(usage_img));
let left_to_pan_img = Picture::from_encoded_bytes(LEFT_TO_PAN);
let left_to_pan_hint = Rc::new(HelpScreen::new(left_to_pan_img));
let copy_notifications_widget = Rc::new(Label::new());
let copy_notifications = CopyNotifications::new(©_notifications_widget);
let bottom_bar = Rc::new(BottomBar::new(&config.borrow()));
let picture_widget = make_picture_widget(
&window,
bottom_bar.clone(),
left_to_pan_hint.clone(),
copy_notifications,
config.clone(),
cache.clone(),
);
if let Some(file_path) = &args.file_path {
picture_widget.jump_to_path(file_path);
}
let picture_area_container = make_picture_area_container();
picture_area_container.add_child(picture_widget.clone());
picture_area_container.add_child(copy_notifications_widget);
picture_area_container.add_child(left_to_pan_hint);
picture_area_container.add_child(help_screen.clone());
picture_area_container.add_child(update_notification.clone());
let root_container = make_root_container();
root_container.add_child(picture_area_container);
root_container.add_child(bottom_bar.widget.clone());
self.update_available = Arc::new(AtomicBool::new(false));
self.update_check_done = Arc::new(AtomicBool::new(false));
let theme = {
Rc::new(Cell::new(match &config.borrow().window {
Some(ConfigWindowSection { theme: Some(theme_cfg), .. }) => *theme_cfg,
_ => cache.lock().unwrap().theme(),
}))
};
let set_theme = {
let update_label = update_label.clone();
let picture_widget = picture_widget.clone();
let update_notification = update_notification.clone();
let window = window.clone();
let theme = theme.clone();
let update_available = self.update_available.clone();
let bottom_bar = bottom_bar.clone();
Rc::new(move || {
match theme.get() {
Theme::Light => {
picture_widget.set_bright_shade(0.96);
window.set_bg_color([0.85, 0.85, 0.85, 1.0]);
update_notification.set_bg_color([0.06, 0.06, 0.06, 1.0]);
update_label.set_icon(Some(update_label_image_light.clone()));
}
Theme::Dark => {
picture_widget.set_bright_shade(0.11);
window.set_bg_color([0.03, 0.03, 0.03, 1.0]);
update_notification.set_bg_color([0.85, 0.85, 0.85, 1.0]);
update_label.set_icon(Some(update_label_image.clone()));
}
}
bottom_bar.set_theme(theme.get(), update_available.load(Ordering::SeqCst));
})
};
set_theme();
{
let cache = cache.clone();
let set_theme = set_theme.clone();
bottom_bar.theme_button.set_on_click(move || {
let new_theme = theme.get().switch_theme();
theme.set(new_theme);
cache.lock().unwrap().set_theme(new_theme);
set_theme();
});
}
{
let slider = bottom_bar.slider.clone();
let picture_widget = picture_widget.clone();
bottom_bar.slider.set_on_value_change(move || {
picture_widget.jump_to_index(slider.value());
});
}
{
let picture_widget = picture_widget.clone();
bottom_bar.orig_scale_button.set_on_click(move || {
picture_widget.set_img_size_to_orig();
});
}
{
let picture_widget = picture_widget.clone();
bottom_bar.fit_best_button.set_on_click(move || {
picture_widget.set_img_size_to_fit(false);
});
}
{
bottom_bar.fit_stretch_button.set_on_click(move || {
picture_widget.set_img_size_to_fit(true);
});
}
let help_visible = Cell::new(first_launch);
help_screen.set_visible(help_visible.get());
update_notification
.set_visible(help_visible.get() && self.update_available.load(Ordering::SeqCst));
{
let update_available = self.update_available.clone();
let help_screen = help_screen.clone();
let update_notification = update_notification.clone();
let bottom_bar_clone = bottom_bar.clone();
bottom_bar.help_button.set_on_click(move || {
help_visible.set(!help_visible.get());
help_screen.set_visible(help_visible.get());
bottom_bar_clone.set_help_visible(help_visible.get());
update_notification
.set_visible(help_visible.get() && update_available.load(Ordering::SeqCst));
});
}
window.set_root(root_container);
let check_updates_enabled =
config.borrow().updates.as_ref().map(|u| u.check_updates).unwrap_or(true);
let update_checker_join_handle = {
let updates = &mut cache.lock().unwrap().updates;
let cache = cache.clone();
let update_available = self.update_available.clone();
let update_check_done = self.update_check_done.clone();
if check_updates_enabled && updates.update_check_needed() {
Some(std::thread::spawn(move || {
let has_update = update::check_for_updates();
update_available.store(has_update, Ordering::SeqCst);
update_check_done.store(true, Ordering::SeqCst);
if !has_update {
cache.lock().unwrap().updates.set_update_check_time();
}
}))
} else {
None
}
};
self.ui_elements =
Some(UiElements { set_theme, update_notification, help_screen });
update_checker_join_handle
}
}
impl ApplicationHandler for AppHandler {
fn handle_can_create_surface(&mut self, event_loop: &mut ActiveEventLoop) {
self.update_checker_join_handle = self.create_window(
event_loop,
self.args.clone(),
self.first_launch,
self.cache.clone(),
self.config.clone(),
);
}
fn handle_window_event(
&mut self,
_event_loop: &ActiveEventLoop,
_window_id: gelatin::winit::window::WindowId,
_event: &WindowEvent,
) -> NextUpdate {
if self.update_presented {
return NextUpdate::Latest;
}
if let Some(ui) = &self.ui_elements {
if self.update_check_done.load(Ordering::SeqCst) {
self.update_presented = true;
(ui.set_theme)();
if ui.help_screen.visible() && self.update_available.load(Ordering::SeqCst) {
ui.update_notification.set_visible(true);
}
}
}
NextUpdate::WaitUntil(Instant::now() + Duration::from_secs(1))
}
fn exiting(&mut self) {
self.cache.lock().unwrap().save(&self.cache_path).unwrap();
if let Some(h) = self.update_checker_join_handle.take() {
h.join().unwrap();
}
}
}
fn make_icon() -> Icon {
let img = image::load_from_memory(include_bytes!("../resource/emulsion48.png")).unwrap();
let rgba = img.into_rgba8();
let (w, h) = rgba.dimensions();
Icon::from_rgba(rgba.into_raw(), w, h).unwrap()
}
fn add_window_movement_listener(window: &Window, cache: Arc<Mutex<Cache>>) {
window.add_global_event_handler(move |window, event| match event {
WindowEvent::Resized(new_size) => {
let mut cache = cache.lock().unwrap();
cache.window.win_w = new_size.width;
cache.window.win_h = new_size.height;
cache.window.maximized = window.window_mut().is_maximized();
}
WindowEvent::Moved(new_pos) => {
let mut cache = cache.lock().unwrap();
cache.window.win_x = new_pos.x;
cache.window.win_y = new_pos.y;
cache.window.maximized = window.window_mut().is_maximized();
}
_ => (),
});
}
fn make_root_container() -> Rc<VerticalLayoutContainer> {
let container = Rc::new(VerticalLayoutContainer::new());
container.set_margin_all(0.0);
container.set_height(Length::Stretch { min: 0.0, max: f32::INFINITY });
container.set_width(Length::Stretch { min: 0.0, max: f32::INFINITY });
container
}
fn make_picture_area_container() -> Rc<VerticalLayoutContainer> {
let picture_area_container = Rc::new(VerticalLayoutContainer::new());
picture_area_container.set_margin_all(0.0);
picture_area_container.set_height(Length::Stretch { min: 0.0, max: f32::INFINITY });
picture_area_container.set_width(Length::Stretch { min: 0.0, max: f32::INFINITY });
picture_area_container
}
fn make_update_label() -> Rc<Label> {
let update_label = Rc::new(Label::new());
update_label.set_margin_top(4.0);
update_label.set_margin_bottom(4.0);
update_label.set_fixed_size(LogicalVector::new(200.0, 24.0));
update_label.set_horizontal_align(Alignment::Center);
update_label
}
fn make_update_notification(update_label: Rc<Label>) -> Rc<HorizontalLayoutContainer> {
let container = Rc::new(HorizontalLayoutContainer::new());
container.set_vertical_align(Alignment::End);
container.set_horizontal_align(Alignment::Start);
container.set_width(Length::Stretch { min: 0.0, max: f32::INFINITY });
container.set_height(Length::Fixed(32.0));
let update_button = Rc::new(Button::new());
let button_image = Rc::new(Picture::from_encoded_bytes(VISIT_SITE));
update_button.set_icon(Some(button_image));
update_button.set_margin_top(4.0);
update_button.set_margin_bottom(4.0);
update_button.set_fixed_size(LogicalVector::new(100.0, 24.0));
update_button.set_horizontal_align(Alignment::Center);
update_button.set_on_click(|| {
open::that("https://arturkovacs.github.io/emulsion-website/").unwrap();
});
container.add_child(update_label);
container.add_child(update_button);
container
}
fn make_picture_widget(
window: &Rc<Window>,
bottom_bar: Rc<BottomBar>,
left_to_pan_hint: Rc<HelpScreen>,
copy_notifications: CopyNotifications,
config: Rc<RefCell<Configuration>>,
cache: Arc<Mutex<Cache>>,
) -> Rc<PictureWidget> {
let picture_widget = Rc::new(PictureWidget::new(
&window.display_mut(),
window,
bottom_bar,
left_to_pan_hint,
copy_notifications,
config,
cache,
));
picture_widget.set_height(Length::Stretch { min: 0.0, max: f32::INFINITY });
picture_widget.set_width(Length::Stretch { min: 0.0, max: f32::INFINITY });
picture_widget
}
pub fn get_config_and_cache_paths() -> (PathBuf, PathBuf) {
let config_folder;
let cache_folder;
if let Some(ref project_dirs) = *PROJECT_DIRS {
config_folder = project_dirs.config_dir().to_owned();
cache_folder = project_dirs.cache_dir().to_owned();
} else {
let exe_path = std::env::current_exe().unwrap();
let exe_folder = exe_path.parent().unwrap();
config_folder = exe_folder.to_owned();
cache_folder = exe_folder.to_owned();
}
if !config_folder.exists() {
std::fs::create_dir_all(&config_folder).unwrap();
}
if !cache_folder.exists() {
std::fs::create_dir_all(&cache_folder).unwrap();
}
(config_folder.join("cfg.toml"), cache_folder.join("cache.toml"))
}
#[cfg(not(feature = "networking"))]
mod update {
pub fn check_for_updates() -> bool {
false
}
}
#[cfg(feature = "networking")]
mod update {
use serde::Deserialize;
#[derive(Deserialize)]
struct ReleaseInfoJson {
tag_name: String,
}
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
fn latest_release() -> Result<ReleaseInfoJson> {
let url = "https://api.github.com/repos/ArturKovacs/emulsion/releases/latest";
let res = ureq::get(url).set("User-Agent", "emulsion").call();
match res {
Ok(res) => {
let release_info = res.into_json()?;
Ok(release_info)
}
Err(err) => Err(Box::from(err)),
}
}
fn compare_release(info: &ReleaseInfoJson) -> Result<bool> {
use crate::version::Version;
use std::str::FromStr;
let current = Version::cargo_pkg_version();
let latest = Version::from_str(&info.tag_name)?;
if latest > current {
println!("Current version is {}, latest version is {}", current, latest);
Ok(true)
} else {
Ok(false)
}
}
pub fn check_for_updates() -> bool {
match latest_release() {
Ok(info) => match compare_release(&info) {
Ok(is_newer) => is_newer,
Err(err) => {
eprintln!("Error parsing release tag: {}", err);
false
}
},
Err(err) => {
eprintln!("Error checking latest release: {}", err);
false
}
}
}
}