/* GSK - The GTK Scene Kit
 *
 * Copyright 2016  Endless
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library. If not, see <http://www.gnu.org/licenses/>.
 */

#include "config.h"

#include "gskrendernodeprivate.h"

#include "gskcairoblurprivate.h"
#include "gskcairorenderer.h"
#include "gskdebugprivate.h"
#include "gskdiffprivate.h"
#include "gskpathprivate.h"
#include "gskrectprivate.h"
#include "gskrendererprivate.h"
#include "gskroundedrectprivate.h"
#include "gskstrokeprivate.h"
#include "gsktransformprivate.h"
#include "gskcomponenttransferprivate.h"
#include "gskprivate.h"
#include "gpu/gskglrenderer.h"

#include "gdk/gdkcairoprivate.h"
#include "gdk/gdkcolorstateprivate.h"
#include "gdk/gdkmemoryformatprivate.h"
#include "gdk/gdkprivate.h"
#include "gdk/gdkrectangleprivate.h"
#include "gdk/gdksubsurfaceprivate.h"
#include "gdk/gdktextureprivate.h"
#include "gdk/gdktexturedownloaderprivate.h"
#include "gdk/gdkrgbaprivate.h"
#include "gdk/gdkcolorstateprivate.h"

#include <cairo.h>
#ifdef CAIRO_HAS_SVG_SURFACE
#include <cairo-svg.h>
#endif
#include <hb-ot.h>

/* for oversized image fallback - we use a smaller size than Cairo actually
 * allows to avoid rounding errors in Cairo */
#define MAX_CAIRO_IMAGE_WIDTH 16384
#define MAX_CAIRO_IMAGE_HEIGHT 16384

/* maximal number of rectangles we keep in a diff region before we throw
 * the towel and just use the bounding box of the parent node.
 * Meant to avoid performance corner cases.
 */
#define MAX_RECTS_IN_DIFF 30

/* This lock protects all on-demand created legacy rgba data of
 * render nodes.
 */
G_LOCK_DEFINE_STATIC (rgba);

/* {{{ Utilities */

static inline gboolean
color_state_is_hdr (GdkColorState *color_state)
{
  GdkColorState *rendering_cs;

  rendering_cs = gdk_color_state_get_rendering_color_state (color_state);

  return rendering_cs != GDK_COLOR_STATE_SRGB &&
         rendering_cs != GDK_COLOR_STATE_SRGB_LINEAR;
}

/* apply a rectangle that bounds @rect in
 * pixel-aligned device coordinates.
 *
 * This is useful for clipping to minimize the rectangle
 * in push_group() or when blurring.
 */
static void
gsk_cairo_rectangle_pixel_aligned (cairo_t               *cr,
                                   const graphene_rect_t *rect)
{
  double x0, x1, x2, x3;
  double y0, y1, y2, y3;
  double xmin, xmax, ymin, ymax;

  x0 = rect->origin.x;
  y0 = rect->origin.y;
  cairo_user_to_device (cr, &x0, &y0);
  x1 = rect->origin.x + rect->size.width;
  y1 = rect->origin.y;
  cairo_user_to_device (cr, &x1, &y1);
  x2 = rect->origin.x;
  y2 = rect->origin.y + rect->size.height;
  cairo_user_to_device (cr, &x2, &y2);
  x3 = rect->origin.x + rect->size.width;
  y3 = rect->origin.y + rect->size.height;
  cairo_user_to_device (cr, &x3, &y3);

  xmin = MIN (MIN (x0, x1), MIN (x2, x3));
  ymin = MIN (MIN (y0, y1), MIN (y2, y3));
  xmax = MAX (MAX (x0, x1), MAX (x2, x3));
  ymax = MAX (MAX (y0, y1), MAX (y2, y3));

  xmin = floor (xmin);
  ymin = floor (ymin);
  xmax = ceil (xmax);
  ymax = ceil (ymax);

  cairo_save (cr);
  cairo_identity_matrix (cr);
  cairo_rectangle (cr, xmin, ymin, xmax - xmin, ymax - ymin);
  cairo_restore (cr);
}

static void
_graphene_rect_init_from_clip_extents (graphene_rect_t *rect,
                                       cairo_t         *cr)
{
  double x1c, y1c, x2c, y2c;

  cairo_clip_extents (cr, &x1c, &y1c, &x2c, &y2c);
  gsk_rect_init (rect, x1c, y1c, x2c - x1c, y2c - y1c);
}

static void
region_union_region_affine (cairo_region_t       *region,
                            const cairo_region_t *sub,
                            float                 scale_x,
                            float                 scale_y,
                            float                 offset_x,
                            float                 offset_y)
{
  cairo_rectangle_int_t rect;
  int i;

  for (i = 0; i < cairo_region_num_rectangles (sub); i++)
    {
      cairo_region_get_rectangle (sub, i, &rect);
      gdk_rectangle_transform_affine (&rect, scale_x, scale_y, offset_x, offset_y, &rect);
      cairo_region_union_rectangle (region, &rect);
    }
}

/* }}} */
/* {{{ GSK_COLOR_NODE */

/**
 * GskColorNode:
 *
 * A render node for a solid color.
 */
struct _GskColorNode
{
  GskRenderNode render_node;
  GdkColor color;
};

static void
gsk_color_node_finalize (GskRenderNode *node)
{
  GskColorNode *self = (GskColorNode *) node;
  GskRenderNodeClass *parent_class = g_type_class_peek (g_type_parent (GSK_TYPE_COLOR_NODE));

  gdk_color_finish (&self->color);

  parent_class->finalize (node);
}

static void
gsk_color_node_draw (GskRenderNode *node,
                     cairo_t       *cr,
                     GdkColorState *ccs)
{
  GskColorNode *self = (GskColorNode *) node;

  gdk_cairo_set_source_color (cr, ccs, &self->color);
  gdk_cairo_rect (cr, &node->bounds);
  cairo_fill (cr);
}

static void
gsk_color_node_diff (GskRenderNode *node1,
                     GskRenderNode *node2,
                     GskDiffData   *data)
{
  GskColorNode *self1 = (GskColorNode *) node1;
  GskColorNode *self2 = (GskColorNode *) node2;

  if (gsk_rect_equal (&node1->bounds, &node2->bounds) &&
      gdk_color_equal (&self1->color, &self2->color))
    return;

  gsk_render_node_diff_impossible (node1, node2, data);
}

static void
gsk_color_node_class_init (gpointer g_class,
                           gpointer class_data)
{
  GskRenderNodeClass *node_class = g_class;

  node_class->node_type = GSK_COLOR_NODE;

  node_class->finalize = gsk_color_node_finalize;
  node_class->draw = gsk_color_node_draw;
  node_class->diff = gsk_color_node_diff;
}

/**
 * gsk_color_node_get_color:
 * @node: (type GskColorNode): a `GskRenderNode`
 *
 * Retrieves the color of the given @node.
 *
 * The value returned by this function will not be correct
 * if the render node was created for a non-sRGB color.
 *
 * Returns: (transfer none): the color of the node
 */
const GdkRGBA *
gsk_color_node_get_color (const GskRenderNode *node)
{
  GskColorNode *self = (GskColorNode *) node;

  g_return_val_if_fail (GSK_IS_RENDER_NODE_TYPE (node, GSK_COLOR_NODE), NULL);

  /* NOTE: This is only correct for nodes with sRGB colors */
  return (const GdkRGBA *) &self->color.values;
}

/*< private >
 * gsk_color_node_get_gdk_color:
 * @node: (type GskColorNode): a `GskRenderNode`
 *
 * Retrieves the color of the given @node.
 *
 * Returns: (transfer none): the color of the node
 */
const GdkColor *
gsk_color_node_get_gdk_color (const GskRenderNode *node)
{
  GskColorNode *self = (GskColorNode *) node;

  g_return_val_if_fail (GSK_IS_RENDER_NODE_TYPE (node, GSK_COLOR_NODE), NULL);

  return &self->color;
}

/**
 * gsk_color_node_new:
 * @rgba: a `GdkRGBA` specifying a color
 * @bounds: the rectangle to render the color into
 *
 * Creates a `GskRenderNode` that will render the color specified by @rgba into
 * the area given by @bounds.
 *
 * Returns: (transfer full) (type GskColorNode): A new `GskRenderNode`
 */
GskRenderNode *
gsk_color_node_new (const GdkRGBA         *rgba,
                    const graphene_rect_t *bounds)
{
  GdkColor color;
  GskRenderNode *node;

  gdk_color_init_from_rgba (&color, rgba);
  node = gsk_color_node_new2 (&color, bounds);
  gdk_color_finish (&color);

  return node;
}

/*< private >
 * gsk_color_node_new2:
 * @color: a `GdkColor` specifying a color
 * @bounds: the rectangle to render the color into
 *
 * Creates a `GskRenderNode` that will render the color specified by @color
 * into the area given by @bounds.
 *
 * Returns: (transfer full) (type GskColorNode): A new `GskRenderNode`
 */
GskRenderNode *
gsk_color_node_new2 (const GdkColor        *color,
                     const graphene_rect_t *bounds)
{
  GskColorNode *self;
  GskRenderNode *node;

  g_return_val_if_fail (color != NULL, NULL);
  g_return_val_if_fail (bounds != NULL, NULL);

  self = gsk_render_node_alloc (GSK_COLOR_NODE);
  node = (GskRenderNode *) self;
  node->offscreen_for_opacity = FALSE;
  node->fully_opaque = gdk_color_is_opaque (color);
  node->preferred_depth = GDK_MEMORY_NONE;
  node->is_hdr = color_state_is_hdr (color->color_state);

  gdk_color_init_copy (&self->color, color);

  gsk_rect_init_from_rect (&node->bounds, bounds);
  gsk_rect_normalize (&node->bounds);

  return node;
}

/* }}} */
/* {{{ GSK_LINEAR_GRADIENT_NODE */

typedef struct _GskGradientLine GskGradientLine;
struct _GskGradientLine
{
  gsize n_stops;
  GskGradientStop *stops;

  GdkColorState *interpolation;
  GskHueInterpolation hue_interpolation;

  GskColorStop *rgba_stops;
};

typedef struct _GskGradientNode GskGradientNode;
struct _GskGradientNode
{
  GskRenderNode render_node;
  GskGradientLine line;
};

static void
gsk_gradient_line_init (GskGradientLine       *line,
                        GdkColorState         *interpolation,
                        GskHueInterpolation    hue_interpolation,
                        const GskGradientStop *stops,
                        gsize                  n_stops,
                        gboolean              *out_opaque)
{
  gboolean opaque = TRUE;

  line->n_stops = n_stops;
  line->stops = g_new (GskGradientStop, n_stops);
  for (gsize i = 0; i < n_stops; i++)
    {
      line->stops[i].offset = stops[i].offset;
      line->stops[i].transition_hint = stops[i].transition_hint;
      gdk_color_init_copy (&line->stops[i].color, &stops[i].color);
      opaque = opaque && gdk_color_is_opaque (&stops[i].color);
    }

  line->interpolation = gdk_color_state_ref (interpolation);
  line->hue_interpolation = hue_interpolation;

  *out_opaque = opaque;
}

static void
gsk_gradient_line_clear (GskGradientLine *line)
{
  for (gsize i = 0; i < line->n_stops; i++)
    gdk_color_finish (&line->stops[i].color);
  g_free (line->stops);
  gdk_color_state_unref (line->interpolation);
  g_free (line->rgba_stops);
}

static gboolean
gsk_gradient_line_equal (const GskGradientLine *line1,
                         const GskGradientLine *line2)
{
  if (!gdk_color_state_equal (line1->interpolation, line2->interpolation) ||
      line1->n_stops != line2->n_stops)
    return FALSE;

  for (gsize i = 0; i < line1->n_stops; i++)
    {
      GskGradientStop *stop1 = &line1->stops[i];
      GskGradientStop *stop2 = &line2->stops[i];

      if (stop1->offset != stop2->offset ||
          stop1->transition_hint != stop2->transition_hint ||
          !gdk_color_equal (&stop1->color, &stop2->color))
        return FALSE;
    }

  return TRUE;
}

static const GskColorStop *
gsk_gradient_line_get_color_stops (GskGradientLine *line,
                                   gsize           *n_stops)
{
  const GskColorStop *stops;

  if (n_stops != NULL)
    *n_stops = line->n_stops;

  G_LOCK (rgba);

  if (line->rgba_stops == NULL)
    {
      line->rgba_stops = g_new (GskColorStop, line->n_stops);
      for (gsize i = 0; i < line->n_stops; i++)
        {
          line->rgba_stops[i].offset = line->stops[i].offset;
          gdk_color_to_float (&line->stops[i].color, GDK_COLOR_STATE_SRGB, (float *) &line->rgba_stops[i].color);
        }
    }

  stops = line->rgba_stops;

  G_UNLOCK (rgba);

  return stops;
}

/**
 * GskRepeatingLinearGradientNode:
 *
 * A render node for a repeating linear gradient.
 */

/**
 * GskLinearGradientNode:
 *
 * A render node for a linear gradient.
 */
struct _GskLinearGradientNode
{
  GskRenderNode render_node;

  GskGradientLine line;

  graphene_point_t start;
  graphene_point_t end;
};

static void
gsk_linear_gradient_node_finalize (GskRenderNode *node)
{
  GskLinearGradientNode *self = (GskLinearGradientNode *) node;
  GskRenderNodeClass *parent_class = g_type_class_peek (g_type_parent (GSK_TYPE_LINEAR_GRADIENT_NODE));

  gsk_gradient_line_clear (&self->line);

  parent_class->finalize (node);
}

static float
adjust_hue (GskHueInterpolation  interp,
            float                h1,
            float                h2)
{
  float d;

  d = h2 - h1;
  while (d > 360)
    {
      h2 -= 360;
      d = h2 - h1;
    }
  while (d < -360)
    {
      h2 += 360;
      d = h2 - h1;
    }

  g_assert (fabsf (d) <= 360);

  switch (interp)
    {
    case GSK_HUE_INTERPOLATION_SHORTER:
      {
        if (d > 180)
          h2 -= 360;
        else if (d < -180)
          h2 += 360;
      }
      g_assert (fabsf (h2 - h1) <= 180);
      break;

    case GSK_HUE_INTERPOLATION_LONGER:
      {
        if (0 < d && d < 180)
          h2 -= 360;
        else if (-180 < d && d <= 0)
          h2 += 360;
      g_assert (fabsf (h2 - h1) >= 180);
      }
      break;

    case GSK_HUE_INTERPOLATION_INCREASING:
      if (h2 < h1)
        h2 += 360;
      d = h2 - h1;
      g_assert (h1 <= h2);
      break;

    case GSK_HUE_INTERPOLATION_DECREASING:
      if (h1 < h2)
        h2 -= 360;
      d = h2 - h1;
      g_assert (h1 >= h2);
      break;

    default:
      g_assert_not_reached ();
    }

  return h2;
}

#define lerp(t,a,b) ((a) + (t) * ((b) - (a)))

typedef void (* ColorStopCallback) (float          offset,
                                    GdkColorState *ccs,
                                    float          values[4],
                                    gpointer       data);

static void
interpolate_color_stops (GdkColorState         *ccs,
                         GdkColorState         *interpolation,
                         GskHueInterpolation    hue_interpolation,
                         float                  offset1,
                         GdkColor              *color1,
                         float                  offset2,
                         GdkColor              *color2,
                         float                  transition_hint,
                         ColorStopCallback      callback,
                         gpointer               data)
{
  float values1[4];
  float values2[4];
  int n;
  float exp;

  gdk_color_to_float (color1, interpolation, values1);
  gdk_color_to_float (color2, interpolation, values2);

  if (gdk_color_state_equal (interpolation, GDK_COLOR_STATE_OKLCH))
    {
      values2[2] = adjust_hue (hue_interpolation, values1[2], values2[2]);
      /* don't make hue steps larger than 30° */
      n = ceilf (fabsf (values2[2] - values1[2]) / 30);
    }
  else
    {
      /* just some steps */
      n = 7;
    }

  if (transition_hint <= 0)
    exp = 0;
  else if (transition_hint >= 1)
    exp = INFINITY;
  else if (transition_hint == 0.5)
    exp = 1;
  else
    exp = - M_LN2 / logf (transition_hint);

  for (int k = 1; k < n; k++)
    {
      float f = k / (float) n;
      float values[4];
      float offset;
      float C;
      GdkColor c;

      if (transition_hint <= 0)
        C = 1;
      else if (transition_hint >= 1)
        C = 0;
      else if (transition_hint == 0.5)
        C = f;
      else
        C = powf (f, exp);

      values[0] = lerp (C, values1[0], values2[0]);
      values[1] = lerp (C, values1[1], values2[1]);
      values[2] = lerp (C, values1[2], values2[2]);
      values[3] = lerp (C, values1[3], values2[3]);
      offset = lerp (f, offset1, offset2);

      gdk_color_init (&c, interpolation, values);
      gdk_color_to_float (&c, ccs, values);

      callback (offset, ccs, values, data);

      gdk_color_finish (&c);
    }
}

static void
add_color_stop_to_pattern (float          offset,
                           GdkColorState *ccs,
                           float          values[4],
                           gpointer       data)
{
  cairo_pattern_t *pattern = data;

  cairo_pattern_add_color_stop_rgba (pattern, offset, values[0], values[1], values[2], values[3]);
}

static void
gsk_linear_gradient_node_draw (GskRenderNode *node,
                               cairo_t       *cr,
                               GdkColorState *ccs)
{
  GskLinearGradientNode *self = (GskLinearGradientNode *) node;
  GskGradientLine *line = &self->line;
  cairo_pattern_t *pattern;
  gsize i;

  pattern = cairo_pattern_create_linear (self->start.x, self->start.y,
                                         self->end.x, self->end.y);

  if (gsk_render_node_get_node_type (node) == GSK_REPEATING_LINEAR_GRADIENT_NODE)
    cairo_pattern_set_extend (pattern, CAIRO_EXTEND_REPEAT);

  if (line->stops[0].offset > 0.0)
    gdk_cairo_pattern_add_color_stop_color (pattern,
                                            ccs,
                                            0.0,
                                            &line->stops[0].color);

  for (i = 0; i < line->n_stops; i++)
    {
      if (!gdk_color_state_equal (line->interpolation, ccs) ||
          line->stops[i].transition_hint != 0.5)
        interpolate_color_stops (ccs,
                                 line->interpolation,
                                 line->hue_interpolation,
                                 i > 0 ? line->stops[i-1].offset : 0,
                                 i > 0 ? &line->stops[i-1].color : &line->stops[i].color,
                                 line->stops[i].offset,
                                 &line->stops[i].color,
                                 i > 0 ? line->stops[i].transition_hint : 0.5,
                                 add_color_stop_to_pattern,
                                 pattern);

      gdk_cairo_pattern_add_color_stop_color (pattern,
                                              ccs,
                                              line->stops[i].offset,
                                              &line->stops[i].color);
    }

  if (line->stops[line->n_stops-1].offset < 1.0)
    {
      if (!gdk_color_state_equal (line->interpolation, ccs))
        interpolate_color_stops (ccs,
                                 line->interpolation,
                                 line->hue_interpolation,
                                 line->stops[line->n_stops-1].offset,
                                 &line->stops[line->n_stops-1].color,
                                 1,
                                 &line->stops[line->n_stops-1].color,
                                 0.5,
                                 add_color_stop_to_pattern,
                                 pattern);

      gdk_cairo_pattern_add_color_stop_color (pattern,
                                              ccs,
                                              1.0,
                                              &line->stops[line->n_stops-1].color);
    }

  cairo_set_source (cr, pattern);
  cairo_pattern_destroy (pattern);

  gdk_cairo_rect (cr, &node->bounds);
  cairo_fill (cr);
}

static void
gsk_linear_gradient_node_diff (GskRenderNode *node1,
                               GskRenderNode *node2,
                               GskDiffData   *data)
{
  GskLinearGradientNode *self1 = (GskLinearGradientNode *) node1;
  GskLinearGradientNode *self2 = (GskLinearGradientNode *) node2;

  if (graphene_point_equal (&self1->start, &self2->start) &&
      graphene_point_equal (&self1->end, &self2->end) &&
      gsk_gradient_line_equal (&self1->line, &self2->line))
    return;

  gsk_render_node_diff_impossible (node1, node2, data);
}

static void
gsk_linear_gradient_node_class_init (gpointer g_class,
                                     gpointer class_data)
{
  GskRenderNodeClass *node_class = g_class;

  node_class->node_type = GSK_LINEAR_GRADIENT_NODE;

  node_class->finalize = gsk_linear_gradient_node_finalize;
  node_class->draw = gsk_linear_gradient_node_draw;
  node_class->diff = gsk_linear_gradient_node_diff;
}

static void
gsk_repeating_linear_gradient_node_class_init (gpointer g_class,
                                               gpointer class_data)
{
  GskRenderNodeClass *node_class = g_class;

  node_class->node_type = GSK_REPEATING_LINEAR_GRADIENT_NODE;

  node_class->finalize = gsk_linear_gradient_node_finalize;
  node_class->draw = gsk_linear_gradient_node_draw;
  node_class->diff = gsk_linear_gradient_node_diff;
}

/**
 * gsk_linear_gradient_node_new:
 * @bounds: the rectangle to render the linear gradient into
 * @start: the point at which the linear gradient will begin
 * @end: the point at which the linear gradient will finish
 * @color_stops: (array length=n_color_stops): a pointer to an array of
 *   `GskColorStop` defining the gradient. The offsets of all color stops
 *   must be increasing. The first stop's offset must be >= 0 and the last
 *   stop's offset must be <= 1.
 * @n_color_stops: the number of elements in @color_stops
 *
 * Creates a `GskRenderNode` that will create a linear gradient from the given
 * points and color stops, and render that into the area given by @bounds.
 *
 * Returns: (transfer full) (type GskLinearGradientNode): A new `GskRenderNode`
 */
GskRenderNode *
gsk_linear_gradient_node_new (const graphene_rect_t  *bounds,
                              const graphene_point_t *start,
                              const graphene_point_t *end,
                              const GskColorStop     *color_stops,
                              gsize                   n_color_stops)
{
  GskGradientStop *stops;
  GskRenderNode *node;

  g_return_val_if_fail (bounds != NULL, NULL);
  g_return_val_if_fail (start != NULL, NULL);
  g_return_val_if_fail (end != NULL, NULL);
  g_return_val_if_fail (color_stops != NULL, NULL);
  g_return_val_if_fail (n_color_stops >= 2, NULL);
  g_return_val_if_fail (color_stops[0].offset >= 0, NULL);

  stops = g_newa (GskGradientStop, n_color_stops);
  for (gsize i = 0; i < n_color_stops; i++)
    {
      stops[i].offset = color_stops[i].offset;
      stops[i].transition_hint = 0.5;
      gdk_color_init_from_rgba (&stops[i].color, &color_stops[i].color);
    }

  node = gsk_linear_gradient_node_new2 (bounds,
                                        start, end,
                                        GDK_COLOR_STATE_SRGB,
                                        GSK_HUE_INTERPOLATION_SHORTER,
                                        stops, n_color_stops);

  for (gsize i = 0; i < n_color_stops; i++)
    gdk_color_finish (&stops[i].color);

  return node;
}

/*< private >
 * gsk_linear_gradient_node_new2:
 * @bounds: the rectangle to render the linear gradient into
 * @start: the point at which the linear gradient will begin
 * @end: the point at which the linear gradient will finish
 * @interpolation: the color state to interpolate in
 * @hue_interpolation: how to interpolate if @interpolation is polar
 * @color_stops: (array length=n_color_stops): a pointer to an array of
 *   `GskGradientStop` defining the gradient. The offsets of all color stops
 *   must be increasing. The first stop's offset must be >= 0 and the last
 *   stop's offset must be <= 1.
 * @n_stops: the number of elements in @stops
 *
 * Creates a `GskRenderNode` that will create a linear gradient from the given
 * points and color stops, and render that into the area given by @bounds.
 *
 * Returns: (transfer full) (type GskLinearGradientNode): A new `GskRenderNode`
 */
GskRenderNode *
gsk_linear_gradient_node_new2 (const graphene_rect_t  *bounds,
                               const graphene_point_t *start,
                               const graphene_point_t *end,
                               GdkColorState          *interpolation,
                               GskHueInterpolation     hue_interpolation,
                               const GskGradientStop  *stops,
                               gsize                   n_stops)
{
  GskLinearGradientNode *self;
  GskRenderNode *node;
  gboolean opaque;

  g_return_val_if_fail (bounds != NULL, NULL);
  g_return_val_if_fail (start != NULL, NULL);
  g_return_val_if_fail (end != NULL, NULL);
  g_return_val_if_fail (stops != NULL, NULL);
  g_return_val_if_fail (n_stops >= 2, NULL);
  g_return_val_if_fail (stops[0].offset >= 0, NULL);

  for (gsize i = 1; i < n_stops; i++)
    g_return_val_if_fail (stops[i].offset >= stops[i - 1].offset, NULL);
  g_return_val_if_fail (stops[n_stops - 1].offset <= 1, NULL);

  self = gsk_render_node_alloc (GSK_LINEAR_GRADIENT_NODE);
  node = (GskRenderNode *) self;
  node->offscreen_for_opacity = FALSE;

  gsk_rect_init_from_rect (&node->bounds, bounds);
  gsk_rect_normalize (&node->bounds);
  graphene_point_init_from_point (&self->start, start);
  graphene_point_init_from_point (&self->end, end);

  gsk_gradient_line_init (&self->line,
                          interpolation,
                          hue_interpolation,
                          stops, n_stops,
                          &opaque);

  node->fully_opaque = opaque;
  node->preferred_depth = gdk_color_state_get_depth (interpolation);
  node->is_hdr = color_state_is_hdr (interpolation);

  return node;
}

/**
 * gsk_repeating_linear_gradient_node_new:
 * @bounds: the rectangle to render the linear gradient into
 * @start: the point at which the linear gradient will begin
 * @end: the point at which the linear gradient will finish
 * @color_stops: (array length=n_color_stops): a pointer to an array of
 * `GskColorStop` defining the gradient. The offsets of all color stops
 *   must be increasing. The first stop's offset must be >= 0 and the last
 *   stop's offset must be <= 1.
 * @n_color_stops: the number of elements in @color_stops
 *
 * Creates a `GskRenderNode` that will create a repeating linear gradient
 * from the given points and color stops, and render that into the area
 * given by @bounds.
 *
 * Returns: (transfer full) (type GskRepeatingLinearGradientNode): A new `GskRenderNode`
 */
GskRenderNode *
gsk_repeating_linear_gradient_node_new (const graphene_rect_t  *bounds,
                                        const graphene_point_t *start,
                                        const graphene_point_t *end,
                                        const GskColorStop     *color_stops,
                                        gsize                   n_color_stops)
{
  GskGradientStop *stops;
  GskRenderNode *node;

  g_return_val_if_fail (bounds != NULL, NULL);
  g_return_val_if_fail (start != NULL, NULL);
  g_return_val_if_fail (end != NULL, NULL);
  g_return_val_if_fail (color_stops != NULL, NULL);
  g_return_val_if_fail (n_color_stops >= 2, NULL);
  g_return_val_if_fail (color_stops[0].offset >= 0, NULL);

  stops = g_newa (GskGradientStop, n_color_stops);
  for (gsize i = 0; i < n_color_stops; i++)
    {
      stops[i].offset = color_stops[i].offset;
      stops[i].transition_hint = 0.5;
      gdk_color_init_from_rgba (&stops[i].color, &color_stops[i].color);
    }

  node = gsk_repeating_linear_gradient_node_new2 (bounds,
                                                  start, end,
                                                  GDK_COLOR_STATE_SRGB,
                                                  GSK_HUE_INTERPOLATION_SHORTER,
                                                  stops, n_color_stops);

  for (gsize i = 0; i < n_color_stops; i++)
    gdk_color_finish (&stops[i].color);

  return node;
}

/*< private >
 * gsk_repeating_linear_gradient_node_new2:
 * @bounds: the rectangle to render the linear gradient into
 * @start: the point at which the linear gradient will begin
 * @end: the point at which the linear gradient will finish
 * @interpolation: the color state to interpolate in
 * @hue_interpolation: how to interpolate if @interpolation is polar
 * @stops: (array length=n_color_stops): a pointer to an array of
 * `GskGradientStop` defining the gradient. The offsets of all color stops
 *   must be increasing. The first stop's offset must be >= 0 and the last
 *   stop's offset must be <= 1.
 * @n_stops: the number of elements in @stops
 *
 * Creates a `GskRenderNode` that will create a repeating linear gradient
 * from the given points and color stops, and render that into the area
 * given by @bounds.
 *
 * Returns: (transfer full) (type GskRepeatingLinearGradientNode): A new `GskRenderNode`
 */
GskRenderNode *
gsk_repeating_linear_gradient_node_new2 (const graphene_rect_t  *bounds,
                                         const graphene_point_t *start,
                                         const graphene_point_t *end,
                                         GdkColorState          *interpolation,
                                         GskHueInterpolation     hue_interpolation,
                                         const GskGradientStop  *stops,
                                         gsize                   n_stops)
{
  GskLinearGradientNode *self;
  GskRenderNode *node;
  gboolean opaque;

  g_return_val_if_fail (bounds != NULL, NULL);
  g_return_val_if_fail (start != NULL, NULL);
  g_return_val_if_fail (end != NULL, NULL);
  g_return_val_if_fail (stops != NULL, NULL);
  g_return_val_if_fail (n_stops >= 2, NULL);
  g_return_val_if_fail (stops[0].offset >= 0, NULL);

  for (gsize i = 1; i < n_stops; i++)
    g_return_val_if_fail (stops[i].offset >= stops[i - 1].offset, NULL);
  g_return_val_if_fail (stops[n_stops - 1].offset <= 1, NULL);

  self = gsk_render_node_alloc (GSK_REPEATING_LINEAR_GRADIENT_NODE);
  node = (GskRenderNode *) self;
  node->offscreen_for_opacity = FALSE;

  gsk_rect_init_from_rect (&node->bounds, bounds);
  gsk_rect_normalize (&node->bounds);
  graphene_point_init_from_point (&self->start, start);
  graphene_point_init_from_point (&self->end, end);

  gsk_gradient_line_init (&self->line,
                          interpolation,
                          hue_interpolation,
                          stops, n_stops,
                          &opaque);

  node->fully_opaque = opaque;
  node->preferred_depth = gdk_color_state_get_depth (interpolation);
  node->is_hdr = color_state_is_hdr (interpolation);

  return node;
}

/**
 * gsk_linear_gradient_node_get_start:
 * @node: (type GskLinearGradientNode): a `GskRenderNode` for a linear gradient
 *
 * Retrieves the initial point of the linear gradient.
 *
 * Returns: (transfer none): the initial point
 */
const graphene_point_t *
gsk_linear_gradient_node_get_start (const GskRenderNode *node)
{
  const GskLinearGradientNode *self = (const GskLinearGradientNode *) node;

  return &self->start;
}

/**
 * gsk_linear_gradient_node_get_end:
 * @node: (type GskLinearGradientNode): a `GskRenderNode` for a linear gradient
 *
 * Retrieves the final point of the linear gradient.
 *
 * Returns: (transfer none): the final point
 */
const graphene_point_t *
gsk_linear_gradient_node_get_end (const GskRenderNode *node)
{
  const GskLinearGradientNode *self = (const GskLinearGradientNode *) node;

  return &self->end;
}

/**
 * gsk_linear_gradient_node_get_n_color_stops:
 * @node: (type GskLinearGradientNode): a `GskRenderNode` for a linear gradient
 *
 * Retrieves the number of color stops in the gradient.
 *
 * Returns: the number of color stops
 */
gsize
gsk_linear_gradient_node_get_n_color_stops (const GskRenderNode *node)
{
  const GskLinearGradientNode *self = (const GskLinearGradientNode *) node;

  return self->line.n_stops;
}

/**
 * gsk_linear_gradient_node_get_color_stops:
 * @node: (type GskLinearGradientNode): a `GskRenderNode` for a linear gradient
 * @n_stops: (out) (optional): the number of color stops in the returned array
 *
 * Retrieves the color stops in the gradient.
 *
 * Returns: (array length=n_stops): the color stops in the gradient
 */
const GskColorStop *
gsk_linear_gradient_node_get_color_stops (const GskRenderNode *node,
                                          gsize               *n_stops)
{
  GskLinearGradientNode *self = (GskLinearGradientNode *) node;

  return gsk_gradient_line_get_color_stops (&self->line, n_stops);
}

/*< private >
 * gsk_gradient_node_get_stops:
 * @node: a `GskRenderNode` for a gradient
 *
 * Retrieves the color stops in the gradient.
 *
 * Returns: the color stops in the gradient
 */
const GskGradientStop *
gsk_gradient_node_get_stops (const GskRenderNode *node)
{
  const GskGradientNode *self = (const GskGradientNode *) node;

  return self->line.stops;
}

/*< private >
 * gsk_gradient_node_get_n_stops:
 * @node: a `GskRenderNode` for a gradient
 *
 * Retrieves the number of color stops in the gradient.
 *
 * Returns: the number of color stops
 */
gsize
gsk_gradient_node_get_n_stops (const GskRenderNode *node)
{
  const GskGradientNode *self = (const GskGradientNode *) node;

  return self->line.n_stops;
}

/*< private >
 * gsk_gradient_node_get_interpolation:
 * @node: a `GskRenderNode` for a gradient
 *
 * Retrieves the color state that the gradient is interpolated in.
 *
 * Returns: the interpolation color state
 */
GdkColorState *
gsk_gradient_node_get_interpolation (const GskRenderNode *node)
{
  const GskGradientNode *self = (const GskGradientNode *) node;

  return self->line.interpolation;
}

/*< private >
 * gsk_gradient_node_get_hue_interpolation:
 * @node: a `GskRenderNode` for a gradient
 *
 * Retrieves the hue interplation for the gradient.
 *
 * Returns: (array length=n_stops): the hue interpolation
 */
GskHueInterpolation
gsk_gradient_node_get_hue_interpolation (const GskRenderNode *node)
{
  const GskGradientNode *self = (const GskGradientNode *) node;

  return self->line.hue_interpolation;
}

/* }}} */
/* {{{ GSK_RADIAL_GRADIENT_NODE */

/**
 * GskRepeatingRadialGradientNode:
 *
 * A render node for a repeating radial gradient.
 */

/**
 * GskRadialGradientNode:
 *
 * A render node for a radial gradient.
 */
struct _GskRadialGradientNode
{
  GskRenderNode render_node;

  GskGradientLine line;

  graphene_point_t center;

  float hradius;
  float vradius;
  float start;
  float end;
};

static void
gsk_radial_gradient_node_finalize (GskRenderNode *node)
{
  GskRadialGradientNode *self = (GskRadialGradientNode *) node;
  GskRenderNodeClass *parent_class = g_type_class_peek (g_type_parent (GSK_TYPE_RADIAL_GRADIENT_NODE));

  gsk_gradient_line_clear (&self->line);

  parent_class->finalize (node);
}

static void
gsk_radial_gradient_node_draw (GskRenderNode *node,
                               cairo_t       *cr,
                               GdkColorState *ccs)
{
  GskRadialGradientNode *self = (GskRadialGradientNode *) node;
  cairo_pattern_t *pattern;
  gsize i;
  GskGradientLine *line = &self->line;

  pattern = cairo_pattern_create_radial (0, 0, self->hradius * self->start,
                                         0, 0, self->hradius * self->end);

  if (self->hradius != self->vradius)
    {
      cairo_matrix_t matrix;

      cairo_matrix_init_scale (&matrix, 1.0, self->hradius / self->vradius);
      cairo_pattern_set_matrix (pattern, &matrix);
    }

  if (gsk_render_node_get_node_type (node) == GSK_REPEATING_RADIAL_GRADIENT_NODE)
    cairo_pattern_set_extend (pattern, CAIRO_EXTEND_REPEAT);
  else
    cairo_pattern_set_extend (pattern, CAIRO_EXTEND_PAD);

  if (line->stops[0].offset > 0.0)
    gdk_cairo_pattern_add_color_stop_color (pattern,
                                            ccs,
                                            0.0,
                                            &line->stops[0].color);

  for (i = 0; i < line->n_stops; i++)
    {
      if (!gdk_color_state_equal (line->interpolation, ccs))
        interpolate_color_stops (ccs,
                                 line->interpolation, line->hue_interpolation,
                                 i > 0 ? line->stops[i-1].offset : 0,
                                 i > 0 ? &line->stops[i-1].color : &line->stops[i].color,
                                 line->stops[i].offset,
                                 &line->stops[i].color,
                                 i > 0 ? line->stops[i].transition_hint : 0.5,
                                 add_color_stop_to_pattern,
                                 pattern);

      gdk_cairo_pattern_add_color_stop_color (pattern,
                                              ccs,
                                              line->stops[i].offset,
                                              &line->stops[i].color);
    }

  if (line->stops[line->n_stops-1].offset < 1.0)
    {
      if (!gdk_color_state_equal (line->interpolation, ccs))
        interpolate_color_stops (ccs,
                                 line->interpolation, line->hue_interpolation,
                                 line->stops[line->n_stops-1].offset,
                                 &line->stops[line->n_stops-1].color,
                                 1,
                                 &line->stops[line->n_stops-1].color,
                                 0.5,
                                 add_color_stop_to_pattern,
                                 pattern);

      gdk_cairo_pattern_add_color_stop_color (pattern,
                                              ccs,
                                              1.0,
                                              &line->stops[line->n_stops-1].color);
    }

  gdk_cairo_rect (cr, &node->bounds);
  cairo_translate (cr, self->center.x, self->center.y);
  cairo_set_source (cr, pattern);
  cairo_fill (cr);

  cairo_pattern_destroy (pattern);
}

