From 93afecf86c9842642e3dd545ae32fa877af317db Mon Sep 17 00:00:00 2001 From: wayne Date: Sun, 16 Aug 2026 10:32:00 +1000 Subject: [PATCH] app-grid-button: decode the icon before the first draw gtk_image_set_from_gicon() defers the load to the first draw, and a GtkFlowBox recycles nothing, so the first scroll through the grid decoded an icon per row it revealed, on the main thread. Decode off the main thread instead and keep the result on the image as a scale-aware surface, refreshed when the theme or the scale changes. Warming GtkIconTheme does not help: every lookup returns a fresh GtkIconInfo, so nothing a previous load produced is reused. Signed-off-by: wayne --- src/app-grid-button.c | 139 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/src/app-grid-button.c b/src/app-grid-button.c index 9c9ced2f5..352c4ed5e 100644 --- a/src/app-grid-button.c +++ b/src/app-grid-button.c @@ -34,6 +34,10 @@ struct _PhoshAppGridButtonPrivate { gulong favorite_changed_watcher; + /* Icon pre-warming, see warm_icon() */ + GCancellable *icon_cancel; + guint icon_warm_id; + GtkWidget *icon; GtkWidget *popover; GtkGesture *long_gesture; @@ -115,12 +119,133 @@ phosh_app_grid_button_get_property (GObject *object, } +/* + * Decode the icon before the button is first drawn. + * + * gtk_image_set_from_gicon() defers the load to the first draw, and a + * GtkFlowBox recycles nothing, so without this the first scroll through the + * grid decodes an icon per row it reveals, on the main thread. + * + * Warming GtkIconTheme instead does not work -- every lookup returns a fresh + * GtkIconInfo, so nothing that a load produced is reused. The decoded pixbuf + * has to be kept, which means putting it on the image. + */ +static void +on_icon_loaded (GObject *source, GAsyncResult *res, gpointer data) +{ + GtkIconInfo *icon_info = GTK_ICON_INFO (source); + PhoshAppGridButton *self = data; + PhoshAppGridButtonPrivate *priv = phosh_app_grid_button_get_instance_private (self); + g_autoptr (GdkPixbuf) pixbuf = NULL; + g_autoptr (GError) err = NULL; + cairo_surface_t *surface; + + pixbuf = gtk_icon_info_load_icon_finish (icon_info, res, &err); + if (pixbuf == NULL) { + if (!g_error_matches (err, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_debug ("Could not pre-load icon: %s", err->message); + /* The GIcon is still on the image, so GTK falls back to decoding at draw */ + goto out; + } + + /* A surface, not the pixbuf: the device scale travels with it */ + surface = gdk_cairo_surface_create_from_pixbuf (pixbuf, + gtk_widget_get_scale_factor (GTK_WIDGET (self)), + gtk_widget_get_window (GTK_WIDGET (self))); + gtk_image_set_from_surface (GTK_IMAGE (priv->icon), surface); + cairo_surface_destroy (surface); + + out: + g_object_unref (self); +} + + +static gboolean +warm_icon (gpointer data) +{ + PhoshAppGridButton *self = data; + PhoshAppGridButtonPrivate *priv = phosh_app_grid_button_get_instance_private (self); + g_autoptr (GtkIconInfo) icon_info = NULL; + GIcon *icon; + int size, scale; + + priv->icon_warm_id = 0; + + if (priv->info == NULL) + return G_SOURCE_REMOVE; + + icon = g_app_info_get_icon (priv->info); + if (icon == NULL) + return G_SOURCE_REMOVE; + + size = gtk_image_get_pixel_size (GTK_IMAGE (priv->icon)); + scale = gtk_widget_get_scale_factor (GTK_WIDGET (self)); + + icon_info = gtk_icon_theme_lookup_by_gicon_for_scale (gtk_icon_theme_get_default (), + icon, + size, + scale, + GTK_ICON_LOOKUP_FORCE_SIZE); + if (icon_info == NULL) + return G_SOURCE_REMOVE; + + g_cancellable_cancel (priv->icon_cancel); + g_clear_object (&priv->icon_cancel); + priv->icon_cancel = g_cancellable_new (); + + /* Hold a reference: a decode already in a thread cannot be taken back */ + gtk_icon_info_load_icon_async (icon_info, + priv->icon_cancel, + on_icon_loaded, + g_object_ref (self)); + + return G_SOURCE_REMOVE; +} + + +static void +schedule_icon_warm (PhoshAppGridButton *self) +{ + PhoshAppGridButtonPrivate *priv = phosh_app_grid_button_get_instance_private (self); + + if (priv->icon_warm_id) + return; + + /* Low priority: the grid is built while the shell is still starting. The + * buttons are created in display order, so the idles queue that way too. */ + priv->icon_warm_id = g_idle_add_full (G_PRIORITY_LOW, warm_icon, self, NULL); + g_source_set_name_by_id (priv->icon_warm_id, "[phosh] app icon pre-warm"); +} + + +static void +on_icon_theme_changed (PhoshAppGridButton *self) +{ + PhoshAppGridButtonPrivate *priv = phosh_app_grid_button_get_instance_private (self); + GIcon *icon; + + if (priv->info == NULL) + return; + + /* The image is holding a surface, which GTK cannot re-resolve itself */ + icon = g_app_info_get_icon (priv->info); + if (icon) + gtk_image_set_from_gicon (GTK_IMAGE (priv->icon), icon, -1); + + schedule_icon_warm (self); +} + + static void phosh_app_grid_button_finalize (GObject *object) { PhoshAppGridButton *self = PHOSH_APP_GRID_BUTTON (object); PhoshAppGridButtonPrivate *priv = phosh_app_grid_button_get_instance_private (self); + g_clear_handle_id (&priv->icon_warm_id, g_source_remove); + g_cancellable_cancel (priv->icon_cancel); + g_clear_object (&priv->icon_cancel); + g_clear_object (&priv->info); g_clear_object (&priv->menu); g_clear_object (&priv->actions); @@ -549,6 +674,17 @@ phosh_app_grid_button_init (PhoshAppGridButton *self) gtk_widget_init_template (GTK_WIDGET (self)); + /* Both invalidate the surface handed to the image */ + g_signal_connect_object (gtk_icon_theme_get_default (), + "changed", + G_CALLBACK (on_icon_theme_changed), + self, + G_CONNECT_SWAPPED); + g_signal_connect (self, + "notify::scale-factor", + G_CALLBACK (on_icon_theme_changed), + NULL); + gtk_popover_bind_model (GTK_POPOVER (priv->popover), G_MENU_MODEL (priv->menu), NULL); @@ -655,6 +791,9 @@ phosh_app_grid_button_set_app_info (PhoshAppGridButton *self, gtk_image_set_from_gicon (GTK_IMAGE (priv->icon), icon, -1); } + /* See warm_icon() */ + schedule_icon_warm (self); + gtk_widget_set_sensitive (GTK_WIDGET (self), TRUE); if (G_IS_DESKTOP_APP_INFO (priv->info)) {