From f33c4bd47050f1f7a84aed8487a245d645bb5ff2 Mon Sep 17 00:00:00 2001 From: Sophie Herold Date: Wed, 15 Mar 2023 22:35:59 +0100 Subject: [PATCH] decoder: Support animated formats (GIF, WEBP, APNG) Closes #27 --- README.md | 34 +++--- src/decoder/formats/heif.rs | 4 +- src/decoder/formats/image_rs_other.rs | 154 +++++++++++++++++++++++++- src/decoder/formats/mod.rs | 18 ++- src/decoder/formats/svg.rs | 4 +- src/decoder/mod.rs | 16 ++- src/decoder/tiling.rs | 154 +++++++++++++++++++------- src/widgets/image.rs | 49 +++++++- src/widgets/sliding_view.rs | 15 ++- 9 files changed, 368 insertions(+), 80 deletions(-) diff --git a/README.md b/README.md index dee6b0a..df5061a 100644 --- a/README.md +++ b/README.md @@ -17,23 +17,23 @@ Bundles will not be automatically updated after installation. ## Supported Formats -| Format | ICC | CICP | EXIF | XMP | Animation | Library | -|----------|-----|------|------|-----|-----------|-------------------------------| -| JPEG | ✔ | — | ✔ | ✘ | — | image-rs | -| PNG | ✔ | ✘ | ✔ | ✘ | ✘ | image-rs | -| GIF | ✘ * | — | — | ✘ | ✘ | image-rs | -| WEBP | — | — | ✔ | ✘ | ✘ | image-rs | -| AVIF | ✔ | ✔ | ✔ | ✘ | ✘ | libheif-rs + libheif (C++) | -| HEIC | ✔ | ✔ | ✔ | ✘ | ✘ | libheif-rs + libheif (C++) ** | -| TIFF | ✘ | — | ✔ | ✘ | — | image-rs | -| SVG | ✘ | — | — | ✘ * | — | librsvg | -| BMP | ✘ | — | — | — | — | image-rs | -| ICO | — | — | — | — | — | image-rs | -| PNM | — | — | — | — | — | image-rs | -| DDS | — | — | — | — | — | image-rs | -| TGA | — | — | — | — | — | image-rs | -| OpenEXR | — | — | — | — | — | image-rs | -| farbfeld | — | — | — | — | — | image-rs | +| Format | View | Optional | ICC | CICP | EXIF | XMP | Animation | Library | +|-----------|------|----------|-----|------|------|-----|-----------|----------------------------| +| AVIF | ✔ | ✔ | ✔ | ✔ | ✔ | ✘ | ✘ | libheif-rs + libheif (C++) | +| BMP | ✔ | | ✘ | — | — | — | — | image-rs | +| DDS | ✔ | | — | — | — | — | — | image-rs | +| farbfeld | ✔ | | — | — | — | — | — | image-rs | +| GIF | ✔ | | ✘ * | — | — | ✘ | ✔ | image-rs | +| HEIC | ✔ | ✔ | ✔ | ✔ | ✔ | ✘ | ✘ | libheif-rs + libheif (C++) | +| ICO | ✔ | | — | — | — | — | — | image-rs | +| JPEG | ✔ | | ✔ | — | ✔ | ✘ | — | image-rs | +| OpenEXR | ✔ | | — | — | — | — | — | image-rs | +| PNG | ✔ | | ✔ | ✘ | ✔ | ✘ | ✔ | image-rs | +| PNM | ✔ | | — | — | — | — | — | image-rs | +| SVG | ✔ | | ✘ | — | — | ✘ * | — | librsvg | +| TGA | ✔ | | — | — | — | — | — | image-rs | +| TIFF | ✔ | | ✘ | — | ✔ | ✘ | — | image-rs | +| WEBP | ✔ | | — | — | ✔ | ✘ | ✔ | image-rs | | Symbol | Meaning | |--------|------------------------------------------------| diff --git a/src/decoder/formats/heif.rs b/src/decoder/formats/heif.rs index 70e9a12..41060c1 100644 --- a/src/decoder/formats/heif.rs +++ b/src/decoder/formats/heif.rs @@ -1,6 +1,6 @@ ///! Decode using libheif-rs use super::*; -use crate::decoder::tiling::{self, TilingStoreExt}; +use crate::decoder::tiling::{self, FrameBufferExt}; use crate::deps::*; use crate::i18n::*; use gtk::prelude::*; @@ -23,7 +23,7 @@ impl Heif { pub fn new( path: PathBuf, updater: UpdateSender, - tiles: Arc>, + tiles: Arc>, ) -> Self { log::debug!("Loading HEIF"); updater.spawn_error_handled(move || { diff --git a/src/decoder/formats/image_rs_other.rs b/src/decoder/formats/image_rs_other.rs index 2971e55..36a75d7 100644 --- a/src/decoder/formats/image_rs_other.rs +++ b/src/decoder/formats/image_rs_other.rs @@ -1,12 +1,13 @@ ///! Decode using image-rs use super::*; -use crate::decoder::tiling::{self, TilingStoreExt}; +use crate::decoder::tiling::{self, FrameBufferExt}; use crate::deps::*; use crate::i18n::*; use anyhow::Context; use arc_swap::ArcSwap; use gtk::prelude::*; +use image_rs::AnimationDecoder; use std::fs::File; use std::io::BufReader; @@ -14,7 +15,20 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; #[derive(Debug)] -pub struct ImageRsOther; +pub struct ImageRsOther { + thread_handle: Option>, + abort: Arc>, +} + +impl Drop for ImageRsOther { + fn drop(&mut self) { + if let Some(handle) = &self.thread_handle { + let mut abort = self.abort.write().unwrap(); + *abort = true; + handle.thread().unpark(); + } + } +} impl ImageRsOther { fn reader( @@ -30,8 +44,38 @@ impl ImageRsOther { path: PathBuf, format: image_rs::ImageFormat, updater: UpdateSender, - tiles: Arc>, + tiles: Arc>, ) -> Self { + let abort: Arc> = Default::default(); + + let thread_handle = match format { + image_rs::ImageFormat::Gif + | image_rs::ImageFormat::WebP + | image_rs::ImageFormat::Png => Some(Self::new_animated( + path, + format, + updater, + tiles, + abort.clone(), + )), + _ => { + Self::new_static(path, format, updater, tiles); + None + } + }; + + ImageRsOther { + thread_handle, + abort, + } + } + + fn new_static( + path: PathBuf, + format: image_rs::ImageFormat, + updater: UpdateSender, + tiles: Arc>, + ) { updater.spawn_error_handled(move || { let reader = Self::reader(&path, format)?; let dimensions = reader @@ -42,7 +86,6 @@ impl ImageRsOther { let mut reader = Self::reader(&path, format)?; - // TODO: Set something huge? reader.limits(image_rs::io::Limits::no_limits()); let dynamic_image = reader.decode().context(i18n("Failed to decode image"))?; @@ -60,8 +103,109 @@ impl ImageRsOther { Ok(()) }); + } - ImageRsOther + fn new_animated( + path: PathBuf, + format: image_rs::ImageFormat, + updater: UpdateSender, + tiles: Arc>, + abort: Arc>, + ) -> std::thread::JoinHandle<()> { + updater.clone().spawn_error_handled(move || { + let mut nth_frame = 1; + + // We are currently decoding for each repetition of the animation + // TODO: Check if/how that can be solved differently + loop { + let file = std::fs::File::open(&path)?; + + let (frames, animated_format) = match format { + image_rs::ImageFormat::Gif => ( + image_rs::codecs::gif::GifDecoder::new(file)?.into_frames(), + ImageFormat::AnimatedGif, + ), + image_rs::ImageFormat::WebP => ( + image_rs::codecs::webp::WebPDecoder::new(file)?.into_frames(), + ImageFormat::AnimatedWebP, + ), + image_rs::ImageFormat::Png => { + let decoder = image_rs::codecs::png::PngDecoder::new(file)?; + if decoder.is_apng() { + (decoder.apng().into_frames(), ImageFormat::AnimatedPng) + } else { + // Static PNG images need a different decoder + Self::new_static(path, format, updater, tiles); + return Ok(()); + } + } + _ => todo!(), + }; + + for frame in frames { + match frame { + Err(err) => { + log::warn!("Skipping frame: {err}"); + } + Ok(frame) => { + if nth_frame == 2 { + // We have at least two frames so this is animated + updater.send(DecoderUpdate::Format(animated_format)); + nth_frame = 3; + } + + let (delay_num, delay_den) = frame.delay().numer_denom_ms(); + let delay = std::time::Duration::from_micros(f64::round( + delay_num as f64 * 1000. / delay_den as f64, + ) + as u64); + let position = (frame.left(), frame.top()); + let rgb_image = frame.into_buffer(); + let dimensions = ( + rgb_image.width() + position.0, + rgb_image.height() + position.1, + ); + + let dynamic_image = image_rs::DynamicImage::from(rgb_image); + + let decoded_image = Decoded { dynamic_image }; + + let tile = tiling::Tile { + position, + zoom_level: tiling::zoom_to_level(1.), + bleed: 0, + texture: decoded_image.into_texture(), + }; + + tiles.push_frame(tile, dimensions, delay); + + if nth_frame == 1 { + updater.send(DecoderUpdate::Dimensions(Default::default())); + updater.send(DecoderUpdate::Redraw); + nth_frame = 2; + } + } + } + + if tiles.n_frames() >= 3 { + std::thread::park(); + } + + if *abort.read().unwrap() { + log::debug!("Terminating decoder thread."); + return Ok(()); + } + } + } + }) + } + + pub fn fill_frame_buffer(&self) { + if let Some(handle) = &self.thread_handle { + handle.thread().unpark(); + } else { + log::error!("Trying to wake up one-shot decoder"); + } } } diff --git a/src/decoder/formats/mod.rs b/src/decoder/formats/mod.rs index 35264b2..f965dc6 100644 --- a/src/decoder/formats/mod.rs +++ b/src/decoder/formats/mod.rs @@ -6,16 +6,29 @@ pub use heif::Heif; pub use image_rs_other::ImageRsOther; pub use svg::{Svg, RSVG_MAX_SIZE}; -use super::{Decoder, UpdateSender}; +use super::{Decoder, DecoderUpdate, UpdateSender}; +use crate::i18n::*; #[derive(Clone, Copy, Debug)] pub enum ImageFormat { ImageRs(image_rs::ImageFormat), + AnimatedGif, + AnimatedWebP, + AnimatedPng, // TODO: Add details about contained format Heif, Svg, } +impl ImageFormat { + pub fn is_animated(&self) -> bool { + matches!( + self, + ImageFormat::AnimatedGif | ImageFormat::AnimatedWebP | ImageFormat::AnimatedPng + ) + } +} + impl std::fmt::Display for ImageFormat { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { match self { @@ -28,6 +41,9 @@ impl std::fmt::Display for ImageFormat { write!(f, "{}", debug) } } + Self::AnimatedGif => write!(f, "{}", i18n("Animated GIF")), + Self::AnimatedWebP => write!(f, "{}", i18n("Animated WebP")), + Self::AnimatedPng => write!(f, "{}", i18n("Animated PNG")), Self::Heif => write!(f, "HEIF Container"), Self::Svg => write!(f, "SVG"), } diff --git a/src/decoder/formats/svg.rs b/src/decoder/formats/svg.rs index 077cf95..9288b45 100644 --- a/src/decoder/formats/svg.rs +++ b/src/decoder/formats/svg.rs @@ -1,6 +1,6 @@ ///! Decode using librsvg use super::*; -use crate::decoder::tiling::{self, TilingStoreExt}; +use crate::decoder::tiling::{self, FrameBufferExt}; use crate::decoder::TileRequest; use crate::deps::*; @@ -47,7 +47,7 @@ impl Svg { pub fn new( path: PathBuf, updater: UpdateSender, - tiles: Arc>, + tiles: Arc>, ) -> Self { let current_request: Arc> = Default::default(); let request_store = current_request.clone(); diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index 5c00a82..ed06fe3 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -8,7 +8,7 @@ use crate::deps::*; use crate::i18n::*; use crate::image_metadata::ImageMetadata; use formats::*; -use tiling::TilingStoreExt; +use tiling::FrameBufferExt; use anyhow::{anyhow, bail, Context}; use arc_swap::ArcSwap; @@ -89,11 +89,11 @@ enum FormatDecoder { impl Decoder { /// Get new image decoder /// - /// The textures will be stored in the passed `TilingStore`. + /// The textures will be stored in the passed `TiledImage`. /// The renderer should listen to updates from the returned receiver. pub async fn new( file: gio::File, - tiles: Arc>, + tiles: Arc>, ) -> anyhow::Result<(Self, mpsc::UnboundedReceiver)> { let path = file.path().context("Need a file path")?; let (sender, receiver) = mpsc::unbounded(); @@ -111,7 +111,7 @@ impl Decoder { fn format_decoder( update_sender: UpdateSender, path: PathBuf, - tiles: Arc>, + tiles: Arc>, ) -> anyhow::Result { update_sender.send(DecoderUpdate::Metadata(ImageMetadata::load(&path))); @@ -177,4 +177,12 @@ impl Decoder { _ => {} }; } + + pub fn fill_frame_buffer(&self) { + if let FormatDecoder::ImageRsOther(decoder) = &self.decoder { + decoder.fill_frame_buffer(); + } else { + log::error!("Trying to fill frame buffer for decoder without animation support"); + } + } } diff --git a/src/decoder/tiling.rs b/src/decoder/tiling.rs index 35a08a8..451133f 100644 --- a/src/decoder/tiling.rs +++ b/src/decoder/tiling.rs @@ -1,7 +1,7 @@ ///! Tiled renderer ///! ///! It is not always feasible or desiareble to store the complete decoded -///! image in the VRAM. [`TilingStore`] allows to compose the parts of +///! image in the VRAM. [`TiledImage`] allows to compose the parts of ///! the image currently viewed, out of smaller [`Tiles`]. ///! ///! This is especially important for SVGs where every the image has to @@ -15,7 +15,9 @@ use arc_swap::ArcSwap; use gtk::prelude::*; use std::collections::BTreeMap; +use std::collections::VecDeque; use std::sync::Arc; +use std::time::Duration; const ZOOM_SIGNIFICANT_DIGETS: i32 = 6; pub const TILE_SIZE: u16 = 4000; @@ -77,18 +79,72 @@ impl Tile { } } +/// Buffers multiple images for animations +/// +/// If the image is not animated, this only contains one image +#[derive(Clone, Debug, Default)] +pub struct FrameBuffer { + /// The first entry is always the currently shown frame + pub images: VecDeque, + pub update_sender: Option, +} + +impl FrameBuffer { + /// Returns mutable reference to the current image + /// + /// This is a convenience functions for the case of non-animated images. + /// If no image exists yet, a new one is inserted an returned. + pub fn current(&mut self) -> &mut TiledImage { + if self.images.is_empty() { + self.images.push_back(TiledImage::default()) + } + self.images.front_mut().unwrap() + } + + pub fn original_dimensions(&self) -> Option { + self.images.front()?.original_dimensions + } + + pub fn set_update_sender(&mut self, sender: UpdateSender) { + self.update_sender = Some(sender); + } + + /// Render the current image into this snapshot + pub fn add_to_snapshot(&self, snapshot: >k::Snapshot, zoom: f64, options: &RenderOptions) { + if let Some(tiling) = self.images.front() { + tiling.add_to_snapshot(snapshot, zoom, options); + } else { + log::error!("Trying to snapshot empty tiling queue"); + } + } + + pub fn cleanup(&mut self, zoom: f64, viewport: graphene::Rect) { + self.current().cleanup(zoom, viewport); + } + + pub fn contains(&self, zoom: f64, coordinates: Coordinates) -> bool { + self.images + .front() + .map_or(false, |x| x.contains(zoom, coordinates)) + } +} + #[derive(Clone, Debug)] /// Store tiles for image -pub struct TilingStore { +/// +/// This is the common representation for image textures. +/// I many cases there might only be one tile. +pub struct TiledImage { pub tiles: BTreeMap>, /// Tile size without bleed pub tile_size: u16, /// Complete image size pub original_dimensions: Option, - pub update_sender: Option, + /// Delay until to show this frame if animated + pub delay: std::time::Duration, } -impl Default for TilingStore { +impl Default for TiledImage { fn default() -> Self { Self::new(TILE_SIZE) } @@ -99,13 +155,13 @@ pub struct RenderOptions { pub scaling_filter: gsk::ScalingFilter, } -impl TilingStore { +impl TiledImage { pub fn new(tile_size: u16) -> Self { - TilingStore { + TiledImage { tiles: Default::default(), tile_size, original_dimensions: None, - update_sender: None, + delay: Default::default(), } } @@ -128,28 +184,6 @@ impl TilingStore { pub fn push(&mut self, tile: Tile) { let map = self.tiles.entry(tile.zoom_level).or_default(); map.insert(tile.coordinates(), tile); - if let Some(updater) = &self.update_sender { - updater.send(DecoderUpdate::Redraw); - } - } - - pub fn set_original_dimensions(&mut self, dimensions: Coordinates) { - self.set_original_dimensions_full(dimensions, Default::default()); - } - - pub fn set_original_dimensions_full( - &mut self, - dimensions: Coordinates, - dimension_details: ImageDimensionDetails, - ) { - self.original_dimensions = Some(dimensions); - if let Some(updater) = &self.update_sender { - updater.send(DecoderUpdate::Dimensions(dimension_details)); - } - } - - pub fn set_update_sender(&mut self, sender: UpdateSender) { - self.update_sender = Some(sender); } pub fn contains(&self, zoom: f64, coordinates: Coordinates) -> bool { @@ -340,8 +374,13 @@ impl RectExt for graphene::Rect { } } -pub trait TilingStoreExt { +pub trait FrameBufferExt { fn push(&self, tile: Tile); + fn push_frame(&self, tile: Tile, dimensions: Coordinates, delay: Duration); + /// Return true if the next frame should be shown and removes the outdated frame + fn frame_timeout(&self, elapsed: Duration) -> bool; + /// Returns the number of currently buffered frames + fn n_frames(&self) -> usize; fn set_original_dimensions(&self, size: Coordinates); fn set_original_dimensions_full( &self, @@ -351,21 +390,52 @@ pub trait TilingStoreExt { fn set_update_sender(&self, sender: UpdateSender); } -impl TilingStoreExt for ArcSwap { +impl FrameBufferExt for ArcSwap { fn push(&self, tile: Tile) { self.rcu(|tiling_store| { let mut new_store = (**tiling_store).clone(); - new_store.push(tile.clone()); + new_store.current().push(tile.clone()); + Arc::new(new_store) + }); + if let Some(updater) = &self.load().update_sender { + updater.send(DecoderUpdate::Redraw); + } + } + + fn push_frame(&self, tile: Tile, dimensions: Coordinates, delay: Duration) { + let mut store = TiledImage::default(); + store.push(tile); + store.original_dimensions = Some(dimensions); + store.delay = delay; + + self.rcu(|tiling_store| { + let mut new_store = (**tiling_store).clone(); + new_store.images.push_back(store.clone()); Arc::new(new_store) }); } + fn frame_timeout(&self, elapsed: Duration) -> bool { + if let Some(next_frame) = self.load().images.get(1) { + if elapsed >= next_frame.delay { + self.rcu(|tiling_store| { + let mut new_store = (**tiling_store).clone(); + new_store.images.pop_front(); + Arc::new(new_store) + }); + return true; + } + } + + false + } + + fn n_frames(&self) -> usize { + self.load().images.len() + } + fn set_original_dimensions(&self, size: Coordinates) { - self.rcu(|tiling_store| { - let mut new_store = (**tiling_store).clone(); - new_store.set_original_dimensions(size); - Arc::new(new_store) - }); + self.set_original_dimensions_full(size, Default::default()); } fn set_original_dimensions_full( @@ -375,9 +445,13 @@ impl TilingStoreExt for ArcSwap { ) { self.rcu(|tiling_store| { let mut new_store = (**tiling_store).clone(); - new_store.set_original_dimensions_full(size, dimension_details.clone()); + new_store.current().original_dimensions = Some(size); Arc::new(new_store) }); + + if let Some(updater) = &self.load().update_sender { + updater.send(DecoderUpdate::Dimensions(dimension_details)); + } } fn set_update_sender(&self, sender: UpdateSender) { @@ -395,7 +469,7 @@ mod test { #[test] fn test_tiling_cleanup() { - let mut store = TilingStore::new(2); + let mut store = TiledImage::new(2); let toplevel = Tile { position: (0, 0), @@ -468,7 +542,7 @@ mod test { #[test] fn test_tiling_cleanup_simple() { - let mut store = TilingStore::new(2); + let mut store = TiledImage::new(2); let toplevel = Tile { position: (0, 0), diff --git a/src/widgets/image.rs b/src/widgets/image.rs index 3998cf0..75ea059 100644 --- a/src/widgets/image.rs +++ b/src/widgets/image.rs @@ -29,6 +29,7 @@ use crate::deps::*; +use crate::decoder::tiling::FrameBufferExt; use crate::decoder::{self, tiling, Decoder, DecoderUpdate}; use crate::i18n::i18n; use crate::image_metadata::LpImageMetadata; @@ -84,7 +85,7 @@ mod imp { pub(super) is_deleted: Cell, /// Track changes to this image pub(super) file_monitor: RefCell>, - pub(super) tiles: Arc>, + pub(super) frame_buffer: Arc>, pub(super) decoder: RefCell>>, pub(super) format: RefCell>, @@ -139,6 +140,9 @@ mod imp { /// Required for calculating delta while moving window around pub(super) last_drag_value: Cell>, + pub(super) tick_callback: RefCell>, + pub(super) last_animated_frame: Cell, + widget_dimensions: Cell<(i32, i32)>, } @@ -549,7 +553,7 @@ mod imp { widget.snapshot_rotate_mirror(snapshot, widget.rotation() as f32, widget.mirrored()); // Add texture(s) - self.tiles + self.frame_buffer .load() .add_to_snapshot(snapshot, applicable_zoom, &render_options); @@ -655,7 +659,7 @@ impl LpImage { log::debug!("Loading file {path:?}"); - let tiles = &self.imp().tiles; + let tiles = &self.imp().frame_buffer; let (decoder, mut decoder_update) = match Decoder::new(file.clone(), tiles.clone()).await { Ok(x) => x, @@ -705,7 +709,7 @@ impl LpImage { } self.queue_draw(); - imp.tiles.rcu(|tiles| { + imp.frame_buffer.rcu(|tiles| { let mut new_tiles = (**tiles).clone(); // TODO: Use an area larger than the viewport new_tiles.cleanup(self.imp().zoom_target.get(), self.viewport()); @@ -718,10 +722,43 @@ impl LpImage { DecoderUpdate::Format(format) => { imp.format.replace(Some(format)); self.notify("format-name"); + + // Do the animation part for animated formats + if format.is_animated() { + let callback_id = self + .add_tick_callback(glib::clone!(@weak self as obj => @default-return glib::Continue(false), move |_, clock| obj.tick_callback(clock))); + imp.tick_callback.replace(Some(callback_id)); + } } } } + /// Roughly called for every frame if image is visible + /// + /// We handle advancing to the next frame for animated GIFs etc here. + fn tick_callback(&self, clock: &gdk::FrameClock) -> glib::Continue { + // Do not animate if not visible + if !self.is_mapped() { + return glib::Continue(true); + } + + let elapsed = clock.frame_time() - self.imp().last_animated_frame.get(); + let duration = std::time::Duration::from_micros(elapsed as u64); + + // Check if it's time to show the next frame + if self.imp().frame_buffer.frame_timeout(duration) { + // Just draw since frame_timeout updated to new frame + self.queue_draw(); + self.imp().last_animated_frame.set(clock.frame_time()); + if let Some(decoder) = self.imp().decoder.borrow().as_ref() { + // Decode new frame and load it into buffer + decoder.fill_frame_buffer(); + } + } + + glib::Continue(true) + } + pub fn is_loaded(&self) -> bool { self.imp().is_loaded.get() } @@ -851,7 +888,7 @@ impl LpImage { ); self.imp() - .tiles + .frame_buffer .load() .add_to_snapshot(&snapshot, scale as f64, &render_options); @@ -1222,7 +1259,7 @@ impl LpImage { } fn original_dimensions(&self) -> (i32, i32) { - if let Some((width, height)) = self.imp().tiles.load().original_dimensions { + if let Some((width, height)) = self.imp().frame_buffer.load().original_dimensions() { (width as i32, height as i32) } else { (0, 0) diff --git a/src/widgets/sliding_view.rs b/src/widgets/sliding_view.rs index eed11f9..f75c3e2 100644 --- a/src/widgets/sliding_view.rs +++ b/src/widgets/sliding_view.rs @@ -1,8 +1,6 @@ //! A sliding view widget for images //! //! This widget it similar to `AdwCarousel`. -//! Currently, there is no swipe-gesture support since it's not working with -//! `GtkScrolledWindow` anyways. use crate::deps::*; use crate::widgets::LpImagePage; @@ -152,15 +150,26 @@ mod imp { let position_shift = self.position_shift.get() as f32; for (page_index, page) in self.pages.borrow().iter().enumerate() { + let page_position = page_index as f32; + // reverse page order for RTL languages let direction_sign = if self.is_rtl() { -1. } else { 1. }; // This positions the pages within the carousel and shifts them // according to the position that should currently be shown. let x = direction_sign - * (page_index as f32 - scroll_position - position_shift) + * (page_position - scroll_position - position_shift) * (width as f32 + self.page_spacing(width)); + // Only show visible images + if page_position == (scroll_position + position_shift).floor() + || page_position == (scroll_position + position_shift).ceil() + { + page.set_visible(true); + } else { + page.set_visible(false); + } + let transform = gsk::Transform::new().translate(&graphene::Point::new(x, 0.)); page.allocate(width, height, 0, Some(transform)); page.image().allocate(width, height, 0, None);