fractal/components/avatar/
mod.rs1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, glib::Enum, Default)]
26#[enum_type(name = "AvatarImageSafetySetting")]
27pub enum AvatarImageSafetySetting {
28 #[default]
31 None,
32
33 MediaPreviews,
38
39 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 #[property(get, set = Self::set_data, explicit_notify, nullable)]
64 data: BoundObject<AvatarData>,
65 #[property(get)]
67 image: BoundObjectWeakRef<AvatarImage>,
68 #[property(get = Self::size, set = Self::set_size, explicit_notify, builder().default_value(-1).minimum(-1))]
70 size: PhantomData<i32>,
71 #[property(get, set = Self::set_watched_safety_setting, explicit_notify, builder(AvatarImageSafetySetting::default()))]
74 watched_safety_setting: Cell<AvatarImageSafetySetting>,
75 #[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 None
131 }
132 }
133
134 #[gtk::template_callbacks]
135 impl Avatar {
136 fn size(&self) -> i32 {
138 self.avatar.size()
139 }
140
141 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 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 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 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 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 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 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 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 fn needs_small_paintable(&self) -> bool {
345 AvatarPaintableSize::from(self.size()) == AvatarPaintableSize::Small
346 }
347
348 #[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 fn update_paintable(&self) {
361 let _old_paintable_ref = self.paintable_ref.take();
362
363 if !self.can_show_image() {
364 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 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 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 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 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 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}