static void
gsk_radial_gradient_node_diff (GskRenderNode *node1,
                               GskRenderNode *node2,
                               GskDiffData   *data)
{
  GskRadialGradientNode *self1 = (GskRadialGradientNode *) node1;
  GskRadialGradientNode *self2 = (GskRadialGradientNode *) node2;

  if (!graphene_point_equal (&self1->center, &self2->center) ||
      self1->hradius != self2->hradius ||
      self1->vradius != self2->vradius ||
      self1->start != self2->start ||
      self1->end != self2->end ||
      !gsk_gradient_line_equal (&self1->line, &self2->line))
    {
      gsk_render_node_diff_impossible (node1, node2, data);
    }
}

static void
gsk_radial_gradient_node_class_init (gpointer g_class,
                                     gpointer class_data)
{
  GskRenderNodeClass *node_class = g_class;

  node_class->node_type = GSK_RADIAL_GRADIENT_NODE;

  node_class->finalize = gsk_radial_gradient_node_finalize;
  node_class->draw = gsk_radial_gradient_node_draw;
  node_class->diff = gsk_radial_gradient_node_diff;
}

static void
gsk_repeating_radial_gradient_node_class_init (gpointer g_class,
                                               gpointer class_data)
{
  GskRenderNodeClass *node_class = g_class;

  node_class->node_type = GSK_REPEATING_RADIAL_GRADIENT_NODE;

  node_class->finalize = gsk_radial_gradient_node_finalize;
  node_class->draw = gsk_radial_gradient_node_draw;
  node_class->diff = gsk_radial_gradient_node_diff;
}

/**
 * gsk_radial_gradient_node_new:
 * @bounds: the bounds of the node
 * @center: the center of the gradient
 * @hradius: the horizontal radius
 * @vradius: the vertical radius
 * @start: a percentage >= 0 that defines the start of the gradient around @center
 * @end: a percentage >= 0 that defines the end of the gradient around @center
 * @color_stops: (array length=n_color_stops): a pointer to an array of
 *   `GskColorStop` defining the gradient. The offsets of all color stops
 *   must be increasing. The first stop's offset must be >= 0 and the last
 *   stop's offset must be <= 1.
 * @n_color_stops: the number of elements in @color_stops
 *
 * Creates a `GskRenderNode` that draws a radial gradient.
 *
 * The radial gradient
 * starts around @center. The size of the gradient is dictated by @hradius
 * in horizontal orientation and by @vradius in vertical orientation.
 *
 * Returns: (transfer full) (type GskRadialGradientNode): A new `GskRenderNode`
 */
GskRenderNode *
gsk_radial_gradient_node_new (const graphene_rect_t  *bounds,
                              const graphene_point_t *center,
                              float                   hradius,
                              float                   vradius,
                              float                   start,
                              float                   end,
                              const GskColorStop     *color_stops,
                              gsize                   n_color_stops)
{
  GskGradientStop *stops;
  GskRenderNode *node;

  g_return_val_if_fail (bounds != NULL, NULL);
  g_return_val_if_fail (center != NULL, NULL);
  g_return_val_if_fail (hradius > 0., NULL);
  g_return_val_if_fail (vradius > 0., NULL);
  g_return_val_if_fail (start >= 0., NULL);
  g_return_val_if_fail (end >= 0., NULL);
  g_return_val_if_fail (end > start, NULL);
  g_return_val_if_fail (color_stops != NULL, NULL);
  g_return_val_if_fail (n_color_stops >= 2, NULL);
  g_return_val_if_fail (color_stops[0].offset >= 0, NULL);

  stops = g_newa (GskGradientStop, n_color_stops);
  for (gsize i = 0; i < n_color_stops; i++)
    {
      stops[i].offset = color_stops[i].offset;
      stops[i].transition_hint = 0.5;
      gdk_color_init_from_rgba (&stops[i].color, &color_stops[i].color);
    }

  node = gsk_radial_gradient_node_new2 (bounds, center,
                                        hradius, vradius,
                                        start, end,
                                        GDK_COLOR_STATE_SRGB,
                                        GSK_HUE_INTERPOLATION_SHORTER,
                                        stops, n_color_stops);

  for (gsize i = 0; i < n_color_stops; i++)
    gdk_color_finish (&stops[i].color);

  return node;
}

/*< private >
 * gsk_radial_gradient_node_new2:
 * @bounds: the bounds of the node
 * @center: the center of the gradient
 * @hradius: the horizontal radius
 * @vradius: the vertical radius
 * @start: a percentage >= 0 that defines the start of the gradient around @center
 * @end: a percentage >= 0 that defines the end of the gradient around @center
 * @interpolation: the color state to interpolate in
 * @hue_interpolation: how to interpolate if @interpolation is polar
 * @stops: (array length=n_color_stops): a pointer to an array of
 *   `GskGradientStop` defining the gradient. The offsets of all color stops
 *   must be increasing. The first stop's offset must be >= 0 and the last
 *   stop's offset must be <= 1.
 * @n_stops: the number of elements in @stops
 *
 * Creates a `GskRenderNode` that draws a radial gradient.
 *
 * The radial gradient
 * starts around @center. The size of the gradient is dictated by @hradius
 * in horizontal orientation and by @vradius in vertical orientation.
 *
 * Returns: (transfer full) (type GskRadialGradientNode): A new `GskRenderNode`
 */
GskRenderNode *
gsk_radial_gradient_node_new2 (const graphene_rect_t   *bounds,
                               const graphene_point_t  *center,
                               float                    hradius,
                               float                    vradius,
                               float                    start,
                               float                    end,
                               GdkColorState           *interpolation,
                               GskHueInterpolation      hue_interpolation,
                               const GskGradientStop   *stops,
                               gsize                    n_stops)
{
  GskRadialGradientNode *self;
  GskRenderNode *node;
  gboolean opaque;

  g_return_val_if_fail (bounds != NULL, NULL);
  g_return_val_if_fail (center != NULL, NULL);
  g_return_val_if_fail (hradius > 0., NULL);
  g_return_val_if_fail (vradius > 0., NULL);
  g_return_val_if_fail (start >= 0., NULL);
  g_return_val_if_fail (end >= 0., NULL);
  g_return_val_if_fail (end > start, NULL);
  g_return_val_if_fail (stops != NULL, NULL);
  g_return_val_if_fail (n_stops >= 2, NULL);
  g_return_val_if_fail (stops[0].offset >= 0, NULL);

  for (gsize i = 1; i < n_stops; i++)
    g_return_val_if_fail (stops[i].offset >= stops[i - 1].offset, NULL);
  g_return_val_if_fail (stops[n_stops - 1].offset <= 1, NULL);

  self = gsk_render_node_alloc (GSK_RADIAL_GRADIENT_NODE);
  node = (GskRenderNode *) self;
  node->offscreen_for_opacity = FALSE;

  gsk_rect_init_from_rect (&node->bounds, bounds);
  gsk_rect_normalize (&node->bounds);
  graphene_point_init_from_point (&self->center, center);

  self->hradius = hradius;
  self->vradius = vradius;
  self->start = start;
  self->end = end;

  gsk_gradient_line_init (&self->line,
                          interpolation,
                          hue_interpolation,
                          stops, n_stops,
                          &opaque);

  node->fully_opaque = opaque;
  node->preferred_depth = gdk_color_state_get_depth (interpolation);
  node->is_hdr = color_state_is_hdr (interpolation);

  return node;
}

/**
 * gsk_repeating_radial_gradient_node_new:
 * @bounds: the bounds of the node
 * @center: the center of the gradient
 * @hradius: the horizontal radius
 * @vradius: the vertical radius
 * @start: a percentage >= 0 that defines the start of the gradient around @center
 * @end: a percentage >= 0 that defines the end of the gradient around @center
 * @color_stops: (array length=n_color_stops): a pointer to an array of
 *   `GskColorStop` defining the gradient. The offsets of all color stops
 *   must be increasing. The first stop's offset must be >= 0 and the last
 *   stop's offset must be <= 1.
 * @n_color_stops: the number of elements in @color_stops
 *
 * Creates a `GskRenderNode` that draws a repeating radial gradient.
 *
 * The radial gradient starts around @center. The size of the gradient
 * is dictated by @hradius in horizontal orientation and by @vradius
 * in vertical orientation.
 *
 * Returns: (transfer full) (type GskRepeatingRadialGradientNode): A new `GskRenderNode`
 */
GskRenderNode *
gsk_repeating_radial_gradient_node_new (const graphene_rect_t  *bounds,
                                        const graphene_point_t *center,
                                        float                   hradius,
                                        float                   vradius,
                                        float                   start,
                                        float                   end,
                                        const GskColorStop     *color_stops,
                                        gsize                   n_color_stops)
{
  GskGradientStop *stops;
  GskRenderNode *node;

  g_return_val_if_fail (bounds != NULL, NULL);
  g_return_val_if_fail (center != NULL, NULL);
  g_return_val_if_fail (hradius > 0., NULL);
  g_return_val_if_fail (vradius > 0., NULL);
  g_return_val_if_fail (start >= 0., NULL);
  g_return_val_if_fail (end >= 0., NULL);
  g_return_val_if_fail (end > start, NULL);
  g_return_val_if_fail (color_stops != NULL, NULL);
  g_return_val_if_fail (n_color_stops >= 2, NULL);
  g_return_val_if_fail (color_stops[0].offset >= 0, NULL);

  stops = g_newa (GskGradientStop, n_color_stops);
  for (gsize i = 0; i < n_color_stops; i++)
    {
      stops[i].offset = color_stops[i].offset;
      stops[i].transition_hint = 0.5;
      gdk_color_init_from_rgba (&stops[i].color, &color_stops[i].color);
    }

  node = gsk_repeating_radial_gradient_node_new2 (bounds, center,
                                                  hradius, vradius,
                                                  start, end,
                                                  GDK_COLOR_STATE_SRGB,
                                                  GSK_HUE_INTERPOLATION_SHORTER,
                                                  stops, n_color_stops);

  for (gsize i = 0; i < n_color_stops; i++)
    gdk_color_finish (&stops[i].color);

  return node;
}

/*< private >
 * gsk_repeating_radial_gradient_node_new2:
 * @bounds: the bounds of the node
 * @center: the center of the gradient
 * @hradius: the horizontal radius
 * @vradius: the vertical radius
 * @start: a percentage >= 0 that defines the start of the gradient around @center
 * @end: a percentage >= 0 that defines the end of the gradient around @center
 * @interpolation: the color state to interpolate in
 * @hue_interpolation: how to interpolate if @interpolation is polar
 * @stops: (array length=n_color_stops): a pointer to an array of
 *   `GskGradientStop` defining the gradient. The offsets of all color stops
 *   must be increasing. The first stop's offset must be >= 0 and the last
 *   stop's offset must be <= 1.
 * @n_stops: the number of elements in @stops
 *
 * Creates a `GskRenderNode` that draws a repeating radial gradient.
 *
 * The radial gradient starts around @center. The size of the gradient
 * is dictated by @hradius in horizontal orientation and by @vradius
 * in vertical orientation.
 *
 * Returns: (transfer full) (type GskRepeatingRadialGradientNode): A new `GskRenderNode`
 */
GskRenderNode *
gsk_repeating_radial_gradient_node_new2 (const graphene_rect_t   *bounds,
                                         const graphene_point_t  *center,
                                         float                    hradius,
                                         float                    vradius,
                                         float                    start,
                                         float                    end,
                                         GdkColorState           *interpolation,
                                         GskHueInterpolation      hue_interpolation,
                                         const GskGradientStop   *stops,
                                         gsize                    n_stops)
{
  GskRadialGradientNode *self;
  GskRenderNode *node;
  gboolean opaque;

  g_return_val_if_fail (bounds != NULL, NULL);
  g_return_val_if_fail (center != NULL, NULL);
  g_return_val_if_fail (hradius > 0., NULL);
  g_return_val_if_fail (vradius > 0., NULL);
  g_return_val_if_fail (start >= 0., NULL);
  g_return_val_if_fail (end >= 0., NULL);
  g_return_val_if_fail (end > start, NULL);
  g_return_val_if_fail (stops != NULL, NULL);
  g_return_val_if_fail (n_stops >= 2, NULL);
  g_return_val_if_fail (stops[0].offset >= 0, NULL);

  for (gsize i = 1; i < n_stops; i++)
    g_return_val_if_fail (stops[i].offset >= stops[i - 1].offset, NULL);
  g_return_val_if_fail (stops[n_stops - 1].offset <= 1, NULL);

  self = gsk_render_node_alloc (GSK_REPEATING_RADIAL_GRADIENT_NODE);
  node = (GskRenderNode *) self;
  node->offscreen_for_opacity = FALSE;

  gsk_rect_init_from_rect (&node->bounds, bounds);
  gsk_rect_normalize (&node->bounds);
  graphene_point_init_from_point (&self->center, center);

  self->hradius = hradius;
  self->vradius = vradius;
  self->start = start;
  self->end = end;

  gsk_gradient_line_init (&self->line,
                          interpolation,
                          hue_interpolation,
                          stops, n_stops,
                          &opaque);

  node->fully_opaque = opaque;
  node->preferred_depth = gdk_color_state_get_depth (interpolation);
  node->is_hdr = color_state_is_hdr (interpolation);

  return node;
}

/**
 * gsk_radial_gradient_node_get_n_color_stops:
 * @node: (type GskRadialGradientNode): a `GskRenderNode` for a radial gradient
 *
 * Retrieves the number of color stops in the gradient.
 *
 * Returns: the number of color stops
 */
gsize
gsk_radial_gradient_node_get_n_color_stops (const GskRenderNode *node)
{
  const GskRadialGradientNode *self = (const GskRadialGradientNode *) node;

  return self->line.n_stops;
}

/**
 * gsk_radial_gradient_node_get_color_stops:
 * @node: (type GskRadialGradientNode): a `GskRenderNode` for a radial gradient
 * @n_stops: (out) (optional): the number of color stops in the returned array
 *
 * Retrieves the color stops in the gradient.
 *
 * Returns: (array length=n_stops): the color stops in the gradient
 */
const GskColorStop *
gsk_radial_gradient_node_get_color_stops (const GskRenderNode *node,
                                          gsize               *n_stops)
{
  GskRadialGradientNode *self = (GskRadialGradientNode *) node;

  return gsk_gradient_line_get_color_stops (&self->line, n_stops);
}

/**
 * gsk_radial_gradient_node_get_center:
 * @node: (type GskRadialGradientNode): a `GskRenderNode` for a radial gradient
 *
 * Retrieves the center pointer for the gradient.
 *
 * Returns: the center point for the gradient
 */
const graphene_point_t *
gsk_radial_gradient_node_get_center (const GskRenderNode *node)
{
  const GskRadialGradientNode *self = (const GskRadialGradientNode *) node;

  return &self->center;
}

/**
 * gsk_radial_gradient_node_get_hradius:
 * @node: (type GskRadialGradientNode): a `GskRenderNode` for a radial gradient
 *
 * Retrieves the horizontal radius for the gradient.
 *
 * Returns: the horizontal radius for the gradient
 */
float
gsk_radial_gradient_node_get_hradius (const GskRenderNode *node)
{
  const GskRadialGradientNode *self = (const GskRadialGradientNode *) node;

  return self->hradius;
}

/**
 * gsk_radial_gradient_node_get_vradius:
 * @node: (type GskRadialGradientNode): a `GskRenderNode` for a radial gradient
 *
 * Retrieves the vertical radius for the gradient.
 *
 * Returns: the vertical radius for the gradient
 */
float
gsk_radial_gradient_node_get_vradius (const GskRenderNode *node)
{
  const GskRadialGradientNode *self = (const GskRadialGradientNode *) node;

  return self->vradius;
}

/**
 * gsk_radial_gradient_node_get_start:
 * @node: (type GskRadialGradientNode): a `GskRenderNode` for a radial gradient
 *
 * Retrieves the start value for the gradient.
 *
 * Returns: the start value for the gradient
 */
float
gsk_radial_gradient_node_get_start (const GskRenderNode *node)
{
  const GskRadialGradientNode *self = (const GskRadialGradientNode *) node;

  return self->start;
}

/**
 * gsk_radial_gradient_node_get_end:
 * @node: (type GskRadialGradientNode): a `GskRenderNode` for a radial gradient
 *
 * Retrieves the end value for the gradient.
 *
 * Returns: the end value for the gradient
 */
float
gsk_radial_gradient_node_get_end (const GskRenderNode *node)
{
  const GskRadialGradientNode *self = (const GskRadialGradientNode *) node;

  return self->end;
}

/* }}} */
/* {{{ GSK_CONIC_GRADIENT_NODE */

/**
 * GskConicGradientNode:
 *
 * A render node for a conic gradient.
 */

struct _GskConicGradientNode
{
  GskRenderNode render_node;

  GskGradientLine line;

  graphene_point_t center;
  float rotation;
  float angle;
};

static void
gsk_conic_gradient_node_finalize (GskRenderNode *node)
{
  GskConicGradientNode *self = (GskConicGradientNode *) node;
  GskRenderNodeClass *parent_class = g_type_class_peek (g_type_parent (GSK_TYPE_CONIC_GRADIENT_NODE));

  gsk_gradient_line_clear (&self->line);

  parent_class->finalize (node);
}

#define DEG_TO_RAD(x)          ((x) * (G_PI / 180.f))

static void
_cairo_mesh_pattern_set_corner_rgba (cairo_pattern_t *pattern,
                                     guint            corner_num,
                                     const float      color[4])
{
  cairo_mesh_pattern_set_corner_color_rgba (pattern, corner_num, color[0], color[1], color[2], color[3]);
}

static void
project (double  angle,
         double  radius,
         double *x_out,
         double *y_out)
{
  double x, y;

#ifdef HAVE_SINCOS
  sincos (angle, &y, &x);
#else
  x = cos (angle);
  y = sin (angle);
#endif
  *x_out = radius * x;
  *y_out = radius * y;
}

static void
gsk_conic_gradient_node_add_patch (cairo_pattern_t *pattern,
                                   float            radius,
                                   float            start_angle,
                                   const float      start_color[4],
                                   float            end_angle,
                                   const float      end_color[4])
{
  double x, y;

  cairo_mesh_pattern_begin_patch (pattern);

  cairo_mesh_pattern_move_to  (pattern, 0, 0);
  project (start_angle, radius, &x, &y);
  cairo_mesh_pattern_line_to  (pattern, x, y);
  project (end_angle, radius, &x, &y);
  cairo_mesh_pattern_line_to  (pattern, x, y);
  cairo_mesh_pattern_line_to  (pattern, 0, 0);

  _cairo_mesh_pattern_set_corner_rgba (pattern, 0, start_color);
  _cairo_mesh_pattern_set_corner_rgba (pattern, 1, start_color);
  _cairo_mesh_pattern_set_corner_rgba (pattern, 2, end_color);
  _cairo_mesh_pattern_set_corner_rgba (pattern, 3, end_color);

  cairo_mesh_pattern_end_patch (pattern);
}

static void
gdk_rgba_color_interpolate (GdkRGBA       *dest,
                            const GdkRGBA *src1,
                            const GdkRGBA *src2,
                            double         progress)
{
  double alpha = src1->alpha * (1.0 - progress) + src2->alpha * progress;

  dest->alpha = alpha;
  if (alpha == 0)
    {
      dest->red = src1->red * (1.0 - progress) + src2->red * progress;
      dest->green = src1->green * (1.0 - progress) + src2->green * progress;
      dest->blue = src1->blue * (1.0 - progress) + src2->blue * progress;
    }
  else
    {
      dest->red = (src1->red * src1->alpha * (1.0 - progress) + src2->red * src2->alpha * progress) / alpha;
      dest->green = (src1->green * src1->alpha * (1.0 - progress) + src2->green * src2->alpha * progress) / alpha;
      dest->blue = (src1->blue * src1->alpha * (1.0 - progress) + src2->blue * src2->alpha * progress) / alpha;
    }
}

static void
add_color_stop_to_array (float    offset,
                         GdkColorState *ccs,
                         float    values[4],
                         gpointer data)
{
  GArray *stops = data;
  GskGradientStop stop;

  stop.offset = offset;
  gdk_color_init (&stop.color, ccs, values);

  g_array_append_val (stops, stop);
}

static void
clear_stop (gpointer data)
{
  GskGradientStop *stop = data;

  gdk_color_finish (&stop->color);
}

static void
gsk_conic_gradient_node_draw (GskRenderNode *node,
                              cairo_t       *cr,
                              GdkColorState *ccs)
{
  GskConicGradientNode *self = (GskConicGradientNode *) node;
  cairo_pattern_t *pattern;
  graphene_point_t corner;
  float radius;
  gsize i;
  GArray *stops;
  GskGradientLine *line = &self->line;

  pattern = cairo_pattern_create_mesh ();
  graphene_rect_get_top_right (&node->bounds, &corner);
  radius = graphene_point_distance (&self->center, &corner, NULL, NULL);
  graphene_rect_get_bottom_right (&node->bounds, &corner);
  radius = MAX (radius, graphene_point_distance (&self->center, &corner, NULL, NULL));
  graphene_rect_get_bottom_left (&node->bounds, &corner);
  radius = MAX (radius, graphene_point_distance (&self->center, &corner, NULL, NULL));
  graphene_rect_get_top_left (&node->bounds, &corner);
  radius = MAX (radius, graphene_point_distance (&self->center, &corner, NULL, NULL));

  stops = g_array_new (FALSE, TRUE, sizeof (GskGradientStop));
  g_array_set_clear_func (stops, clear_stop);

  if (gdk_color_state_equal (line->interpolation, ccs))
    {
      for (i = 0; i < line->n_stops; i++)
        {
          GskGradientStop *stop = &line->stops[i];
          g_array_append_val (stops, *stop);
          /* take a ref, since clear_stop removes one */
          gdk_color_state_ref (stop->color.color_state);
        }
    }
  else
    {
      g_array_append_val (stops, line->stops[0]);

      for (i = 1; i < line->n_stops; i++)
        {
          interpolate_color_stops (ccs,
                                   line->interpolation, line->hue_interpolation,
                                   line->stops[i-1].offset, &line->stops[i-1].color,
                                   line->stops[i].offset, &line->stops[i].color,
                                   line->stops[i].transition_hint,
                                   add_color_stop_to_array,
                                   stops);
          g_array_append_val (stops, line->stops[i]);
          /* take a ref, since clear_stop removes one */
          gdk_color_state_ref (line->stops[i].color.color_state);
        }
    }

  for (i = 0; i <= stops->len; i++)
    {
      GskGradientStop *stop1 = &g_array_index (stops, GskGradientStop, MAX (i, 1) - 1);
      GskGradientStop *stop2 = &g_array_index (stops, GskGradientStop, MIN (i, stops->len - 1));
      double offset1 = i > 0 ? stop1->offset : 0;
      double offset2 = i < line->n_stops ? stop2->offset : 1;
      double transition_hint = i > 0 && i < line->n_stops ? stop2->transition_hint : 0.5;
      double start_angle, end_angle;
      float color1[4];
      float color2[4];
      double exp;

      offset1 = offset1 * 360 + self->rotation - 90;
      offset2 = offset2 * 360 + self->rotation - 90;

      gdk_color_to_float (&stop1->color, ccs, color1);
      gdk_color_to_float (&stop2->color, ccs, color2);

      if (transition_hint <= 0)
        exp = 0;
      else if (transition_hint >= 1)
        exp = INFINITY;
      else if (transition_hint == 0.5)
        exp = 1;
      else
        exp = - M_LN2 / logf (transition_hint);

      for (start_angle = offset1; start_angle < offset2; start_angle = end_angle)
        {
          float f, C;
          float start_color[4], end_color[4];

          end_angle = (floor (start_angle / 45) + 1) * 45;
          end_angle = MIN (end_angle, offset2);

          f = (start_angle - offset1) / (offset2 - offset1);
          if (transition_hint <= 0)
            C = 1;
          else if (transition_hint >= 1)
            C = 0;
          else if (transition_hint == 0.5)
            C = f;
          else
            C = powf (f, exp);

          gdk_rgba_color_interpolate ((GdkRGBA *) &start_color,
                                      (const GdkRGBA *) &color1,
                                      (const GdkRGBA *) &color2,
                                      C);

          f = (end_angle - offset1) / (offset2 - offset1);
          if (transition_hint <= 0)
            C = 1;
          else if (transition_hint >= 1)
            C = 0;
          else if (transition_hint == 0.5)
            C = f;
          else
            C = powf (f, exp);

          gdk_rgba_color_interpolate ((GdkRGBA *) &end_color,
                                      (const GdkRGBA *) &color1,
                                      (const GdkRGBA *) &color2,
                                      C);

          gsk_conic_gradient_node_add_patch (pattern,
                                             radius,
                                             DEG_TO_RAD (start_angle),
                                             start_color,
                                             DEG_TO_RAD (end_angle),
                                             end_color);
        }
    }

  g_array_unref (stops);

  cairo_pattern_set_extend (pattern, CAIRO_EXTEND_PAD);

  gdk_cairo_rect (cr, &node->bounds);
  cairo_translate (cr, self->center.x, self->center.y);
  cairo_set_source (cr, pattern);
  cairo_fill (cr);

  cairo_pattern_destroy (pattern);
}

static void
gsk_conic_gradient_node_diff (GskRenderNode *node1,
                              GskRenderNode *node2,
                              GskDiffData   *data)
{
  GskConicGradientNode *self1 = (GskConicGradientNode *) node1;
  GskConicGradientNode *self2 = (GskConicGradientNode *) node2;

  if (!graphene_point_equal (&self1->center, &self2->center) ||
      self1->rotation != self2->rotation ||
      !gsk_gradient_line_equal (&self1->line, &self2->line))
    {
      gsk_render_node_diff_impossible (node1, node2, data);
    }
}

static void
gsk_conic_gradient_node_class_init (gpointer g_class,
                                    gpointer class_data)
{
  GskRenderNodeClass *node_class = g_class;

  node_class->node_type = GSK_CONIC_GRADIENT_NODE;

  node_class->finalize = gsk_conic_gradient_node_finalize;
  node_class->draw = gsk_conic_gradient_node_draw;
  node_class->diff = gsk_conic_gradient_node_diff;
}

/**
 * gsk_conic_gradient_node_new:
 * @bounds: the bounds of the node
 * @center: the center of the gradient
 * @rotation: the rotation of the gradient in degrees
 * @color_stops: (array length=n_color_stops): a pointer to an array of
 *   `GskColorStop` defining the gradient. The offsets of all color stops
 *   must be increasing. The first stop's offset must be >= 0 and the last
 *   stop's offset must be <= 1.
 * @n_color_stops: the number of elements in @color_stops
 *
 * Creates a `GskRenderNode` that draws a conic gradient.
 *
 * The conic gradient
 * starts around @center in the direction of @rotation. A rotation of 0 means
 * that the gradient points up. Color stops are then added clockwise.
 *
 * Returns: (transfer full) (type GskConicGradientNode): A new `GskRenderNode`
 */
GskRenderNode *
gsk_conic_gradient_node_new (const graphene_rect_t  *bounds,
                             const graphene_point_t *center,
                             float                   rotation,
                             const GskColorStop     *color_stops,
                             gsize                   n_color_stops)
{
  GskGradientStop *stops;
  GskRenderNode *node;

  g_return_val_if_fail (bounds != NULL, NULL);
  g_return_val_if_fail (center != NULL, NULL);
  g_return_val_if_fail (color_stops != NULL, NULL);
  g_return_val_if_fail (n_color_stops >= 2, NULL);
  g_return_val_if_fail (color_stops[0].offset >= 0, NULL);

  stops = g_newa (GskGradientStop, n_color_stops);
  for (gsize i = 0; i < n_color_stops; i++)
    {
      stops[i].offset = color_stops[i].offset;
      stops[i].transition_hint = 0.5;
      gdk_color_init_from_rgba (&stops[i].color, &color_stops[i].color);
    }

  node = gsk_conic_gradient_node_new2 (bounds,
                                       center, rotation,
                                       GDK_COLOR_STATE_SRGB,
                                       GSK_HUE_INTERPOLATION_SHORTER,
                                       stops, n_color_stops);

  for (gsize i = 0; i < n_color_stops; i++)
    gdk_color_finish (&stops[i].color);

  return node;
}

/*< private >
 * gsk_conic_gradient_node_new2:
 * @bounds: the bounds of the node
 * @center: the center of the gradient
 * @rotation: the rotation of the gradient in degrees
 * @interpolation: the color state to interpolate in
 * @hue_interpolation: how to interpolate if @interpolation is polar
 * @stops: (array length=n_color_stops): a pointer to an array of
 *   `GskColorStop` defining the gradient. The offsets of all color stops
 *   must be increasing. The first stop's offset must be >= 0 and the last
 *   stop's offset must be <= 1.
 * @n_stops: the number of elements in @stops
 *
 * Creates a `GskRenderNode` that draws a conic gradient.
 *
 * The conic gradient
 * starts around @center in the direction of @rotation. A rotation of 0 means
 * that the gradient points up. Color stops are then added clockwise.
 *
 * Returns: (transfer full) (type GskConicGradientNode): A new `GskRenderNode`
 */
GskRenderNode *
gsk_conic_gradient_node_new2 (const graphene_rect_t   *bounds,
                              const graphene_point_t  *center,
                              float                    rotation,
                              GdkColorState           *interpolation,
                              GskHueInterpolation      hue_interpolation,
                              const GskGradientStop   *stops,
                              gsize                    n_stops)
{
  GskConicGradientNode *self;
  GskRenderNode *node;
  gboolean opaque;

  g_return_val_if_fail (bounds != NULL, NULL);
  g_return_val_if_fail (center != NULL, NULL);
  g_return_val_if_fail (stops != NULL, NULL);
  g_return_val_if_fail (n_stops >= 2, NULL);
  g_return_val_if_fail (stops[0].offset >= 0, NULL);

  for (gsize i = 1; i < n_stops; i++)
    g_return_val_if_fail (stops[i].offset >= stops[i - 1].offset, NULL);
  g_return_val_if_fail (stops[n_stops - 1].offset <= 1, NULL);

  self = gsk_render_node_alloc (GSK_CONIC_GRADIENT_NODE);
  node = (GskRenderNode *) self;
  node->offscreen_for_opacity = FALSE;

  gsk_rect_init_from_rect (&node->bounds, bounds);
  gsk_rect_normalize (&node->bounds);
  graphene_point_init_from_point (&self->center, center);

  self->rotation = rotation;

  gsk_gradient_line_init (&self->line,
                          interpolation,
                          hue_interpolation,
                          stops, n_stops,
                          &opaque);

  node->fully_opaque = opaque;
  node->preferred_depth = gdk_color_state_get_depth (interpolation);
  node->is_hdr = color_state_is_hdr (interpolation);

  self->angle = 90.f - self->rotation;
  self->angle = G_PI * self->angle / 180.f;
  self->angle = fmodf (self->angle, 2.f * G_PI);
  if (self->angle < 0.f)
    self->angle += 2.f * G_PI;

  return node;
}

/**
 * gsk_conic_gradient_node_get_n_color_stops:
 * @node: (type GskConicGradientNode): a `GskRenderNode` for a conic gradient
 *
 * Retrieves the number of color stops in the gradient.
 *
 * Returns: the number of color stops
 */
gsize
gsk_conic_gradient_node_get_n_color_stops (const GskRenderNode *node)
{
  const GskConicGradientNode *self = (const GskConicGradientNode *) node;

  return self->line.n_stops;
}

/**
 * gsk_conic_gradient_node_get_color_stops:
 * @node: (type GskConicGradientNode): a `GskRenderNode` for a conic gradient
 * @n_stops: (out) (optional): the number of color stops in the returned array
 *
 * Retrieves the color stops in the gradient.
 *
 * Returns: (array length=n_stops): the color stops in the gradient
 */
const GskColorStop *
gsk_conic_gradient_node_get_color_stops (const GskRenderNode *node,
                                         gsize               *n_stops)
{
  GskConicGradientNode *self = (GskConicGradientNode *) node;

  return gsk_gradient_line_get_color_stops (&self->line, n_stops);
}

/**
 * gsk_conic_gradient_node_get_center:
 * @node: (type GskConicGradientNode): a `GskRenderNode` for a conic gradient
 *
 * Retrieves the center pointer for the gradient.
 *
 * Returns: the center point for the gradient
 */
const graphene_point_t *
gsk_conic_gradient_node_get_center (const GskRenderNode *node)
{
  const GskConicGradientNode *self = (const GskConicGradientNode *) node;

  return &self->center;
}

/**
 * gsk_conic_gradient_node_get_rotation:
 * @node: (type GskConicGradientNode): a `GskRenderNode` for a conic gradient
 *
 * Retrieves the rotation for the gradient in degrees.
 *
 * Returns: the rotation for the gradient
 */
float
gsk_conic_gradient_node_get_rotation (const GskRenderNode *node)
{
  const GskConicGradientNode *self = (const GskConicGradientNode *) node;

  return self->rotation;
}

/**
 * gsk_conic_gradient_node_get_angle:
 * @node: (type GskConicGradientNode): a `GskRenderNode` for a conic gradient
 *
 * Retrieves the angle for the gradient in radians, normalized in [0, 2 * PI].
 *
 * The angle is starting at the top and going clockwise, as expressed
 * in the css specification:
 *
 *     angle = 90 - gsk_conic_gradient_node_get_rotation()
 *
 * Returns: the angle for the gradient
 *
 * Since: 4.2
 */
float
gsk_conic_gradient_node_get_angle (const GskRenderNode *node)
{
  const GskConicGradientNode *self = (const GskConicGradientNode *) node;

  return self->angle;
}

/* }}} */
/* {{{ GSK_BORDER_NODE */

/**
 * GskBorderNode:
 *
 * A render node for a border.
 */
struct _GskBorderNode
{
  GskRenderNode render_node;

  bool uniform_width: 1;
  bool uniform_color: 1;
  GskRoundedRect outline;
  float border_width[4];
  GdkColor border_color[4];
  GdkRGBA *border_rgba;
};

static void
gsk_border_node_finalize (GskRenderNode *node)
{
  GskBorderNode *self = (GskBorderNode *) node;
  GskRenderNodeClass *parent_class = g_type_class_peek (g_type_parent (GSK_TYPE_BORDER_NODE));

  for (int i = 0; i < 4; i++)
    gdk_color_finish (&self->border_color[i]);

  g_free (self->border_rgba);

  parent_class->finalize (node);
}

static void
gsk_border_node_mesh_add_patch (cairo_pattern_t *pattern,
                                GdkColorState   *ccs,
                                const GdkColor  *color,
                                double           x0,
                                double           y0,
                                double           x1,
                                double           y1,
                                double           x2,
                                double           y2,
                                double           x3,
                                double           y3)
{
  float values[4];

  gdk_color_to_float (color, ccs, values);

  cairo_mesh_pattern_begin_patch (pattern);
  cairo_mesh_pattern_move_to (pattern, x0, y0);
  cairo_mesh_pattern_line_to (pattern, x1, y1);
  cairo_mesh_pattern_line_to (pattern, x2, y2);
  cairo_mesh_pattern_line_to (pattern, x3, y3);
  cairo_mesh_pattern_set_corner_color_rgba (pattern, 0, values[0], values[1], values[2], values[3]);
  cairo_mesh_pattern_set_corner_color_rgba (pattern, 1, values[0], values[1], values[2], values[3]);
  cairo_mesh_pattern_set_corner_color_rgba (pattern, 2, values[0], values[1], values[2], values[3]);
  cairo_mesh_pattern_set_corner_color_rgba (pattern, 3, values[0], values[1], values[2], values[3]);
  cairo_mesh_pattern_end_patch (pattern);
}

static void
gsk_border_node_draw (GskRenderNode *node,
                       cairo_t       *cr,
                       GdkColorState *ccs)
{
  GskBorderNode *self = (GskBorderNode *) node;
  GskRoundedRect inside;

  cairo_save (cr);

  gsk_rounded_rect_init_copy (&inside, &self->outline);
  gsk_rounded_rect_shrink (&inside,
                           self->border_width[0], self->border_width[1],
                           self->border_width[2], self->border_width[3]);

  cairo_set_fill_rule (cr, CAIRO_FILL_RULE_EVEN_ODD);
  gsk_rounded_rect_path (&self->outline, cr);
  gsk_rounded_rect_path (&inside, cr);

  if (gdk_color_equal (&self->border_color[0], &self->border_color[1]) &&
      gdk_color_equal (&self->border_color[0], &self->border_color[2]) &&
      gdk_color_equal (&self->border_color[0], &self->border_color[3]))
    {
      gdk_cairo_set_source_color (cr, ccs, &self->border_color[0]);
    }
  else
    {
      const graphene_rect_t *bounds = &self->outline.bounds;
      /* distance to center "line":
       * +-------------------------+
       * |                         |
       * |                         |
       * |     ---this-line---     |
       * |                         |
       * |                         |
       * +-------------------------+
       * That line is equidistant from all sides. It's either horizontal
       * or vertical, depending on if the rect is wider or taller.
       * We use the 4 sides spanned up by connecting the line to the corner
       * points to color the regions of the rectangle differently.
       * Note that the call to cairo_fill() will add the potential final
       * segment by closing the path, so we don't have to care.
       */
      cairo_pattern_t *mesh;
      cairo_matrix_t mat;
      graphene_point_t tl, br;
      float scale;

      mesh = cairo_pattern_create_mesh ();
      cairo_matrix_init_translate (&mat, -bounds->origin.x, -bounds->origin.y);
      cairo_pattern_set_matrix (mesh, &mat);

      scale = MIN (bounds->size.width / (self->border_width[1] + self->border_width[3]),
                   bounds->size.height / (self->border_width[0] + self->border_width[2]));
      graphene_point_init (&tl,
                           self->border_width[3] * scale,
                           self->border_width[0] * scale);
      graphene_point_init (&br,
                           bounds->size.width - self->border_width[1] * scale,
                           bounds->size.height - self->border_width[2] * scale);

      /* Top */
      if (self->border_width[0] > 0)
        {
          gsk_border_node_mesh_add_patch (mesh,
                                          ccs,
                                          &self->border_color[0],
                                          0, 0,
                                          tl.x, tl.y,
                                          br.x, tl.y,
                                          bounds->size.width, 0);
        }

      /* Right */
      if (self->border_width[1] > 0)
        {
          gsk_border_node_mesh_add_patch (mesh,
                                          ccs,
                                          &self->border_color[1],
                                          bounds->size.width, 0,
                                          br.x, tl.y,
                                          br.x, br.y,
                                          bounds->size.width, bounds->size.height);
        }

      /* Bottom */
      if (self->border_width[2] > 0)
        {
          gsk_border_node_mesh_add_patch (mesh,
                                          ccs,
                                          &self->border_color[2],
                                          0, bounds->size.height,
                                          tl.x, br.y,
                                          br.x, br.y,
                                          bounds->size.width, bounds->size.height);
        }

      /* Left */
      if (self->border_width[3] > 0)
        {
          gsk_border_node_mesh_add_patch (mesh,
                                          ccs,
                                          &self->border_color[3],
                                          0, 0,
                                          tl.x, tl.y,
                                          tl.x, br.y,
                                          0, bounds->size.height);
        }

      cairo_set_source (cr, mesh);
      cairo_pattern_destroy (mesh);
    }

  cairo_fill (cr);
  cairo_restore (cr);
}

