Skip to main content

fractal/components/media/
animated_image_paintable.rs

1use glycin::{Frame, Image};
2use gtk::{gdk, glib, glib::clone, graphene, prelude::*, subclass::prelude::*};
3use tracing::error;
4
5use crate::{
6    prelude::*,
7    spawn,
8    utils::{CountedRef, File},
9};
10
11mod imp {
12    use std::cell::{OnceCell, RefCell};
13
14    use super::*;
15
16    #[derive(Default)]
17    pub struct AnimatedImagePaintable {
18        /// The image decoder.
19        decoder: OnceCell<Image>,
20        /// The file of the image.
21        ///
22        /// We need to keep a strong reference to the temporary file or it will
23        /// be destroyed.
24        file: OnceCell<File>,
25        /// The current frame that is displayed.
26        pub(super) current_frame: RefCell<Option<Frame>>,
27        /// The next frame of the animation, if any.
28        next_frame: RefCell<Option<Frame>>,
29        /// The source ID of the timeout to load the next frame, if any.
30        timeout_source_id: RefCell<Option<glib::SourceId>>,
31        /// The counted reference for the animation.
32        ///
33        /// When the count is 0, the animation is paused.
34        animation_ref: OnceCell<CountedRef>,
35    }
36
37    #[glib::object_subclass]
38    impl ObjectSubclass for AnimatedImagePaintable {
39        const NAME: &'static str = "AnimatedImagePaintable";
40        type Type = super::AnimatedImagePaintable;
41        type Interfaces = (gdk::Paintable,);
42    }
43
44    impl ObjectImpl for AnimatedImagePaintable {}
45
46    impl PaintableImpl for AnimatedImagePaintable {
47        fn intrinsic_height(&self) -> i32 {
48            self.current_frame
49                .borrow()
50                .as_ref()
51                .map_or_else(|| self.decoder().height(), glycin::Frame::height)
52                .try_into()
53                .unwrap_or(i32::MAX)
54        }
55
56        fn intrinsic_width(&self) -> i32 {
57            self.current_frame
58                .borrow()
59                .as_ref()
60                .map_or_else(|| self.decoder().width(), glycin::Frame::width)
61                .try_into()
62                .unwrap_or(i32::MAX)
63        }
64
65        fn snapshot(&self, snapshot: &gdk::Snapshot, width: f64, height: f64) {
66            if let Some(frame) = &*self.current_frame.borrow() {
67                frame.texture().snapshot(snapshot, width, height);
68            } else {
69                let snapshot = snapshot.downcast_ref::<gtk::Snapshot>().unwrap();
70                snapshot.append_color(
71                    &gdk::RGBA::BLACK,
72                    &graphene::Rect::new(0., 0., width as f32, height as f32),
73                );
74            }
75        }
76
77        fn flags(&self) -> gdk::PaintableFlags {
78            gdk::PaintableFlags::STATIC_SIZE
79        }
80
81        fn current_image(&self) -> gdk::Paintable {
82            let snapshot = gtk::Snapshot::new();
83            self.snapshot(
84                snapshot.upcast_ref(),
85                self.intrinsic_width().into(),
86                self.intrinsic_height().into(),
87            );
88
89            snapshot
90                .to_paintable(None)
91                .expect("snapshot should always work")
92        }
93    }
94
95    impl AnimatedImagePaintable {
96        /// The image decoder.
97        fn decoder(&self) -> &Image {
98            self.decoder.get().expect("decoder should be initialized")
99        }
100
101        /// Initialize the image.
102        pub(super) fn init(&self, decoder: Image, first_frame: Frame, file: Option<File>) {
103            self.decoder
104                .set(decoder)
105                .expect("decoder should be uninitialized");
106            self.current_frame.replace(Some(first_frame));
107
108            if let Some(file) = file {
109                self.file.set(file).expect("file should be uninitialized");
110            }
111
112            self.update_animation();
113        }
114
115        /// Show the next frame of the animation.
116        fn show_next_frame(&self) {
117            // Drop the timeout source ID so we know we are not waiting for it.
118            self.timeout_source_id.take();
119
120            let Some(next_frame) = self.next_frame.take() else {
121                // Wait for the next frame to be loaded.
122                return;
123            };
124
125            self.current_frame.replace(Some(next_frame));
126
127            // Invalidate the contents so that the new frame will be rendered.
128            self.obj().invalidate_contents();
129
130            self.update_animation();
131        }
132
133        /// The counted reference of the animation.
134        pub(super) fn animation_ref(&self) -> &CountedRef {
135            self.animation_ref.get_or_init(|| {
136                CountedRef::new(
137                    clone!(
138                        #[weak(rename_to = imp)]
139                        self,
140                        move || {
141                            imp.update_animation();
142                        }
143                    ),
144                    clone!(
145                        #[weak(rename_to = imp)]
146                        self,
147                        move || {
148                            imp.update_animation();
149                        }
150                    ),
151                )
152            })
153        }
154
155        /// Prepare the next frame of the animation or stop the animation,
156        /// depending on the refcount.
157        fn update_animation(&self) {
158            if self.animation_ref().count() == 0 {
159                // We should not animate, remove the timeout if it exists.
160                if let Some(source) = self.timeout_source_id.take() {
161                    source.remove();
162                }
163                return;
164            } else if self.timeout_source_id.borrow().is_some() {
165                // We are already waiting for the next update.
166                return;
167            }
168
169            let Some(delay) = self
170                .current_frame
171                .borrow()
172                .as_ref()
173                .and_then(GlycinFrameExt::delay_duration)
174            else {
175                return;
176            };
177
178            // Set the timeout to update the animation.
179            let source_id = glib::timeout_add_local_once(
180                delay,
181                clone!(
182                    #[weak(rename_to = imp)]
183                    self,
184                    move || {
185                        imp.show_next_frame();
186                    }
187                ),
188            );
189            self.timeout_source_id.replace(Some(source_id));
190
191            spawn!(clone!(
192                #[weak(rename_to = imp)]
193                self,
194                async move {
195                    imp.load_next_frame_inner().await;
196                }
197            ));
198        }
199
200        async fn load_next_frame_inner(&self) {
201            match self.decoder().next_frame_future().await {
202                Ok(next_frame) => {
203                    self.next_frame.replace(Some(next_frame));
204
205                    // In case loading the frame took longer than the delay between frames.
206                    if self.timeout_source_id.borrow().is_none() {
207                        self.show_next_frame();
208                    }
209                }
210                Err(error) => {
211                    error!("Failed to load next frame: {error}");
212                    // Do nothing, the animation will stop.
213                }
214            }
215        }
216    }
217}
218
219glib::wrapper! {
220    /// A paintable to display an animated image.
221    pub struct AnimatedImagePaintable(ObjectSubclass<imp::AnimatedImagePaintable>)
222        @implements gdk::Paintable;
223}
224
225impl AnimatedImagePaintable {
226    /// Construct an `AnimatedImagePaintable` with the given  decoder, first
227    /// frame, and the file containing the image, if any.
228    pub(crate) fn new(decoder: Image, first_frame: Frame, file: Option<File>) -> Self {
229        let obj = glib::Object::new::<Self>();
230
231        obj.imp().init(decoder, first_frame, file);
232
233        obj
234    }
235
236    /// Get the current `GdkTexture` of this paintable, if any.
237    pub(crate) fn current_texture(&self) -> Option<gdk::Texture> {
238        Some(self.imp().current_frame.borrow().as_ref()?.texture())
239    }
240
241    /// Get an animation ref.
242    pub(crate) fn animation_ref(&self) -> CountedRef {
243        self.imp().animation_ref().clone()
244    }
245}