Repository: weclaw1/image-roll Branch: main Commit: 50870799d10c Files: 29 Total size: 224.8 KB Directory structure: gitextract_s858hyto/ ├── .github/ │ └── workflows/ │ └── ci.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── build.rs ├── debian/ │ └── postinst └── src/ ├── app.rs ├── file_list.rs ├── image.rs ├── image_list.rs ├── image_operation.rs ├── main.rs ├── resources/ │ ├── cargo-sources.json │ ├── com.github.weclaw1.ImageRoll.desktop │ ├── com.github.weclaw1.ImageRoll.gschema.xml │ ├── com.github.weclaw1.ImageRoll.metainfo.xml │ ├── com.github.weclaw1.ImageRoll.yaml │ ├── image-roll.cmb │ ├── image-roll.ui │ ├── resources.gresource │ └── resources.xml ├── settings.rs ├── test_utils.rs ├── ui/ │ ├── action.rs │ ├── controllers.rs │ ├── event.rs │ └── widgets.rs └── ui.rs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/ci.yml ================================================ on: [push, pull_request] name: Continuous integration jobs: check: name: Check runs-on: ubuntu-22.04 steps: - name: Checkout sources uses: actions/checkout@v2 - name: Install stable toolchain uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true - name: Install dependencies run: | sudo apt update sudo apt install -y libgtk-4-dev - name: Set up cache uses: actions/cache@v2 with: path: | ~/.cargo/registry ~/.cargo/git target key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - name: Run cargo check uses: actions-rs/cargo@v1 with: command: check test: name: Test Suite runs-on: ubuntu-22.04 steps: - name: Checkout sources uses: actions/checkout@v2 - name: Install stable toolchain uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true - name: Install dependencies run: | sudo apt update sudo apt install -y libgtk-4-dev - name: Set up cache uses: actions/cache@v2 with: path: | ~/.cargo/registry ~/.cargo/git target key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - name: Run cargo test uses: actions-rs/cargo@v1 with: command: test lints: name: Lints runs-on: ubuntu-22.04 steps: - name: Checkout sources uses: actions/checkout@v2 - name: Install stable toolchain uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true components: rustfmt, clippy - name: Run cargo fmt uses: actions-rs/cargo@v1 with: command: fmt args: --all -- --check - name: Install dependencies run: | sudo apt update sudo apt install -y libgtk-4-dev - name: Set up cache uses: actions/cache@v2 with: path: | ~/.cargo/registry ~/.cargo/git target key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - name: Run cargo clippy uses: actions-rs/cargo@v1 with: command: clippy args: -- -D warnings build: name: Build needs: [check, test] runs-on: ubuntu-22.04 steps: - name: Checkout sources uses: actions/checkout@v2 - name: Install stable toolchain uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true - name: Install dependencies run: | sudo apt update sudo apt install -y libgtk-4-dev - name: Set up cache uses: actions/cache@v2 with: path: | ~/.cargo/registry ~/.cargo/git target key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - name: Run cargo build uses: actions-rs/cargo@v1 with: command: build args: --release - name: Upload Artifact uses: actions/upload-artifact@v2 with: name: image-roll path: target/release/image-roll - name: Create debian package run: | cargo install cargo-deb cargo deb - name: Upload debian Artifact uses: actions/upload-artifact@v2 with: name: image-roll-deb path: target/debian/image-roll*.deb ================================================ FILE: .gitignore ================================================ /target /build-dir src/resources/image-roll_ui.glade~ /.flatpak-builder ================================================ FILE: Cargo.toml ================================================ [package] name = "image-roll" version = "2.1.0" license = "MIT" description = "Image Roll is a simple and fast GTK image viewer with basic image manipulation tools." homepage = "https://github.com/weclaw1/image-roll" repository = "https://github.com/weclaw1/image-roll" readme = "README.md" authors = ["Robert Węcławski "] edition = "2021" [dependencies] log = "0.4.17" env_logger = "0.9.0" anyhow = "1.0.58" ashpd = { version = "0.3.2", optional = true } [dev-dependencies] itertools = "0.10.3" rand = "0.8.5" infer = "0.9.0" [dependencies.gtk] package = "gtk4" version = "0.4.8" features = ["v4_4"] [features] default = ["wallpaper"] wallpaper = ["ashpd"] # set image as wallpaper [package.metadata.deb] license-file = ["LICENSE", "0"] section = "graphics" depends = "$auto" maintainer-scripts = "debian" assets = [ ["target/release/image-roll", "usr/bin/", "755"], ["README.md", "usr/share/doc/image-roll/README", "644"], ["src/resources/com.github.weclaw1.ImageRoll.desktop", "usr/share/applications/", "644"], ["src/resources/com.github.weclaw1.ImageRoll.svg", "usr/share/icons/hicolor/scalable/apps/", "644"], ["src/resources/com.github.weclaw1.ImageRoll.Devel.svg", "usr/share/icons/hicolor/scalable/apps/", "644"], ["src/resources/com.github.weclaw1.ImageRoll-symbolic.svg", "usr/share/icons/hicolor/scalable/apps/", "644"], ["src/resources/com.github.weclaw1.ImageRoll.gschema.xml", "/usr/share/glib-2.0/schemas/", "644"], ] ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2021 Robert Węcławski Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Image Roll ![Image Roll](https://raw.githubusercontent.com/weclaw1/image-roll/main/src/resources/com.github.weclaw1.ImageRoll.svg) **Image Roll** is a simple and fast GTK image viewer with basic image manipulation tools. ## Features - Written in Rust - uses modern GTK 4 - adaptive - can be used on desktop and mobile devices - crop image - rotate image - resize image - undo and redo image edits ![Screenshot](https://raw.githubusercontent.com/weclaw1/image-roll/main/src/resources/screenshot.png) ## Installation ### Requirements If you use AUR or Flatpak you may skip this section. For this application you are required to have at least GTK 4.4. #### Ubuntu/Debian ``` sudo apt install libgtk-4-dev ``` #### Fedora/CentOS ``` sudo dnf install gtk4-devel glib2-devel ``` ### Flatpak Flatpak is the recommended install method. In order to install Image Roll using Flatpak run: ``` flatpak install flathub com.github.weclaw1.ImageRoll ``` ### Alpine Linux Alpine Linux provides [image-roll](https://pkgs.alpinelinux.org/packages?name=image-roll) package. ``` apk add image-roll ``` ### AUR If you run Arch Linux, you can use one of the AUR packages. There are 3, `image-roll`, `image-roll-bin`, and `image-roll-git`. Replace `yay` with your AUR helper of choice. ``` yay -S image-roll ``` ### Debian package On the releases page can be found deb packages which can be used on Debian and its derivatives. ### Precompiled binaries Ready-to-go executables can be found on the releases page. ### Cargo To install Image Roll using cargo run the following command: ``` cargo install image-roll ``` ================================================ FILE: build.rs ================================================ use std::{env, process::Command}; fn main() { Command::new("glib-compile-resources") .args(&["src/resources/resources.xml", "--sourcedir=src/resources"]) .status() .unwrap(); let python_installed = Command::new("sh") .args(&["-c", "command -v python3"]) .status() .unwrap() .success(); let pip_installed = Command::new("sh") .args(&["-c", "command -v pip3"]) .status() .unwrap() .success(); let wget_installed = Command::new("sh") .args(&["-c", "command -v wget"]) .status() .unwrap() .success(); if python_installed && pip_installed && wget_installed { Command::new("pip3") .args(&["install", "aiohttp", "toml"]) .status() .unwrap(); Command::new("wget").arg("https://raw.githubusercontent.com/flatpak/flatpak-builder-tools/master/cargo/flatpak-cargo-generator.py").status().unwrap(); Command::new("python3") .args(&[ "flatpak-cargo-generator.py", "Cargo.lock", "-o", "src/resources/cargo-sources.json", ]) .status() .unwrap(); Command::new("rm") .arg("flatpak-cargo-generator.py") .status() .unwrap(); } if Ok("debug".to_owned()) == env::var("PROFILE") { Command::new("sh") .args(&["-c", "mkdir -p $HOME/.local/share/glib-2.0/schemas"]) .status() .unwrap(); Command::new("sh").args(&["-c", "install -D src/resources/com.github.weclaw1.ImageRoll.gschema.xml $HOME/.local/share/glib-2.0/schemas/"]).status().unwrap(); Command::new("sh") .args(&[ "-c", "glib-compile-schemas $HOME/.local/share/glib-2.0/schemas/", ]) .status() .unwrap(); } println!("cargo:rerun-if-changed=src/resources/resources.xml"); println!("cargo:rerun-if-changed=src/resources/image-roll.ui"); println!("cargo:rerun-if-changed=src/resources/icons/crop-symbolic.svg"); println!("cargo:rerun-if-changed=src/resources/com.github.weclaw1.ImageRoll.svg"); println!("cargo:rerun-if-changed=src/resources/com.github.weclaw1.ImageRoll.gschema.xml"); println!("cargo:rerun-if-changed=Cargo.lock"); } ================================================ FILE: debian/postinst ================================================ #!/bin/sh set -e glib-compile-schemas /usr/share/glib-2.0/schemas exit 0 ================================================ FILE: src/app.rs ================================================ use gtk::{ gdk::Display, gio, glib::{self, timeout_future}, prelude::*, Builder, }; use std::{ cell::{Cell, RefCell}, rc::Rc, time::Duration, }; use crate::image_list::ImageList; use crate::settings::Settings; use crate::ui::{ event::{post_event, Event}, widgets::Widgets, }; use crate::{file_list::FileList, ui::controllers::Controllers}; use crate::{ image::CoordinatesPair, ui::{action, event}, }; pub struct App { application: gtk::Application, controllers: Controllers, widgets: Widgets, image_list: Rc>, file_list: FileList, selection_coords: Rc>>, settings: Settings, sender: glib::Sender, } impl App { pub fn create(application: >k::Application, file: Option<&gio::File>) { let bytes = glib::Bytes::from_static(include_bytes!("resources/resources.gresource")); let resources = gio::Resource::from_data(&bytes).expect("Couldn't load resources"); gio::resources_register(&resources); let builder = Builder::from_resource("/com/github/weclaw1/image-roll/image-roll.ui"); let widgets: Widgets = Widgets::init(builder, application); let controllers = Controllers::init(); gtk::IconTheme::for_display(&Display::default().unwrap()) .add_resource_path("/com/github/weclaw1/image-roll/icons/"); let image_list: Rc> = Rc::new(RefCell::new(ImageList::new())); let file_list: FileList = FileList::new(None).unwrap(); let selection_coords: Rc>> = Rc::new(Cell::new(None)); let settings: Settings = Settings::new(application.application_id().unwrap().as_str()); let (window_width, window_height) = settings.window_size(); widgets .window() .set_default_size(window_width as i32, window_height as i32); let (sender, receiver) = glib::MainContext::channel::(glib::PRIORITY_DEFAULT); if let Some(file) = file { let main_context = glib::MainContext::default(); let second_sender = sender.clone(); let file = file.clone(); main_context.spawn_local(async move { timeout_future(Duration::from_millis(10)).await; post_event(&second_sender, Event::OpenFile(file)); }); } let mut app = Self { application: application.clone(), controllers, widgets, image_list, file_list, selection_coords, settings, sender, }; event::connect_events( app.widgets.clone(), app.sender.clone(), app.image_list.clone(), app.selection_coords.clone(), app.settings.clone(), ); event::connect_controllers( app.sender.clone(), app.widgets.clone(), app.controllers.clone(), ); action::update_buttons_state( &app.widgets, &app.file_list, app.image_list.clone(), &app.settings, ); receiver.attach(None, move |e| { app.process_event(e); glib::Continue(true) }); } pub fn process_event(&mut self, event: Event) { match event { Event::OpenFile(file) => action::open_file( &self.sender, self.image_list.clone(), &mut self.file_list, file, ), Event::LoadImage(file_path) => action::load_image( &self.sender, &mut self.settings, &self.widgets, self.image_list.clone(), file_path, ), Event::DisplayMessage(message, message_type) => { action::display_message(&self.widgets, message.as_str(), message_type) } Event::ImageViewportResize(viewport_size) => { action::image_viewport_resize(&self.sender, &mut self.settings, viewport_size) } Event::RefreshPreview(preview_size) => { action::refresh_preview(&self.widgets, self.image_list.clone(), preview_size) } Event::ChangePreviewSize(preview_size) => action::change_preview_size( &self.sender, &self.widgets, &mut self.settings, preview_size, ), Event::ImageEdit(image_operation) => action::image_edit( &self.sender, &self.settings, self.image_list.clone(), &self.file_list, image_operation, ), Event::StartSelection(position) if self.widgets.crop_button().is_active() => { action::start_selection( &self.widgets, self.image_list.clone(), self.selection_coords.clone(), position, ) } Event::DragSelection(position) if self.widgets.crop_button().is_active() => { action::drag_selection( &self.widgets, self.image_list.clone(), self.selection_coords.clone(), position, ) } Event::SaveCurrentImage(filename) => { action::save_current_image(&self.sender, self.image_list.clone(), filename); if self.file_list.current_folder_monitor_mut().is_none() { action::refresh_file_list(&self.sender, &mut self.file_list); } } Event::DeleteCurrentImage => { action::delete_current_image( &self.sender, &mut self.file_list, self.image_list.clone(), ); if self.file_list.current_folder_monitor_mut().is_none() { action::refresh_file_list(&self.sender, &mut self.file_list); } } Event::EndSelection if self.widgets.crop_button().is_active() => action::end_selection( &self.sender, &self.widgets, self.image_list.clone(), self.selection_coords.clone(), ), Event::PreviewSmaller(value) => { action::preview_smaller(&self.sender, &self.settings, value) } Event::PreviewLarger(value) => { action::preview_larger(&self.sender, &self.settings, value) } Event::PreviewFitScreen => action::preview_fit_screen(&self.sender), Event::NextImage => { action::next_image(&self.sender, self.image_list.clone(), &mut self.file_list) } Event::PreviousImage => { action::previous_image(&self.sender, self.image_list.clone(), &mut self.file_list) } Event::RefreshFileList => action::refresh_file_list(&self.sender, &mut self.file_list), Event::ResizePopoverDisplayed => { action::resize_popover_displayed(&self.widgets, self.image_list.clone()) } Event::UpdateResizePopoverWidth => { action::update_resize_popover_width(&self.widgets, self.image_list.clone()) } Event::UpdateResizePopoverHeight => { action::update_resize_popover_height(&self.widgets, self.image_list.clone()) } Event::UndoOperation => { action::undo_operation(&self.sender, &self.settings, self.image_list.clone()) } Event::RedoOperation => { action::redo_operation(&self.sender, &self.settings, self.image_list.clone()) } Event::Print => action::print(&self.sender, &self.widgets, self.image_list.clone()), Event::HideInfoPanel => action::hide_info_panel(&self.widgets), Event::ToggleFullscreen => action::toggle_fullscreen(&self.widgets, &mut self.settings), Event::SetAsWallpaper => action::set_as_wallpaper(&self.sender, &self.file_list), Event::StartZoomGesture => action::start_zoom_gesture(&mut self.settings), Event::ZoomGestureScaleChanged(zoom_scale) => { action::change_scale_on_zoom_gesture(&self.sender, &self.settings, zoom_scale) } Event::CopyCurrentImage => action::copy_current_image(self.image_list.clone()), Event::Quit => action::quit(&self.application), event => debug!("Discarded unused event: {:?}", event), } action::update_buttons_state( &self.widgets, &self.file_list, self.image_list.clone(), &self.settings, ); } } ================================================ FILE: src/file_list.rs ================================================ use std::path::PathBuf; use anyhow::{anyhow, Result}; use gtk::{ gio::{self, Cancellable, FileMonitorFlags, FileQueryInfoFlags, FileType}, prelude::FileExt, }; pub struct FileList { file_list: Vec, current_file: Option<(usize, gio::File)>, current_folder: Option, current_folder_monitor: Option, } impl FileList { pub fn new(current_file: Option) -> Result { if let Some(current_file) = current_file { let current_folder = current_file.parent().ok_or_else(|| { anyhow!( "Couldn't get parent folder for file: {}", current_file.parse_name() ) })?; let mut file_list: Vec = FileList::enumerate_files(¤t_folder)?; file_list.sort_by_key(|file| { file.name() .file_name() .unwrap() .to_str() .unwrap() .to_owned() }); let current_file_index = file_list .iter() .position(|file| file.name() == current_file.basename().unwrap_or_default()) .ok_or_else(|| { anyhow!( "Couldn't find {} in enumerated files", current_file.parse_name() ) })?; let folder_monitor = current_folder .monitor_directory(FileMonitorFlags::NONE, >::None) .ok(); if folder_monitor.is_none() { warn!( "Couldn't get monitor for directory: {}", current_folder.path().unwrap().to_str().unwrap() ); } Ok(FileList { file_list, current_file: Some((current_file_index, current_file)), current_folder: Some(current_folder), current_folder_monitor: folder_monitor, }) } else { Ok(FileList { file_list: Vec::new(), current_file: None, current_folder: None, current_folder_monitor: None, }) } } pub fn refresh(&mut self) -> Result<()> { if let Some(current_folder) = &self.current_folder { if !current_folder.query_exists(>::None) { self.file_list = Vec::new(); self.current_file = None; self.current_folder = None; return Ok(()); } self.file_list = FileList::enumerate_files(current_folder)?; self.file_list.sort_by_key(|file| { file.name() .file_name() .unwrap() .to_str() .unwrap() .to_owned() }); match &self.current_file { Some((_, current_file)) => { let file_index = self.file_list.iter().position(|file| { file.name() == current_file.basename().unwrap_or_default() }); if let Some(file_index) = file_index { self.current_file = Some((file_index, self.current_file.take().unwrap().1)); } else { self.next(); } } None => self.next(), } } Ok(()) } pub fn next(&mut self) { if let Some(current_folder) = &self.current_folder { self.current_file = match self.current_file.take() { Some((_, _)) if self.file_list.is_empty() => None, Some((index, _)) if index + 1 < self.file_list.len() => Some(( index + 1, current_folder.child(self.file_list[index + 1].name()), )), Some((index, _)) if index + 1 >= self.file_list.len() => { Some((0, current_folder.child(self.file_list[0].name()))) } None if !self.file_list.is_empty() => { Some((0, current_folder.child(self.file_list[0].name()))) } _ => None, } } } pub fn previous(&mut self) { if let Some(current_folder) = &self.current_folder { self.current_file = match self.current_file.take() { Some((_, _)) if self.file_list.is_empty() => None, Some((index, _)) if index as i64 > 0 => Some(( index - 1, current_folder.child(self.file_list[index - 1].name()), )), Some((index, _)) if index as i64 - 1 < 0 => Some(( self.file_list.len() - 1, current_folder.child(self.file_list[self.file_list.len() - 1].name()), )), None if !self.file_list.is_empty() => { Some((0, current_folder.child(self.file_list[0].name()))) } _ => None, } } } // pub fn current_folder(&self) -> Option<&gio::File> { // self.current_folder.as_ref() // } pub fn current_file(&self) -> Option<&gio::File> { self.current_file.as_ref().map(|(_, file)| file) } #[allow(dead_code)] // currently used only with feature "wallpaper" pub fn current_file_uri(&self) -> Option { self.current_file .as_ref() .map(|(_, file)| file.uri().to_string()) } pub fn current_file_path(&self) -> Option { self.current_file.as_ref().and_then(|(_, file)| file.path()) } pub fn len(&self) -> usize { self.file_list.len() } fn enumerate_files(folder: &gio::File) -> Result> { Ok(folder .enumerate_children( "standard::*", FileQueryInfoFlags::NONE, >::None, )? .into_iter() .filter_map(|file| file.ok()) .filter(|file| file.file_type() == FileType::Regular) .filter(|file| { file.content_type() .filter(|content_type| content_type.to_string().starts_with("image")) .is_some() }) .collect()) } pub fn current_folder_monitor_mut(&mut self) -> Option<&mut gio::FileMonitor> { self.current_folder_monitor.as_mut() } pub fn delete_current_file(&mut self) -> Result { let deleted_file = self .current_file() .ok_or_else(|| { anyhow!("Cannot delete current file because file list does not have a current file") })? .to_owned(); let deleted_file_path = deleted_file .path() .ok_or_else(|| anyhow!("Deleted file does not have a valid path"))?; self.next(); deleted_file.trash(>::None)?; Ok(deleted_file_path) } } #[cfg(test)] mod tests { use itertools::Itertools; use rand::{distributions::Alphanumeric, Rng}; use crate::test_utils::TestResources; use super::*; const TEST_IMAGE: &[u8] = include_bytes!("resources/test/test_image.png"); #[test] fn file_list_contains_image_files() { let mut test_resources = TestResources::new("test/file_list_contains_image_files"); test_resources.add_file("test.png", TEST_IMAGE); test_resources.add_file("tes2.png", TEST_IMAGE); let file_list = FileList::new(Some(gio::File::for_path( test_resources.file_folder().join("test.png"), ))) .unwrap(); assert_eq!(2, file_list.len()); } #[test] fn file_list_does_not_contain_other_files() { let mut test_resources = TestResources::new("test/file_list_does_not_contain_other_files"); test_resources.add_file("test.png", TEST_IMAGE); test_resources.add_file("test2.png", TEST_IMAGE); test_resources.add_file("test.txt", "test"); let file_list = FileList::new(Some(gio::File::for_path( test_resources.file_folder().join("test.png"), ))) .unwrap(); assert_eq!(2, file_list.len()); } #[test] fn file_list_contains_images_without_extension() { let mut test_resources = TestResources::new("test/file_list_contains_images_without_extension"); test_resources.add_file("test", TEST_IMAGE); test_resources.add_file("test2", TEST_IMAGE); let file_list = FileList::new(Some(gio::File::for_path( test_resources.file_folder().join("test"), ))) .unwrap(); assert_eq!(2, file_list.len()); } #[test] fn file_list_does_not_contain_other_files_without_extension() { let mut test_resources = TestResources::new("test/file_list_does_not_contain_other_files_without_extension"); test_resources.add_file("test", TEST_IMAGE); test_resources.add_file("test2", TEST_IMAGE); test_resources.add_file("test", TEST_IMAGE); test_resources.add_file("testtxt", "test"); let file_list = FileList::new(Some(gio::File::for_path( test_resources.file_folder().join("test"), ))) .unwrap(); assert_eq!(2, file_list.len()); } #[test] fn file_list_is_in_alphabetical_order() { let mut test_resources = TestResources::new("test/file_list_is_in_alphabetical_order"); let mut random_file_names: Vec = rand::thread_rng() .sample_iter(Alphanumeric) .map(char::from) .chunks(10) .into_iter() .map(|chunk| chunk.collect::()) .take(100) .map(|name| format!("{}.{}", name, "png")) .collect(); random_file_names .iter() .for_each(|file_name| test_resources.add_file(file_name, TEST_IMAGE)); random_file_names.sort(); let mut file_list = FileList::new(Some(gio::File::for_path( test_resources .file_folder() .join(random_file_names.first().unwrap()), ))) .unwrap(); assert_eq!(100, file_list.len()); for file_name in random_file_names.iter() { assert_eq!( file_name, file_list .current_file() .unwrap() .basename() .unwrap() .to_str() .unwrap() ); file_list.next(); } } #[test] fn refresh_file_list_loads_new_images() { let mut test_resources = TestResources::new("test/refresh_file_list_loads_new_images"); test_resources.add_file("test.png", TEST_IMAGE); let mut file_list = FileList::new(Some(gio::File::for_path( test_resources.file_folder().join("test.png"), ))) .unwrap(); assert_eq!(1, file_list.len()); test_resources.add_file("test2.png", TEST_IMAGE); file_list.refresh().unwrap(); assert_eq!(2, file_list.len()); } #[test] fn refresh_file_list_removes_deleted_images() { let mut test_resources = TestResources::new("test/refresh_file_list_removes_deleted_images"); test_resources.add_file("test.png", TEST_IMAGE); test_resources.add_file("test2.png", TEST_IMAGE); let mut file_list = FileList::new(Some(gio::File::for_path( test_resources.file_folder().join("test.png"), ))) .unwrap(); assert_eq!(2, file_list.len()); test_resources.remove_file("test2.png"); file_list.refresh().unwrap(); assert_eq!(1, file_list.len()); } #[test] fn test_change_to_next_image() { let mut empty_file_list = FileList::new(None).unwrap(); assert!(empty_file_list.current_file().is_none()); empty_file_list.next(); assert!(empty_file_list.current_file().is_none()); let mut test_resources = TestResources::new("test/test_change_to_next_image"); test_resources.add_file("test1.png", TEST_IMAGE); test_resources.add_file("test2.png", TEST_IMAGE); test_resources.add_file("test3.png", TEST_IMAGE); let mut file_list = FileList::new(Some(gio::File::for_path( test_resources.file_folder().join("test2.png"), ))) .unwrap(); file_list.next(); assert_eq!( "test3.png", file_list .current_file() .unwrap() .basename() .unwrap() .to_str() .unwrap() ); file_list.next(); assert_eq!( "test1.png", file_list .current_file() .unwrap() .basename() .unwrap() .to_str() .unwrap() ); } #[test] fn test_change_to_previous_image() { let mut empty_file_list = FileList::new(None).unwrap(); assert!(empty_file_list.current_file().is_none()); empty_file_list.previous(); assert!(empty_file_list.current_file().is_none()); let mut test_resources = TestResources::new("test/test_change_to_previous_image"); test_resources.add_file("test1.png", TEST_IMAGE); test_resources.add_file("test2.png", TEST_IMAGE); test_resources.add_file("test3.png", TEST_IMAGE); let mut file_list = FileList::new(Some(gio::File::for_path( test_resources.file_folder().join("test2.png"), ))) .unwrap(); file_list.previous(); assert_eq!( "test1.png", file_list .current_file() .unwrap() .basename() .unwrap() .to_str() .unwrap() ); file_list.previous(); assert_eq!( "test3.png", file_list .current_file() .unwrap() .basename() .unwrap() .to_str() .unwrap() ); } #[test] fn delete_current_file_deletes_file_from_filesystem() { let mut test_resources = TestResources::new("test/delete_current_file_deletes_file_from_filesystem"); test_resources.add_file("test.png", TEST_IMAGE); let image_path = test_resources.file_folder().join("test.png"); let mut file_list = FileList::new(Some(gio::File::for_path(image_path.as_path()))).unwrap(); file_list.delete_current_file().unwrap(); assert!(std::fs::File::open(image_path).is_err()); } #[test] fn delete_current_file_returns_deleted_file_path() { let mut test_resources = TestResources::new("test/delete_current_file_returns_deleted_file_path"); test_resources.add_file("test.png", TEST_IMAGE); let image_path = test_resources.file_folder().join("test.png"); let mut file_list = FileList::new(Some(gio::File::for_path(image_path.as_path()))).unwrap(); let file_list_current_file_path = file_list.current_file_path().unwrap(); let deleted_file_path = file_list.delete_current_file().unwrap(); assert_eq!(file_list_current_file_path, deleted_file_path); } #[test] fn file_list_goes_to_next_file_after_removal_of_current_file() { let mut test_resources = TestResources::new("test/file_list_goes_to_next_file_after_removal_of_current_file"); test_resources.add_file("test.png", TEST_IMAGE); test_resources.add_file("test2.png", TEST_IMAGE); test_resources.add_file("test3.png", TEST_IMAGE); let image_path = test_resources.file_folder().join("test.png"); let mut file_list = FileList::new(Some(gio::File::for_path(image_path.as_path()))).unwrap(); let deleted_file_path = file_list.delete_current_file().unwrap(); assert_ne!(deleted_file_path, file_list.current_file_path().unwrap()); } } ================================================ FILE: src/image.rs ================================================ use std::path::Path; use anyhow::{anyhow, Result}; use gtk::gdk_pixbuf::{InterpType, Pixbuf}; use crate::image_operation::{ApplyImageOperation, ImageOperation}; pub type Coordinates = (u32, u32); pub type CoordinatesPair = (Coordinates, Coordinates); pub struct Image { original_image_buffer: Option, current_image_buffer: Option, preview_image_buffer: Option, operations: Vec, current_operation_index: Option, } impl Image { pub fn load>(path: P) -> Result { let image_buffer = Pixbuf::from_file(path)?; Ok(Image { original_image_buffer: Some(image_buffer.clone()), current_image_buffer: Some(image_buffer), preview_image_buffer: None, operations: Vec::new(), current_operation_index: None, }) } pub fn save>(&mut self, path: P, clear_operations: bool) -> Result<()> { let current_image_buffer = self .current_image_buffer .as_mut() .ok_or_else(|| anyhow!("Image buffer is missing!"))?; let extension = path .as_ref() .extension() .and_then(|extension| extension.to_str()) .ok_or_else(|| anyhow!("File path doesn't have file extension"))?; let lowercase_extension = extension.to_lowercase(); let file_type = match lowercase_extension.as_str() { file_type @ "jpeg" | file_type @ "png" | file_type @ "tiff" | file_type @ "ico" | file_type @ "bmp" => file_type, "jpg" => "jpeg", "tif" => "tiff", _ => "png", }; let options: &[(&str, &str)] = match file_type { "jpeg" => &[("quality", "100")], "png" => &[("compression", "9")], _ => &[], }; current_image_buffer.savev(path.as_ref(), file_type, options)?; if clear_operations { self.original_image_buffer = Some(current_image_buffer.clone()); self.current_operation_index = None; self.operations.clear(); } Ok(()) } pub fn reload>(self, path: P) -> Result { let original_image_buffer = Pixbuf::from_file(path)?; let mut current_image_buffer = original_image_buffer.clone(); if let Some(current_operation_index) = self.current_operation_index { current_image_buffer = self .operations .iter() .take(current_operation_index + 1) .fold(current_image_buffer, |image, operation| { image.apply_operation(operation).unwrap_or(image) }); } Ok(Image { original_image_buffer: Some(original_image_buffer), current_image_buffer: Some(current_image_buffer), preview_image_buffer: None, operations: self.operations, current_operation_index: self.current_operation_index, }) } pub fn remove_image_buffers(&mut self) { self.original_image_buffer = None; self.current_image_buffer = None; self.preview_image_buffer = None; } fn image_buffer_scale_to_fit(&self, canvas_width: u32, canvas_height: u32) -> Option { if let Some(image_buffer) = &self.current_image_buffer { let image_width = image_buffer.width() as f64; let image_height = image_buffer.height() as f64; let width_ratio = canvas_width as f64 / image_width; let height_ratio = canvas_height as f64 / image_height; let scale_ratio = width_ratio.min(height_ratio); image_buffer.scale_simple( (image_width * scale_ratio) as i32, (image_height * scale_ratio) as i32, InterpType::Nearest, ) } else { None } } fn image_buffer_resize(&self, scale: u32) -> Option { if let Some(image_buffer) = &self.current_image_buffer { image_buffer.scale_simple( (image_buffer.width() as f64 * (scale as f64 / 100.0)) as i32, (image_buffer.height() as f64 * (scale as f64 / 100.0)) as i32, InterpType::Bilinear, ) } else { None } } pub fn create_preview_image_buffer(&mut self, preview_size: PreviewSize) { self.preview_image_buffer = match preview_size { PreviewSize::BestFit(canvas_width, canvas_height) => { self.image_buffer_scale_to_fit(canvas_width, canvas_height) } PreviewSize::OriginalSize => self.current_image_buffer.clone(), PreviewSize::Resized(scale) => self.image_buffer_resize(scale), }; } pub fn create_print_image_buffer( &self, canvas_width: u32, canvas_height: u32, ) -> Option { if let Some((image_width, image_height)) = self.image_size() { if image_width > canvas_width || image_height > canvas_height { self.image_buffer_scale_to_fit(canvas_width, canvas_height) } else { self.current_image_buffer.clone() } } else { None } } pub fn preview_image_buffer(&self) -> Option<&Pixbuf> { self.preview_image_buffer.as_ref() } pub fn current_image_buffer(&self) -> Option<&Pixbuf> { self.current_image_buffer.as_ref() } pub fn image_size(&self) -> Option<(u32, u32)> { self.current_image_buffer .as_ref() .map(|image_buffer| (image_buffer.width() as u32, image_buffer.height() as u32)) } pub fn image_aspect_ratio(&self) -> Option { self.image_size() .map(|(image_width, image_height)| image_width as f64 / image_height as f64) } pub fn preview_image_buffer_size(&self) -> Option<(u32, u32)> { self.preview_image_buffer .as_ref() .map(|image_buffer| (image_buffer.width() as u32, image_buffer.height() as u32)) } pub fn preview_coords_to_image_coords( &self, coords: CoordinatesPair, ) -> Option { let ((start_coord_x, start_coord_y), (end_coord_x, end_coord_y)) = coords; if let Some((image_width, image_height)) = self.image_size() { if let Some((preview_width, preview_height)) = self.preview_image_buffer_size() { Some(( ( (start_coord_x as f64 * (image_width as f64 / preview_width as f64)) as u32, (start_coord_y as f64 * (image_height as f64 / preview_height as f64)) as u32, ), ( (end_coord_x as f64 * (image_width as f64 / preview_width as f64)) as u32, (end_coord_y as f64 * (image_height as f64 / preview_height as f64)) as u32, ), )) } else { None } } else { None } } pub fn has_operations(&self) -> bool { !self.operations.is_empty() && self.current_operation_index.is_some() } pub fn can_undo_operation(&self) -> bool { self.current_operation_index.is_some() } pub fn undo_operation(&mut self) { if self.can_undo_operation() { self.current_operation_index = self.current_operation_index.unwrap().checked_sub(1); self.current_image_buffer = Some( self.operations .iter() .take( self.current_operation_index .map_or(0, |operation_index| operation_index + 1), ) .fold( self.original_image_buffer.clone().unwrap(), |image, operation| image.apply_operation(operation).unwrap_or(image), ), ); } } pub fn can_redo_operation(&self) -> bool { match self.current_operation_index { Some(operation_index) => operation_index + 1 < self.operations.len(), None => !self.operations.is_empty(), } } pub fn redo_operation(&mut self) { if self.can_redo_operation() { self.current_operation_index = self .current_operation_index .map_or(Some(0), |current_operation_index| { Some(current_operation_index + 1) }); self.current_image_buffer = Some( self.operations .iter() .take(self.current_operation_index.unwrap() + 1) .fold( self.original_image_buffer.clone().unwrap(), |image, operation| image.apply_operation(operation).unwrap_or(image), ), ); } } } impl ApplyImageOperation for Image { type Result = Self; fn apply_operation(mut self, image_operation: &ImageOperation) -> Self::Result { if let Some(image_buffer) = self.current_image_buffer { let applied_operation_image_buffer = image_buffer.apply_operation(image_operation); if applied_operation_image_buffer.is_some() { if let Some(current_operation_index) = self.current_operation_index { self.operations.truncate(current_operation_index + 1); } self.operations.push(*image_operation); self.current_operation_index = Some(self.operations.len() - 1); } self.current_image_buffer = Some(applied_operation_image_buffer.unwrap_or(image_buffer)); } self } } #[derive(Clone, Copy, Debug)] pub enum PreviewSize { BestFit(u32, u32), OriginalSize, Resized(u32), } impl From for String { fn from(value: PreviewSize) -> Self { match value { PreviewSize::BestFit(_, _) => String::from("Fit screen"), PreviewSize::OriginalSize => String::from("100%"), PreviewSize::Resized(value) => format!("{}%", value), } } } impl PreviewSize { pub fn smaller(self) -> Option { match self { PreviewSize::BestFit(_, _) => Some(PreviewSize::OriginalSize), PreviewSize::OriginalSize => Some(PreviewSize::Resized(75)), PreviewSize::Resized(value) if value > 200 => Some(PreviewSize::Resized(200)), PreviewSize::Resized(value) if value > 150 => Some(PreviewSize::Resized(150)), PreviewSize::Resized(value) if value > 133 => Some(PreviewSize::Resized(133)), PreviewSize::Resized(value) if value > 100 => Some(PreviewSize::OriginalSize), PreviewSize::Resized(value) if value > 75 => Some(PreviewSize::Resized(75)), PreviewSize::Resized(value) if value > 66 => Some(PreviewSize::Resized(66)), PreviewSize::Resized(value) if value > 50 => Some(PreviewSize::Resized(50)), PreviewSize::Resized(value) if value > 33 => Some(PreviewSize::Resized(33)), PreviewSize::Resized(value) if value > 25 => Some(PreviewSize::Resized(25)), PreviewSize::Resized(value) if value > 10 => Some(PreviewSize::Resized(10)), PreviewSize::Resized(value) if value > 5 => Some(PreviewSize::Resized(5)), PreviewSize::Resized(_) => None, } } pub fn smaller_by(self, value: u32) -> Option { let old_value = match self { PreviewSize::BestFit(_, _) => return Some(PreviewSize::OriginalSize), PreviewSize::OriginalSize => 100, PreviewSize::Resized(value) => value, }; old_value .checked_sub(value) .filter(|value| value >= &5) .map(|value| { if value == 100 { PreviewSize::OriginalSize } else { PreviewSize::Resized(value) } }) } pub fn can_be_smaller(&self) -> bool { !matches!(self, PreviewSize::Resized(value) if value <= &5) } pub fn larger(self) -> Option { match self { PreviewSize::BestFit(_, _) => Some(PreviewSize::OriginalSize), PreviewSize::OriginalSize => Some(PreviewSize::Resized(133)), PreviewSize::Resized(value) if value < 10 => Some(PreviewSize::Resized(10)), PreviewSize::Resized(value) if value < 25 => Some(PreviewSize::Resized(25)), PreviewSize::Resized(value) if value < 33 => Some(PreviewSize::Resized(33)), PreviewSize::Resized(value) if value < 50 => Some(PreviewSize::Resized(50)), PreviewSize::Resized(value) if value < 66 => Some(PreviewSize::Resized(66)), PreviewSize::Resized(value) if value < 75 => Some(PreviewSize::Resized(75)), PreviewSize::Resized(value) if value < 100 => Some(PreviewSize::OriginalSize), PreviewSize::Resized(value) if value < 133 => Some(PreviewSize::Resized(133)), PreviewSize::Resized(value) if value < 150 => Some(PreviewSize::Resized(150)), PreviewSize::Resized(value) if value < 200 => Some(PreviewSize::Resized(200)), PreviewSize::Resized(value) if value < 500 => Some(PreviewSize::Resized(500)), PreviewSize::Resized(_) => None, } } pub fn larger_by(self, value: u32) -> Option { let old_value = match self { PreviewSize::BestFit(_, _) => return Some(PreviewSize::OriginalSize), PreviewSize::OriginalSize => 100, PreviewSize::Resized(value) => value, }; old_value .checked_add(value) .filter(|value| value <= &500) .map(|value| { if value == 100 { PreviewSize::OriginalSize } else { PreviewSize::Resized(value) } }) } pub fn can_be_larger(&self) -> bool { !matches!(self, PreviewSize::Resized(value) if value >= &500) } } #[cfg(test)] mod tests { use gtk::gdk_pixbuf::PixbufRotation; use crate::test_utils::TestResources; use super::*; const TEST_IMAGE: &[u8] = include_bytes!("resources/test/test_image.png"); #[test] fn test_load_image() { let mut test_resources = TestResources::new("test/test_load_image"); test_resources.add_file("test.png", TEST_IMAGE); let image = Image::load(test_resources.file_folder().join("test.png")).unwrap(); assert_eq!( Pixbuf::from_file(test_resources.file_folder().join("test.png")) .unwrap() .pixel_bytes(), image.original_image_buffer.unwrap().pixel_bytes() ); assert_eq!( Pixbuf::from_file(test_resources.file_folder().join("test.png")) .unwrap() .pixel_bytes(), image.current_image_buffer.unwrap().pixel_bytes() ); assert!(image.operations.is_empty()); } #[test] fn save_image() { let mut test_resources = TestResources::new("test/save_image"); test_resources.add_file("test.png", TEST_IMAGE); let mut image = Image::load(test_resources.file_folder().join("test.png")).unwrap(); let saved_file_path = test_resources.file_folder().join("test2.png"); image.save(&saved_file_path, false).unwrap(); assert!(std::fs::File::open(saved_file_path).is_ok()); } #[test] fn test_save_image_without_clear_operations() { let mut test_resources = TestResources::new("test/test_save_image_without_clear_operations"); test_resources.add_file("test.png", TEST_IMAGE); let mut image = Image::load(test_resources.file_folder().join("test.png")).unwrap(); image = image.apply_operation(&ImageOperation::Rotate(PixbufRotation::Clockwise)); assert!(image.has_operations()); image .save(test_resources.file_folder().join("test2.png"), false) .unwrap(); assert!(image.has_operations()); assert_ne!( image.original_image_buffer.unwrap().pixel_bytes(), image.current_image_buffer.unwrap().pixel_bytes() ) } #[test] fn test_save_image_with_clear_operations() { let mut test_resources = TestResources::new("test/test_save_image_with_clear_operations"); test_resources.add_file("test.png", TEST_IMAGE); let mut image = Image::load(test_resources.file_folder().join("test.png")).unwrap(); image = image.apply_operation(&ImageOperation::Rotate(PixbufRotation::Clockwise)); assert!(image.has_operations()); image .save(test_resources.file_folder().join("test2.png"), true) .unwrap(); assert!(!image.has_operations()); assert_eq!( image.original_image_buffer.unwrap().pixel_bytes(), image.current_image_buffer.unwrap().pixel_bytes() ) } #[test] fn save_image_uses_extensions_for_file_types_supported_by_pixbuf_save() { let mut test_resources = TestResources::new( "test/save_image_uses_extensions_for_file_types_supported_by_pixbuf_save", ); let file_extensions = vec!["png", "jpg", "tif", "ico", "bmp"]; for extension in file_extensions { let file_name = format!("{}.{}", "test", extension); test_resources.add_file(&file_name, TEST_IMAGE); let mut image = Image::load(test_resources.file_folder().join(file_name)).unwrap(); let saved_file_path = test_resources .file_folder() .join(format!("{}.{}", "test2", extension)); image.save(&saved_file_path, false).unwrap(); let saved_file_inferred_extension = infer::get_from_path(saved_file_path) .unwrap() .unwrap() .extension(); assert_eq!(saved_file_inferred_extension, extension); } } #[test] fn file_extensions_jpg_and_jpeg_are_supported() { let mut test_resources = TestResources::new("test/save_file_extensions_jpg_and_jpeg_are_supported"); test_resources.add_file("test.jpg", TEST_IMAGE); let mut image = Image::load(test_resources.file_folder().join("test.jpg")).unwrap(); let saved_file_path = test_resources.file_folder().join("test2.jpg"); image.save(&saved_file_path, false).unwrap(); let saved_file_inferred_extension = infer::get_from_path(saved_file_path) .unwrap() .unwrap() .extension(); assert_eq!(saved_file_inferred_extension, "jpg"); test_resources.add_file("test.jpeg", TEST_IMAGE); let mut image = Image::load(test_resources.file_folder().join("test.jpeg")).unwrap(); let saved_file_path = test_resources.file_folder().join("test2.jpeg"); image.save(&saved_file_path, false).unwrap(); let saved_file_inferred_extension = infer::get_from_path(saved_file_path) .unwrap() .unwrap() .extension(); assert_eq!(saved_file_inferred_extension, "jpg"); } #[test] fn test_image_reload() { let mut test_resources = TestResources::new("test/test_image_reload"); test_resources.add_file("test.png", TEST_IMAGE); let mut image = Image::load(test_resources.file_folder().join("test.png")).unwrap(); image = image.apply_operation(&ImageOperation::Rotate(PixbufRotation::Clockwise)); let original_image_buffer = image.original_image_buffer.clone(); let current_image_buffer = image.current_image_buffer.clone(); image.remove_image_buffers(); assert!(image.original_image_buffer.is_none() && image.current_image_buffer.is_none()); image = image .reload(test_resources.file_folder().join("test.png")) .unwrap(); assert_eq!( original_image_buffer.unwrap().pixel_bytes(), image.original_image_buffer.unwrap().pixel_bytes() ); assert_eq!( current_image_buffer.unwrap().pixel_bytes(), image.current_image_buffer.unwrap().pixel_bytes() ); } #[test] fn create_preview_original_size() { let mut test_resources = TestResources::new("test/create_preview_original_size"); test_resources.add_file("test.png", TEST_IMAGE); let mut image = Image::load(test_resources.file_folder().join("test.png")).unwrap(); image.create_preview_image_buffer(PreviewSize::OriginalSize); assert_eq!( image.current_image_buffer.unwrap().pixel_bytes(), image.preview_image_buffer.unwrap().pixel_bytes() ); } #[test] fn create_preview_scale_to_fit() { let mut test_resources = TestResources::new("test/create_preview_scale_to_fit"); test_resources.add_file("test.png", TEST_IMAGE); let mut image = Image::load(test_resources.file_folder().join("test.png")).unwrap(); image = image.apply_operation(&ImageOperation::Resize((1000, 500))); image.create_preview_image_buffer(PreviewSize::BestFit(500, 500)); assert_eq!((500, 250), image.preview_image_buffer_size().unwrap()); } #[test] fn create_preview_resized() { let mut test_resources = TestResources::new("test/create_preview_resized"); test_resources.add_file("test.png", TEST_IMAGE); let mut image = Image::load(test_resources.file_folder().join("test.png")).unwrap(); image = image.apply_operation(&ImageOperation::Resize((100, 100))); image.create_preview_image_buffer(PreviewSize::Resized(90)); assert_eq!((90, 90), image.preview_image_buffer_size().unwrap()); } #[test] fn preview_coords_to_image_coords() { let mut test_resources = TestResources::new("test/preview_coords_to_image_coords"); test_resources.add_file("test.png", TEST_IMAGE); let mut image = Image::load(test_resources.file_folder().join("test.png")).unwrap(); image = image.apply_operation(&ImageOperation::Resize((100, 100))); image.create_preview_image_buffer(PreviewSize::Resized(200)); assert_eq!( ((10, 10), (20, 20)), image .preview_coords_to_image_coords(((20, 20), (40, 40))) .unwrap() ); } #[test] fn undo_operation() { let mut test_resources = TestResources::new("test/undo_operation"); test_resources.add_file("test.png", TEST_IMAGE); let mut image = Image::load(test_resources.file_folder().join("test.png")).unwrap(); image = image.apply_operation(&ImageOperation::Resize((100, 100))); image = image.apply_operation(&ImageOperation::Rotate(PixbufRotation::Clockwise)); assert!(image.can_undo_operation()); image.undo_operation(); assert!( image.can_redo_operation() && image.current_operation_index == Some(0) && image.operations.len() == 2 ); image.undo_operation(); assert!( image.can_redo_operation() && image.current_operation_index == None && image.operations.len() == 2 ); } #[test] fn redo_operation() { let mut test_resources = TestResources::new("test/redo_operation"); test_resources.add_file("test.png", TEST_IMAGE); let mut image = Image::load(test_resources.file_folder().join("test.png")).unwrap(); image = image.apply_operation(&ImageOperation::Resize((100, 100))); image = image.apply_operation(&ImageOperation::Rotate(PixbufRotation::Clockwise)); assert!(!image.can_redo_operation()); image.undo_operation(); assert!( image.can_redo_operation() && image.current_operation_index == Some(0) && image.operations.len() == 2 ); image.redo_operation(); assert!( !image.can_redo_operation() && image.current_operation_index == Some(1) && image.operations.len() == 2 ); } #[test] fn apply_operation() { let mut test_resources = TestResources::new("test/apply_operation"); test_resources.add_file("test.png", TEST_IMAGE); let mut image = Image::load(test_resources.file_folder().join("test.png")).unwrap(); assert!(image.operations.is_empty() && image.current_operation_index.is_none()); image = image.apply_operation(&ImageOperation::Resize((100, 100))); assert!(image.operations.len() == 1 && image.current_operation_index == Some(0)); assert!( image.original_image_buffer.unwrap().pixel_bytes() != image.current_image_buffer.unwrap().pixel_bytes() ); } } ================================================ FILE: src/image_list.rs ================================================ use std::{ collections::HashMap, ops::{Index, IndexMut}, path::{Path, PathBuf}, }; use crate::image::Image; use anyhow::{anyhow, Result}; use gtk::gdk::Texture; pub struct ImageList { images: HashMap, current_image_path: Option, } impl ImageList { pub fn new() -> Self { Self { images: HashMap::new(), current_image_path: None, } } pub fn remove(&mut self, key: &Path) -> Option { self.images.remove(key) } pub fn insert(&mut self, key: PathBuf, value: Image) { self.images.insert(key, value); } pub fn set_current_image_path(&mut self, current_image_path: Option) { self.current_image_path = current_image_path; } // pub fn current_image(&self) -> Option<&Image> { // self.current_image_path.as_ref().map(|image_path| self.images.get(image_path)).flatten() // } pub fn remove_current_image(&mut self) -> Option { self.current_image_path .clone() .and_then(|image_path| self.remove(&image_path)) } pub fn current_image_mut(&mut self) -> Option<&mut Image> { self.current_image_path .clone() .and_then(move |image_path| self.images.get_mut(&image_path)) } pub fn current_image(&self) -> Option<&Image> { self.current_image_path .as_ref() .and_then(|image_path| self.images.get(image_path)) } pub fn current_image_path(&self) -> Option { self.current_image_path.clone() } pub fn save_current_image(&mut self, filename: Option) -> Result<()> { let (filename, clear_operations) = if let Some(filename) = filename { (filename, false) } else { ( self.current_image_path .clone() .ok_or_else(|| anyhow!("Current image path is not set"))?, true, ) }; let current_image = self .current_image_mut() .ok_or_else(|| anyhow!("Couldn't load current image"))?; current_image.save(filename, clear_operations)?; Ok(()) } pub fn copy_current_image(&self, clipboard: gtk::gdk::Clipboard) { if let Some(current_image) = self.current_image() { if let Some(buffer) = current_image.current_image_buffer() { clipboard.set_texture(&Texture::for_pixbuf(buffer)); } } } } impl Index<&PathBuf> for ImageList { type Output = Image; fn index(&self, index: &PathBuf) -> &Self::Output { &self.images[index] } } impl IndexMut<&PathBuf> for ImageList { fn index_mut(&mut self, index: &PathBuf) -> &mut Self::Output { self.images.get_mut(index).unwrap() } } #[cfg(test)] mod tests { use crate::{ image_operation::{ApplyImageOperation, ImageOperation}, test_utils::TestResources, }; use super::*; const TEST_IMAGE: &[u8] = include_bytes!("resources/test/test_image.png"); #[test] fn save_current_image_overwrites_image_at_current_image_path_when_filename_is_set_to_none() { let mut test_resources = TestResources::new("test/save_current_image_overwrites_image_at_current_image_path_when_filename_is_set_to_none"); test_resources.add_file("test.png", TEST_IMAGE); let image_path = test_resources.file_folder().join("test.png"); let creation_date = std::fs::File::open(&image_path) .unwrap() .metadata() .unwrap() .modified() .unwrap(); let image = Image::load(&image_path).unwrap(); let mut image_list = ImageList::new(); image_list.insert(image_path.clone(), image); image_list.set_current_image_path(Some(image_path.clone())); image_list.save_current_image(None).unwrap(); let modification_date = std::fs::File::open(&image_path) .unwrap() .metadata() .unwrap() .modified() .unwrap(); assert!(modification_date > creation_date); } #[test] fn save_current_image_creates_a_new_image_when_filename_is_set() { let mut test_resources = TestResources::new("test/save_current_image_creates_a_new_image_when_filename_is_set"); test_resources.add_file("test.png", TEST_IMAGE); let image_path = test_resources.file_folder().join("test.png"); let image = Image::load(&image_path).unwrap(); let mut image_list = ImageList::new(); image_list.insert(image_path.clone(), image); image_list.set_current_image_path(Some(image_path.clone())); let new_image_path = test_resources.file_folder().join("test2.png"); image_list .save_current_image(Some(new_image_path.clone())) .unwrap(); assert!(std::fs::File::open(new_image_path).is_ok()); } #[test] fn save_current_image_clears_image_operations_when_filename_is_set_to_none() { let mut test_resources = TestResources::new( "test/save_current_image_clears_image_operations_when_filename_is_set_to_none", ); test_resources.add_file("test.png", TEST_IMAGE); let image_path = test_resources.file_folder().join("test.png"); let mut image = Image::load(&image_path).unwrap(); image = image.apply_operation(&ImageOperation::Resize((10, 10))); let mut image_list = ImageList::new(); image_list.insert(image_path.clone(), image); image_list.set_current_image_path(Some(image_path.clone())); assert!(image_list.current_image().unwrap().has_operations()); image_list.save_current_image(None).unwrap(); assert!(!image_list.current_image().unwrap().has_operations()); } #[test] fn save_current_image_does_not_clear_image_operations_when_filename_is_set() { let mut test_resources = TestResources::new( "test/save_current_image_does_not_clear_image_operations_when_filename_is_set", ); test_resources.add_file("test.png", TEST_IMAGE); let image_path = test_resources.file_folder().join("test.png"); let mut image = Image::load(&image_path).unwrap(); image = image.apply_operation(&ImageOperation::Resize((10, 10))); let mut image_list = ImageList::new(); image_list.insert(image_path.clone(), image); image_list.set_current_image_path(Some(image_path.clone())); assert!(image_list.current_image().unwrap().has_operations()); image_list .save_current_image(Some(test_resources.file_folder().join("test2.png"))) .unwrap(); assert!(image_list.current_image().unwrap().has_operations()); } } ================================================ FILE: src/image_operation.rs ================================================ use std::cmp; use gtk::gdk_pixbuf::{InterpType, Pixbuf, PixbufRotation}; use crate::image::CoordinatesPair; #[derive(Copy, Clone, Debug)] pub enum ImageOperation { Rotate(PixbufRotation), Crop(CoordinatesPair), Resize((u32, u32)), } pub trait ApplyImageOperation { type Result; fn apply_operation(self, image_operation: &ImageOperation) -> Self::Result; } impl ApplyImageOperation for &Pixbuf { type Result = Option; fn apply_operation(self, image_operation: &ImageOperation) -> Self::Result { match image_operation { ImageOperation::Rotate(rotation) => self.rotate_simple(*rotation), ImageOperation::Crop(( (start_position_x, start_position_y), (end_position_x, end_position_y), )) => { let x = *cmp::min(start_position_x, end_position_x); let y = *cmp::min(start_position_y, end_position_y); let width = *cmp::max(start_position_x, end_position_x) - x; let height = *cmp::max(start_position_y, end_position_y) - y; self.new_subpixbuf(x as i32, y as i32, width as i32, height as i32) } ImageOperation::Resize((width, height)) => { self.scale_simple(*width as i32, *height as i32, InterpType::Bilinear) } } } } #[cfg(test)] mod tests { use crate::test_utils::TestResources; use super::*; const TEST_IMAGE: &[u8] = include_bytes!("resources/test/test_image.png"); #[test] fn test_apply_rotate_image_operation_on_pixbuf() { let mut test_resources = TestResources::new("test/test_apply_rotate_image_operation_on_pixbuf"); test_resources.add_file("test.png", TEST_IMAGE); let pixbuf = Pixbuf::from_file(test_resources.file_folder().join("test.png")).unwrap(); let image_operation = ImageOperation::Rotate(PixbufRotation::Clockwise); assert_eq!( pixbuf .rotate_simple(PixbufRotation::Clockwise) .unwrap() .pixel_bytes(), pixbuf .apply_operation(&image_operation) .unwrap() .pixel_bytes() ); } #[test] fn test_apply_crop_image_operation_on_pixbuf() { let mut test_resources = TestResources::new("test/test_apply_crop_image_operation_on_pixbuf"); test_resources.add_file("test.png", TEST_IMAGE); let pixbuf = Pixbuf::from_file(test_resources.file_folder().join("test.png")).unwrap(); let image_operation = ImageOperation::Crop(((10, 10), (20, 20))); assert_eq!( pixbuf.new_subpixbuf(10, 10, 10, 10).unwrap().pixel_bytes(), pixbuf .apply_operation(&image_operation) .unwrap() .pixel_bytes() ); } #[test] fn test_apply_resize_image_operation_on_pixbuf() { let mut test_resources = TestResources::new("test/test_apply_resize_image_operation_on_pixbuf"); test_resources.add_file("test.png", TEST_IMAGE); let pixbuf = Pixbuf::from_file(test_resources.file_folder().join("test.png")).unwrap(); let image_operation = ImageOperation::Resize((10, 10)); assert_eq!( pixbuf .scale_simple(10, 10, InterpType::Bilinear) .unwrap() .pixel_bytes(), pixbuf .apply_operation(&image_operation) .unwrap() .pixel_bytes() ); } } ================================================ FILE: src/main.rs ================================================ use app::App; use gtk::{gio::ApplicationFlags, prelude::*, Application}; #[macro_use] extern crate log; mod app; mod file_list; mod image; mod image_list; mod image_operation; mod settings; mod ui; #[cfg(test)] mod test_utils; fn main() { env_logger::init(); let application = Application::new( Some("com.github.weclaw1.ImageRoll"), ApplicationFlags::HANDLES_OPEN | ApplicationFlags::NON_UNIQUE, ); application.connect_activate(|app| { App::create(app, None); }); application.connect_open(move |app, files, _| { App::create(app, Some(&files[0])); }); application.run(); } ================================================ FILE: src/resources/cargo-sources.json ================================================ [ { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/aho-corasick/aho-corasick-0.7.18.crate", "sha256": "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f", "dest": "cargo/vendor/aho-corasick-0.7.18" }, { "type": "inline", "contents": "{\"package\": \"1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f\", \"files\": {}}", "dest": "cargo/vendor/aho-corasick-0.7.18", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/anyhow/anyhow-1.0.58.crate", "sha256": "bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704", "dest": "cargo/vendor/anyhow-1.0.58" }, { "type": "inline", "contents": "{\"package\": \"bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704\", \"files\": {}}", "dest": "cargo/vendor/anyhow-1.0.58", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/ashpd/ashpd-0.3.2.crate", "sha256": "6dcc8ed0b5211687437636d8c95f6a608f4281d142101b3b5d314b38bfadd40f", "dest": "cargo/vendor/ashpd-0.3.2" }, { "type": "inline", "contents": "{\"package\": \"6dcc8ed0b5211687437636d8c95f6a608f4281d142101b3b5d314b38bfadd40f\", \"files\": {}}", "dest": "cargo/vendor/ashpd-0.3.2", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/async-broadcast/async-broadcast-0.3.4.crate", "sha256": "90622698a1218e0b2fb846c97b5f19a0831f6baddee73d9454156365ccfa473b", "dest": "cargo/vendor/async-broadcast-0.3.4" }, { "type": "inline", "contents": "{\"package\": \"90622698a1218e0b2fb846c97b5f19a0831f6baddee73d9454156365ccfa473b\", \"files\": {}}", "dest": "cargo/vendor/async-broadcast-0.3.4", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/async-channel/async-channel-1.6.1.crate", "sha256": "2114d64672151c0c5eaa5e131ec84a74f06e1e559830dabba01ca30605d66319", "dest": "cargo/vendor/async-channel-1.6.1" }, { "type": "inline", "contents": "{\"package\": \"2114d64672151c0c5eaa5e131ec84a74f06e1e559830dabba01ca30605d66319\", \"files\": {}}", "dest": "cargo/vendor/async-channel-1.6.1", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/async-executor/async-executor-1.4.1.crate", "sha256": "871f9bb5e0a22eeb7e8cf16641feb87c9dc67032ccf8ff49e772eb9941d3a965", "dest": "cargo/vendor/async-executor-1.4.1" }, { "type": "inline", "contents": "{\"package\": \"871f9bb5e0a22eeb7e8cf16641feb87c9dc67032ccf8ff49e772eb9941d3a965\", \"files\": {}}", "dest": "cargo/vendor/async-executor-1.4.1", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/async-io/async-io-1.6.0.crate", "sha256": "a811e6a479f2439f0c04038796b5cfb3d2ad56c230e0f2d3f7b04d68cfee607b", "dest": "cargo/vendor/async-io-1.6.0" }, { "type": "inline", "contents": "{\"package\": \"a811e6a479f2439f0c04038796b5cfb3d2ad56c230e0f2d3f7b04d68cfee607b\", \"files\": {}}", "dest": "cargo/vendor/async-io-1.6.0", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/async-lock/async-lock-2.4.0.crate", "sha256": "e6a8ea61bf9947a1007c5cada31e647dbc77b103c679858150003ba697ea798b", "dest": "cargo/vendor/async-lock-2.4.0" }, { "type": "inline", "contents": "{\"package\": \"e6a8ea61bf9947a1007c5cada31e647dbc77b103c679858150003ba697ea798b\", \"files\": {}}", "dest": "cargo/vendor/async-lock-2.4.0", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/async-recursion/async-recursion-0.3.2.crate", "sha256": "d7d78656ba01f1b93024b7c3a0467f1608e4be67d725749fdcd7d2c7678fd7a2", "dest": "cargo/vendor/async-recursion-0.3.2" }, { "type": "inline", "contents": "{\"package\": \"d7d78656ba01f1b93024b7c3a0467f1608e4be67d725749fdcd7d2c7678fd7a2\", \"files\": {}}", "dest": "cargo/vendor/async-recursion-0.3.2", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/async-task/async-task-4.1.0.crate", "sha256": "677d306121baf53310a3fd342d88dc0824f6bbeace68347593658525565abee8", "dest": "cargo/vendor/async-task-4.1.0" }, { "type": "inline", "contents": "{\"package\": \"677d306121baf53310a3fd342d88dc0824f6bbeace68347593658525565abee8\", \"files\": {}}", "dest": "cargo/vendor/async-task-4.1.0", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/async-trait/async-trait-0.1.52.crate", "sha256": "061a7acccaa286c011ddc30970520b98fa40e00c9d644633fb26b5fc63a265e3", "dest": "cargo/vendor/async-trait-0.1.52" }, { "type": "inline", "contents": "{\"package\": \"061a7acccaa286c011ddc30970520b98fa40e00c9d644633fb26b5fc63a265e3\", \"files\": {}}", "dest": "cargo/vendor/async-trait-0.1.52", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/atty/atty-0.2.14.crate", "sha256": "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8", "dest": "cargo/vendor/atty-0.2.14" }, { "type": "inline", "contents": "{\"package\": \"d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8\", \"files\": {}}", "dest": "cargo/vendor/atty-0.2.14", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/autocfg/autocfg-1.0.1.crate", "sha256": "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a", "dest": "cargo/vendor/autocfg-1.0.1" }, { "type": "inline", "contents": "{\"package\": \"cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a\", \"files\": {}}", "dest": "cargo/vendor/autocfg-1.0.1", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/bitflags/bitflags-1.3.2.crate", "sha256": "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a", "dest": "cargo/vendor/bitflags-1.3.2" }, { "type": "inline", "contents": "{\"package\": \"bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a\", \"files\": {}}", "dest": "cargo/vendor/bitflags-1.3.2", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/byteorder/byteorder-1.4.3.crate", "sha256": "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610", "dest": "cargo/vendor/byteorder-1.4.3" }, { "type": "inline", "contents": "{\"package\": \"14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610\", \"files\": {}}", "dest": "cargo/vendor/byteorder-1.4.3", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/cache-padded/cache-padded-1.2.0.crate", "sha256": "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c", "dest": "cargo/vendor/cache-padded-1.2.0" }, { "type": "inline", "contents": "{\"package\": \"c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c\", \"files\": {}}", "dest": "cargo/vendor/cache-padded-1.2.0", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/cairo-rs/cairo-rs-0.15.1.crate", "sha256": "b869e97a87170f96762f9f178eae8c461147e722ba21dd8814105bf5716bf14a", "dest": "cargo/vendor/cairo-rs-0.15.1" }, { "type": "inline", "contents": "{\"package\": \"b869e97a87170f96762f9f178eae8c461147e722ba21dd8814105bf5716bf14a\", \"files\": {}}", "dest": "cargo/vendor/cairo-rs-0.15.1", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/cairo-sys-rs/cairo-sys-rs-0.15.1.crate", "sha256": "3c55d429bef56ac9172d25fecb85dc8068307d17acd74b377866b7a1ef25d3c8", "dest": "cargo/vendor/cairo-sys-rs-0.15.1" }, { "type": "inline", "contents": "{\"package\": \"3c55d429bef56ac9172d25fecb85dc8068307d17acd74b377866b7a1ef25d3c8\", \"files\": {}}", "dest": "cargo/vendor/cairo-sys-rs-0.15.1", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/cc/cc-1.0.73.crate", "sha256": "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11", "dest": "cargo/vendor/cc-1.0.73" }, { "type": "inline", "contents": "{\"package\": \"2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11\", \"files\": {}}", "dest": "cargo/vendor/cc-1.0.73", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/cfb/cfb-0.7.3.crate", "sha256": "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f", "dest": "cargo/vendor/cfb-0.7.3" }, { "type": "inline", "contents": "{\"package\": \"d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f\", \"files\": {}}", "dest": "cargo/vendor/cfb-0.7.3", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/cfg-expr/cfg-expr-0.10.1.crate", "sha256": "295b6eb918a60a25fec0b23a5e633e74fddbaf7bb04411e65a10c366aca4b5cd", "dest": "cargo/vendor/cfg-expr-0.10.1" }, { "type": "inline", "contents": "{\"package\": \"295b6eb918a60a25fec0b23a5e633e74fddbaf7bb04411e65a10c366aca4b5cd\", \"files\": {}}", "dest": "cargo/vendor/cfg-expr-0.10.1", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/cfg-if/cfg-if-1.0.0.crate", "sha256": "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd", "dest": "cargo/vendor/cfg-if-1.0.0" }, { "type": "inline", "contents": "{\"package\": \"baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd\", \"files\": {}}", "dest": "cargo/vendor/cfg-if-1.0.0", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/concurrent-queue/concurrent-queue-1.2.2.crate", "sha256": "30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3", "dest": "cargo/vendor/concurrent-queue-1.2.2" }, { "type": "inline", "contents": "{\"package\": \"30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3\", \"files\": {}}", "dest": "cargo/vendor/concurrent-queue-1.2.2", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/derivative/derivative-2.2.0.crate", "sha256": "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b", "dest": "cargo/vendor/derivative-2.2.0" }, { "type": "inline", "contents": "{\"package\": \"fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b\", \"files\": {}}", "dest": "cargo/vendor/derivative-2.2.0", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/easy-parallel/easy-parallel-3.2.0.crate", "sha256": "6907e25393cdcc1f4f3f513d9aac1e840eb1cc341a0fccb01171f7d14d10b946", "dest": "cargo/vendor/easy-parallel-3.2.0" }, { "type": "inline", "contents": "{\"package\": \"6907e25393cdcc1f4f3f513d9aac1e840eb1cc341a0fccb01171f7d14d10b946\", \"files\": {}}", "dest": "cargo/vendor/easy-parallel-3.2.0", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/either/either-1.6.1.crate", "sha256": "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457", "dest": "cargo/vendor/either-1.6.1" }, { "type": "inline", "contents": "{\"package\": \"e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457\", \"files\": {}}", "dest": "cargo/vendor/either-1.6.1", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/enumflags2/enumflags2-0.7.3.crate", "sha256": "a25c90b056b3f84111cf183cbeddef0d3a0bbe9a674f057e1a1533c315f24def", "dest": "cargo/vendor/enumflags2-0.7.3" }, { "type": "inline", "contents": "{\"package\": \"a25c90b056b3f84111cf183cbeddef0d3a0bbe9a674f057e1a1533c315f24def\", \"files\": {}}", "dest": "cargo/vendor/enumflags2-0.7.3", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/enumflags2_derive/enumflags2_derive-0.7.3.crate", "sha256": "144ec79496cbab6f84fa125dc67be9264aef22eb8a28da8454d9c33f15108da4", "dest": "cargo/vendor/enumflags2_derive-0.7.3" }, { "type": "inline", "contents": "{\"package\": \"144ec79496cbab6f84fa125dc67be9264aef22eb8a28da8454d9c33f15108da4\", \"files\": {}}", "dest": "cargo/vendor/enumflags2_derive-0.7.3", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/env_logger/env_logger-0.9.0.crate", "sha256": "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3", "dest": "cargo/vendor/env_logger-0.9.0" }, { "type": "inline", "contents": "{\"package\": \"0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3\", \"files\": {}}", "dest": "cargo/vendor/env_logger-0.9.0", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/event-listener/event-listener-2.5.2.crate", "sha256": "77f3309417938f28bf8228fcff79a4a37103981e3e186d2ccd19c74b38f4eb71", "dest": "cargo/vendor/event-listener-2.5.2" }, { "type": "inline", "contents": "{\"package\": \"77f3309417938f28bf8228fcff79a4a37103981e3e186d2ccd19c74b38f4eb71\", \"files\": {}}", "dest": "cargo/vendor/event-listener-2.5.2", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/fastrand/fastrand-1.7.0.crate", "sha256": "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf", "dest": "cargo/vendor/fastrand-1.7.0" }, { "type": "inline", "contents": "{\"package\": \"c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf\", \"files\": {}}", "dest": "cargo/vendor/fastrand-1.7.0", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/field-offset/field-offset-0.3.4.crate", "sha256": "1e1c54951450cbd39f3dbcf1005ac413b49487dabf18a720ad2383eccfeffb92", "dest": "cargo/vendor/field-offset-0.3.4" }, { "type": "inline", "contents": "{\"package\": \"1e1c54951450cbd39f3dbcf1005ac413b49487dabf18a720ad2383eccfeffb92\", \"files\": {}}", "dest": "cargo/vendor/field-offset-0.3.4", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/fnv/fnv-1.0.7.crate", "sha256": "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1", "dest": "cargo/vendor/fnv-1.0.7" }, { "type": "inline", "contents": "{\"package\": \"3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1\", \"files\": {}}", "dest": "cargo/vendor/fnv-1.0.7", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/futures/futures-0.3.16.crate", "sha256": "1adc00f486adfc9ce99f77d717836f0c5aa84965eb0b4f051f4e83f7cab53f8b", "dest": "cargo/vendor/futures-0.3.16" }, { "type": "inline", "contents": "{\"package\": \"1adc00f486adfc9ce99f77d717836f0c5aa84965eb0b4f051f4e83f7cab53f8b\", \"files\": {}}", "dest": "cargo/vendor/futures-0.3.16", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/futures-channel/futures-channel-0.3.16.crate", "sha256": "74ed2411805f6e4e3d9bc904c95d5d423b89b3b25dc0250aa74729de20629ff9", "dest": "cargo/vendor/futures-channel-0.3.16" }, { "type": "inline", "contents": "{\"package\": \"74ed2411805f6e4e3d9bc904c95d5d423b89b3b25dc0250aa74729de20629ff9\", \"files\": {}}", "dest": "cargo/vendor/futures-channel-0.3.16", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/futures-core/futures-core-0.3.16.crate", "sha256": "af51b1b4a7fdff033703db39de8802c673eb91855f2e0d47dcf3bf2c0ef01f99", "dest": "cargo/vendor/futures-core-0.3.16" }, { "type": "inline", "contents": "{\"package\": \"af51b1b4a7fdff033703db39de8802c673eb91855f2e0d47dcf3bf2c0ef01f99\", \"files\": {}}", "dest": "cargo/vendor/futures-core-0.3.16", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/futures-executor/futures-executor-0.3.16.crate", "sha256": "4d0d535a57b87e1ae31437b892713aee90cd2d7b0ee48727cd11fc72ef54761c", "dest": "cargo/vendor/futures-executor-0.3.16" }, { "type": "inline", "contents": "{\"package\": \"4d0d535a57b87e1ae31437b892713aee90cd2d7b0ee48727cd11fc72ef54761c\", \"files\": {}}", "dest": "cargo/vendor/futures-executor-0.3.16", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/futures-io/futures-io-0.3.16.crate", "sha256": "0b0e06c393068f3a6ef246c75cdca793d6a46347e75286933e5e75fd2fd11582", "dest": "cargo/vendor/futures-io-0.3.16" }, { "type": "inline", "contents": "{\"package\": \"0b0e06c393068f3a6ef246c75cdca793d6a46347e75286933e5e75fd2fd11582\", \"files\": {}}", "dest": "cargo/vendor/futures-io-0.3.16", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/futures-lite/futures-lite-1.12.0.crate", "sha256": "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48", "dest": "cargo/vendor/futures-lite-1.12.0" }, { "type": "inline", "contents": "{\"package\": \"7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48\", \"files\": {}}", "dest": "cargo/vendor/futures-lite-1.12.0", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/futures-macro/futures-macro-0.3.16.crate", "sha256": "c54913bae956fb8df7f4dc6fc90362aa72e69148e3f39041fbe8742d21e0ac57", "dest": "cargo/vendor/futures-macro-0.3.16" }, { "type": "inline", "contents": "{\"package\": \"c54913bae956fb8df7f4dc6fc90362aa72e69148e3f39041fbe8742d21e0ac57\", \"files\": {}}", "dest": "cargo/vendor/futures-macro-0.3.16", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/futures-sink/futures-sink-0.3.21.crate", "sha256": "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868", "dest": "cargo/vendor/futures-sink-0.3.21" }, { "type": "inline", "contents": "{\"package\": \"21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868\", \"files\": {}}", "dest": "cargo/vendor/futures-sink-0.3.21", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/futures-task/futures-task-0.3.16.crate", "sha256": "bbe54a98670017f3be909561f6ad13e810d9a51f3f061b902062ca3da80799f2", "dest": "cargo/vendor/futures-task-0.3.16" }, { "type": "inline", "contents": "{\"package\": \"bbe54a98670017f3be909561f6ad13e810d9a51f3f061b902062ca3da80799f2\", \"files\": {}}", "dest": "cargo/vendor/futures-task-0.3.16", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/futures-util/futures-util-0.3.16.crate", "sha256": "67eb846bfd58e44a8481a00049e82c43e0ccb5d61f8dc071057cb19249dd4d78", "dest": "cargo/vendor/futures-util-0.3.16" }, { "type": "inline", "contents": "{\"package\": \"67eb846bfd58e44a8481a00049e82c43e0ccb5d61f8dc071057cb19249dd4d78\", \"files\": {}}", "dest": "cargo/vendor/futures-util-0.3.16", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/gdk-pixbuf/gdk-pixbuf-0.15.4.crate", "sha256": "73aa2f5de1b45710da90a55863276667dc3a3264aaf6a2aeace62bb015244d49", "dest": "cargo/vendor/gdk-pixbuf-0.15.4" }, { "type": "inline", "contents": "{\"package\": \"73aa2f5de1b45710da90a55863276667dc3a3264aaf6a2aeace62bb015244d49\", \"files\": {}}", "dest": "cargo/vendor/gdk-pixbuf-0.15.4", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/gdk-pixbuf-sys/gdk-pixbuf-sys-0.15.1.crate", "sha256": "413424d9818621fa3cfc8a3a915cdb89a7c3c507d56761b4ec83a9a98e587171", "dest": "cargo/vendor/gdk-pixbuf-sys-0.15.1" }, { "type": "inline", "contents": "{\"package\": \"413424d9818621fa3cfc8a3a915cdb89a7c3c507d56761b4ec83a9a98e587171\", \"files\": {}}", "dest": "cargo/vendor/gdk-pixbuf-sys-0.15.1", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/gdk4/gdk4-0.4.8.crate", "sha256": "4fabb7cf843c26b085a5d68abb95d0c0bf27a9ae2eeff9c4adb503a1eb580876", "dest": "cargo/vendor/gdk4-0.4.8" }, { "type": "inline", "contents": "{\"package\": \"4fabb7cf843c26b085a5d68abb95d0c0bf27a9ae2eeff9c4adb503a1eb580876\", \"files\": {}}", "dest": "cargo/vendor/gdk4-0.4.8", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/gdk4-sys/gdk4-sys-0.4.8.crate", "sha256": "efe7dcb44f5c00aeabff3f69abfc5673de46559070f89bd3fbb7b66485d9cef2", "dest": "cargo/vendor/gdk4-sys-0.4.8" }, { "type": "inline", "contents": "{\"package\": \"efe7dcb44f5c00aeabff3f69abfc5673de46559070f89bd3fbb7b66485d9cef2\", \"files\": {}}", "dest": "cargo/vendor/gdk4-sys-0.4.8", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/getrandom/getrandom-0.2.4.crate", "sha256": "418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c", "dest": "cargo/vendor/getrandom-0.2.4" }, { "type": "inline", "contents": "{\"package\": \"418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c\", \"files\": {}}", "dest": "cargo/vendor/getrandom-0.2.4", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/gio/gio-0.15.5.crate", "sha256": "59105fa464928adf56b159c8d980cc11fbfbe414befb904caac5163d383049bf", "dest": "cargo/vendor/gio-0.15.5" }, { "type": "inline", "contents": "{\"package\": \"59105fa464928adf56b159c8d980cc11fbfbe414befb904caac5163d383049bf\", \"files\": {}}", "dest": "cargo/vendor/gio-0.15.5", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/gio-sys/gio-sys-0.15.5.crate", "sha256": "4f0bc4cfc9ebcdd05cc5057bc51b99c32f8f9bf246274f6a556ffd27279f8fe3", "dest": "cargo/vendor/gio-sys-0.15.5" }, { "type": "inline", "contents": "{\"package\": \"4f0bc4cfc9ebcdd05cc5057bc51b99c32f8f9bf246274f6a556ffd27279f8fe3\", \"files\": {}}", "dest": "cargo/vendor/gio-sys-0.15.5", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/glib/glib-0.15.5.crate", "sha256": "41dcfbdb6cc6c02aee163339465d8a40d6f3f64c3a43f729a4195f0e153338b7", "dest": "cargo/vendor/glib-0.15.5" }, { "type": "inline", "contents": "{\"package\": \"41dcfbdb6cc6c02aee163339465d8a40d6f3f64c3a43f729a4195f0e153338b7\", \"files\": {}}", "dest": "cargo/vendor/glib-0.15.5", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/glib-macros/glib-macros-0.15.3.crate", "sha256": "e58b262ff65ef771003873cea8c10e0fe854f1c508d48d62a4111a1ff163f7d1", "dest": "cargo/vendor/glib-macros-0.15.3" }, { "type": "inline", "contents": "{\"package\": \"e58b262ff65ef771003873cea8c10e0fe854f1c508d48d62a4111a1ff163f7d1\", \"files\": {}}", "dest": "cargo/vendor/glib-macros-0.15.3", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/glib-sys/glib-sys-0.15.5.crate", "sha256": "fa1d4e1a63d8574541e5b92931e4e669ddc87ffa85d58e84e631dba13ad2e10c", "dest": "cargo/vendor/glib-sys-0.15.5" }, { "type": "inline", "contents": "{\"package\": \"fa1d4e1a63d8574541e5b92931e4e669ddc87ffa85d58e84e631dba13ad2e10c\", \"files\": {}}", "dest": "cargo/vendor/glib-sys-0.15.5", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/gobject-sys/gobject-sys-0.15.5.crate", "sha256": "df6859463843c20cf3837e3a9069b6ab2051aeeadf4c899d33344f4aea83189a", "dest": "cargo/vendor/gobject-sys-0.15.5" }, { "type": "inline", "contents": "{\"package\": \"df6859463843c20cf3837e3a9069b6ab2051aeeadf4c899d33344f4aea83189a\", \"files\": {}}", "dest": "cargo/vendor/gobject-sys-0.15.5", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/graphene-rs/graphene-rs-0.15.1.crate", "sha256": "7c54f9fbbeefdb62c99f892dfca35f83991e2cb5b46a8dc2a715e58612f85570", "dest": "cargo/vendor/graphene-rs-0.15.1" }, { "type": "inline", "contents": "{\"package\": \"7c54f9fbbeefdb62c99f892dfca35f83991e2cb5b46a8dc2a715e58612f85570\", \"files\": {}}", "dest": "cargo/vendor/graphene-rs-0.15.1", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/graphene-sys/graphene-sys-0.15.10.crate", "sha256": "fa691fc7337ba1df599afb55c3bcb85c04f1b3f17362570e9bb0ff0d1bc3028a", "dest": "cargo/vendor/graphene-sys-0.15.10" }, { "type": "inline", "contents": "{\"package\": \"fa691fc7337ba1df599afb55c3bcb85c04f1b3f17362570e9bb0ff0d1bc3028a\", \"files\": {}}", "dest": "cargo/vendor/graphene-sys-0.15.10", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/gsk4/gsk4-0.4.8.crate", "sha256": "05e9020d333280b3aa38d496495bfa9b50712eebf1ad63f0ec5bcddb5eb61be4", "dest": "cargo/vendor/gsk4-0.4.8" }, { "type": "inline", "contents": "{\"package\": \"05e9020d333280b3aa38d496495bfa9b50712eebf1ad63f0ec5bcddb5eb61be4\", \"files\": {}}", "dest": "cargo/vendor/gsk4-0.4.8", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/gsk4-sys/gsk4-sys-0.4.8.crate", "sha256": "7add39ccf60078508c838643a2dcc91f045c46ed63b5ea6ab701b2e25bda3fea", "dest": "cargo/vendor/gsk4-sys-0.4.8" }, { "type": "inline", "contents": "{\"package\": \"7add39ccf60078508c838643a2dcc91f045c46ed63b5ea6ab701b2e25bda3fea\", \"files\": {}}", "dest": "cargo/vendor/gsk4-sys-0.4.8", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/gtk4/gtk4-0.4.8.crate", "sha256": "c64f0c2a3d80e899dc3febddad5bac193ffcf74a0fd7e31037f30dd34d6f7396", "dest": "cargo/vendor/gtk4-0.4.8" }, { "type": "inline", "contents": "{\"package\": \"c64f0c2a3d80e899dc3febddad5bac193ffcf74a0fd7e31037f30dd34d6f7396\", \"files\": {}}", "dest": "cargo/vendor/gtk4-0.4.8", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/gtk4-macros/gtk4-macros-0.4.8.crate", "sha256": "fafbcc920af4eb677d7d164853e7040b9de5a22379c596f570190c675d45f7a7", "dest": "cargo/vendor/gtk4-macros-0.4.8" }, { "type": "inline", "contents": "{\"package\": \"fafbcc920af4eb677d7d164853e7040b9de5a22379c596f570190c675d45f7a7\", \"files\": {}}", "dest": "cargo/vendor/gtk4-macros-0.4.8", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/gtk4-sys/gtk4-sys-0.4.8.crate", "sha256": "5bc8006eea634b7c72da3ff79e24606e45f21b3b832a3c5a1f543f5f97eb0f63", "dest": "cargo/vendor/gtk4-sys-0.4.8" }, { "type": "inline", "contents": "{\"package\": \"5bc8006eea634b7c72da3ff79e24606e45f21b3b832a3c5a1f543f5f97eb0f63\", \"files\": {}}", "dest": "cargo/vendor/gtk4-sys-0.4.8", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/heck/heck-0.4.0.crate", "sha256": "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9", "dest": "cargo/vendor/heck-0.4.0" }, { "type": "inline", "contents": "{\"package\": \"2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9\", \"files\": {}}", "dest": "cargo/vendor/heck-0.4.0", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/hermit-abi/hermit-abi-0.1.19.crate", "sha256": "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33", "dest": "cargo/vendor/hermit-abi-0.1.19" }, { "type": "inline", "contents": "{\"package\": \"62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33\", \"files\": {}}", "dest": "cargo/vendor/hermit-abi-0.1.19", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/hex/hex-0.4.3.crate", "sha256": "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70", "dest": "cargo/vendor/hex-0.4.3" }, { "type": "inline", "contents": "{\"package\": \"7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70\", \"files\": {}}", "dest": "cargo/vendor/hex-0.4.3", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/humantime/humantime-2.1.0.crate", "sha256": "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4", "dest": "cargo/vendor/humantime-2.1.0" }, { "type": "inline", "contents": "{\"package\": \"9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4\", \"files\": {}}", "dest": "cargo/vendor/humantime-2.1.0", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/infer/infer-0.9.0.crate", "sha256": "f178e61cdbfe084aa75a2f4f7a25a5bb09701a47ae1753608f194b15783c937a", "dest": "cargo/vendor/infer-0.9.0" }, { "type": "inline", "contents": "{\"package\": \"f178e61cdbfe084aa75a2f4f7a25a5bb09701a47ae1753608f194b15783c937a\", \"files\": {}}", "dest": "cargo/vendor/infer-0.9.0", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/instant/instant-0.1.12.crate", "sha256": "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c", "dest": "cargo/vendor/instant-0.1.12" }, { "type": "inline", "contents": "{\"package\": \"7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c\", \"files\": {}}", "dest": "cargo/vendor/instant-0.1.12", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/itertools/itertools-0.10.3.crate", "sha256": "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3", "dest": "cargo/vendor/itertools-0.10.3" }, { "type": "inline", "contents": "{\"package\": \"a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3\", \"files\": {}}", "dest": "cargo/vendor/itertools-0.10.3", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/lazy_static/lazy_static-1.4.0.crate", "sha256": "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646", "dest": "cargo/vendor/lazy_static-1.4.0" }, { "type": "inline", "contents": "{\"package\": \"e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646\", \"files\": {}}", "dest": "cargo/vendor/lazy_static-1.4.0", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/libc/libc-0.2.118.crate", "sha256": "06e509672465a0504304aa87f9f176f2b2b716ed8fb105ebe5c02dc6dce96a94", "dest": "cargo/vendor/libc-0.2.118" }, { "type": "inline", "contents": "{\"package\": \"06e509672465a0504304aa87f9f176f2b2b716ed8fb105ebe5c02dc6dce96a94\", \"files\": {}}", "dest": "cargo/vendor/libc-0.2.118", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/log/log-0.4.17.crate", "sha256": "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e", "dest": "cargo/vendor/log-0.4.17" }, { "type": "inline", "contents": "{\"package\": \"abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e\", \"files\": {}}", "dest": "cargo/vendor/log-0.4.17", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/memchr/memchr-2.4.0.crate", "sha256": "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc", "dest": "cargo/vendor/memchr-2.4.0" }, { "type": "inline", "contents": "{\"package\": \"b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc\", \"files\": {}}", "dest": "cargo/vendor/memchr-2.4.0", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/memoffset/memoffset-0.6.4.crate", "sha256": "59accc507f1338036a0477ef61afdae33cde60840f4dfe481319ce3ad116ddf9", "dest": "cargo/vendor/memoffset-0.6.4" }, { "type": "inline", "contents": "{\"package\": \"59accc507f1338036a0477ef61afdae33cde60840f4dfe481319ce3ad116ddf9\", \"files\": {}}", "dest": "cargo/vendor/memoffset-0.6.4", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/nix/nix-0.23.1.crate", "sha256": "9f866317acbd3a240710c63f065ffb1e4fd466259045ccb504130b7f668f35c6", "dest": "cargo/vendor/nix-0.23.1" }, { "type": "inline", "contents": "{\"package\": \"9f866317acbd3a240710c63f065ffb1e4fd466259045ccb504130b7f668f35c6\", \"files\": {}}", "dest": "cargo/vendor/nix-0.23.1", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/once_cell/once_cell-1.8.0.crate", "sha256": "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56", "dest": "cargo/vendor/once_cell-1.8.0" }, { "type": "inline", "contents": "{\"package\": \"692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56\", \"files\": {}}", "dest": "cargo/vendor/once_cell-1.8.0", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/ordered-stream/ordered-stream-0.0.1.crate", "sha256": "44630c059eacfd6e08bdaa51b1db2ce33119caa4ddc1235e923109aa5f25ccb1", "dest": "cargo/vendor/ordered-stream-0.0.1" }, { "type": "inline", "contents": "{\"package\": \"44630c059eacfd6e08bdaa51b1db2ce33119caa4ddc1235e923109aa5f25ccb1\", \"files\": {}}", "dest": "cargo/vendor/ordered-stream-0.0.1", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/pango/pango-0.15.2.crate", "sha256": "79211eff430c29cc38c69e0ab54bc78fa1568121ca9737707eee7f92a8417a94", "dest": "cargo/vendor/pango-0.15.2" }, { "type": "inline", "contents": "{\"package\": \"79211eff430c29cc38c69e0ab54bc78fa1568121ca9737707eee7f92a8417a94\", \"files\": {}}", "dest": "cargo/vendor/pango-0.15.2", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/pango-sys/pango-sys-0.15.1.crate", "sha256": "7022c2fb88cd2d9d55e1a708a8c53a3ae8678234c4a54bf623400aeb7f31fac2", "dest": "cargo/vendor/pango-sys-0.15.1" }, { "type": "inline", "contents": "{\"package\": \"7022c2fb88cd2d9d55e1a708a8c53a3ae8678234c4a54bf623400aeb7f31fac2\", \"files\": {}}", "dest": "cargo/vendor/pango-sys-0.15.1", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/parking/parking-2.0.0.crate", "sha256": "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72", "dest": "cargo/vendor/parking-2.0.0" }, { "type": "inline", "contents": "{\"package\": \"427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72\", \"files\": {}}", "dest": "cargo/vendor/parking-2.0.0", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/pest/pest-2.1.3.crate", "sha256": "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53", "dest": "cargo/vendor/pest-2.1.3" }, { "type": "inline", "contents": "{\"package\": \"10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53\", \"files\": {}}", "dest": "cargo/vendor/pest-2.1.3", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/pin-project-lite/pin-project-lite-0.2.7.crate", "sha256": "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443", "dest": "cargo/vendor/pin-project-lite-0.2.7" }, { "type": "inline", "contents": "{\"package\": \"8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443\", \"files\": {}}", "dest": "cargo/vendor/pin-project-lite-0.2.7", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/pin-utils/pin-utils-0.1.0.crate", "sha256": "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184", "dest": "cargo/vendor/pin-utils-0.1.0" }, { "type": "inline", "contents": "{\"package\": \"8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184\", \"files\": {}}", "dest": "cargo/vendor/pin-utils-0.1.0", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/pkg-config/pkg-config-0.3.25.crate", "sha256": "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae", "dest": "cargo/vendor/pkg-config-0.3.25" }, { "type": "inline", "contents": "{\"package\": \"1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae\", \"files\": {}}", "dest": "cargo/vendor/pkg-config-0.3.25", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/polling/polling-2.2.0.crate", "sha256": "685404d509889fade3e86fe3a5803bca2ec09b0c0778d5ada6ec8bf7a8de5259", "dest": "cargo/vendor/polling-2.2.0" }, { "type": "inline", "contents": "{\"package\": \"685404d509889fade3e86fe3a5803bca2ec09b0c0778d5ada6ec8bf7a8de5259\", \"files\": {}}", "dest": "cargo/vendor/polling-2.2.0", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/ppv-lite86/ppv-lite86-0.2.16.crate", "sha256": "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872", "dest": "cargo/vendor/ppv-lite86-0.2.16" }, { "type": "inline", "contents": "{\"package\": \"eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872\", \"files\": {}}", "dest": "cargo/vendor/ppv-lite86-0.2.16", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/proc-macro-crate/proc-macro-crate-1.0.0.crate", "sha256": "41fdbd1df62156fbc5945f4762632564d7d038153091c3fcf1067f6aef7cff92", "dest": "cargo/vendor/proc-macro-crate-1.0.0" }, { "type": "inline", "contents": "{\"package\": \"41fdbd1df62156fbc5945f4762632564d7d038153091c3fcf1067f6aef7cff92\", \"files\": {}}", "dest": "cargo/vendor/proc-macro-crate-1.0.0", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/proc-macro-error/proc-macro-error-1.0.4.crate", "sha256": "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c", "dest": "cargo/vendor/proc-macro-error-1.0.4" }, { "type": "inline", "contents": "{\"package\": \"da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c\", \"files\": {}}", "dest": "cargo/vendor/proc-macro-error-1.0.4", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/proc-macro-error-attr/proc-macro-error-attr-1.0.4.crate", "sha256": "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869", "dest": "cargo/vendor/proc-macro-error-attr-1.0.4" }, { "type": "inline", "contents": "{\"package\": \"a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869\", \"files\": {}}", "dest": "cargo/vendor/proc-macro-error-attr-1.0.4", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/proc-macro-hack/proc-macro-hack-0.5.19.crate", "sha256": "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5", "dest": "cargo/vendor/proc-macro-hack-0.5.19" }, { "type": "inline", "contents": "{\"package\": \"dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5\", \"files\": {}}", "dest": "cargo/vendor/proc-macro-hack-0.5.19", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/proc-macro-nested/proc-macro-nested-0.1.7.crate", "sha256": "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086", "dest": "cargo/vendor/proc-macro-nested-0.1.7" }, { "type": "inline", "contents": "{\"package\": \"bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086\", \"files\": {}}", "dest": "cargo/vendor/proc-macro-nested-0.1.7", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/proc-macro2/proc-macro2-1.0.28.crate", "sha256": "5c7ed8b8c7b886ea3ed7dde405212185f423ab44682667c8c6dd14aa1d9f6612", "dest": "cargo/vendor/proc-macro2-1.0.28" }, { "type": "inline", "contents": "{\"package\": \"5c7ed8b8c7b886ea3ed7dde405212185f423ab44682667c8c6dd14aa1d9f6612\", \"files\": {}}", "dest": "cargo/vendor/proc-macro2-1.0.28", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/quick-xml/quick-xml-0.22.0.crate", "sha256": "8533f14c8382aaad0d592c812ac3b826162128b65662331e1127b45c3d18536b", "dest": "cargo/vendor/quick-xml-0.22.0" }, { "type": "inline", "contents": "{\"package\": \"8533f14c8382aaad0d592c812ac3b826162128b65662331e1127b45c3d18536b\", \"files\": {}}", "dest": "cargo/vendor/quick-xml-0.22.0", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/quote/quote-1.0.9.crate", "sha256": "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7", "dest": "cargo/vendor/quote-1.0.9" }, { "type": "inline", "contents": "{\"package\": \"c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7\", \"files\": {}}", "dest": "cargo/vendor/quote-1.0.9", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/rand/rand-0.8.5.crate", "sha256": "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404", "dest": "cargo/vendor/rand-0.8.5" }, { "type": "inline", "contents": "{\"package\": \"34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404\", \"files\": {}}", "dest": "cargo/vendor/rand-0.8.5", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/rand_chacha/rand_chacha-0.3.1.crate", "sha256": "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88", "dest": "cargo/vendor/rand_chacha-0.3.1" }, { "type": "inline", "contents": "{\"package\": \"e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88\", \"files\": {}}", "dest": "cargo/vendor/rand_chacha-0.3.1", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/rand_core/rand_core-0.6.3.crate", "sha256": "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7", "dest": "cargo/vendor/rand_core-0.6.3" }, { "type": "inline", "contents": "{\"package\": \"d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7\", \"files\": {}}", "dest": "cargo/vendor/rand_core-0.6.3", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/regex/regex-1.5.4.crate", "sha256": "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461", "dest": "cargo/vendor/regex-1.5.4" }, { "type": "inline", "contents": "{\"package\": \"d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461\", \"files\": {}}", "dest": "cargo/vendor/regex-1.5.4", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/regex-syntax/regex-syntax-0.6.25.crate", "sha256": "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b", "dest": "cargo/vendor/regex-syntax-0.6.25" }, { "type": "inline", "contents": "{\"package\": \"f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b\", \"files\": {}}", "dest": "cargo/vendor/regex-syntax-0.6.25", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/rustc_version/rustc_version-0.3.3.crate", "sha256": "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee", "dest": "cargo/vendor/rustc_version-0.3.3" }, { "type": "inline", "contents": "{\"package\": \"f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee\", \"files\": {}}", "dest": "cargo/vendor/rustc_version-0.3.3", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/semver/semver-0.11.0.crate", "sha256": "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6", "dest": "cargo/vendor/semver-0.11.0" }, { "type": "inline", "contents": "{\"package\": \"f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6\", \"files\": {}}", "dest": "cargo/vendor/semver-0.11.0", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/semver-parser/semver-parser-0.10.2.crate", "sha256": "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7", "dest": "cargo/vendor/semver-parser-0.10.2" }, { "type": "inline", "contents": "{\"package\": \"00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7\", \"files\": {}}", "dest": "cargo/vendor/semver-parser-0.10.2", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/serde/serde-1.0.127.crate", "sha256": "f03b9878abf6d14e6779d3f24f07b2cfa90352cfec4acc5aab8f1ac7f146fae8", "dest": "cargo/vendor/serde-1.0.127" }, { "type": "inline", "contents": "{\"package\": \"f03b9878abf6d14e6779d3f24f07b2cfa90352cfec4acc5aab8f1ac7f146fae8\", \"files\": {}}", "dest": "cargo/vendor/serde-1.0.127", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/serde_derive/serde_derive-1.0.127.crate", "sha256": "a024926d3432516606328597e0f224a51355a493b49fdd67e9209187cbe55ecc", "dest": "cargo/vendor/serde_derive-1.0.127" }, { "type": "inline", "contents": "{\"package\": \"a024926d3432516606328597e0f224a51355a493b49fdd67e9209187cbe55ecc\", \"files\": {}}", "dest": "cargo/vendor/serde_derive-1.0.127", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/serde_repr/serde_repr-0.1.7.crate", "sha256": "98d0516900518c29efa217c298fa1f4e6c6ffc85ae29fd7f4ee48f176e1a9ed5", "dest": "cargo/vendor/serde_repr-0.1.7" }, { "type": "inline", "contents": "{\"package\": \"98d0516900518c29efa217c298fa1f4e6c6ffc85ae29fd7f4ee48f176e1a9ed5\", \"files\": {}}", "dest": "cargo/vendor/serde_repr-0.1.7", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/sha1/sha1-0.6.1.crate", "sha256": "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770", "dest": "cargo/vendor/sha1-0.6.1" }, { "type": "inline", "contents": "{\"package\": \"c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770\", \"files\": {}}", "dest": "cargo/vendor/sha1-0.6.1", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/sha1_smol/sha1_smol-1.0.0.crate", "sha256": "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012", "dest": "cargo/vendor/sha1_smol-1.0.0" }, { "type": "inline", "contents": "{\"package\": \"ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012\", \"files\": {}}", "dest": "cargo/vendor/sha1_smol-1.0.0", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/slab/slab-0.4.4.crate", "sha256": "c307a32c1c5c437f38c7fd45d753050587732ba8628319fbdf12a7e289ccc590", "dest": "cargo/vendor/slab-0.4.4" }, { "type": "inline", "contents": "{\"package\": \"c307a32c1c5c437f38c7fd45d753050587732ba8628319fbdf12a7e289ccc590\", \"files\": {}}", "dest": "cargo/vendor/slab-0.4.4", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/smallvec/smallvec-1.6.1.crate", "sha256": "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e", "dest": "cargo/vendor/smallvec-1.6.1" }, { "type": "inline", "contents": "{\"package\": \"fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e\", \"files\": {}}", "dest": "cargo/vendor/smallvec-1.6.1", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/socket2/socket2-0.4.4.crate", "sha256": "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0", "dest": "cargo/vendor/socket2-0.4.4" }, { "type": "inline", "contents": "{\"package\": \"66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0\", \"files\": {}}", "dest": "cargo/vendor/socket2-0.4.4", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/static_assertions/static_assertions-1.1.0.crate", "sha256": "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f", "dest": "cargo/vendor/static_assertions-1.1.0" }, { "type": "inline", "contents": "{\"package\": \"a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f\", \"files\": {}}", "dest": "cargo/vendor/static_assertions-1.1.0", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/syn/syn-1.0.74.crate", "sha256": "1873d832550d4588c3dbc20f01361ab00bfe741048f71e3fecf145a7cc18b29c", "dest": "cargo/vendor/syn-1.0.74" }, { "type": "inline", "contents": "{\"package\": \"1873d832550d4588c3dbc20f01361ab00bfe741048f71e3fecf145a7cc18b29c\", \"files\": {}}", "dest": "cargo/vendor/syn-1.0.74", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/system-deps/system-deps-6.0.2.crate", "sha256": "a1a45a1c4c9015217e12347f2a411b57ce2c4fc543913b14b6fe40483328e709", "dest": "cargo/vendor/system-deps-6.0.2" }, { "type": "inline", "contents": "{\"package\": \"a1a45a1c4c9015217e12347f2a411b57ce2c4fc543913b14b6fe40483328e709\", \"files\": {}}", "dest": "cargo/vendor/system-deps-6.0.2", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/termcolor/termcolor-1.1.2.crate", "sha256": "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4", "dest": "cargo/vendor/termcolor-1.1.2" }, { "type": "inline", "contents": "{\"package\": \"2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4\", \"files\": {}}", "dest": "cargo/vendor/termcolor-1.1.2", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/thiserror/thiserror-1.0.26.crate", "sha256": "93119e4feac1cbe6c798c34d3a53ea0026b0b1de6a120deef895137c0529bfe2", "dest": "cargo/vendor/thiserror-1.0.26" }, { "type": "inline", "contents": "{\"package\": \"93119e4feac1cbe6c798c34d3a53ea0026b0b1de6a120deef895137c0529bfe2\", \"files\": {}}", "dest": "cargo/vendor/thiserror-1.0.26", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/thiserror-impl/thiserror-impl-1.0.26.crate", "sha256": "060d69a0afe7796bf42e9e2ff91f5ee691fb15c53d38b4b62a9a53eb23164745", "dest": "cargo/vendor/thiserror-impl-1.0.26" }, { "type": "inline", "contents": "{\"package\": \"060d69a0afe7796bf42e9e2ff91f5ee691fb15c53d38b4b62a9a53eb23164745\", \"files\": {}}", "dest": "cargo/vendor/thiserror-impl-1.0.26", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/toml/toml-0.5.8.crate", "sha256": "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa", "dest": "cargo/vendor/toml-0.5.8" }, { "type": "inline", "contents": "{\"package\": \"a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa\", \"files\": {}}", "dest": "cargo/vendor/toml-0.5.8", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/ucd-trie/ucd-trie-0.1.3.crate", "sha256": "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c", "dest": "cargo/vendor/ucd-trie-0.1.3" }, { "type": "inline", "contents": "{\"package\": \"56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c\", \"files\": {}}", "dest": "cargo/vendor/ucd-trie-0.1.3", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/unicode-xid/unicode-xid-0.2.2.crate", "sha256": "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3", "dest": "cargo/vendor/unicode-xid-0.2.2" }, { "type": "inline", "contents": "{\"package\": \"8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3\", \"files\": {}}", "dest": "cargo/vendor/unicode-xid-0.2.2", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/uuid/uuid-1.1.2.crate", "sha256": "dd6469f4314d5f1ffec476e05f17cc9a78bc7a27a6a857842170bdf8d6f98d2f", "dest": "cargo/vendor/uuid-1.1.2" }, { "type": "inline", "contents": "{\"package\": \"dd6469f4314d5f1ffec476e05f17cc9a78bc7a27a6a857842170bdf8d6f98d2f\", \"files\": {}}", "dest": "cargo/vendor/uuid-1.1.2", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/version-compare/version-compare-0.1.0.crate", "sha256": "fe88247b92c1df6b6de80ddc290f3976dbdf2f5f5d3fd049a9fb598c6dd5ca73", "dest": "cargo/vendor/version-compare-0.1.0" }, { "type": "inline", "contents": "{\"package\": \"fe88247b92c1df6b6de80ddc290f3976dbdf2f5f5d3fd049a9fb598c6dd5ca73\", \"files\": {}}", "dest": "cargo/vendor/version-compare-0.1.0", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/version_check/version_check-0.9.3.crate", "sha256": "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe", "dest": "cargo/vendor/version_check-0.9.3" }, { "type": "inline", "contents": "{\"package\": \"5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe\", \"files\": {}}", "dest": "cargo/vendor/version_check-0.9.3", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/waker-fn/waker-fn-1.1.0.crate", "sha256": "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca", "dest": "cargo/vendor/waker-fn-1.1.0" }, { "type": "inline", "contents": "{\"package\": \"9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca\", \"files\": {}}", "dest": "cargo/vendor/waker-fn-1.1.0", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/wasi/wasi-0.10.2+wasi-snapshot-preview1.crate", "sha256": "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6", "dest": "cargo/vendor/wasi-0.10.2+wasi-snapshot-preview1" }, { "type": "inline", "contents": "{\"package\": \"fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6\", \"files\": {}}", "dest": "cargo/vendor/wasi-0.10.2+wasi-snapshot-preview1", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/wepoll-ffi/wepoll-ffi-0.1.2.crate", "sha256": "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb", "dest": "cargo/vendor/wepoll-ffi-0.1.2" }, { "type": "inline", "contents": "{\"package\": \"d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb\", \"files\": {}}", "dest": "cargo/vendor/wepoll-ffi-0.1.2", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/winapi/winapi-0.3.9.crate", "sha256": "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419", "dest": "cargo/vendor/winapi-0.3.9" }, { "type": "inline", "contents": "{\"package\": \"5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419\", \"files\": {}}", "dest": "cargo/vendor/winapi-0.3.9", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/winapi-i686-pc-windows-gnu/winapi-i686-pc-windows-gnu-0.4.0.crate", "sha256": "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6", "dest": "cargo/vendor/winapi-i686-pc-windows-gnu-0.4.0" }, { "type": "inline", "contents": "{\"package\": \"ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6\", \"files\": {}}", "dest": "cargo/vendor/winapi-i686-pc-windows-gnu-0.4.0", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/winapi-util/winapi-util-0.1.5.crate", "sha256": "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178", "dest": "cargo/vendor/winapi-util-0.1.5" }, { "type": "inline", "contents": "{\"package\": \"70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178\", \"files\": {}}", "dest": "cargo/vendor/winapi-util-0.1.5", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/winapi-x86_64-pc-windows-gnu/winapi-x86_64-pc-windows-gnu-0.4.0.crate", "sha256": "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f", "dest": "cargo/vendor/winapi-x86_64-pc-windows-gnu-0.4.0" }, { "type": "inline", "contents": "{\"package\": \"712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f\", \"files\": {}}", "dest": "cargo/vendor/winapi-x86_64-pc-windows-gnu-0.4.0", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/zbus/zbus-2.1.1.crate", "sha256": "7bb86f3d4592e26a48b2719742aec94f8ae6238ebde20d98183ee185d1275e9a", "dest": "cargo/vendor/zbus-2.1.1" }, { "type": "inline", "contents": "{\"package\": \"7bb86f3d4592e26a48b2719742aec94f8ae6238ebde20d98183ee185d1275e9a\", \"files\": {}}", "dest": "cargo/vendor/zbus-2.1.1", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/zbus_macros/zbus_macros-2.1.1.crate", "sha256": "36823cc10fddc3c6b19f048903262dacaf8274170e9a255784bdd8b4570a8040", "dest": "cargo/vendor/zbus_macros-2.1.1" }, { "type": "inline", "contents": "{\"package\": \"36823cc10fddc3c6b19f048903262dacaf8274170e9a255784bdd8b4570a8040\", \"files\": {}}", "dest": "cargo/vendor/zbus_macros-2.1.1", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/zbus_names/zbus_names-2.1.0.crate", "sha256": "45dfcdcf87b71dad505d30cc27b1b7b88a64b6d1c435648f48f9dbc1fdc4b7e1", "dest": "cargo/vendor/zbus_names-2.1.0" }, { "type": "inline", "contents": "{\"package\": \"45dfcdcf87b71dad505d30cc27b1b7b88a64b6d1c435648f48f9dbc1fdc4b7e1\", \"files\": {}}", "dest": "cargo/vendor/zbus_names-2.1.0", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/zvariant/zvariant-3.1.2.crate", "sha256": "49ea5dc38b2058fae6a5b79009388143dadce1e91c26a67f984a0fc0381c8033", "dest": "cargo/vendor/zvariant-3.1.2" }, { "type": "inline", "contents": "{\"package\": \"49ea5dc38b2058fae6a5b79009388143dadce1e91c26a67f984a0fc0381c8033\", \"files\": {}}", "dest": "cargo/vendor/zvariant-3.1.2", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", "url": "https://static.crates.io/crates/zvariant_derive/zvariant_derive-3.1.2.crate", "sha256": "8c2cecc5a61c2a053f7f653a24cd15b3b0195d7f7ddb5042c837fb32e161fb7a", "dest": "cargo/vendor/zvariant_derive-3.1.2" }, { "type": "inline", "contents": "{\"package\": \"8c2cecc5a61c2a053f7f653a24cd15b3b0195d7f7ddb5042c837fb32e161fb7a\", \"files\": {}}", "dest": "cargo/vendor/zvariant_derive-3.1.2", "dest-filename": ".cargo-checksum.json" }, { "type": "inline", "contents": "[source.vendored-sources]\ndirectory = \"cargo/vendor\"\n\n[source.crates-io]\nreplace-with = \"vendored-sources\"\n", "dest": "cargo", "dest-filename": "config" } ] ================================================ FILE: src/resources/com.github.weclaw1.ImageRoll.desktop ================================================ [Desktop Entry] Type=Application Name=Image Roll Comment=Image viewer with basic image manipulation tools Exec=image-roll %U Icon=com.github.weclaw1.ImageRoll Terminal=false StartupWMClass=image-roll TryExec=image-roll Categories=Graphics; X-Purism-FormFactor=Workstation;Mobile; MimeType=image/bmp;image/gif;image/jpeg;image/jpg;image/pjpeg;image/png;image/tiff;image/x-bmp;image/x-gray;image/x-icb;image/x-ico;image/x-png;image/x-portable-anymap;image/x-portable-bitmap;image/x-portable-graymap;image/x-portable-pixmap;image/x-xbitmap;image/x-xpixmap;image/x-pcx;image/svg+xml;image/svg+xml-compressed;image/vnd.wap.wbmp;image/x-icns; ================================================ FILE: src/resources/com.github.weclaw1.ImageRoll.gschema.xml ================================================ 1024 Last window width 768 Last window height ================================================ FILE: src/resources/com.github.weclaw1.ImageRoll.metainfo.xml ================================================ com.github.weclaw1.ImageRoll Image Roll Image viewer with basic image manipulation tools CC0-1.0 MIT