static void
gsk_border_node_diff (GskRenderNode *node1,
                      GskRenderNode *node2,
                      GskDiffData   *data)
{
  GskBorderNode *self1 = (GskBorderNode *) node1;
  GskBorderNode *self2 = (GskBorderNode *) node2;
  gboolean uniform1 = self1->uniform_width && self1->uniform_color;
  gboolean uniform2 = self2->uniform_width && self2->uniform_color;

  if (uniform1 &&
      uniform2 &&
      self1->border_width[0] == self2->border_width[0] &&
      gsk_rounded_rect_equal (&self1->outline, &self2->outline) &&
      gdk_color_equal (&self1->border_color[0], &self2->border_color[0]))
    return;

  /* Different uniformity -> diff impossible */
  if (uniform1 ^ uniform2)
    {
      gsk_render_node_diff_impossible (node1, node2, data);
      return;
    }

  if (self1->border_width[0] == self2->border_width[0] &&
      self1->border_width[1] == self2->border_width[1] &&
      self1->border_width[2] == self2->border_width[2] &&
      self1->border_width[3] == self2->border_width[3] &&
      gdk_color_equal (&self1->border_color[0], &self2->border_color[0]) &&
      gdk_color_equal (&self1->border_color[1], &self2->border_color[1]) &&
      gdk_color_equal (&self1->border_color[2], &self2->border_color[2]) &&
      gdk_color_equal (&self1->border_color[3], &self2->border_color[3]) &&
      gsk_rounded_rect_equal (&self1->outline, &self2->outline))
    return;

  gsk_render_node_diff_impossible (node1, node2, data);
}

static void
gsk_border_node_class_init (gpointer g_class,
                            gpointer class_data)
{
  GskRenderNodeClass *node_class = g_class;

  node_class->node_type = GSK_BORDER_NODE;

  node_class->finalize = gsk_border_node_finalize;
  node_class->draw = gsk_border_node_draw;
  node_class->diff = gsk_border_node_diff;
}

/**
 * gsk_border_node_get_outline:
 * @node: (type GskBorderNode): a `GskRenderNode` for a border
 *
 * Retrieves the outline of the border.
 *
 * Returns: the outline of the border
 */
const GskRoundedRect *
gsk_border_node_get_outline (const GskRenderNode *node)
{
  const GskBorderNode *self = (const GskBorderNode *) node;

  return &self->outline;
}

/**
 * gsk_border_node_get_widths:
 * @node: (type GskBorderNode): a `GskRenderNode` for a border
 *
 * Retrieves the stroke widths of the border.
 *
 * Returns: (transfer none) (array fixed-size=4): an array of 4 floats
 *   for the top, right, bottom and left stroke width of the border,
 *   respectively
 */
const float *
gsk_border_node_get_widths (const GskRenderNode *node)
{
  const GskBorderNode *self = (const GskBorderNode *) node;

  return self->border_width;
}

/**
 * gsk_border_node_get_colors:
 * @node: (type GskBorderNode): a `GskRenderNode` for a border
 *
 * Retrieves the colors of the border.
 *
 * Returns: (transfer none) (array fixed-size=4): an array of 4 `GdkRGBA`
 *   structs for the top, right, bottom and left color of the border
 */
const GdkRGBA *
gsk_border_node_get_colors (const GskRenderNode *node)
{
  GskBorderNode *self = (GskBorderNode *) node;
  const GdkRGBA *colors;

  G_LOCK (rgba);

  if (self->border_rgba == NULL)
    {
      self->border_rgba = g_new (GdkRGBA, 4);
      for (int i = 0; i < 4; i++)
        gdk_color_to_float (&self->border_color[i], GDK_COLOR_STATE_SRGB, (float *) &self->border_rgba[i]);
    }

  colors = self->border_rgba;

  G_UNLOCK (rgba);

  return colors;
}

/**
 * gsk_border_node_new:
 * @outline: a `GskRoundedRect` describing the outline of the border
 * @border_width: (array fixed-size=4): the stroke width of the border on
 *     the top, right, bottom and left side respectively.
 * @border_color: (array fixed-size=4): the color used on the top, right,
 *     bottom and left side.
 *
 * Creates a `GskRenderNode` that will stroke a border rectangle inside the
 * given @outline.
 *
 * The 4 sides of the border can have different widths and colors.
 *
 * Returns: (transfer full) (type GskBorderNode): A new `GskRenderNode`
 */
GskRenderNode *
gsk_border_node_new (const GskRoundedRect *outline,
                     const float           border_width[4],
                     const GdkRGBA         border_color[4])
{
  GskRenderNode *node;
  GdkColor color[4];

  for (int i = 0; i < 4; i++)
    gdk_color_init_from_rgba (&color[i], &border_color[i]);

  node = gsk_border_node_new2 (outline, border_width, color);

  for (int i = 0; i < 4; i++)
    gdk_color_finish (&color[i]);

  return node;
}

/*< private >
 * gsk_border_node_new2:
 * @outline: a `GskRoundedRect` describing the outline of the border
 * @border_width: (array fixed-size=4): the stroke width of the border on
 *     the top, right, bottom and left side respectively.
 * @border_color: (array fixed-size=4): the color used on the top, right,
 *     bottom and left side.
 *
 * Creates a `GskRenderNode` that will stroke a border rectangle inside the
 * given @outline.
 *
 * The 4 sides of the border can have different widths and colors.
 *
 * Returns: (transfer full) (type GskBorderNode): A new `GskRenderNode`
 */
GskRenderNode *
gsk_border_node_new2 (const GskRoundedRect *outline,
                      const float           border_width[4],
                      const GdkColor        border_color[4])
{
  GskBorderNode *self;
  GskRenderNode *node;

  g_return_val_if_fail (outline != NULL, NULL);
  g_return_val_if_fail (border_width != NULL, NULL);
  g_return_val_if_fail (border_color != NULL, NULL);

  self = gsk_render_node_alloc (GSK_BORDER_NODE);
  node = (GskRenderNode *) self;
  node->offscreen_for_opacity = FALSE;
  node->preferred_depth = GDK_MEMORY_NONE;

  gsk_rounded_rect_init_copy (&self->outline, outline);
  memcpy (self->border_width, border_width, sizeof (self->border_width));
  memcpy (self->border_color, border_color, sizeof (self->border_color));
  for (int i = 0; i < 4; i++)
    gdk_color_init_copy (&self->border_color[i], &border_color[i]);

  if (border_width[0] == border_width[1] &&
      border_width[0] == border_width[2] &&
      border_width[0] == border_width[3])
    self->uniform_width = TRUE;
  else
    self->uniform_width = FALSE;

  if (gdk_color_equal (&border_color[0], &border_color[1]) &&
      gdk_color_equal (&border_color[0], &border_color[2]) &&
      gdk_color_equal (&border_color[0], &border_color[3]))
    self->uniform_color = TRUE;
  else
    self->uniform_color = FALSE;

  gsk_rect_init_from_rect (&node->bounds, &self->outline.bounds);

  return node;
}

/*< private >
 * gsk_border_node_get_gdk_colors:
 * @node: (type GskBorderNode): a `GskRenderNode` for a border
 *
 * Retrieves the colors of the border.
 *
 * Returns: (transfer none) (array fixed-size=4): an array of 4 `GdkColor`
 *   structs for the top, right, bottom and left color of the border
 */
const GdkColor *
gsk_border_node_get_gdk_colors (const GskRenderNode *node)
{
  const GskBorderNode *self = (const GskBorderNode *) node;

  return self->border_color;
}

bool
gsk_border_node_get_uniform (const GskRenderNode *self)
{
  const GskBorderNode *node = (const GskBorderNode *)self;
  return node->uniform_width && node->uniform_color;
}

bool
gsk_border_node_get_uniform_color (const GskRenderNode *self)
{
  const GskBorderNode *node = (const GskBorderNode *)self;
  return node->uniform_color;
}

/* }}} */
/* {{{ GSK_TEXTURE_NODE */

/**
 * GskTextureNode:
 *
 * A render node for a `GdkTexture`.
 */
struct _GskTextureNode
{
  GskRenderNode render_node;

  GdkTexture *texture;
};

static void
gsk_texture_node_finalize (GskRenderNode *node)
{
  GskTextureNode *self = (GskTextureNode *) node;
  GskRenderNodeClass *parent_class = g_type_class_peek (g_type_parent (GSK_TYPE_TEXTURE_NODE));

  g_clear_object (&self->texture);

  parent_class->finalize (node);
}

static void
gsk_texture_node_draw_oversized (GskRenderNode *node,
                                 cairo_t       *cr,
                                 GdkColorState *ccs)
{
  GskTextureNode *self = (GskTextureNode *) node;
  cairo_surface_t *surface;
  int x, y, width, height;
  GdkTextureDownloader downloader;
  GBytes *bytes;
  const guchar *data;
  gsize stride;

  width = gdk_texture_get_width (self->texture);
  height = gdk_texture_get_height (self->texture);
  gdk_texture_downloader_init (&downloader, self->texture);
  gdk_texture_downloader_set_format (&downloader, GDK_MEMORY_DEFAULT);
  bytes = gdk_texture_downloader_download_bytes (&downloader, &stride);
  gdk_texture_downloader_finish (&downloader);
  data = g_bytes_get_data (bytes, NULL);
  gdk_memory_convert_color_state ((guchar *) data,
                                  &GDK_MEMORY_LAYOUT_SIMPLE (
                                      GDK_MEMORY_DEFAULT,
                                      stride,
                                      width,
                                      height
                                  ),
                                  GDK_COLOR_STATE_SRGB,
                                  ccs);

  gsk_cairo_rectangle_pixel_aligned (cr, &node->bounds);
  cairo_clip (cr);

  cairo_push_group (cr);
  cairo_set_operator (cr, CAIRO_OPERATOR_ADD);
  cairo_translate (cr,
                   node->bounds.origin.x,
                   node->bounds.origin.y);
  cairo_scale (cr, 
               node->bounds.size.width / width,
               node->bounds.size.height / height);

  for (x = 0; x < width; x += MAX_CAIRO_IMAGE_WIDTH)
    {
      int tile_width = MIN (MAX_CAIRO_IMAGE_WIDTH, width - x);
      for (y = 0; y < height; y += MAX_CAIRO_IMAGE_HEIGHT)
        {
          int tile_height = MIN (MAX_CAIRO_IMAGE_HEIGHT, height - y);
          surface = cairo_image_surface_create_for_data ((guchar *) data + stride * y + 4 * x,
                                                         CAIRO_FORMAT_ARGB32,
                                                         tile_width, tile_height,
                                                         stride);

          cairo_set_source_surface (cr, surface, x, y);
          cairo_pattern_set_extend (cairo_get_source (cr), CAIRO_EXTEND_PAD);
          cairo_rectangle (cr, x, y, tile_width, tile_height);
          cairo_fill (cr);

          cairo_surface_finish (surface);
          cairo_surface_destroy (surface);
        }
    }

  cairo_pop_group_to_source (cr);
  cairo_paint (cr);
}

static void
gsk_texture_node_draw (GskRenderNode *node,
                       cairo_t       *cr,
                       GdkColorState *ccs)
{
  GskTextureNode *self = (GskTextureNode *) node;
  cairo_surface_t *surface;
  cairo_pattern_t *pattern;
  cairo_matrix_t matrix;
  int width, height;

  width = gdk_texture_get_width (self->texture);
  height = gdk_texture_get_height (self->texture);
  if (width > MAX_CAIRO_IMAGE_WIDTH || height > MAX_CAIRO_IMAGE_HEIGHT)
    {
      gsk_texture_node_draw_oversized (node, cr, ccs);
      return;
    }

  surface = gdk_texture_download_surface (self->texture, ccs);
  pattern = cairo_pattern_create_for_surface (surface);
  cairo_pattern_set_extend (pattern, CAIRO_EXTEND_PAD);

  cairo_matrix_init_scale (&matrix,
                           width / node->bounds.size.width,
                           height / node->bounds.size.height);
  cairo_matrix_translate (&matrix,
                          -node->bounds.origin.x,
                          -node->bounds.origin.y);
  cairo_pattern_set_matrix (pattern, &matrix);

  cairo_set_source (cr, pattern);
  cairo_pattern_destroy (pattern);
  cairo_surface_destroy (surface);

  gdk_cairo_rect (cr, &node->bounds);
  cairo_fill (cr);
}

static void
gsk_texture_node_diff (GskRenderNode *node1,
                       GskRenderNode *node2,
                       GskDiffData   *data)
{
  GskTextureNode *self1 = (GskTextureNode *) node1;
  GskTextureNode *self2 = (GskTextureNode *) node2;
  cairo_region_t *sub;

  if (!gsk_rect_equal (&node1->bounds, &node2->bounds) ||
      gdk_texture_get_width (self1->texture) != gdk_texture_get_width (self2->texture) ||
      gdk_texture_get_height (self1->texture) != gdk_texture_get_height (self2->texture))
    {
      gsk_render_node_diff_impossible (node1, node2, data);
      return;
    }

  if (self1->texture == self2->texture)
    return;

  sub = cairo_region_create ();
  gdk_texture_diff (self1->texture, self2->texture, sub);
  region_union_region_affine (data->region,
                              sub,
                              node1->bounds.size.width / gdk_texture_get_width (self1->texture),
                              node1->bounds.size.height / gdk_texture_get_height (self1->texture),
                              node1->bounds.origin.x,
                              node1->bounds.origin.y);
  cairo_region_destroy (sub);
}

static void
gsk_texture_node_class_init (gpointer g_class,
                             gpointer class_data)
{
  GskRenderNodeClass *node_class = g_class;

  node_class->node_type = GSK_TEXTURE_NODE;

  node_class->finalize = gsk_texture_node_finalize;
  node_class->draw = gsk_texture_node_draw;
  node_class->diff = gsk_texture_node_diff;
}

/**
 * gsk_texture_node_get_texture:
 * @node: (type GskTextureNode): a `GskRenderNode` of type %GSK_TEXTURE_NODE
 *
 * Retrieves the `GdkTexture` used when creating this `GskRenderNode`.
 *
 * Returns: (transfer none): the `GdkTexture`
 */
GdkTexture *
gsk_texture_node_get_texture (const GskRenderNode *node)
{
  const GskTextureNode *self = (const GskTextureNode *) node;

  return self->texture;
}

/**
 * gsk_texture_node_new:
 * @texture: the `GdkTexture`
 * @bounds: the rectangle to render the texture into
 *
 * Creates a `GskRenderNode` that will render the given
 * @texture into the area given by @bounds.
 *
 * Note that GSK applies linear filtering when textures are
 * scaled and transformed. See [class@Gsk.TextureScaleNode]
 * for a way to influence filtering.
 *
 * Returns: (transfer full) (type GskTextureNode): A new `GskRenderNode`
 */
GskRenderNode *
gsk_texture_node_new (GdkTexture            *texture,
                      const graphene_rect_t *bounds)
{
  GskTextureNode *self;
  GskRenderNode *node;

  g_return_val_if_fail (GDK_IS_TEXTURE (texture), NULL);
  g_return_val_if_fail (bounds != NULL, NULL);

  self = gsk_render_node_alloc (GSK_TEXTURE_NODE);
  node = (GskRenderNode *) self;
  node->offscreen_for_opacity = FALSE;
  node->fully_opaque = gdk_memory_format_alpha (gdk_texture_get_format (texture)) == GDK_MEMORY_ALPHA_OPAQUE;
  node->is_hdr = color_state_is_hdr (gdk_texture_get_color_state (texture));

  self->texture = g_object_ref (texture);
  gsk_rect_init_from_rect (&node->bounds, bounds);
  gsk_rect_normalize (&node->bounds);

  node->preferred_depth = gdk_texture_get_depth (texture);

  return node;
}

/* }}} */
/* {{{ GSK_TEXTURE_SCALE_NODE */

/**
 * GskTextureScaleNode:
 *
 * A render node for a `GdkTexture`, with control over scaling.
 *
 * Since: 4.10
 */
struct _GskTextureScaleNode
{
  GskRenderNode render_node;

  GdkTexture *texture;
  GskScalingFilter filter;
};

static void
gsk_texture_scale_node_finalize (GskRenderNode *node)
{
  GskTextureScaleNode *self = (GskTextureScaleNode *) node;
  GskRenderNodeClass *parent_class = g_type_class_peek (g_type_parent (GSK_TYPE_TEXTURE_SCALE_NODE));

  g_clear_object (&self->texture);

  parent_class->finalize (node);
}

static void
gsk_texture_scale_node_draw (GskRenderNode *node,
                             cairo_t       *cr,
                             GdkColorState *ccs)
{
  GskTextureScaleNode *self = (GskTextureScaleNode *) node;
  cairo_surface_t *surface;
  cairo_pattern_t *pattern;
  cairo_matrix_t matrix;
  cairo_filter_t filters[] = {
    CAIRO_FILTER_BILINEAR,
    CAIRO_FILTER_NEAREST,
    CAIRO_FILTER_GOOD,
  };
  cairo_t *cr2;
  cairo_surface_t *surface2;
  graphene_rect_t clip_rect;

  /* Make sure we draw the minimum region by using the clip */
  gdk_cairo_rect (cr, &node->bounds);
  cairo_clip (cr);
  _graphene_rect_init_from_clip_extents (&clip_rect, cr);
  if (clip_rect.size.width <= 0 || clip_rect.size.height <= 0)
    return;

  surface2 = cairo_image_surface_create (CAIRO_FORMAT_ARGB32,
                                         (int) ceilf (clip_rect.size.width),
                                         (int) ceilf (clip_rect.size.height));
  cairo_surface_set_device_offset (surface2, -clip_rect.origin.x, -clip_rect.origin.y);
  cr2 = cairo_create (surface2);

  surface = gdk_texture_download_surface (self->texture, ccs);
  pattern = cairo_pattern_create_for_surface (surface);
  cairo_pattern_set_extend (pattern, CAIRO_EXTEND_PAD);

  cairo_matrix_init_scale (&matrix,
                           gdk_texture_get_width (self->texture) / node->bounds.size.width,
                           gdk_texture_get_height (self->texture) / node->bounds.size.height);
  cairo_matrix_translate (&matrix, -node->bounds.origin.x, -node->bounds.origin.y);
  cairo_pattern_set_matrix (pattern, &matrix);
  cairo_pattern_set_filter (pattern, filters[self->filter]);

  cairo_set_source (cr2, pattern);
  cairo_pattern_destroy (pattern);
  cairo_surface_destroy (surface);

  gdk_cairo_rect (cr2, &node->bounds);
  cairo_fill (cr2);

  cairo_destroy (cr2);

  cairo_save (cr);

  cairo_set_source_surface (cr, surface2, 0, 0);
  cairo_pattern_set_extend (cairo_get_source (cr), CAIRO_EXTEND_PAD);
  cairo_surface_destroy (surface2);

  cairo_paint (cr);

  cairo_restore (cr);
}

static void
gsk_texture_scale_node_diff (GskRenderNode *node1,
                             GskRenderNode *node2,
                             GskDiffData   *data)
{
  GskTextureScaleNode *self1 = (GskTextureScaleNode *) node1;
  GskTextureScaleNode *self2 = (GskTextureScaleNode *) node2;
  cairo_region_t *sub;

  if (!gsk_rect_equal (&node1->bounds, &node2->bounds) ||
      self1->filter != self2->filter ||
      gdk_texture_get_width (self1->texture) != gdk_texture_get_width (self2->texture) ||
      gdk_texture_get_height (self1->texture) != gdk_texture_get_height (self2->texture))
    {
      gsk_render_node_diff_impossible (node1, node2, data);
      return;
    }

  if (self1->texture == self2->texture)
    return;

  sub = cairo_region_create ();
  gdk_texture_diff (self1->texture, self2->texture, sub);
  region_union_region_affine (data->region,
                              sub,
                              node1->bounds.size.width / gdk_texture_get_width (self1->texture),
                              node1->bounds.size.height / gdk_texture_get_height (self1->texture),
                              node1->bounds.origin.x,
                              node1->bounds.origin.y);
  cairo_region_destroy (sub);
}

static void
gsk_texture_scale_node_class_init (gpointer g_class,
                                   gpointer class_data)
{
  GskRenderNodeClass *node_class = g_class;

  node_class->node_type = GSK_TEXTURE_SCALE_NODE;

  node_class->finalize = gsk_texture_scale_node_finalize;
  node_class->draw = gsk_texture_scale_node_draw;
  node_class->diff = gsk_texture_scale_node_diff;
}

/**
 * gsk_texture_scale_node_get_texture:
 * @node: (type GskTextureScaleNode): a `GskRenderNode` of type %GSK_TEXTURE_SCALE_NODE
 *
 * Retrieves the `GdkTexture` used when creating this `GskRenderNode`.
 *
 * Returns: (transfer none): the `GdkTexture`
 *
 * Since: 4.10
 */
GdkTexture *
gsk_texture_scale_node_get_texture (const GskRenderNode *node)
{
  const GskTextureScaleNode *self = (const GskTextureScaleNode *) node;

  return self->texture;
}

/**
 * gsk_texture_scale_node_get_filter:
 * @node: (type GskTextureScaleNode): a `GskRenderNode` of type %GSK_TEXTURE_SCALE_NODE
 *
 * Retrieves the `GskScalingFilter` used when creating this `GskRenderNode`.
 *
 * Returns: (transfer none): the `GskScalingFilter`
 *
 * Since: 4.10
 */
GskScalingFilter
gsk_texture_scale_node_get_filter (const GskRenderNode *node)
{
  const GskTextureScaleNode *self = (const GskTextureScaleNode *) node;

  return self->filter;
}

/**
 * gsk_texture_scale_node_new:
 * @texture: the texture to scale
 * @bounds: the size of the texture to scale to
 * @filter: how to scale the texture
 *
 * Creates a node that scales the texture to the size given by the
 * bounds using the filter and then places it at the bounds' position.
 *
 * Note that further scaling and other transformations which are
 * applied to the node will apply linear filtering to the resulting
 * texture, as usual.
 *
 * This node is intended for tight control over scaling applied
 * to a texture, such as in image editors and requires the
 * application to be aware of the whole render tree as further
 * transforms may be applied that conflict with the desired effect
 * of this node.
 *
 * Returns: (transfer full) (type GskTextureScaleNode): A new `GskRenderNode`
 *
 * Since: 4.10
 */
GskRenderNode *
gsk_texture_scale_node_new (GdkTexture            *texture,
                            const graphene_rect_t *bounds,
                            GskScalingFilter       filter)
{
  GskTextureScaleNode *self;
  GskRenderNode *node;

  g_return_val_if_fail (GDK_IS_TEXTURE (texture), NULL);
  g_return_val_if_fail (bounds != NULL, NULL);

  self = gsk_render_node_alloc (GSK_TEXTURE_SCALE_NODE);
  node = (GskRenderNode *) self;
  node->offscreen_for_opacity = FALSE;
  node->fully_opaque = gdk_memory_format_alpha (gdk_texture_get_format (texture)) == GDK_MEMORY_ALPHA_OPAQUE &&
    bounds->size.width == floor (bounds->size.width) &&
    bounds->size.height == floor (bounds->size.height);
  node->is_hdr = color_state_is_hdr (gdk_texture_get_color_state (texture));

  self->texture = g_object_ref (texture);
  gsk_rect_init_from_rect (&node->bounds, bounds);
  gsk_rect_normalize (&node->bounds);
  self->filter = filter;

  node->preferred_depth = gdk_texture_get_depth (texture);

  return node;
}

/* }}} */
/* {{{ GSK_INSET_SHADOW_NODE */

/**
 * GskInsetShadowNode:
 *
 * A render node for an inset shadow.
 */
struct _GskInsetShadowNode
{
  GskRenderNode render_node;

  GskRoundedRect outline;
  GdkColor color;
  graphene_point_t offset;
  float spread;
  float blur_radius;
};

static void
gsk_inset_shadow_node_finalize (GskRenderNode *node)
{
  GskInsetShadowNode *self = (GskInsetShadowNode *) node;
  GskRenderNodeClass *parent_class = g_type_class_peek (g_type_parent (GSK_TYPE_INSET_SHADOW_NODE));

  gdk_color_finish (&self->color);

  parent_class->finalize (node);
}

static gboolean
has_empty_clip (cairo_t *cr)
{
  double x1, y1, x2, y2;

  cairo_clip_extents (cr, &x1, &y1, &x2, &y2);
  return x1 >= x2 || y1 >= y2;
}

static void
draw_shadow (cairo_t              *cr,
             GdkColorState        *ccs,
             gboolean              inset,
             const GskRoundedRect *box,
             const GskRoundedRect *clip_box,
             float                 radius,
             const GdkColor       *color,
             GskBlurFlags          blur_flags)
{
  cairo_t *shadow_cr;

  if (has_empty_clip (cr))
    return;

  gdk_cairo_set_source_color (cr, ccs, color);
  shadow_cr = gsk_cairo_blur_start_drawing (cr, radius, blur_flags);

  cairo_set_fill_rule (shadow_cr, CAIRO_FILL_RULE_EVEN_ODD);
  gsk_rounded_rect_path (box, shadow_cr);
  if (inset)
    gdk_cairo_rect (shadow_cr, &clip_box->bounds);

  cairo_fill (shadow_cr);

  gsk_cairo_blur_finish_drawing (shadow_cr, ccs, radius, color, blur_flags);
}

typedef struct {
  float radius;
  graphene_size_t corner;
} CornerMask;

typedef enum {
  TOP,
  RIGHT,
  BOTTOM,
  LEFT
} Side;

static guint
corner_mask_hash (CornerMask *mask)
{
  return ((guint)mask->radius << 24) ^
    ((guint)(mask->corner.width*4)) << 12 ^
    ((guint)(mask->corner.height*4)) << 0;
}

static gboolean
corner_mask_equal (CornerMask *mask1,
                   CornerMask *mask2)
{
  return
    mask1->radius == mask2->radius &&
    mask1->corner.width == mask2->corner.width &&
    mask1->corner.height == mask2->corner.height;
}

static void
draw_shadow_corner (cairo_t               *cr,
                    GdkColorState         *ccs,
                    gboolean               inset,
                    const GskRoundedRect  *box,
                    const GskRoundedRect  *clip_box,
                    float                  radius,
                    const GdkColor        *color,
                    GskCorner              corner,
                    cairo_rectangle_int_t *drawn_rect)
{
  float clip_radius;
  int x1, x2, x3, y1, y2, y3, x, y;
  GskRoundedRect corner_box;
  cairo_t *mask_cr;
  cairo_surface_t *mask;
  cairo_pattern_t *pattern;
  cairo_matrix_t matrix;
  float sx, sy;
  static GHashTable *corner_mask_cache = NULL;
  float max_other;
  CornerMask key;
  gboolean overlapped;

  clip_radius = gsk_cairo_blur_compute_pixels (radius);

  overlapped = FALSE;
  if (corner == GSK_CORNER_TOP_LEFT || corner == GSK_CORNER_BOTTOM_LEFT)
    {
      x1 = floor (box->bounds.origin.x - clip_radius);
      x2 = ceil (box->bounds.origin.x + box->corner[corner].width + clip_radius);
      x = x1;
      sx = 1;
      max_other = MAX(box->corner[GSK_CORNER_TOP_RIGHT].width, box->corner[GSK_CORNER_BOTTOM_RIGHT].width);
      x3 = floor (box->bounds.origin.x + box->bounds.size.width - max_other - clip_radius);
      if (x2 > x3)
        overlapped = TRUE;
    }
  else
    {
      x1 = floor (box->bounds.origin.x + box->bounds.size.width - box->corner[corner].width - clip_radius);
      x2 = ceil (box->bounds.origin.x + box->bounds.size.width + clip_radius);
      x = x2;
      sx = -1;
      max_other = MAX(box->corner[GSK_CORNER_TOP_LEFT].width, box->corner[GSK_CORNER_BOTTOM_LEFT].width);
      x3 = ceil (box->bounds.origin.x + max_other + clip_radius);
      if (x3 > x1)
        overlapped = TRUE;
    }

  if (corner == GSK_CORNER_TOP_LEFT || corner == GSK_CORNER_TOP_RIGHT)
    {
      y1 = floor (box->bounds.origin.y - clip_radius);
      y2 = ceil (box->bounds.origin.y + box->corner[corner].height + clip_radius);
      y = y1;
      sy = 1;
      max_other = MAX(box->corner[GSK_CORNER_BOTTOM_LEFT].height, box->corner[GSK_CORNER_BOTTOM_RIGHT].height);
      y3 = floor (box->bounds.origin.y + box->bounds.size.height - max_other - clip_radius);
      if (y2 > y3)
        overlapped = TRUE;
    }
  else
    {
      y1 = floor (box->bounds.origin.y + box->bounds.size.height - box->corner[corner].height - clip_radius);
      y2 = ceil (box->bounds.origin.y + box->bounds.size.height + clip_radius);
      y = y2;
      sy = -1;
      max_other = MAX(box->corner[GSK_CORNER_TOP_LEFT].height, box->corner[GSK_CORNER_TOP_RIGHT].height);
      y3 = ceil (box->bounds.origin.y + max_other + clip_radius);
      if (y3 > y1)
        overlapped = TRUE;
    }

  drawn_rect->x = x1;
  drawn_rect->y = y1;
  drawn_rect->width = x2 - x1;
  drawn_rect->height = y2 - y1;

  cairo_rectangle (cr, x1, y1, x2 - x1, y2 - y1);
  cairo_clip (cr);

  if (inset || overlapped)
    {
      /* Fall back to generic path if inset or if the corner radius
         runs into each other */
      draw_shadow (cr, ccs, inset, box, clip_box, radius, color, GSK_BLUR_X | GSK_BLUR_Y);
      return;
    }

  if (has_empty_clip (cr))
    return;

  /* At this point we're drawing a blurred outset corner. The only
   * things that affect the output of the blurred mask in this case
   * is:
   *
   * What corner this is, which defines the orientation (sx,sy)
   * and position (x,y)
   *
   * The blur radius (which also defines the clip_radius)
   *
   * The horizontal and vertical corner radius
   *
   * We apply the first position and orientation when drawing the
   * mask, so we cache rendered masks based on the blur radius and the
   * corner radius.
   */
  if (corner_mask_cache == NULL)
    corner_mask_cache = g_hash_table_new_full ((GHashFunc)corner_mask_hash,
                                               (GEqualFunc)corner_mask_equal,
                                               g_free, (GDestroyNotify)cairo_surface_destroy);

  key.radius = radius;
  key.corner = box->corner[corner];

  mask = g_hash_table_lookup (corner_mask_cache, &key);
  if (mask == NULL)
    {
      mask = cairo_surface_create_similar_image (cairo_get_target (cr), CAIRO_FORMAT_A8,
                                                 drawn_rect->width + clip_radius,
                                                 drawn_rect->height + clip_radius);
      mask_cr = cairo_create (mask);
      gsk_rounded_rect_init_from_rect (&corner_box, &GRAPHENE_RECT_INIT (clip_radius, clip_radius, 2*drawn_rect->width, 2*drawn_rect->height), 0);
      corner_box.corner[0] = box->corner[corner];
      gsk_rounded_rect_path (&corner_box, mask_cr);
      cairo_fill (mask_cr);
      gsk_cairo_blur_surface (mask, radius, GSK_BLUR_X | GSK_BLUR_Y);
      cairo_destroy (mask_cr);
      g_hash_table_insert (corner_mask_cache, g_memdup2 (&key, sizeof (key)), mask);
    }

  gdk_cairo_set_source_color (cr, ccs, color);
  pattern = cairo_pattern_create_for_surface (mask);
  cairo_matrix_init_identity (&matrix);
  cairo_matrix_scale (&matrix, sx, sy);
  cairo_matrix_translate (&matrix, -x, -y);
  cairo_pattern_set_matrix (pattern, &matrix);
  cairo_mask (cr, pattern);
  cairo_pattern_destroy (pattern);
}

static void
draw_shadow_side (cairo_t               *cr,
                  GdkColorState         *ccs,
                  gboolean               inset,
                  const GskRoundedRect  *box,
                  const GskRoundedRect  *clip_box,
                  float                  radius,
                  const GdkColor        *color,
                  Side                   side,
                  cairo_rectangle_int_t *drawn_rect)
{
  GskBlurFlags blur_flags = GSK_BLUR_REPEAT;
  double clip_radius;
  int x1, x2, y1, y2;

  clip_radius = gsk_cairo_blur_compute_pixels (radius);

  if (side == TOP || side == BOTTOM)
    {
      blur_flags |= GSK_BLUR_Y;
      x1 = floor (box->bounds.origin.x - clip_radius);
      x2 = ceil (box->bounds.origin.x + box->bounds.size.width + clip_radius);
    }
  else if (side == LEFT)
    {
      x1 = floor (box->bounds.origin.x -clip_radius);
      x2 = ceil (box->bounds.origin.x + clip_radius);
    }
  else
    {
      x1 = floor (box->bounds.origin.x + box->bounds.size.width -clip_radius);
      x2 = ceil (box->bounds.origin.x + box->bounds.size.width + clip_radius);
    }

  if (side == LEFT || side == RIGHT)
    {
      blur_flags |= GSK_BLUR_X;
      y1 = floor (box->bounds.origin.y - clip_radius);
      y2 = ceil (box->bounds.origin.y + box->bounds.size.height + clip_radius);
    }
  else if (side == TOP)
    {
      y1 = floor (box->bounds.origin.y -clip_radius);
      y2 = ceil (box->bounds.origin.y + clip_radius);
    }
  else
    {
      y1 = floor (box->bounds.origin.y + box->bounds.size.height -clip_radius);
      y2 = ceil (box->bounds.origin.y + box->bounds.size.height + clip_radius);
    }

  drawn_rect->x = x1;
  drawn_rect->y = y1;
  drawn_rect->width = x2 - x1;
  drawn_rect->height = y2 - y1;

  cairo_rectangle (cr, x1, y1, x2 - x1, y2 - y1);
  cairo_clip (cr);
  draw_shadow (cr, ccs, inset, box, clip_box, radius, color, blur_flags);
}

static gboolean
needs_blur (double radius)
{
  /* The code doesn't actually do any blurring for radius 1, as it
   * ends up with box filter size 1 */
  if (radius <= 1.0)
    return FALSE;

  return TRUE;
}

