diff --git a/data/gtk/help_overlay.ui b/data/gtk/help_overlay.ui index 9a5c3e6292015a26f5a5ce588ba1a2e4f39bf02e..dd4d60ac4c719ce88f1ccffa02fdd34f1d154f9d 100644 --- a/data/gtk/help_overlay.ui +++ b/data/gtk/help_overlay.ui @@ -102,6 +102,18 @@ Right + + + First Image + win.first + + + + + Last Image + win.last + + diff --git a/data/gtk/image_page.ui b/data/gtk/image_page.ui index 86f3a2400ab234a96bed103a35e4e454385b3fad..6ad4d93e411b3db0da5a459f4c5196de1945993d 100644 --- a/data/gtk/image_page.ui +++ b/data/gtk/image_page.ui @@ -63,7 +63,7 @@ - + 3 diff --git a/src/application.rs b/src/application.rs index 164481f747df4e0fca21aba71a119bcdebd3549f..1309892df24d2182c2ab0a4cc7f35fbaf6dac1f8 100644 --- a/src/application.rs +++ b/src/application.rs @@ -180,8 +180,10 @@ impl LpApplication { self.set_accels_for_action("win.toggle-fullscreen", &["F11"]); self.set_accels_for_action("win.set-background", &["F8"]); - self.set_accels_for_action("win.previous", &["Left"]); - self.set_accels_for_action("win.next", &["Right"]); + self.set_accels_for_action("win.image-left", &["Left", "Page_Down"]); + self.set_accels_for_action("win.image-right", &["Right", "Page_Up"]); + self.set_accels_for_action("win.first", &["Home"]); + self.set_accels_for_action("win.last", &["End"]); self.set_accels_for_action("win.zoom-to(1.0)", &["1", "KP_1", "1", "KP_1"]); self.set_accels_for_action("win.zoom-to(2.0)", &["2", "KP_2", "2", "KP_2"]); diff --git a/src/file_model.rs b/src/file_model.rs index e701c3d6f6ffcc81ea0d91600e2fee618b102b30..f3b2dc0fc00d579a31d4f793beae5a5234b17388 100644 --- a/src/file_model.rs +++ b/src/file_model.rs @@ -207,6 +207,16 @@ impl LpFileModel { .collect() } + /// Return first path + pub fn first(&self) -> Option { + self.imp().files.borrow().first().cloned() + } + + /// Returns last path + pub fn last(&self) -> Option { + self.imp().files.borrow().last().cloned() + } + /// Currently sorts by name fn sort(files: &mut IndexSet) { files.sort_by(|x, y| util::compare_by_name(x, y)); diff --git a/src/util.rs b/src/util.rs index 22a9ec37aad8ba190288d4fbbc17349b5036453b..3c94ac76478fa0e892327d3ec16ce9fed2d290f3 100644 --- a/src/util.rs +++ b/src/util.rs @@ -123,3 +123,15 @@ pub async fn spawn( .spawn(async_global_executor::spawn_blocking(f))? .await) } + +#[derive(Debug, Clone, Copy)] +pub enum Position { + First, + Last, +} + +#[derive(Debug, Clone, Copy)] +pub enum Direction { + Back, + Forward, +} diff --git a/src/widgets/image.rs b/src/widgets/image.rs index 8109c37a5f5c4052b94878616b7b209c4018e95b..04bb93d9b3637921d8cb1462d723381438cf7983 100644 --- a/src/widgets/image.rs +++ b/src/widgets/image.rs @@ -30,13 +30,27 @@ use once_cell::unsync::OnceCell; use std::cell::{Cell, RefCell}; use std::path::{Path, PathBuf}; +/// Milliseconds const ZOOM_ANIMATION_DURATION: u32 = 200; +/// Milliseconds const ROTATION_ANIMATION_DURATION: u32 = 200; +/// Relative to current zoom level const ZOOM_FACTOR_BUTTON: f64 = 1.5; +/// Relative to current zoom level const ZOOM_FACTOR_WHEEL: f64 = 1.3; -const ZOOM_FACTOR_WHEEL_HI_RES: f64 = 0.1; +/// Relative to current zoom level +const ZOOM_FACTOR_WHEEL_HI_RES: f64 = 0.001; +/// Relative to best-fit level +const ZOOM_FACTOR_DOUBLE_TAP: f64 = 2.5; + +/// Relative to best-fit and `MAX_ZOOM_LEVEL` +const ZOOM_FACTOR_MAX_RUBBERBAND: f64 = 2.; +/// Smaller values make the band feel stiffer +const RUBBERBANDING_EXPONENT: f64 = 0.4; + +/// Max zoom level 2000% const MAX_ZOOM_LEVEL: f64 = 20.0; mod imp { @@ -304,6 +318,33 @@ mod imp { fn connect_gestures(&self) { let obj = self.instance(); + // Double click for fullscreen (mouse/touchpad) or zoom (touch screen) + let left_click_gesture = gtk::GestureClick::builder().button(1).build(); + obj.add_controller(&left_click_gesture); + left_click_gesture.connect_pressed( + glib::clone!(@weak obj => move |gesture, n_press, x, y| { + // only handle double clicks + if n_press != 2 { + return; + } + + if gesture.device().map(|x| x.source()) == Some(gdk::InputSource::Touchscreen) { + // zoom + obj.imp().pointer_position.set(Some((x, y))); + if obj.is_best_fit() { + // zoom in + obj.zoom_to(ZOOM_FACTOR_DOUBLE_TAP * obj.zoom_level_best_fit()); + } else { + // zoom back out + obj.zoom_best_fit(); + } + } else { + // fullscreen + obj.activate_action("win.toggle-fullscreen", None).unwrap(); + } + }), + ); + // Drag for moving image around let drag_gesture = gtk::GestureDrag::new(); obj.add_controller(&drag_gesture); @@ -837,8 +878,29 @@ impl LpImage { } /// Set zoom level aiming for given position or center if not available - fn set_zoom_aiming(&self, zoom: f64, aiming: Option<(f64, f64)>) { - if zoom == self.zoom() || zoom <= 0. { + fn set_zoom_aiming(&self, mut zoom: f64, aiming: Option<(f64, f64)>) { + // allow some deviantion from max value for rubberbanding + if zoom > MAX_ZOOM_LEVEL { + let max_deviation = MAX_ZOOM_LEVEL * ZOOM_FACTOR_MAX_RUBBERBAND; + let deviation = zoom / MAX_ZOOM_LEVEL; + zoom = f64::min( + MAX_ZOOM_LEVEL * deviation.powf(RUBBERBANDING_EXPONENT), + max_deviation, + ); + } + + if zoom < self.zoom_level_best_fit() { + let minimum = self.zoom_level_best_fit(); + let max_deviation = minimum / ZOOM_FACTOR_MAX_RUBBERBAND; + let deviation = zoom / minimum; + zoom = f64::max( + minimum * deviation.powf(RUBBERBANDING_EXPONENT), + max_deviation, + ); + dbg!(zoom); + } + + if zoom == self.zoom() { return; } diff --git a/src/widgets/image_page.rs b/src/widgets/image_page.rs index 724583ed675d1f0a9427bf5e403d76336a4ac903..1c1f1fed068ce6d46f22e0cb33f1861a5833278c 100644 --- a/src/widgets/image_page.rs +++ b/src/widgets/image_page.rs @@ -49,7 +49,7 @@ mod imp { #[template_child] pub(super) popover: TemplateChild, #[template_child] - pub(super) click_gesture: TemplateChild, + pub(super) right_click_gesture: TemplateChild, #[template_child] pub(super) press_gesture: TemplateChild, @@ -100,7 +100,7 @@ mod imp { self.parent_constructed(); - self.click_gesture + self.right_click_gesture .connect_pressed(clone!(@weak obj => move |gesture, _, x, y| { obj.show_popover_at(x, y); gesture.set_state(gtk::EventSequenceState::Claimed); diff --git a/src/widgets/image_view.rs b/src/widgets/image_view.rs index cb2bafe366505537625a39c99c4d86f53a8e0e7c..9ce9dfb0196635a9797d591870f8af0fcfd344d9 100644 --- a/src/widgets/image_view.rs +++ b/src/widgets/image_view.rs @@ -21,6 +21,7 @@ use crate::deps::*; use crate::file_model::LpFileModel; use crate::i18n::*; use crate::thumbnail::Thumbnail; +use crate::util::{Direction, Position}; use crate::widgets::{LpImage, LpImagePage, LpSlidingView}; use adw::prelude::*; @@ -270,12 +271,11 @@ impl LpImageView { } /// Move forward or backwards - pub fn navigate(&self, direction: adw::NavigationDirection) { + pub fn navigate(&self, direction: Direction) { if let Some(current_path) = self.current_path() { let new_path = match direction { - adw::NavigationDirection::Forward => self.model().after(¤t_path), - adw::NavigationDirection::Back => self.model().before(¤t_path), - _ => unimplemented!("Navigation direction should only be back or forward."), + Direction::Forward => self.model().after(¤t_path), + Direction::Back => self.model().before(¤t_path), }; if let Some(new_path) = new_path { @@ -284,6 +284,18 @@ impl LpImageView { } } + /// Jump to position + pub fn jump(&self, position: Position) { + let new_path = match position { + Position::First => self.model().first(), + Position::Last => self.model().last(), + }; + + if let Some(new_path) = new_path { + self.navigate_to_path(&new_path); + } + } + /// Used for drag and drop fn navigate_to_path(&self, new_path: &Path) { let sliding_view = self.sliding_view(); diff --git a/src/window.rs b/src/window.rs index f0a79119bc4cffe8aa2bce8441071e5709f41869..05324e504f3a14359bc271156732c38c603b07af 100644 --- a/src/window.rs +++ b/src/window.rs @@ -30,7 +30,7 @@ use std::cell::RefCell; use std::path::{Path, PathBuf}; use crate::config; -use crate::util; +use crate::util::{self, Direction, Position}; use crate::widgets::{LpImage, LpImagePage, LpImageView, LpPropertiesView}; mod imp { @@ -99,15 +99,35 @@ mod imp { }); klass.install_action("win.next", None, move |win, _, _| { - win.imp() - .image_view - .navigate(adw::NavigationDirection::Forward); + win.imp().image_view.navigate(Direction::Forward); }); klass.install_action("win.previous", None, move |win, _, _| { - win.imp() - .image_view - .navigate(adw::NavigationDirection::Back); + win.imp().image_view.navigate(Direction::Back); + }); + + klass.install_action("win.image-right", None, move |win, _, _| { + if win.direction() == gtk::TextDirection::Rtl { + win.imp().image_view.navigate(Direction::Back); + } else { + win.imp().image_view.navigate(Direction::Forward); + } + }); + + klass.install_action("win.image-left", None, move |win, _, _| { + if win.direction() == gtk::TextDirection::Rtl { + win.imp().image_view.navigate(Direction::Forward); + } else { + win.imp().image_view.navigate(Direction::Back); + } + }); + + klass.install_action("win.first", None, move |win, _, _| { + win.imp().image_view.jump(Position::First); + }); + + klass.install_action("win.last", None, move |win, _, _| { + win.imp().image_view.jump(Position::Last); }); klass.install_action("win.zoom-out", None, move |win, _, _| {