1 /*
2  * Copyright (C) 2020 Purism SPC
3  * Copyright (C) 2020 Felipe Borges
4  *
5  * Authors:
6  * Felipe Borges <felipeborges@gnome.org>
7  * Julian Sparber <julian@sparber.net>
8  *
9  * SPDX-License-Identifier: LGPL-2.1+
10  *
11  */
12 
13 #include "config.h"
14 #include <math.h>
15 
16 #include "hdy-avatar.h"
17 #include "hdy-avatar-icon-private.h"
18 #include "hdy-cairo-private.h"
19 #include "hdy-css-private.h"
20 
21 #define NUMBER_OF_COLORS 14
22 #define LOAD_BUFFER_SIZE 65536
23 /**
24  * SECTION:hdy-avatar
25  * @short_description: A widget displaying an image, with a generated fallback.
26  * @Title: HdyAvatar
27  *
28  * #HdyAvatar is a widget to display a round avatar.
29  * A provided image is made round before displaying, if no image is given this
30  * widget generates a round fallback with the initials of the #HdyAvatar:text
31  * on top of a colord background.
32  * The color is picked based on the hash of the #HdyAvatar:text.
33  * If #HdyAvatar:show-initials is set to %FALSE, `avatar-default-symbolic` is
34  * shown in place of the initials.
35  * Use hdy_avatar_set_loadable_icon() or #HdyAvatar:loadable-icon to set a
36  * custom image.
37  *
38  * # CSS nodes
39  *
40  * #HdyAvatar has a single CSS node with name avatar.
41  *
42  */
43 
44 struct _HdyAvatar
45 {
46   GtkDrawingArea parent_instance;
47 
48   gchar *icon_name;
49   gchar *text;
50   PangoLayout *layout;
51   gboolean show_initials;
52   guint color_class;
53   gint size;
54   GdkPixbuf *round_image;
55 
56   HdyAvatarIcon *load_func_icon;
57   GLoadableIcon *icon;
58   GCancellable *cancellable;
59   guint currently_loading_size;
60   gboolean loading_error;
61 };
62 
63 G_DEFINE_TYPE (HdyAvatar, hdy_avatar, GTK_TYPE_DRAWING_AREA);
64 
65 enum {
66   PROP_0,
67   PROP_ICON_NAME,
68   PROP_TEXT,
69   PROP_SHOW_INITIALS,
70   PROP_SIZE,
71   PROP_LOADABLE_ICON,
72   PROP_LAST_PROP,
73 };
74 static GParamSpec *props[PROP_LAST_PROP];
75 
76 typedef struct {
77   gint size;
78   gint scale_factor;
79 } SizeData;
80 
81 static void
size_data_free(SizeData * data)82 size_data_free (SizeData *data)
83 {
84   g_slice_free (SizeData, data);
85 }
86 
87 static void
88 load_icon_async (HdyAvatar           *self,
89                  gint                 size,
90                  GCancellable        *cancellable,
91                  GAsyncReadyCallback  callback,
92                  gpointer             user_data);
93 
94 static inline GLoadableIcon *
get_icon(HdyAvatar * self)95 get_icon (HdyAvatar *self)
96 {
97   if (self->icon)
98     return self->icon;
99 
100   return G_LOADABLE_ICON (self->load_func_icon);
101 }
102 
103 static inline gboolean
is_scaled(GdkPixbuf * pixbuf)104 is_scaled (GdkPixbuf *pixbuf)
105 {
106   return (pixbuf && g_object_get_data (G_OBJECT (pixbuf), "scaled") != NULL);
107 }
108 
109 static GdkPixbuf *
make_round_image(GdkPixbuf * pixbuf,gdouble size)110 make_round_image (GdkPixbuf *pixbuf,
111                   gdouble    size)
112 {
113   g_autoptr (cairo_surface_t) surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, size, size);
114   g_autoptr (cairo_t) cr = cairo_create (surface);
115   gint width = gdk_pixbuf_get_width (pixbuf);
116   gint height = gdk_pixbuf_get_height (pixbuf);
117 
118   /* Clip a circle */
119   cairo_arc (cr, size / 2.0, size / 2.0, size / 2.0, 0, 2 * G_PI);
120   cairo_clip (cr);
121   cairo_new_path (cr);
122 
123   gdk_cairo_set_source_pixbuf (cr, pixbuf, (size - width) / 2, (size - height) / 2);
124   cairo_paint (cr);
125 
126   return gdk_pixbuf_get_from_surface (surface, 0, 0, size, size);
127 }
128 
129 static gchar *
extract_initials_from_text(const gchar * text)130 extract_initials_from_text (const gchar *text)
131 {
132   GString *initials;
133   g_autofree gchar *p = g_utf8_strup (text, -1);
134   g_autofree gchar *normalized = g_utf8_normalize (g_strstrip (p), -1, G_NORMALIZE_DEFAULT_COMPOSE);
135   gunichar unichar;
136   gchar *q = NULL;
137 
138   if (normalized == NULL)
139     return NULL;
140 
141   initials = g_string_new ("");
142 
143   unichar = g_utf8_get_char (normalized);
144   g_string_append_unichar (initials, unichar);
145 
146   q = g_utf8_strrchr (normalized, -1, ' ');
147   if (q != NULL && g_utf8_next_char (q) != NULL) {
148     q = g_utf8_next_char (q);
149 
150     unichar = g_utf8_get_char (q);
151     g_string_append_unichar (initials, unichar);
152   }
153 
154   return g_string_free (initials, FALSE);
155 }
156 
157 static GdkPixbuf *
update_custom_image(GdkPixbuf * pixbuf_from_icon,GdkPixbuf * round_image,gint new_size)158 update_custom_image (GdkPixbuf *pixbuf_from_icon,
159                      GdkPixbuf *round_image,
160                      gint       new_size)
161 {
162   if (round_image &&
163       gdk_pixbuf_get_width (round_image) == new_size &&
164       !is_scaled (round_image))
165     return g_object_ref (round_image);
166 
167   if (pixbuf_from_icon) {
168     gint pixbuf_from_icon_size = MIN (gdk_pixbuf_get_width (pixbuf_from_icon),
169                                       gdk_pixbuf_get_height (pixbuf_from_icon));
170     if (pixbuf_from_icon_size == new_size)
171       return make_round_image (pixbuf_from_icon, new_size);
172   }
173 
174   if (round_image) {
175     /* Use a scaled image till we get the new image from async loading */
176     GdkPixbuf *pixbuf = gdk_pixbuf_scale_simple (round_image,
177                                                  new_size,
178                                                  new_size,
179                                                  GDK_INTERP_BILINEAR);
180     g_object_set_data (G_OBJECT (pixbuf), "scaled", GINT_TO_POINTER (TRUE));
181 
182     return pixbuf;
183   }
184 
185   return NULL;
186 }
187 
188 static void
size_prepared_cb(GdkPixbufLoader * loader,gint width,gint height,gpointer user_data)189 size_prepared_cb (GdkPixbufLoader *loader,
190                   gint             width,
191                   gint             height,
192                   gpointer         user_data)
193 {
194   gint size = GPOINTER_TO_INT (user_data);
195   gdouble ratio = (gdouble) width / (gdouble) height;
196 
197   if (width < height) {
198     width = size;
199     height = size / ratio;
200   } else {
201     width = size * ratio;
202     height = size;
203   }
204 
205   gdk_pixbuf_loader_set_size (loader, width, height);
206 }
207 
208 /* This function is copied from the gdk-pixbuf project,
209  * from the file gdk-pixbuf/gdk-pixbuf/gdk-pixbuf-io.c.
210  * It was modified to fit libhandy's code style.
211  */
212 static void
load_from_stream_async_cb(GObject * stream,GAsyncResult * res,gpointer data)213 load_from_stream_async_cb (GObject      *stream,
214                            GAsyncResult *res,
215                            gpointer      data)
216 {
217   g_autoptr (GTask) task = data;
218   GdkPixbufLoader *loader = g_task_get_task_data (task);
219   g_autoptr (GBytes) bytes = NULL;
220   GError *error = NULL;
221 
222   bytes = g_input_stream_read_bytes_finish (G_INPUT_STREAM (stream), res, &error);
223   if (bytes == NULL) {
224     gdk_pixbuf_loader_close (loader, NULL);
225     g_task_return_error (task, error);
226 
227     return;
228   }
229 
230   if (g_bytes_get_size (bytes) == 0) {
231     if (!gdk_pixbuf_loader_close (loader, &error)) {
232       g_task_return_error (task, error);
233 
234       return;
235     }
236 
237     g_task_return_pointer (task,
238                            g_object_ref (gdk_pixbuf_loader_get_pixbuf (loader)),
239                            g_object_unref);
240 
241     return;
242   }
243 
244   if (!gdk_pixbuf_loader_write (loader,
245                                 g_bytes_get_data (bytes, NULL),
246                                 g_bytes_get_size (bytes),
247                                 &error)) {
248     gdk_pixbuf_loader_close (loader, NULL);
249     g_task_return_error (task, error);
250 
251     return;
252   }
253 
254   g_input_stream_read_bytes_async (G_INPUT_STREAM (stream),
255                                    LOAD_BUFFER_SIZE,
256                                    G_PRIORITY_DEFAULT,
257                                    g_task_get_cancellable (task),
258                                    load_from_stream_async_cb,
259                                    g_object_ref (task));
260 }
261 
262 static void
icon_load_async_cb(GLoadableIcon * icon,GAsyncResult * res,GTask * task)263 icon_load_async_cb (GLoadableIcon *icon,
264                     GAsyncResult  *res,
265                     GTask         *task)
266 {
267   GdkPixbufLoader *loader = g_task_get_task_data (task);
268   g_autoptr (GInputStream) stream = NULL;
269   g_autoptr (GError) error = NULL;
270 
271   stream = g_loadable_icon_load_finish (icon, res, NULL, &error);
272   if (stream == NULL) {
273     gdk_pixbuf_loader_close (loader, NULL);
274     g_task_return_error (task, g_steal_pointer (&error));
275     g_object_unref (task);
276 
277     return;
278   }
279 
280   g_input_stream_read_bytes_async (stream,
281                                    LOAD_BUFFER_SIZE,
282                                    G_PRIORITY_DEFAULT,
283                                    g_task_get_cancellable (task),
284                                    load_from_stream_async_cb,
285                                    task);
286 }
287 
288 static GdkPixbuf *
load_from_gicon_async_finish(GAsyncResult * async_result,GError ** error)289 load_from_gicon_async_finish (GAsyncResult  *async_result,
290                               GError       **error)
291 {
292   GTask *task = G_TASK (async_result);
293 
294   return g_task_propagate_pointer (task, error);
295 }
296 
297 static void
load_from_gicon_async_for_display_cb(HdyAvatar * self,GAsyncResult * res,gpointer * user_data)298 load_from_gicon_async_for_display_cb (HdyAvatar    *self,
299                                       GAsyncResult *res,
300                                       gpointer     *user_data)
301 {
302   g_autoptr (GError) error = NULL;
303   g_autoptr (GdkPixbuf) pixbuf = NULL;
304 
305   pixbuf = load_from_gicon_async_finish (res, &error);
306 
307   if (error != NULL) {
308     if (g_error_matches (error, HDY_AVATAR_ICON_ERROR, HDY_AVATAR_ICON_ERROR_EMPTY)) {
309       self->loading_error = TRUE;
310     } else if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
311       g_warning ("Failed to load icon: %s", error->message);
312       self->loading_error = TRUE;
313     }
314   }
315 
316   self->currently_loading_size = -1;
317 
318   if (pixbuf) {
319     g_autoptr (GdkPixbuf) custom_image = NULL;
320     GtkStyleContext *context = gtk_widget_get_style_context (GTK_WIDGET (self));
321     gint width = gtk_widget_get_allocated_width (GTK_WIDGET (self));
322     gint height = gtk_widget_get_allocated_height (GTK_WIDGET (self));
323     gint scale_factor = gtk_widget_get_scale_factor (GTK_WIDGET (self));
324     gint new_size = MIN (width, height) * scale_factor;
325 
326     if (get_icon (self)) {
327       custom_image = update_custom_image (pixbuf,
328                                           NULL,
329                                           new_size);
330 
331       if (!self->round_image && custom_image)
332         gtk_style_context_add_class (context, "image");
333     }
334 
335     g_set_object (&self->round_image, custom_image);
336     gtk_widget_queue_draw (GTK_WIDGET (self));
337   }
338 }
339 
340 static void
load_from_gicon_async_for_export_cb(HdyAvatar * self,GAsyncResult * res,gpointer * user_data)341 load_from_gicon_async_for_export_cb (HdyAvatar    *self,
342                                      GAsyncResult *res,
343                                      gpointer     *user_data)
344 {
345   GTask *task = G_TASK (user_data);
346   g_autoptr (GError) error = NULL;
347   g_autoptr (GdkPixbuf) pixbuf = NULL;
348 
349   pixbuf = load_from_gicon_async_finish (res, &error);
350 
351   if (error &&
352       !g_error_matches (error, HDY_AVATAR_ICON_ERROR, HDY_AVATAR_ICON_ERROR_EMPTY) &&
353       !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
354     g_warning ("Failed to load icon: %s", error->message);
355   }
356 
357   g_task_return_pointer (task,
358                          g_steal_pointer (&pixbuf),
359                          g_object_unref);
360   g_object_unref (task);
361 }
362 
363 static void
load_icon_async(HdyAvatar * self,gint size,GCancellable * cancellable,GAsyncReadyCallback callback,gpointer user_data)364 load_icon_async (HdyAvatar           *self,
365                  gint                 size,
366                  GCancellable        *cancellable,
367                  GAsyncReadyCallback  callback,
368                  gpointer             user_data)
369 {
370   GTask *task = g_task_new (self, cancellable, callback, user_data);
371   GdkPixbufLoader *loader = gdk_pixbuf_loader_new ();
372 
373   g_signal_connect (loader, "size-prepared",
374                     G_CALLBACK (size_prepared_cb),
375                     GINT_TO_POINTER (size));
376 
377   g_task_set_task_data (task, loader, g_object_unref);
378 
379   g_loadable_icon_load_async (get_icon (self),
380                               size,
381                               cancellable,
382                               (GAsyncReadyCallback) icon_load_async_cb,
383                               task);
384 }
385 
386 /* This function is copied from the gdk-pixbuf project,
387  * from the file gdk-pixbuf/gdk-pixbuf/gdk-pixbuf-io.c.
388  * It was modified to fit libhandy's code style.
389  */
390 static GdkPixbuf *
load_from_stream(GdkPixbufLoader * loader,GInputStream * stream,GCancellable * cancellable,GError ** error)391 load_from_stream (GdkPixbufLoader  *loader,
392                   GInputStream     *stream,
393                   GCancellable     *cancellable,
394                   GError          **error)
395 {
396   GdkPixbuf *pixbuf;
397   guchar buffer[LOAD_BUFFER_SIZE];
398 
399   while (TRUE) {
400     gssize n_read = g_input_stream_read (stream, buffer, sizeof (buffer),
401                                          cancellable, error);
402 
403     if (n_read < 0) {
404       gdk_pixbuf_loader_close (loader, NULL);
405 
406       return NULL;
407     }
408 
409     if (n_read == 0)
410       break;
411 
412     if (!gdk_pixbuf_loader_write (loader, buffer, n_read, error)) {
413       gdk_pixbuf_loader_close (loader, NULL);
414 
415       return NULL;
416     }
417   }
418 
419   if (!gdk_pixbuf_loader_close (loader, error))
420     return NULL;
421 
422   pixbuf = gdk_pixbuf_loader_get_pixbuf (loader);
423   if (pixbuf == NULL)
424     return NULL;
425 
426   return g_object_ref (pixbuf);
427 }
428 
429 static GdkPixbuf *
load_icon_sync(GLoadableIcon * icon,gint size)430 load_icon_sync (GLoadableIcon *icon,
431                 gint           size)
432 {
433   g_autoptr (GError) error = NULL;
434   g_autoptr (GInputStream) stream = g_loadable_icon_load (icon, size, NULL, NULL, &error);
435   g_autoptr (GdkPixbufLoader) loader = gdk_pixbuf_loader_new ();
436   g_autoptr (GdkPixbuf) pixbuf = NULL;
437 
438   if (error) {
439     g_warning ("Failed to load icon: %s", error->message);
440     return NULL;
441   }
442 
443   g_signal_connect (loader, "size-prepared",
444                     G_CALLBACK (size_prepared_cb),
445                     GINT_TO_POINTER (size));
446 
447   pixbuf = load_from_stream (loader, stream, NULL, &error);
448 
449   if (error) {
450     g_warning ("Failed to load pixbuf from GLoadableIcon: %s", error->message);
451     return NULL;
452   }
453 
454   return g_steal_pointer (&pixbuf);
455 }
456 
457 static void
set_class_color(HdyAvatar * self)458 set_class_color (HdyAvatar *self)
459 {
460   GtkStyleContext *context = gtk_widget_get_style_context (GTK_WIDGET (self));
461   g_autofree GRand *rand = NULL;
462   g_autofree gchar *new_class = NULL;
463   g_autofree gchar *old_class = g_strdup_printf ("color%d", self->color_class);
464 
465   gtk_style_context_remove_class (context, old_class);
466 
467   if (self->text == NULL || strlen (self->text) == 0) {
468     /* Use a random color if we don't have a text */
469     rand = g_rand_new ();
470     self->color_class = g_rand_int_range (rand, 1, NUMBER_OF_COLORS);
471   } else {
472     self->color_class = (g_str_hash (self->text) % NUMBER_OF_COLORS) + 1;
473   }
474 
475   new_class = g_strdup_printf ("color%d", self->color_class);
476   gtk_style_context_add_class (context, new_class);
477 }
478 
479 static void
set_class_contrasted(HdyAvatar * self,gint size)480 set_class_contrasted (HdyAvatar *self,
481                       gint       size)
482 {
483   GtkStyleContext *context = gtk_widget_get_style_context (GTK_WIDGET (self));
484 
485   if (size < 25)
486     gtk_style_context_add_class (context, "contrasted");
487   else
488     gtk_style_context_remove_class (context, "contrasted");
489 }
490 
491 static void
clear_pango_layout(HdyAvatar * self)492 clear_pango_layout (HdyAvatar *self)
493 {
494   g_clear_object (&self->layout);
495 }
496 
497 static void
ensure_pango_layout(HdyAvatar * self)498 ensure_pango_layout (HdyAvatar *self)
499 {
500   g_autofree gchar *initials = NULL;
501 
502   if (self->layout != NULL || self->text == NULL || strlen (self->text) == 0)
503     return;
504 
505   initials = extract_initials_from_text (self->text);
506   self->layout = gtk_widget_create_pango_layout (GTK_WIDGET (self), initials);
507 }
508 
509 static void
set_font_size(HdyAvatar * self,gint size)510 set_font_size (HdyAvatar *self,
511                gint       size)
512 {
513   GtkStyleContext *context;
514   PangoFontDescription *font_desc;
515   gint width, height;
516   gdouble padding;
517   gdouble sqr_size;
518   gdouble max_size;
519   gdouble new_font_size;
520 
521   if (self->round_image != NULL || self->layout == NULL)
522     return;
523 
524   context = gtk_widget_get_style_context (GTK_WIDGET (self));
525   gtk_style_context_get (context, gtk_style_context_get_state (context),
526                          "font", &font_desc, NULL);
527 
528   pango_layout_set_font_description (self->layout, font_desc);
529   pango_layout_get_pixel_size (self->layout, &width, &height);
530 
531   /* This is the size of the biggest square fitting inside the circle */
532   sqr_size = (gdouble)size / 1.4142;
533   /* The padding has to be a function of the overall size.
534    * The 0.4 is how steep the linear function grows and the -5 is just
535    * an adjustment for smaller sizes which doesn't have a big impact on bigger sizes.
536    * Make also sure we don't have a negative padding */
537   padding = MAX (size * 0.4 - 5, 0);
538   max_size = sqr_size - padding;
539   new_font_size = (gdouble)height * (max_size / (gdouble)width);
540 
541   font_desc = pango_font_description_copy (font_desc);
542   pango_font_description_set_absolute_size (font_desc,
543                                             CLAMP (new_font_size, 0, max_size) * PANGO_SCALE);
544   pango_layout_set_font_description (self->layout, font_desc);
545   pango_font_description_free (font_desc);
546 }
547 
548 static void
hdy_avatar_get_property(GObject * object,guint property_id,GValue * value,GParamSpec * pspec)549 hdy_avatar_get_property (GObject    *object,
550                          guint       property_id,
551                          GValue     *value,
552                          GParamSpec *pspec)
553 {
554   HdyAvatar *self = HDY_AVATAR (object);
555 
556   switch (property_id) {
557   case PROP_ICON_NAME:
558     g_value_set_string (value, hdy_avatar_get_icon_name (self));
559     break;
560 
561   case PROP_TEXT:
562     g_value_set_string (value, hdy_avatar_get_text (self));
563     break;
564 
565   case PROP_SHOW_INITIALS:
566     g_value_set_boolean (value, hdy_avatar_get_show_initials (self));
567     break;
568 
569   case PROP_SIZE:
570     g_value_set_int (value, hdy_avatar_get_size (self));
571     break;
572 
573   case PROP_LOADABLE_ICON:
574     g_value_set_object (value, hdy_avatar_get_loadable_icon (self));
575     break;
576 
577   default:
578     G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
579     break;
580   }
581 }
582 
583 static void
hdy_avatar_set_property(GObject * object,guint property_id,const GValue * value,GParamSpec * pspec)584 hdy_avatar_set_property (GObject      *object,
585                          guint         property_id,
586                          const GValue *value,
587                          GParamSpec   *pspec)
588 {
589   HdyAvatar *self = HDY_AVATAR (object);
590 
591   switch (property_id) {
592   case PROP_ICON_NAME:
593     hdy_avatar_set_icon_name (self, g_value_get_string (value));
594     break;
595 
596   case PROP_TEXT:
597     hdy_avatar_set_text (self, g_value_get_string (value));
598     break;
599 
600   case PROP_SHOW_INITIALS:
601     hdy_avatar_set_show_initials (self, g_value_get_boolean (value));
602     break;
603 
604   case PROP_SIZE:
605     hdy_avatar_set_size (self, g_value_get_int (value));
606     break;
607 
608   case PROP_LOADABLE_ICON:
609     hdy_avatar_set_loadable_icon (self, g_value_get_object (value));
610     break;
611 
612   default:
613     G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
614     break;
615   }
616 }
617 
618 static void
hdy_avatar_dispose(GObject * object)619 hdy_avatar_dispose (GObject *object)
620 {
621   HdyAvatar *self = HDY_AVATAR (object);
622 
623   g_cancellable_cancel (self->cancellable);
624   g_clear_object (&self->icon);
625   g_clear_object (&self->load_func_icon);
626 
627   G_OBJECT_CLASS (hdy_avatar_parent_class)->dispose (object);
628 }
629 
630 static void
hdy_avatar_finalize(GObject * object)631 hdy_avatar_finalize (GObject *object)
632 {
633   HdyAvatar *self = HDY_AVATAR (object);
634 
635   g_clear_pointer (&self->icon_name, g_free);
636   g_clear_pointer (&self->text, g_free);
637   g_clear_object (&self->round_image);
638   g_clear_object (&self->layout);
639   g_clear_object (&self->cancellable);
640 
641   G_OBJECT_CLASS (hdy_avatar_parent_class)->finalize (object);
642 }
643 
644 static void
draw_for_size(HdyAvatar * self,cairo_t * cr,GdkPixbuf * custom_image,gint width,gint height,gint scale_factor)645 draw_for_size (HdyAvatar *self,
646                cairo_t   *cr,
647                GdkPixbuf *custom_image,
648                gint       width,
649                gint       height,
650                gint       scale_factor)
651 {
652   GtkStyleContext *context = gtk_widget_get_style_context (GTK_WIDGET (self));
653   gint size = MIN (width, height);
654   gdouble x = (gdouble)(width - size) / 2.0;
655   gdouble y = (gdouble)(height - size) / 2.0;
656   const gchar *icon_name;
657   GdkRGBA color;
658   g_autoptr (GtkIconInfo) icon = NULL;
659   g_autoptr (GdkPixbuf) pixbuf = NULL;
660   g_autoptr (GError) error = NULL;
661   g_autoptr (cairo_surface_t) surface = NULL;
662 
663   set_class_contrasted (self, size);
664 
665   if (custom_image) {
666     surface = gdk_cairo_surface_create_from_pixbuf (custom_image, scale_factor,
667                                                     gtk_widget_get_window (GTK_WIDGET (self)));
668     gtk_render_icon_surface (context, cr, surface, x, y);
669     gtk_render_background (context, cr, x, y, size, size);
670     gtk_render_frame (context, cr, x, y, size, size);
671     return;
672   }
673 
674   gtk_render_background (context, cr, x, y, size, size);
675   gtk_render_frame (context, cr, x, y, size, size);
676 
677   ensure_pango_layout (self);
678 
679   if (self->show_initials && self->layout != NULL) {
680     set_font_size (self, size);
681     pango_layout_get_pixel_size (self->layout, &width, &height);
682 
683     gtk_render_layout (context, cr,
684                        ((gdouble) (size - width) / 2.0) + x,
685                        ((gdouble) (size - height) / 2.0) + y,
686                        self->layout);
687     return;
688   }
689 
690   icon_name = self->icon_name && *self->icon_name != '\0' ?
691     self->icon_name : "avatar-default-symbolic";
692   icon = gtk_icon_theme_lookup_icon_for_scale (gtk_icon_theme_get_default (),
693                                      icon_name,
694                                      size / 2, scale_factor,
695                                      GTK_ICON_LOOKUP_FORCE_SYMBOLIC);
696   if (icon == NULL) {
697     g_critical ("Failed to load icon `%s'", icon_name);
698     return;
699   }
700 
701   gtk_style_context_get_color (context, gtk_style_context_get_state (context), &color);
702   pixbuf = gtk_icon_info_load_symbolic (icon, &color, NULL, NULL, NULL, NULL, &error);
703   if (error != NULL) {
704     g_critical ("Failed to load icon `%s': %s", icon_name, error->message);
705     return;
706   }
707 
708   surface = gdk_cairo_surface_create_from_pixbuf (pixbuf, scale_factor,
709                                                   gtk_widget_get_window (GTK_WIDGET (self)));
710 
711   width = cairo_image_surface_get_width (surface);
712   height = cairo_image_surface_get_height (surface);
713   gtk_render_icon_surface (context, cr, surface,
714                            (((gdouble) size - ((gdouble) width / (gdouble) scale_factor)) / 2.0) + x,
715                            (((gdouble) size - ((gdouble) height / (gdouble) scale_factor)) / 2.0) + y);
716 }
717 
718 static gboolean
hdy_avatar_draw(GtkWidget * widget,cairo_t * cr)719 hdy_avatar_draw (GtkWidget *widget,
720                  cairo_t   *cr)
721 {
722   HdyAvatar *self = HDY_AVATAR (widget);
723   GdkPixbuf *custom_image = NULL;
724   GtkStyleContext *context = gtk_widget_get_style_context (widget);
725   gint width = gtk_widget_get_allocated_width (widget);
726   gint height = gtk_widget_get_allocated_height (widget);
727   gint scale_factor = gtk_widget_get_scale_factor (widget);
728   gint new_size = MIN (width, height) * scale_factor;
729 
730   if (get_icon (self)) {
731     custom_image = update_custom_image (NULL, self->round_image, new_size);
732 
733     if ((!custom_image &&
734         !self->loading_error) ||
735         (self->currently_loading_size != new_size &&
736         is_scaled (custom_image))) {
737       self->currently_loading_size = new_size;
738       g_cancellable_cancel (self->cancellable);
739       g_set_object (&self->cancellable, g_cancellable_new ());
740       load_icon_async (self,
741                        new_size,
742                        self->cancellable,
743                        (GAsyncReadyCallback) load_from_gicon_async_for_display_cb,
744                        NULL);
745     }
746 
747     /* We don't want to draw a broken custom image, because it may be scaled
748        and we prefer to use the generated one in this case */
749     if (self->loading_error)
750       g_clear_object (&custom_image);
751   }
752 
753   if (self->round_image && !custom_image)
754     gtk_style_context_remove_class (context, "image");
755 
756   if (!self->round_image && custom_image)
757     gtk_style_context_add_class (context, "image");
758 
759   g_set_object (&self->round_image, custom_image);
760   draw_for_size (self, cr, self->round_image, width, height, scale_factor);
761 
762   return FALSE;
763 }
764 
765 /* This private method is prefixed by the class name because it will be a
766  * virtual method in GTK 4.
767  */
768 static void
hdy_avatar_measure(GtkWidget * widget,GtkOrientation orientation,gint for_size,gint * minimum,gint * natural,gint * minimum_baseline,gint * natural_baseline)769 hdy_avatar_measure (GtkWidget      *widget,
770                     GtkOrientation  orientation,
771                     gint            for_size,
772                     gint           *minimum,
773                     gint           *natural,
774                     gint           *minimum_baseline,
775                     gint           *natural_baseline)
776 {
777   HdyAvatar *self = HDY_AVATAR (widget);
778 
779   if (minimum)
780     *minimum = self->size;
781   if (natural)
782     *natural = self->size;
783 
784   hdy_css_measure (widget, orientation, minimum, natural);
785 }
786 
787 static void
hdy_avatar_get_preferred_width(GtkWidget * widget,gint * minimum,gint * natural)788 hdy_avatar_get_preferred_width (GtkWidget *widget,
789                                 gint      *minimum,
790                                 gint      *natural)
791 {
792   hdy_avatar_measure (widget, GTK_ORIENTATION_HORIZONTAL, -1,
793                       minimum, natural, NULL, NULL);
794 }
795 
796 static void
hdy_avatar_get_preferred_width_for_height(GtkWidget * widget,gint height,gint * minimum,gint * natural)797 hdy_avatar_get_preferred_width_for_height (GtkWidget *widget,
798                                            gint       height,
799                                            gint      *minimum,
800                                            gint      *natural)
801 {
802   hdy_avatar_measure (widget, GTK_ORIENTATION_HORIZONTAL, height,
803                       minimum, natural, NULL, NULL);
804 }
805 
806 static void
hdy_avatar_get_preferred_height(GtkWidget * widget,gint * minimum,gint * natural)807 hdy_avatar_get_preferred_height (GtkWidget *widget,
808                                  gint      *minimum,
809                                  gint      *natural)
810 {
811   hdy_avatar_measure (widget, GTK_ORIENTATION_VERTICAL, -1,
812                       minimum, natural, NULL, NULL);
813 }
814 
815 static void
hdy_avatar_get_preferred_height_for_width(GtkWidget * widget,gint width,gint * minimum,gint * natural)816 hdy_avatar_get_preferred_height_for_width (GtkWidget *widget,
817                                            gint       width,
818                                            gint      *minimum,
819                                            gint      *natural)
820 {
821   hdy_avatar_measure (widget, GTK_ORIENTATION_VERTICAL, width,
822                       minimum, natural, NULL, NULL);
823 }
824 
825 static GtkSizeRequestMode
hdy_avatar_get_request_mode(GtkWidget * widget)826 hdy_avatar_get_request_mode (GtkWidget *widget)
827 {
828   return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH;
829 }
830 
831 static void
hdy_avatar_size_allocate(GtkWidget * widget,GtkAllocation * allocation)832 hdy_avatar_size_allocate (GtkWidget     *widget,
833                           GtkAllocation *allocation)
834 {
835   GtkAllocation clip;
836 
837   hdy_css_size_allocate_self (widget, allocation);
838   gtk_widget_set_allocation (widget, allocation);
839 
840   gtk_render_background_get_clip (gtk_widget_get_style_context (widget),
841                                   allocation->x,
842                                   allocation->y,
843                                   allocation->width,
844                                   allocation->height,
845                                   &clip);
846 
847   GTK_WIDGET_CLASS (hdy_avatar_parent_class)->size_allocate (widget, allocation);
848   gtk_widget_set_clip (widget, &clip);
849 }
850 
851 static void
hdy_avatar_class_init(HdyAvatarClass * klass)852 hdy_avatar_class_init (HdyAvatarClass *klass)
853 {
854   GObjectClass *object_class = G_OBJECT_CLASS (klass);
855   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
856 
857   object_class->dispose = hdy_avatar_dispose;
858   object_class->finalize = hdy_avatar_finalize;
859   object_class->set_property = hdy_avatar_set_property;
860   object_class->get_property = hdy_avatar_get_property;
861 
862   widget_class->draw = hdy_avatar_draw;
863   widget_class->get_request_mode = hdy_avatar_get_request_mode;
864   widget_class->get_preferred_width = hdy_avatar_get_preferred_width;
865   widget_class->get_preferred_height = hdy_avatar_get_preferred_height;
866   widget_class->get_preferred_width_for_height = hdy_avatar_get_preferred_width_for_height;
867   widget_class->get_preferred_height_for_width = hdy_avatar_get_preferred_height_for_width;
868   widget_class->size_allocate = hdy_avatar_size_allocate;
869 
870   /**
871    * HdyAvatar:size:
872    *
873    * The avatar size of the avatar.
874    */
875   props[PROP_SIZE] =
876     g_param_spec_int ("size",
877                       "Size",
878                       "The size of the avatar",
879                       -1, INT_MAX, -1,
880                       G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
881 
882   /**
883    * HdyAvatar:icon-name:
884    *
885    * The name of the icon in the icon theme to use when the icon should be
886    * displayed.
887    * If no name is set, the avatar-default-symbolic icon will be used.
888    * If the name doesn't match a valid icon, it is an error and no icon will be
889    * displayed.
890    * If the icon theme is changed, the image will be updated automatically.
891    *
892    * Since: 1.0
893    */
894   props[PROP_ICON_NAME] =
895     g_param_spec_string ("icon-name",
896                          "Icon name",
897                          "The name of the icon from the icon theme",
898                          NULL,
899                          G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
900 
901   /**
902    * HdyAvatar:text:
903    *
904    * The text used for the initials and for generating the color.
905    * If #HdyAvatar:show-initials is %FALSE it's only used to generate the color.
906    */
907   props[PROP_TEXT] =
908     g_param_spec_string ("text",
909                          "Text",
910                          "The text used to generate the color and the initials",
911                          NULL,
912                          G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
913 
914   /**
915    * HdyAvatar:show_initials:
916    *
917    * Whether to show the initials or the fallback icon on the generated avatar.
918    */
919   props[PROP_SHOW_INITIALS] =
920     g_param_spec_boolean ("show-initials",
921                           "Show initials",
922                           "Whether to show the initials",
923                           FALSE,
924                           G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
925 
926   /**
927    * HdyAvatar:loadable-icon:
928    *
929    * A #GLoadableIcon used to load the avatar.
930    *
931    * Since: 1.2
932    */
933   props[PROP_LOADABLE_ICON] =
934     g_param_spec_object ("loadable-icon",
935                          "Loadable Icon",
936                          "The loadable icon used to load the avatar",
937                          G_TYPE_LOADABLE_ICON,
938                          G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
939 
940   g_object_class_install_properties (object_class, PROP_LAST_PROP, props);
941 
942   gtk_widget_class_set_css_name (widget_class, "avatar");
943 }
944 
945 static void
hdy_avatar_init(HdyAvatar * self)946 hdy_avatar_init (HdyAvatar *self)
947 {
948   set_class_color (self);
949   g_signal_connect (self, "screen-changed", G_CALLBACK (clear_pango_layout), NULL);
950 }
951 
952 /**
953  * hdy_avatar_new:
954  * @size: The size of the avatar
955  * @text: (nullable): The text used to generate the color and initials if
956  * @show_initials is %TRUE. The color is selected at random if @text is empty.
957  * @show_initials: whether to show the initials or the fallback icon on
958  * top of the color generated based on @text.
959  *
960  * Creates a new #HdyAvatar.
961  *
962  * Returns: the newly created #HdyAvatar
963  */
964 GtkWidget *
hdy_avatar_new(gint size,const gchar * text,gboolean show_initials)965 hdy_avatar_new (gint         size,
966                 const gchar *text,
967                 gboolean     show_initials)
968 {
969   return g_object_new (HDY_TYPE_AVATAR,
970                        "size", size,
971                        "text", text,
972                        "show-initials", show_initials,
973                        NULL);
974 }
975 
976 /**
977  * hdy_avatar_get_icon_name:
978  * @self: a #HdyAvatar
979  *
980  * Gets the name of the icon in the icon theme to use when the icon should be
981  * displayed.
982  *
983  * Returns: (nullable) (transfer none): the name of the icon from the icon theme.
984  *
985  * Since: 1.0
986  */
987 const gchar *
hdy_avatar_get_icon_name(HdyAvatar * self)988 hdy_avatar_get_icon_name (HdyAvatar *self)
989 {
990   g_return_val_if_fail (HDY_IS_AVATAR (self), NULL);
991 
992   return self->icon_name;
993 }
994 
995 /**
996  * hdy_avatar_set_icon_name:
997  * @self: a #HdyAvatar
998  * @icon_name: (nullable): the name of the icon from the icon theme
999  *
1000  * Sets the name of the icon in the icon theme to use when the icon should be
1001  * displayed.
1002  * If no name is set, the avatar-default-symbolic icon will be used.
1003  * If the name doesn't match a valid icon, it is an error and no icon will be
1004  * displayed.
1005  * If the icon theme is changed, the image will be updated automatically.
1006  *
1007  * Since: 1.0
1008  */
1009 void
hdy_avatar_set_icon_name(HdyAvatar * self,const gchar * icon_name)1010 hdy_avatar_set_icon_name (HdyAvatar   *self,
1011                           const gchar *icon_name)
1012 {
1013   g_return_if_fail (HDY_IS_AVATAR (self));
1014 
1015   if (g_strcmp0 (self->icon_name, icon_name) == 0)
1016     return;
1017 
1018   g_clear_pointer (&self->icon_name, g_free);
1019   self->icon_name = g_strdup (icon_name);
1020 
1021   if (!self->round_image &&
1022       (!self->show_initials || self->layout == NULL))
1023     gtk_widget_queue_draw (GTK_WIDGET (self));
1024 
1025   g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ICON_NAME]);
1026 }
1027 
1028 /**
1029  * hdy_avatar_get_text:
1030  * @self: a #HdyAvatar
1031  *
1032  * Get the text used to generate the fallback initials and color
1033  *
1034  * Returns: (nullable) (transfer none): returns the text used to generate
1035  * the fallback initials. This is the internal string used by
1036  * the #HdyAvatar, and must not be modified.
1037  */
1038 const gchar *
hdy_avatar_get_text(HdyAvatar * self)1039 hdy_avatar_get_text (HdyAvatar *self)
1040 {
1041   g_return_val_if_fail (HDY_IS_AVATAR (self), NULL);
1042 
1043   return self->text;
1044 }
1045 
1046 /**
1047  * hdy_avatar_set_text:
1048  * @self: a #HdyAvatar
1049  * @text: (nullable): the text used to get the initials and color
1050  *
1051  * Set the text used to generate the fallback initials color
1052  */
1053 void
hdy_avatar_set_text(HdyAvatar * self,const gchar * text)1054 hdy_avatar_set_text (HdyAvatar   *self,
1055                      const gchar *text)
1056 {
1057   g_return_if_fail (HDY_IS_AVATAR (self));
1058 
1059   if (g_strcmp0 (self->text, text) == 0)
1060     return;
1061 
1062   g_clear_pointer (&self->text, g_free);
1063   self->text = g_strdup (text);
1064 
1065   clear_pango_layout (self);
1066   set_class_color (self);
1067   gtk_widget_queue_draw (GTK_WIDGET (self));
1068 
1069   g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TEXT]);
1070 }
1071 
1072 /**
1073  * hdy_avatar_get_show_initials:
1074  * @self: a #HdyAvatar
1075  *
1076  * Returns whether initials are used for the fallback or the icon.
1077  *
1078  * Returns: %TRUE if the initials are used for the fallback.
1079  */
1080 gboolean
hdy_avatar_get_show_initials(HdyAvatar * self)1081 hdy_avatar_get_show_initials (HdyAvatar *self)
1082 {
1083   g_return_val_if_fail (HDY_IS_AVATAR (self), FALSE);
1084 
1085   return self->show_initials;
1086 }
1087 
1088 /**
1089  * hdy_avatar_set_show_initials:
1090  * @self: a #HdyAvatar
1091  * @show_initials: whether the initials should be shown on the fallback avatar
1092  * or the icon.
1093  *
1094  * Sets whether the initials should be shown on the fallback avatar or the icon.
1095  */
1096 void
hdy_avatar_set_show_initials(HdyAvatar * self,gboolean show_initials)1097 hdy_avatar_set_show_initials (HdyAvatar *self,
1098                               gboolean   show_initials)
1099 {
1100   g_return_if_fail (HDY_IS_AVATAR (self));
1101 
1102   if (self->show_initials == show_initials)
1103     return;
1104 
1105   self->show_initials = show_initials;
1106 
1107   gtk_widget_queue_draw (GTK_WIDGET (self));
1108   g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SHOW_INITIALS]);
1109 }
1110 
1111 /**
1112  * hdy_avatar_set_image_load_func:
1113  * @self: a #HdyAvatar
1114  * @load_image: (closure user_data) (nullable): callback to set a custom image
1115  * @user_data: (nullable): user data passed to @load_image
1116  * @destroy: (nullable): destroy notifier for @user_data
1117  *
1118  * A callback which is called when the custom image need to be reloaded for some
1119  * reason (e.g. scale-factor changes).
1120  *
1121  * Deprecated: 1.2: use hdy_avatar_set_loadable_icon() instead.
1122  */
1123 void
hdy_avatar_set_image_load_func(HdyAvatar * self,HdyAvatarImageLoadFunc load_image,gpointer user_data,GDestroyNotify destroy)1124 hdy_avatar_set_image_load_func (HdyAvatar              *self,
1125                                 HdyAvatarImageLoadFunc  load_image,
1126                                 gpointer                user_data,
1127                                 GDestroyNotify          destroy)
1128 {
1129   g_autoptr (HdyAvatarIcon) icon = NULL;
1130 
1131   g_return_if_fail (HDY_IS_AVATAR (self));
1132   g_return_if_fail (user_data != NULL || (user_data == NULL && destroy == NULL));
1133 
1134   if (load_image != NULL)
1135     icon = hdy_avatar_icon_new (load_image, user_data, destroy);
1136 
1137   if (self->load_func_icon && !self->icon) {
1138     g_cancellable_cancel (self->cancellable);
1139     g_clear_object (&self->cancellable);
1140     self->currently_loading_size = -1;
1141     self->loading_error = FALSE;
1142   }
1143 
1144   g_set_object (&self->load_func_icon, icon);
1145 
1146   /* Don't update the custom avatar when we have a user set GLoadableIcon */
1147   if (self->icon)
1148     return;
1149 
1150   if (self->load_func_icon) {
1151     gint scale_factor = gtk_widget_get_scale_factor (GTK_WIDGET (self));
1152 
1153     self->cancellable = g_cancellable_new ();
1154     self->currently_loading_size = self->size * scale_factor;
1155     load_icon_async (self,
1156                      self->currently_loading_size,
1157                      self->cancellable,
1158                      (GAsyncReadyCallback) load_from_gicon_async_for_display_cb,
1159                      NULL);
1160   } else {
1161     gtk_widget_queue_draw (GTK_WIDGET (self));
1162   }
1163 }
1164 
1165 /**
1166  * hdy_avatar_get_size:
1167  * @self: a #HdyAvatar
1168  *
1169  * Returns the size of the avatar.
1170  *
1171  * Returns: the size of the avatar.
1172  */
1173 gint
hdy_avatar_get_size(HdyAvatar * self)1174 hdy_avatar_get_size (HdyAvatar *self)
1175 {
1176   g_return_val_if_fail (HDY_IS_AVATAR (self), 0);
1177 
1178   return self->size;
1179 }
1180 
1181 /**
1182  * hdy_avatar_set_size:
1183  * @self: a #HdyAvatar
1184  * @size: The size to be used for the avatar
1185  *
1186  * Sets the size of the avatar.
1187  */
1188 void
hdy_avatar_set_size(HdyAvatar * self,gint size)1189 hdy_avatar_set_size (HdyAvatar *self,
1190                      gint       size)
1191 {
1192   g_return_if_fail (HDY_IS_AVATAR (self));
1193   g_return_if_fail (size >= -1);
1194 
1195   if (self->size == size)
1196     return;
1197 
1198   self->size = size;
1199 
1200   gtk_widget_queue_resize (GTK_WIDGET (self));
1201   g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SIZE]);
1202 }
1203 
1204 /**
1205  * hdy_avatar_draw_to_pixbuf:
1206  * @self: a #HdyAvatar
1207  * @size: The size of the pixbuf
1208  * @scale_factor: The scale factor
1209  *
1210  * Renders @self into a pixbuf at @size and @scale_factor. This can be used to export the fallback avatar.
1211  *
1212  * Returns: (transfer full): the pixbuf.
1213  *
1214  * Since: 1.2
1215  */
1216 GdkPixbuf *
hdy_avatar_draw_to_pixbuf(HdyAvatar * self,gint size,gint scale_factor)1217 hdy_avatar_draw_to_pixbuf (HdyAvatar *self,
1218                            gint       size,
1219                            gint       scale_factor)
1220 {
1221   g_autoptr (cairo_surface_t) surface = NULL;
1222   g_autoptr (cairo_t) cr = NULL;
1223   g_autoptr (GdkPixbuf) custom_image = NULL;
1224   g_autoptr (GdkPixbuf) pixbuf_from_icon = NULL;
1225   gint scaled_size = size * scale_factor;
1226   GtkStyleContext *context;
1227   GtkAllocation bounds;
1228 
1229   g_return_val_if_fail (HDY_IS_AVATAR (self), NULL);
1230   g_return_val_if_fail (size > 0, NULL);
1231   g_return_val_if_fail (scale_factor > 0, NULL);
1232 
1233   context = gtk_widget_get_style_context (GTK_WIDGET (self));
1234   gtk_render_background_get_clip (context, 0, 0, size, size, &bounds);
1235 
1236   surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32,
1237                                         bounds.width * scale_factor,
1238                                         bounds.height * scale_factor);
1239   cairo_surface_set_device_scale (surface, scale_factor, scale_factor);
1240   cr = cairo_create (surface);
1241 
1242   cairo_translate (cr, -bounds.x, -bounds.y);
1243 
1244   if (get_icon (self)) {
1245     /* Only used the cached round_image if it fits the size and it isn't scaled*/
1246     if (!self->round_image ||
1247         is_scaled (self->round_image) ||
1248         gdk_pixbuf_get_width (self->round_image) != scaled_size) {
1249       pixbuf_from_icon = load_icon_sync (get_icon (self), scaled_size);
1250       custom_image = update_custom_image (pixbuf_from_icon, NULL, scaled_size);
1251       gtk_style_context_add_class (context, "image");
1252     } else {
1253       custom_image = update_custom_image (NULL, self->round_image, scaled_size);
1254     }
1255   }
1256 
1257   draw_for_size (self, cr, custom_image, size, size, scale_factor);
1258 
1259   return gdk_pixbuf_get_from_surface (surface, 0, 0,
1260                                       bounds.width * scale_factor,
1261                                       bounds.height * scale_factor);
1262 }
1263 
1264 /**
1265  * hdy_avatar_draw_to_pixbuf_async:
1266  * @self: a #HdyAvatar
1267  * @size: The size of the pixbuf
1268  * @scale_factor: The scale factor
1269  * @cancellable: (nullable): optional #GCancellable object, %NULL to ignore
1270  * @callback: (scope async): a #GAsyncReadyCallback to call when the avatar is generated
1271  * @user_data: (closure): the data to pass to callback function
1272 
1273  * Renders asynchronously @self into a pixbuf at @size and @scale_factor.
1274  * This can be used to export the fallback avatar.
1275  *
1276  * Since: 1.2
1277  */
1278 void
hdy_avatar_draw_to_pixbuf_async(HdyAvatar * self,gint size,gint scale_factor,GCancellable * cancellable,GAsyncReadyCallback callback,gpointer user_data)1279 hdy_avatar_draw_to_pixbuf_async (HdyAvatar           *self,
1280                                  gint                 size,
1281                                  gint                 scale_factor,
1282                                  GCancellable        *cancellable,
1283                                  GAsyncReadyCallback  callback,
1284                                  gpointer             user_data)
1285 {
1286   g_autoptr (GTask) task = NULL;
1287   gint scaled_size = size * scale_factor;
1288   SizeData *data;
1289 
1290   g_return_if_fail (HDY_IS_AVATAR (self));
1291   g_return_if_fail (size > 0);
1292   g_return_if_fail (scale_factor > 0);
1293 
1294   data = g_slice_new (SizeData);
1295   data->size = size;
1296   data->scale_factor = scale_factor;
1297 
1298   task = g_task_new (self, cancellable, callback, user_data);
1299   g_task_set_source_tag (task, hdy_avatar_draw_to_pixbuf_async);
1300   g_task_set_task_data (task, data, (GDestroyNotify) size_data_free);
1301 
1302   if (get_icon (self) &&
1303       (!self->round_image ||
1304        gdk_pixbuf_get_width (self->round_image) != scaled_size ||
1305        is_scaled (self->round_image)))
1306     load_icon_async (self,
1307                      scaled_size,
1308                      cancellable,
1309                      (GAsyncReadyCallback) load_from_gicon_async_for_export_cb,
1310                      g_steal_pointer (&task));
1311   else
1312     g_task_return_pointer (task, NULL, NULL);
1313 }
1314 
1315 /**
1316  * hdy_avatar_draw_to_pixbuf_finish:
1317  * @self: a #HdyAvatar
1318  * @async_result: a #GAsyncResult
1319  *
1320  * Finishes an asynchronous draw of an avatar to a pixbuf.
1321  *
1322  * Returns: (transfer full): a #GdkPixbuf
1323  *
1324  * Since: 1.2
1325  */
1326 GdkPixbuf *
hdy_avatar_draw_to_pixbuf_finish(HdyAvatar * self,GAsyncResult * async_result)1327 hdy_avatar_draw_to_pixbuf_finish (HdyAvatar    *self,
1328                                   GAsyncResult *async_result)
1329 {
1330   GTask *task;
1331   g_autoptr (GdkPixbuf) pixbuf_from_icon = NULL;
1332   g_autoptr (GdkPixbuf) custom_image = NULL;
1333   g_autoptr (cairo_surface_t) surface = NULL;
1334   g_autoptr (cairo_t) cr = NULL;
1335   SizeData *data;
1336   GtkStyleContext *context;
1337   GtkAllocation bounds;
1338 
1339   g_return_val_if_fail (G_IS_TASK (async_result), NULL);
1340 
1341   task = G_TASK (async_result);
1342 
1343   g_warn_if_fail (g_task_get_source_tag (task) == hdy_avatar_draw_to_pixbuf_async);
1344 
1345   data = g_task_get_task_data (task);
1346 
1347   context = gtk_widget_get_style_context (GTK_WIDGET (self));
1348   gtk_render_background_get_clip (context, 0, 0, data->size, data->size, &bounds);
1349 
1350   surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32,
1351                                         bounds.width * data->scale_factor,
1352                                         bounds.height * data->scale_factor);
1353   cairo_surface_set_device_scale (surface, data->scale_factor, data->scale_factor);
1354   cr = cairo_create (surface);
1355 
1356   cairo_translate (cr, -bounds.x, -bounds.y);
1357 
1358   pixbuf_from_icon = g_task_propagate_pointer (task, NULL);
1359   custom_image = update_custom_image (pixbuf_from_icon,
1360                                       NULL,
1361                                       data->size * data->scale_factor);
1362   draw_for_size (self, cr, custom_image, data->size, data->size, data->scale_factor);
1363 
1364   return gdk_pixbuf_get_from_surface (surface, 0, 0,
1365                                       bounds.width * data->scale_factor,
1366                                       bounds.height * data->scale_factor);
1367 }
1368 
1369 /**
1370  * hdy_avatar_get_loadable_icon:
1371  * @self: a #HdyAvatar
1372  *
1373  * Gets the #GLoadableIcon set via hdy_avatar_set_loadable_icon().
1374  *
1375  * Returns: (nullable) (transfer none): the #GLoadableIcon
1376  *
1377  * Since: 1.2
1378  */
1379 GLoadableIcon *
hdy_avatar_get_loadable_icon(HdyAvatar * self)1380 hdy_avatar_get_loadable_icon (HdyAvatar *self)
1381 {
1382   g_return_val_if_fail (HDY_IS_AVATAR (self), NULL);
1383 
1384   return self->icon;
1385 }
1386 
1387 /**
1388  * hdy_avatar_set_loadable_icon:
1389  * @self: a #HdyAvatar
1390  * @icon: (nullable): a #GLoadableIcon
1391  *
1392  * Sets the #GLoadableIcon to use as an avatar.
1393  * The previous avatar is displayed till the new avatar is loaded,
1394  * to immediately remove the custom avatar set the loadable-icon to %NULL.
1395  *
1396  * The #GLoadableIcon set via this function is prefered over a set #HdyAvatarImageLoadFunc.
1397  *
1398  * Since: 1.2
1399  */
1400 void
hdy_avatar_set_loadable_icon(HdyAvatar * self,GLoadableIcon * icon)1401 hdy_avatar_set_loadable_icon (HdyAvatar     *self,
1402                               GLoadableIcon *icon)
1403 {
1404   g_return_if_fail (HDY_IS_AVATAR (self));
1405   g_return_if_fail (icon == NULL || G_IS_LOADABLE_ICON (icon));
1406 
1407   if (icon == self->icon)
1408     return;
1409 
1410   if (self->icon) {
1411     g_cancellable_cancel (self->cancellable);
1412     g_clear_object (&self->cancellable);
1413     self->currently_loading_size = -1;
1414     self->loading_error = FALSE;
1415   }
1416 
1417   g_set_object (&self->icon, icon);
1418 
1419   if (self->icon) {
1420     gint scale_factor = gtk_widget_get_scale_factor (GTK_WIDGET (self));
1421     self->currently_loading_size = self->size * scale_factor;
1422     load_icon_async (self,
1423                      self->currently_loading_size,
1424                      self->cancellable,
1425                      (GAsyncReadyCallback) load_from_gicon_async_for_display_cb,
1426                      NULL);
1427   } else {
1428     gtk_widget_queue_draw (GTK_WIDGET (self));
1429   }
1430 
1431   g_object_notify_by_pspec (G_OBJECT (self), props[PROP_LOADABLE_ICON]);
1432 }
1433