static void
gsk_inset_shadow_node_draw (GskRenderNode *node,
                            cairo_t       *cr,
                            GdkColorState *ccs)
{
  GskInsetShadowNode *self = (GskInsetShadowNode *) node;
  GskRoundedRect box, clip_box;
  int clip_radius;
  graphene_rect_t clip_rect;
  double blur_radius;

  /* We don't need to draw invisible shadows */
  if (gdk_color_is_clear (&self->color))
    return;

  _graphene_rect_init_from_clip_extents (&clip_rect, cr);
  if (!gsk_rounded_rect_intersects_rect (&self->outline, &clip_rect))
    return;

  blur_radius = self->blur_radius / 2;

  clip_radius = gsk_cairo_blur_compute_pixels (blur_radius);

  cairo_save (cr);

  gsk_rounded_rect_path (&self->outline, cr);
  cairo_clip (cr);

  gsk_rounded_rect_init_copy (&box, &self->outline);
  gsk_rounded_rect_offset (&box, self->offset.x, self->offset.y);
  gsk_rounded_rect_shrink (&box, self->spread, self->spread, self->spread, self->spread);

  gsk_rounded_rect_init_copy (&clip_box, &self->outline);
  gsk_rounded_rect_shrink (&clip_box, -clip_radius, -clip_radius, -clip_radius, -clip_radius);

  if (!needs_blur (blur_radius))
    {
      draw_shadow (cr, ccs, TRUE, &box, &clip_box, blur_radius, &self->color, GSK_BLUR_NONE);
    }
  else
    {
      cairo_region_t *remaining;
      cairo_rectangle_int_t r;
      int i;

      /* For the blurred case we divide the rendering into 9 parts,
       * 4 of the corners, 4 for the horizonat/vertical lines and
       * one for the interior. We make the non-interior parts
       * large enough to fit the full radius of the blur, so that
       * the interior part can be drawn solidly.
       */

      /* In the inset case we want to paint the whole clip-box.
       * We could remove the part of "box" where the blur doesn't
       * reach, but computing that is a bit tricky since the
       * rounded corners are on the "inside" of it. */
      gsk_rect_to_cairo_grow (&clip_box.bounds, &r);
      remaining = cairo_region_create_rectangle (&r);

      /* First do the corners of box */
      for (i = 0; i < 4; i++)
        {
          cairo_save (cr);
          /* Always clip with remaining to ensure we never draw any area twice */
          gdk_cairo_region (cr, remaining);
          cairo_clip (cr);
          draw_shadow_corner (cr, ccs, TRUE, &box, &clip_box, blur_radius, &self->color, i, &r);
          cairo_restore (cr);

          /* We drew the region, remove it from remaining */
          cairo_region_subtract_rectangle (remaining, &r);
        }

      /* Then the sides */
      for (i = 0; i < 4; i++)
        {
          cairo_save (cr);
          /* Always clip with remaining to ensure we never draw any area twice */
          gdk_cairo_region (cr, remaining);
          cairo_clip (cr);
          draw_shadow_side (cr, ccs, TRUE, &box, &clip_box, blur_radius, &self->color, i, &r);
          cairo_restore (cr);

          /* We drew the region, remove it from remaining */
          cairo_region_subtract_rectangle (remaining, &r);
        }

      /* Then the rest, which needs no blurring */

      cairo_save (cr);
      gdk_cairo_region (cr, remaining);
      cairo_clip (cr);
      draw_shadow (cr, ccs, TRUE, &box, &clip_box, blur_radius, &self->color, GSK_BLUR_NONE);
      cairo_restore (cr);

      cairo_region_destroy (remaining);
    }

  cairo_restore (cr);
}

static void
gsk_inset_shadow_node_diff (GskRenderNode *node1,
                            GskRenderNode *node2,
                            GskDiffData   *data)
{
  GskInsetShadowNode *self1 = (GskInsetShadowNode *) node1;
  GskInsetShadowNode *self2 = (GskInsetShadowNode *) node2;

  if (gsk_rounded_rect_equal (&self1->outline, &self2->outline) &&
      gdk_color_equal (&self1->color, &self2->color) &&
      graphene_point_equal (&self1->offset, &self2->offset) &&
      self1->spread == self2->spread &&
      self1->blur_radius == self2->blur_radius)
    return;

  gsk_render_node_diff_impossible (node1, node2, data);
}

static void
gsk_inset_shadow_node_class_init (gpointer g_class,
                                  gpointer class_data)
{
  GskRenderNodeClass *node_class = g_class;

  node_class->node_type = GSK_INSET_SHADOW_NODE;

  node_class->finalize = gsk_inset_shadow_node_finalize;
  node_class->draw = gsk_inset_shadow_node_draw;
  node_class->diff = gsk_inset_shadow_node_diff;
}

/**
 * gsk_inset_shadow_node_new:
 * @outline: outline of the region containing the shadow
 * @color: color of the shadow
 * @dx: horizontal offset of shadow
 * @dy: vertical offset of shadow
 * @spread: how far the shadow spreads towards the inside
 * @blur_radius: how much blur to apply to the shadow
 *
 * Creates a `GskRenderNode` that will render an inset shadow
 * into the box given by @outline.
 *
 * Returns: (transfer full) (type GskInsetShadowNode): A new `GskRenderNode`
 */
GskRenderNode *
gsk_inset_shadow_node_new (const GskRoundedRect *outline,
                           const GdkRGBA        *color,
                           float                 dx,
                           float                 dy,
                           float                 spread,
                           float                 blur_radius)
{
  GdkColor color2;
  GskRenderNode *node;

  gdk_color_init_from_rgba (&color2, color);
  node = gsk_inset_shadow_node_new2 (outline,
                                     &color2,
                                     &GRAPHENE_POINT_INIT (dx, dy),
                                     spread, blur_radius);
  gdk_color_finish (&color2);

  return node;
}

/*< private >
 * gsk_inset_shadow_node_new2:
 * @outline: outline of the region containing the shadow
 * @color: color of the shadow
 * @offset: offset of shadow
 * @spread: how far the shadow spreads towards the inside
 * @blur_radius: how much blur to apply to the shadow
 *
 * Creates a `GskRenderNode` that will render an inset shadow
 * into the box given by @outline.
 *
 * Returns: (transfer full) (type GskInsetShadowNode): A new `GskRenderNode`
 */
GskRenderNode *
gsk_inset_shadow_node_new2 (const GskRoundedRect   *outline,
                            const GdkColor         *color,
                            const graphene_point_t *offset,
                            float                   spread,
                            float                   blur_radius)
{
  GskInsetShadowNode *self;
  GskRenderNode *node;

  g_return_val_if_fail (outline != NULL, NULL);
  g_return_val_if_fail (color != NULL, NULL);
  g_return_val_if_fail (offset != NULL, NULL);
  g_return_val_if_fail (blur_radius >= 0, NULL);

  self = gsk_render_node_alloc (GSK_INSET_SHADOW_NODE);
  node = (GskRenderNode *) self;
  node->offscreen_for_opacity = FALSE;
  node->preferred_depth = GDK_MEMORY_NONE;

  gsk_rounded_rect_init_copy (&self->outline, outline);
  gdk_color_init_copy (&self->color, color);
  self->offset = *offset;
  self->spread = spread;
  self->blur_radius = blur_radius;

  gsk_rect_init_from_rect (&node->bounds, &self->outline.bounds);

  return node;
}

/**
 * gsk_inset_shadow_node_get_outline:
 * @node: (type GskInsetShadowNode): a `GskRenderNode` for an inset shadow
 *
 * Retrieves the outline rectangle of the inset shadow.
 *
 * Returns: (transfer none): a rounded rectangle
 */
const GskRoundedRect *
gsk_inset_shadow_node_get_outline (const GskRenderNode *node)
{
  const GskInsetShadowNode *self = (const GskInsetShadowNode *) node;

  return &self->outline;
}

/**
 * gsk_inset_shadow_node_get_color:
 * @node: (type GskInsetShadowNode): a `GskRenderNode` for an inset shadow
 *
 * Retrieves the color of the inset shadow.
 *
 * The value returned by this function will not be correct
 * if the render node was created for a non-sRGB color.
 *
 * Returns: (transfer none): the color of the shadow
 */
const GdkRGBA *
gsk_inset_shadow_node_get_color (const GskRenderNode *node)
{
  const GskInsetShadowNode *self = (const GskInsetShadowNode *) node;

  /* NOTE: This is only correct for nodes with sRGB colors */
  return (const GdkRGBA *) &self->color.values;
}

/*< private >
 * gsk_inset_shadow_node_get_gdk_color:
 * @node: (type GskInsetShadowNode): a `GskRenderNode`
 *
 * Retrieves the color of the given @node.
 *
 * Returns: (transfer none): the color of the node
 */
const GdkColor *
gsk_inset_shadow_node_get_gdk_color (const GskRenderNode *node)
{
  const GskInsetShadowNode *self = (const GskInsetShadowNode *) node;

  return &self->color;
}

/**
 * gsk_inset_shadow_node_get_dx:
 * @node: (type GskInsetShadowNode): a `GskRenderNode` for an inset shadow
 *
 * Retrieves the horizontal offset of the inset shadow.
 *
 * Returns: an offset, in pixels
 */
float
gsk_inset_shadow_node_get_dx (const GskRenderNode *node)
{
  const GskInsetShadowNode *self = (const GskInsetShadowNode *) node;

  return self->offset.x;
}

/**
 * gsk_inset_shadow_node_get_dy:
 * @node: (type GskInsetShadowNode): a `GskRenderNode` for an inset shadow
 *
 * Retrieves the vertical offset of the inset shadow.
 *
 * Returns: an offset, in pixels
 */
float
gsk_inset_shadow_node_get_dy (const GskRenderNode *node)
{
  const GskInsetShadowNode *self = (const GskInsetShadowNode *) node;

  return self->offset.y;
}

/**
 * gsk_inset_shadow_node_get_offset:
 * @node: (type GskInsetShadowNode): a `GskRenderNode` for an inset shadow
 *
 * Retrieves the offset of the inset shadow.
 *
 * Returns: an offset, in pixels
 **/
const graphene_point_t *
gsk_inset_shadow_node_get_offset (const GskRenderNode *node)
{
  const GskInsetShadowNode *self = (const GskInsetShadowNode *) node;

  return &self->offset;
}

/**
 * gsk_inset_shadow_node_get_spread:
 * @node: (type GskInsetShadowNode): a `GskRenderNode` for an inset shadow
 *
 * Retrieves how much the shadow spreads inwards.
 *
 * Returns: the size of the shadow, in pixels
 */
float
gsk_inset_shadow_node_get_spread (const GskRenderNode *node)
{
  const GskInsetShadowNode *self = (const GskInsetShadowNode *) node;

  return self->spread;
}

/**
 * gsk_inset_shadow_node_get_blur_radius:
 * @node: (type GskInsetShadowNode): a `GskRenderNode` for an inset shadow
 *
 * Retrieves the blur radius to apply to the shadow.
 *
 * Returns: the blur radius, in pixels
 */
float
gsk_inset_shadow_node_get_blur_radius (const GskRenderNode *node)
{
  const GskInsetShadowNode *self = (const GskInsetShadowNode *) node;

  return self->blur_radius;
}

/* }}} */
/* {{{ GSK_OUTSET_SHADOW_NODE */

/**
 * GskOutsetShadowNode:
 *
 * A render node for an outset shadow.
 */
struct _GskOutsetShadowNode
{
  GskRenderNode render_node;

  GskRoundedRect outline;
  GdkColor color;
  graphene_point_t offset;
  float spread;
  float blur_radius;
};

static void
gsk_outset_shadow_node_finalize (GskRenderNode *node)
{
  GskInsetShadowNode *self = (GskInsetShadowNode *) node;
  GskRenderNodeClass *parent_class = g_type_class_peek (g_type_parent (GSK_TYPE_OUTSET_SHADOW_NODE));

  gdk_color_finish (&self->color);

  parent_class->finalize (node);
}

static void
gsk_outset_shadow_get_extents (GskOutsetShadowNode *self,
                               float               *top,
                               float               *right,
                               float               *bottom,
                               float               *left)
{
  float clip_radius;

  clip_radius = gsk_cairo_blur_compute_pixels (ceil (self->blur_radius / 2.0));
  *top = MAX (0, ceil (clip_radius + self->spread - self->offset.y));
  *right = MAX (0, ceil (clip_radius + self->spread + self->offset.x));
  *bottom = MAX (0, ceil (clip_radius + self->spread + self->offset.y));
  *left = MAX (0, ceil (clip_radius + self->spread - self->offset.x));
}

static void
gsk_outset_shadow_node_draw (GskRenderNode *node,
                             cairo_t       *cr,
                             GdkColorState *ccs)
{
  GskOutsetShadowNode *self = (GskOutsetShadowNode *) node;
  GskRoundedRect box, clip_box;
  int clip_radius;
  graphene_rect_t clip_rect;
  float top, right, bottom, left;
  double blur_radius;

  /* We don't need to draw invisible shadows */
  if (gdk_color_is_clear (&self->color))
    return;

  _graphene_rect_init_from_clip_extents (&clip_rect, cr);
  if (!gsk_rounded_rect_intersects_rect (&self->outline, &clip_rect))
    return;

  blur_radius = self->blur_radius / 2;

  clip_radius = gsk_cairo_blur_compute_pixels (blur_radius);

  cairo_save (cr);

  gsk_rounded_rect_init_copy (&clip_box, &self->outline);
  gsk_outset_shadow_get_extents (self, &top, &right, &bottom, &left);
  gsk_rounded_rect_shrink (&clip_box, -top, -right, -bottom, -left);

  cairo_set_fill_rule (cr, CAIRO_FILL_RULE_EVEN_ODD);
  gsk_rounded_rect_path (&self->outline, cr);
  gdk_cairo_rect (cr, &clip_box.bounds);

  cairo_clip (cr);

  gsk_rounded_rect_init_copy (&box, &self->outline);
  gsk_rounded_rect_offset (&box, self->offset.x, self->offset.y);
  gsk_rounded_rect_shrink (&box, -self->spread, -self->spread, -self->spread, -self->spread);

  if (!needs_blur (blur_radius))
    {
      draw_shadow (cr, ccs, FALSE, &box, &clip_box, blur_radius, &self->color, GSK_BLUR_NONE);
    }
  else
    {
      int i;
      cairo_region_t *remaining;
      cairo_rectangle_int_t r;

      /* For the blurred case we divide the rendering into 9 parts,
       * 4 of the corners, 4 for the horizonat/vertical lines and
       * one for the interior. We make the non-interior parts
       * large enough to fit the full radius of the blur, so that
       * the interior part can be drawn solidly.
       */

      /* In the outset case we want to paint the entire box, plus as far
       * as the radius reaches from it */
      r.x = floor (box.bounds.origin.x - clip_radius);
      r.y = floor (box.bounds.origin.y - clip_radius);
      r.width = ceil (box.bounds.origin.x + box.bounds.size.width + clip_radius) - r.x;
      r.height = ceil (box.bounds.origin.y + box.bounds.size.height + clip_radius) - r.y;

      remaining = cairo_region_create_rectangle (&r);

      /* First do the corners of box */
      for (i = 0; i < 4; i++)
        {
          cairo_save (cr);
          /* Always clip with remaining to ensure we never draw any area twice */
          gdk_cairo_region (cr, remaining);
          cairo_clip (cr);
          draw_shadow_corner (cr, ccs, FALSE, &box, &clip_box, blur_radius, &self->color, i, &r);
          cairo_restore (cr);

          /* We drew the region, remove it from remaining */
          cairo_region_subtract_rectangle (remaining, &r);
        }

      /* Then the sides */
      for (i = 0; i < 4; i++)
        {
          cairo_save (cr);
          /* Always clip with remaining to ensure we never draw any area twice */
          gdk_cairo_region (cr, remaining);
          cairo_clip (cr);
          draw_shadow_side (cr, ccs, FALSE, &box, &clip_box, blur_radius, &self->color, i, &r);
          cairo_restore (cr);

          /* We drew the region, remove it from remaining */
          cairo_region_subtract_rectangle (remaining, &r);
        }

      /* Then the rest, which needs no blurring */

      cairo_save (cr);
      gdk_cairo_region (cr, remaining);
      cairo_clip (cr);
      draw_shadow (cr, ccs, FALSE, &box, &clip_box, blur_radius, &self->color, GSK_BLUR_NONE);
      cairo_restore (cr);

      cairo_region_destroy (remaining);
    }

  cairo_restore (cr);
}

static void
gsk_outset_shadow_node_diff (GskRenderNode *node1,
                             GskRenderNode *node2,
                             GskDiffData   *data)
{
  GskOutsetShadowNode *self1 = (GskOutsetShadowNode *) node1;
  GskOutsetShadowNode *self2 = (GskOutsetShadowNode *) node2;

  if (gsk_rounded_rect_equal (&self1->outline, &self2->outline) &&
      gdk_color_equal (&self1->color, &self2->color) &&
      graphene_point_equal (&self1->offset, &self2->offset) &&
      self1->spread == self2->spread &&
      self1->blur_radius == self2->blur_radius)
    return;

  gsk_render_node_diff_impossible (node1, node2, data);
}

static void
gsk_outset_shadow_node_class_init (gpointer g_class,
                                   gpointer class_data)
{
  GskRenderNodeClass *node_class = g_class;

  node_class->node_type = GSK_OUTSET_SHADOW_NODE;

  node_class->finalize = gsk_outset_shadow_node_finalize;
  node_class->draw = gsk_outset_shadow_node_draw;
  node_class->diff = gsk_outset_shadow_node_diff;
}

/**
 * gsk_outset_shadow_node_new:
 * @outline: outline of the region surrounded by shadow
 * @color: color of the shadow
 * @dx: horizontal offset of shadow
 * @dy: vertical offset of shadow
 * @spread: how far the shadow spreads towards the inside
 * @blur_radius: how much blur to apply to the shadow
 *
 * Creates a `GskRenderNode` that will render an outset shadow
 * around the box given by @outline.
 *
 * Returns: (transfer full) (type GskOutsetShadowNode): A new `GskRenderNode`
 */
GskRenderNode *
gsk_outset_shadow_node_new (const GskRoundedRect *outline,
                            const GdkRGBA        *color,
                            float                 dx,
                            float                 dy,
                            float                 spread,
                            float                 blur_radius)
{
  GdkColor color2;
  GskRenderNode *node;

  gdk_color_init_from_rgba (&color2, color);
  node = gsk_outset_shadow_node_new2 (outline,
                                      &color2,
                                      &GRAPHENE_POINT_INIT (dx, dy),
                                      spread, blur_radius);
  gdk_color_finish (&color2);

  return node;
}

/*< private >
 * gsk_outset_shadow_node_new2:
 * @outline: outline of the region surrounded by shadow
 * @color: color of the shadow
 * @offset: offset of shadow
 * @spread: how far the shadow spreads towards the inside
 * @blur_radius: how much blur to apply to the shadow
 *
 * Creates a `GskRenderNode` that will render an outset shadow
 * around the box given by @outline.
 *
 * Returns: (transfer full) (type GskOutsetShadowNode): A new `GskRenderNode`
 */
GskRenderNode *
gsk_outset_shadow_node_new2 (const GskRoundedRect   *outline,
                             const GdkColor         *color,
                             const graphene_point_t *offset,
                             float                   spread,
                             float                   blur_radius)
{
  GskOutsetShadowNode *self;
  GskRenderNode *node;
  float top, right, bottom, left;

  g_return_val_if_fail (outline != NULL, NULL);
  g_return_val_if_fail (color != NULL, NULL);
  g_return_val_if_fail (blur_radius >= 0, NULL);

  self = gsk_render_node_alloc (GSK_OUTSET_SHADOW_NODE);
  node = (GskRenderNode *) self;
  node->offscreen_for_opacity = FALSE;
  node->preferred_depth = GDK_MEMORY_NONE;

  gsk_rounded_rect_init_copy (&self->outline, outline);
  gdk_color_init_copy (&self->color, color);
  self->offset = *offset;
  self->spread = spread;
  self->blur_radius = blur_radius;

  gsk_outset_shadow_get_extents (self, &top, &right, &bottom, &left);

  gsk_rect_init_from_rect (&node->bounds, &self->outline.bounds);
  node->bounds.origin.x -= left;
  node->bounds.origin.y -= top;
  node->bounds.size.width += left + right;
  node->bounds.size.height += top + bottom;

  return node;
}

/**
 * gsk_outset_shadow_node_get_outline:
 * @node: (type GskOutsetShadowNode): a `GskRenderNode` for an outset shadow
 *
 * Retrieves the outline rectangle of the outset shadow.
 *
 * Returns: (transfer none): a rounded rectangle
 */
const GskRoundedRect *
gsk_outset_shadow_node_get_outline (const GskRenderNode *node)
{
  const GskOutsetShadowNode *self = (const GskOutsetShadowNode *) node;

  return &self->outline;
}

/**
 * gsk_outset_shadow_node_get_color:
 * @node: (type GskOutsetShadowNode): a `GskRenderNode` for an outset shadow
 *
 * Retrieves the color of the outset shadow.
 *
 * The value returned by this function will not be correct
 * if the render node was created for a non-sRGB color.
 *
 * Returns: (transfer none): a color
 */
const GdkRGBA *
gsk_outset_shadow_node_get_color (const GskRenderNode *node)
{
  const GskOutsetShadowNode *self = (const GskOutsetShadowNode *) node;

  /* NOTE: This is only correct for nodes with sRGB colors */
  return (const GdkRGBA *) &self->color.values;
}

/*< private >
 * gsk_outset_shadow_node_get_gdk_color:
 * @node: (type GskOutsetShadowNode): a `GskRenderNode`
 *
 * Retrieves the color of the given @node.
 *
 * Returns: (transfer none): the color of the node
 */
const GdkColor *
gsk_outset_shadow_node_get_gdk_color (const GskRenderNode *node)
{
  const GskOutsetShadowNode *self = (const GskOutsetShadowNode *) node;

  return &self->color;
}

/**
 * gsk_outset_shadow_node_get_dx:
 * @node: (type GskOutsetShadowNode): a `GskRenderNode` for an outset shadow
 *
 * Retrieves the horizontal offset of the outset shadow.
 *
 * Returns: an offset, in pixels
 */
float
gsk_outset_shadow_node_get_dx (const GskRenderNode *node)
{
  const GskOutsetShadowNode *self = (const GskOutsetShadowNode *) node;

  return self->offset.x;
}

/**
 * gsk_outset_shadow_node_get_dy:
 * @node: (type GskOutsetShadowNode): a `GskRenderNode` for an outset shadow
 *
 * Retrieves the vertical offset of the outset shadow.
 *
 * Returns: an offset, in pixels
 */
float
gsk_outset_shadow_node_get_dy (const GskRenderNode *node)
{
  const GskOutsetShadowNode *self = (const GskOutsetShadowNode *) node;

  return self->offset.y;
}

/**
 * gsk_outset_shadow_node_get_offset:
 * @node: (type GskOutsetShadowNode): a `GskRenderNode` for an outset shadow
 *
 * Retrieves the offset of the outset shadow.
 *
 * Returns: an offset, in pixels
 **/
const graphene_point_t *
gsk_outset_shadow_node_get_offset (const GskRenderNode *node)
{
  const GskOutsetShadowNode *self = (const GskOutsetShadowNode *) node;

  return &self->offset;
}

/**
 * gsk_outset_shadow_node_get_spread:
 * @node: (type GskOutsetShadowNode): a `GskRenderNode` for an outset shadow
 *
 * Retrieves how much the shadow spreads outwards.
 *
 * Returns: the size of the shadow, in pixels
 */
float
gsk_outset_shadow_node_get_spread (const GskRenderNode *node)
{
  const GskOutsetShadowNode *self = (const GskOutsetShadowNode *) node;

  return self->spread;
}

/**
 * gsk_outset_shadow_node_get_blur_radius:
 * @node: (type GskOutsetShadowNode): a `GskRenderNode` for an outset shadow
 *
 * Retrieves the blur radius of the shadow.
 *
 * Returns: the blur radius, in pixels
 */
float
gsk_outset_shadow_node_get_blur_radius (const GskRenderNode *node)
{
  const GskOutsetShadowNode *self = (const GskOutsetShadowNode *) node;

  return self->blur_radius;
}

/* }}} */
/* {{{ GSK_CAIRO_NODE */

/**
 * GskCairoNode:
 *
 * A render node for a Cairo surface.
 */
struct _GskCairoNode
{
  GskRenderNode render_node;

  cairo_surface_t *surface;
};

static void
gsk_cairo_node_finalize (GskRenderNode *node)
{
  GskCairoNode *self = (GskCairoNode *) node;
  GskRenderNodeClass *parent_class = g_type_class_peek (g_type_parent (GSK_TYPE_CAIRO_NODE));

  if (self->surface)
    cairo_surface_destroy (self->surface);

  parent_class->finalize (node);
}

static void
gsk_cairo_node_draw (GskRenderNode *node,
                     cairo_t       *cr,
                     GdkColorState *ccs)
{
  GskCairoNode *self = (GskCairoNode *) node;

  if (self->surface == NULL)
    return;

  if (gdk_color_state_equal (ccs, GDK_COLOR_STATE_SRGB))
    {
      cairo_set_source_surface (cr, self->surface, 0, 0);
      cairo_paint (cr);
    }
  else
    {
      cairo_save (cr);
      gdk_cairo_rect (cr, &node->bounds);
      cairo_clip (cr);
      cairo_push_group (cr);

      cairo_set_source_surface (cr, self->surface, 0, 0);
      cairo_paint (cr);
      gdk_cairo_surface_convert_color_state (cairo_get_group_target (cr),
                                             GDK_COLOR_STATE_SRGB,
                                             ccs);
      cairo_pop_group_to_source (cr);
      cairo_paint (cr);
      cairo_restore (cr);
    }
}

static void
gsk_cairo_node_class_init (gpointer g_class,
                           gpointer class_data)
{
  GskRenderNodeClass *node_class = g_class;

  node_class->node_type = GSK_CAIRO_NODE;

  node_class->finalize = gsk_cairo_node_finalize;
  node_class->draw = gsk_cairo_node_draw;
}

/**
 * gsk_cairo_node_get_surface:
 * @node: (type GskCairoNode): a `GskRenderNode` for a Cairo surface
 *
 * Retrieves the Cairo surface used by the render node.
 *
 * Returns: (transfer none): a Cairo surface
 */
cairo_surface_t *
gsk_cairo_node_get_surface (GskRenderNode *node)
{
  GskCairoNode *self = (GskCairoNode *) node;

  g_return_val_if_fail (GSK_IS_RENDER_NODE_TYPE (node, GSK_CAIRO_NODE), NULL);

  return self->surface;
}

void
gsk_cairo_node_set_surface (GskRenderNode   *node,
                            cairo_surface_t *surface)
{
  GskCairoNode *self = (GskCairoNode *) node;

  g_return_if_fail (GSK_IS_RENDER_NODE_TYPE (node, GSK_CAIRO_NODE));
  g_return_if_fail (self->surface == NULL);

  self->surface = cairo_surface_reference (surface);
}

/**
 * gsk_cairo_node_new:
 * @bounds: the rectangle to render to
 *
 * Creates a `GskRenderNode` that will render a cairo surface
 * into the area given by @bounds.
 *
 * You can draw to the cairo surface using [method@Gsk.CairoNode.get_draw_context].
 *
 * Returns: (transfer full) (type GskCairoNode): A new `GskRenderNode`
 */
GskRenderNode *
gsk_cairo_node_new (const graphene_rect_t *bounds)
{
  GskCairoNode *self;
  GskRenderNode *node;

  g_return_val_if_fail (bounds != NULL, NULL);

  self = gsk_render_node_alloc (GSK_CAIRO_NODE);
  node = (GskRenderNode *) self;
  node->offscreen_for_opacity = FALSE;
  node->preferred_depth = gdk_color_state_get_depth (GDK_COLOR_STATE_SRGB);

  gsk_rect_init_from_rect (&node->bounds, bounds);
  gsk_rect_normalize (&node->bounds);

  return node;
}

/**
 * gsk_cairo_node_get_draw_context:
 * @node: (type GskCairoNode): a `GskRenderNode` for a Cairo surface
 *
 * Creates a Cairo context for drawing using the surface associated
 * to the render node.
 *
 * If no surface exists yet, a surface will be created optimized for
 * rendering to @renderer.
 *
 * Returns: (transfer full): a Cairo context used for drawing; use
 *   cairo_destroy() when done drawing
 */
cairo_t *
gsk_cairo_node_get_draw_context (GskRenderNode *node)
{
  GskCairoNode *self = (GskCairoNode *) node;
  int width, height;
  cairo_t *res;

  g_return_val_if_fail (GSK_IS_RENDER_NODE_TYPE (node, GSK_CAIRO_NODE), NULL);

  width = ceilf (node->bounds.size.width);
  height = ceilf (node->bounds.size.height);

  if (width <= 0 || height <= 0)
    {
      cairo_surface_t *surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, 0, 0);
      res = cairo_create (surface);
      cairo_surface_destroy (surface);
    }
  else if (self->surface == NULL)
    {
      self->surface = cairo_recording_surface_create (CAIRO_CONTENT_COLOR_ALPHA,
                                                      &(cairo_rectangle_t) {
                                                          node->bounds.origin.x,
                                                          node->bounds.origin.y,
                                                          node->bounds.size.width,
                                                          node->bounds.size.height
                                                      });
      res = cairo_create (self->surface);
    }
  else
    {
      res = cairo_create (self->surface);
    }

  gdk_cairo_rect (res, &node->bounds);
  cairo_clip (res);

  return res;
}

/* }}} */
/* {{{ GSK_CONTAINER_NODE */

/**
 * GskContainerNode:
 *
 * A render node that can contain other render nodes.
 */
struct _GskContainerNode
{
  GskRenderNode render_node;

  gboolean disjoint;
  graphene_rect_t opaque; /* Can be 0 0 0 0 to mean no opacity */
  guint n_children;
  GskRenderNode **children;
};

static void
gsk_container_node_finalize (GskRenderNode *node)
{
  GskContainerNode *container = (GskContainerNode *) node;
  GskRenderNodeClass *parent_class = g_type_class_peek (g_type_parent (GSK_TYPE_CONTAINER_NODE));

  for (guint i = 0; i < container->n_children; i++)
    gsk_render_node_unref (container->children[i]);

  g_free (container->children);

  parent_class->finalize (node);
}

static void
gsk_container_node_draw (GskRenderNode *node,
                         cairo_t       *cr,
                         GdkColorState *ccs)
{
  GskContainerNode *container = (GskContainerNode *) node;
  guint i;

  for (i = 0; i < container->n_children; i++)
    {
      gsk_render_node_draw_ccs (container->children[i], cr, ccs);
    }
}

static int
gsk_container_node_compare_func (gconstpointer elem1, gconstpointer elem2, gpointer data)
{
  return gsk_render_node_can_diff ((const GskRenderNode *) elem1, (const GskRenderNode *) elem2) ? 0 : 1;
}

static GskDiffResult
gsk_container_node_keep_func (gconstpointer elem1, gconstpointer elem2, gpointer user_data)
{
  GskDiffData *data = user_data;
  gsk_render_node_diff ((GskRenderNode *) elem1, (GskRenderNode *) elem2, data);
  if (cairo_region_num_rectangles (data->region) > MAX_RECTS_IN_DIFF)
    return GSK_DIFF_ABORTED;

  return GSK_DIFF_OK;
}

static GskDiffResult
gsk_container_node_change_func (gconstpointer elem, gsize idx, gpointer user_data)
{
  const GskRenderNode *node = elem;
  GskDiffData *data = user_data;
  cairo_rectangle_int_t rect;

  gsk_rect_to_cairo_grow (&node->bounds, &rect);
  cairo_region_union_rectangle (data->region, &rect);
  if (cairo_region_num_rectangles (data->region) > MAX_RECTS_IN_DIFF)
    return GSK_DIFF_ABORTED;

  return GSK_DIFF_OK;
}

static GskDiffSettings *
gsk_container_node_get_diff_settings (void)
{
  static GskDiffSettings *settings = NULL;

  if (G_LIKELY (settings))
    return settings;

  settings = gsk_diff_settings_new (gsk_container_node_compare_func,
                                    gsk_container_node_keep_func,
                                    gsk_container_node_change_func,
                                    gsk_container_node_change_func);
  gsk_diff_settings_set_allow_abort (settings, TRUE);

  return settings;
}

static gboolean
gsk_render_node_diff_multiple (GskRenderNode **nodes1,
                               gsize           n_nodes1,
                               GskRenderNode **nodes2,
                               gsize           n_nodes2,
                               GskDiffData    *data)
{
  return gsk_diff ((gconstpointer *) nodes1, n_nodes1,
                   (gconstpointer *) nodes2, n_nodes2,
                   gsk_container_node_get_diff_settings (),
                   data) == GSK_DIFF_OK;
}

void
gsk_container_node_diff_with (GskRenderNode *container,
                              GskRenderNode *other,
                              GskDiffData   *data)
{
  GskContainerNode *self = (GskContainerNode *) container;

  if (gsk_render_node_diff_multiple (self->children,
                                     self->n_children,
                                     &other,
                                     1,
                                     data))
    return;

  gsk_render_node_diff_impossible (container, other, data);
}

static void
gsk_container_node_diff (GskRenderNode *node1,
                         GskRenderNode *node2,
                         GskDiffData   *data)
{
  GskContainerNode *self1 = (GskContainerNode *) node1;
  GskContainerNode *self2 = (GskContainerNode *) node2;

  if (gsk_render_node_diff_multiple (self1->children,
                                     self1->n_children,
                                     self2->children,
                                     self2->n_children,
                                     data))
    return;

  gsk_render_node_diff_impossible (node1, node2, data);
}

static gboolean
gsk_container_node_get_opaque_rect (GskRenderNode   *node,
                                    graphene_rect_t *opaque)
{
  GskContainerNode *self = (GskContainerNode *) node;

  if (self->opaque.size.width <= 0 && self->opaque.size.height <= 0)
    return FALSE;

  *opaque = self->opaque;
  return TRUE;
}

static void
gsk_container_node_class_init (gpointer g_class,
                               gpointer class_data)
{
  GskRenderNodeClass *node_class = g_class;

  node_class->node_type = GSK_CONTAINER_NODE;

  node_class->finalize = gsk_container_node_finalize;
  node_class->draw = gsk_container_node_draw;
  node_class->diff = gsk_container_node_diff;
  node_class->get_opaque_rect = gsk_container_node_get_opaque_rect;
}

/**
 * gsk_container_node_new:
 * @children: (array length=n_children) (transfer none): The children of the node
 * @n_children: Number of children in the @children array
 *
 * Creates a new `GskRenderNode` instance for holding the given @children.
 *
 * The new node will acquire a reference to each of the children.
 *
 * Returns: (transfer full) (type GskContainerNode): the new `GskRenderNode`
 */
GskRenderNode *
gsk_container_node_new (GskRenderNode **children,
                        guint           n_children)
{
  GskContainerNode *self;
  GskRenderNode *node;

  self = gsk_render_node_alloc (GSK_CONTAINER_NODE);
  node = (GskRenderNode *) self;

  self->disjoint = TRUE;
  self->n_children = n_children;

  if (n_children == 0)
    {
      gsk_rect_init_from_rect (&node->bounds, graphene_rect_zero ());
      node->preferred_depth = GDK_MEMORY_NONE;
    }
  else
    {
      graphene_rect_t child_opaque;
      gboolean have_opaque;
      gboolean is_hdr;

      self->children = g_malloc_n (n_children, sizeof (GskRenderNode *));

      self->children[0] = gsk_render_node_ref (children[0]);
      node->offscreen_for_opacity = children[0]->offscreen_for_opacity;
      node->preferred_depth = children[0]->preferred_depth;
      gsk_rect_init_from_rect (&node->bounds, &(children[0]->bounds));
      have_opaque = gsk_render_node_get_opaque_rect (self->children[0], &self->opaque);
      is_hdr = gsk_render_node_is_hdr (self->children[0]);

      for (guint i = 1; i < n_children; i++)
        {
          self->children[i] = gsk_render_node_ref (children[i]);
          self->disjoint = self->disjoint && !gsk_rect_intersects (&node->bounds, &(children[i]->bounds));
          graphene_rect_union (&node->bounds, &(children[i]->bounds), &node->bounds);
          node->preferred_depth = gdk_memory_depth_merge (node->preferred_depth, children[i]->preferred_depth);
          node->offscreen_for_opacity = node->offscreen_for_opacity || children[i]->offscreen_for_opacity;
          if (gsk_render_node_get_opaque_rect (self->children[i], &child_opaque))
            {
              if (have_opaque)
                gsk_rect_coverage (&self->opaque, &child_opaque, &self->opaque);
              else
                {
                  self->opaque = child_opaque;
                  have_opaque = TRUE;
                }
            }

          is_hdr |= gsk_render_node_is_hdr (self->children[i]);
        }

      node->offscreen_for_opacity = node->offscreen_for_opacity || !self->disjoint;
      node->fully_opaque = have_opaque && graphene_rect_equal (&node->bounds, &self->opaque);
      node->is_hdr = is_hdr;
   }

  return node;
}

/**
 * gsk_container_node_get_n_children:
 * @node: (type GskContainerNode): a container `GskRenderNode`
 *
 * Retrieves the number of direct children of @node.
 *
 * Returns: the number of children of the `GskRenderNode`
 */
guint
gsk_container_node_get_n_children (const GskRenderNode *node)
{
  const GskContainerNode *self = (const GskContainerNode *) node;

  return self->n_children;
}

/**
 * gsk_container_node_get_child:
 * @node: (type GskContainerNode): a container `GskRenderNode`
 * @idx: the position of the child to get
 *
 * Gets one of the children of @container.
 *
 * Returns: (transfer none): the @idx'th child of @container
 */
GskRenderNode *
gsk_container_node_get_child (const GskRenderNode *node,
                              guint                idx)
{
  const GskContainerNode *self = (const GskContainerNode *) node;

  g_return_val_if_fail (GSK_IS_RENDER_NODE_TYPE (node, GSK_CONTAINER_NODE), NULL);
  g_return_val_if_fail (idx < self->n_children, NULL);

  return self->children[idx];
}

GskRenderNode **
gsk_container_node_get_children (const GskRenderNode *node,
                                 guint               *n_children)
{
  const GskContainerNode *self = (const GskContainerNode *) node;

  *n_children = self->n_children;

  return self->children;
}

/*< private>
 * gsk_container_node_is_disjoint:
 * @node: a container `GskRenderNode`
 *
 * Returns `TRUE` if it is known that the child nodes are not
 * overlapping. There is no guarantee that they do overlap
 * if this function return FALSE.
 *
 * Returns: `TRUE` if children don't overlap
 */
gboolean
gsk_container_node_is_disjoint (const GskRenderNode *node)
{
  const GskContainerNode *self = (const GskContainerNode *) node;

  return self->disjoint;
}

/* }}} */
/* {{{ GSK_TRANSFORM_NODE */

/**
 * GskTransformNode:
 *
 * A render node applying a `GskTransform` to its single child node.
 */
struct _GskTransformNode
{
  GskRenderNode render_node;

