fractal/components/avatar/
image.rs1use 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#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, glib::Enum)]
29#[enum_type(name = "AvatarUriSource")]
30pub enum AvatarUriSource {
31 #[default]
33 User,
34 Room,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub(crate) enum AvatarPaintableSize {
41 Small,
43 Big,
45}
46
47impl AvatarPaintableSize {
48 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 #[property(get, construct_only)]
84 session: OnceCell<Session>,
85 uri: RefCell<Option<OwnedMxcUri>>,
87 #[property(get = Self::uri_string)]
89 uri_string: PhantomData<Option<String>>,
90 info: RefCell<Option<ImageInfo>>,
92 #[property(get, construct_only, builder(AvatarUriSource::default()))]
94 uri_source: Cell<AvatarUriSource>,
95 #[property(get, set = Self::set_scale_factor, explicit_notify, default = 1, minimum = 1)]
97 scale_factor: Cell<u32>,
98 small_paintable_ref: OnceCell<CountedRef>,
103 #[property(get)]
105 small_paintable: RefCell<Option<gdk::Paintable>>,
106 big_paintable_ref: OnceCell<CountedRef>,
111 #[property(get)]
113 big_paintable: RefCell<Option<gdk::Paintable>>,
114 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 pub(super) fn uri(&self) -> Option<OwnedMxcUri> {
155 self.uri.borrow().clone()
156 }
157
158 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 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 self.error.take();
198 }
199 }
200
201 fn uri_string(&self) -> Option<String> {
203 self.uri.borrow().as_ref().map(ToString::to_string)
204 }
205
206 pub(super) fn info(&self) -> Option<ImageInfo> {
208 self.info.borrow().clone()
209 }
210
211 pub(super) fn set_info(&self, info: Option<ImageInfo>) {
213 self.info.replace(info);
214 }
215
216 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 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 self.error.take();
255 }
256 }
257
258 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 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 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 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 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 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 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 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 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 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 pub struct AvatarImage(ObjectSubclass<imp::AvatarImage>);
427}
428
429impl AvatarImage {
430 pub(crate) const SMALL_PAINTABLE_SIZE: u32 = 48;
439
440 pub(crate) const BIG_PAINTABLE_SIZE: u32 = 150;
447
448 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 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 pub(crate) fn uri(&self) -> Option<OwnedMxcUri> {
474 self.imp().uri()
475 }
476
477 pub(crate) fn small_paintable_ref(&self) -> CountedRef {
479 self.imp().small_paintable_ref().clone()
480 }
481
482 pub(crate) fn big_paintable_ref(&self) -> CountedRef {
484 self.imp().big_paintable_ref().clone()
485 }
486
487 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 pub(crate) fn error(&self) -> Option<ImageError> {
515 self.imp().error.get()
516 }
517
518 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}