summaryrefslogtreecommitdiffstats
path: root/src/gs-feature-tile.c
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:57:27 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:57:27 +0000
commit6f0f7d1b40a8fa8d46a2d6f4317600001cdbbb18 (patch)
treed423850ae901365e582137bdf2b5cbdffd7ca266 /src/gs-feature-tile.c
parentInitial commit. (diff)
downloadgnome-software-6f0f7d1b40a8fa8d46a2d6f4317600001cdbbb18.tar.xz
gnome-software-6f0f7d1b40a8fa8d46a2d6f4317600001cdbbb18.zip
Adding upstream version 43.5.upstream/43.5upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/gs-feature-tile.c')
-rw-r--r--src/gs-feature-tile.c621
1 files changed, 621 insertions, 0 deletions
diff --git a/src/gs-feature-tile.c b/src/gs-feature-tile.c
new file mode 100644
index 0000000..bb8bed4
--- /dev/null
+++ b/src/gs-feature-tile.c
@@ -0,0 +1,621 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2013 Matthias Clasen <mclasen@redhat.com>
+ * Copyright (C) 2019 Richard Hughes <richard@hughsie.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <gtk/gtk.h>
+
+#include "gs-feature-tile.h"
+#include "gs-layout-manager.h"
+#include "gs-common.h"
+#include "gs-css.h"
+
+#define GS_TYPE_FEATURE_TILE_LAYOUT (gs_feature_tile_layout_get_type ())
+G_DECLARE_FINAL_TYPE (GsFeatureTileLayout, gs_feature_tile_layout, GS, FEATURE_TILE_LAYOUT, GsLayoutManager)
+
+struct _GsFeatureTileLayout
+{
+ GsLayoutManager parent_instance;
+
+ gboolean narrow_mode;
+};
+
+G_DEFINE_TYPE (GsFeatureTileLayout, gs_feature_tile_layout, GS_TYPE_LAYOUT_MANAGER)
+
+enum {
+ SIGNAL_NARROW_MODE_CHANGED,
+ SIGNAL_LAST
+};
+
+static guint signals [SIGNAL_LAST] = { 0 };
+
+static void
+gs_feature_tile_layout_allocate (GtkLayoutManager *layout_manager,
+ GtkWidget *widget,
+ gint width,
+ gint height,
+ gint baseline)
+{
+ GsFeatureTileLayout *self = GS_FEATURE_TILE_LAYOUT (layout_manager);
+ gboolean narrow_mode;
+
+ GTK_LAYOUT_MANAGER_CLASS (gs_feature_tile_layout_parent_class)->allocate (layout_manager,
+ widget, width, height, baseline);
+
+ /* Engage ‘narrow mode’ if the allocation becomes too narrow. The exact
+ * choice of width is arbitrary here. */
+ narrow_mode = (width < 600);
+ if (self->narrow_mode != narrow_mode) {
+ self->narrow_mode = narrow_mode;
+ g_signal_emit (self, signals[SIGNAL_NARROW_MODE_CHANGED], 0, self->narrow_mode);
+ }
+}
+
+static void
+gs_feature_tile_layout_class_init (GsFeatureTileLayoutClass *klass)
+{
+ GtkLayoutManagerClass *layout_manager_class = GTK_LAYOUT_MANAGER_CLASS (klass);
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ layout_manager_class->allocate = gs_feature_tile_layout_allocate;
+
+ signals [SIGNAL_NARROW_MODE_CHANGED] =
+ g_signal_new ("narrow-mode-changed",
+ G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL, g_cclosure_marshal_generic,
+ G_TYPE_NONE, 1, G_TYPE_BOOLEAN);
+}
+
+static void
+gs_feature_tile_layout_init (GsFeatureTileLayout *self)
+{
+}
+
+/* ********************************************************************* */
+
+struct _GsFeatureTile
+{
+ GsAppTile parent_instance;
+ GtkWidget *stack;
+ GtkWidget *image;
+ GtkWidget *title;
+ GtkWidget *subtitle;
+ const gchar *markup_cache; /* (unowned) (nullable) */
+ GtkCssProvider *tile_provider; /* (owned) (nullable) */
+ GtkCssProvider *title_provider; /* (owned) (nullable) */
+ GtkCssProvider *subtitle_provider; /* (owned) (nullable) */
+ GArray *key_colors_cache; /* (unowned) (nullable) */
+ gboolean narrow_mode;
+ guint refresh_id;
+};
+
+static void gs_feature_tile_refresh (GsAppTile *self);
+
+static gboolean
+gs_feature_tile_refresh_idle_cb (gpointer user_data)
+{
+ GsFeatureTile *tile = user_data;
+
+ tile->refresh_id = 0;
+
+ gs_feature_tile_refresh (GS_APP_TILE (tile));
+
+ return G_SOURCE_REMOVE;
+}
+
+static void
+gs_feature_tile_layout_narrow_mode_changed_cb (GtkLayoutManager *layout_manager,
+ gboolean narrow_mode,
+ gpointer user_data)
+{
+ GsFeatureTile *self = GS_FEATURE_TILE (user_data);
+
+ if (self->narrow_mode != narrow_mode && !self->refresh_id) {
+ self->narrow_mode = narrow_mode;
+ self->refresh_id = g_idle_add (gs_feature_tile_refresh_idle_cb, self);
+ }
+}
+
+/* A colour represented in hue, saturation, brightness form; with an additional
+ * field for its contrast calculated with respect to some external colour.
+ *
+ * See https://en.wikipedia.org/wiki/HSL_and_HSV */
+typedef struct
+{
+ gfloat hue; /* [0.0, 1.0] */
+ gfloat saturation; /* [0.0, 1.0] */
+ gfloat brightness; /* [0.0, 1.0]; also known as lightness (HSL) or value (HSV) */
+ gfloat contrast; /* (0.047, 21] */
+} GsHSBC;
+
+G_DEFINE_TYPE (GsFeatureTile, gs_feature_tile, GS_TYPE_APP_TILE)
+
+static void
+gs_feature_tile_dispose (GObject *object)
+{
+ GsFeatureTile *tile = GS_FEATURE_TILE (object);
+
+ if (tile->refresh_id) {
+ g_source_remove (tile->refresh_id);
+ tile->refresh_id = 0;
+ }
+
+ g_clear_object (&tile->tile_provider);
+ g_clear_object (&tile->title_provider);
+ g_clear_object (&tile->subtitle_provider);
+
+ G_OBJECT_CLASS (gs_feature_tile_parent_class)->dispose (object);
+}
+
+/* These are subjectively chosen. See below. */
+static const gfloat min_valid_saturation = 0.5;
+static const gfloat max_valid_saturation = 0.85;
+
+/* The minimum absolute contrast ratio between the foreground and background
+ * colours, from WCAG:
+ * https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html */
+static const gfloat min_abs_contrast = 4.5;
+
+/* Sort two candidate background colours for the feature tile, ranking them by
+ * suitability for being chosen as the background colour, with the most suitable
+ * first.
+ *
+ * There are several criteria being used here:
+ * 1. First, colours are sorted by whether their saturation is in the range
+ * [0.5, 0.85], which is a subjectively-chosen range of ‘light, but not too
+ * saturated’ colours.
+ * 2. Colours with saturation in that valid range are then sorted by contrast,
+ * with higher contrast being preferred. The contrast is calculated against
+ * an external colour by the caller.
+ * 3. Colours with saturation outside that valid range are sorted by their
+ * absolute distance from the range, so that colours which are nearer to
+ * having a valid saturation are preferred. This is useful in the case where
+ * none of the key colours in this array have valid saturations; the caller
+ * will want the one which is closest to being valid.
+ */
+static gboolean
+saturation_is_valid (const GsHSBC *hsbc,
+ gfloat *distance_from_valid_range)
+{
+ *distance_from_valid_range = (hsbc->saturation > max_valid_saturation) ? hsbc->saturation - max_valid_saturation : min_valid_saturation - hsbc->saturation;
+ return (hsbc->saturation >= min_valid_saturation && hsbc->saturation <= max_valid_saturation);
+}
+
+static gint
+colors_sort_cb (gconstpointer a,
+ gconstpointer b)
+{
+ const GsHSBC *hsbc_a = a;
+ const GsHSBC *hsbc_b = b;
+ gfloat hsbc_a_distance_from_range, hsbc_b_distance_from_range;
+ gboolean hsbc_a_saturation_in_range = saturation_is_valid (hsbc_a, &hsbc_a_distance_from_range);
+ gboolean hsbc_b_saturation_in_range = saturation_is_valid (hsbc_b, &hsbc_b_distance_from_range);
+
+ if (hsbc_a_saturation_in_range && !hsbc_b_saturation_in_range)
+ return -1;
+ else if (!hsbc_a_saturation_in_range && hsbc_b_saturation_in_range)
+ return 1;
+ else if (!hsbc_a_saturation_in_range && !hsbc_b_saturation_in_range)
+ return hsbc_a_distance_from_range - hsbc_b_distance_from_range;
+ else
+ return ABS (hsbc_b->contrast) - ABS (hsbc_a->contrast);
+}
+
+static gint
+colors_sort_contrast_cb (gconstpointer a,
+ gconstpointer b)
+{
+ const GsHSBC *hsbc_a = a;
+ const GsHSBC *hsbc_b = b;
+
+ return hsbc_b->contrast - hsbc_a->contrast;
+}
+
+/* Calculate the relative luminance of @colour. This is [0.0, 1.0], where 0.0 is
+ * the darkest black, and 1.0 is the lightest white.
+ *
+ * https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef */
+static gfloat
+relative_luminance (const GsHSBC *colour)
+{
+ gfloat red, green, blue;
+ gfloat r, g, b;
+ gfloat luminance;
+
+ /* Convert to sRGB */
+ gtk_hsv_to_rgb (colour->hue, colour->saturation, colour->brightness,
+ &red, &green, &blue);
+
+ r = (red <= 0.03928) ? red / 12.92 : pow ((red + 0.055) / 1.055, 2.4);
+ g = (green <= 0.03928) ? green / 12.92 : pow ((green + 0.055) / 1.055, 2.4);
+ b = (blue <= 0.03928) ? blue / 12.92 : pow ((blue + 0.055) / 1.055, 2.4);
+
+ luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
+ g_assert (luminance >= 0.0 && luminance <= 1.0);
+ return luminance;
+}
+
+/* Calculate the WCAG contrast ratio between the two colours. The returned ratio
+ * is in the range (0.047, 21].
+ *
+ * https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html#contrast-ratiodef */
+static gfloat
+wcag_contrast (const GsHSBC *foreground,
+ const GsHSBC *background)
+{
+ const GsHSBC *lighter, *darker;
+ gfloat ratio;
+
+ if (foreground->brightness >= background->brightness) {
+ lighter = foreground;
+ darker = background;
+ } else {
+ lighter = background;
+ darker = foreground;
+ }
+
+ ratio = (relative_luminance (lighter) + 0.05) / (relative_luminance (darker) + 0.05);
+ g_assert (ratio > 0.047 && ratio <= 21);
+ return ratio;
+}
+
+/* Calculate a new brightness value for @background which improves its contrast
+ * (as calculated using wcag_contrast()) with @foreground to at least
+ * @desired_contrast.
+ *
+ * The return value is in the range [0.0, 1.0].
+ */
+static gfloat
+wcag_contrast_find_brightness (const GsHSBC *foreground,
+ const GsHSBC *background,
+ gfloat desired_contrast)
+{
+ GsHSBC modified_background;
+
+ g_assert (desired_contrast > 0.047 && desired_contrast <= 21);
+
+ /* This is an optimisation problem of modifying @background until
+ * the WCAG contrast is at least @desired_contrast. There might be a
+ * closed-form solution to this but I can’t be bothered to work it out
+ * right now. An optimisation loop should work.
+ *
+ * wcag_contrast() compares the lightest and darkest of the two colours,
+ * so ensure the background brightness is modified in the correct
+ * direction (increased or decreased) depending on whether the
+ * foreground colour is originally the brighter. This gives the largest
+ * search space for the background colour brightness, and ensures the
+ * optimisation works with dark and light themes. */
+ for (modified_background = *background;
+ modified_background.brightness >= 0.0 &&
+ modified_background.brightness <= 1.0 &&
+ wcag_contrast (foreground, &modified_background) < desired_contrast;
+ modified_background.brightness += ((foreground->brightness > 0.5) ? -0.1 : 0.1)) {
+ /* Nothing to do here */
+ }
+
+ return CLAMP (modified_background.brightness, 0.0, 1.0);
+}
+
+static void
+gs_feature_tile_refresh (GsAppTile *self)
+{
+ GsFeatureTile *tile = GS_FEATURE_TILE (self);
+ GsApp *app = gs_app_tile_get_app (self);
+ const gchar *markup = NULL;
+ g_autofree gchar *name = NULL;
+ GtkStyleContext *context;
+ g_autoptr(GIcon) icon = NULL;
+ guint icon_size;
+
+ if (app == NULL)
+ return;
+
+ gtk_stack_set_visible_child_name (GTK_STACK (tile->stack), "content");
+
+ /* Set the narrow mode. */
+ context = gtk_widget_get_style_context (GTK_WIDGET (self));
+ if (tile->narrow_mode)
+ gtk_style_context_add_class (context, "narrow");
+ else
+ gtk_style_context_remove_class (context, "narrow");
+
+ /* Update the icon. Try a 160px version if not in narrow mode, and it’s
+ * available; otherwise use 128px. */
+ if (!tile->narrow_mode) {
+ icon = gs_app_get_icon_for_size (app,
+ 160,
+ gtk_widget_get_scale_factor (tile->image),
+ NULL);
+ icon_size = 160;
+ }
+ if (icon == NULL) {
+ icon = gs_app_get_icon_for_size (app,
+ 128,
+ gtk_widget_get_scale_factor (tile->image),
+ NULL);
+ icon_size = 128;
+ }
+
+ if (icon != NULL) {
+ gtk_image_set_from_gicon (GTK_IMAGE (tile->image), icon);
+ gtk_image_set_pixel_size (GTK_IMAGE (tile->image), icon_size);
+ gtk_widget_show (tile->image);
+ } else {
+ gtk_widget_hide (tile->image);
+ }
+
+ /* Update text and let it wrap if the widget is narrow. */
+ gtk_label_set_label (GTK_LABEL (tile->title), gs_app_get_name (app));
+ gtk_label_set_label (GTK_LABEL (tile->subtitle), gs_app_get_summary (app));
+
+ gtk_label_set_wrap (GTK_LABEL (tile->subtitle), tile->narrow_mode);
+ gtk_label_set_lines (GTK_LABEL (tile->subtitle), tile->narrow_mode ? 2 : 1);
+
+ /* perhaps set custom css; cache it so that images don’t get reloaded
+ * unnecessarily. The custom CSS is direction-dependent, and will be
+ * reloaded when the direction changes. If RTL CSS isn’t set, fall back
+ * to the LTR CSS. */
+ if (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL)
+ markup = gs_app_get_metadata_item (app, "GnomeSoftware::FeatureTile-css-rtl");
+ if (markup == NULL)
+ markup = gs_app_get_metadata_item (app, "GnomeSoftware::FeatureTile-css");
+
+ if (tile->markup_cache != markup && markup != NULL) {
+ g_autoptr(GsCss) css = gs_css_new ();
+ g_autofree gchar *modified_markup = gs_utils_set_key_colors_in_css (markup, app);
+ if (modified_markup != NULL)
+ gs_css_parse (css, modified_markup, NULL);
+ gs_utils_widget_set_css (GTK_WIDGET (tile), &tile->tile_provider, "feature-tile",
+ gs_css_get_markup_for_id (css, "tile"));
+ gs_utils_widget_set_css (tile->title, &tile->title_provider, "feature-tile-name",
+ gs_css_get_markup_for_id (css, "name"));
+ gs_utils_widget_set_css (tile->subtitle, &tile->subtitle_provider, "feature-tile-subtitle",
+ gs_css_get_markup_for_id (css, "summary"));
+ tile->markup_cache = markup;
+ } else if (markup == NULL) {
+ GArray *key_colors = gs_app_get_key_colors (app);
+ g_autofree gchar *css = NULL;
+
+ /* If there is no override CSS for the app, default to a solid
+ * background colour based on the app’s key colors.
+ *
+ * Choose an arbitrary key color from the app’s key colors, and
+ * ensure that it’s:
+ * - a light, not too saturated version of the dominant color
+ * of the icon
+ * - always light enough that grey text is visible on it
+ *
+ * Cache the result until the app’s key colours change, as the
+ * amount of calculation going on here is not entirely trivial.
+ */
+ if (key_colors != tile->key_colors_cache) {
+ g_autoptr(GArray) colors = NULL;
+ GdkRGBA fg_rgba;
+ gboolean fg_rgba_valid;
+ GsHSBC fg_hsbc;
+ const GsHSBC *chosen_hsbc;
+ GsHSBC chosen_hsbc_modified;
+ gboolean use_chosen_hsbc = FALSE;
+
+ /* Look up the foreground colour for the feature tile,
+ * which is the colour of the text. This should always
+ * be provided as a named colour by the theme.
+ *
+ * Knowing the foreground colour allows calculation of
+ * the contrast between candidate background colours and
+ * the foreground which will be rendered on top of them.
+ *
+ * We want to choose a background colour with at least
+ * @min_abs_contrast contrast with the foreground, so
+ * that the text is legible.
+ */
+ fg_rgba_valid = gtk_style_context_lookup_color (context, "theme_fg_color", &fg_rgba);
+ g_assert (fg_rgba_valid);
+
+ gtk_rgb_to_hsv (fg_rgba.red, fg_rgba.green, fg_rgba.blue,
+ &fg_hsbc.hue, &fg_hsbc.saturation, &fg_hsbc.brightness);
+
+ g_debug ("FG color: RGB: (%f, %f, %f), HSB: (%f, %f, %f)",
+ fg_rgba.red, fg_rgba.green, fg_rgba.blue,
+ fg_hsbc.hue, fg_hsbc.saturation, fg_hsbc.brightness);
+
+ /* Convert all the RGBA key colours to HSB, and
+ * calculate their contrast against the foreground
+ * colour.
+ *
+ * The contrast is calculated as the Weber contrast,
+ * which is valid for small amounts of foreground colour
+ * (i.e. text) against larger background areas. Contrast
+ * is strictly calculated using luminance, but it’s OK
+ * to subjectively calculate it using brightness, as
+ * brightness is the subjective impression of luminance.
+ */
+ if (key_colors != NULL)
+ colors = g_array_sized_new (FALSE, FALSE, sizeof (GsHSBC), key_colors->len);
+
+ g_debug ("Candidate background colors for %s:", gs_app_get_id (app));
+ for (guint i = 0; key_colors != NULL && i < key_colors->len; i++) {
+ const GdkRGBA *rgba = &g_array_index (key_colors, GdkRGBA, i);
+ GsHSBC hsbc;
+
+ gtk_rgb_to_hsv (rgba->red, rgba->green, rgba->blue,
+ &hsbc.hue, &hsbc.saturation, &hsbc.brightness);
+ hsbc.contrast = wcag_contrast (&fg_hsbc, &hsbc);
+ g_array_append_val (colors, hsbc);
+
+ g_debug (" • RGB: (%f, %f, %f), HSB: (%f, %f, %f), contrast: %f",
+ rgba->red, rgba->green, rgba->blue,
+ hsbc.hue, hsbc.saturation, hsbc.brightness,
+ hsbc.contrast);
+ }
+
+ /* Sort the candidate background colours to find the
+ * most appropriate one. */
+ g_array_sort (colors, colors_sort_cb);
+
+ /* If the developer/distro has provided override colours,
+ * use them. If there’s more than one override colour,
+ * use the one with the highest contrast with the
+ * foreground colour, unmodified. If there’s only one,
+ * modify it as below.
+ *
+ * If there are no override colours, take the top colour
+ * after sorting above. If it’s not good enough, modify
+ * its brightness to improve the contrast, and clamp its
+ * saturation to the valid range.
+ *
+ * If there are no colours, fall through and leave @css
+ * as %NULL. */
+ if (gs_app_get_user_key_colors (app) &&
+ colors != NULL &&
+ colors->len > 1) {
+ g_array_sort (colors, colors_sort_contrast_cb);
+
+ chosen_hsbc = &g_array_index (colors, GsHSBC, 0);
+ chosen_hsbc_modified = *chosen_hsbc;
+
+ use_chosen_hsbc = TRUE;
+ } else if (colors != NULL && colors->len > 0) {
+ chosen_hsbc = &g_array_index (colors, GsHSBC, 0);
+ chosen_hsbc_modified = *chosen_hsbc;
+
+ chosen_hsbc_modified.saturation = CLAMP (chosen_hsbc->saturation, min_valid_saturation, max_valid_saturation);
+
+ if (chosen_hsbc->contrast >= -min_abs_contrast &&
+ chosen_hsbc->contrast <= min_abs_contrast)
+ chosen_hsbc_modified.brightness = wcag_contrast_find_brightness (&fg_hsbc, &chosen_hsbc_modified, min_abs_contrast);
+
+ use_chosen_hsbc = TRUE;
+ }
+
+ if (use_chosen_hsbc) {
+ GdkRGBA chosen_rgba;
+
+ gtk_hsv_to_rgb (chosen_hsbc_modified.hue,
+ chosen_hsbc_modified.saturation,
+ chosen_hsbc_modified.brightness,
+ &chosen_rgba.red, &chosen_rgba.green, &chosen_rgba.blue);
+
+ g_debug ("Chosen background colour for %s (saturation %s, brightness %s): RGB: (%f, %f, %f), HSB: (%f, %f, %f)",
+ gs_app_get_id (app),
+ (chosen_hsbc_modified.saturation == chosen_hsbc->saturation) ? "not modified" : "modified",
+ (chosen_hsbc_modified.brightness == chosen_hsbc->brightness) ? "not modified" : "modified",
+ chosen_rgba.red, chosen_rgba.green, chosen_rgba.blue,
+ chosen_hsbc_modified.hue, chosen_hsbc_modified.saturation, chosen_hsbc_modified.brightness);
+
+ css = g_strdup_printf ("background-color: rgb(%.0f,%.0f,%.0f);",
+ chosen_rgba.red * 255.f,
+ chosen_rgba.green * 255.f,
+ chosen_rgba.blue * 255.f);
+ }
+
+ gs_utils_widget_set_css (GTK_WIDGET (tile), &tile->tile_provider, "feature-tile", css);
+ gs_utils_widget_set_css (tile->title, &tile->title_provider, "feature-tile-name", NULL);
+ gs_utils_widget_set_css (tile->subtitle, &tile->subtitle_provider, "feature-tile-subtitle", NULL);
+
+ tile->key_colors_cache = key_colors;
+ }
+ }
+
+ switch (gs_app_get_state (app)) {
+ case GS_APP_STATE_INSTALLED:
+ case GS_APP_STATE_REMOVING:
+ case GS_APP_STATE_UPDATABLE:
+ case GS_APP_STATE_UPDATABLE_LIVE:
+ name = g_strdup_printf ("%s (%s)",
+ gs_app_get_name (app),
+ C_("Single app", "Installed"));
+ break;
+ case GS_APP_STATE_AVAILABLE:
+ case GS_APP_STATE_INSTALLING:
+ default:
+ name = g_strdup (gs_app_get_name (app));
+ break;
+ }
+
+ if (name != NULL) {
+ gtk_accessible_update_property (GTK_ACCESSIBLE (tile),
+ GTK_ACCESSIBLE_PROPERTY_LABEL, name,
+ GTK_ACCESSIBLE_PROPERTY_DESCRIPTION, gs_app_get_summary (app),
+ -1);
+ }
+}
+
+static void
+gs_feature_tile_direction_changed (GtkWidget *widget, GtkTextDirection previous_direction)
+{
+ GsFeatureTile *tile = GS_FEATURE_TILE (widget);
+
+ gs_feature_tile_refresh (GS_APP_TILE (tile));
+}
+
+static void
+gs_feature_tile_css_changed (GtkWidget *widget,
+ GtkCssStyleChange *css_change)
+{
+ GsFeatureTile *tile = GS_FEATURE_TILE (widget);
+
+ /* Clear the key colours cache, as the tile background colour will
+ * potentially need recalculating if the widget’s foreground colour has
+ * changed. */
+ tile->key_colors_cache = NULL;
+
+ gs_feature_tile_refresh (GS_APP_TILE (tile));
+
+ GTK_WIDGET_CLASS (gs_feature_tile_parent_class)->css_changed (widget, css_change);
+}
+
+static void
+gs_feature_tile_init (GsFeatureTile *tile)
+{
+ GtkLayoutManager *layout_manager;
+
+ gtk_widget_init_template (GTK_WIDGET (tile));
+
+ layout_manager = gtk_widget_get_layout_manager (GTK_WIDGET (tile));
+ g_warn_if_fail (layout_manager != NULL);
+ g_signal_connect_object (layout_manager, "narrow-mode-changed",
+ G_CALLBACK (gs_feature_tile_layout_narrow_mode_changed_cb), tile, 0);
+}
+
+static void
+gs_feature_tile_class_init (GsFeatureTileClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+ GsAppTileClass *app_tile_class = GS_APP_TILE_CLASS (klass);
+
+ object_class->dispose = gs_feature_tile_dispose;
+
+ widget_class->css_changed = gs_feature_tile_css_changed;
+ widget_class->direction_changed = gs_feature_tile_direction_changed;
+
+ app_tile_class->refresh = gs_feature_tile_refresh;
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Software/gs-feature-tile.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, GsFeatureTile, stack);
+ gtk_widget_class_bind_template_child (widget_class, GsFeatureTile, image);
+ gtk_widget_class_bind_template_child (widget_class, GsFeatureTile, title);
+ gtk_widget_class_bind_template_child (widget_class, GsFeatureTile, subtitle);
+
+ gtk_widget_class_set_css_name (widget_class, "feature-tile");
+ gtk_widget_class_set_layout_manager_type (widget_class, GS_TYPE_FEATURE_TILE_LAYOUT);
+}
+
+GtkWidget *
+gs_feature_tile_new (GsApp *app)
+{
+ return g_object_new (GS_TYPE_FEATURE_TILE,
+ "vexpand", FALSE,
+ "app", app,
+ NULL);
+}