  GskRenderNode *child;
  GskTransform *transform;
  float dx, dy;
};

static void
gsk_transform_node_finalize (GskRenderNode *node)
{
  GskTransformNode *self = (GskTransformNode *) node;
  GskRenderNodeClass *parent_class = g_type_class_peek (g_type_parent (GSK_TYPE_TRANSFORM_NODE));

  gsk_render_node_unref (self->child);
  gsk_transform_unref (self->transform);

  parent_class->finalize (node);
}

static void
gsk_transform_node_draw (GskRenderNode *node,
                         cairo_t       *cr,
                         GdkColorState *ccs)
{
  GskTransformNode *self = (GskTransformNode *) node;
  float xx, yx, xy, yy, dx, dy;
  cairo_matrix_t ctm;

  if (gsk_transform_get_category (self->transform) < GSK_TRANSFORM_CATEGORY_2D)
    {
      GdkRGBA pink = { 255 / 255., 105 / 255., 180 / 255., 1.0 };
      gdk_cairo_set_source_rgba_ccs (cr, ccs, &pink);
      gdk_cairo_rect (cr, &node->bounds);
      cairo_fill (cr);
      return;
    }

  gsk_transform_to_2d (self->transform, &xx, &yx, &xy, &yy, &dx, &dy);
  cairo_matrix_init (&ctm, xx, yx, xy, yy, dx, dy);
  if (xx * yy == xy * yx)
    {
      /* broken matrix here. This can happen during transitions
       * (like when flipping an axis at the point where scale == 0)
       * and just means that nothing should be drawn.
       * But Cairo throws lots of ugly errors instead of silently
       * going on. So we silently go on.
       */
      return;
    }
  cairo_transform (cr, &ctm);

  gsk_render_node_draw_ccs (self->child, cr, ccs);
}

static gboolean
gsk_transform_node_can_diff (const GskRenderNode *node1,
                             const GskRenderNode *node2)
{
  GskTransformNode *self1 = (GskTransformNode *) node1;
  GskTransformNode *self2 = (GskTransformNode *) node2;

  if (!gsk_transform_equal (self1->transform, self2->transform))
    return FALSE;

  return gsk_render_node_can_diff (self1->child, self2->child);
}

static void
gsk_transform_node_diff (GskRenderNode *node1,
                         GskRenderNode *node2,
                         GskDiffData   *data)
{
  GskTransformNode *self1 = (GskTransformNode *) node1;
  GskTransformNode *self2 = (GskTransformNode *) node2;

  if (!gsk_transform_equal (self1->transform, self2->transform))
    {
      gsk_render_node_diff_impossible (node1, node2, data);
      return;
    }

  if (self1->child == self2->child)
    return;

  switch (gsk_transform_get_category (self1->transform))
    {
    case GSK_TRANSFORM_CATEGORY_IDENTITY:
      gsk_render_node_diff (self1->child, self2->child, data);
      break;

    case GSK_TRANSFORM_CATEGORY_2D_TRANSLATE:
      {
        cairo_region_t *sub;
        float dx, dy;
        gsk_transform_to_translate (self1->transform, &dx, &dy);
        sub = cairo_region_create ();
        gsk_render_node_diff (self1->child, self2->child, &(GskDiffData) { sub, data->surface });
        cairo_region_translate (sub, floorf (dx), floorf (dy));
        if (floorf (dx) != dx)
          {
            cairo_region_t *tmp = cairo_region_copy (sub);
            cairo_region_translate (tmp, 1, 0);
            cairo_region_union (sub, tmp);
            cairo_region_destroy (tmp);
          }
        if (floorf (dy) != dy)
          {
            cairo_region_t *tmp = cairo_region_copy (sub);
            cairo_region_translate (tmp, 0, 1);
            cairo_region_union (sub, tmp);
            cairo_region_destroy (tmp);
          }
        cairo_region_union (data->region, sub);
        cairo_region_destroy (sub);
      }
      break;

    case GSK_TRANSFORM_CATEGORY_2D_AFFINE:
      {
        cairo_region_t *sub;
        float scale_x, scale_y, dx, dy;
        gsk_transform_to_affine (self1->transform, &scale_x, &scale_y, &dx, &dy);
        sub = cairo_region_create ();
        gsk_render_node_diff (self1->child, self2->child, &(GskDiffData) { sub, data->surface });
        region_union_region_affine (data->region, sub, scale_x, scale_y, dx, dy);
        cairo_region_destroy (sub);
      }
      break;

    case GSK_TRANSFORM_CATEGORY_UNKNOWN:
    case GSK_TRANSFORM_CATEGORY_ANY:
    case GSK_TRANSFORM_CATEGORY_3D:
    case GSK_TRANSFORM_CATEGORY_2D:
    default:
      gsk_render_node_diff_impossible (node1, node2, data);
      break;
    }
}

static gboolean
gsk_transform_node_get_opaque_rect (GskRenderNode   *node,
                                    graphene_rect_t *opaque)
{
  GskTransformNode *self = (GskTransformNode *) node;
  graphene_rect_t child_opaque;

  if (gsk_transform_get_fine_category (self->transform) < GSK_FINE_TRANSFORM_CATEGORY_2D_DIHEDRAL)
    return FALSE;

  if (!gsk_render_node_get_opaque_rect (self->child, &child_opaque))
    return FALSE;

  gsk_transform_transform_bounds (self->transform,
                                  &child_opaque,
                                  opaque);
  return TRUE;
}

static void
gsk_transform_node_class_init (gpointer g_class,
                               gpointer class_data)
{
  GskRenderNodeClass *node_class = g_class;

  node_class->node_type = GSK_TRANSFORM_NODE;

  node_class->finalize = gsk_transform_node_finalize;
  node_class->draw = gsk_transform_node_draw;
  node_class->can_diff = gsk_transform_node_can_diff;
  node_class->diff = gsk_transform_node_diff;
  node_class->get_opaque_rect = gsk_transform_node_get_opaque_rect;
}

/**
 * gsk_transform_node_new:
 * @child: The node to transform
 * @transform: (transfer none): The transform to apply
 *
 * Creates a `GskRenderNode` that will transform the given @child
 * with the given @transform.
 *
 * Returns: (transfer full) (type GskTransformNode): A new `GskRenderNode`
 */
GskRenderNode *
gsk_transform_node_new (GskRenderNode *child,
                        GskTransform  *transform)
{
  GskTransformNode *self;
  GskRenderNode *node;
  GskTransformCategory category;

  g_return_val_if_fail (GSK_IS_RENDER_NODE (child), NULL);
  g_return_val_if_fail (transform != NULL, NULL);

  category = gsk_transform_get_category (transform);

  self = gsk_render_node_alloc (GSK_TRANSFORM_NODE);
  node = (GskRenderNode *) self;
  node->offscreen_for_opacity = child->offscreen_for_opacity;
  node->fully_opaque = child->fully_opaque && category >= GSK_TRANSFORM_CATEGORY_2D_AFFINE;

  self->child = gsk_render_node_ref (child);
  self->transform = gsk_transform_ref (transform);

  if (category >= GSK_TRANSFORM_CATEGORY_2D_TRANSLATE)
    gsk_transform_to_translate (transform, &self->dx, &self->dy);
  else
    self->dx = self->dy = 0;

  gsk_transform_transform_bounds (self->transform,
                                  &child->bounds,
                                  &node->bounds);

  node->preferred_depth = gsk_render_node_get_preferred_depth (child);
  node->is_hdr = gsk_render_node_is_hdr (child);

  return node;
}

/**
 * gsk_transform_node_get_child:
 * @node: (type GskTransformNode): a `GskRenderNode` for a transform
 *
 * Gets the child node that is getting transformed by the given @node.
 *
 * Returns: (transfer none): The child that is getting transformed
 */
GskRenderNode *
gsk_transform_node_get_child (const GskRenderNode *node)
{
  const GskTransformNode *self = (const GskTransformNode *) node;

  return self->child;
}

/**
 * gsk_transform_node_get_transform:
 * @node: (type GskTransformNode): a `GskRenderNode` for a transform
 *
 * Retrieves the `GskTransform` used by the @node.
 *
 * Returns: (transfer none): a `GskTransform`
 */
GskTransform *
gsk_transform_node_get_transform (const GskRenderNode *node)
{
  const GskTransformNode *self = (const GskTransformNode *) node;

  return self->transform;
}

void
gsk_transform_node_get_translate (const GskRenderNode *node,
                                  float               *dx,
                                  float               *dy)
{
  const GskTransformNode *self = (const GskTransformNode *) node;

  *dx = self->dx;
  *dy = self->dy;
}

/* }}} */
/* {{{ GSK_OPACITY_NODE */

/**
 * GskOpacityNode:
 *
 * A render node controlling the opacity of its single child node.
 */
struct _GskOpacityNode
{
  GskRenderNode render_node;

  GskRenderNode *child;
  float opacity;
};

static void
gsk_opacity_node_finalize (GskRenderNode *node)
{
  GskOpacityNode *self = (GskOpacityNode *) node;
  GskRenderNodeClass *parent_class = g_type_class_peek (g_type_parent (GSK_TYPE_OPACITY_NODE));

  gsk_render_node_unref (self->child);

  parent_class->finalize (node);
}

static void
gsk_opacity_node_draw (GskRenderNode *node,
                       cairo_t       *cr,
                       GdkColorState *ccs)
{
  GskOpacityNode *self = (GskOpacityNode *) node;

  /* clip so the push_group() creates a smaller surface */
  gsk_cairo_rectangle_pixel_aligned (cr, &node->bounds);
  cairo_clip (cr);

  if (has_empty_clip (cr))
    return;

  cairo_push_group (cr);

  gsk_render_node_draw_ccs (self->child, cr, ccs);

  cairo_pop_group_to_source (cr);
  cairo_paint_with_alpha (cr, self->opacity);
}

static void
gsk_opacity_node_diff (GskRenderNode *node1,
                       GskRenderNode *node2,
                       GskDiffData   *data)
{
  GskOpacityNode *self1 = (GskOpacityNode *) node1;
  GskOpacityNode *self2 = (GskOpacityNode *) node2;

  if (self1->opacity == self2->opacity)
    gsk_render_node_diff (self1->child, self2->child, data);
  else
    gsk_render_node_diff_impossible (node1, node2, data);
}

static void
gsk_opacity_node_class_init (gpointer g_class,
                             gpointer class_data)
{
  GskRenderNodeClass *node_class = g_class;

  node_class->node_type = GSK_OPACITY_NODE;

  node_class->finalize = gsk_opacity_node_finalize;
  node_class->draw = gsk_opacity_node_draw;
  node_class->diff = gsk_opacity_node_diff;
}

/**
 * gsk_opacity_node_new:
 * @child: The node to draw
 * @opacity: The opacity to apply
 *
 * Creates a `GskRenderNode` that will drawn the @child with reduced
 * @opacity.
 *
 * Returns: (transfer full) (type GskOpacityNode): A new `GskRenderNode`
 */
GskRenderNode *
gsk_opacity_node_new (GskRenderNode *child,
                      float          opacity)
{
  GskOpacityNode *self;
  GskRenderNode *node;

  g_return_val_if_fail (GSK_IS_RENDER_NODE (child), NULL);

  self = gsk_render_node_alloc (GSK_OPACITY_NODE);
  node = (GskRenderNode *) self;
  node->offscreen_for_opacity = child->offscreen_for_opacity;

  self->child = gsk_render_node_ref (child);
  self->opacity = CLAMP (opacity, 0.0, 1.0);

  gsk_rect_init_from_rect (&node->bounds, &child->bounds);

  node->preferred_depth = gsk_render_node_get_preferred_depth (child);
  node->is_hdr = gsk_render_node_is_hdr (child);

  return node;
}

/**
 * gsk_opacity_node_get_child:
 * @node: (type GskOpacityNode): a `GskRenderNode` for an opacity
 *
 * Gets the child node that is getting opacityed by the given @node.
 *
 * Returns: (transfer none): The child that is getting opacityed
 */
GskRenderNode *
gsk_opacity_node_get_child (const GskRenderNode *node)
{
  const GskOpacityNode *self = (const GskOpacityNode *) node;

  return self->child;
}

/**
 * gsk_opacity_node_get_opacity:
 * @node: (type GskOpacityNode): a `GskRenderNode` for an opacity
 *
 * Gets the transparency factor for an opacity node.
 *
 * Returns: the opacity factor
 */
float
gsk_opacity_node_get_opacity (const GskRenderNode *node)
{
  const GskOpacityNode *self = (const GskOpacityNode *) node;

  return self->opacity;
}

/* }}} */
/* {{{ GSK_COLOR_MATRIX_NODE */

/**
 * GskColorMatrixNode:
 *
 * A render node controlling the color matrix of its single child node.
 */
struct _GskColorMatrixNode
{
  GskRenderNode render_node;

  GskRenderNode *child;
  graphene_matrix_t color_matrix;
  graphene_vec4_t color_offset;
};

static void
gsk_color_matrix_node_finalize (GskRenderNode *node)
{
  GskColorMatrixNode *self = (GskColorMatrixNode *) node;
  GskRenderNodeClass *parent_class = g_type_class_peek (g_type_parent (GSK_TYPE_COLOR_MATRIX_NODE));

  gsk_render_node_unref (self->child);

  parent_class->finalize (node);
}

static void
apply_color_matrix_to_pattern (cairo_pattern_t         *pattern,
                               const graphene_matrix_t *color_matrix,
                               const graphene_vec4_t   *color_offset)
{
  cairo_surface_t *surface, *image_surface;
  guchar *data;
  gsize x, y, width, height, stride;
  float alpha;
  graphene_vec4_t pixel;
  guint32* pixel_data;

  cairo_pattern_get_surface (pattern, &surface);
  image_surface = cairo_surface_map_to_image (surface, NULL);

  data = cairo_image_surface_get_data (image_surface);
  width = cairo_image_surface_get_width (image_surface);
  height = cairo_image_surface_get_height (image_surface);
  stride = cairo_image_surface_get_stride (image_surface);

  for (y = 0; y < height; y++)
    {
      pixel_data = (guint32 *) data;
      for (x = 0; x < width; x++)
        {
          alpha = ((pixel_data[x] >> 24) & 0xFF) / 255.0;

          if (alpha == 0)
            {
              graphene_vec4_init (&pixel, 0.0, 0.0, 0.0, 0.0);
            }
          else
            {
              graphene_vec4_init (&pixel,
                                  ((pixel_data[x] >> 16) & 0xFF) / (255.0 * alpha),
                                  ((pixel_data[x] >>  8) & 0xFF) / (255.0 * alpha),
                                  ( pixel_data[x]        & 0xFF) / (255.0 * alpha),
                                  alpha);
              graphene_matrix_transform_vec4 (color_matrix, &pixel, &pixel);
            }

          graphene_vec4_add (&pixel, color_offset, &pixel);

          alpha = graphene_vec4_get_w (&pixel);

          if (alpha > 0.0)
            {
              alpha = MIN (alpha, 1.0);
              pixel_data[x] = (((guint32) roundf (alpha * 255)) << 24) |
                              (((guint32) roundf (CLAMP (graphene_vec4_get_x (&pixel), 0, 1) * alpha * 255)) << 16) |
                              (((guint32) roundf (CLAMP (graphene_vec4_get_y (&pixel), 0, 1) * alpha * 255)) <<  8) |
                               ((guint32) roundf (CLAMP (graphene_vec4_get_z (&pixel), 0, 1) * alpha * 255));
            }
          else
            {
              pixel_data[x] = 0;
            }
        }
      data += stride;
    }

  cairo_surface_mark_dirty (image_surface);
  cairo_surface_unmap_image (surface, image_surface);
  /* https://gitlab.freedesktop.org/cairo/cairo/-/merge_requests/487 */
  cairo_surface_mark_dirty (surface);
}

static void
gsk_color_matrix_node_draw (GskRenderNode *node,
                            cairo_t       *cr,
                            GdkColorState *ccs)
{
  GskColorMatrixNode *self = (GskColorMatrixNode *) node;
  cairo_pattern_t *pattern;

  /* clip so the push_group() creates a smaller surface */
  gdk_cairo_rect (cr, &node->bounds);
  cairo_clip (cr);

  if (has_empty_clip (cr))
    return;

  cairo_push_group (cr);

  gsk_render_node_draw_ccs (self->child, cr, ccs);

  pattern = cairo_pop_group (cr);
  apply_color_matrix_to_pattern (pattern, &self->color_matrix, &self->color_offset);

  cairo_set_source (cr, pattern);
  cairo_paint (cr);

  cairo_pattern_destroy (pattern);
}

static void
gsk_color_matrix_node_diff (GskRenderNode *node1,
                            GskRenderNode *node2,
                            GskDiffData   *data)
{
  GskColorMatrixNode *self1 = (GskColorMatrixNode *) node1;
  GskColorMatrixNode *self2 = (GskColorMatrixNode *) node2;

  if (!graphene_vec4_equal (&self1->color_offset, &self2->color_offset))
    goto nope;

  if (!graphene_matrix_equal_fast (&self1->color_matrix, &self2->color_matrix))
    goto nope;

  gsk_render_node_diff (self1->child, self2->child, data);
  return;

nope:
  gsk_render_node_diff_impossible (node1, node2, data);
  return;
}

static void
gsk_color_matrix_node_class_init (gpointer g_class,
                                  gpointer class_data)
{
  GskRenderNodeClass *node_class = g_class;

  node_class->node_type = GSK_COLOR_MATRIX_NODE;

  node_class->finalize = gsk_color_matrix_node_finalize;
  node_class->draw = gsk_color_matrix_node_draw;
  node_class->diff = gsk_color_matrix_node_diff;
}

/**
 * gsk_color_matrix_node_new:
 * @child: The node to draw
 * @color_matrix: The matrix to apply
 * @color_offset: Values to add to the color
 *
 * Creates a `GskRenderNode` that will drawn the @child with
 * @color_matrix.
 *
 * In particular, the node will transform colors by applying
 *
 *     pixel = transpose(color_matrix) * pixel + color_offset
 *
 * for every pixel. The transformation operates on unpremultiplied
 * colors, with color components ordered R, G, B, A.
 *
 * Returns: (transfer full) (type GskColorMatrixNode): A new `GskRenderNode`
 */
GskRenderNode *
gsk_color_matrix_node_new (GskRenderNode           *child,
                           const graphene_matrix_t *color_matrix,
                           const graphene_vec4_t   *color_offset)
{
  GskColorMatrixNode *self;
  GskRenderNode *node;

  g_return_val_if_fail (GSK_IS_RENDER_NODE (child), NULL);

  self = gsk_render_node_alloc (GSK_COLOR_MATRIX_NODE);
  node = (GskRenderNode *) self;
  node->offscreen_for_opacity = child->offscreen_for_opacity;

  self->child = gsk_render_node_ref (child);
  graphene_matrix_init_from_matrix (&self->color_matrix, color_matrix);
  graphene_vec4_init_from_vec4 (&self->color_offset, color_offset);

  gsk_rect_init_from_rect (&node->bounds, &child->bounds);

  node->preferred_depth = gsk_render_node_get_preferred_depth (child);
  node->is_hdr = gsk_render_node_is_hdr (child);

  return node;
}

/**
 * gsk_color_matrix_node_get_child:
 * @node: (type GskColorMatrixNode): a color matrix `GskRenderNode`
 *
 * Gets the child node that is getting its colors modified by the given @node.
 *
 * Returns: (transfer none): The child that is getting its colors modified
 **/
GskRenderNode *
gsk_color_matrix_node_get_child (const GskRenderNode *node)
{
  const GskColorMatrixNode *self = (const GskColorMatrixNode *) node;

  return self->child;
}

/**
 * gsk_color_matrix_node_get_color_matrix:
 * @node: (type GskColorMatrixNode): a color matrix `GskRenderNode`
 *
 * Retrieves the color matrix used by the @node.
 *
 * Returns: a 4x4 color matrix
 */
const graphene_matrix_t *
gsk_color_matrix_node_get_color_matrix (const GskRenderNode *node)
{
  const GskColorMatrixNode *self = (const GskColorMatrixNode *) node;

  return &self->color_matrix;
}

/**
 * gsk_color_matrix_node_get_color_offset:
 * @node: (type GskColorMatrixNode): a color matrix `GskRenderNode`
 *
 * Retrieves the color offset used by the @node.
 *
 * Returns: a color vector
 */
const graphene_vec4_t *
gsk_color_matrix_node_get_color_offset (const GskRenderNode *node)
{
  const GskColorMatrixNode *self = (const GskColorMatrixNode *) node;

  return &self->color_offset;
}

/* }}} */
/* {{{ GSK_REPEAT_NODE */

/**
 * GskRepeatNode:
 *
 * A render node repeating its single child node.
 */
struct _GskRepeatNode
{
  GskRenderNode render_node;

  GskRenderNode *child;
  graphene_rect_t child_bounds;
};

static void
gsk_repeat_node_finalize (GskRenderNode *node)
{
  GskRepeatNode *self = (GskRepeatNode *) node;
  GskRenderNodeClass *parent_class = g_type_class_peek (g_type_parent (GSK_TYPE_REPEAT_NODE));

  gsk_render_node_unref (self->child);

  parent_class->finalize (node);
}

static void
gsk_repeat_node_draw_tiled (cairo_t                *cr,
                            GdkColorState          *ccs,
                            const graphene_rect_t  *rect,
                            float                   x,
                            float                   y,
                            GskRenderNode          *child,
                            const graphene_rect_t  *child_bounds)
{
  cairo_pattern_t *pattern;
  cairo_matrix_t matrix;

  cairo_save (cr);
  /* reset the clip so we get an unclipped pattern for repeating */
  cairo_reset_clip (cr);
  cairo_translate (cr,
                   x * child_bounds->size.width,
                   y * child_bounds->size.height);
  gdk_cairo_rect (cr, child_bounds);
  cairo_clip (cr);

  cairo_push_group (cr);
  gsk_render_node_draw_ccs (child, cr, ccs);
  pattern = cairo_pop_group (cr);
  cairo_restore (cr);

  cairo_pattern_set_extend (pattern, CAIRO_EXTEND_REPEAT);
  cairo_pattern_get_matrix (pattern, &matrix);
  cairo_matrix_translate (&matrix,
                          - x * child_bounds->size.width,
                          - y * child_bounds->size.height);
  cairo_pattern_set_matrix (pattern, &matrix);
  cairo_set_source (cr, pattern);
  cairo_pattern_destroy (pattern);

  gdk_cairo_rect (cr, rect);
  cairo_fill (cr);
}

static void
gsk_repeat_node_draw (GskRenderNode *node,
                      cairo_t       *cr,
                      GdkColorState *ccs)
{
  GskRepeatNode *self = (GskRepeatNode *) node;
  graphene_rect_t clip_bounds;
  float tile_left, tile_right, tile_top, tile_bottom;

  gsk_cairo_rectangle_pixel_aligned (cr, &node->bounds);
  cairo_clip (cr);
  _graphene_rect_init_from_clip_extents (&clip_bounds, cr);

  tile_left = (clip_bounds.origin.x - self->child_bounds.origin.x) / self->child_bounds.size.width;
  tile_right = (clip_bounds.origin.x + clip_bounds.size.width - self->child_bounds.origin.x) / self->child_bounds.size.width;
  tile_top = (clip_bounds.origin.y - self->child_bounds.origin.y) / self->child_bounds.size.height;
  tile_bottom = (clip_bounds.origin.y + clip_bounds.size.height - self->child_bounds.origin.y) / self->child_bounds.size.height;

  /* the 1st check tests that a tile fully fits into the bounds,
   * the 2nd check is to catch the case where it fits exactly */
  if (ceilf (tile_left) < floorf (tile_right) &&
      clip_bounds.size.width > self->child_bounds.size.width)
    {
      if (ceilf (tile_top) < floorf (tile_bottom) &&
          clip_bounds.size.height > self->child_bounds.size.height)
        {
          /* tile in both directions */
          gsk_repeat_node_draw_tiled (cr,
                                      ccs,
                                      &clip_bounds,
                                      ceilf (tile_left),
                                      ceilf (tile_top),
                                      self->child,
                                      &self->child_bounds);
        }
      else
        {
          /* tile horizontally, repeat vertically */
          float y;
          for (y = floorf (tile_top); y < ceilf (tile_bottom); y++)
            {
              float start_y = MAX (clip_bounds.origin.y,
                                   self->child_bounds.origin.y + y * self->child_bounds.size.height);
              float end_y = MAX (clip_bounds.origin.y + clip_bounds.size.height,
                                 self->child_bounds.origin.y + (y + 1) * self->child_bounds.size.height);
              gsk_repeat_node_draw_tiled (cr,
                                          ccs,
                                          &GRAPHENE_RECT_INIT (
                                              clip_bounds.origin.x,
                                              start_y,
                                              clip_bounds.size.width,
                                              end_y - start_y
                                          ),
                                          ceilf (tile_left),
                                          y,
                                          self->child,
                                          &self->child_bounds);
            }
        }
    }
  else if (ceilf (tile_top) < floorf (tile_bottom) &&
           clip_bounds.size.height > self->child_bounds.size.height)
    {
      /* repeat horizontally, tile vertically */
      float x;
      for (x = floorf (tile_left); x < ceilf (tile_right); x++)
        {
          float start_x = MAX (clip_bounds.origin.x,
                               self->child_bounds.origin.x + x * self->child_bounds.size.width);
          float end_x = MAX (clip_bounds.origin.x + clip_bounds.size.width,
                             self->child_bounds.origin.x + (x + 1) * self->child_bounds.size.width);
          gsk_repeat_node_draw_tiled (cr,
                                      ccs,
                                      &GRAPHENE_RECT_INIT (
                                          start_x,
                                          clip_bounds.origin.y,
                                          end_x - start_x,
                                          clip_bounds.size.height
                                      ),
                                      x,
                                      ceilf (tile_top),
                                      self->child,
                                      &self->child_bounds);
        }
    }
  else
    {
      /* repeat in both directions */
      float x, y;

      for (x = floorf (tile_left); x < ceilf (tile_right); x++)
        {
          for (y = floorf (tile_top); y < ceilf (tile_bottom); y++)
            {
              cairo_save (cr);
              cairo_translate (cr,
                               x * self->child_bounds.size.width,
                               y * self->child_bounds.size.height);
              gdk_cairo_rect (cr, &self->child_bounds);
              cairo_clip (cr);
              gsk_render_node_draw_ccs (self->child, cr, ccs);
              cairo_restore (cr);
            }
        }
    }
}

static void
gsk_repeat_node_diff (GskRenderNode *node1,
                      GskRenderNode *node2,
                      GskDiffData   *data)
{
  GskRepeatNode *self1 = (GskRepeatNode *) node1;
  GskRepeatNode *self2 = (GskRepeatNode *) node2;

  if (gsk_rect_equal (&node1->bounds, &node2->bounds) &&
      gsk_rect_equal (&self1->child_bounds, &self2->child_bounds))
    {
      cairo_region_t *sub;

      sub = cairo_region_create();
      gsk_render_node_diff (self1->child, self2->child, &(GskDiffData) { sub, data->surface });
      if (cairo_region_is_empty (sub))
        {
          cairo_region_destroy (sub);
          return;
        }
      cairo_region_destroy (sub);
    }

  gsk_render_node_diff_impossible (node1, node2, data);
}

static void
gsk_repeat_node_class_init (gpointer g_class,
                            gpointer class_data)
{
  GskRenderNodeClass *node_class = g_class;

  node_class->node_type = GSK_REPEAT_NODE;

  node_class->finalize = gsk_repeat_node_finalize;
  node_class->draw = gsk_repeat_node_draw;
  node_class->diff = gsk_repeat_node_diff;
}

/**
 * gsk_repeat_node_new:
 * @bounds: The bounds of the area to be painted
 * @child: The child to repeat
 * @child_bounds: (nullable): The area of the child to repeat or %NULL to
 *     use the child's bounds
 *
 * Creates a `GskRenderNode` that will repeat the drawing of @child across
 * the given @bounds.
 *
 * Returns: (transfer full) (type GskRepeatNode): A new `GskRenderNode`
 */
GskRenderNode *
gsk_repeat_node_new (const graphene_rect_t *bounds,
                     GskRenderNode         *child,
                     const graphene_rect_t *child_bounds)
{
  GskRepeatNode *self;
  GskRenderNode *node;

  g_return_val_if_fail (bounds != NULL, NULL);
  g_return_val_if_fail (GSK_IS_RENDER_NODE (child), NULL);

  self = gsk_render_node_alloc (GSK_REPEAT_NODE);
  node = (GskRenderNode *) self;
  node->offscreen_for_opacity = TRUE;

  gsk_rect_init_from_rect (&node->bounds, bounds);
  gsk_rect_normalize (&node->bounds);

  self->child = gsk_render_node_ref (child);

  if (child_bounds)
    {
      gsk_rect_init_from_rect (&self->child_bounds, child_bounds);
      gsk_rect_normalize (&self->child_bounds);
    }
  else
    {
      gsk_rect_init_from_rect (&self->child_bounds, &child->bounds);
    }

  node->preferred_depth = gsk_render_node_get_preferred_depth (child);
  node->is_hdr = gsk_render_node_is_hdr (child);
  node->fully_opaque = child->fully_opaque &&
                       gsk_rect_contains_rect (&child->bounds, &self->child_bounds) &&
                       !gsk_rect_is_empty (&self->child_bounds);

  return node;
}

/**
 * gsk_repeat_node_get_child:
 * @node: (type GskRepeatNode): a repeat `GskRenderNode`
 *
 * Retrieves the child of @node.
 *
 * Returns: (transfer none): a `GskRenderNode`
 */
GskRenderNode *
gsk_repeat_node_get_child (const GskRenderNode *node)
{
  const GskRepeatNode *self = (const GskRepeatNode *) node;

  return self->child;
}

/**
 * gsk_repeat_node_get_child_bounds:
 * @node: (type GskRepeatNode): a repeat `GskRenderNode`
 *
 * Retrieves the bounding rectangle of the child of @node.
 *
 * Returns: (transfer none): a bounding rectangle
 */
const graphene_rect_t *
gsk_repeat_node_get_child_bounds (const GskRenderNode *node)
{
  const GskRepeatNode *self = (const GskRepeatNode *) node;

  return &self->child_bounds;
}

/* }}} */
/* {{{ GSK_CLIP_NODE */

/**
 * GskClipNode:
 *
 * A render node applying a rectangular clip to its single child node.
 */
struct _GskClipNode
{
  GskRenderNode render_node;

  GskRenderNode *child;
  graphene_rect_t clip;
};

static void
gsk_clip_node_finalize (GskRenderNode *node)
{
  GskClipNode *self = (GskClipNode *) node;
  GskRenderNodeClass *parent_class = g_type_class_peek (g_type_parent (GSK_TYPE_CLIP_NODE));

  gsk_render_node_unref (self->child);

  parent_class->finalize (node);
}

static void
gsk_clip_node_draw (GskRenderNode *node,
                    cairo_t       *cr,
                    GdkColorState *ccs)
{
  GskClipNode *self = (GskClipNode *) node;

  cairo_save (cr);

  gdk_cairo_rect (cr, &self->clip);
  cairo_clip (cr);

  gsk_render_node_draw_ccs (self->child, cr, ccs);

  cairo_restore (cr);
}

static void
gsk_clip_node_diff (GskRenderNode *node1,
                    GskRenderNode *node2,
                    GskDiffData   *data)
{
  GskClipNode *self1 = (GskClipNode *) node1;
  GskClipNode *self2 = (GskClipNode *) node2;

  if (gsk_rect_equal (&self1->clip, &self2->clip))
    {
      cairo_region_t *sub;
      cairo_rectangle_int_t clip_rect;

      sub = cairo_region_create();
      gsk_render_node_diff (self1->child, self2->child, &(GskDiffData) { sub, data->surface });
      gsk_rect_to_cairo_grow (&self1->clip, &clip_rect);
      cairo_region_intersect_rectangle (sub, &clip_rect);
      cairo_region_union (data->region, sub);
      cairo_region_destroy (sub);
    }
  else
    {
      gsk_render_node_diff_impossible (node1, node2, data);
    }
}
 
static gboolean
gsk_clip_node_get_opaque_rect (GskRenderNode   *node,
                               graphene_rect_t *opaque)
{
  GskClipNode *self = (GskClipNode *) node;
  graphene_rect_t child_opaque;

  if (!gsk_render_node_get_opaque_rect (self->child, &child_opaque))
    return FALSE;

  return graphene_rect_intersection (&self->clip, &child_opaque, opaque);
}

static void
gsk_clip_node_class_init (gpointer g_class,
                          gpointer class_data)
{
  GskRenderNodeClass *node_class = g_class;

  node_class->node_type = GSK_CLIP_NODE;

  node_class->finalize = gsk_clip_node_finalize;
  node_class->draw = gsk_clip_node_draw;
  node_class->diff = gsk_clip_node_diff;
  node_class->get_opaque_rect = gsk_clip_node_get_opaque_rect;
}

/**
 * gsk_clip_node_new:
 * @child: The node to draw
 * @clip: The clip to apply
 *
 * Creates a `GskRenderNode` that will clip the @child to the area
 * given by @clip.
 *
 * Returns: (transfer full) (type GskClipNode): A new `GskRenderNode`
 */
GskRenderNode *
gsk_clip_node_new (GskRenderNode         *child,
                   const graphene_rect_t *clip)
{
  GskClipNode *self;
  GskRenderNode *node;

  g_return_val_if_fail (GSK_IS_RENDER_NODE (child), NULL);
  g_return_val_if_fail (clip != NULL, NULL);

  self = gsk_render_node_alloc (GSK_CLIP_NODE);
  node = (GskRenderNode *) self;
  node->offscreen_for_opacity = child->offscreen_for_opacity;
  /* because of the intersection when computing bounds */
  node->fully_opaque = child->fully_opaque;

  self->child = gsk_render_node_ref (child);
  gsk_rect_init_from_rect (&self->clip, clip);
  gsk_rect_normalize (&self->clip);

  gsk_rect_intersection (&self->clip, &child->bounds, &node->bounds);

  node->preferred_depth = gsk_render_node_get_preferred_depth (child);
  node->is_hdr = gsk_render_node_is_hdr (child);

  return node;
}

/**
 * gsk_clip_node_get_child:
 * @node: (type GskClipNode): a clip @GskRenderNode
 *
 * Gets the child node that is getting clipped by the given @node.
 *
 * Returns: (transfer none): The child that is getting clipped
 **/
GskRenderNode *
gsk_clip_node_get_child (const GskRenderNode *node)
{
  const GskClipNode *self = (const GskClipNode *) node;

  return self->child;
}

/**
 * gsk_clip_node_get_clip:
 * @node: (type GskClipNode): a `GskClipNode`
 *
 * Retrieves the clip rectangle for @node.
 *
 * Returns: a clip rectangle
 */
const graphene_rect_t *
gsk_clip_node_get_clip (const GskRenderNode *node)
{
  const GskClipNode *self = (const GskClipNode *) node;

  return &self->clip;
}

/* }}} */
/* {{{ GSK_ROUNDED_CLIP_NODE */

/**
 * GskRoundedClipNode:
 *
 * A render node applying a rounded rectangle clip to its single child.
 */
struct _GskRoundedClipNode
{
  GskRenderNode render_node;

  GskRenderNode *child;
  GskRoundedRect clip;
};

static void
gsk_rounded_clip_node_finalize (GskRenderNode *node)
{
  GskRoundedClipNode *self = (GskRoundedClipNode *) node;
  GskRenderNodeClass *parent_class = g_type_class_peek (g_type_parent (GSK_TYPE_ROUNDED_CLIP_NODE));

  gsk_render_node_unref (self->child);

  parent_class->finalize (node);
}

static void
gsk_rounded_clip_node_draw (GskRenderNode *node,
                            cairo_t       *cr,
                            GdkColorState *ccs)
{
  GskRoundedClipNode *self = (GskRoundedClipNode *) node;

  cairo_save (cr);

  gsk_rounded_rect_path (&self->clip, cr);
  cairo_clip (cr);

  gsk_render_node_draw_ccs (self->child, cr, ccs);

  cairo_restore (cr);
}

static void
gsk_rounded_clip_node_diff (GskRenderNode *node1,
                            GskRenderNode *node2,
                            GskDiffData   *data)
{
  GskRoundedClipNode *self1 = (GskRoundedClipNode *) node1;
  GskRoundedClipNode *self2 = (GskRoundedClipNode *) node2;

  if (gsk_rounded_rect_equal (&self1->clip, &self2->clip))
    {
      cairo_region_t *sub;
      cairo_rectangle_int_t clip_rect;

      sub = cairo_region_create();
      gsk_render_node_diff (self1->child, self2->child, &(GskDiffData) { sub, data->surface });
      gsk_rect_to_cairo_grow (&self1->clip.bounds, &clip_rect);
      cairo_region_intersect_rectangle (sub, &clip_rect);
      cairo_region_union (data->region, sub);
      cairo_region_destroy (sub);
    }
  else
    {
      gsk_render_node_diff_impossible (node1, node2, data);
    }
}

