fractal/components/media/
animated_image_paintable.rs1use 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 decoder: OnceCell<Image>,
20 file: OnceCell<File>,
25 pub(super) current_frame: RefCell<Option<Frame>>,
27 next_frame: RefCell<Option<Frame>>,
29 timeout_source_id: RefCell<Option<glib::SourceId>>,
31 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 fn decoder(&self) -> &Image {
98 self.decoder.get().expect("decoder should be initialized")
99 }
100
101 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 fn show_next_frame(&self) {
117 self.timeout_source_id.take();
119
120 let Some(next_frame) = self.next_frame.take() else {
121 return;
123 };
124
125 self.current_frame.replace(Some(next_frame));
126
127 self.obj().invalidate_contents();
129
130 self.update_animation();
131 }
132
133 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 fn update_animation(&self) {
158 if self.animation_ref().count() == 0 {
159 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 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 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 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 }
214 }
215 }
216 }
217}
218
219glib::wrapper! {
220 pub struct AnimatedImagePaintable(ObjectSubclass<imp::AnimatedImagePaintable>)
222 @implements gdk::Paintable;
223}
224
225impl AnimatedImagePaintable {
226 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 pub(crate) fn current_texture(&self) -> Option<gdk::Texture> {
238 Some(self.imp().current_frame.borrow().as_ref()?.texture())
239 }
240
241 pub(crate) fn animation_ref(&self) -> CountedRef {
243 self.imp().animation_ref().clone()
244 }
245}