Skip to main content

fractal/components/avatar/
image.rs

1use gtk::{
2    gdk, glib,
3    glib::{clone, closure_local},
4    prelude::*,
5    subclass::prelude::*,
6};
7use ruma::{
8    OwnedMxcUri, api::client::media::get_content_thumbnail::v3::Method,
9    events::room::avatar::ImageInfo,
10};
11
12use crate::{
13    session::Session,
14    spawn,
15    utils::{
16        CountedRef,
17        media::{
18            FrameDimensions,
19            image::{
20                ImageError, ImageRequestPriority, ImageSource, ThumbnailDownloader,
21                ThumbnailSettings,
22            },
23        },
24    },
25};
26
27/// The source of an avatar's URI.
28#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, glib::Enum)]
29#[enum_type(name = "AvatarUriSource")]
30pub enum AvatarUriSource {
31    /// The URI comes from a Matrix user.
32    #[default]
33    User,
34    /// The URI comes from a Matrix room.
35    Room,
36}
37
38/// The size of the paintable to load.
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub(crate) enum AvatarPaintableSize {
41    /// A small paintable, of size [`AvatarImage::SMALL_PAINTABLE_SIZE`].
42    Small,
43    /// A big paintable, of size [`AvatarImage::BIG_PAINTABLE_SIZE`].
44    Big,
45}
46
47impl AvatarPaintableSize {
48    /// The size in pixels for this paintable size.
49    fn size(self) -> u32 {
50        match self {
51            Self::Small => AvatarImage::SMALL_PAINTABLE_SIZE,
52            Self::Big => AvatarImage::BIG_PAINTABLE_SIZE,
53        }
54    }
55}
56
57impl From<i32> for AvatarPaintableSize {
58    fn from(value: i32) -> Self {
59        let value = u32::try_from(value).unwrap_or_default();
60        if value <= AvatarImage::SMALL_PAINTABLE_SIZE {
61            Self::Small
62        } else {
63            Self::Big
64        }
65    }
66}
67
68mod imp {
69    use std::{
70        cell::{Cell, OnceCell, RefCell},
71        marker::PhantomData,
72        sync::LazyLock,
73    };
74
75    use glib::subclass::Signal;
76
77    use super::*;
78
79    #[derive(Debug, glib::Properties)]
80    #[properties(wrapper_type = super::AvatarImage)]
81    pub struct AvatarImage {
82        /// The current session.
83        #[property(get, construct_only)]
84        session: OnceCell<Session>,
85        /// The Matrix URI of the avatar.
86        uri: RefCell<Option<OwnedMxcUri>>,
87        /// The Matrix URI of the `AvatarImage`, as a string.
88        #[property(get = Self::uri_string)]
89        uri_string: PhantomData<Option<String>>,
90        /// Information about the avatar.
91        info: RefCell<Option<ImageInfo>>,
92        /// The source of the URI avatar.
93        #[property(get, construct_only, builder(AvatarUriSource::default()))]
94        uri_source: Cell<AvatarUriSource>,
95        /// The scale factor to use to load the cached paintable.
96        #[property(get, set = Self::set_scale_factor, explicit_notify, default = 1, minimum = 1)]
97        scale_factor: Cell<u32>,
98        /// The counted reference for the small paintable.
99        ///
100        /// The small paintable is cached indefinitely after the first reference
101        /// is taken.
102        small_paintable_ref: OnceCell<CountedRef>,
103        /// The cached paintable of the avatar at small size, if any.
104        #[property(get)]
105        small_paintable: RefCell<Option<gdk::Paintable>>,
106        /// The counted reference for the big paintable.
107        ///
108        /// The big paintable is cached after the first reference is taken and
109        /// dropped when the last reference is dropped.
110        big_paintable_ref: OnceCell<CountedRef>,
111        /// The cached paintable of the avatar at big size, if any.
112        #[property(get)]
113        big_paintable: RefCell<Option<gdk::Paintable>>,
114        /// The last error encountered when loading the cached paintable of the
115        /// avatar, if any.
116        pub(super) error: Cell<Option<ImageError>>,
117    }
118
119    impl Default for AvatarImage {
120        fn default() -> Self {
121            Self {
122                session: Default::default(),
123                uri: Default::default(),
124                uri_string: Default::default(),
125                info: Default::default(),
126                uri_source: Default::default(),
127                scale_factor: Cell::new(1),
128                small_paintable_ref: Default::default(),
129                small_paintable: Default::default(),
130                big_paintable_ref: Default::default(),
131                big_paintable: Default::default(),
132                error: Default::default(),
133            }
134        }
135    }
136
137    #[glib::object_subclass]
138    impl ObjectSubclass for AvatarImage {
139        const NAME: &'static str = "AvatarImage";
140        type Type = super::AvatarImage;
141    }
142
143    #[glib::derived_properties]
144    impl ObjectImpl for AvatarImage {
145        fn signals() -> &'static [Signal] {
146            static SIGNALS: LazyLock<Vec<Signal>> =
147                LazyLock::new(|| vec![Signal::builder("error-changed").build()]);
148            SIGNALS.as_ref()
149        }
150    }
151
152    impl AvatarImage {
153        /// The Matrix URI of the `AvatarImage`.
154        pub(super) fn uri(&self) -> Option<OwnedMxcUri> {
155            self.uri.borrow().clone()
156        }
157
158        /// Set the Matrix URI of the `AvatarImage`.
159        ///
160        /// Returns whether the URI changed.
161        pub(super) fn set_uri(&self, uri: Option<OwnedMxcUri>) {
162            if *self.uri.borrow() == uri {
163                return;
164            }
165
166            let has_uri = uri.is_some();
167            self.uri.replace(uri);
168            self.obj().notify_uri_string();
169
170            if has_uri && self.small_paintable_ref().count() != 0 {
171                spawn!(
172                    glib::Priority::LOW,
173                    clone!(
174                        #[weak(rename_to = imp)]
175                        self,
176                        async move {
177                            imp.load_small_paintable(false).await;
178                        }
179                    )
180                );
181            } else {
182                // Reset the paintable so it is reloaded later.
183                self.small_paintable.take();
184                self.error.take();
185            }
186
187            if has_uri && self.big_paintable_ref().count() != 0 {
188                spawn!(clone!(
189                    #[weak(rename_to = imp)]
190                    self,
191                    async move {
192                        imp.load_big_paintable().await;
193                    }
194                ));
195            } else {
196                // Reset the error so the paintable can be reloaded later.
197                self.error.take();
198            }
199        }
200
201        /// The Matrix URI of the `AvatarImage`, as a string.
202        fn uri_string(&self) -> Option<String> {
203            self.uri.borrow().as_ref().map(ToString::to_string)
204        }
205
206        /// Information about the avatar.
207        pub(super) fn info(&self) -> Option<ImageInfo> {
208            self.info.borrow().clone()
209        }
210
211        /// Set information about the avatar.
212        pub(super) fn set_info(&self, info: Option<ImageInfo>) {
213            self.info.replace(info);
214        }
215
216        /// Set the scale factor to use to load the cached paintable.
217        ///
218        /// Only the biggest size will be stored.
219        fn set_scale_factor(&self, scale_factor: u32) {
220            if self.scale_factor.get() >= scale_factor {
221                return;
222            }
223
224            self.scale_factor.set(scale_factor);
225            self.obj().notify_scale_factor();
226
227            if self.small_paintable_ref().count() != 0 {
228                spawn!(
229                    glib::Priority::LOW,
230                    clone!(
231                        #[weak(rename_to = imp)]
232                        self,
233                        async move {
234                            imp.load_small_paintable(false).await;
235                        }
236                    )
237                );
238            } else {
239                // Reset the paintable so it is reloaded later.
240                self.small_paintable.take();
241                self.error.take();
242            }
243
244            if self.big_paintable_ref().count() != 0 {
245                spawn!(clone!(
246                    #[weak(rename_to = imp)]
247                    self,
248                    async move {
249                        imp.load_big_paintable().await;
250                    }
251                ));
252            } else {
253                // Reset the error so the paintable can be reloaded later.
254                self.error.take();
255            }
256        }
257
258        /// The counted reference for the small paintable.
259        pub(super) fn small_paintable_ref(&self) -> &CountedRef {
260            self.small_paintable_ref.get_or_init(|| {
261                CountedRef::new(
262                    || {},
263                    clone!(
264                        #[weak(rename_to = imp)]
265                        self,
266                        move || {
267                            if imp.small_paintable.borrow().is_none() && imp.error.get().is_none() {
268                                spawn!(
269                                    glib::Priority::LOW,
270                                    clone!(
271                                        #[weak]
272                                        imp,
273                                        async move {
274                                            imp.load_small_paintable(false).await;
275                                        }
276                                    )
277                                );
278                            }
279                        }
280                    ),
281                )
282            })
283        }
284
285        /// Load the small paintable.
286        pub(super) async fn load_small_paintable(&self, high_priority: bool) {
287            let priority = if high_priority {
288                ImageRequestPriority::High
289            } else {
290                ImageRequestPriority::Low
291            };
292            let paintable = self.load(AvatarPaintableSize::Small, priority).await;
293
294            if self.small_paintable_ref().count() == 0 {
295                // The last reference was dropped while we were loading the paintable, do not
296                // cache it.
297                return;
298            }
299
300            let (paintable, error) = match paintable {
301                Ok(paintable) => (paintable, None),
302                Err(error) => (None, Some(error)),
303            };
304
305            if *self.small_paintable.borrow() != paintable {
306                self.small_paintable.replace(paintable);
307                self.obj().notify_small_paintable();
308            }
309
310            self.set_error(error);
311        }
312
313        /// The counted reference for the big paintable.
314        pub(super) fn big_paintable_ref(&self) -> &CountedRef {
315            self.big_paintable_ref.get_or_init(|| {
316                CountedRef::new(
317                    clone!(
318                        #[weak(rename_to = imp)]
319                        self,
320                        move || {
321                            imp.big_paintable.take();
322                        }
323                    ),
324                    clone!(
325                        #[weak(rename_to = imp)]
326                        self,
327                        move || {
328                            if imp.big_paintable.borrow().is_none() && imp.error.get().is_none() {
329                                spawn!(clone!(
330                                    #[weak]
331                                    imp,
332                                    async move {
333                                        imp.load_big_paintable().await;
334                                    }
335                                ));
336                            }
337                        }
338                    ),
339                )
340            })
341        }
342
343        /// Load the big paintable.
344        async fn load_big_paintable(&self) {
345            let paintable = self
346                .load(AvatarPaintableSize::Big, ImageRequestPriority::High)
347                .await;
348
349            if self.big_paintable_ref().count() == 0 {
350                // The last reference was dropped while we were loading the paintable, do not
351                // cache it.
352                return;
353            }
354
355            let (paintable, error) = match paintable {
356                Ok(paintable) => (paintable, None),
357                Err(error) => (None, Some(error)),
358            };
359
360            if *self.big_paintable.borrow() != paintable {
361                self.big_paintable.replace(paintable);
362                self.obj().notify_big_paintable();
363            }
364
365            self.set_error(error);
366        }
367
368        /// Set the error encountered when loading the avatar, if any.
369        fn set_error(&self, error: Option<ImageError>) {
370            if self.error.get() == error {
371                return;
372            }
373
374            self.error.set(error);
375            self.obj().emit_by_name::<()>("error-changed", &[]);
376        }
377
378        /// Load a paintable of the avatar for the given size.
379        async fn load(
380            &self,
381            size: AvatarPaintableSize,
382            priority: ImageRequestPriority,
383        ) -> Result<Option<gdk::Paintable>, ImageError> {
384            let Some(uri) = self.uri() else {
385                // We do not have an avatar to load.
386                return Ok(None);
387            };
388
389            let client = self.session.get().expect("session is initialized").client();
390            let info = self.info();
391
392            let dimension = size.size();
393            let scale_factor = self.scale_factor.get();
394            let dimensions = FrameDimensions {
395                width: dimension,
396                height: dimension,
397            }
398            .scale(scale_factor);
399
400            let downloader = ThumbnailDownloader {
401                main: ImageSource {
402                    source: (&uri).into(),
403                    info: info.as_ref().map(Into::into),
404                },
405                // Avatars are not encrypted so we should always generate the thumbnail from the
406                // original.
407                alt: None,
408            };
409            let settings = ThumbnailSettings {
410                dimensions,
411                method: Method::Crop,
412                animated: true,
413                prefer_thumbnail: true,
414            };
415
416            downloader
417                .download(client, settings, priority)
418                .await
419                .map(|image| Some(image.into()))
420        }
421    }
422}
423
424glib::wrapper! {
425    /// The image data for an avatar.
426    pub struct AvatarImage(ObjectSubclass<imp::AvatarImage>);
427}
428
429impl AvatarImage {
430    /// The small size of the paintable.
431    ///
432    /// This is usually the size presented in the timeline or the sidebar. This
433    /// is also the size of the avatar in GNOME Shell notifications.
434    ///
435    /// This matches an avatar of size `48` or smaller. This size is cached
436    /// indefinitely after the first [`AvatarImage::small_paintable_ref()`] is
437    /// taken.
438    pub(crate) const SMALL_PAINTABLE_SIZE: u32 = 48;
439
440    /// The big size of the paintable.
441    ///
442    /// This is usually the size presented in the room details or user profile.
443    ///
444    /// This matches an avatar of size `150` or smaller. This is only cached
445    /// when at least one [`AvatarImage::big_paintable_ref()`] is held.
446    pub(crate) const BIG_PAINTABLE_SIZE: u32 = 150;
447
448    /// Construct a new `AvatarImage` with the given session, Matrix URI and
449    /// avatar info.
450    pub(crate) fn new(
451        session: &Session,
452        uri_source: AvatarUriSource,
453        uri: Option<OwnedMxcUri>,
454        info: Option<ImageInfo>,
455    ) -> Self {
456        let obj = glib::Object::builder::<Self>()
457            .property("session", session)
458            .property("uri-source", uri_source)
459            .build();
460
461        obj.set_uri_and_info(uri, info);
462        obj
463    }
464
465    /// Set the Matrix URI and information of the avatar.
466    pub(crate) fn set_uri_and_info(&self, uri: Option<OwnedMxcUri>, info: Option<ImageInfo>) {
467        let imp = self.imp();
468        imp.set_info(info);
469        imp.set_uri(uri);
470    }
471
472    /// The Matrix URI of the avatar.
473    pub(crate) fn uri(&self) -> Option<OwnedMxcUri> {
474        self.imp().uri()
475    }
476
477    /// Get a small paintable ref.
478    pub(crate) fn small_paintable_ref(&self) -> CountedRef {
479        self.imp().small_paintable_ref().clone()
480    }
481
482    /// Get a big paintable ref.
483    pub(crate) fn big_paintable_ref(&self) -> CountedRef {
484        self.imp().big_paintable_ref().clone()
485    }
486
487    /// Get the small paintable.
488    ///
489    /// We first try to get it from the cache, and load it if it is not cached.
490    pub(crate) async fn load_small_paintable(&self) -> Result<Option<gdk::Paintable>, ImageError> {
491        if let Some(paintable) = self.small_paintable() {
492            return Ok(Some(paintable));
493        }
494
495        if let Some(error) = self.error() {
496            return Err(error);
497        }
498
499        self.imp().load_small_paintable(true).await;
500
501        if let Some(paintable) = self.small_paintable() {
502            return Ok(Some(paintable));
503        }
504
505        if let Some(error) = self.error() {
506            return Err(error);
507        }
508
509        Ok(None)
510    }
511
512    /// The last error encountered when loading the paintable of the avatar, if
513    /// any.
514    pub(crate) fn error(&self) -> Option<ImageError> {
515        self.imp().error.get()
516    }
517
518    /// Connect to the signal emitted when the error changed.
519    pub fn connect_error_changed<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
520        self.connect_closure(
521            "error-changed",
522            true,
523            closure_local!(|obj: Self| {
524                f(&obj);
525            }),
526        )
527    }
528}