static gboolean
gsk_rounded_clip_node_get_opaque_rect (GskRenderNode   *node,
                                       graphene_rect_t *opaque)
{
  GskRoundedClipNode *self = (GskRoundedClipNode *) node;
  graphene_rect_t child_opaque, wide_opaque, high_opaque;
  double start, end;

  if (!gsk_render_node_get_opaque_rect (self->child, &child_opaque))
    return FALSE;

  wide_opaque = self->clip.bounds;
  start = MAX(self->clip.corner[GSK_CORNER_TOP_LEFT].height, self->clip.corner[GSK_CORNER_TOP_RIGHT].height);
  end = MAX(self->clip.corner[GSK_CORNER_BOTTOM_LEFT].height, self->clip.corner[GSK_CORNER_BOTTOM_RIGHT].height);
  wide_opaque.size.height -= MIN (wide_opaque.size.height, start + end);
  wide_opaque.origin.y += start;
  graphene_rect_intersection (&wide_opaque, &child_opaque, &wide_opaque);

  high_opaque = self->clip.bounds;
  start = MAX(self->clip.corner[GSK_CORNER_TOP_LEFT].width, self->clip.corner[GSK_CORNER_BOTTOM_LEFT].width);
  end = MAX(self->clip.corner[GSK_CORNER_TOP_RIGHT].width, self->clip.corner[GSK_CORNER_BOTTOM_RIGHT].width);
  high_opaque.size.width -= MIN (high_opaque.size.width, start + end);
  high_opaque.origin.x += start;
  graphene_rect_intersection (&high_opaque, &child_opaque, &high_opaque);

  if (wide_opaque.size.width * wide_opaque.size.height > high_opaque.size.width * high_opaque.size.height)
    *opaque = wide_opaque;
  else
    *opaque = high_opaque;

  return TRUE;
}

static void
gsk_rounded_clip_node_class_init (gpointer g_class,
                                  gpointer class_data)
{
  GskRenderNodeClass *node_class = g_class;

  node_class->node_type = GSK_ROUNDED_CLIP_NODE;

  node_class->finalize = gsk_rounded_clip_node_finalize;
  node_class->draw = gsk_rounded_clip_node_draw;
  node_class->diff = gsk_rounded_clip_node_diff;
  node_class->get_opaque_rect = gsk_rounded_clip_node_get_opaque_rect;
}

/**
 * gsk_rounded_clip_node_new:
 * @child: The node to draw
 * @clip: The clip to apply
 *
 * Creates a `GskRenderNode` that will clip the @child to the area
 * given by @clip.
 *
 * Returns: (transfer full) (type GskRoundedClipNode): A new `GskRenderNode`
 */
GskRenderNode *
gsk_rounded_clip_node_new (GskRenderNode         *child,
                           const GskRoundedRect  *clip)
{
  GskRoundedClipNode *self;
  GskRenderNode *node;

  g_return_val_if_fail (GSK_IS_RENDER_NODE (child), NULL);
  g_return_val_if_fail (clip != NULL, NULL);

  self = gsk_render_node_alloc (GSK_ROUNDED_CLIP_NODE);
  node = (GskRenderNode *) self;
  node->offscreen_for_opacity = child->offscreen_for_opacity;

  self->child = gsk_render_node_ref (child);
  gsk_rounded_rect_init_copy (&self->clip, clip);

  gsk_rect_intersection (&self->clip.bounds, &child->bounds, &node->bounds);

  node->preferred_depth = gsk_render_node_get_preferred_depth (child);
  node->is_hdr = gsk_render_node_is_hdr (child);

  return node;
}

/**
 * gsk_rounded_clip_node_get_child:
 * @node: (type GskRoundedClipNode): a rounded clip `GskRenderNode`
 *
 * Gets the child node that is getting clipped by the given @node.
 *
 * Returns: (transfer none): The child that is getting clipped
 **/
GskRenderNode *
gsk_rounded_clip_node_get_child (const GskRenderNode *node)
{
  const GskRoundedClipNode *self = (const GskRoundedClipNode *) node;

  return self->child;
}

/**
 * gsk_rounded_clip_node_get_clip:
 * @node: (type GskRoundedClipNode): a rounded clip `GskRenderNode`
 *
 * Retrieves the rounded rectangle used to clip the contents of the @node.
 *
 * Returns: (transfer none): a rounded rectangle
 */
const GskRoundedRect *
gsk_rounded_clip_node_get_clip (const GskRenderNode *node)
{
  const GskRoundedClipNode *self = (const GskRoundedClipNode *) node;

  return &self->clip;
}

/* }}} */
/* {{{ GSK_FILL_NODE */

/**
 * GskFillNode:
 *
 * A render node filling the area given by [struct@Gsk.Path]
 * and [enum@Gsk.FillRule] with the child node.
 *
 * Since: 4.14
 */

struct _GskFillNode
{
  GskRenderNode render_node;

  GskRenderNode *child;
  GskPath *path;
  GskFillRule fill_rule;
};

static void
gsk_fill_node_finalize (GskRenderNode *node)
{
  GskFillNode *self = (GskFillNode *) node;
  GskRenderNodeClass *parent_class = g_type_class_peek (g_type_parent (GSK_TYPE_FILL_NODE));

  gsk_render_node_unref (self->child);
  gsk_path_unref (self->path);

  parent_class->finalize (node);
}

static void
gsk_fill_node_draw (GskRenderNode *node,
                    cairo_t       *cr,
                    GdkColorState *ccs)
{
  GskFillNode *self = (GskFillNode *) node;

  switch (self->fill_rule)
  {
    case GSK_FILL_RULE_WINDING:
      cairo_set_fill_rule (cr, CAIRO_FILL_RULE_WINDING);
      break;
    case GSK_FILL_RULE_EVEN_ODD:
      cairo_set_fill_rule (cr, CAIRO_FILL_RULE_EVEN_ODD);
      break;
    default:
      g_assert_not_reached ();
      break;
  }
  gsk_path_to_cairo (self->path, cr);
  if (gsk_render_node_get_node_type (self->child) == GSK_COLOR_NODE &&
      gsk_rect_contains_rect (&self->child->bounds, &node->bounds))
    {
      gdk_cairo_set_source_rgba_ccs (cr, ccs, gsk_color_node_get_color (self->child));
      cairo_fill (cr);
    }
  else
    {
      cairo_clip (cr);
      gsk_render_node_draw_ccs (self->child, cr, ccs);
    }
}

static void
gsk_fill_node_diff (GskRenderNode *node1,
                    GskRenderNode *node2,
                    GskDiffData   *data)
{
  GskFillNode *self1 = (GskFillNode *) node1;
  GskFillNode *self2 = (GskFillNode *) node2;

  if (self1->path == self2->path)
    {
      cairo_region_t *sub;
      cairo_rectangle_int_t clip_rect;

      sub = cairo_region_create();
      gsk_render_node_diff (self1->child, self2->child, &(GskDiffData) { sub, data->surface });
      gsk_rect_to_cairo_grow (&node1->bounds, &clip_rect);
      cairo_region_intersect_rectangle (sub, &clip_rect);
      cairo_region_union (data->region, sub);
      cairo_region_destroy (sub);
    }
  else
    {
      gsk_render_node_diff_impossible (node1, node2, data);
    }
}

static void
gsk_fill_node_class_init (gpointer g_class,
                          gpointer class_data)
{
  GskRenderNodeClass *node_class = g_class;

  node_class->node_type = GSK_FILL_NODE;

  node_class->finalize = gsk_fill_node_finalize;
  node_class->draw = gsk_fill_node_draw;
  node_class->diff = gsk_fill_node_diff;
}

/**
 * gsk_fill_node_new:
 * @child: The node to fill the area with
 * @path: The path describing the area to fill
 * @fill_rule: The fill rule to use
 *
 * Creates a `GskRenderNode` that will fill the @child in the area
 * given by @path and @fill_rule.
 *
 * Returns: (transfer none) (type GskFillNode): A new `GskRenderNode`
 *
 * Since: 4.14
 */
GskRenderNode *
gsk_fill_node_new (GskRenderNode *child,
                   GskPath       *path,
                   GskFillRule    fill_rule)
{
  GskFillNode *self;
  GskRenderNode *node;
  graphene_rect_t path_bounds;

  g_return_val_if_fail (GSK_IS_RENDER_NODE (child), NULL);
  g_return_val_if_fail (path != NULL, NULL);

  self = gsk_render_node_alloc (GSK_FILL_NODE);
  node = (GskRenderNode *) self;
  node->offscreen_for_opacity = child->offscreen_for_opacity;
  node->preferred_depth = gsk_render_node_get_preferred_depth (child);
  node->is_hdr = gsk_render_node_is_hdr (child);

  self->child = gsk_render_node_ref (child);
  self->path = gsk_path_ref (path);
  self->fill_rule = fill_rule;

  if (gsk_path_get_bounds (path, &path_bounds))
    gsk_rect_intersection (&path_bounds, &child->bounds, &node->bounds);
  else
    gsk_rect_init (&node->bounds, 0.f, 0.f, 0.f, 0.f);

  return node;
}

/**
 * gsk_fill_node_get_child:
 * @node: (type GskFillNode): a fill `GskRenderNode`
 *
 * Gets the child node that is getting drawn by the given @node.
 *
 * Returns: (transfer none): The child that is getting drawn
 *
 * Since: 4.14
 */
GskRenderNode *
gsk_fill_node_get_child (const GskRenderNode *node)
{
  const GskFillNode *self = (const GskFillNode *) node;

  g_return_val_if_fail (GSK_IS_RENDER_NODE_TYPE (node, GSK_FILL_NODE), NULL);

  return self->child;
}

/**
 * gsk_fill_node_get_path:
 * @node: (type GskFillNode): a fill `GskRenderNode`
 *
 * Retrieves the path used to describe the area filled with the contents of
 * the @node.
 *
 * Returns: (transfer none): a `GskPath`
 *
 * Since: 4.14
 */
GskPath *
gsk_fill_node_get_path (const GskRenderNode *node)
{
  const GskFillNode *self = (const GskFillNode *) node;

  g_return_val_if_fail (GSK_IS_RENDER_NODE_TYPE (node, GSK_FILL_NODE), NULL);

  return self->path;
}

/**
 * gsk_fill_node_get_fill_rule:
 * @node: (type GskFillNode): a fill `GskRenderNode`
 *
 * Retrieves the fill rule used to determine how the path is filled.
 *
 * Returns: a `GskFillRule`
 *
 * Since: 4.14
 */
GskFillRule
gsk_fill_node_get_fill_rule (const GskRenderNode *node)
{
  const GskFillNode *self = (const GskFillNode *) node;

  g_return_val_if_fail (GSK_IS_RENDER_NODE_TYPE (node, GSK_FILL_NODE), GSK_FILL_RULE_WINDING);

  return self->fill_rule;
}

/* }}} */
/* {{{ GSK_STROKE_NODE */

/**
 * GskStrokeNode:
 *
 * A render node that will fill the area determined by stroking the the given
 * [struct@Gsk.Path] using the [struct@Gsk.Stroke] attributes.
 *
 * Since: 4.14
 */

struct _GskStrokeNode
{
  GskRenderNode render_node;

  GskRenderNode *child;
  GskPath *path;
  GskStroke stroke;
};

static void
gsk_stroke_node_finalize (GskRenderNode *node)
{
  GskStrokeNode *self = (GskStrokeNode *) node;
  GskRenderNodeClass *parent_class = g_type_class_peek (g_type_parent (GSK_TYPE_STROKE_NODE));

  gsk_render_node_unref (self->child);
  gsk_path_unref (self->path);
  gsk_stroke_clear (&self->stroke);

  parent_class->finalize (node);
}

static void
gsk_stroke_node_draw (GskRenderNode *node,
                      cairo_t       *cr,
                      GdkColorState *ccs)
{
  GskStrokeNode *self = (GskStrokeNode *) node;

  if (gsk_render_node_get_node_type (self->child) == GSK_COLOR_NODE &&
      gsk_rect_contains_rect (&self->child->bounds, &node->bounds))
    {
      gdk_cairo_set_source_rgba_ccs (cr, ccs, gsk_color_node_get_color (self->child));
    }
  else
    {
      gsk_cairo_rectangle_pixel_aligned (cr, &self->child->bounds);
      cairo_clip (cr);
      if (has_empty_clip (cr))
        return;

      cairo_push_group (cr);
      gsk_render_node_draw_ccs (self->child, cr, ccs);
      cairo_pop_group_to_source (cr);
    }

  gsk_stroke_to_cairo (&self->stroke, cr);

  gsk_path_to_cairo (self->path, cr);
  cairo_stroke (cr);
}

static void
gsk_stroke_node_diff (GskRenderNode *node1,
                      GskRenderNode *node2,
                      GskDiffData   *data)
{
  GskStrokeNode *self1 = (GskStrokeNode *) node1;
  GskStrokeNode *self2 = (GskStrokeNode *) node2;

  if (self1->path == self2->path &&
      gsk_stroke_equal (&self1->stroke, &self2->stroke))
    {
      cairo_region_t *sub;
      cairo_rectangle_int_t clip_rect;

      sub = cairo_region_create();
      gsk_render_node_diff (self1->child, self2->child, &(GskDiffData) { sub, data->surface });
      gsk_rect_to_cairo_grow (&node1->bounds, &clip_rect);
      cairo_region_intersect_rectangle (sub, &clip_rect);
      cairo_region_union (data->region, sub);
      cairo_region_destroy (sub);
    }
  else
    {
      gsk_render_node_diff_impossible (node1, node2, data);
    }
}

static void
gsk_stroke_node_class_init (gpointer g_class,
                            gpointer class_data)
{
  GskRenderNodeClass *node_class = g_class;

  node_class->node_type = GSK_STROKE_NODE;

  node_class->finalize = gsk_stroke_node_finalize;
  node_class->draw = gsk_stroke_node_draw;
  node_class->diff = gsk_stroke_node_diff;
}

/**
 * gsk_stroke_node_new:
 * @child: The node to stroke the area with
 * @path: (transfer none): The path describing the area to stroke
 * @stroke: (transfer none): The stroke attributes to use
 *
 * Creates a #GskRenderNode that will fill the outline generated by stroking
 * the given @path using the attributes defined in @stroke.
 *
 * The area is filled with @child.
 *
 * Returns: (transfer none) (type GskStrokeNode): A new #GskRenderNode
 *
 * Since: 4.14
 */
GskRenderNode *
gsk_stroke_node_new (GskRenderNode   *child,
                     GskPath         *path,
                     const GskStroke *stroke)
{
  GskStrokeNode *self;
  GskRenderNode *node;
  graphene_rect_t stroke_bounds;

  g_return_val_if_fail (GSK_IS_RENDER_NODE (child), NULL);
  g_return_val_if_fail (path != NULL, NULL);
  g_return_val_if_fail (stroke != NULL, NULL);

  self = gsk_render_node_alloc (GSK_STROKE_NODE);
  node = (GskRenderNode *) self;
  node->offscreen_for_opacity = child->offscreen_for_opacity;
  node->preferred_depth = gsk_render_node_get_preferred_depth (child);
  node->is_hdr = gsk_render_node_is_hdr (child);

  self->child = gsk_render_node_ref (child);
  self->path = gsk_path_ref (path);
  self->stroke = GSK_STROKE_INIT_COPY (stroke);

  if (gsk_path_get_stroke_bounds (self->path, &self->stroke, &stroke_bounds))
    gsk_rect_intersection (&stroke_bounds, &child->bounds, &node->bounds);
  else
    gsk_rect_init (&node->bounds, 0.f, 0.f, 0.f, 0.f);

  return node;
}

/**
 * gsk_stroke_node_get_child:
 * @node: (type GskStrokeNode): a stroke #GskRenderNode
 *
 * Gets the child node that is getting drawn by the given @node.
 *
 * Returns: (transfer none): The child that is getting drawn
 *
 * Since: 4.14
 */
GskRenderNode *
gsk_stroke_node_get_child (const GskRenderNode *node)
{
  const GskStrokeNode *self = (const GskStrokeNode *) node;

  g_return_val_if_fail (GSK_IS_RENDER_NODE_TYPE (node, GSK_STROKE_NODE), NULL);

  return self->child;
}

/**
 * gsk_stroke_node_get_path:
 * @node: (type GskStrokeNode): a stroke #GskRenderNode
 *
 * Retrieves the path that will be stroked with the contents of
 * the @node.
 *
 * Returns: (transfer none): a #GskPath
 *
 * Since: 4.14
 */
GskPath *
gsk_stroke_node_get_path (const GskRenderNode *node)
{
  const GskStrokeNode *self = (const GskStrokeNode *) node;

  g_return_val_if_fail (GSK_IS_RENDER_NODE_TYPE (node, GSK_STROKE_NODE), NULL);

  return self->path;
}

/**
 * gsk_stroke_node_get_stroke:
 * @node: (type GskStrokeNode): a stroke #GskRenderNode
 *
 * Retrieves the stroke attributes used in this @node.
 *
 * Returns: a #GskStroke
 *
 * Since: 4.14
 */
const GskStroke *
gsk_stroke_node_get_stroke (const GskRenderNode *node)
{
  const GskStrokeNode *self = (const GskStrokeNode *) node;

  g_return_val_if_fail (GSK_IS_RENDER_NODE_TYPE (node, GSK_STROKE_NODE), NULL);

  return &self->stroke;
}

/* }}} */
/* {{{ GSK_SHADOW_NODE */

/**
 * GskShadowNode:
 *
 * A render node drawing one or more shadows behind its single child node.
 */
struct _GskShadowNode
{
  GskRenderNode render_node;

  GskRenderNode *child;

  gsize n_shadows;
  GskShadowEntry *shadows;

  GskShadow *rgba_shadows;
};

static void
gsk_shadow_node_finalize (GskRenderNode *node)
{
  GskShadowNode *self = (GskShadowNode *) node;
  GskRenderNodeClass *parent_class = g_type_class_peek (g_type_parent (GSK_TYPE_SHADOW_NODE));

  gsk_render_node_unref (self->child);

  for (gsize i = 0; i < self->n_shadows; i++)
    gdk_color_finish (&self->shadows[i].color);
  g_free (self->shadows);

  g_free (self->rgba_shadows);

  parent_class->finalize (node);
}

static void
gsk_shadow_node_draw (GskRenderNode *node,
                      cairo_t       *cr,
                      GdkColorState *ccs)
{
  GskShadowNode *self = (GskShadowNode *) node;
  gsize i;

  /* clip so the blur area stays small */
  gsk_cairo_rectangle_pixel_aligned (cr, &node->bounds);
  cairo_clip (cr);
  if (has_empty_clip (cr))
    return;

  for (i = 0; i < self->n_shadows; i++)
    {
      GskShadowEntry *shadow = &self->shadows[i];
      cairo_pattern_t *pattern;

      /* We don't need to draw invisible shadows */
      if (gdk_color_is_clear (&shadow->color))
        continue;

      cairo_save (cr);
      cr = gsk_cairo_blur_start_drawing (cr, 0.5 * shadow->radius, GSK_BLUR_X | GSK_BLUR_Y);

      cairo_save (cr);
      cairo_translate (cr, shadow->offset.x, shadow->offset.y);
      cairo_push_group (cr);
      gsk_render_node_draw_ccs (self->child, cr, ccs);
      pattern = cairo_pop_group (cr);
      cairo_reset_clip (cr);
      gdk_cairo_set_source_color (cr, ccs, &shadow->color);
      cairo_mask (cr, pattern);
      cairo_pattern_destroy (pattern);
      cairo_restore (cr);

      cr = gsk_cairo_blur_finish_drawing (cr, ccs, 0.5 * shadow->radius, &shadow->color, GSK_BLUR_X | GSK_BLUR_Y);
      cairo_restore (cr);
    }

  gsk_render_node_draw_ccs (self->child, cr, ccs);
}

static void
gsk_shadow_node_diff (GskRenderNode *node1,
                      GskRenderNode *node2,
                      GskDiffData   *data)
{
  GskShadowNode *self1 = (GskShadowNode *) node1;
  GskShadowNode *self2 = (GskShadowNode *) node2;
  int top = 0, right = 0, bottom = 0, left = 0;
  cairo_region_t *sub;
  cairo_rectangle_int_t rect;
  gsize i, n;

  if (self1->n_shadows != self2->n_shadows)
    {
      gsk_render_node_diff_impossible (node1, node2, data);
      return;
    }

  for (i = 0; i < self1->n_shadows; i++)
    {
      GskShadowEntry *shadow1 = &self1->shadows[i];
      GskShadowEntry *shadow2 = &self2->shadows[i];
      float clip_radius;

      if (!gdk_color_equal (&shadow1->color, &shadow2->color) ||
          !graphene_point_equal (&shadow1->offset, &shadow2->offset) ||
          shadow1->radius != shadow2->radius)
        {
          gsk_render_node_diff_impossible (node1, node2, data);
          return;
        }

      clip_radius = gsk_cairo_blur_compute_pixels (shadow1->radius / 2.0);
      top = MAX (top, ceil (clip_radius - shadow1->offset.y));
      right = MAX (right, ceil (clip_radius + shadow1->offset.x));
      bottom = MAX (bottom, ceil (clip_radius + shadow1->offset.y));
      left = MAX (left, ceil (clip_radius - shadow1->offset.x));
    }

  sub = cairo_region_create ();
  gsk_render_node_diff (self1->child, self2->child, &(GskDiffData) { sub, data->surface });

  n = cairo_region_num_rectangles (sub);
  for (i = 0; i < n; i++)
    {
      cairo_region_get_rectangle (sub, i, &rect);
      rect.x -= left;
      rect.y -= top;
      rect.width += left + right;
      rect.height += top + bottom;
      cairo_region_union_rectangle (data->region, &rect);
    }
  cairo_region_destroy (sub);
}

static void
gsk_shadow_node_get_bounds (GskShadowNode *self,
                            graphene_rect_t *bounds)
{
  float top = 0, right = 0, bottom = 0, left = 0;
  gsize i;

  gsk_rect_init_from_rect (bounds, &self->child->bounds);

  for (i = 0; i < self->n_shadows; i++)
    {
      float clip_radius = gsk_cairo_blur_compute_pixels (self->shadows[i].radius / 2.0);
      top = MAX (top, clip_radius - self->shadows[i].offset.y);
      right = MAX (right, clip_radius + self->shadows[i].offset.x);
      bottom = MAX (bottom, clip_radius + self->shadows[i].offset.y);
      left = MAX (left, clip_radius - self->shadows[i].offset.x);
    }

  bounds->origin.x -= left;
  bounds->origin.y -= top;
  bounds->size.width += left + right;
  bounds->size.height += top + bottom;
}

static void
gsk_shadow_node_class_init (gpointer g_class,
                            gpointer class_data)
{
  GskRenderNodeClass *node_class = g_class;

  node_class->node_type = GSK_SHADOW_NODE;

  node_class->finalize = gsk_shadow_node_finalize;
  node_class->draw = gsk_shadow_node_draw;
  node_class->diff = gsk_shadow_node_diff;
}

/**
 * gsk_shadow_node_new:
 * @child: The node to draw
 * @shadows: (array length=n_shadows): The shadows to apply
 * @n_shadows: number of entries in the @shadows array
 *
 * Creates a `GskRenderNode` that will draw a @child with the given
 * @shadows below it.
 *
 * Returns: (transfer full) (type GskShadowNode): A new `GskRenderNode`
 */
GskRenderNode *
gsk_shadow_node_new (GskRenderNode   *child,
                     const GskShadow *shadows,
                     gsize            n_shadows)
{
  GskShadowEntry *shadows2;
  GskRenderNode *node;

  g_return_val_if_fail (GSK_IS_RENDER_NODE (child), NULL);
  g_return_val_if_fail (shadows != NULL, NULL);
  g_return_val_if_fail (n_shadows > 0, NULL);

  shadows2 = g_new (GskShadowEntry, n_shadows);
  for (gsize i = 0; i < n_shadows; i++)
    {
      gdk_color_init_from_rgba (&shadows2[i].color, &shadows[i].color);
      graphene_point_init (&shadows2[i].offset, shadows[i].dx, shadows[i].dy);
      shadows2[i].radius = shadows[i].radius;
    }

  node = gsk_shadow_node_new2 (child, shadows2, n_shadows);

  for (gsize i = 0; i < n_shadows; i++)
    gdk_color_finish (&shadows2[i].color);
  g_free (shadows2);

  return node;
}

/*< private >
 * gsk_shadow_node_new2:
 * @child: The node to draw
 * @shadows: (array length=n_shadows): The shadows to apply
 * @n_shadows: number of entries in the @shadows array
 *
 * Creates a `GskRenderNode` that will draw a @child with the given
 * @shadows below it.
 *
 * Returns: (transfer full) (type GskShadowNode): A new `GskRenderNode`
 */
GskRenderNode *
gsk_shadow_node_new2 (GskRenderNode        *child,
                      const GskShadowEntry *shadows,
                      gsize                 n_shadows)
{
  GskShadowNode *self;
  GskRenderNode *node;
  gsize i;
  gboolean is_hdr;

  g_return_val_if_fail (GSK_IS_RENDER_NODE (child), NULL);
  g_return_val_if_fail (shadows != NULL, NULL);
  g_return_val_if_fail (n_shadows > 0, NULL);

  self = gsk_render_node_alloc (GSK_SHADOW_NODE);
  node = (GskRenderNode *) self;
  node->offscreen_for_opacity = TRUE;

  self->child = gsk_render_node_ref (child);
  self->n_shadows = n_shadows;
  self->shadows = g_new (GskShadowEntry, n_shadows);

  is_hdr = gsk_render_node_is_hdr (child);

  for (i = 0; i < n_shadows; i++)
    {
      gdk_color_init_copy (&self->shadows[i].color, &shadows[i].color);
      graphene_point_init_from_point (&self->shadows[i].offset, &shadows[i].offset);
      self->shadows[i].radius = shadows[i].radius;
      is_hdr = is_hdr || color_state_is_hdr (shadows[i].color.color_state);
    }

  node->preferred_depth = gsk_render_node_get_preferred_depth (child);
  node->is_hdr = is_hdr;

  gsk_shadow_node_get_bounds (self, &node->bounds);

  return node;
}

/**
 * gsk_shadow_node_get_child:
 * @node: (type GskShadowNode): a shadow `GskRenderNode`
 *
 * Retrieves the child `GskRenderNode` of the shadow @node.
 *
 * Returns: (transfer none): the child render node
 */
GskRenderNode *
gsk_shadow_node_get_child (const GskRenderNode *node)
{
  const GskShadowNode *self = (const GskShadowNode *) node;

  return self->child;
}

/**
 * gsk_shadow_node_get_shadow:
 * @node: (type GskShadowNode): a shadow `GskRenderNode`
 * @i: the given index
 *
 * Retrieves the shadow data at the given index @i.
 *
 * Returns: (transfer none): the shadow data
 */
const GskShadow *
gsk_shadow_node_get_shadow (const GskRenderNode *node,
                            gsize                i)
{
  GskShadowNode *self = (GskShadowNode *) node;
  const GskShadow *shadow;

  G_LOCK (rgba);

  if (self->rgba_shadows == NULL)
    {
      self->rgba_shadows = g_new (GskShadow, self->n_shadows);
      for (gsize j = 0; j < self->n_shadows; j++)
        {
          gdk_color_to_float (&self->shadows[j].color,
                              GDK_COLOR_STATE_SRGB,
                              (float *) &self->rgba_shadows[j].color);
          self->rgba_shadows[j].dx = self->shadows[j].offset.x;
          self->rgba_shadows[j].dy = self->shadows[j].offset.y;
          self->rgba_shadows[j].radius = self->shadows[j].radius;
        }
    }

  shadow = &self->rgba_shadows[i];

  G_UNLOCK (rgba);

  return shadow;
}

/*< private >
 * gsk_shadow_node_get_shadow_entry:
 * @node: (type GskShadowNode): a shadow `GskRenderNode`
 * @i: the given index
 *
 * Retrieves the shadow data at the given index @i.
 *
 * Returns: (transfer none): the shadow data
 */
const GskShadowEntry *
gsk_shadow_node_get_shadow_entry (const GskRenderNode *node,
                                  gsize                i)
{
  const GskShadowNode *self = (const GskShadowNode *) node;

  return &self->shadows[i];
}

/**
 * gsk_shadow_node_get_n_shadows:
 * @node: (type GskShadowNode): a shadow `GskRenderNode`
 *
 * Retrieves the number of shadows in the @node.
 *
 * Returns: the number of shadows.
 */
gsize
gsk_shadow_node_get_n_shadows (const GskRenderNode *node)
{
  const GskShadowNode *self = (const GskShadowNode *) node;

  return self->n_shadows;
}

/* }}} */
/* {{{ GSK_BLEND_NODE */

/**
 * GskBlendNode:
 *
 * A render node applying a blending function between its two child nodes.
 */
struct _GskBlendNode
{
  GskRenderNode render_node;

  GskRenderNode *bottom;
  GskRenderNode *top;
  GskBlendMode blend_mode;
};

static cairo_operator_t
gsk_blend_mode_to_cairo_operator (GskBlendMode blend_mode)
{
  switch (blend_mode)
    {
    default:
      g_assert_not_reached ();
    case GSK_BLEND_MODE_DEFAULT:
      return CAIRO_OPERATOR_OVER;
    case GSK_BLEND_MODE_MULTIPLY:
      return CAIRO_OPERATOR_MULTIPLY;
    case GSK_BLEND_MODE_SCREEN:
      return CAIRO_OPERATOR_SCREEN;
    case GSK_BLEND_MODE_OVERLAY:
      return CAIRO_OPERATOR_OVERLAY;
    case GSK_BLEND_MODE_DARKEN:
      return CAIRO_OPERATOR_DARKEN;
    case GSK_BLEND_MODE_LIGHTEN:
      return CAIRO_OPERATOR_LIGHTEN;
    case GSK_BLEND_MODE_COLOR_DODGE:
      return CAIRO_OPERATOR_COLOR_DODGE;
    case GSK_BLEND_MODE_COLOR_BURN:
      return CAIRO_OPERATOR_COLOR_BURN;
    case GSK_BLEND_MODE_HARD_LIGHT:
      return CAIRO_OPERATOR_HARD_LIGHT;
    case GSK_BLEND_MODE_SOFT_LIGHT:
      return CAIRO_OPERATOR_SOFT_LIGHT;
    case GSK_BLEND_MODE_DIFFERENCE:
      return CAIRO_OPERATOR_DIFFERENCE;
    case GSK_BLEND_MODE_EXCLUSION:
      return CAIRO_OPERATOR_EXCLUSION;
    case GSK_BLEND_MODE_COLOR:
      return CAIRO_OPERATOR_HSL_COLOR;
    case GSK_BLEND_MODE_HUE:
      return CAIRO_OPERATOR_HSL_HUE;
    case GSK_BLEND_MODE_SATURATION:
      return CAIRO_OPERATOR_HSL_SATURATION;
    case GSK_BLEND_MODE_LUMINOSITY:
      return CAIRO_OPERATOR_HSL_LUMINOSITY;
    }
}

static void
gsk_blend_node_finalize (GskRenderNode *node)
{
  GskBlendNode *self = (GskBlendNode *) node;
  GskRenderNodeClass *parent_class = g_type_class_peek (g_type_parent (GSK_TYPE_BLEND_NODE));

  gsk_render_node_unref (self->bottom);
  gsk_render_node_unref (self->top);

  parent_class->finalize (node);
}

static void
gsk_blend_node_draw (GskRenderNode *node,
                     cairo_t       *cr,
                     GdkColorState *ccs)
{
  GskBlendNode *self = (GskBlendNode *) node;

  if (has_empty_clip (cr))
    return;

  if (!gdk_color_state_equal (ccs, GDK_COLOR_STATE_SRGB))
    g_warning ("blend node in non-srgb colorstate isn't implemented yet.");

  cairo_push_group (cr);
  gsk_render_node_draw_ccs (self->bottom, cr, ccs);

  cairo_push_group (cr);
  gsk_render_node_draw_ccs (self->top, cr, ccs);

  cairo_pop_group_to_source (cr);
  cairo_set_operator (cr, gsk_blend_mode_to_cairo_operator (self->blend_mode));
  cairo_paint (cr);

  cairo_pop_group_to_source (cr); /* resets operator */
  cairo_paint (cr);
}

static void
gsk_blend_node_diff (GskRenderNode *node1,
                     GskRenderNode *node2,
                     GskDiffData   *data)
{
  GskBlendNode *self1 = (GskBlendNode *) node1;
  GskBlendNode *self2 = (GskBlendNode *) node2;

  if (self1->blend_mode == self2->blend_mode)
    {
      gsk_render_node_diff (self1->top, self2->top, data);
      gsk_render_node_diff (self1->bottom, self2->bottom, data);
    }
  else
    {
      gsk_render_node_diff_impossible (node1, node2, data);
    }
}

static void
gsk_blend_node_class_init (gpointer g_class,
                           gpointer class_data)
{
  GskRenderNodeClass *node_class = g_class;

  node_class->node_type = GSK_BLEND_NODE;

  node_class->finalize = gsk_blend_node_finalize;
  node_class->draw = gsk_blend_node_draw;
  node_class->diff = gsk_blend_node_diff;
}

/**
 * gsk_blend_node_new:
 * @bottom: The bottom node to be drawn
 * @top: The node to be blended onto the @bottom node
 * @blend_mode: The blend mode to use
 *
 * Creates a `GskRenderNode` that will use @blend_mode to blend the @top
 * node onto the @bottom node.
 *
 * Returns: (transfer full) (type GskBlendNode): A new `GskRenderNode`
 */
GskRenderNode *
gsk_blend_node_new (GskRenderNode *bottom,
                    GskRenderNode *top,
                    GskBlendMode   blend_mode)
{
  GskBlendNode *self;
  GskRenderNode *node;

  g_return_val_if_fail (GSK_IS_RENDER_NODE (bottom), NULL);
  g_return_val_if_fail (GSK_IS_RENDER_NODE (top), NULL);

  self = gsk_render_node_alloc (GSK_BLEND_NODE);
  node = (GskRenderNode *) self;
  node->offscreen_for_opacity = TRUE;

  self->bottom = gsk_render_node_ref (bottom);
  self->top = gsk_render_node_ref (top);
  self->blend_mode = blend_mode;

  graphene_rect_union (&bottom->bounds, &top->bounds, &node->bounds);

  node->preferred_depth = gdk_memory_depth_merge (gsk_render_node_get_preferred_depth (bottom),
                                                  gsk_render_node_get_preferred_depth (top));
  node->is_hdr = gsk_render_node_is_hdr (bottom) ||
                 gsk_render_node_is_hdr (top);

  return node;
}

/**
 * gsk_blend_node_get_bottom_child:
 * @node: (type GskBlendNode): a blending `GskRenderNode`
 *
 * Retrieves the bottom `GskRenderNode` child of the @node.
 *
 * Returns: (transfer none): the bottom child node
 */
GskRenderNode *
gsk_blend_node_get_bottom_child (const GskRenderNode *node)
{
  const GskBlendNode *self = (const GskBlendNode *) node;

  return self->bottom;
}

/**
 * gsk_blend_node_get_top_child:
 * @node: (type GskBlendNode): a blending `GskRenderNode`
 *
 * Retrieves the top `GskRenderNode` child of the @node.
 *
 * Returns: (transfer none): the top child node
 */
GskRenderNode *
gsk_blend_node_get_top_child (const GskRenderNode *node)
{
  const GskBlendNode *self = (const GskBlendNode *) node;

  return self->top;
}

/**
 * gsk_blend_node_get_blend_mode:
 * @node: (type GskBlendNode): a blending `GskRenderNode`
 *
 * Retrieves the blend mode used by @node.
 *
 * Returns: the blend mode
 */
GskBlendMode
gsk_blend_node_get_blend_mode (const GskRenderNode *node)
{
  const GskBlendNode *self = (const GskBlendNode *) node;

  return self->blend_mode;
}

/* }}} */
/* {{{ GSK_CROSS_FADE_NODE */

/**
 * GskCrossFadeNode:
 *
 * A render node cross fading between two child nodes.
 */
struct _GskCrossFadeNode
{
  GskRenderNode render_node;

  GskRenderNode *start;
  GskRenderNode *end;
  float          progress;
};

static void
gsk_cross_fade_node_finalize (GskRenderNode *node)
{
  GskCrossFadeNode *self = (GskCrossFadeNode *) node;
  GskRenderNodeClass *parent_class = g_type_class_peek (g_type_parent (GSK_TYPE_CROSS_FADE_NODE));

  gsk_render_node_unref (self->start);
  gsk_render_node_unref (self->end);

  parent_class->finalize (node);
}

static void
gsk_cross_fade_node_draw (GskRenderNode *node,
                          cairo_t       *cr,
                          GdkColorState *ccs)
{
  GskCrossFadeNode *self = (GskCrossFadeNode *) node;

  if (has_empty_clip (cr))
    return;

  cairo_push_group_with_content (cr, CAIRO_CONTENT_COLOR_ALPHA);
  gsk_render_node_draw_ccs (self->start, cr, ccs);

  cairo_push_group_with_content (cr, CAIRO_CONTENT_COLOR_ALPHA);
  gsk_render_node_draw_ccs (self->end, cr, ccs);

  cairo_pop_group_to_source (cr);
  cairo_set_operator (cr, CAIRO_OPERATOR_SOURCE);
  cairo_paint_with_alpha (cr, self->progress);

  cairo_pop_group_to_source (cr); /* resets operator */
  cairo_paint (cr);
}

static void
gsk_cross_fade_node_diff (GskRenderNode *node1,
                          GskRenderNode *node2,
                          GskDiffData   *data)
{
  GskCrossFadeNode *self1 = (GskCrossFadeNode *) node1;
  GskCrossFadeNode *self2 = (GskCrossFadeNode *) node2;

  if (self1->progress == self2->progress)
    {
      gsk_render_node_diff (self1->start, self2->start, data);
      gsk_render_node_diff (self1->end, self2->end, data);
      return;
    }

  gsk_render_node_diff_impossible (node1, node2, data);
}

