Browse Source
It is sandboxed so loading unknown media is safer. It also supports more formats than image-rs.merge-requests/1714/head
20 changed files with 911 additions and 697 deletions
@ -0,0 +1,188 @@ |
|||||||
|
use glycin::{Frame, Image}; |
||||||
|
use gtk::{gdk, glib, glib::clone, graphene, prelude::*, subclass::prelude::*}; |
||||||
|
use tracing::error; |
||||||
|
|
||||||
|
use crate::{spawn, spawn_tokio}; |
||||||
|
|
||||||
|
mod imp { |
||||||
|
use std::{ |
||||||
|
cell::{OnceCell, RefCell}, |
||||||
|
sync::Arc, |
||||||
|
}; |
||||||
|
|
||||||
|
use super::*; |
||||||
|
|
||||||
|
#[derive(Default)] |
||||||
|
pub struct AnimatedImagePaintable { |
||||||
|
/// The source image.
|
||||||
|
image: OnceCell<Arc<Image<'static>>>, |
||||||
|
/// The current frame that is displayed.
|
||||||
|
pub current_frame: RefCell<Option<Frame>>, |
||||||
|
/// The next frame of the animation, if any.
|
||||||
|
next_frame: RefCell<Option<Frame>>, |
||||||
|
/// The source ID of the timeout to load the next frame, if any.
|
||||||
|
timeout_source_id: RefCell<Option<glib::SourceId>>, |
||||||
|
} |
||||||
|
|
||||||
|
#[glib::object_subclass] |
||||||
|
impl ObjectSubclass for AnimatedImagePaintable { |
||||||
|
const NAME: &'static str = "AnimatedImagePaintable"; |
||||||
|
type Type = super::AnimatedImagePaintable; |
||||||
|
type Interfaces = (gdk::Paintable,); |
||||||
|
} |
||||||
|
|
||||||
|
impl ObjectImpl for AnimatedImagePaintable {} |
||||||
|
|
||||||
|
impl PaintableImpl for AnimatedImagePaintable { |
||||||
|
fn intrinsic_height(&self) -> i32 { |
||||||
|
self.current_frame |
||||||
|
.borrow() |
||||||
|
.as_ref() |
||||||
|
.map(|f| f.height()) |
||||||
|
.unwrap_or_else(|| self.image().info().height) as i32 |
||||||
|
} |
||||||
|
|
||||||
|
fn intrinsic_width(&self) -> i32 { |
||||||
|
self.current_frame |
||||||
|
.borrow() |
||||||
|
.as_ref() |
||||||
|
.map(|f| f.width()) |
||||||
|
.unwrap_or_else(|| self.image().info().width) as i32 |
||||||
|
} |
||||||
|
|
||||||
|
fn snapshot(&self, snapshot: &gdk::Snapshot, width: f64, height: f64) { |
||||||
|
if let Some(frame) = &*self.current_frame.borrow() { |
||||||
|
frame.texture().snapshot(snapshot, width, height); |
||||||
|
} else { |
||||||
|
let snapshot = snapshot.downcast_ref::<gtk::Snapshot>().unwrap(); |
||||||
|
snapshot.append_color( |
||||||
|
&gdk::RGBA::BLACK, |
||||||
|
&graphene::Rect::new(0., 0., width as f32, height as f32), |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fn flags(&self) -> gdk::PaintableFlags { |
||||||
|
gdk::PaintableFlags::SIZE |
||||||
|
} |
||||||
|
|
||||||
|
fn current_image(&self) -> gdk::Paintable { |
||||||
|
let snapshot = gtk::Snapshot::new(); |
||||||
|
self.snapshot( |
||||||
|
snapshot.upcast_ref(), |
||||||
|
self.intrinsic_width() as f64, |
||||||
|
self.intrinsic_height() as f64, |
||||||
|
); |
||||||
|
|
||||||
|
snapshot |
||||||
|
.to_paintable(None) |
||||||
|
.expect("snapshot should always work") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl AnimatedImagePaintable { |
||||||
|
/// The source image.
|
||||||
|
fn image(&self) -> &Arc<Image<'static>> { |
||||||
|
self.image.get().unwrap() |
||||||
|
} |
||||||
|
|
||||||
|
/// Initialize the image.
|
||||||
|
pub(super) fn init(&self, image: Image<'static>, first_frame: Frame) { |
||||||
|
self.image.set(Arc::new(image)).unwrap(); |
||||||
|
self.current_frame.replace(Some(first_frame)); |
||||||
|
|
||||||
|
self.prepare_next_frame(); |
||||||
|
} |
||||||
|
|
||||||
|
/// Show the next frame of the animation.
|
||||||
|
fn show_next_frame(&self) { |
||||||
|
// Drop the timeout source ID so we know we are not waiting for it.
|
||||||
|
self.timeout_source_id.take(); |
||||||
|
|
||||||
|
let Some(next_frame) = self.next_frame.take() else { |
||||||
|
// Wait for the next frame to be loaded.
|
||||||
|
return; |
||||||
|
}; |
||||||
|
|
||||||
|
self.current_frame.replace(Some(next_frame)); |
||||||
|
|
||||||
|
// Invalidate the contents so that the new frame will be rendered.
|
||||||
|
self.obj().invalidate_contents(); |
||||||
|
|
||||||
|
self.prepare_next_frame(); |
||||||
|
} |
||||||
|
|
||||||
|
/// Prepare the next frame of the animation.
|
||||||
|
fn prepare_next_frame(&self) { |
||||||
|
let Some(delay) = self.current_frame.borrow().as_ref().and_then(|f| f.delay()) else { |
||||||
|
return; |
||||||
|
}; |
||||||
|
|
||||||
|
// Set the timeout to update the animation.
|
||||||
|
let source_id = glib::timeout_add_local_once( |
||||||
|
delay, |
||||||
|
clone!( |
||||||
|
#[weak(rename_to = imp)] |
||||||
|
self, |
||||||
|
move || { |
||||||
|
imp.show_next_frame(); |
||||||
|
} |
||||||
|
), |
||||||
|
); |
||||||
|
self.timeout_source_id.replace(Some(source_id)); |
||||||
|
|
||||||
|
spawn!(clone!( |
||||||
|
#[weak(rename_to = imp)] |
||||||
|
self, |
||||||
|
async move { |
||||||
|
imp.load_next_frame_inner().await; |
||||||
|
} |
||||||
|
)); |
||||||
|
} |
||||||
|
|
||||||
|
async fn load_next_frame_inner(&self) { |
||||||
|
let image = self.image().clone(); |
||||||
|
|
||||||
|
let result = spawn_tokio!(async move { image.next_frame().await }) |
||||||
|
.await |
||||||
|
.unwrap(); |
||||||
|
|
||||||
|
match result { |
||||||
|
Ok(next_frame) => { |
||||||
|
self.next_frame.replace(Some(next_frame)); |
||||||
|
|
||||||
|
// In case loading the frame took longer than the delay between frames.
|
||||||
|
if self.timeout_source_id.borrow().is_none() { |
||||||
|
self.show_next_frame(); |
||||||
|
} |
||||||
|
} |
||||||
|
Err(error) => { |
||||||
|
error!("Failed to load next frame: {error}"); |
||||||
|
// Do nothing, the animation will stop.
|
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
glib::wrapper! { |
||||||
|
/// A paintable to display an animated image.
|
||||||
|
pub struct AnimatedImagePaintable(ObjectSubclass<imp::AnimatedImagePaintable>) |
||||||
|
@implements gdk::Paintable; |
||||||
|
} |
||||||
|
|
||||||
|
impl AnimatedImagePaintable { |
||||||
|
/// Load an image from the given file.
|
||||||
|
pub fn new(image: Image<'static>, first_frame: Frame) -> Self { |
||||||
|
let obj = glib::Object::new::<Self>(); |
||||||
|
|
||||||
|
obj.imp().init(image, first_frame); |
||||||
|
|
||||||
|
obj |
||||||
|
} |
||||||
|
|
||||||
|
/// Get the current `GdkTexture` of this paintable, if any.
|
||||||
|
pub fn current_texture(&self) -> Option<gdk::Texture> { |
||||||
|
Some(self.imp().current_frame.borrow().as_ref()?.texture()) |
||||||
|
} |
||||||
|
} |
||||||
@ -1,404 +0,0 @@ |
|||||||
use std::{ |
|
||||||
io::{BufRead, BufReader, Cursor, Seek}, |
|
||||||
time::Duration, |
|
||||||
}; |
|
||||||
|
|
||||||
use gtk::{gdk, gio, glib, graphene, prelude::*, subclass::prelude::*}; |
|
||||||
use image::{ |
|
||||||
codecs::{gif::GifDecoder, png::PngDecoder}, |
|
||||||
flat::SampleLayout, |
|
||||||
AnimationDecoder, DynamicImage, ImageFormat, |
|
||||||
}; |
|
||||||
use tracing::error; |
|
||||||
|
|
||||||
/// A single frame of an animation.
|
|
||||||
pub struct Frame { |
|
||||||
pub texture: gdk::Texture, |
|
||||||
pub duration: Duration, |
|
||||||
} |
|
||||||
|
|
||||||
impl From<image::Frame> for Frame { |
|
||||||
fn from(f: image::Frame) -> Self { |
|
||||||
let mut duration = Duration::from(f.delay()); |
|
||||||
|
|
||||||
// The convention is to use 100 milliseconds duration if it is defined as 0.
|
|
||||||
if duration.is_zero() { |
|
||||||
duration = Duration::from_millis(100); |
|
||||||
} |
|
||||||
|
|
||||||
let sample = f.into_buffer().into_flat_samples(); |
|
||||||
let texture = texture_from_data( |
|
||||||
&sample.samples, |
|
||||||
sample.layout, |
|
||||||
gdk::MemoryFormat::R8g8b8a8, |
|
||||||
image::ColorType::Rgba8.bytes_per_pixel(), |
|
||||||
); |
|
||||||
|
|
||||||
Frame { |
|
||||||
texture: texture.upcast(), |
|
||||||
duration, |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
mod imp { |
|
||||||
use std::{ |
|
||||||
cell::{Cell, RefCell}, |
|
||||||
marker::PhantomData, |
|
||||||
}; |
|
||||||
|
|
||||||
use super::*; |
|
||||||
|
|
||||||
#[derive(Default, glib::Properties)] |
|
||||||
#[properties(wrapper_type = super::ImagePaintable)] |
|
||||||
pub struct ImagePaintable { |
|
||||||
/// The frames of the animation, if any.
|
|
||||||
pub frames: RefCell<Option<Vec<Frame>>>, |
|
||||||
/// The image if this is not an animation, otherwise this is the next
|
|
||||||
/// frame to display.
|
|
||||||
pub frame: RefCell<Option<gdk::Texture>>, |
|
||||||
/// The current index in the animation.
|
|
||||||
pub current_idx: Cell<usize>, |
|
||||||
/// The source ID of the timeout to load the next frame, if any.
|
|
||||||
pub timeout_source_id: RefCell<Option<glib::SourceId>>, |
|
||||||
/// Whether this image is an animation.
|
|
||||||
#[property(get = Self::is_animation)] |
|
||||||
pub is_animation: PhantomData<bool>, |
|
||||||
/// The width of this image.
|
|
||||||
#[property(get = Self::intrinsic_width, default = -1)] |
|
||||||
pub width: PhantomData<i32>, |
|
||||||
/// The height of this image.
|
|
||||||
#[property(get = Self::intrinsic_height, default = -1)] |
|
||||||
pub height: PhantomData<i32>, |
|
||||||
} |
|
||||||
|
|
||||||
#[glib::object_subclass] |
|
||||||
impl ObjectSubclass for ImagePaintable { |
|
||||||
const NAME: &'static str = "ImagePaintable"; |
|
||||||
type Type = super::ImagePaintable; |
|
||||||
type Interfaces = (gdk::Paintable,); |
|
||||||
} |
|
||||||
|
|
||||||
#[glib::derived_properties] |
|
||||||
impl ObjectImpl for ImagePaintable {} |
|
||||||
|
|
||||||
impl PaintableImpl for ImagePaintable { |
|
||||||
fn intrinsic_height(&self) -> i32 { |
|
||||||
self.frame |
|
||||||
.borrow() |
|
||||||
.as_ref() |
|
||||||
.map(|texture| texture.height()) |
|
||||||
.unwrap_or(-1) |
|
||||||
} |
|
||||||
|
|
||||||
fn intrinsic_width(&self) -> i32 { |
|
||||||
self.frame |
|
||||||
.borrow() |
|
||||||
.as_ref() |
|
||||||
.map(|texture| texture.width()) |
|
||||||
.unwrap_or(-1) |
|
||||||
} |
|
||||||
|
|
||||||
fn snapshot(&self, snapshot: &gdk::Snapshot, width: f64, height: f64) { |
|
||||||
if let Some(texture) = &*self.frame.borrow() { |
|
||||||
texture.snapshot(snapshot, width, height); |
|
||||||
} else { |
|
||||||
let snapshot = snapshot.downcast_ref::<gtk::Snapshot>().unwrap(); |
|
||||||
snapshot.append_color( |
|
||||||
&gdk::RGBA::BLACK, |
|
||||||
&graphene::Rect::new(0f32, 0f32, width as f32, height as f32), |
|
||||||
); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
fn flags(&self) -> gdk::PaintableFlags { |
|
||||||
if self.obj().is_animation() { |
|
||||||
gdk::PaintableFlags::SIZE |
|
||||||
} else { |
|
||||||
gdk::PaintableFlags::SIZE | gdk::PaintableFlags::CONTENTS |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
fn current_image(&self) -> gdk::Paintable { |
|
||||||
self.frame |
|
||||||
.borrow() |
|
||||||
.clone() |
|
||||||
.map(|frame| frame.upcast()) |
|
||||||
.or_else(|| { |
|
||||||
let snapshot = gtk::Snapshot::new(); |
|
||||||
self.obj().snapshot(&snapshot, 1.0, 1.0); |
|
||||||
|
|
||||||
snapshot.to_paintable(None) |
|
||||||
}) |
|
||||||
.expect("there should be a fallback paintable") |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl ImagePaintable { |
|
||||||
/// Whether this image is an animation.
|
|
||||||
fn is_animation(&self) -> bool { |
|
||||||
self.frames.borrow().is_some() |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
glib::wrapper! { |
|
||||||
/// A paintable that loads images with the `image` crate.
|
|
||||||
///
|
|
||||||
/// It handles more image types than GDK-Pixbuf and can also handle
|
|
||||||
/// animations from GIF and APNG files.
|
|
||||||
pub struct ImagePaintable(ObjectSubclass<imp::ImagePaintable>) |
|
||||||
@implements gdk::Paintable; |
|
||||||
} |
|
||||||
|
|
||||||
impl ImagePaintable { |
|
||||||
/// Load an image from the given reader in the optional format.
|
|
||||||
///
|
|
||||||
/// The actual format will try to be guessed from the content.
|
|
||||||
pub fn new<R: BufRead + Seek>( |
|
||||||
reader: R, |
|
||||||
format: Option<ImageFormat>, |
|
||||||
) -> Result<Self, Box<dyn std::error::Error>> { |
|
||||||
let obj = glib::Object::new::<Self>(); |
|
||||||
|
|
||||||
let mut reader = image::io::Reader::new(reader); |
|
||||||
|
|
||||||
if let Some(format) = format { |
|
||||||
reader.set_format(format); |
|
||||||
} |
|
||||||
|
|
||||||
let reader = reader.with_guessed_format()?; |
|
||||||
|
|
||||||
obj.load_inner(reader)?; |
|
||||||
|
|
||||||
Ok(obj) |
|
||||||
} |
|
||||||
|
|
||||||
/// Load an image or animation from the given reader.
|
|
||||||
fn load_inner<R: BufRead + Seek>( |
|
||||||
&self, |
|
||||||
reader: image::io::Reader<R>, |
|
||||||
) -> Result<(), Box<dyn std::error::Error>> { |
|
||||||
let imp = self.imp(); |
|
||||||
let format = reader.format().ok_or("Could not detect image format")?; |
|
||||||
|
|
||||||
let read = reader.into_inner(); |
|
||||||
|
|
||||||
// Handle animations.
|
|
||||||
match format { |
|
||||||
image::ImageFormat::Gif => { |
|
||||||
let decoder = GifDecoder::new(read)?; |
|
||||||
|
|
||||||
let frames = decoder |
|
||||||
.into_frames() |
|
||||||
.collect_frames()? |
|
||||||
.into_iter() |
|
||||||
.map(Frame::from) |
|
||||||
.collect::<Vec<_>>(); |
|
||||||
|
|
||||||
if frames.len() == 1 { |
|
||||||
if let Some(frame) = frames.into_iter().next() { |
|
||||||
imp.frame.replace(Some(frame.texture)); |
|
||||||
} |
|
||||||
} else { |
|
||||||
imp.frames.replace(Some(frames)); |
|
||||||
self.update_frame(); |
|
||||||
} |
|
||||||
} |
|
||||||
image::ImageFormat::Png => { |
|
||||||
let decoder = PngDecoder::new(read)?; |
|
||||||
|
|
||||||
if decoder.is_apng().unwrap_or_default() { |
|
||||||
let decoder = decoder.apng()?; |
|
||||||
let frames = decoder |
|
||||||
.into_frames() |
|
||||||
.collect_frames()? |
|
||||||
.into_iter() |
|
||||||
.map(Frame::from) |
|
||||||
.collect::<Vec<_>>(); |
|
||||||
imp.frames.replace(Some(frames)); |
|
||||||
self.update_frame(); |
|
||||||
} else { |
|
||||||
let image = DynamicImage::from_decoder(decoder)?; |
|
||||||
self.set_image(image); |
|
||||||
} |
|
||||||
} |
|
||||||
_ => { |
|
||||||
let image = image::load(read, format)?; |
|
||||||
self.set_image(image); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
|
|
||||||
/// Set the image that is displayed by this paintable.
|
|
||||||
fn set_image(&self, image: DynamicImage) { |
|
||||||
let texture = match image.color() { |
|
||||||
image::ColorType::L8 | image::ColorType::Rgb8 => { |
|
||||||
let sample = image.into_rgb8().into_flat_samples(); |
|
||||||
texture_from_data( |
|
||||||
&sample.samples, |
|
||||||
sample.layout, |
|
||||||
gdk::MemoryFormat::R8g8b8, |
|
||||||
image::ColorType::Rgb8.bytes_per_pixel(), |
|
||||||
) |
|
||||||
} |
|
||||||
image::ColorType::La8 | image::ColorType::Rgba8 => { |
|
||||||
let sample = image.into_rgba8().into_flat_samples(); |
|
||||||
texture_from_data( |
|
||||||
&sample.samples, |
|
||||||
sample.layout, |
|
||||||
gdk::MemoryFormat::R8g8b8a8, |
|
||||||
image::ColorType::Rgba8.bytes_per_pixel(), |
|
||||||
) |
|
||||||
} |
|
||||||
image::ColorType::L16 | image::ColorType::Rgb16 => { |
|
||||||
let sample = image.into_rgb16().into_flat_samples(); |
|
||||||
let bytes = sample |
|
||||||
.samples |
|
||||||
.into_iter() |
|
||||||
.flat_map(|b| b.to_ne_bytes()) |
|
||||||
.collect::<Vec<_>>(); |
|
||||||
texture_from_data( |
|
||||||
&bytes, |
|
||||||
sample.layout, |
|
||||||
gdk::MemoryFormat::R16g16b16, |
|
||||||
image::ColorType::Rgb16.bytes_per_pixel(), |
|
||||||
) |
|
||||||
} |
|
||||||
image::ColorType::La16 | image::ColorType::Rgba16 => { |
|
||||||
let sample = image.into_rgba16().into_flat_samples(); |
|
||||||
let bytes = sample |
|
||||||
.samples |
|
||||||
.into_iter() |
|
||||||
.flat_map(|b| b.to_ne_bytes()) |
|
||||||
.collect::<Vec<_>>(); |
|
||||||
texture_from_data( |
|
||||||
&bytes, |
|
||||||
sample.layout, |
|
||||||
gdk::MemoryFormat::R16g16b16a16, |
|
||||||
image::ColorType::Rgba16.bytes_per_pixel(), |
|
||||||
) |
|
||||||
} |
|
||||||
image::ColorType::Rgb32F => { |
|
||||||
let sample = image.into_rgb32f().into_flat_samples(); |
|
||||||
let bytes = sample |
|
||||||
.samples |
|
||||||
.into_iter() |
|
||||||
.flat_map(|b| b.to_ne_bytes()) |
|
||||||
.collect::<Vec<_>>(); |
|
||||||
texture_from_data( |
|
||||||
&bytes, |
|
||||||
sample.layout, |
|
||||||
gdk::MemoryFormat::R32g32b32Float, |
|
||||||
image::ColorType::Rgb32F.bytes_per_pixel(), |
|
||||||
) |
|
||||||
} |
|
||||||
image::ColorType::Rgba32F => { |
|
||||||
let sample = image.into_rgb32f().into_flat_samples(); |
|
||||||
let bytes = sample |
|
||||||
.samples |
|
||||||
.into_iter() |
|
||||||
.flat_map(|b| b.to_ne_bytes()) |
|
||||||
.collect::<Vec<_>>(); |
|
||||||
texture_from_data( |
|
||||||
&bytes, |
|
||||||
sample.layout, |
|
||||||
gdk::MemoryFormat::R32g32b32Float, |
|
||||||
image::ColorType::Rgb32F.bytes_per_pixel(), |
|
||||||
) |
|
||||||
} |
|
||||||
c => { |
|
||||||
error!("Received image of unsupported color format: {c:?}"); |
|
||||||
return; |
|
||||||
} |
|
||||||
}; |
|
||||||
|
|
||||||
self.imp().frame.replace(Some(texture.upcast())); |
|
||||||
} |
|
||||||
|
|
||||||
/// Creates a new paintable by loading an image from the given file.
|
|
||||||
pub fn from_file(file: &gio::File) -> Result<Self, Box<dyn std::error::Error>> { |
|
||||||
let stream = file.read(gio::Cancellable::NONE)?; |
|
||||||
let reader = BufReader::new(stream.into_read()); |
|
||||||
let format = file |
|
||||||
.path() |
|
||||||
.and_then(|path| ImageFormat::from_path(path).ok()); |
|
||||||
|
|
||||||
Self::new(reader, format) |
|
||||||
} |
|
||||||
|
|
||||||
/// Creates a new paintable by loading an image from memory.
|
|
||||||
pub fn from_bytes( |
|
||||||
bytes: &[u8], |
|
||||||
content_type: Option<&str>, |
|
||||||
) -> Result<Self, Box<dyn std::error::Error>> { |
|
||||||
let reader = Cursor::new(bytes); |
|
||||||
let format = content_type.and_then(ImageFormat::from_mime_type); |
|
||||||
|
|
||||||
Self::new(reader, format) |
|
||||||
} |
|
||||||
|
|
||||||
/// Update the current frame of the animation.
|
|
||||||
fn update_frame(&self) { |
|
||||||
let imp = self.imp(); |
|
||||||
let frames_ref = imp.frames.borrow(); |
|
||||||
|
|
||||||
// If it's not an animation, we return early.
|
|
||||||
let frames = match &*frames_ref { |
|
||||||
Some(frames) => frames, |
|
||||||
None => return, |
|
||||||
}; |
|
||||||
|
|
||||||
let idx = imp.current_idx.get(); |
|
||||||
let next_frame = frames.get(idx).unwrap(); |
|
||||||
imp.frame.replace(Some(next_frame.texture.clone())); |
|
||||||
|
|
||||||
// Invalidate the contents so that the new frame will be rendered.
|
|
||||||
self.invalidate_contents(); |
|
||||||
|
|
||||||
// Update the frame when the duration is elapsed.
|
|
||||||
let update_frame_callback = glib::clone!( |
|
||||||
#[weak(rename_to = obj)] |
|
||||||
self, |
|
||||||
move || { |
|
||||||
obj.imp().timeout_source_id.take(); |
|
||||||
obj.update_frame(); |
|
||||||
} |
|
||||||
); |
|
||||||
let source_id = glib::timeout_add_local_once(next_frame.duration, update_frame_callback); |
|
||||||
imp.timeout_source_id.replace(Some(source_id)); |
|
||||||
|
|
||||||
// Update the index for the next call.
|
|
||||||
let mut new_idx = idx + 1; |
|
||||||
if new_idx >= frames.len() { |
|
||||||
new_idx = 0; |
|
||||||
} |
|
||||||
imp.current_idx.set(new_idx); |
|
||||||
} |
|
||||||
|
|
||||||
/// Get the current frame of this `ImagePaintable`, if any.
|
|
||||||
pub fn current_frame(&self) -> Option<gdk::Texture> { |
|
||||||
self.imp().frame.borrow().clone() |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
fn texture_from_data( |
|
||||||
bytes: &[u8], |
|
||||||
layout: SampleLayout, |
|
||||||
format: gdk::MemoryFormat, |
|
||||||
bpp: u8, |
|
||||||
) -> gdk::MemoryTexture { |
|
||||||
let bytes = glib::Bytes::from(bytes); |
|
||||||
|
|
||||||
let stride = layout.width * bpp as u32; |
|
||||||
|
|
||||||
gdk::MemoryTexture::new( |
|
||||||
layout.width as i32, |
|
||||||
layout.height as i32, |
|
||||||
format, |
|
||||||
&bytes, |
|
||||||
stride as usize, |
|
||||||
) |
|
||||||
} |
|
||||||
@ -1,14 +1,14 @@ |
|||||||
|
mod animated_image_paintable; |
||||||
mod audio_player; |
mod audio_player; |
||||||
mod content_viewer; |
mod content_viewer; |
||||||
mod image_paintable; |
|
||||||
mod location_viewer; |
mod location_viewer; |
||||||
mod video_player; |
mod video_player; |
||||||
mod video_player_renderer; |
mod video_player_renderer; |
||||||
|
|
||||||
pub use self::{ |
pub use self::{ |
||||||
|
animated_image_paintable::AnimatedImagePaintable, |
||||||
audio_player::AudioPlayer, |
audio_player::AudioPlayer, |
||||||
content_viewer::{ContentType, MediaContentViewer}, |
content_viewer::{ContentType, MediaContentViewer}, |
||||||
image_paintable::ImagePaintable, |
|
||||||
location_viewer::LocationViewer, |
location_viewer::LocationViewer, |
||||||
video_player::VideoPlayer, |
video_player::VideoPlayer, |
||||||
}; |
}; |
||||||
|
|||||||
Loading…
Reference in new issue