fractal/components/avatar/
mod.rs

1use adw::subclass::prelude::*;
2use gtk::{gdk, glib, glib::clone, prelude::*, CompositeTemplate};
3
4mod crop_circle;
5mod data;
6mod editable;
7mod image;
8mod overlapping;
9
10use self::image::AvatarPaintableSize;
11pub use self::{
12    data::AvatarData,
13    editable::EditableAvatar,
14    image::{AvatarImage, AvatarUriSource},
15    overlapping::OverlappingAvatars,
16};
17use crate::{
18    components::AnimatedImagePaintable,
19    session::model::Room,
20    utils::{BoundObject, BoundObjectWeakRef, CountedRef},
21};
22
23/// The safety setting to watch to decide whether the image of the avatar should
24/// be displayed.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, glib::Enum, Default)]
26#[enum_type(name = "AvatarImageSafetySetting")]
27pub enum AvatarImageSafetySetting {
28    /// No setting needs to be watched, the image is always shown when
29    /// available.
30    #[default]
31    None,
32
33    /// The media previews safety setting should be watched, with the image only
34    /// shown when allowed.
35    ///
36    /// This setting also requires the [`Room`] where the avatar is presented.
37    MediaPreviews,
38
39    /// The invite avatars safety setting should be watched, with the image only
40    /// shown when allowed.
41    ///
42    /// This setting also requires the [`Room`] where the avatar is presented.
43    InviteAvatars,
44}
45
46mod imp {
47    use std::{
48        cell::{Cell, RefCell},
49        marker::PhantomData,
50    };
51
52    use glib::subclass::InitializingObject;
53
54    use super::*;
55
56    #[derive(Debug, Default, CompositeTemplate, glib::Properties)]
57    #[template(resource = "/org/gnome/Fractal/ui/components/avatar/mod.ui")]
58    #[properties(wrapper_type = super::Avatar)]
59    pub struct Avatar {
60        #[template_child]
61        avatar: TemplateChild<adw::Avatar>,
62        /// The [`AvatarData`] displayed by this widget.
63        #[property(get, set = Self::set_data, explicit_notify, nullable)]
64        data: BoundObject<AvatarData>,
65        /// The [`AvatarImage`] watched by this widget.
66        #[property(get)]
67        image: BoundObjectWeakRef<AvatarImage>,
68        /// The size of the Avatar.
69        #[property(get = Self::size, set = Self::set_size, explicit_notify, builder().default_value(-1).minimum(-1))]
70        size: PhantomData<i32>,
71        /// The safety setting to watch to decide whether the image of the
72        /// avatar should be displayed.
73        #[property(get, set = Self::set_watched_safety_setting, explicit_notify, builder(AvatarImageSafetySetting::default()))]
74        watched_safety_setting: Cell<AvatarImageSafetySetting>,
75        /// The room to watch to apply the current safety settings.
76        ///
77        /// This is required if `watched_safety_setting` is not `None`.
78        #[property(get, set = Self::set_watched_room, explicit_notify, nullable)]
79        watched_room: RefCell<Option<Room>>,
80        paintable_ref: RefCell<Option<CountedRef>>,
81        paintable_animation_ref: RefCell<Option<CountedRef>>,
82        watched_room_handler: RefCell<Option<glib::SignalHandlerId>>,
83        watched_session_settings_handler: RefCell<Option<glib::SignalHandlerId>>,
84    }
85
86    #[glib::object_subclass]
87    impl ObjectSubclass for Avatar {
88        const NAME: &'static str = "Avatar";
89        type Type = super::Avatar;
90        type ParentType = adw::Bin;
91
92        fn class_init(klass: &mut Self::Class) {
93            AvatarImage::ensure_type();
94
95            Self::bind_template(klass);
96            Self::bind_template_callbacks(klass);
97
98            klass.set_accessible_role(gtk::AccessibleRole::Img);
99        }
100
101        fn instance_init(obj: &InitializingObject<Self>) {
102            obj.init_template();
103        }
104    }
105
106    #[glib::derived_properties]
107    impl ObjectImpl for Avatar {
108        fn dispose(&self) {
109            self.disconnect_safety_setting_signals();
110        }
111    }
112
113    impl WidgetImpl for Avatar {
114        fn map(&self) {
115            self.parent_map();
116            self.update_paintable();
117        }
118
119        fn unmap(&self) {
120            self.parent_unmap();
121            self.update_animated_paintable_state();
122        }
123    }
124
125    impl BinImpl for Avatar {}
126
127    impl AccessibleImpl for Avatar {
128        fn first_accessible_child(&self) -> Option<gtk::Accessible> {
129            // Hide the children in the a11y tree.
130            None
131        }
132    }
133
134    #[gtk::template_callbacks]
135    impl Avatar {
136        /// The size of the Avatar.
137        fn size(&self) -> i32 {
138            self.avatar.size()
139        }
140
141        /// Set the size of the Avatar.
142        fn set_size(&self, size: i32) {
143            if self.size() == size {
144                return;
145            }
146
147            self.avatar.set_size(size);
148
149            self.update_paintable();
150            self.obj().notify_size();
151        }
152
153        /// Set the safety setting to watch to decide whether the image of the
154        /// avatar should be displayed.
155        fn set_watched_safety_setting(&self, setting: AvatarImageSafetySetting) {
156            if self.watched_safety_setting.get() == setting {
157                return;
158            }
159
160            self.disconnect_safety_setting_signals();
161
162            self.watched_safety_setting.set(setting);
163
164            self.connect_safety_setting_signals();
165            self.obj().notify_watched_safety_setting();
166        }
167
168        /// Set the room to watch to apply the current safety settings.
169        fn set_watched_room(&self, room: Option<Room>) {
170            if *self.watched_room.borrow() == room {
171                return;
172            }
173
174            self.disconnect_safety_setting_signals();
175
176            self.watched_room.replace(room);
177
178            self.connect_safety_setting_signals();
179            self.obj().notify_watched_room();
180        }
181
182        /// Connect to the proper signals for the current safety setting.
183        fn connect_safety_setting_signals(&self) {
184            let Some(room) = self.watched_room.borrow().clone() else {
185                return;
186            };
187            let Some(session) = room.session() else {
188                return;
189            };
190
191            match self.watched_safety_setting.get() {
192                AvatarImageSafetySetting::None => {}
193                AvatarImageSafetySetting::MediaPreviews => {
194                    let room_handler = room.connect_join_rule_notify(clone!(
195                        #[weak(rename_to = imp)]
196                        self,
197                        move |_| {
198                            imp.update_paintable();
199                        }
200                    ));
201                    self.watched_room_handler.replace(Some(room_handler));
202
203                    let session_settings_handler = session
204                        .settings()
205                        .connect_media_previews_enabled_changed(clone!(
206                            #[weak(rename_to = imp)]
207                            self,
208                            move |_| {
209                                imp.update_paintable();
210                            }
211                        ));
212                    self.watched_session_settings_handler
213                        .replace(Some(session_settings_handler));
214                }
215                AvatarImageSafetySetting::InviteAvatars => {
216                    let room_handler = room.connect_is_invite_notify(clone!(
217                        #[weak(rename_to = imp)]
218                        self,
219                        move |_| {
220                            imp.update_paintable();
221                        }
222                    ));
223                    self.watched_room_handler.replace(Some(room_handler));
224
225                    let session_settings_handler = session
226                        .settings()
227                        .connect_invite_avatars_enabled_notify(clone!(
228                            #[weak(rename_to = imp)]
229                            self,
230                            move |_| {
231                                imp.update_paintable();
232                            }
233                        ));
234                    self.watched_session_settings_handler
235                        .replace(Some(session_settings_handler));
236                }
237            }
238
239            self.update_paintable();
240        }
241
242        /// Disconnect the handlers for the signals of the safety setting.
243        fn disconnect_safety_setting_signals(&self) {
244            if let Some(room) = self.watched_room.borrow().as_ref() {
245                if let Some(handler) = self.watched_room_handler.take() {
246                    room.disconnect(handler);
247                }
248
249                if let Some(handler) = self.watched_session_settings_handler.take() {
250                    room.session()
251                        .inspect(|session| session.settings().disconnect(handler));
252                }
253            }
254        }
255
256        /// Whether we can display the image of the avatar with the current
257        /// state.
258        fn can_show_image(&self) -> bool {
259            let watched_safety_setting = self.watched_safety_setting.get();
260
261            if watched_safety_setting == AvatarImageSafetySetting::None {
262                return true;
263            }
264
265            let Some(room) = self.watched_room.borrow().clone() else {
266                return false;
267            };
268            let Some(session) = room.session() else {
269                return false;
270            };
271
272            match watched_safety_setting {
273                AvatarImageSafetySetting::None => unreachable!(),
274                AvatarImageSafetySetting::MediaPreviews => {
275                    session.settings().should_room_show_media_previews(&room)
276                }
277                AvatarImageSafetySetting::InviteAvatars => {
278                    !room.is_invite() || session.settings().invite_avatars_enabled()
279                }
280            }
281        }
282
283        /// Set the [`AvatarData`] displayed by this widget.
284        fn set_data(&self, data: Option<AvatarData>) {
285            if self.data.obj() == data {
286                return;
287            }
288
289            self.data.disconnect_signals();
290
291            if let Some(data) = data {
292                let image_handler = data.connect_image_notify(clone!(
293                    #[weak(rename_to = imp)]
294                    self,
295                    move |_| {
296                        imp.update_image();
297                    }
298                ));
299
300                self.data.set(data, vec![image_handler]);
301            }
302
303            self.update_image();
304            self.obj().notify_data();
305        }
306
307        /// Set the [`AvatarImage`] watched by this widget.
308        fn update_image(&self) {
309            let image = self.data.obj().and_then(|data| data.image());
310
311            if self.image.obj() == image {
312                return;
313            }
314
315            self.image.disconnect_signals();
316
317            if let Some(image) = &image {
318                let small_paintable_handler = image.connect_small_paintable_notify(clone!(
319                    #[weak(rename_to = imp)]
320                    self,
321                    move |_| {
322                        imp.update_paintable();
323                    }
324                ));
325                let big_paintable_handler = image.connect_big_paintable_notify(clone!(
326                    #[weak(rename_to = imp)]
327                    self,
328                    move |_| {
329                        imp.update_paintable();
330                    }
331                ));
332
333                self.image
334                    .set(image, vec![small_paintable_handler, big_paintable_handler]);
335            }
336
337            self.update_scale_factor();
338            self.update_paintable();
339
340            self.obj().notify_image();
341        }
342
343        /// Whether this avatar needs a small paintable.
344        fn needs_small_paintable(&self) -> bool {
345            AvatarPaintableSize::from(self.size()) == AvatarPaintableSize::Small
346        }
347
348        /// Update the scale factor used to load the paintable.
349        #[template_callback]
350        fn update_scale_factor(&self) {
351            let Some(image) = self.image.obj() else {
352                return;
353            };
354
355            let scale_factor = self.obj().scale_factor().try_into().unwrap_or(1);
356            image.set_scale_factor(scale_factor);
357        }
358
359        /// Update the paintable for this avatar.
360        fn update_paintable(&self) {
361            let _old_paintable_ref = self.paintable_ref.take();
362
363            if !self.can_show_image() {
364                // We need to unset the paintable.
365                self.avatar.set_custom_image(None::<&gdk::Paintable>);
366                self.update_animated_paintable_state();
367                return;
368            }
369
370            if !self.obj().is_mapped() {
371                // We do not need a paintable.
372                self.update_animated_paintable_state();
373                return;
374            }
375
376            let Some(image) = self.image.obj() else {
377                self.update_animated_paintable_state();
378                return;
379            };
380
381            let (paintable, paintable_ref) = if self.needs_small_paintable() {
382                (image.small_paintable(), image.small_paintable_ref())
383            } else {
384                (
385                    // Fallback to small paintable while the big paintable is loading.
386                    image.big_paintable().or_else(|| image.small_paintable()),
387                    image.big_paintable_ref(),
388                )
389            };
390            self.avatar.set_custom_image(paintable.as_ref());
391            self.paintable_ref.replace(Some(paintable_ref));
392
393            self.update_animated_paintable_state();
394        }
395
396        /// Update the state of the animated paintable for this avatar.
397        fn update_animated_paintable_state(&self) {
398            let _old_paintable_animation_ref = self.paintable_animation_ref.take();
399
400            if !self.can_show_image() || !self.obj().is_mapped() {
401                // We do not need to animate the paintable.
402                return;
403            }
404
405            let Some(image) = self.image.obj() else {
406                return;
407            };
408
409            let paintable = if self.needs_small_paintable() {
410                image.small_paintable()
411            } else {
412                image.big_paintable()
413            };
414
415            let Some(paintable) = paintable.and_downcast::<AnimatedImagePaintable>() else {
416                return;
417            };
418
419            self.paintable_animation_ref
420                .replace(Some(paintable.animation_ref()));
421        }
422    }
423}
424
425glib::wrapper! {
426    /// A widget displaying an `Avatar` for a `Room` or `User`.
427    pub struct Avatar(ObjectSubclass<imp::Avatar>)
428        @extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
429}
430
431impl Avatar {
432    pub fn new() -> Self {
433        glib::Object::new()
434    }
435}