static gboolean
gsk_cross_fade_node_get_opaque_rect (GskRenderNode   *node,
                                     graphene_rect_t *opaque)
{
  GskCrossFadeNode *self = (GskCrossFadeNode *) node;
  graphene_rect_t start_opaque, end_opaque;

  if (!gsk_render_node_get_opaque_rect (self->start, &start_opaque) ||
      !gsk_render_node_get_opaque_rect (self->end, &end_opaque))
    return FALSE;

  return graphene_rect_intersection (&start_opaque, &end_opaque, opaque);
}

static void
gsk_cross_fade_node_class_init (gpointer g_class,
                                gpointer class_data)
{
  GskRenderNodeClass *node_class = g_class;

  node_class->node_type = GSK_CROSS_FADE_NODE;

  node_class->finalize = gsk_cross_fade_node_finalize;
  node_class->draw = gsk_cross_fade_node_draw;
  node_class->diff = gsk_cross_fade_node_diff;
  node_class->get_opaque_rect = gsk_cross_fade_node_get_opaque_rect;
}

/**
 * gsk_cross_fade_node_new:
 * @start: The start node to be drawn
 * @end: The node to be cross_fadeed onto the @start node
 * @progress: How far the fade has progressed from start to end. The value will
 *     be clamped to the range [0 ... 1]
 *
 * Creates a `GskRenderNode` that will do a cross-fade between @start and @end.
 *
 * Returns: (transfer full) (type GskCrossFadeNode): A new `GskRenderNode`
 */
GskRenderNode *
gsk_cross_fade_node_new (GskRenderNode *start,
                         GskRenderNode *end,
                         float          progress)
{
  GskCrossFadeNode *self;
  GskRenderNode *node;

  g_return_val_if_fail (GSK_IS_RENDER_NODE (start), NULL);
  g_return_val_if_fail (GSK_IS_RENDER_NODE (end), NULL);

  self = gsk_render_node_alloc (GSK_CROSS_FADE_NODE);
  node = (GskRenderNode *) self;
  node->offscreen_for_opacity = TRUE;
  node->fully_opaque = start->fully_opaque && end->fully_opaque &&
    gsk_rect_equal (&start->bounds, &end->bounds);

  self->start = gsk_render_node_ref (start);
  self->end = gsk_render_node_ref (end);
  self->progress = CLAMP (progress, 0.0, 1.0);

  graphene_rect_union (&start->bounds, &end->bounds, &node->bounds);

  node->preferred_depth = gdk_memory_depth_merge (gsk_render_node_get_preferred_depth (start),
                                                  gsk_render_node_get_preferred_depth (end));
  node->is_hdr = gsk_render_node_is_hdr (start) ||
                 gsk_render_node_is_hdr (end);

  return node;
}

/**
 * gsk_cross_fade_node_get_start_child:
 * @node: (type GskCrossFadeNode): a cross-fading `GskRenderNode`
 *
 * Retrieves the child `GskRenderNode` at the beginning of the cross-fade.
 *
 * Returns: (transfer none): a `GskRenderNode`
 */
GskRenderNode *
gsk_cross_fade_node_get_start_child (const GskRenderNode *node)
{
  const GskCrossFadeNode *self = (const GskCrossFadeNode *) node;

  return self->start;
}

/**
 * gsk_cross_fade_node_get_end_child:
 * @node: (type GskCrossFadeNode): a cross-fading `GskRenderNode`
 *
 * Retrieves the child `GskRenderNode` at the end of the cross-fade.
 *
 * Returns: (transfer none): a `GskRenderNode`
 */
GskRenderNode *
gsk_cross_fade_node_get_end_child (const GskRenderNode *node)
{
  const GskCrossFadeNode *self = (const GskCrossFadeNode *) node;

  return self->end;
}

/**
 * gsk_cross_fade_node_get_progress:
 * @node: (type GskCrossFadeNode): a cross-fading `GskRenderNode`
 *
 * Retrieves the progress value of the cross fade.
 *
 * Returns: the progress value, between 0 and 1
 */
float
gsk_cross_fade_node_get_progress (const GskRenderNode *node)
{
  const GskCrossFadeNode *self = (const GskCrossFadeNode *) node;

  return self->progress;
}

/* }}} */
/* {{{ GSK_TEXT_NODE */

/**
 * GskTextNode:
 *
 * A render node drawing a set of glyphs.
 */
struct _GskTextNode
{
  GskRenderNode render_node;

  PangoFontMap *fontmap;
  PangoFont *font;
  gboolean has_color_glyphs;
  cairo_hint_style_t hint_style;

  GdkColor color;
  graphene_point_t offset;

  guint num_glyphs;
  PangoGlyphInfo *glyphs;
};

static void
gsk_text_node_finalize (GskRenderNode *node)
{
  GskTextNode *self = (GskTextNode *) node;
  GskRenderNodeClass *parent_class = g_type_class_peek (g_type_parent (GSK_TYPE_TEXT_NODE));

  g_object_unref (self->font);
  g_object_unref (self->fontmap);
  g_free (self->glyphs);
  gdk_color_finish (&self->color);

  parent_class->finalize (node);
}

static void
gsk_text_node_draw (GskRenderNode *node,
                    cairo_t       *cr,
                    GdkColorState *ccs)
{
  GskTextNode *self = (GskTextNode *) node;
  PangoGlyphString glyphs;

  glyphs.num_glyphs = self->num_glyphs;
  glyphs.glyphs = self->glyphs;
  glyphs.log_clusters = NULL;

  cairo_save (cr);

  if (!gdk_color_state_equal (ccs, GDK_COLOR_STATE_SRGB) &&
      self->has_color_glyphs)
    {
      g_warning ("whoopsie, color glyphs and we're not in sRGB");
    }
  else
    {
      gdk_cairo_set_source_color (cr, ccs, &self->color);
      cairo_translate (cr, self->offset.x, self->offset.y);
      pango_cairo_show_glyph_string (cr, self->font, &glyphs);
    }

  cairo_restore (cr);
}

static void
gsk_text_node_diff (GskRenderNode *node1,
                    GskRenderNode *node2,
                    GskDiffData   *data)
{
  GskTextNode *self1 = (GskTextNode *) node1;
  GskTextNode *self2 = (GskTextNode *) node2;

  if (self1->font == self2->font &&
      gdk_color_equal (&self1->color, &self2->color) &&
      graphene_point_equal (&self1->offset, &self2->offset) &&
      self1->num_glyphs == self2->num_glyphs)
    {
      guint i;

      for (i = 0; i < self1->num_glyphs; i++)
        {
          PangoGlyphInfo *info1 = &self1->glyphs[i];
          PangoGlyphInfo *info2 = &self2->glyphs[i];

          if (info1->glyph == info2->glyph &&
              info1->geometry.width == info2->geometry.width &&
              info1->geometry.x_offset == info2->geometry.x_offset &&
              info1->geometry.y_offset == info2->geometry.y_offset &&
              info1->attr.is_cluster_start == info2->attr.is_cluster_start &&
              info1->attr.is_color == info2->attr.is_color)
            continue;

          gsk_render_node_diff_impossible (node1, node2, data);
          return;
        }

      return;
    }

  gsk_render_node_diff_impossible (node1, node2, data);
}

static void
gsk_text_node_class_init (gpointer g_class,
                          gpointer class_data)
{
  GskRenderNodeClass *node_class = g_class;

  node_class->node_type = GSK_TEXT_NODE;

  node_class->finalize = gsk_text_node_finalize;
  node_class->draw = gsk_text_node_draw;
  node_class->diff = gsk_text_node_diff;
}

static inline float
pango_units_to_float (int i)
{
  return (float) i / PANGO_SCALE;
}

/**
 * gsk_text_node_new:
 * @font: the `PangoFont` containing the glyphs
 * @glyphs: the `PangoGlyphString` to render
 * @color: the foreground color to render with
 * @offset: offset of the baseline
 *
 * Creates a render node that renders the given glyphs.
 *
 * Note that @color may not be used if the font contains
 * color glyphs.
 *
 * Returns: (nullable) (transfer full) (type GskTextNode): a new `GskRenderNode`
 */
GskRenderNode *
gsk_text_node_new (PangoFont              *font,
                   PangoGlyphString       *glyphs,
                   const GdkRGBA          *color,
                   const graphene_point_t *offset)
{
  GdkColor color2;
  GskRenderNode *node;

  gdk_color_init_from_rgba (&color2, color);
  node = gsk_text_node_new2 (font, glyphs, &color2, offset);
  gdk_color_finish (&color2);

  return node;
}

/*< private >
 * gsk_text_node_new2:
 * @font: the `PangoFont` containing the glyphs
 * @glyphs: the `PangoGlyphString` to render
 * @color: the foreground color to render with
 * @offset: offset of the baseline
 *
 * Creates a render node that renders the given glyphs.
 *
 * Note that @color may not be used if the font contains
 * color glyphs.
 *
 * Returns: (nullable) (transfer full) (type GskTextNode): a new `GskRenderNode`
 */
GskRenderNode *
gsk_text_node_new2 (PangoFont              *font,
                    PangoGlyphString       *glyphs,
                    const GdkColor         *color,
                    const graphene_point_t *offset)
{
  GskTextNode *self;
  GskRenderNode *node;
  PangoRectangle ink_rect;
  PangoGlyphInfo *glyph_infos;
  int n;

  gsk_get_glyph_string_extents (glyphs, font, &ink_rect);

  /* Don't create nodes with empty bounds */
  if (ink_rect.width == 0 || ink_rect.height == 0)
    return NULL;

  self = gsk_render_node_alloc (GSK_TEXT_NODE);
  node = (GskRenderNode *) self;
  node->offscreen_for_opacity = FALSE;
  node->preferred_depth = GDK_MEMORY_NONE;
  node->is_hdr = color_state_is_hdr (color->color_state);

  self->fontmap = g_object_ref (pango_font_get_font_map (font));
  self->font = g_object_ref (font);
  gdk_color_init_copy (&self->color, color);
  self->offset = *offset;
  self->has_color_glyphs = FALSE;
  self->hint_style = gsk_font_get_hint_style (font);

  glyph_infos = g_malloc_n (glyphs->num_glyphs, sizeof (PangoGlyphInfo));

  n = 0;
  for (int i = 0; i < glyphs->num_glyphs; i++)
    {
      /* skip empty glyphs */
      if (glyphs->glyphs[i].glyph == PANGO_GLYPH_EMPTY)
        continue;

      glyph_infos[n] = glyphs->glyphs[i];

      if (glyphs->glyphs[i].attr.is_color)
        self->has_color_glyphs = TRUE;

      n++;
    }

  self->glyphs = glyph_infos;
  self->num_glyphs = n;

  gsk_rect_init (&node->bounds,
                 offset->x + pango_units_to_float (ink_rect.x),
                 offset->y + pango_units_to_float (ink_rect.y),
                 pango_units_to_float (ink_rect.width),
                 pango_units_to_float (ink_rect.height));

  return node;
}


/**
 * gsk_text_node_get_color:
 * @node: (type GskTextNode): a text `GskRenderNode`
 *
 * Retrieves the color used by the text @node.
 *
 * The value returned by this function will not be correct
 * if the render node was created for a non-sRGB color.
 *
 * Returns: (transfer none): the text color
 */
const GdkRGBA *
gsk_text_node_get_color (const GskRenderNode *node)
{
  const GskTextNode *self = (const GskTextNode *) node;

  /* NOTE: This is only correct for nodes with sRGB colors */
  return (const GdkRGBA *) &self->color;
}

/*< private >
 * gsk_text_node_get_gdk_color:
 * @node: (type GskTextNode): a `GskRenderNode`
 *
 * Retrieves the color of the given @node.
 *
 * Returns: (transfer none): the color of the node
 */
const GdkColor *
gsk_text_node_get_gdk_color (const GskRenderNode *node)
{
  GskTextNode *self = (GskTextNode *) node;

  return &self->color;
}

/**
 * gsk_text_node_get_font:
 * @node: (type GskTextNode): The `GskRenderNode`
 *
 * Returns the font used by the text @node.
 *
 * Returns: (transfer none): the font
 */
PangoFont *
gsk_text_node_get_font (const GskRenderNode *node)
{
  const GskTextNode *self = (const GskTextNode *) node;

  return self->font;
}

cairo_hint_style_t
gsk_text_node_get_font_hint_style (const GskRenderNode *node)
{
  const GskTextNode *self = (const GskTextNode *) node;

  return self->hint_style;
}

/**
 * gsk_text_node_has_color_glyphs:
 * @node: (type GskTextNode): a text `GskRenderNode`
 *
 * Checks whether the text @node has color glyphs.
 *
 * Returns: %TRUE if the text node has color glyphs
 *
 * Since: 4.2
 */
gboolean
gsk_text_node_has_color_glyphs (const GskRenderNode *node)
{
  const GskTextNode *self = (const GskTextNode *) node;

  return self->has_color_glyphs;
}

/**
 * gsk_text_node_get_num_glyphs:
 * @node: (type GskTextNode): a text `GskRenderNode`
 *
 * Retrieves the number of glyphs in the text node.
 *
 * Returns: the number of glyphs
 */
guint
gsk_text_node_get_num_glyphs (const GskRenderNode *node)
{
  const GskTextNode *self = (const GskTextNode *) node;

  return self->num_glyphs;
}

/**
 * gsk_text_node_get_glyphs:
 * @node: (type GskTextNode): a text `GskRenderNode`
 * @n_glyphs: (out) (optional): the number of glyphs returned
 *
 * Retrieves the glyph information in the @node.
 *
 * Returns: (transfer none) (array length=n_glyphs): the glyph information
 */
const PangoGlyphInfo *
gsk_text_node_get_glyphs (const GskRenderNode *node,
                          guint               *n_glyphs)
{
  const GskTextNode *self = (const GskTextNode *) node;

  if (n_glyphs != NULL)
    *n_glyphs = self->num_glyphs;

  return self->glyphs;
}

/**
 * gsk_text_node_get_offset:
 * @node: (type GskTextNode): a text `GskRenderNode`
 *
 * Retrieves the offset applied to the text.
 *
 * Returns: (transfer none): a point with the horizontal and vertical offsets
 */
const graphene_point_t *
gsk_text_node_get_offset (const GskRenderNode *node)
{
  const GskTextNode *self = (const GskTextNode *) node;

  return &self->offset;
}

/* }}} */
/* {{{ GSK_BLUR_NODE */

/**
 * GskBlurNode:
 *
 * A render node applying a blur effect to its single child.
 */
struct _GskBlurNode
{
  GskRenderNode render_node;

  GskRenderNode *child;
  float radius;
};

static void
gsk_blur_node_finalize (GskRenderNode *node)
{
  GskBlurNode *self = (GskBlurNode *) node;
  GskRenderNodeClass *parent_class = g_type_class_peek (g_type_parent (GSK_TYPE_BLUR_NODE));

  gsk_render_node_unref (self->child);

  parent_class->finalize (node);
}

static void
blur_once (cairo_surface_t *src,
           cairo_surface_t *dest,
           int radius,
           guchar *div_kernel_size)
{
  int width, height, src_rowstride, dest_rowstride, n_channels;
  guchar *p_src, *p_dest, *c1, *c2;
  int x, y, i, i1, i2, width_minus_1, height_minus_1, radius_plus_1;
  int r, g, b, a;
  guchar *p_dest_row, *p_dest_col;

  width = cairo_image_surface_get_width (src);
  height = cairo_image_surface_get_height (src);
  n_channels = 4;
  radius_plus_1 = radius + 1;

  /* horizontal blur */
  p_src = cairo_image_surface_get_data (src);
  p_dest = cairo_image_surface_get_data (dest);
  src_rowstride = cairo_image_surface_get_stride (src);
  dest_rowstride = cairo_image_surface_get_stride (dest);

  width_minus_1 = width - 1;
  for (y = 0; y < height; y++)
    {
      /* calc the initial sums of the kernel */
      r = g = b = a = 0;
      for (i = -radius; i <= radius; i++)
        {
          c1 = p_src + (CLAMP (i, 0, width_minus_1) * n_channels);
          r += c1[0];
          g += c1[1];
          b += c1[2];
          a += c1[3];
        }
      p_dest_row = p_dest;
      for (x = 0; x < width; x++)
        {
          /* set as the mean of the kernel */
          p_dest_row[0] = div_kernel_size[r];
          p_dest_row[1] = div_kernel_size[g];
          p_dest_row[2] = div_kernel_size[b];
          p_dest_row[3] = div_kernel_size[a];
          p_dest_row += n_channels;

          /* the pixel to add to the kernel */
          i1 = x + radius_plus_1;
          if (i1 > width_minus_1)
            i1 = width_minus_1;
          c1 = p_src + (i1 * n_channels);

          /* the pixel to remove from the kernel */
          i2 = x - radius;
          if (i2 < 0)
            i2 = 0;
          c2 = p_src + (i2 * n_channels);

          /* calc the new sums of the kernel */
          r += c1[0] - c2[0];
          g += c1[1] - c2[1];
          b += c1[2] - c2[2];
          a += c1[3] - c2[3];
        }

      p_src += src_rowstride;
      p_dest += dest_rowstride;
    }

  /* vertical blur */
  p_src = cairo_image_surface_get_data (dest);
  p_dest = cairo_image_surface_get_data (src);
  src_rowstride = cairo_image_surface_get_stride (dest);
  dest_rowstride = cairo_image_surface_get_stride (src);

  height_minus_1 = height - 1;
  for (x = 0; x < width; x++)
    {
      /* calc the initial sums of the kernel */
      r = g = b = a = 0;
      for (i = -radius; i <= radius; i++)
        {
          c1 = p_src + (CLAMP (i, 0, height_minus_1) * src_rowstride);
          r += c1[0];
          g += c1[1];
          b += c1[2];
          a += c1[3];
        }

      p_dest_col = p_dest;
      for (y = 0; y < height; y++)
        {
          /* set as the mean of the kernel */

          p_dest_col[0] = div_kernel_size[r];
          p_dest_col[1] = div_kernel_size[g];
          p_dest_col[2] = div_kernel_size[b];
          p_dest_col[3] = div_kernel_size[a];
          p_dest_col += dest_rowstride;

          /* the pixel to add to the kernel */
          i1 = y + radius_plus_1;
          if (i1 > height_minus_1)
            i1 = height_minus_1;
          c1 = p_src + (i1 * src_rowstride);

          /* the pixel to remove from the kernel */
          i2 = y - radius;
          if (i2 < 0)
            i2 = 0;
          c2 = p_src + (i2 * src_rowstride);
          /* calc the new sums of the kernel */
          r += c1[0] - c2[0];
          g += c1[1] - c2[1];
          b += c1[2] - c2[2];
          a += c1[3] - c2[3];
        }

      p_src += n_channels;
      p_dest += n_channels;
    }
}

static void
blur_image_surface (cairo_surface_t *surface, int radius, int iterations)
{
  int kernel_size;
  int i;
  guchar *div_kernel_size;
  cairo_surface_t *tmp;
  int width, height;

  g_assert (radius >= 0);

  width = cairo_image_surface_get_width (surface);
  height = cairo_image_surface_get_height (surface);
  tmp = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width, height);

  kernel_size = 2 * radius + 1;
  div_kernel_size = g_new (guchar, 256 * kernel_size);
  for (i = 0; i < 256 * kernel_size; i++)
    div_kernel_size[i] = (guchar) (i / kernel_size);

  while (iterations-- > 0)
    blur_once (surface, tmp, radius, div_kernel_size);

  g_free (div_kernel_size);
  cairo_surface_destroy (tmp);
}

static void
gsk_blur_node_draw (GskRenderNode *node,
                    cairo_t       *cr,
                    GdkColorState *ccs)
{
  GskBlurNode *self = (GskBlurNode *) node;
  cairo_surface_t *surface;
  cairo_t *cr2;
  graphene_rect_t blur_bounds;
  double clip_radius;

  clip_radius = gsk_cairo_blur_compute_pixels (0.5 * self->radius);

  /* We need to extend the clip by the blur radius
   * so we can blur pixels in that region */
  _graphene_rect_init_from_clip_extents (&blur_bounds, cr);
  graphene_rect_inset (&blur_bounds, - clip_radius, - clip_radius);
  if (!gsk_rect_intersection (&blur_bounds, &node->bounds, &blur_bounds))
    return;

  surface = cairo_surface_create_similar_image (cairo_get_target (cr),
                                                CAIRO_FORMAT_ARGB32,
                                                ceil (blur_bounds.size.width),
                                                ceil (blur_bounds.size.height));
  cairo_surface_set_device_offset (surface,
                                   - blur_bounds.origin.x,
                                   - blur_bounds.origin.y);

  cr2 = cairo_create (surface);
  gsk_render_node_draw_ccs (self->child, cr2, ccs);
  cairo_destroy (cr2);

  blur_image_surface (surface, (int) ceil (0.5 * self->radius), 3);
  cairo_surface_mark_dirty (surface);

  cairo_set_source_surface (cr, surface, 0, 0);
  cairo_rectangle (cr,
                   node->bounds.origin.x, node->bounds.origin.y,
                   node->bounds.size.width, node->bounds.size.height);
  cairo_fill (cr);

  cairo_surface_destroy (surface);
}

static void
gsk_blur_node_diff (GskRenderNode *node1,
                    GskRenderNode *node2,
                    GskDiffData   *data)
{
  GskBlurNode *self1 = (GskBlurNode *) node1;
  GskBlurNode *self2 = (GskBlurNode *) node2;

  if (self1->radius == self2->radius)
    {
      cairo_rectangle_int_t rect;
      cairo_region_t *sub;
      int i, n, clip_radius;

      clip_radius = ceil (gsk_cairo_blur_compute_pixels (self1->radius / 2.0));
      sub = cairo_region_create ();
      gsk_render_node_diff (self1->child, self2->child, &(GskDiffData) { sub, data->surface });

      n = cairo_region_num_rectangles (sub);
      for (i = 0; i < n; i++)
        {
          cairo_region_get_rectangle (sub, i, &rect);
          rect.x -= clip_radius;
          rect.y -= clip_radius;
          rect.width += 2 * clip_radius;
          rect.height += 2 * clip_radius;
          cairo_region_union_rectangle (data->region, &rect);
        }
      cairo_region_destroy (sub);
    }
  else
    {
      gsk_render_node_diff_impossible (node1, node2, data);
    }
}

static void
gsk_blur_node_class_init (gpointer g_class,
                          gpointer class_data)
{
  GskRenderNodeClass *node_class = g_class;

  node_class->node_type = GSK_BLUR_NODE;

  node_class->finalize = gsk_blur_node_finalize;
  node_class->draw = gsk_blur_node_draw;
  node_class->diff = gsk_blur_node_diff;
}

/**
 * gsk_blur_node_new:
 * @child: the child node to blur
 * @radius: the blur radius. Must be positive
 *
 * Creates a render node that blurs the child.
 *
 * Returns: (transfer full) (type GskBlurNode): a new `GskRenderNode`
 */
GskRenderNode *
gsk_blur_node_new (GskRenderNode *child,
                   float          radius)
{
  GskBlurNode *self;
  GskRenderNode *node;
  float clip_radius;

  g_return_val_if_fail (GSK_IS_RENDER_NODE (child), NULL);
  g_return_val_if_fail (radius >= 0, NULL);

  self = gsk_render_node_alloc (GSK_BLUR_NODE);
  node = (GskRenderNode *) self;
  node->offscreen_for_opacity = child->offscreen_for_opacity;

  self->child = gsk_render_node_ref (child);
  self->radius = radius;

  clip_radius = gsk_cairo_blur_compute_pixels (radius / 2.0);

  gsk_rect_init_from_rect (&node->bounds, &child->bounds);
  graphene_rect_inset (&self->render_node.bounds, - clip_radius, - clip_radius);

  node->preferred_depth = gsk_render_node_get_preferred_depth (child);
  node->is_hdr = gsk_render_node_is_hdr (child);

  return node;
}

/**
 * gsk_blur_node_get_child:
 * @node: (type GskBlurNode): a blur `GskRenderNode`
 *
 * Retrieves the child `GskRenderNode` of the blur @node.
 *
 * Returns: (transfer none): the blurred child node
 */
GskRenderNode *
gsk_blur_node_get_child (const GskRenderNode *node)
{
  const GskBlurNode *self = (const GskBlurNode *) node;

  return self->child;
}

/**
 * gsk_blur_node_get_radius:
 * @node: (type GskBlurNode): a blur `GskRenderNode`
 *
 * Retrieves the blur radius of the @node.
 *
 * Returns: the blur radius
 */
float
gsk_blur_node_get_radius (const GskRenderNode *node)
{
  const GskBlurNode *self = (const GskBlurNode *) node;

  return self->radius;
}

/* }}} */
/* {{{ GSK_MASK_NODE */

/**
 * GskMaskNode:
 *
 * A render node masking one child node with another.
 *
 * Since: 4.10
 */
typedef struct _GskMaskNode GskMaskNode;

struct _GskMaskNode
{
  GskRenderNode render_node;

  GskRenderNode *mask;
  GskRenderNode *source;
  GskMaskMode mask_mode;
};

static void
gsk_mask_node_finalize (GskRenderNode *node)
{
  GskMaskNode *self = (GskMaskNode *) node;
  GskRenderNodeClass *parent_class = g_type_class_peek (g_type_parent (GSK_TYPE_MASK_NODE));

  gsk_render_node_unref (self->source);
  gsk_render_node_unref (self->mask);

  parent_class->finalize (node);
}

static void
apply_luminance_to_pattern (cairo_pattern_t *pattern,
                            gboolean         invert_luminance)
{
  cairo_surface_t *surface, *image_surface;
  guchar *data;
  gsize x, y, width, height, stride;
  guint red, green, blue, alpha, luminance;
  guint32* pixel_data;

  cairo_pattern_get_surface (pattern, &surface);
  image_surface = cairo_surface_map_to_image (surface, NULL);

  data = cairo_image_surface_get_data (image_surface);
  width = cairo_image_surface_get_width (image_surface);
  height = cairo_image_surface_get_height (image_surface);
  stride = cairo_image_surface_get_stride (image_surface);

  for (y = 0; y < height; y++)
    {
      pixel_data = (guint32 *) data;
      for (x = 0; x < width; x++)
        {
          alpha = (pixel_data[x] >> 24) & 0xFF;
          red   = (pixel_data[x] >> 16) & 0xFF;
          green = (pixel_data[x] >>  8) & 0xFF;
          blue  = (pixel_data[x] >>  0) & 0xFF;

          luminance = 2126 * red + 7152 * green + 722 * blue;
          if (invert_luminance)
            luminance = 10000 * alpha - luminance;
          luminance = (luminance + 5000) / 10000;

          pixel_data[x] = luminance * 0x1010101;
        }
      data += stride;
    }

  cairo_surface_mark_dirty (image_surface);
  cairo_surface_unmap_image (surface, image_surface);
  /* https://gitlab.freedesktop.org/cairo/cairo/-/merge_requests/487 */
  cairo_surface_mark_dirty (surface);
}

static void
gsk_mask_node_draw (GskRenderNode *node,
                    cairo_t       *cr,
                    GdkColorState *ccs)
{
  GskMaskNode *self = (GskMaskNode *) node;
  cairo_pattern_t *mask_pattern;
  graphene_matrix_t color_matrix;
  graphene_vec4_t color_offset;

  /* clip so the push_group() creates a smaller surface */
  gsk_cairo_rectangle_pixel_aligned (cr, &node->bounds);
  cairo_clip (cr);

  if (has_empty_clip (cr))
    return;

  cairo_push_group (cr);
  gsk_render_node_draw_ccs (self->source, cr, ccs);
  cairo_pop_group_to_source (cr);

  cairo_push_group (cr);
  gsk_render_node_draw_ccs (self->mask, cr, ccs);
  mask_pattern = cairo_pop_group (cr);

  switch (self->mask_mode)
    {
    case GSK_MASK_MODE_ALPHA:
      break;
    case GSK_MASK_MODE_INVERTED_ALPHA:
      graphene_matrix_init_from_float (&color_matrix, (float[]){  0,  0,  0,  0,
                                                                  0,  0,  0,  0,
                                                                  0,  0,  0,  0,
                                                                 -1, -1, -1, -1 });
      graphene_vec4_init (&color_offset, 1, 1, 1, 1);
      apply_color_matrix_to_pattern (mask_pattern, &color_matrix, &color_offset);
      break;
    case GSK_MASK_MODE_LUMINANCE:
      apply_luminance_to_pattern (mask_pattern, FALSE);
      break;
    case GSK_MASK_MODE_INVERTED_LUMINANCE:
      apply_luminance_to_pattern (mask_pattern, TRUE);
      break;
    default:
      g_assert_not_reached ();
    }

  cairo_mask (cr, mask_pattern);

  cairo_pattern_destroy (mask_pattern);
}

static void
gsk_mask_node_diff (GskRenderNode *node1,
                    GskRenderNode *node2,
                    GskDiffData   *data)
{
  GskMaskNode *self1 = (GskMaskNode *) node1;
  GskMaskNode *self2 = (GskMaskNode *) node2;

  if (self1->mask_mode != self2->mask_mode)
    {
      gsk_render_node_diff_impossible (node1, node2, data);
      return;
    }

  gsk_render_node_diff (self1->source, self2->source, data);
  gsk_render_node_diff (self1->mask, self2->mask, data);
}

static void
gsk_mask_node_class_init (gpointer g_class,
                          gpointer class_data)
{
  GskRenderNodeClass *node_class = g_class;

  node_class->node_type = GSK_MASK_NODE;

  node_class->finalize = gsk_mask_node_finalize;
  node_class->draw = gsk_mask_node_draw;
  node_class->diff = gsk_mask_node_diff;
}

/**
 * gsk_mask_node_new:
 * @source: The source node to be drawn
 * @mask: The node to be used as mask
 * @mask_mode: The mask mode to use
 *
 * Creates a `GskRenderNode` that will mask a given node by another.
 *
 * The @mask_mode determines how the 'mask values' are derived from
 * the colors of the @mask. Applying the mask consists of multiplying
 * the 'mask value' with the alpha of the source.
 *
 * Returns: (transfer full) (type GskMaskNode): A new `GskRenderNode`
 *
 * Since: 4.10
 */
GskRenderNode *
gsk_mask_node_new (GskRenderNode *source,
                   GskRenderNode *mask,
                   GskMaskMode    mask_mode)
{
  GskMaskNode *self;

  g_return_val_if_fail (GSK_IS_RENDER_NODE (source), NULL);
  g_return_val_if_fail (GSK_IS_RENDER_NODE (mask), NULL);

  self = gsk_render_node_alloc (GSK_MASK_NODE);
  self->source = gsk_render_node_ref (source);
  self->mask = gsk_render_node_ref (mask);
  self->mask_mode = mask_mode;

  if (mask_mode == GSK_MASK_MODE_INVERTED_ALPHA)
    self->render_node.bounds = source->bounds;
  else if (!gsk_rect_intersection (&source->bounds, &mask->bounds, &self->render_node.bounds))
    self->render_node.bounds = *graphene_rect_zero ();

  self->render_node.preferred_depth = gsk_render_node_get_preferred_depth (source);
  self->render_node.is_hdr = gsk_render_node_is_hdr (source) ||
                             gsk_render_node_is_hdr (mask);

  return &self->render_node;
}

/**
 * gsk_mask_node_get_source:
 * @node: (type GskMaskNode): a mask `GskRenderNode`
 *
 * Retrieves the source `GskRenderNode` child of the @node.
 *
 * Returns: (transfer none): the source child node
 *
 * Since: 4.10
 */
GskRenderNode *
gsk_mask_node_get_source (const GskRenderNode *node)
{
  const GskMaskNode *self = (const GskMaskNode *) node;

  g_return_val_if_fail (GSK_IS_RENDER_NODE_TYPE (node, GSK_MASK_NODE), NULL);

  return self->source;
}

/**
 * gsk_mask_node_get_mask:
 * @node: (type GskMaskNode): a mask `GskRenderNode`
 *
 * Retrieves the mask `GskRenderNode` child of the @node.
 *
 * Returns: (transfer none): the mask child node
 *
 * Since: 4.10
 */
GskRenderNode *
gsk_mask_node_get_mask (const GskRenderNode *node)
{
  const GskMaskNode *self = (const GskMaskNode *) node;

  g_return_val_if_fail (GSK_IS_RENDER_NODE_TYPE (node, GSK_MASK_NODE), NULL);

  return self->mask;
}

/**
 * gsk_mask_node_get_mask_mode:
 * @node: (type GskMaskNode): a blending `GskRenderNode`
 *
 * Retrieves the mask mode used by @node.
 *
 * Returns: the mask mode
 *
 * Since: 4.10
 */
GskMaskMode
gsk_mask_node_get_mask_mode (const GskRenderNode *node)
{
  const GskMaskNode *self = (const GskMaskNode *) node;

  return self->mask_mode;
}

/* }}} */
/* {{{ GSK_DEBUG_NODE */

/**
 * GskDebugNode:
 *
 * A render node that emits a debugging message when drawing its
 * child node.
 */
struct _GskDebugNode
{
  GskRenderNode render_node;

  GskRenderNode *child;
  char *message;
};

static void
gsk_debug_node_finalize (GskRenderNode *node)
{
  GskDebugNode *self = (GskDebugNode *) node;
  GskRenderNodeClass *parent_class = g_type_class_peek (g_type_parent (GSK_TYPE_DEBUG_NODE));

  gsk_render_node_unref (self->child);
  g_free (self->message);

  parent_class->finalize (node);
}

static void
gsk_debug_node_draw (GskRenderNode *node,
                     cairo_t       *cr,
                     GdkColorState *ccs)
{
  GskDebugNode *self = (GskDebugNode *) node;

  gsk_render_node_draw_ccs (self->child, cr, ccs);
}

static gboolean
gsk_debug_node_can_diff (const GskRenderNode *node1,
                         const GskRenderNode *node2)
{
  GskDebugNode *self1 = (GskDebugNode *) node1;
  GskDebugNode *self2 = (GskDebugNode *) node2;

  return gsk_render_node_can_diff (self1->child, self2->child);
}

static void
gsk_debug_node_diff (GskRenderNode *node1,
                     GskRenderNode *node2,
                     GskDiffData   *data)
{
  GskDebugNode *self1 = (GskDebugNode *) node1;
  GskDebugNode *self2 = (GskDebugNode *) node2;

  gsk_render_node_diff (self1->child, self2->child, data);
}

static gboolean
gsk_debug_node_get_opaque_rect (GskRenderNode   *node,
                                graphene_rect_t *out_opaque)
{
  GskDebugNode *self = (GskDebugNode *) node;

  return gsk_render_node_get_opaque_rect (self->child, out_opaque);
}

static void
gsk_debug_node_class_init (gpointer g_class,
                           gpointer class_data)
{
  GskRenderNodeClass *node_class = g_class;

  node_class->node_type = GSK_DEBUG_NODE;

  node_class->finalize = gsk_debug_node_finalize;
  node_class->draw = gsk_debug_node_draw;
  node_class->can_diff = gsk_debug_node_can_diff;
  node_class->diff = gsk_debug_node_diff;
  node_class->get_opaque_rect = gsk_debug_node_get_opaque_rect;
}

/**
 * gsk_debug_node_new:
 * @child: The child to add debug info for
 * @message: (transfer full): The debug message
 *
 * Creates a `GskRenderNode` that will add debug information about
 * the given @child.
 *
 * Adding this node has no visual effect.
 *
 * Returns: (transfer full) (type GskDebugNode): A new `GskRenderNode`
 */
GskRenderNode *
gsk_debug_node_new (GskRenderNode *child,
                    char          *message)
{
  GskDebugNode *self;
  GskRenderNode *node;

  g_return_val_if_fail (GSK_IS_RENDER_NODE (child), NULL);

  self = gsk_render_node_alloc (GSK_DEBUG_NODE);
  node = (GskRenderNode *) self;
  node->offscreen_for_opacity = child->offscreen_for_opacity;
  node->fully_opaque = child->fully_opaque;

  self->child = gsk_render_node_ref (child);
  self->message = message;

  gsk_rect_init_from_rect (&node->bounds, &child->bounds);

  node->preferred_depth = gsk_render_node_get_preferred_depth (child);
  self->render_node.is_hdr = gsk_render_node_is_hdr (child);

  return node;
}

/**
 * gsk_debug_node_get_child:
 * @node: (type GskDebugNode): a debug `GskRenderNode`
 *
 * Gets the child node that is getting drawn by the given @node.
 *
 * Returns: (transfer none): the child `GskRenderNode`
 **/
GskRenderNode *
gsk_debug_node_get_child (const GskRenderNode *node)
{
  const GskDebugNode *self = (const GskDebugNode *) node;

  return self->child;
}

/**
 * gsk_debug_node_get_message:
 * @node: (type GskDebugNode): a debug `GskRenderNode`
 *
 * Gets the debug message that was set on this node
 *
 * Returns: (transfer none): The debug message
 **/
const char *
gsk_debug_node_get_message (const GskRenderNode *node)
{
  const GskDebugNode *self = (const GskDebugNode *) node;

  return self->message;
}

/* }}} */
/* {{{ GSK_GL_SHADER_NODE */

G_GNUC_BEGIN_IGNORE_DEPRECATIONS
/**
 * GskGLShaderNode:
 *
 * A render node using a GL shader when drawing its children nodes.
 */
struct _GskGLShaderNode
{
  GskRenderNode render_node;

  GskGLShader *shader;
  GBytes *args;
  GskRenderNode **children;
  guint n_children;
};

static void
gsk_gl_shader_node_finalize (GskRenderNode *node)
{
  GskGLShaderNode *self = (GskGLShaderNode *) node;
  GskRenderNodeClass *parent_class = g_type_class_peek (g_type_parent (GSK_TYPE_GL_SHADER_NODE));

  for (guint i = 0; i < self->n_children; i++)
    gsk_render_node_unref (self->children[i]);
  g_free (self->children);

  g_bytes_unref (self->args);

  g_object_unref (self->shader);

  parent_class->finalize (node);
}