Image Roll is a simple and fast GTK image viewer with basic image manipulation tools. Written in rust.

com.github.weclaw1.ImageRoll.desktop https://raw.githubusercontent.com/weclaw1/image-roll/main/src/resources/screenshot.png Robert Węcławski https://github.com/weclaw1/image-roll https://github.com/weclaw1/image-roll/issues
================================================ FILE: src/resources/com.github.weclaw1.ImageRoll.yaml ================================================ app-id: com.github.weclaw1.ImageRoll runtime: org.gnome.Platform runtime-version: '42' sdk: org.gnome.Sdk sdk-extensions: - org.freedesktop.Sdk.Extension.rust-stable command: image-roll finish-args: - --share=ipc - --socket=fallback-x11 - --socket=wayland - --filesystem=home - --filesystem=/mnt - --filesystem=/media - --filesystem=/run/media - --device=dri build-options: append-path: /usr/lib/sdk/rust-stable/bin env: CARGO_HOME: /run/build/image-roll/cargo modules: - name: image-roll buildsystem: simple build-commands: - cargo --offline fetch --manifest-path Cargo.toml - cargo --offline build --release - install -Dm755 ./target/release/image-roll -t /app/bin/ - install -Dm644 ./src/resources/com.github.weclaw1.ImageRoll.svg -t /app/share/icons/hicolor/scalable/apps/ - install -Dm644 ./src/resources/com.github.weclaw1.ImageRoll.desktop -t /app/share/applications/ - install -Dm644 ./src/resources/com.github.weclaw1.ImageRoll.metainfo.xml -t /app/share/metainfo/ - install -Dm644 ./src/resources/com.github.weclaw1.ImageRoll.gschema.xml -t /app/share/glib-2.0/schemas/ - glib-compile-schemas /app/share/glib-2.0/schemas sources: - cargo-sources.json - type: git url: https://github.com/weclaw1/image-roll.git tag: 2.1.0 ================================================ FILE: src/resources/image-roll.cmb ================================================ (1,None,"image-roll.ui","image-roll.ui",None,None,None,None,None,None) (1,1,"GtkApplicationWindow","main_window",None,None,None,None,None), (1,2,"GtkHeaderBar","headerbar",1,None,"titlebar",None,-1), (1,3,"GtkBox",None,2,None,"end",None,None), (1,4,"GtkBox",None,2,None,"start",None,None), (1,5,"GtkButton","preview_smaller_button",4,None,None,None,None), (1,6,"GtkLabel","preview_size_label",4,None,None,None,1), (1,7,"GtkButton","preview_larger_button",4,None,None,None,2), (1,8,"GtkButton","preview_fit_screen_button",4,None,None,None,3), (1,9,"GtkButton","delete_button",3,None,None,None,None), (1,10,"GtkMenuButton","menu_button",3,None,None,None,1), (1,11,"GtkPopoverMenu","popover_menu",None,None,None,None,None), (1,12,"GtkBox",None,11,None,None,None,None), (1,13,"GtkButton","open_menu_button",12,None,None,None,None), (1,14,"GtkButton","save_menu_button",12,None,None,None,1), (1,15,"GtkButton","set_as_wallpaper_menu_button",12,None,None,None,4), (1,16,"GtkButton","print_menu_button",12,None,None,None,5), (1,17,"GtkButton","copy_menu_button",12,None,None,None,3), (1,18,"GtkButton","save_as_menu_button",12,None,None,None,2), (1,23,"GtkBox",None,1,None,None,None,None), (1,24,"GtkInfoBar","error_info_bar",23,None,None,None,None), (1,25,"GtkLabel","error_info_bar_text",24,None,None,None,None), (1,26,"GtkScrolledWindow","image_scrolled_window",23,None,None,None,1), (1,27,"GtkViewport","image_viewport",26,None,None,None,None), (1,29,"GtkBox","action_bar",23,None,None,None,2), (1,30,"GtkButton","previous_button",29,None,None,None,None), (1,31,"GtkFlowBox",None,29,None,None,None,1), (1,32,"GtkButton","next_button",29,None,None,None,2), (1,33,"GtkButton","undo_button",31,None,None,None,None), (1,34,"GtkButton","rotate_counterclockwise_button",31,None,None,None,1), (1,36,"GtkMenuButton","resize_button",31,None,None,None,3), (1,37,"GtkToggleButton","crop_button",31,None,None,None,2), (1,38,"GtkPopover","resize_popover",None,None,None,None,None), (1,39,"GtkBox",None,38,None,None,None,None), (1,40,"GtkToggleButton","link_aspect_ratio_button",39,None,None,None,None), (1,41,"GtkSpinButton","width_spin_button",39,None,None,None,1), (1,42,"GtkAdjustment","width_adjustment",None,None,None,None,None), (1,43,"GtkLabel","x_label",39,None,None,None,2), (1,44,"GtkSpinButton","height_spin_button",39,None,None,None,3), (1,45,"GtkAdjustment","height_adjustment",None,None,None,None,None), (1,46,"GtkButton","apply_resize_button",39,None,None,None,4), (1,47,"GtkButton","rotate_clockwise_button",31,None,None,None,4), (1,48,"GtkButton","redo_button",31,None,None,None,5), (1,49,"GtkDrawingArea","image_widget",27,None,None,None,None) (1,1,"GtkWindow","child",None,None,None,None,None,23), (1,1,"GtkWindow","default-height","768",None,None,None,None,None), (1,1,"GtkWindow","default-width","1024",None,None,None,None,None), (1,1,"GtkWindow","icon-name","com.github.weclaw1.ImageRoll",None,None,None,None,None), (1,1,"GtkWindow","title","Image Roll",None,None,None,None,None), (1,3,"GtkBox","spacing","5",None,None,None,None,None), (1,5,"GtkButton","icon-name","zoom-out-symbolic",None,None,None,None,None), (1,6,"GtkLabel","label","Fit screen",None,None,None,None,None), (1,7,"GtkButton","icon-name","zoom-in-symbolic",None,None,None,None,None), (1,8,"GtkButton","icon-name","zoom-fit-best-symbolic",None,None,None,None,None), (1,9,"GtkButton","icon-name","user-trash-symbolic",None,None,None,None,None), (1,10,"GtkMenuButton","direction","none",None,None,None,None,None), (1,10,"GtkMenuButton","popover","11",None,None,None,None,None), (1,11,"GtkPopover","child",None,None,None,None,None,12), (1,12,"GtkOrientable","orientation","vertical",None,None,None,None,None), (1,13,"GtkButton","has-frame","False",None,None,None,None,None), (1,13,"GtkButton","label","Open...",None,None,None,None,None), (1,14,"GtkButton","has-frame","False",None,None,None,None,None), (1,14,"GtkButton","label","Save",None,None,None,None,None), (1,15,"GtkButton","has-frame","False",None,None,None,None,None), (1,15,"GtkButton","label","Set as wallpaper",None,None,None,None,None), (1,16,"GtkButton","has-frame","False",None,None,None,None,None), (1,16,"GtkButton","label","Print",None,None,None,None,None), (1,17,"GtkButton","has-frame","False",None,None,None,None,None), (1,17,"GtkButton","label","Copy",None,None,None,None,None), (1,18,"GtkButton","has-frame","False",None,None,None,None,None), (1,18,"GtkButton","label","Save as...",None,None,None,None,None), (1,23,"GtkOrientable","orientation","vertical",None,None,None,None,None), (1,24,"GtkInfoBar","message-type","error",None,None,None,None,None), (1,24,"GtkInfoBar","revealed","False",None,None,None,None,None), (1,24,"GtkInfoBar","show-close-button","True",None,None,None,None,None), (1,25,"GtkLabel","label","ERROR",None,None,None,None,None), (1,26,"GtkScrolledWindow","child",None,None,None,None,None,27), (1,26,"GtkWidget","vexpand","True",None,None,None,None,None), (1,27,"GtkViewport","child",None,None,None,None,None,49), (1,30,"GtkButton","has-frame","False",None,None,None,None,None), (1,30,"GtkButton","icon-name","go-previous-symbolic",None,None,None,None,None), (1,30,"GtkWidget","halign","start",None,None,None,None,None), (1,30,"GtkWidget","hexpand","True",None,None,None,None,None), (1,31,"GtkFlowBox","column-spacing","8",None,None,None,None,None), (1,31,"GtkFlowBox","max-children-per-line","6",None,None,None,None,None), (1,31,"GtkWidget","halign","center",None,None,None,None,None), (1,31,"GtkWidget","width-request","300",None,None,None,None,None), (1,32,"GtkButton","has-frame","False",None,None,None,None,None), (1,32,"GtkButton","icon-name","go-next-symbolic",None,None,None,None,None), (1,32,"GtkWidget","halign","end",None,None,None,None,None), (1,32,"GtkWidget","hexpand","True",None,None,None,None,None), (1,33,"GtkButton","has-frame","False",None,None,None,None,None), (1,33,"GtkButton","icon-name","edit-undo-symbolic",None,None,None,None,None), (1,34,"GtkButton","has-frame","False",None,None,None,None,None), (1,34,"GtkButton","icon-name","object-rotate-left-symbolic",None,None,None,None,None), (1,36,"GtkMenuButton","direction","up",None,None,None,None,None), (1,36,"GtkMenuButton","has-frame","False",None,None,None,None,None), (1,36,"GtkMenuButton","icon-name","view-fullscreen-symbolic",None,None,None,None,None), (1,36,"GtkMenuButton","popover","38",None,None,None,None,None), (1,37,"GtkButton","has-frame","False",None,None,None,None,None), (1,37,"GtkButton","icon-name","crop-symbolic",None,None,None,None,None), (1,38,"GtkPopover","child",None,None,None,None,None,39), (1,38,"GtkPopover","position","top",None,None,None,None,None), (1,40,"GtkButton","has-frame","False",None,None,None,None,None), (1,40,"GtkButton","icon-name","insert-link-symbolic",None,None,None,None,None), (1,41,"GtkOrientable","orientation","vertical",None,None,None,None,None), (1,41,"GtkSpinButton","adjustment","42",None,None,None,None,None), (1,41,"GtkSpinButton","climb-rate","0.5",None,None,None,None,None), (1,42,"GtkAdjustment","page-increment","10.0",None,None,None,None,None), (1,42,"GtkAdjustment","step-increment","1.0",None,None,None,None,None), (1,42,"GtkAdjustment","upper","2147483647.0",None,None,None,None,None), (1,43,"GtkLabel","label","x",None,None,None,None,None), (1,43,"GtkWidget","margin-end","5",None,None,None,None,None), (1,43,"GtkWidget","margin-start","5",None,None,None,None,None), (1,44,"GtkOrientable","orientation","vertical",None,None,None,None,None), (1,44,"GtkSpinButton","adjustment","45",None,None,None,None,None), (1,44,"GtkSpinButton","climb-rate","0.5",None,None,None,None,None), (1,45,"GtkAdjustment","page-increment","10.0",None,None,None,None,None), (1,45,"GtkAdjustment","step-increment","1.0",None,None,None,None,None), (1,45,"GtkAdjustment","upper","2147483647.0",None,None,None,None,None), (1,46,"GtkButton","has-frame","False",None,None,None,None,None), (1,46,"GtkButton","icon-name","emblem-ok-symbolic",None,None,None,None,None), (1,47,"GtkButton","has-frame","False",None,None,None,None,None), (1,47,"GtkButton","icon-name","object-rotate-right-symbolic",None,None,None,None,None), (1,48,"GtkButton","has-frame","False",None,None,None,None,None), (1,48,"GtkButton","icon-name","edit-redo-symbolic",None,None,None,None,None), (1,49,"GtkWidget","halign","center",None,None,None,None,None), (1,49,"GtkWidget","valign","center",None,None,None,None,None) ================================================ FILE: src/resources/image-roll.ui ================================================ vertical error False True ERROR center center True start False True go-previous-symbolic 8 center 6 300 False edit-undo-symbolic False object-rotate-left-symbolic False crop-symbolic up False view-fullscreen-symbolic resize_popover False object-rotate-right-symbolic False edit-redo-symbolic end False True go-next-symbolic 768 1024 com.github.weclaw1.ImageRoll Image Roll 5 user-trash-symbolic none popover_menu zoom-out-symbolic Fit screen zoom-in-symbolic zoom-fit-best-symbolic vertical False Open... False Save False Save as... False Copy False Set as wallpaper False Print False insert-link-symbolic width_adjustment 0.5 vertical x 5 5 height_adjustment 0.5 vertical False emblem-ok-symbolic top 10.0 1.0 2147483647.0 10.0 1.0 2147483647.0 ================================================ FILE: src/resources/resources.xml ================================================ image-roll.ui com.github.weclaw1.ImageRoll.svg icons/crop-symbolic.svg ================================================ FILE: src/settings.rs ================================================ use gtk::gio; use gtk::gio::prelude::SettingsExt; use gtk::gio::SettingsSchemaSource; use crate::image::PreviewSize; #[derive(Clone)] pub struct Settings { gio_settings: Option, scale: PreviewSize, scale_before_zoom_gesture: Option, fullscreen: bool, } impl Settings { pub fn new(application_id: &str) -> Settings { let gio_settings = match SettingsSchemaSource::default() { Some(schema_source) => { if schema_source.lookup(application_id, true).is_some() { Some(gio::Settings::new(application_id)) } else { None } } None => None, }; Settings { gio_settings, scale: PreviewSize::BestFit(0, 0), scale_before_zoom_gesture: None, fullscreen: false, } } pub fn set_window_size(&self, window_size: (u32, u32)) { if let Some(gio_settings) = self.gio_settings.as_ref() { let (window_width, window_height) = window_size; gio_settings .set_uint("window-width", window_width) .expect("Could not set setting window-width."); gio_settings .set_uint("window-height", window_height) .expect("Could not set setting window-height."); } } pub fn window_size(&self) -> (u32, u32) { match self.gio_settings.as_ref() { Some(gio_settings) => ( gio_settings.uint("window-width"), gio_settings.uint("window-height"), ), None => (1024, 768), } } pub fn set_scale(&mut self, preview_size: PreviewSize) { self.scale = preview_size; } pub fn scale(&self) -> PreviewSize { self.scale } pub fn set_fullscreen(&mut self, fullscreen: bool) { self.fullscreen = fullscreen; } pub fn fullscreen(&self) -> bool { self.fullscreen } pub fn scale_before_zoom_gesture(&self) -> Option { self.scale_before_zoom_gesture } pub fn set_scale_before_zoom_gesture( &mut self, scale_before_zoom_gesture: Option, ) { self.scale_before_zoom_gesture = scale_before_zoom_gesture; } } ================================================ FILE: src/test_utils.rs ================================================ use std::path::{Path, PathBuf}; pub struct TestResources { file_folder: PathBuf, } impl TestResources { pub fn new>(file_folder: P) -> Self { std::fs::create_dir_all(&file_folder).unwrap(); Self { file_folder: file_folder.as_ref().to_path_buf(), } } pub fn add_file, C: AsRef<[u8]>>(&mut self, file_name: T, contents: C) { std::fs::write(self.file_folder.join(file_name.as_ref()), contents).unwrap(); } pub fn remove_file>(&mut self, file_name: T) { std::fs::remove_file(self.file_folder.join(file_name.as_ref())).unwrap(); } pub fn file_folder(&self) -> &Path { self.file_folder.as_path() } } impl Drop for TestResources { fn drop(&mut self) { std::fs::remove_dir_all(self.file_folder.as_path()).unwrap(); } } ================================================ FILE: src/ui/action.rs ================================================ use std::{ cell::{Cell, RefCell}, path::PathBuf, rc::Rc, }; #[cfg(feature = "wallpaper")] use ashpd::{ desktop::wallpaper::{self, SetOn}, WindowIdentifier, }; use gtk::{ gdk, gio, glib::{self, timeout_future_seconds, Sender}, prelude::{ DisplayExt, FileMonitorExt, GdkCairoContextExt, GtkApplicationExt, GtkWindowExt, PrintOperationExt, ToggleButtonExt, WidgetExt, }, traits::DrawingAreaExt, MessageType, }; use crate::{ file_list::FileList, image::{self, CoordinatesPair, PreviewSize}, image_list::ImageList, image_operation::{ApplyImageOperation, ImageOperation}, settings::Settings, }; use super::{ event::{post_event, Event}, widgets::Widgets, }; pub fn refresh_file_list(sender: &Sender, file_list: &mut FileList) { post_event(sender, Event::HideInfoPanel); if let Err(error) = file_list.refresh() { post_event( sender, Event::DisplayMessage(error.to_string(), MessageType::Error), ); return; }; post_event(sender, Event::LoadImage(file_list.current_file_path())); } pub fn open_file( sender: &Sender, image_list: Rc>, file_list: &mut FileList, file: gio::File, ) { post_event(sender, Event::HideInfoPanel); image_list.replace(ImageList::new()); let new_file_list = match FileList::new(Some(file)) { Ok(file_list) => file_list, Err(error) => { post_event( sender, Event::DisplayMessage(error.to_string(), MessageType::Error), ); return; } }; *file_list = new_file_list; post_event(sender, Event::LoadImage(file_list.current_file_path())); let sender = sender.clone(); file_list .current_folder_monitor_mut() .unwrap() .connect_changed(move |_, _, _, _| { post_event(&sender, Event::RefreshFileList); }); } pub fn load_image( sender: &Sender, settings: &mut Settings, widgets: &Widgets, image_list: Rc>, file_path: Option, ) { hide_info_panel(widgets); let mut image_list = image_list.borrow_mut(); if let Some(file_path) = file_path { let image = if let Some(image) = image_list.remove(&file_path) { image.reload(&file_path) } else { image::Image::load(&file_path) }; let image = match image { Ok(image) => image, Err(error) => { image_list.set_current_image_path(None); post_event(sender, Event::RefreshPreview(settings.scale())); post_event( sender, Event::DisplayMessage(error.to_string(), MessageType::Error), ); return; } }; image_list.insert(file_path.clone(), image); widgets.window().set_title( file_path .file_name() .and_then(|file_name| file_name.to_str()), ); image_list.set_current_image_path(Some(file_path)); if let PreviewSize::BestFit(0, 0) = settings.scale() { let new_scale = PreviewSize::BestFit( widgets.image_viewport().allocation().width() as u32, widgets.image_viewport().allocation().height() as u32, ); settings.set_scale(new_scale); } post_event(sender, Event::RefreshPreview(settings.scale())); } else { widgets.window().set_title(Some("Image Roll")); image_list.set_current_image_path(None); post_event(sender, Event::RefreshPreview(settings.scale())); } } pub fn next_image( sender: &Sender, image_list: Rc>, file_list: &mut FileList, ) { if let Some(current_image) = image_list.borrow_mut().current_image_mut() { current_image.remove_image_buffers(); } file_list.next(); post_event(sender, Event::LoadImage(file_list.current_file_path())); } pub fn previous_image( sender: &Sender, image_list: Rc>, file_list: &mut FileList, ) { if let Some(current_image) = image_list.borrow_mut().current_image_mut() { current_image.remove_image_buffers(); } file_list.previous(); post_event(sender, Event::LoadImage(file_list.current_file_path())); } pub fn image_viewport_resize( sender: &Sender, settings: &mut Settings, viewport_size: (u32, u32), ) { if let PreviewSize::BestFit(_, _) = settings.scale() { let new_scale = PreviewSize::BestFit(viewport_size.0, viewport_size.1); settings.set_scale(new_scale); post_event(sender, Event::RefreshPreview(new_scale)); } } pub fn refresh_preview( widgets: &Widgets, image_list: Rc>, preview_size: PreviewSize, ) { widgets .preview_size_label() .set_text(String::from(preview_size).as_str()); if let Some(image) = image_list.borrow_mut().current_image_mut() { image.create_preview_image_buffer(preview_size); if let Some((preview_image_width, preview_image_height)) = image.preview_image_buffer_size() { widgets .image_widget() .set_content_width(preview_image_width as i32); widgets .image_widget() .set_content_height(preview_image_height as i32); } } else { widgets.image_widget().set_content_width(0); widgets.image_widget().set_content_height(0); } widgets.image_widget().queue_draw(); } pub fn change_preview_size( sender: &Sender, widgets: &Widgets, settings: &mut Settings, mut preview_size: PreviewSize, ) { if let PreviewSize::BestFit(_, _) = preview_size { let viewport_allocation = widgets.image_viewport().allocation(); preview_size = PreviewSize::BestFit( viewport_allocation.width() as u32, viewport_allocation.height() as u32, ); } settings.set_scale(preview_size); post_event(sender, Event::RefreshPreview(preview_size)); } pub fn preview_smaller(sender: &Sender, settings: &Settings, value: Option) { let new_scale = match value { None => settings.scale().smaller(), Some(value) => settings.scale().smaller_by(value), }; if let Some(new_scale) = new_scale { post_event(sender, Event::ChangePreviewSize(new_scale)); } } pub fn preview_larger(sender: &Sender, settings: &Settings, value: Option) { let new_scale = match value { None => settings.scale().larger(), Some(value) => settings.scale().larger_by(value), }; if let Some(new_scale) = new_scale { post_event(sender, Event::ChangePreviewSize(new_scale)); } } pub fn preview_fit_screen(sender: &Sender) { let new_scale = PreviewSize::BestFit(0, 0); post_event(sender, Event::ChangePreviewSize(new_scale)); } pub fn image_edit( sender: &Sender, settings: &Settings, image_list: Rc>, file_list: &FileList, image_operation: ImageOperation, ) { let mut image_list = image_list.borrow_mut(); if let Some(mut current_image) = image_list.remove_current_image() { current_image = current_image.apply_operation(&image_operation); image_list.insert(file_list.current_file_path().unwrap(), current_image); post_event(sender, Event::RefreshPreview(settings.scale())); } } pub fn start_selection( widgets: &Widgets, image_list: Rc>, selection_coords: Rc>>, position: (u32, u32), ) { if image_list.borrow().current_image().is_some() { selection_coords.replace(Some((position, position))); widgets.image_widget().queue_draw(); } } pub fn drag_selection( widgets: &Widgets, image_list: Rc>, selection_coords: Rc>>, position: (u32, u32), ) { if let Some(((start_position_x, start_position_y), (_, _))) = selection_coords.get() { if let Some(current_image) = image_list.borrow().current_image() { let (position_x, position_y) = position; let (image_width, image_height) = current_image.preview_image_buffer_size().unwrap(); if position_x >= image_width || position_y >= image_height { return; } selection_coords.replace(Some(((start_position_x, start_position_y), position))); widgets.image_widget().queue_draw(); } } } pub fn end_selection( sender: &Sender, widgets: &Widgets, image_list: Rc>, selection_coords: Rc>>, ) { if let Some(selection_coords) = selection_coords.take() { if let Some(current_image) = image_list.borrow().current_image() { let crop_operation = ImageOperation::Crop( current_image .preview_coords_to_image_coords(selection_coords) .unwrap(), ); post_event(sender, Event::ImageEdit(crop_operation)); widgets.image_widget().queue_draw(); widgets.crop_button().set_active(false); } } } pub fn resize_popover_displayed(widgets: &Widgets, image_list: Rc>) { if let Some(current_image) = image_list.borrow().current_image() { let (image_width, image_height) = current_image.image_size().unwrap(); widgets.width_spin_button().set_value(image_width as f64); widgets.height_spin_button().set_value(image_height as f64); } } pub fn update_resize_popover_width(widgets: &Widgets, image_list: Rc>) { if let Some(current_image) = image_list.borrow().current_image() { let aspect_ratio = current_image.image_aspect_ratio().unwrap(); widgets .width_spin_button() .set_value(widgets.height_spin_button().value() * aspect_ratio); } } pub fn update_resize_popover_height(widgets: &Widgets, image_list: Rc>) { if let Some(current_image) = image_list.borrow().current_image() { let aspect_ratio = current_image.image_aspect_ratio().unwrap(); widgets .height_spin_button() .set_value(widgets.width_spin_button().value() / aspect_ratio); } } pub fn save_current_image( sender: &Sender, image_list: Rc>, filename: Option, ) { if let Err(error) = image_list.borrow_mut().save_current_image(filename) { post_event( sender, Event::DisplayMessage(error.to_string(), MessageType::Error), ); } } pub fn delete_current_image( sender: &Sender, file_list: &mut FileList, image_list: Rc>, ) { match file_list.delete_current_file() { Ok(image_path) => { image_list.borrow_mut().remove(image_path.as_path()); post_event( sender, Event::DisplayMessage( format!( "Image {} was moved to trash", image_path .file_name() .and_then(|file_name| file_name.to_str()) .unwrap_or_default() ), MessageType::Info, ), ) } Err(error) => post_event( sender, Event::DisplayMessage(error.to_string(), MessageType::Error), ), } } pub fn print(sender: &Sender, widgets: &Widgets, image_list: Rc>) { let print_operation = gtk::PrintOperation::new(); print_operation.connect_begin_print(move |print_operation, _| { print_operation.set_n_pages(1); }); let cloned_sender = sender.clone(); print_operation.connect_draw_page(move |_, print_context, _| { if let Some(print_image_buffer) = image_list .borrow() .current_image() .and_then(|current_image| { current_image.create_print_image_buffer( print_context.width() as u32, print_context.height() as u32, ) }) { let cairo_context = print_context.cairo_context(); cairo_context.set_source_pixbuf( &print_image_buffer, (print_context.width() - print_image_buffer.width() as f64) / 2.0, (print_context.height() - print_image_buffer.height() as f64) / 2.0, ); if let Err(error) = cairo_context.paint() { post_event( &cloned_sender, Event::DisplayMessage( format!("Couldn't print current image: {}", error), MessageType::Error, ), ); } } }); print_operation.set_allow_async(true); if let Err(error) = print_operation.run( gtk::PrintOperationAction::PrintDialog, Option::from(widgets.window()), ) { post_event( sender, Event::DisplayMessage( format!("Couldn't print current image: {}", error), MessageType::Error, ), ); }; } pub fn undo_operation( sender: &Sender, settings: &Settings, image_list: Rc>, ) { if let Some(current_image) = image_list.borrow_mut().current_image_mut() { current_image.undo_operation(); post_event(sender, Event::RefreshPreview(settings.scale())); } } pub fn redo_operation( sender: &Sender, settings: &Settings, image_list: Rc>, ) { if let Some(current_image) = image_list.borrow_mut().current_image_mut() { current_image.redo_operation(); post_event(sender, Event::RefreshPreview(settings.scale())); } } pub fn display_message(widgets: &Widgets, message: &str, message_type: gtk::MessageType) { match message_type { MessageType::Error => error!("{}", message), MessageType::Warning => warn!("{}", message), MessageType::Info => info!("{}", message), _ => info!("{}", message), }; widgets.info_bar().set_message_type(message_type); widgets.info_bar_text().set_text(message); widgets.info_bar().set_revealed(true); let main_context = glib::MainContext::default(); let info_bar = widgets.info_bar().clone(); main_context.spawn_local(async move { timeout_future_seconds(5).await; info_bar.set_revealed(false); }); } pub fn hide_info_panel(widgets: &Widgets) { if widgets.info_bar().message_type() != gtk::MessageType::Info && widgets.info_bar().message_type() != gtk::MessageType::Warning { widgets.info_bar().set_revealed(false); } } pub fn toggle_fullscreen(widgets: &Widgets, settings: &mut Settings) { if !settings.fullscreen() { widgets.window().fullscreen(); settings.set_fullscreen(true); } else { widgets.window().unfullscreen(); settings.set_fullscreen(false); } } pub fn quit(application: >k::Application) { application .windows() .iter() .for_each(|window| window.close()); } #[cfg(feature = "wallpaper")] pub fn set_as_wallpaper(sender: &Sender, file_list: &FileList) { if let Some(current_file_uri) = file_list.current_file_uri() { let sender = sender.clone(); let main_context = glib::MainContext::default(); main_context.spawn_local(async move { if let Err(error) = wallpaper::set_from_uri( &WindowIdentifier::default(), current_file_uri.as_str(), true, SetOn::Background, ) .await { post_event( &sender, Event::DisplayMessage(error.to_string(), MessageType::Error), ); } }); } } #[cfg(not(feature = "wallpaper"))] pub fn set_as_wallpaper(_sender: &Sender, _file_list: &FileList) { error!("This program was built without the wallpaper feature"); } pub fn copy_current_image(image_list: Rc>) { let display = gdk::Display::default().unwrap(); image_list.borrow().copy_current_image(display.clipboard()); } pub fn start_zoom_gesture(settings: &mut Settings) { settings.set_scale_before_zoom_gesture(Some(settings.scale())); } pub fn change_scale_on_zoom_gesture(sender: &Sender, settings: &Settings, zoom_scale: f64) { if let Some(scale_before_zoom_gesture) = settings.scale_before_zoom_gesture() { let new_preview_size = match scale_before_zoom_gesture { PreviewSize::BestFit(_, _) | PreviewSize::OriginalSize => { PreviewSize::Resized((zoom_scale * 100.0) as u32) } PreviewSize::Resized(old_scale) => { PreviewSize::Resized((old_scale as f64 * zoom_scale) as u32) } }; post_event(sender, Event::ChangePreviewSize(new_preview_size)); } } pub fn update_buttons_state( widgets: &Widgets, file_list: &FileList, image_list: Rc>, settings: &Settings, ) { let previous_next_active = file_list.len() > 1; widgets.next_button().set_sensitive(previous_next_active); widgets .previous_button() .set_sensitive(previous_next_active); let buttons_active = if let Some(current_image) = image_list.borrow().current_image() { widgets .undo_button() .set_sensitive(current_image.can_undo_operation()); widgets .redo_button() .set_sensitive(current_image.can_redo_operation()); widgets .save_menu_button() .set_sensitive(current_image.has_operations()); true } else { widgets.undo_button().set_sensitive(false); widgets.redo_button().set_sensitive(false); widgets.save_menu_button().set_sensitive(false); false }; widgets .rotate_counterclockwise_button() .set_sensitive(buttons_active); widgets .rotate_clockwise_button() .set_sensitive(buttons_active); widgets.crop_button().set_sensitive(buttons_active); widgets.resize_button().set_sensitive(buttons_active); widgets.print_menu_button().set_sensitive(buttons_active); widgets.save_as_menu_button().set_sensitive(buttons_active); widgets.delete_button().set_sensitive(buttons_active); #[cfg(feature = "wallpaper")] widgets .set_as_wallpaper_menu_button() .set_sensitive(buttons_active); #[cfg(not(feature = "wallpaper"))] widgets.set_as_wallpaper_menu_button().set_sensitive(false); widgets.copy_menu_button().set_sensitive(buttons_active); widgets .preview_smaller_button() .set_sensitive(settings.scale().can_be_smaller()); widgets .preview_larger_button() .set_sensitive(settings.scale().can_be_larger()); } ================================================ FILE: src/ui/controllers.rs ================================================ use gtk::EventControllerScrollFlags; #[derive(Clone)] pub struct Controllers { window_key_event_controller: gtk::EventControllerKey, image_click_gesture: gtk::GestureClick, image_motion_event_controller: gtk::EventControllerMotion, image_zoom_gesture: gtk::GestureZoom, image_scrolled_window_scroll_controller: gtk::EventControllerScroll, } impl Controllers { pub fn init() -> Self { Self { window_key_event_controller: gtk::EventControllerKey::new(), image_click_gesture: gtk::GestureClick::new(), image_motion_event_controller: gtk::EventControllerMotion::new(), image_zoom_gesture: gtk::GestureZoom::new(), image_scrolled_window_scroll_controller: gtk::EventControllerScroll::new( EventControllerScrollFlags::BOTH_AXES, ), } } pub fn image_click_gesture(&self) -> >k::GestureClick { &self.image_click_gesture } pub fn image_motion_event_controller(&self) -> >k::EventControllerMotion { &self.image_motion_event_controller } pub fn image_zoom_gesture(&self) -> >k::GestureZoom { &self.image_zoom_gesture } pub fn window_key_event_controller(&self) -> >k::EventControllerKey { &self.window_key_event_controller } pub fn image_scrolled_window_scroll_controller(&self) -> >k::EventControllerScroll { &self.image_scrolled_window_scroll_controller } } ================================================ FILE: src/ui/event.rs ================================================ use gtk::{ gdk::{self, Key}, gdk_pixbuf::PixbufRotation, gio, glib::{self, timeout_future, Sender}, prelude::{ ButtonExt, DrawingAreaExtManual, FileChooserExt, FileExt, GdkCairoContextExt, NativeDialogExt, PopoverExt, ToggleButtonExt, WidgetExt, }, traits::{GestureExt, GestureSingleExt, GtkWindowExt}, MessageType, Window, }; use std::{ cell::{Cell, RefCell}, path::PathBuf, rc::Rc, time::Duration, }; use crate::{ image::{CoordinatesPair, PreviewSize}, image_list::ImageList, image_operation::ImageOperation, settings::Settings, }; use super::{controllers::Controllers, widgets::Widgets}; #[derive(Debug)] pub enum Event { OpenFile(gio::File), LoadImage(Option), ImageViewportResize((u32, u32)), RefreshPreview(PreviewSize), ChangePreviewSize(PreviewSize), ImageEdit(ImageOperation), StartSelection((u32, u32)), DragSelection((u32, u32)), SaveCurrentImage(Option), DeleteCurrentImage, EndSelection, StartZoomGesture, ZoomGestureScaleChanged(f64), PreviewSmaller(Option), PreviewLarger(Option), PreviewFitScreen, NextImage, PreviousImage, RefreshFileList, ResizePopoverDisplayed, UpdateResizePopoverWidth, UpdateResizePopoverHeight, UndoOperation, RedoOperation, Print, DisplayMessage(String, gtk::MessageType), HideInfoPanel, ToggleFullscreen, CopyCurrentImage, Quit, SetAsWallpaper, } pub fn post_event(sender: &glib::Sender, action: Event) { if let Err(err) = sender.send(action) { error!("Send error: {}", err); } } pub fn connect_events( widgets: Widgets, sender: Sender, image_list: Rc>, selection_coords: Rc>>, settings: Settings, ) { connect_open_menu_button_clicked(widgets.clone(), sender.clone()); connect_next_button_clicked(widgets.clone(), sender.clone()); connect_previous_button_clicked(widgets.clone(), sender.clone()); connect_window_default_width_notify(widgets.clone(), settings.clone(), sender.clone()); connect_window_default_height_notify(widgets.clone(), settings, sender.clone()); connect_window_maximized_notify(widgets.clone(), sender.clone()); connect_window_fullscreened_notify(widgets.clone(), sender.clone()); connect_preview_smaller_button_clicked(widgets.clone(), sender.clone()); connect_preview_larger_button_clicked(widgets.clone(), sender.clone()); connect_preview_fit_screen_button_clicked(widgets.clone(), sender.clone()); connect_rotate_counterclockwise_button_clicked(widgets.clone(), sender.clone()); connect_rotate_clockwise_button_clicked(widgets.clone(), sender.clone()); connect_image_widget_draw(widgets.clone(), image_list.clone(), selection_coords); connect_resize_button_activated(widgets.clone(), sender.clone()); connect_width_spin_button_value_changed(widgets.clone(), sender.clone()); connect_height_spin_button_value_changed(widgets.clone(), sender.clone()); connect_apply_resize_button_clicked(widgets.clone(), sender.clone()); connect_save_menu_button_clicked(widgets.clone(), sender.clone()); connect_print_menu_button_clicked(widgets.clone(), sender.clone()); connect_undo_button_clicked(widgets.clone(), sender.clone()); connect_redo_button_clicked(widgets.clone(), sender.clone()); connect_save_as_menu_button_clicked(widgets.clone(), image_list, sender.clone()); connect_delete_button_clicked(widgets.clone(), sender.clone()); connect_info_bar_response(widgets.clone()); connect_set_as_wallpaper_menu_button_clicked(widgets.clone(), sender.clone()); connect_copy_menu_button_clicked(widgets.clone(), sender); widgets.window().present(); } pub fn connect_controllers(sender: Sender, widgets: Widgets, controllers: Controllers) { controllers .image_click_gesture() .set_button(gtk::gdk::BUTTON_PRIMARY); connect_controllers_to_widgets(widgets.clone(), controllers.clone()); connect_keybinds(controllers.clone(), widgets, sender.clone()); connect_image_click_pressed_gesture(controllers.clone(), sender.clone()); connect_image_motion_event_controller_motion(controllers.clone(), sender.clone()); connect_image_click_released_gesture(controllers.clone(), sender.clone()); connect_zoom_gesture_begin(controllers.clone(), sender.clone()); connect_zoom_gesture_scale_changed(controllers.clone(), sender.clone()); connect_image_scrolled_window_scroll_controller_scroll(controllers, sender); } fn connect_controllers_to_widgets(widgets: Widgets, controllers: Controllers) { widgets .window() .add_controller(controllers.window_key_event_controller()); widgets .image_widget() .add_controller(controllers.image_click_gesture()); widgets .image_widget() .add_controller(controllers.image_motion_event_controller()); widgets .image_widget() .add_controller(controllers.image_zoom_gesture()); widgets .image_scrolled_window() .add_controller(controllers.image_scrolled_window_scroll_controller()); } pub fn connect_keybinds(controllers: Controllers, widgets: Widgets, sender: Sender) { controllers .window_key_event_controller() .connect_key_pressed(move |_, key, _, state| { match key { Key::F11 => post_event(&sender, Event::ToggleFullscreen), Key::Left | Key::h => { if widgets.previous_button().is_sensitive() { widgets.previous_button().emit_clicked(); } } Key::Right | Key::l => { if widgets.next_button().is_sensitive() { widgets.next_button().emit_clicked(); } } Key::minus | Key::KP_Subtract => { if widgets.preview_smaller_button().is_sensitive() { widgets.preview_smaller_button().emit_clicked(); } } Key::plus | Key::KP_Add => { if widgets.preview_larger_button().is_sensitive() { widgets.preview_larger_button().emit_clicked(); } } Key::f => { if widgets.preview_fit_screen_button().is_sensitive() { widgets.preview_fit_screen_button().emit_clicked(); } } Key::Delete => { if widgets.delete_button().is_sensitive() { widgets.delete_button().emit_clicked(); } } Key::S if state == (gdk::ModifierType::SHIFT_MASK | gdk::ModifierType::CONTROL_MASK) => { if widgets.save_as_menu_button().is_sensitive() { widgets.save_as_menu_button().emit_clicked(); } } Key::R if state == (gdk::ModifierType::SHIFT_MASK | gdk::ModifierType::CONTROL_MASK) => { if widgets.rotate_counterclockwise_button().is_sensitive() { widgets.rotate_counterclockwise_button().emit_clicked(); } } Key::C if state == gdk::ModifierType::SHIFT_MASK => { if widgets.crop_button().is_sensitive() { widgets.crop_button().emit_clicked(); } } Key::S if state == gdk::ModifierType::SHIFT_MASK => { if widgets.resize_button().is_sensitive() { widgets.resize_button().emit_activate(); } } Key::q if state == gdk::ModifierType::CONTROL_MASK => { post_event(&sender, Event::Quit) } Key::o if state == gdk::ModifierType::CONTROL_MASK => { widgets.open_menu_button().emit_clicked(); } Key::s if state == gdk::ModifierType::CONTROL_MASK => { if widgets.save_menu_button().is_sensitive() { widgets.save_menu_button().emit_clicked(); } } Key::c if state == gdk::ModifierType::CONTROL_MASK => { if widgets.copy_menu_button().is_sensitive() { widgets.copy_menu_button().emit_clicked(); } } Key::p if state == gdk::ModifierType::CONTROL_MASK => { if widgets.print_menu_button().is_sensitive() { widgets.print_menu_button().emit_clicked(); } } Key::z if state == gdk::ModifierType::CONTROL_MASK => { if widgets.undo_button().is_sensitive() { widgets.undo_button().emit_clicked(); } } Key::y if state == gdk::ModifierType::CONTROL_MASK => { if widgets.redo_button().is_sensitive() { widgets.redo_button().emit_clicked(); } } Key::r if state == gdk::ModifierType::CONTROL_MASK => { if widgets.rotate_clockwise_button().is_sensitive() { widgets.rotate_clockwise_button().emit_clicked(); } } Key::j if state == gdk::ModifierType::CONTROL_MASK => { if widgets.preview_smaller_button().is_sensitive() { widgets.preview_smaller_button().emit_clicked(); } } Key::k if state == gdk::ModifierType::CONTROL_MASK => { if widgets.preview_larger_button().is_sensitive() { widgets.preview_larger_button().emit_clicked(); } } _ => {} } gtk::Inhibit(false) }); } fn connect_open_menu_button_clicked(widgets: Widgets, sender: Sender) { widgets .clone() .open_menu_button() .connect_clicked(move |_| { widgets.popover_menu().popdown(); let file_chooser = gtk::FileChooserNative::new( Some("Open file"), gtk::Window::NONE, gtk::FileChooserAction::Open, None, None, ); file_chooser.set_transient_for(Some(widgets.window())); let file_filter = gtk::FileFilter::new(); file_filter.add_mime_type("image/*"); file_filter.set_name(Some("Image")); file_chooser.add_filter(&file_filter); let sender = sender.clone(); file_chooser.connect_response(move |file_chooser, response| { if response == gtk::ResponseType::Accept { let file = if let Some(file) = file_chooser.file() { file } else { post_event( &sender, Event::DisplayMessage( String::from("Couldn't load file"), MessageType::Error, ), ); return; }; post_event(&sender, Event::OpenFile(file)); } file_chooser.destroy(); }); file_chooser.show(); widgets.file_chooser().replace(Some(file_chooser)); }); } fn connect_next_button_clicked(widgets: Widgets, sender: Sender) { widgets.next_button().connect_clicked(move |_| { post_event(&sender, Event::NextImage); }); } fn connect_previous_button_clicked(widgets: Widgets, sender: Sender) { widgets.previous_button().connect_clicked(move |_| { post_event(&sender, Event::PreviousImage); }); } fn connect_window_default_width_notify( widgets: Widgets, settings: Settings, sender: Sender, ) { widgets .clone() .window() .connect_default_width_notify(move |window| { settings.set_window_size((window.width() as u32, window.height() as u32)); post_event( &sender, Event::ImageViewportResize(( widgets.image_viewport().allocation().width() as u32, widgets.image_viewport().allocation().height() as u32, )), ); }); } fn connect_window_default_height_notify( widgets: Widgets, settings: Settings, sender: Sender, ) { widgets .clone() .window() .connect_default_height_notify(move |window| { settings.set_window_size((window.width() as u32, window.height() as u32)); post_event( &sender, Event::ImageViewportResize(( widgets.image_viewport().allocation().width() as u32, widgets.image_viewport().allocation().height() as u32, )), ); }); } fn connect_window_fullscreened_notify(widgets: Widgets, sender: Sender) { widgets .clone() .window() .connect_fullscreened_notify(move |_| { let main_context = glib::MainContext::default(); let sender = sender.clone(); let widgets = widgets.clone(); main_context.spawn_local(async move { timeout_future(Duration::from_millis(5)).await; post_event( &sender, Event::ImageViewportResize(( widgets.image_viewport().allocation().width() as u32, widgets.image_viewport().allocation().height() as u32, )), ); }); }); } fn connect_window_maximized_notify(widgets: Widgets, sender: Sender) { widgets.clone().window().connect_maximized_notify(move |_| { let main_context = glib::MainContext::default(); let sender = sender.clone(); let widgets = widgets.clone(); main_context.spawn_local(async move { timeout_future(Duration::from_millis(5)).await; post_event( &sender, Event::ImageViewportResize(( widgets.image_viewport().allocation().width() as u32, widgets.image_viewport().allocation().height() as u32, )), ); }); }); } fn connect_preview_smaller_button_clicked(widgets: Widgets, sender: Sender) { widgets.preview_smaller_button().connect_clicked(move |_| { post_event(&sender, Event::PreviewSmaller(None)); }); } fn connect_preview_larger_button_clicked(widgets: Widgets, sender: Sender) { widgets.preview_larger_button().connect_clicked(move |_| { post_event(&sender, Event::PreviewLarger(None)); }); } fn connect_preview_fit_screen_button_clicked(widgets: Widgets, sender: Sender) { widgets .preview_fit_screen_button() .connect_clicked(move |_| { post_event(&sender, Event::PreviewFitScreen); }); } fn connect_rotate_counterclockwise_button_clicked(widgets: Widgets, sender: Sender) { widgets .rotate_counterclockwise_button() .connect_clicked(move |_| { post_event( &sender, Event::ImageEdit(ImageOperation::Rotate(PixbufRotation::Counterclockwise)), ); }); } fn connect_rotate_clockwise_button_clicked(widgets: Widgets, sender: Sender) { widgets.rotate_clockwise_button().connect_clicked(move |_| { post_event( &sender, Event::ImageEdit(ImageOperation::Rotate(PixbufRotation::Clockwise)), ); }); } fn connect_image_click_pressed_gesture(controllers: Controllers, sender: Sender) { controllers .image_click_gesture() .connect_pressed(move |_, _, x, y| { post_event(&sender, Event::StartSelection((x as u32, y as u32))); }); } fn connect_image_motion_event_controller_motion(controllers: Controllers, sender: Sender) { controllers .image_motion_event_controller() .connect_motion(move |_, x, y| { post_event(&sender, Event::DragSelection((x as u32, y as u32))); }); } fn connect_image_click_released_gesture(controllers: Controllers, sender: Sender) { controllers .image_click_gesture() .connect_released(move |_, _, _, _| { post_event(&sender, Event::EndSelection); }); } fn connect_image_widget_draw( widgets: Widgets, image_list: Rc>, selection_coords: Rc>>, ) { widgets .image_widget() .set_draw_func(move |_, cairo_context, _, _| { if let Some(current_image) = image_list.borrow().current_image() { if let Some(image_buffer) = current_image.preview_image_buffer() { cairo_context.set_source_pixbuf(image_buffer, 0.0, 0.0); if let Err(error) = cairo_context.paint() { error!("{}", error); return; } if let Some(( (start_selection_coord_x, start_selection_coord_y), (end_selection_coord_x, end_selection_coord_y), )) = selection_coords.get() { cairo_context.set_source_rgb(0.0, 0.0, 0.0); cairo_context.set_line_width(1.0); cairo_context.rectangle( start_selection_coord_x as f64, start_selection_coord_y as f64, (end_selection_coord_x as i32 - start_selection_coord_x as i32) as f64, (end_selection_coord_y as i32 - start_selection_coord_y as i32) as f64, ); if let Err(error) = cairo_context.stroke() { error!("{}", error); } } } } }); } fn connect_resize_button_activated(widgets: Widgets, sender: Sender) { widgets.resize_button().connect_activate(move |_| { post_event(&sender, Event::ResizePopoverDisplayed); }); } fn connect_width_spin_button_value_changed(widgets: Widgets, sender: Sender) { widgets .clone() .width_spin_button() .connect_value_changed(move |_| { if widgets.link_aspect_ratio_button().is_active() { post_event(&sender, Event::UpdateResizePopoverHeight); } }); } fn connect_height_spin_button_value_changed(widgets: Widgets, sender: Sender) { widgets .clone() .height_spin_button() .connect_value_changed(move |_| { if widgets.link_aspect_ratio_button().is_active() { post_event(&sender, Event::UpdateResizePopoverWidth); } }); } fn connect_apply_resize_button_clicked(widgets: Widgets, sender: Sender) { widgets .clone() .apply_resize_button() .connect_clicked(move |_| { post_event( &sender, Event::ImageEdit(ImageOperation::Resize(( widgets.width_spin_button().value() as u32, widgets.height_spin_button().value() as u32, ))), ); widgets.resize_button().popdown(); }); } fn connect_save_menu_button_clicked(widgets: Widgets, sender: Sender) { widgets .clone() .save_menu_button() .connect_clicked(move |_| { widgets.popover_menu().popdown(); post_event(&sender, Event::SaveCurrentImage(None)); }); } fn connect_save_as_menu_button_clicked( widgets: Widgets, image_list: Rc>, sender: Sender, ) { widgets .clone() .save_as_menu_button() .connect_clicked(move |_| { widgets.popover_menu().popdown(); let file_chooser = gtk::FileChooserNative::new( Some("Save as..."), >::None, gtk::FileChooserAction::Save, None, None, ); file_chooser.set_transient_for(Some(widgets.window())); if let Some(file_path) = image_list.borrow().current_image_path() { if let Err(error) = file_chooser.set_file(&gio::File::for_path(file_path)) { post_event( &sender, Event::DisplayMessage(error.to_string(), MessageType::Warning), ); } } let file_filter = gtk::FileFilter::new(); file_filter.add_mime_type("image/*"); file_filter.set_name(Some("Image")); file_chooser.add_filter(&file_filter); let sender = sender.clone(); file_chooser.connect_response(move |file_chooser, response| { if response == gtk::ResponseType::Accept { let file = if let Some(file) = file_chooser.file() { file } else { post_event( &sender, Event::DisplayMessage( String::from("Couldn't save file"), MessageType::Error, ), ); return; }; post_event(&sender, Event::SaveCurrentImage(Some(file.path().unwrap()))); } file_chooser.destroy(); }); file_chooser.show(); widgets.file_chooser().replace(Some(file_chooser)); }); } fn connect_print_menu_button_clicked(widgets: Widgets, sender: Sender) { widgets .clone() .print_menu_button() .connect_clicked(move |_| { widgets.popover_menu().popdown(); post_event(&sender, Event::Print); }); } fn connect_undo_button_clicked(widgets: Widgets, sender: Sender) { widgets.undo_button().connect_clicked(move |_| { post_event(&sender, Event::UndoOperation); }); } fn connect_redo_button_clicked(widgets: Widgets, sender: Sender) { widgets.redo_button().connect_clicked(move |_| { post_event(&sender, Event::RedoOperation); }); } fn connect_delete_button_clicked(widgets: Widgets, sender: Sender) { widgets.delete_button().connect_clicked(move |_| { post_event(&sender, Event::DeleteCurrentImage); }); } fn connect_info_bar_response(widgets: Widgets) { widgets.info_bar().connect_response(|info_bar, response| { if response == gtk::ResponseType::Close { info_bar.set_revealed(false); } }); } fn connect_image_scrolled_window_scroll_controller_scroll( controllers: Controllers, sender: Sender, ) { controllers .image_scrolled_window_scroll_controller() .connect_scroll(move |_, _, y| { if y < 0.0 { post_event(&sender, Event::PreviewLarger(Some(5))); } if y > 0.0 { post_event(&sender, Event::PreviewSmaller(Some(5))); } gtk::Inhibit(true) }); } fn connect_set_as_wallpaper_menu_button_clicked(widgets: Widgets, sender: Sender) { widgets .set_as_wallpaper_menu_button() .connect_clicked(move |_| { post_event(&sender, Event::SetAsWallpaper); }); } fn connect_copy_menu_button_clicked(widgets: Widgets, sender: Sender) { widgets .clone() .copy_menu_button() .connect_clicked(move |_| { widgets.popover_menu().popdown(); post_event(&sender, Event::CopyCurrentImage); }); } fn connect_zoom_gesture_begin(controllers: Controllers, sender: Sender) { controllers.image_zoom_gesture().connect_begin(move |_, _| { post_event(&sender, Event::StartZoomGesture); }); } fn connect_zoom_gesture_scale_changed(controllers: Controllers, sender: Sender) { controllers .image_zoom_gesture() .connect_scale_changed(move |_, scale| { post_event(&sender, Event::ZoomGestureScaleChanged(scale)); }); } ================================================ FILE: src/ui/widgets.rs ================================================ use std::cell::RefCell; use gtk::{ prelude::{GtkWindowExt, WidgetExt}, ApplicationWindow, Builder, }; #[derive(Clone)] pub struct Widgets { window: ApplicationWindow, open_menu_button: gtk::Button, image_widget: gtk::DrawingArea, popover_menu: gtk::PopoverMenu, next_button: gtk::Button, previous_button: gtk::Button, preview_smaller_button: gtk::Button, preview_larger_button: gtk::Button, image_scrolled_window: gtk::ScrolledWindow, image_viewport: gtk::Viewport, preview_size_label: gtk::Label, rotate_counterclockwise_button: gtk::Button, rotate_clockwise_button: gtk::Button, crop_button: gtk::ToggleButton, resize_button: gtk::MenuButton, width_spin_button: gtk::SpinButton, height_spin_button: gtk::SpinButton, link_aspect_ratio_button: gtk::ToggleButton, apply_resize_button: gtk::Button, info_bar: gtk::InfoBar, info_bar_text: gtk::Label, save_menu_button: gtk::Button, print_menu_button: gtk::Button, undo_button: gtk::Button, redo_button: gtk::Button, save_as_menu_button: gtk::Button, preview_fit_screen_button: gtk::Button, delete_button: gtk::Button, copy_menu_button: gtk::Button, set_as_wallpaper_menu_button: gtk::Button, file_chooser: RefCell>, } impl Widgets { pub fn init(builder: Builder, application: >k::Application) -> Self { let window: ApplicationWindow = builder .object("main_window") .expect("Couldn't get main_window"); window.set_application(Some(application)); let open_menu_button: gtk::Button = builder .object("open_menu_button") .expect("Couldn't get open_menu_button"); let image_widget: gtk::DrawingArea = builder .object("image_widget") .expect("Couldn't get image_widget"); let popover_menu: gtk::PopoverMenu = builder .object("popover_menu") .expect("Couldn't get popover_menu"); let next_button: gtk::Button = builder .object("next_button") .expect("Couldn't get next_button"); let previous_button: gtk::Button = builder .object("previous_button") .expect("Couldn't get previous_button"); let preview_smaller_button: gtk::Button = builder .object("preview_smaller_button") .expect("Couldn't get preview_smaller_button"); let preview_larger_button: gtk::Button = builder .object("preview_larger_button") .expect("Couldn't get preview_larger_button"); let image_scrolled_window: gtk::ScrolledWindow = builder .object("image_scrolled_window") .expect("Couldn't get image_scrolled_window"); let image_viewport: gtk::Viewport = builder .object("image_viewport") .expect("Couldn't get image_viewport"); let preview_size_label: gtk::Label = builder .object("preview_size_label") .expect("Couldn't get preview_size_label"); let rotate_counterclockwise_button: gtk::Button = builder .object("rotate_counterclockwise_button") .expect("Couldn't get rotate_counterclockwise_button"); let rotate_clockwise_button: gtk::Button = builder .object("rotate_clockwise_button") .expect("Couldn't get rotate_clockwise_button"); let crop_button: gtk::ToggleButton = builder .object("crop_button") .expect("Couldn't get crop_button"); let resize_button: gtk::MenuButton = builder .object("resize_button") .expect("Couldn't get resize_button"); resize_button.set_sensitive(false); let width_spin_button: gtk::SpinButton = builder .object("width_spin_button") .expect("Couldn't get width_spin_button"); let height_spin_button: gtk::SpinButton = builder .object("height_spin_button") .expect("Couldn't get height_spin_button"); let link_aspect_ratio_button: gtk::ToggleButton = builder .object("link_aspect_ratio_button") .expect("Couldn't get link_aspect_ratio_button"); let apply_resize_button: gtk::Button = builder .object("apply_resize_button") .expect("Couldn't get apply_resize_button"); let error_info_bar: gtk::InfoBar = builder .object("error_info_bar") .expect("Couldn't get error_info_bar"); let error_info_bar_text: gtk::Label = builder .object("error_info_bar_text") .expect("Couldn't get error_info_bar_text"); let save_menu_button: gtk::Button = builder .object("save_menu_button") .expect("Couldn't get save_menu_button"); let print_menu_button: gtk::Button = builder .object("print_menu_button") .expect("Couldn't get print_menu_button"); let undo_button: gtk::Button = builder .object("undo_button") .expect("Couldn't get undo_button"); let redo_button: gtk::Button = builder .object("redo_button") .expect("Couldn't get redo_button"); let save_as_menu_button: gtk::Button = builder .object("save_as_menu_button") .expect("Couldn't get save_as_menu_button"); let preview_fit_screen_button: gtk::Button = builder .object("preview_fit_screen_button") .expect("Couldn't get preview_fit_screen_button"); let delete_button: gtk::Button = builder .object("delete_button") .expect("Couldn't get delete_button"); delete_button.add_css_class("destructive-action"); let copy_menu_button: gtk::Button = builder .object("copy_menu_button") .expect("Couldn't get copy_menu_button"); let set_as_wallpaper_menu_button: gtk::Button = builder .object("set_as_wallpaper_menu_button") .expect("Couldn't get set_as_wallpaper_menu_button"); Self { window, open_menu_button, image_widget, popover_menu, next_button, previous_button, preview_smaller_button, preview_larger_button, image_scrolled_window, image_viewport, preview_size_label, rotate_counterclockwise_button, rotate_clockwise_button, crop_button, resize_button, width_spin_button, height_spin_button, link_aspect_ratio_button, apply_resize_button, info_bar: error_info_bar, info_bar_text: error_info_bar_text, save_menu_button, print_menu_button, undo_button, redo_button, save_as_menu_button, preview_fit_screen_button, delete_button, copy_menu_button, set_as_wallpaper_menu_button, file_chooser: RefCell::new(None), } } /// Get a reference to the widgets's window. pub fn window(&self) -> &ApplicationWindow { &self.window } /// Get a reference to the widgets's open menu button. pub fn open_menu_button(&self) -> >k::Button { &self.open_menu_button } /// Get a reference to the widgets's image widget. pub fn image_widget(&self) -> >k::DrawingArea { &self.image_widget } /// Get a reference to the widgets's popover menu. pub fn popover_menu(&self) -> >k::PopoverMenu { &self.popover_menu } /// Get a reference to the widgets's next button. pub fn next_button(&self) -> >k::Button { &self.next_button } /// Get a reference to the widgets's previous button. pub fn previous_button(&self) -> >k::Button { &self.previous_button } /// Get a reference to the widgets's preview smaller button. pub fn preview_smaller_button(&self) -> >k::Button { &self.preview_smaller_button } /// Get a reference to the widgets's preview larger button. pub fn preview_larger_button(&self) -> >k::Button { &self.preview_larger_button } /// Get a reference to the widgets's image viewport. pub fn image_viewport(&self) -> >k::Viewport { &self.image_viewport } /// Get a reference to the widgets's rotate counterclockwise button. pub fn rotate_counterclockwise_button(&self) -> >k::Button { &self.rotate_counterclockwise_button } /// Get a reference to the widgets's rotate clockwise button. pub fn rotate_clockwise_button(&self) -> >k::Button { &self.rotate_clockwise_button } /// Get a reference to the widgets's crop button. pub fn crop_button(&self) -> >k::ToggleButton { &self.crop_button } /// Get a reference to the widgets's resize button. pub fn resize_button(&self) -> >k::MenuButton { &self.resize_button } /// Get a reference to the widgets's width spin button. pub fn width_spin_button(&self) -> >k::SpinButton { &self.width_spin_button } /// Get a reference to the widgets's height spin button. pub fn height_spin_button(&self) -> >k::SpinButton { &self.height_spin_button } /// Get a reference to the widgets's link aspect ratio button. pub fn link_aspect_ratio_button(&self) -> >k::ToggleButton { &self.link_aspect_ratio_button } /// Get a reference to the widgets's apply resize button. pub fn apply_resize_button(&self) -> >k::Button { &self.apply_resize_button } /// Get a reference to the widgets's error info bar. pub fn info_bar(&self) -> >k::InfoBar { &self.info_bar } /// Get a reference to the widgets's error info bar text. pub fn info_bar_text(&self) -> >k::Label { &self.info_bar_text } /// Get a reference to the widgets's save menu button. pub fn save_menu_button(&self) -> >k::Button { &self.save_menu_button } /// Get a reference to the widgets's print menu button. pub fn print_menu_button(&self) -> >k::Button { &self.print_menu_button } /// Get a reference to the widgets's undo button. pub fn undo_button(&self) -> >k::Button { &self.undo_button } /// Get a reference to the widgets's redo button. pub fn redo_button(&self) -> >k::Button { &self.redo_button } /// Get a reference to the widgets's save as menu button. pub fn save_as_menu_button(&self) -> >k::Button { &self.save_as_menu_button } /// Get a reference to the widgets's preview fit screen button. pub fn preview_fit_screen_button(&self) -> >k::Button { &self.preview_fit_screen_button } /// Get a reference to the widgets's delete button. pub fn delete_button(&self) -> >k::Button { &self.delete_button } /// Get a reference to the widgets's preview size label. pub fn preview_size_label(&self) -> >k::Label { &self.preview_size_label } /// Get a reference to the widgets's image scrolled window. pub fn image_scrolled_window(&self) -> >k::ScrolledWindow { &self.image_scrolled_window } /// Get a reference to the widgets's set as wallpaper menu button. pub fn set_as_wallpaper_menu_button(&self) -> >k::Button { &self.set_as_wallpaper_menu_button } /// Get a reference to the widget's copy menu button pub fn copy_menu_button(&self) -> >k::Button { &self.copy_menu_button } pub fn file_chooser(&self) -> &RefCell> { &self.file_chooser } } ================================================ FILE: src/ui.rs ================================================ pub mod action; pub mod controllers; pub mod event; pub mod widgets;