static void
gsk_gl_shader_node_draw (GskRenderNode *node,
                         cairo_t       *cr,
                         GdkColorState *ccs)
{
  GdkRGBA pink = { 255 / 255., 105 / 255., 180 / 255., 1.0 };

  gdk_cairo_set_source_rgba_ccs (cr, ccs, &pink);
  gdk_cairo_rect (cr, &node->bounds);
  cairo_fill (cr);
}

static void
gsk_gl_shader_node_diff (GskRenderNode *node1,
                         GskRenderNode *node2,
                         GskDiffData   *data)
{
  GskGLShaderNode *self1 = (GskGLShaderNode *) node1;
  GskGLShaderNode *self2 = (GskGLShaderNode *) node2;

  if (gsk_rect_equal (&node1->bounds, &node2->bounds) &&
      self1->shader == self2->shader &&
      g_bytes_compare (self1->args, self2->args) == 0 &&
      self1->n_children == self2->n_children)
    {
      cairo_region_t *child_region = cairo_region_create();
      for (guint i = 0; i < self1->n_children; i++)
        gsk_render_node_diff (self1->children[i], self2->children[i], &(GskDiffData) { child_region, data->surface });
      if (!cairo_region_is_empty (child_region))
        gsk_render_node_diff_impossible (node1, node2, data);
      cairo_region_destroy (child_region);
    }
  else
    {
      gsk_render_node_diff_impossible (node1, node2, data);
    }
}

static void
gsk_gl_shader_node_class_init (gpointer g_class,
                               gpointer class_data)
{
  GskRenderNodeClass *node_class = g_class;

  node_class->node_type = GSK_GL_SHADER_NODE;

  node_class->finalize = gsk_gl_shader_node_finalize;
  node_class->draw = gsk_gl_shader_node_draw;
  node_class->diff = gsk_gl_shader_node_diff;
}

/**
 * gsk_gl_shader_node_new:
 * @shader: the `GskGLShader`
 * @bounds: the rectangle to render the shader into
 * @args: Arguments for the uniforms
 * @children: (nullable) (array length=n_children): array of child nodes,
 *   these will be rendered to textures and used as input.
 * @n_children: Length of @children (currently the GL backend supports
 *   up to 4 children)
 *
 * Creates a `GskRenderNode` that will render the given @shader into the
 * area given by @bounds.
 *
 * The @args is a block of data to use for uniform input, as per types and
 * offsets defined by the @shader. Normally this is generated by
 * [method@Gsk.GLShader.format_args] or [struct@Gsk.ShaderArgsBuilder].
 *
 * See [class@Gsk.GLShader] for details about how the shader should be written.
 *
 * All the children will be rendered into textures (if they aren't already
 * `GskTextureNodes`, which will be used directly). These textures will be
 * sent as input to the shader.
 *
 * If the renderer doesn't support GL shaders, or if there is any problem
 * when compiling the shader, then the node will draw pink. You should use
 * [method@Gsk.GLShader.compile] to ensure the @shader will work for the
 * renderer before using it.
 *
 * Returns: (transfer full) (type GskGLShaderNode): A new `GskRenderNode`
 *
 * Deprecated: 4.16: GTK's new Vulkan-focused rendering
 *   does not support this feature. Use [GtkGLArea](../gtk4/class.GLArea.html)
 *   for OpenGL rendering.
 */
GskRenderNode *
gsk_gl_shader_node_new (GskGLShader           *shader,
                        const graphene_rect_t *bounds,
                        GBytes                *args,
                        GskRenderNode        **children,
                        guint                  n_children)
{
  GskGLShaderNode *self;
  GskRenderNode *node;

  g_return_val_if_fail (GSK_IS_GL_SHADER (shader), NULL);
  g_return_val_if_fail (bounds != NULL, NULL);
  g_return_val_if_fail (args != NULL, NULL);
  g_return_val_if_fail (g_bytes_get_size (args) == gsk_gl_shader_get_args_size (shader), NULL);
  g_return_val_if_fail ((children == NULL && n_children == 0) ||
                        (n_children == gsk_gl_shader_get_n_textures (shader)), NULL);

  self = gsk_render_node_alloc (GSK_GL_SHADER_NODE);
  node = (GskRenderNode *) self;
  node->offscreen_for_opacity = TRUE;
  node->preferred_depth = gdk_color_state_get_depth (GDK_COLOR_STATE_SRGB);

  gsk_rect_init_from_rect (&node->bounds, bounds);
  gsk_rect_normalize (&node->bounds);

  self->shader = g_object_ref (shader);

  self->args = g_bytes_ref (args);

  self->n_children = n_children;
  if (n_children > 0)
    {
      self->children = g_malloc_n (n_children, sizeof (GskRenderNode *));
      for (guint i = 0; i < n_children; i++)
        {
          self->children[i] = gsk_render_node_ref (children[i]);
          node->preferred_depth = gdk_memory_depth_merge (node->preferred_depth,
                                                          gsk_render_node_get_preferred_depth (children[i]));
        }
    }

  return node;
}

/**
 * gsk_gl_shader_node_get_n_children:
 * @node: (type GskGLShaderNode): a `GskRenderNode` for a gl shader
 *
 * Returns the number of children
 *
 * Returns: The number of children
 *
 * Deprecated: 4.16: GTK's new Vulkan-focused rendering
 *   does not support this feature. Use [GtkGLArea](../gtk4/class.GLArea.html)
 *   for OpenGL rendering.
 */
guint
gsk_gl_shader_node_get_n_children (const GskRenderNode *node)
{
  const GskGLShaderNode *self = (const GskGLShaderNode *) node;

  return self->n_children;
}

/**
 * gsk_gl_shader_node_get_child:
 * @node: (type GskGLShaderNode): a `GskRenderNode` for a gl shader
 * @idx: the position of the child to get
 *
 * Gets one of the children.
 *
 * Returns: (transfer none): the @idx'th child of @node
 *
 * Deprecated: 4.16: GTK's new Vulkan-focused rendering
 *   does not support this feature. Use [GtkGLArea](../gtk4/class.GLArea.html)
 *   for OpenGL rendering.
 */
GskRenderNode *
gsk_gl_shader_node_get_child (const GskRenderNode *node,
                              guint                idx)
{
  const GskGLShaderNode *self = (const GskGLShaderNode *) node;

  return self->children[idx];
}

/**
 * gsk_gl_shader_node_get_shader:
 * @node: (type GskGLShaderNode): a `GskRenderNode` for a gl shader
 *
 * Gets shader code for the node.
 *
 * Returns: (transfer none): the `GskGLShader` shader
 */
GskGLShader *
gsk_gl_shader_node_get_shader (const GskRenderNode *node)
{
  const GskGLShaderNode *self = (const GskGLShaderNode *) node;

  return self->shader;
}

/**
 * gsk_gl_shader_node_get_args:
 * @node: (type GskGLShaderNode): a `GskRenderNode` for a gl shader
 *
 * Gets args for the node.
 *
 * Returns: (transfer none): A `GBytes` with the uniform arguments
 *
 * Deprecated: 4.16: GTK's new Vulkan-focused rendering
 *   does not support this feature. Use [GtkGLArea](../gtk4/class.GLArea.html)
 *   for OpenGL rendering.
 */
GBytes *
gsk_gl_shader_node_get_args (const GskRenderNode *node)
{
  const GskGLShaderNode *self = (const GskGLShaderNode *) node;

  return self->args;
}
G_GNUC_END_IGNORE_DEPRECATIONS

/* }}} */
/* {{{ GSK_SUBSURFACE_NODE */

/**
 * GskSubsurfaceNode:
 *
 * A render node that potentially diverts a part of the scene graph to a subsurface.
 *
 * Since: 4.14
 */
struct _GskSubsurfaceNode
{
  GskRenderNode render_node;

  GskRenderNode *child;
  GdkSubsurface *subsurface;
};

static void
gsk_subsurface_node_finalize (GskRenderNode *node)
{
  GskSubsurfaceNode *self = (GskSubsurfaceNode *) node;
  GskRenderNodeClass *parent_class = g_type_class_peek (g_type_parent (GSK_TYPE_SUBSURFACE_NODE));

  gsk_render_node_unref (self->child);
  g_clear_object (&self->subsurface);

  parent_class->finalize (node);
}

static void
gsk_subsurface_node_draw (GskRenderNode *node,
                          cairo_t       *cr,
                          GdkColorState *ccs)
{
  GskSubsurfaceNode *self = (GskSubsurfaceNode *) node;

  gsk_render_node_draw_ccs (self->child, cr, ccs);
}

static gboolean
gsk_subsurface_node_can_diff (const GskRenderNode *node1,
                              const GskRenderNode *node2)
{
  GskSubsurfaceNode *self1 = (GskSubsurfaceNode *) node1;
  GskSubsurfaceNode *self2 = (GskSubsurfaceNode *) node2;

  return self1->subsurface == self2->subsurface;
}

static void
gsk_subsurface_node_diff (GskRenderNode *node1,
                          GskRenderNode *node2,
                          GskDiffData   *data)
{
  GskSubsurfaceNode *self1 = (GskSubsurfaceNode *) node1;
  GskSubsurfaceNode *self2 = (GskSubsurfaceNode *) node2;

  if (self1->subsurface != self2->subsurface)
    {
      /* Shouldn't happen, can_diff() avoids this, but to be sure */
      gsk_render_node_diff_impossible (node1, node2, data);
    }
  else if (self1->subsurface && self1->subsurface->parent != data->surface)
    {
      /* The inspector case */
      gsk_render_node_diff (self1->child, self2->child, data);
    }
  else if (self1->subsurface && gdk_subsurface_get_texture (self1->subsurface) != NULL)
    {
      /* offloaded, no contents to compare */
    }
  else
    {
      /* not offloaded, diff the children */
      gsk_render_node_diff (self1->child, self2->child, data);
    }
}

static gboolean
gsk_subsurface_node_get_opaque_rect (GskRenderNode   *node,
                                     graphene_rect_t *out_opaque)
{
  GskSubsurfaceNode *self = (GskSubsurfaceNode *) node;

  return gsk_render_node_get_opaque_rect (self->child, out_opaque);
}

static void
gsk_subsurface_node_class_init (gpointer g_class,
                                gpointer class_data)
{
  GskRenderNodeClass *node_class = g_class;

  node_class->node_type = GSK_SUBSURFACE_NODE;

  node_class->finalize = gsk_subsurface_node_finalize;
  node_class->draw = gsk_subsurface_node_draw;
  node_class->can_diff = gsk_subsurface_node_can_diff;
  node_class->diff = gsk_subsurface_node_diff;
  node_class->get_opaque_rect = gsk_subsurface_node_get_opaque_rect;
}

/**
 * gsk_subsurface_node_new: (skip)
 * @child: The child to divert to a subsurface
 * @subsurface: (nullable): the subsurface to use
 *
 * Creates a `GskRenderNode` that will possibly divert the child
 * node to a subsurface.
 *
 * Note: Since subsurfaces are currently private, these nodes cannot
 * currently be created outside of GTK. See
 * [GtkGraphicsOffload](../gtk4/class.GraphicsOffload.html).
 *
 * Returns: (transfer full) (type GskSubsurfaceNode): A new `GskRenderNode`
 *
 * Since: 4.14
 */
GskRenderNode *
gsk_subsurface_node_new (GskRenderNode *child,
                         gpointer       subsurface)
{
  GskSubsurfaceNode *self;
  GskRenderNode *node;

  g_return_val_if_fail (GSK_IS_RENDER_NODE (child), NULL);

  self = gsk_render_node_alloc (GSK_SUBSURFACE_NODE);
  node = (GskRenderNode *) self;
  node->offscreen_for_opacity = child->offscreen_for_opacity;
  node->fully_opaque = child->fully_opaque;

  self->child = gsk_render_node_ref (child);
  if (subsurface)
    self->subsurface = g_object_ref (subsurface);
  else
    self->subsurface = NULL;

  gsk_rect_init_from_rect (&node->bounds, &child->bounds);

  node->preferred_depth = gsk_render_node_get_preferred_depth (child);
  node->is_hdr = gsk_render_node_is_hdr (child);

  return node;
}

/**
 * gsk_subsurface_node_get_child:
 * @node: (type GskSubsurfaceNode): a debug `GskRenderNode`
 *
 * Gets the child node that is getting drawn by the given @node.
 *
 * Returns: (transfer none): the child `GskRenderNode`
 *
 * Since: 4.14
 */
GskRenderNode *
gsk_subsurface_node_get_child (const GskRenderNode *node)
{
  const GskSubsurfaceNode *self = (const GskSubsurfaceNode *) node;

  return self->child;
}

/**
 * gsk_subsurface_node_get_subsurface: (skip)
 * @node: (type GskDebugNode): a debug `GskRenderNode`
 *
 * Gets the subsurface that was set on this node
 *
 * Returns: (transfer none) (nullable): the subsurface
 *
 * Since: 4.14
 */
gpointer
gsk_subsurface_node_get_subsurface (const GskRenderNode *node)
{
  const GskSubsurfaceNode *self = (const GskSubsurfaceNode *) node;

  return self->subsurface;
}

/* }}} */
/* {{{ GSK_COMPONENT_TRANSFER_NODE */

struct _GskComponentTransferNode
{
  GskRenderNode render_node;

  GskRenderNode *child;
  GskComponentTransfer transfer[4];
};

static void
gsk_component_transfer_node_finalize (GskRenderNode *node)
{
  GskComponentTransferNode *self = (GskComponentTransferNode *) node;
  GskRenderNodeClass *parent_class = g_type_class_peek (g_type_parent (GSK_TYPE_COMPONENT_TRANSFER_NODE));

  gsk_render_node_unref (self->child);

  parent_class->finalize (node);
}

static void
gsk_component_transfer_node_draw (GskRenderNode *node,
                                  cairo_t       *cr,
                                  GdkColorState *ccs)
{
  GskComponentTransferNode *self = (GskComponentTransferNode *) node;
  int width, height;
  cairo_surface_t *surface;
  cairo_t *cr2;
  guchar *data;
  gsize stride;
  guint32 pixel;
  float r, g, b, a;
  cairo_pattern_t *pattern;

  width = ceil (node->bounds.size.width);
  height = ceil (node->bounds.size.height);

  surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width, height);

  cr2 = cairo_create (surface);
  gsk_render_node_draw (self->child, cr2);
  cairo_destroy (cr2);

  data = cairo_image_surface_get_data (surface);
  stride = cairo_image_surface_get_stride (surface);

  for (guint y = 0; y < height; y++)
    {
      for (guint x = 0; x < width; x++)
        {
          pixel = *(guint32 *)(data + y * stride + 4 * x);

          a = ((pixel >> 24) & 0xff) / 255.;
          r = ((pixel >> 16) & 0xff) / 255.;
          g = ((pixel >> 8) & 0xff) / 255.;
          b = ((pixel >> 0) & 0xff) / 255.;

          r /= a;
          g /= a;
          b /= a;

          r = gsk_component_transfer_apply (&self->transfer[0], r);
          g = gsk_component_transfer_apply (&self->transfer[1], g);
          b = gsk_component_transfer_apply (&self->transfer[2], b);
          a = gsk_component_transfer_apply (&self->transfer[3], a);

          r *= a;
          g *= a;
          b *= a;

          pixel = CLAMP ((int) roundf (a * 255), 0, 255) << 24 |
                  CLAMP ((int) roundf (r * 255), 0, 255) << 16 |
                  CLAMP ((int) roundf (g * 255), 0, 255) << 8 |
                  CLAMP ((int) roundf (b * 255), 0, 255) << 0;

          *(guint32 *)(data + y * stride + 4 * x) = pixel;
        }
    }

  pattern = cairo_pattern_create_for_surface (surface);
  cairo_pattern_set_extend (pattern, CAIRO_EXTEND_PAD);

  cairo_set_source (cr, pattern);
  cairo_pattern_destroy (pattern);
  cairo_surface_destroy (surface);

  gdk_cairo_rect (cr, &node->bounds);
  cairo_fill (cr);
}

static gboolean
gsk_component_transfer_node_can_diff (const GskRenderNode *node1,
                                      const GskRenderNode *node2)
{
  GskComponentTransferNode *self1 = (GskComponentTransferNode *) node1;
  GskComponentTransferNode *self2 = (GskComponentTransferNode *) node2;

  return gsk_component_transfer_equal (&self1->transfer[0], &self2->transfer[0]) &&
         gsk_component_transfer_equal (&self1->transfer[1], &self2->transfer[1]) &&
         gsk_component_transfer_equal (&self1->transfer[2], &self2->transfer[2]) &&
         gsk_component_transfer_equal (&self1->transfer[3], &self2->transfer[3]);
}

static void
gsk_component_transfer_node_diff (GskRenderNode *node1,
                                  GskRenderNode *node2,
                                  GskDiffData   *data)
{
  GskComponentTransferNode *self1 = (GskComponentTransferNode *) node1;
  GskComponentTransferNode *self2 = (GskComponentTransferNode *) node2;

  if (gsk_component_transfer_node_can_diff (node1, node2))
    gsk_render_node_diff (self1->child, self2->child, data);

  gsk_render_node_diff_impossible (node1, node2, data);
}

static void
gsk_component_transfer_node_class_init (gpointer g_class,
                                        gpointer class_data)
{
  GskRenderNodeClass *node_class = g_class;

  node_class->node_type = GSK_COMPONENT_TRANSFER_NODE;

  node_class->finalize = gsk_component_transfer_node_finalize;
  node_class->draw = gsk_component_transfer_node_draw;
  node_class->can_diff = gsk_component_transfer_node_can_diff;
  node_class->diff = gsk_component_transfer_node_diff;
}

/**
 * gsk_component_transfer_node_new:
 * @child: The child to apply component transfers to
 * @r: transfer for the red component
 * @g: transfer for the green component
 * @b: transfer for the blue component
 * @a: transfer for the alpha component
 *
 * Creates a render node that will apply component
 * transfers to a child node.
 *
 * Returns: (transfer full) (type GskComponentTransferNode): A new `GskRenderNode`
 *
 * Since: 4.20
 */
GskRenderNode *
gsk_component_transfer_node_new (GskRenderNode              *child,
                                 const GskComponentTransfer *r,
                                 const GskComponentTransfer *g,
                                 const GskComponentTransfer *b,
                                 const GskComponentTransfer *a)
{
  GskComponentTransferNode *self;
  GskRenderNode *node;

  g_return_val_if_fail (GSK_IS_RENDER_NODE (child), NULL);

  self = gsk_render_node_alloc (GSK_COMPONENT_TRANSFER_NODE);
  node = (GskRenderNode *) self;
  node->offscreen_for_opacity = child->offscreen_for_opacity;
  node->fully_opaque = FALSE;

  self->child = gsk_render_node_ref (child);

  gsk_component_transfer_init_copy (&self->transfer[0], r);
  gsk_component_transfer_init_copy (&self->transfer[1], g);
  gsk_component_transfer_init_copy (&self->transfer[2], b);
  gsk_component_transfer_init_copy (&self->transfer[3], a);

  gsk_rect_init_from_rect (&node->bounds, &child->bounds);

  node->preferred_depth = gsk_render_node_get_preferred_depth (child);
  node->is_hdr = gsk_render_node_is_hdr (child);

  return node;
}

/**
 * gsk_component_transfer_node_get_child:
 * @node: (type GskComponentTransferNode): a debug `GskRenderNode`
 *
 * Gets the child node that is getting drawn by the given @node.
 *
 * Returns: (transfer none): the child `GskRenderNode`
 *
 * Since: 4.20
 */
GskRenderNode *
gsk_component_transfer_node_get_child (const GskRenderNode *node)
{
  const GskComponentTransferNode *self = (const GskComponentTransferNode *) node;

  return self->child;
}

/**
 * gsk_component_transfer_node_get_transfer:
 * @node: (type GskComponentTransferNode): a debug `GskRenderNode`
 * @component: a value between 0 and 3 to indicate the red, green, blue
 *   or alpha component
 *
 * Gets the component transfer for one of the components.
 *
 * Returns: (transfer none): the `GskComponentTransfer`
 *
 * Since: 4.20
 */
const GskComponentTransfer *
gsk_component_transfer_node_get_transfer (const GskRenderNode *node,
                                          guint                component)
{
  const GskComponentTransferNode *self = (const GskComponentTransferNode *) node;

  g_return_val_if_fail (component < 4, NULL);

  return &self->transfer[component];
}

/* }}} */
/* {{{ Serialization */

static void
gsk_render_node_serialize_bytes_finish (GObject      *source,
                                        GAsyncResult *result,
                                        gpointer      serializer)
{
  GOutputStream *stream = G_OUTPUT_STREAM (source);
  GError *error = NULL;

  if (g_output_stream_splice_finish (stream, result, &error) < 0)
    gdk_content_serializer_return_error (serializer, error);
  else
    gdk_content_serializer_return_success (serializer);
}

static void
gsk_render_node_serialize_bytes (GdkContentSerializer *serializer,
                                 GBytes               *bytes)
{
  GInputStream *input;

  input = g_memory_input_stream_new_from_bytes (bytes);

  g_output_stream_splice_async (gdk_content_serializer_get_output_stream (serializer),
                                input,
                                G_OUTPUT_STREAM_SPLICE_CLOSE_SOURCE,
                                gdk_content_serializer_get_priority (serializer),
                                gdk_content_serializer_get_cancellable (serializer),
                                gsk_render_node_serialize_bytes_finish,
                                serializer);
  g_object_unref (input);
  g_bytes_unref (bytes);
}

#ifdef CAIRO_HAS_SVG_SURFACE
static cairo_status_t
gsk_render_node_cairo_serializer_write (gpointer             user_data,
                                        const unsigned char *data,
                                        unsigned int         length)
{
  g_byte_array_append (user_data, data, length);

  return CAIRO_STATUS_SUCCESS;
}

static void
gsk_render_node_svg_serializer (GdkContentSerializer *serializer)
{
  GskRenderNode *node;
  cairo_surface_t *surface;
  cairo_t *cr;
  graphene_rect_t bounds;
  GByteArray *array;

  node = gsk_value_get_render_node (gdk_content_serializer_get_value (serializer));
  gsk_render_node_get_bounds (node, &bounds);
  array = g_byte_array_new ();

  surface = cairo_svg_surface_create_for_stream (gsk_render_node_cairo_serializer_write,
                                                 array,
                                                 bounds.size.width,
                                                 bounds.size.height);
  cairo_svg_surface_set_document_unit (surface, CAIRO_SVG_UNIT_PX);
  cairo_surface_set_device_offset (surface, -bounds.origin.x, -bounds.origin.y);

  cr = cairo_create (surface);
  gsk_render_node_draw (node, cr);
  cairo_destroy (cr);

  cairo_surface_finish (surface);
  if (cairo_surface_status (surface) == CAIRO_STATUS_SUCCESS)
    {
      gsk_render_node_serialize_bytes (serializer, g_byte_array_free_to_bytes (array));
    }
  else
    {
      GError *error = g_error_new_literal (G_IO_ERROR, G_IO_ERROR_FAILED,
                                           cairo_status_to_string (cairo_surface_status (surface)));
      gdk_content_serializer_return_error (serializer, error);
      g_byte_array_unref (array);
    }
  cairo_surface_destroy (surface);
}
#endif

static void
gsk_render_node_png_serializer (GdkContentSerializer *serializer)
{
  GskRenderNode *node;
  GdkTexture *texture;
  GskRenderer *renderer;
  GBytes *bytes;

  node = gsk_value_get_render_node (gdk_content_serializer_get_value (serializer));

  renderer = gsk_gl_renderer_new ();
  if (!gsk_renderer_realize (renderer, NULL, NULL))
    {
      g_object_unref (renderer);
      renderer = gsk_cairo_renderer_new ();
      if (!gsk_renderer_realize (renderer, NULL, NULL))
        {
          g_assert_not_reached ();
        }
    }
  texture = gsk_renderer_render_texture (renderer, node, NULL);
  gsk_renderer_unrealize (renderer);
  g_object_unref (renderer);

  bytes = gdk_texture_save_to_png_bytes (texture);
  g_object_unref (texture);

  gsk_render_node_serialize_bytes (serializer, bytes);
}

static void
gsk_render_node_content_serializer (GdkContentSerializer *serializer)
{
  const GValue *value;
  GskRenderNode *node;
  GBytes *bytes;

  value = gdk_content_serializer_get_value (serializer);
  node = gsk_value_get_render_node (value);
  bytes = gsk_render_node_serialize (node);

  gsk_render_node_serialize_bytes (serializer, bytes);
}

static void
gsk_render_node_content_deserializer_finish (GObject      *source,
                                             GAsyncResult *result,
                                             gpointer      deserializer)
{
  GOutputStream *stream = G_OUTPUT_STREAM (source);
  GError *error = NULL;
  gssize written;
  GValue *value;
  GskRenderNode *node;
  GBytes *bytes;

  written = g_output_stream_splice_finish (stream, result, &error);
  if (written < 0)
    {
      gdk_content_deserializer_return_error (deserializer, error);
      return;
    }

  bytes = g_memory_output_stream_steal_as_bytes (G_MEMORY_OUTPUT_STREAM (stream));

  /* For now, we ignore any parsing errors. We might want to revisit that if it turns
   * out copy/paste leads to too many errors */
  node = gsk_render_node_deserialize (bytes, NULL, NULL);

  value = gdk_content_deserializer_get_value (deserializer);
  gsk_value_take_render_node (value, node);

  gdk_content_deserializer_return_success (deserializer);
}

static void
gsk_render_node_content_deserializer (GdkContentDeserializer *deserializer)
{
  GOutputStream *output;

  output = g_memory_output_stream_new_resizable ();

  g_output_stream_splice_async (output,
                                gdk_content_deserializer_get_input_stream (deserializer),
                                G_OUTPUT_STREAM_SPLICE_CLOSE_SOURCE | G_OUTPUT_STREAM_SPLICE_CLOSE_TARGET,
                                gdk_content_deserializer_get_priority (deserializer),
                                gdk_content_deserializer_get_cancellable (deserializer),
                                gsk_render_node_content_deserializer_finish,
                                deserializer);
  g_object_unref (output);
}

static void
gsk_render_node_init_content_serializers (void)
{
  gdk_content_register_serializer (GSK_TYPE_RENDER_NODE,
                                   "application/x-gtk-render-node",
                                   gsk_render_node_content_serializer,
                                   NULL,
                                   NULL);
  gdk_content_register_serializer (GSK_TYPE_RENDER_NODE,
                                   "text/plain;charset=utf-8",
                                   gsk_render_node_content_serializer,
                                   NULL,
                                   NULL);
  /* The serialization format only outputs ASCII, so we can do this */
  gdk_content_register_serializer (GSK_TYPE_RENDER_NODE,
                                   "text/plain",
                                   gsk_render_node_content_serializer,
                                   NULL,
                                   NULL);
#ifdef CAIRO_HAS_SVG_SURFACE
  gdk_content_register_serializer (GSK_TYPE_RENDER_NODE,
                                   "image/svg+xml",
                                   gsk_render_node_svg_serializer,
                                   NULL,
                                   NULL);
#endif
  gdk_content_register_serializer (GSK_TYPE_RENDER_NODE,
                                   "image/png",
                                   gsk_render_node_png_serializer,
                                   NULL,
                                   NULL);

  gdk_content_register_deserializer ("application/x-gtk-render-node",
                                     GSK_TYPE_RENDER_NODE,
                                     gsk_render_node_content_deserializer,
                                     NULL,
                                     NULL);
}

/* }}} */
/* {{{ Type registration */

GType gsk_render_node_types[GSK_RENDER_NODE_TYPE_N_TYPES];

#ifndef I_
# define I_(str) g_intern_static_string ((str))
#endif

#define GSK_DEFINE_RENDER_NODE_TYPE(type_name, TYPE_ENUM_VALUE) \
GType \
type_name ## _get_type (void) { \
  gsk_render_node_init_types (); \
  g_assert (gsk_render_node_types[TYPE_ENUM_VALUE] != G_TYPE_INVALID); \
  return gsk_render_node_types[TYPE_ENUM_VALUE]; \
}

GSK_DEFINE_RENDER_NODE_TYPE (gsk_container_node, GSK_CONTAINER_NODE)
GSK_DEFINE_RENDER_NODE_TYPE (gsk_cairo_node, GSK_CAIRO_NODE)
GSK_DEFINE_RENDER_NODE_TYPE (gsk_color_node, GSK_COLOR_NODE)
GSK_DEFINE_RENDER_NODE_TYPE (gsk_linear_gradient_node, GSK_LINEAR_GRADIENT_NODE)
GSK_DEFINE_RENDER_NODE_TYPE (gsk_repeating_linear_gradient_node, GSK_REPEATING_LINEAR_GRADIENT_NODE)
GSK_DEFINE_RENDER_NODE_TYPE (gsk_radial_gradient_node, GSK_RADIAL_GRADIENT_NODE)
GSK_DEFINE_RENDER_NODE_TYPE (gsk_repeating_radial_gradient_node, GSK_REPEATING_RADIAL_GRADIENT_NODE)
GSK_DEFINE_RENDER_NODE_TYPE (gsk_conic_gradient_node, GSK_CONIC_GRADIENT_NODE)
GSK_DEFINE_RENDER_NODE_TYPE (gsk_border_node, GSK_BORDER_NODE)
GSK_DEFINE_RENDER_NODE_TYPE (gsk_texture_node, GSK_TEXTURE_NODE)
GSK_DEFINE_RENDER_NODE_TYPE (gsk_texture_scale_node, GSK_TEXTURE_SCALE_NODE)
GSK_DEFINE_RENDER_NODE_TYPE (gsk_inset_shadow_node, GSK_INSET_SHADOW_NODE)
GSK_DEFINE_RENDER_NODE_TYPE (gsk_outset_shadow_node, GSK_OUTSET_SHADOW_NODE)
GSK_DEFINE_RENDER_NODE_TYPE (gsk_transform_node, GSK_TRANSFORM_NODE)
GSK_DEFINE_RENDER_NODE_TYPE (gsk_opacity_node, GSK_OPACITY_NODE)
GSK_DEFINE_RENDER_NODE_TYPE (gsk_color_matrix_node, GSK_COLOR_MATRIX_NODE)
GSK_DEFINE_RENDER_NODE_TYPE (gsk_repeat_node, GSK_REPEAT_NODE)
GSK_DEFINE_RENDER_NODE_TYPE (gsk_clip_node, GSK_CLIP_NODE)
GSK_DEFINE_RENDER_NODE_TYPE (gsk_rounded_clip_node, GSK_ROUNDED_CLIP_NODE)
GSK_DEFINE_RENDER_NODE_TYPE (gsk_fill_node, GSK_FILL_NODE)
GSK_DEFINE_RENDER_NODE_TYPE (gsk_stroke_node, GSK_STROKE_NODE)
GSK_DEFINE_RENDER_NODE_TYPE (gsk_shadow_node, GSK_SHADOW_NODE)
GSK_DEFINE_RENDER_NODE_TYPE (gsk_blend_node, GSK_BLEND_NODE)
GSK_DEFINE_RENDER_NODE_TYPE (gsk_cross_fade_node, GSK_CROSS_FADE_NODE)
GSK_DEFINE_RENDER_NODE_TYPE (gsk_text_node, GSK_TEXT_NODE)
GSK_DEFINE_RENDER_NODE_TYPE (gsk_blur_node, GSK_BLUR_NODE)
GSK_DEFINE_RENDER_NODE_TYPE (gsk_mask_node, GSK_MASK_NODE)
GSK_DEFINE_RENDER_NODE_TYPE (gsk_gl_shader_node, GSK_GL_SHADER_NODE)
GSK_DEFINE_RENDER_NODE_TYPE (gsk_debug_node, GSK_DEBUG_NODE)
GSK_DEFINE_RENDER_NODE_TYPE (gsk_subsurface_node, GSK_SUBSURFACE_NODE)
GSK_DEFINE_RENDER_NODE_TYPE (gsk_component_transfer_node, GSK_COMPONENT_TRANSFER_NODE)

#define GSK_INIT_NODE_TYPE_WITH_STRUCT(TYPE, T_N, S_N, t_n) \
  gsk_render_node_types[TYPE] = gsk_render_node_type_register_static (I_(#T_N), \
                                                                      sizeof (S_N), \
                                                                      t_n ## _class_init);

#define GSK_INIT_NODE_TYPE(TYPE, T_N, t_n) GSK_INIT_NODE_TYPE_WITH_STRUCT (TYPE, T_N, T_N, t_n)

static void
gsk_render_node_init_types_once (void)
{
  GSK_INIT_NODE_TYPE (GSK_CONTAINER_NODE, GskContainerNode, gsk_container_node)
  GSK_INIT_NODE_TYPE (GSK_CAIRO_NODE, GskCairoNode, gsk_cairo_node)
  GSK_INIT_NODE_TYPE (GSK_COLOR_NODE, GskColorNode, gsk_color_node)
  GSK_INIT_NODE_TYPE (GSK_LINEAR_GRADIENT_NODE, GskLinearGradientNode, gsk_linear_gradient_node)
  GSK_INIT_NODE_TYPE_WITH_STRUCT (GSK_REPEATING_LINEAR_GRADIENT_NODE, GskRepeatingLinearGradientNode, GskLinearGradientNode, gsk_repeating_linear_gradient_node)
  GSK_INIT_NODE_TYPE (GSK_RADIAL_GRADIENT_NODE, GskRadialGradientNode, gsk_radial_gradient_node)
  GSK_INIT_NODE_TYPE_WITH_STRUCT (GSK_REPEATING_RADIAL_GRADIENT_NODE, GskRepeatingRadialGradientNode, GskRadialGradientNode, gsk_repeating_radial_gradient_node)
  GSK_INIT_NODE_TYPE (GSK_CONIC_GRADIENT_NODE, GskConicGradientNode, gsk_conic_gradient_node)
  GSK_INIT_NODE_TYPE (GSK_BORDER_NODE, GskBorderNode, gsk_border_node)
  GSK_INIT_NODE_TYPE (GSK_TEXTURE_NODE, GskTextureNode, gsk_texture_node)
  GSK_INIT_NODE_TYPE (GSK_TEXTURE_SCALE_NODE, GskTextureScaleNode, gsk_texture_scale_node)
  GSK_INIT_NODE_TYPE (GSK_INSET_SHADOW_NODE, GskInsetShadowNode, gsk_inset_shadow_node)
  GSK_INIT_NODE_TYPE (GSK_OUTSET_SHADOW_NODE, GskOutsetShadowNode, gsk_outset_shadow_node)
  GSK_INIT_NODE_TYPE (GSK_TRANSFORM_NODE, GskTransformNode, gsk_transform_node)
  GSK_INIT_NODE_TYPE (GSK_OPACITY_NODE, GskOpacityNode, gsk_opacity_node)
  GSK_INIT_NODE_TYPE (GSK_COLOR_MATRIX_NODE, GskColorMatrixNode, gsk_color_matrix_node)
  GSK_INIT_NODE_TYPE (GSK_REPEAT_NODE, GskRepeatNode, gsk_repeat_node)
  GSK_INIT_NODE_TYPE (GSK_CLIP_NODE, GskClipNode, gsk_clip_node)
  GSK_INIT_NODE_TYPE (GSK_ROUNDED_CLIP_NODE, GskRoundedClipNode, gsk_rounded_clip_node)
  GSK_INIT_NODE_TYPE (GSK_SHADOW_NODE, GskShadowNode, gsk_shadow_node)
  GSK_INIT_NODE_TYPE (GSK_BLEND_NODE, GskBlendNode, gsk_blend_node)
  GSK_INIT_NODE_TYPE (GSK_CROSS_FADE_NODE, GskCrossFadeNode, gsk_cross_fade_node)
  GSK_INIT_NODE_TYPE (GSK_TEXT_NODE, GskTextNode, gsk_text_node)
  GSK_INIT_NODE_TYPE (GSK_BLUR_NODE, GskBlurNode, gsk_blur_node)
  GSK_INIT_NODE_TYPE (GSK_MASK_NODE, GskMaskNode, gsk_mask_node)
G_GNUC_BEGIN_IGNORE_DEPRECATIONS
  GSK_INIT_NODE_TYPE (GSK_GL_SHADER_NODE, GskGLShaderNode, gsk_gl_shader_node)
G_GNUC_END_IGNORE_DEPRECATIONS
  GSK_INIT_NODE_TYPE (GSK_DEBUG_NODE, GskDebugNode, gsk_debug_node)
  GSK_INIT_NODE_TYPE (GSK_FILL_NODE, GskFillNode, gsk_fill_node)
  GSK_INIT_NODE_TYPE (GSK_STROKE_NODE, GskStrokeNode, gsk_stroke_node)
  GSK_INIT_NODE_TYPE (GSK_SUBSURFACE_NODE, GskSubsurfaceNode, gsk_subsurface_node)
  GSK_INIT_NODE_TYPE (GSK_COMPONENT_TRANSFER_NODE, GskComponentTransferNode, gsk_component_transfer_node)
}

/*< private >
 * gsk_render_node_init_types:
 *
 * Initialize all the `GskRenderNode` types provided by GSK.
 */
void
gsk_render_node_init_types (void)
{
  static gsize register_types__volatile;

  if (g_once_init_enter (&register_types__volatile))
    {
      gboolean initialized = TRUE;
      gsk_render_node_init_types_once ();
      gsk_render_node_init_content_serializers ();
      g_once_init_leave (&register_types__volatile, initialized);
    }
}

/* vim:set foldmethod=marker: */
