[gtk/path-work-rebased: 114/121] gtk-demo: Add a curve editor demo




commit 21e71c395771589c63080fe481081d0cdfa53660
Author: Matthias Clasen <mclasen redhat com>
Date:   Sat Dec 4 22:15:37 2021 -0500

    gtk-demo: Add a curve editor demo

 demos/gtk-demo/curve-editor.c     | 2644 +++++++++++++++++++++++++++++++++++++
 demos/gtk-demo/curve-editor.h     |   36 +
 demos/gtk-demo/curve.c            |  272 ++++
 demos/gtk-demo/demo.gresource.xml |    5 +
 demos/gtk-demo/meson.build        |    2 +
 5 files changed, 2959 insertions(+)
---
diff --git a/demos/gtk-demo/curve-editor.c b/demos/gtk-demo/curve-editor.c
new file mode 100644
index 0000000000..81e1083e86
--- /dev/null
+++ b/demos/gtk-demo/curve-editor.c
@@ -0,0 +1,2644 @@
+#include "curve-editor.h"
+
+#include <gtk/gtk.h>
+
+#define DRAW_RADIUS 5
+#define CLICK_RADIUS 8
+
+/* {{{ Types and structures */
+
+static const char *
+op_to_string (GskPathOperation op)
+{
+  switch (op)
+    {
+    case GSK_PATH_MOVE:
+      return "move";
+    case GSK_PATH_LINE:
+      return "line";
+    case GSK_PATH_CURVE:
+      return "curve";
+    case GSK_PATH_CONIC:
+      return "conic";
+    case GSK_PATH_CLOSE:
+      return "close";
+    default:
+      g_assert_not_reached ();
+    }
+}
+
+static GskPathOperation
+op_from_string (const char *s)
+{
+  if (strcmp (s, "move") == 0)
+    return GSK_PATH_MOVE;
+  else if (strcmp (s, "line") == 0)
+    return GSK_PATH_LINE;
+  else if (strcmp (s, "curve") == 0)
+    return GSK_PATH_CURVE;
+  else if (strcmp (s, "conic") == 0)
+    return GSK_PATH_CONIC;
+  else if (strcmp (s, "close") == 0)
+    return GSK_PATH_CLOSE;
+  else
+    g_assert_not_reached ();
+}
+
+typedef enum
+{
+  CUSP,
+  SMOOTH,
+  SYMMETRIC,
+  AUTO
+} PointType;
+
+static const char *
+point_type_to_string (PointType type)
+{
+  switch (type)
+    {
+    case CUSP:
+      return "cusp";
+    case SMOOTH:
+      return "smooth";
+    case SYMMETRIC:
+      return "symmetric";
+    case AUTO:
+      return "auto";
+    default:
+      g_assert_not_reached ();
+    }
+}
+
+static PointType
+point_type_from_string (const char *s)
+{
+  if (strcmp (s, "cusp") == 0)
+    return CUSP;
+  else if (strcmp (s, "smooth") == 0)
+    return SMOOTH;
+  else if (strcmp (s, "symmetric") == 0)
+    return SYMMETRIC;
+  else if (strcmp (s, "auto") == 0)
+    return AUTO;
+  else
+    g_assert_not_reached ();
+}
+
+typedef struct
+{
+  GskPathOperation op;
+  graphene_point_t p[4];
+  float weight;
+  PointType type;
+  int dragged;
+  int hovered;
+} Segment;
+
+struct _CurveEditor
+{
+  GtkWidget parent_instance;
+  GArray *segments;
+  int context;
+  float context_pos;
+  gboolean edit;
+  int edited_point;
+  int edited_segment;
+  int molded;
+  int dragged;
+
+  GtkWidget *menu;
+  GActionMap *actions;
+  GskStroke *stroke;
+  GdkRGBA color;
+
+  gboolean show_outline;
+};
+
+struct _CurveEditorClass
+{
+  GtkWidgetClass parent_class;
+};
+
+G_DEFINE_TYPE (CurveEditor, curve_editor, GTK_TYPE_WIDGET)
+
+/* }}} */
+/* {{{ Misc. geometry */
+
+/* Set q to the projection of p onto the line through a and b */
+static void
+closest_point (const graphene_point_t *p,
+               const graphene_point_t *a,
+               const graphene_point_t *b,
+               graphene_point_t       *q)
+{
+  graphene_vec2_t n;
+  graphene_vec2_t ap;
+  float t;
+
+  graphene_vec2_init (&n, b->x - a->x, b->y - a->y);
+  graphene_vec2_init (&ap, p->x - a->x, p->y - a->y);
+
+  t = graphene_vec2_dot (&ap, &n) / graphene_vec2_dot (&n, &n);
+
+  q->x = a->x + t * (b->x - a->x);
+  q->y = a->y + t * (b->y - a->y);
+}
+
+static void
+find_point_on_line (const graphene_point_t *p1,
+                    const graphene_point_t *p2,
+                    const graphene_point_t *q,
+                    float                  *t)
+{
+  float tx = p2->x - p1->x;
+  float ty = p2->y - p1->y;
+  float sx = q->x - p1->x;
+  float sy = q->y - p1->y;
+
+  *t = (tx*sx + ty*sy) / (tx*tx + ty*ty);
+}
+
+/* Determine if p is on the line through a and b */
+static gboolean
+collinear (const graphene_point_t *p,
+           const graphene_point_t *a,
+           const graphene_point_t *b)
+{
+  graphene_point_t q;
+
+  closest_point (p, a, b, &q);
+
+  return graphene_point_near (p, &q, 0.0001);
+}
+
+/* Set q to the point on the line through p and a that is
+ * at a distance of d from p, on the opposite side
+ */
+static void
+opposite_point (const graphene_point_t *p,
+                const graphene_point_t *a,
+                float                   d,
+                graphene_point_t       *q)
+{
+  graphene_vec2_t ap;
+  float t;
+
+  graphene_vec2_init (&ap, p->x - a->x, p->y - a->y);
+  t = - sqrt (d * d / graphene_vec2_dot (&ap, &ap));
+
+  q->x = p->x + t * (a->x - p->x);
+  q->y = p->y + t * (a->y - p->y);
+}
+
+/* Set q to the point on the line through p and a that is
+ * at a distance of d from p, on the same side
+ */
+static void
+scale_point (const graphene_point_t *p,
+             const graphene_point_t *a,
+             float                   d,
+             graphene_point_t       *q)
+{
+  graphene_vec2_t ap;
+  float t;
+
+  graphene_vec2_init (&ap, p->x - a->x, p->y - a->y);
+  t = sqrt (d * d / graphene_vec2_dot (&ap, &ap));
+
+  q->x = p->x + t * (a->x - p->x);
+  q->y = p->y + t * (a->y - p->y);
+}
+
+/* Set p to the intersection of the lines through a, b
+ * and c, d
+ */
+static void
+line_intersection (const graphene_point_t *a,
+                   const graphene_point_t *b,
+                   const graphene_point_t *c,
+                   const graphene_point_t *d,
+                   graphene_point_t       *p)
+{
+  double a1 = b->y - a->y;
+  double b1 = a->x - b->x;
+  double c1 = a1*a->x + b1*a->y;
+
+  double a2 = d->y - c->y;
+  double b2 = c->x - d->x;
+  double c2 = a2*c->x+ b2*c->y;
+
+  double det = a1*b2 - a2*b1;
+
+  if (det == 0)
+    {
+      p->x = NAN;
+      p->y = NAN;
+    }
+  else
+    {
+      p->x = (b2*c1 - b1*c2) / det;
+      p->y = (a1*c2 - a2*c1) / det;
+    }
+}
+
+/* Given 3 points, determine the center of a circle that
+ * passes through all of them.
+ */
+static void
+circle_through_points (const graphene_point_t *a,
+                       const graphene_point_t *b,
+                       const graphene_point_t *c,
+                       graphene_point_t       *center)
+{
+  graphene_point_t ab;
+  graphene_point_t ac;
+  graphene_point_t ab2;
+  graphene_point_t ac2;
+
+  ab.x = (a->x + b->x) / 2;
+  ab.y = (a->y + b->y) / 2;
+  ac.x = (a->x + c->x) / 2;
+  ac.y = (a->y + c->y) / 2;
+
+  ab2.x = ab.x + a->y - b->y;
+  ab2.y = ab.y + b->x - a->x;
+  ac2.x = ac.x + a->y - c->y;
+  ac2.y = ac.y + c->x - a->x;
+
+  line_intersection (&ab, &ab2, &ac, &ac2, center);
+}
+
+/* Return the cosine of the angle between b1 - a and b2 - a */
+static double
+three_point_angle (const graphene_point_t *a,
+                   const graphene_point_t *b1,
+                   const graphene_point_t *b2)
+{
+  graphene_vec2_t u;
+  graphene_vec2_t v;
+
+  graphene_vec2_init (&u, b1->x - a->x, b1->y - a->y);
+  graphene_vec2_init (&v, b2->x - a->x, b2->y - a->y);
+  graphene_vec2_normalize (&u, &u);
+  graphene_vec2_normalize (&v, &v);
+
+  return graphene_vec2_dot (&u, &v);
+}
+
+/* }}} */
+/* {{{ Misc. Bezier math */
+
+/* Given Bezier control points and a t value between 0 and 1,
+ * return new Bezier control points for two segments in left
+ * and right that are obtained by splitting the curve at the
+ * point for t.
+ *
+ * Note that the points in the right array are in returned in
+ * reverse order.
+ */
+static void
+split_bezier (graphene_point_t *points,
+              int               length,
+              float             t,
+              graphene_point_t *left,
+              int              *left_pos,
+              graphene_point_t *right,
+              int              *right_pos)
+{
+  if (length == 1)
+    {
+      left[*left_pos] = points[0];
+      (*left_pos)++;
+      right[*right_pos] = points[0];
+      (*right_pos)++;
+    }
+  else
+    {
+      graphene_point_t *newpoints;
+      int i;
+
+      newpoints = g_alloca (sizeof (graphene_point_t) * (length - 1));
+      for (i = 0; i < length - 1; i++)
+        {
+          if (i == 0)
+            {
+              left[*left_pos] = points[i];
+              (*left_pos)++;
+            }
+          if (i + 1 == length - 1)
+            {
+              right[*right_pos] = points[i + 1];
+              (*right_pos)++;
+            }
+          graphene_point_interpolate (&points[i], &points[i + 1], t, &newpoints[i]);
+        }
+      split_bezier (newpoints, length - 1, t, left, left_pos, right, right_pos);
+    }
+}
+
+static double
+projection_ratio (double t)
+{
+  double top, bottom;
+
+  if (t == 0 || t == 1)
+    return t;
+
+  top = pow (1 - t, 3),
+  bottom = pow (t, 3) + top;
+
+  return top / bottom;
+}
+
+static double
+abc_ratio (double t)
+{
+  double top, bottom;
+
+  if (t == 0 || t == 1)
+    return t;
+
+  bottom = pow (t, 3) + pow (1 - t, 3);
+  top = bottom - 1;
+
+  return fabs (top / bottom);
+}
+
+static void
+find_control_points (double                  t,
+                     const graphene_point_t *A,
+                     const graphene_point_t *B,
+                     const graphene_point_t *C,
+                     const graphene_point_t *S,
+                     const graphene_point_t *E,
+                     graphene_point_t       *C1,
+                     graphene_point_t       *C2)
+{
+  double angle;
+  double dist;
+  double bc;
+  double de1;
+  double de2;
+  graphene_point_t c;
+  graphene_point_t t0, t1;
+  double tlength;
+  double dx, dy;
+  graphene_point_t e1, e2;
+  graphene_point_t v1, v2;
+
+  dist = graphene_point_distance (S, E, NULL, NULL);
+  angle = atan2 (E->y - S->y, E->x - S->x) - atan2 (B->y - S->y, B->x - S->x);
+  bc = (angle < 0 || angle > M_PI ? -1 : 1) * dist / 3;
+  de1 = t * bc;
+  de2 = (1 - t) * bc;
+
+  circle_through_points (S, B, E, &c);
+
+  t0.x = B->x - (B->y - c.y);
+  t0.y = B->y + (B->x - c.x);
+  t1.x = B->x + (B->y - c.y);
+  t1.y = B->y - (B->x - c.x);
+
+  tlength = graphene_point_distance (&t0, &t1, NULL, NULL);
+  dx = (t1.x - t0.x) / tlength;
+  dy = (t1.y - t0.y) / tlength;
+
+  e1.x = B->x + de1 * dx;
+  e1.y = B->y + de1 * dy;
+  e2.x = B->x - de2 * dx;
+  e2.y = B->y - de2 * dy;
+
+  v1.x = A->x + (e1.x - A->x) / (1 - t);
+  v1.y = A->y + (e1.y - A->y) / (1 - t);
+
+  v2.x = A->x + (e2.x - A->x) / t;
+  v2.y = A->y + (e2.y - A->y) / t;
+
+  C1->x = S->x + (v1.x - S->x) / t;
+  C1->y = S->y + (v1.y - S->y) / t;
+
+  C2->x = E->x + (v2.x - E->x) / (1 - t);
+  C2->y = E->y + (v2.y - E->y) / (1 - t);
+}
+
+/* Given points S, B, E, determine control
+ * points C1, C2 such that B lies on the
+ * Bezier segment given bY S, C1, C2, E.
+ */
+static void
+bezier_through (const graphene_point_t *S,
+                const graphene_point_t *B,
+                const graphene_point_t *E,
+                graphene_point_t       *C1,
+                graphene_point_t       *C2)
+{
+  double d1, d2, t;
+  double u, um, s;
+  graphene_point_t A, C;
+
+  d1 = graphene_point_distance (S, B, NULL, NULL);
+  d2 = graphene_point_distance (E, B, NULL, NULL);
+  t = d1 / (d1 + d2);
+
+  u = projection_ratio (t);
+  um = 1 - u;
+
+  C.x = u * S->x + um * E->x;
+  C.y = u * S->y + um * E->y;
+
+  s = abc_ratio (t);
+
+  A.x = B->x + (B->x - C.x) / s;
+  A.y = B->y + (B->y - C.y) / s;
+
+  find_control_points (t, &A, B, &C, S, E, C1, C2);
+}
+
+/* conics */
+
+static void
+get_conic_shoulder_point (const graphene_point_t  p[3],
+                          float                   w,
+                          graphene_point_t       *q)
+{
+  graphene_point_t m;
+
+  graphene_point_interpolate (&p[0], &p[2], 0.5, &m);
+  graphene_point_interpolate (&m, &p[1], w / (1 + w), q);
+}
+
+static void
+split_bezier3d_recurse (const graphene_point3d_t *p,
+                        int                       l,
+                        float                     t,
+                        graphene_point3d_t       *left,
+                        graphene_point3d_t       *right,
+                        int                      *lpos,
+                        int                      *rpos)
+{
+  if (l == 1)
+    {
+      left[*lpos] = p[0];
+      right[*rpos] = p[0];
+    }
+  else
+    {
+      graphene_point3d_t *np;
+      int i;
+
+      np = g_alloca (sizeof (graphene_point3d_t) * (l - 1));
+      for (i = 0; i < l - 1; i++)
+        {
+          if (i == 0)
+            {
+              left[*lpos] = p[i];
+              (*lpos)++;
+            }
+          if (i + 1 == l - 1)
+            {
+              right[*rpos] = p[i + 1];
+              (*rpos)--;
+            }
+          graphene_point3d_interpolate (&p[i], &p[i + 1], t, &np[i]);
+        }
+      split_bezier3d_recurse (np, l - 1, t, left, right, lpos, rpos);
+    }
+}
+
+static void
+split_bezier3d (const graphene_point3d_t *p,
+                int                       l,
+                float                     t,
+                graphene_point3d_t       *left,
+                graphene_point3d_t       *right)
+{
+  int lpos = 0;
+  int rpos = l - 1;
+  split_bezier3d_recurse (p, l, t, left, right, &lpos, &rpos);
+}
+
+static void
+split_conic (const graphene_point_t points[3], float weight,
+             float t,
+             graphene_point_t lp[3], float *lw,
+             graphene_point_t rp[3], float *rw)
+{
+  /* Given control points and weight for a rational quadratic
+   * Bezier and t, create two sets of the same that give the
+   * same curve as the original and split the curve at t.
+   */
+  graphene_point3d_t p[3];
+  graphene_point3d_t l[3], r[3];
+  int i;
+
+  /* do de Casteljau in homogeneous coordinates... */
+  for (i = 0; i < 3; i++)
+    {
+      p[i].x = points[i].x;
+      p[i].y = points[i].y;
+      p[i].z = 1;
+    }
+
+  p[1].x *= weight;
+  p[1].y *= weight;
+  p[1].z *= weight;
+
+  split_bezier3d (p, 3, t, l, r);
+
+  /* then project the control points down */
+  for (i = 0; i < 3; i++)
+    {
+      lp[i].x = l[i].x / l[i].z;
+      lp[i].y = l[i].y / l[i].z;
+      rp[i].x = r[i].x / r[i].z;
+      rp[i].y = r[i].y / r[i].z;
+    }
+
+  /* normalize the outer weights to be 1 by using
+   * the fact that weights w_i and c*w_i are equivalent
+   * for any nonzero constant c
+   */
+  for (i = 0; i < 3; i++)
+    {
+      l[i].z /= l[0].z;
+      r[i].z /= r[2].z;
+    }
+
+  /* normalize the inner weight to be 1 by using
+   * the fact that w_0*w_2/w_1^2 is a constant for
+   * all equivalent weights.
+   */
+  *lw = l[1].z / sqrt (l[2].z);
+  *rw = r[1].z / sqrt (r[0].z);
+}
+
+/* }}} */
+/* {{{ Utilities */
+
+static Segment *
+get_segment (CurveEditor *self,
+             int          idx)
+{
+  idx = idx % (int)self->segments->len;
+  if (idx < 0)
+    idx += (int)self->segments->len;
+  return &g_array_index (self->segments, Segment, idx);
+}
+
+static void
+set_segment_start (CurveEditor      *self,
+                   int               idx,
+                   graphene_point_t *p)
+{
+  Segment *seg = get_segment (self, idx);
+  Segment *seg1 = get_segment (self, idx - 1);
+
+  seg->p[0] = *p;
+  seg1->p[3] = *p;
+}
+
+static const graphene_point_t *
+get_line_point (CurveEditor *self,
+                int          idx)
+{
+  Segment *seg = get_segment (self, idx);
+  return &seg->p[0];
+}
+
+static graphene_point_t *
+get_left_control_point (CurveEditor *self,
+                        int          idx)
+{
+  Segment *seg = get_segment (self, idx - 1);
+  return &seg->p[2];
+}
+
+static graphene_point_t *
+get_right_control_point (CurveEditor *self,
+                         int          idx)
+{
+  Segment *seg = get_segment (self, idx);
+  return &seg->p[1];
+}
+
+static gboolean
+point_is_visible (CurveEditor *self,
+                  int          point,
+                  int          point1)
+{
+  Segment *seg;
+
+  if (!self->edit)
+    return FALSE;
+
+  seg = get_segment (self, point);
+  switch (point1)
+    {
+    case 0: /* point on curve */
+      return TRUE;
+
+    case 1:
+      if (self->edited_segment == point &&
+          seg->op != GSK_PATH_LINE)
+        return TRUE;
+      if (seg->op == GSK_PATH_CONIC &&
+          (self->edited_point == point + 1 ||
+           (self->edited_point == 0 && point + 1 == self->segments->len)))
+        return TRUE;
+      if (self->edited_point == point &&
+          (seg->op == GSK_PATH_CURVE || seg->op == GSK_PATH_CONIC))
+        return TRUE;
+      break;
+
+    case 2:
+      if (self->edited_segment == point &&
+          seg->op != GSK_PATH_LINE)
+        return TRUE;
+      if (seg->op == GSK_PATH_CURVE &&
+          (self->edited_point == point + 1 ||
+           (self->edited_point == 0 && point + 1 == self->segments->len)))
+        return TRUE;
+      break;
+
+    default:
+      g_assert_not_reached ();
+    }
+
+  return FALSE;
+}
+
+static void
+maintain_smoothness (CurveEditor *self,
+                     int          point)
+{
+  Segment *seg, *seg1;
+  const graphene_point_t *p, *p2;
+  graphene_point_t *c, *c2;
+  float d;
+
+  seg = get_segment (self, point);
+  seg1 = get_segment (self, point - 1);
+
+  if (seg->type == CUSP)
+    return;
+
+  if (seg->op == GSK_PATH_LINE && seg1->op == GSK_PATH_LINE)
+    return;
+
+  p = &seg->p[0];
+  c = &seg1->p[2];
+  c2 = &seg->p[1];
+
+  if (seg->op == GSK_PATH_CURVE && seg1->op == GSK_PATH_CURVE)
+    {
+      d = graphene_point_distance (c, p, NULL, NULL);
+      opposite_point (p, c2, d, c);
+    }
+  else if (seg->op == GSK_PATH_CURVE)
+    {
+      if (seg1->op == GSK_PATH_LINE)
+        p2 = &seg1->p[0];
+      else if (seg1->op == GSK_PATH_CONIC)
+        p2 = &seg1->p[1];
+      else
+        g_assert_not_reached ();
+      d = graphene_point_distance (c2, p, NULL, NULL);
+      opposite_point (p, p2, d, c2);
+    }
+  else if (seg1->op == GSK_PATH_CURVE)
+    {
+      if (seg->op == GSK_PATH_LINE)
+        p2 = &seg->p[3];
+      else if (seg->op == GSK_PATH_CONIC)
+        p2 = &seg->p[1];
+      else
+        g_assert_not_reached ();
+      d = graphene_point_distance (c, p, NULL, NULL);
+      opposite_point (p, p2, d, c);
+    }
+  else if (seg->op == GSK_PATH_CONIC && seg1->op == GSK_PATH_CONIC)
+    {
+      graphene_point_t h, a, b;
+
+      h.x = seg->p[0].x + seg->p[1].x - seg1->p[1].x;
+      h.y = seg->p[0].y + seg->p[1].y - seg1->p[1].y;
+      line_intersection (&seg->p[0], &h, &seg1->p[0], &seg1->p[1], &a);
+      line_intersection (&seg->p[0], &h, &seg->p[1], &seg->p[3], &b);
+
+      seg1->p[1] = a;
+      seg->p[1] = b;
+    }
+}
+
+static void
+maintain_symmetry (CurveEditor *self,
+                   int          point)
+{
+  Segment *seg, *seg1;
+  const graphene_point_t *p;
+  graphene_point_t *c, *c2;
+  double l1, l2, l;
+
+  seg = get_segment (self, point);
+  seg1 = get_segment (self, point - 1);
+
+  if (seg->type != SYMMETRIC)
+    return;
+
+  if (seg->op != GSK_PATH_CURVE || seg1->op != GSK_PATH_CURVE)
+    return;
+
+  p = &seg->p[0];
+  c = &seg1->p[2];
+  c2 = &seg->p[1];
+
+  l1 = graphene_point_distance (p, c, NULL, NULL);
+  l2 = graphene_point_distance (p, c2, NULL, NULL);
+
+  if (l1 != l2)
+    {
+      l = (l1 + l2) / 2;
+
+      scale_point (p, c, l, c);
+      scale_point (p, c2, l, c2);
+    }
+}
+
+/* Make the line through the control points perpendicular
+ * to the line bisecting the angle between neighboring
+ * points, and make the lengths 1/3 of the distance to
+ * the corresponding neighboring points.
+ */
+static void
+update_automatic (CurveEditor *self,
+                  int          point)
+{
+  Segment *seg;
+  const graphene_point_t *p, *p1, *p2;
+  double l1, l2;
+  graphene_point_t a;
+  graphene_point_t *c1, *c2;
+
+  seg = get_segment (self, point);
+
+  if (seg->type != AUTO)
+    return;
+
+  if (seg->op != GSK_PATH_CURVE || get_segment (self, point - 1)->op != GSK_PATH_CURVE)
+    return;
+
+  p = get_line_point (self, point);
+  c1 = get_left_control_point (self, point);
+  c2 = get_right_control_point (self, point);
+
+  p1 = get_line_point (self, point - 1);
+  p2 = get_line_point (self, point + 1);
+
+  l1 = graphene_point_distance (p, p1, NULL, NULL);
+  l2 = graphene_point_distance (p, p2, NULL, NULL);
+
+  a.x = p2->x + (p->x - p1->x);
+  a.y = p2->y + (p->y - p1->y);
+
+  scale_point (p, &a, l2/3, c2);
+  opposite_point (p, &a, l1/3, c1);
+}
+
+static void
+maintain_automatic (CurveEditor *self,
+                    int          point)
+{
+  if (get_segment (self, point)->op != GSK_PATH_CURVE ||
+      get_segment (self, point - 1)->op != GSK_PATH_CURVE)
+    return;
+
+  update_automatic (self, point);
+  update_automatic (self, point - 1);
+  update_automatic (self, point + 1);
+}
+
+static void
+maintain_conic (CurveEditor *self,
+                int          idx)
+{
+  Segment *seg = get_segment (self, idx);
+  graphene_point_t p[3];
+
+  if (seg->op != GSK_PATH_CONIC)
+    return;
+
+  p[0] = seg->p[0];
+  p[1] = seg->p[1];
+  p[2] = seg->p[3];
+
+  get_conic_shoulder_point (p, seg->weight, &seg->p[2]);
+}
+
+/* Check if the points arount point currently satisfy
+ * smoothness conditions. Set PointData.type accordingly.
+ */
+static void
+check_smoothness (CurveEditor *self,
+                  int          point)
+{
+  GskPathOperation op, op1;
+  const graphene_point_t *p, *p1, *p2;
+  Segment *seg, *seg1;
+
+  seg = get_segment (self, point);
+  seg1 = get_segment (self, point - 1);
+  p = get_line_point (self, point);
+
+  op = seg->op;
+  op1 = seg1->op;
+
+  if (op == GSK_PATH_CURVE)
+    p2 = get_right_control_point (self, point);
+  else if (op == GSK_PATH_LINE)
+    p2 = get_line_point (self, point + 1);
+  else
+    p2 = NULL;
+
+  if (op1 == GSK_PATH_CURVE)
+    p1 = get_left_control_point (self, point);
+  else if (op1 == GSK_PATH_LINE)
+    p1 = get_line_point (self, point - 1);
+  else
+    p1 = NULL;
+
+  if (!p1 || !p2 || !collinear (p, p1, p2))
+    seg->type = CUSP;
+  else
+    seg->type = SMOOTH;
+}
+
+static void
+insert_point (CurveEditor *self,
+              int          point,
+              double       pos)
+{
+  Segment *seg, *seg1, *seg2;
+  Segment ns = { 0, };;
+
+  seg = get_segment (self, point);
+  if (seg->op == GSK_PATH_MOVE)
+    return;
+
+  g_array_insert_val (self->segments, point + 1, ns);
+
+  seg = get_segment (self, point);
+  seg1 = get_segment (self, point + 1);
+  seg2 = get_segment (self, point + 2);
+
+  seg1->type = SMOOTH;
+  seg1->hovered = -1;
+  seg1->dragged = -1;
+
+  switch (seg->op)
+    {
+    case GSK_PATH_LINE:
+      seg1->op = GSK_PATH_LINE;
+
+      graphene_point_interpolate (&seg->p[0], &seg->p[3], pos, &seg1->p[0]);
+      seg->p[3] = seg->p[0];
+      seg1->p[3] = seg2->p[0];
+      break;
+
+    case GSK_PATH_CURVE:
+      {
+        graphene_point_t left[4];
+        graphene_point_t right[4];
+        int left_pos = 0;
+        int right_pos = 0;
+
+        seg1->op = GSK_PATH_CURVE;
+
+        split_bezier (seg->p, 4, pos, left, &left_pos, right, &right_pos);
+
+        seg->p[0] = left[0];
+        seg->p[1] = left[1];
+        seg->p[2] = left[2];
+        seg->p[3] = left[3];
+        seg1->p[0] = right[3];
+        seg1->p[1] = right[2];
+        seg1->p[2] = right[1];
+        seg1->p[3] = right[0];
+      }
+      break;
+
+    case GSK_PATH_CONIC:
+      {
+        graphene_point_t points[3];
+        graphene_point_t left[3];
+        graphene_point_t right[3];
+        float lw, rw;
+
+        seg1->op = GSK_PATH_CONIC;
+
+        points[0] = seg->p[0];
+        points[1] = seg->p[1];
+        points[2] = seg->p[3];
+        split_conic (points, seg->weight, pos, left, &lw, right, &rw);
+
+        seg->p[0] = left[0];
+        seg->p[1] = left[1];
+        seg->p[3] = left[2];
+        seg1->p[0] = right[0];
+        seg1->p[1] = right[1];
+        seg1->p[3] = right[2];
+
+        seg->weight = lw;
+        seg1->weight = rw;
+
+        get_conic_shoulder_point (seg->p, seg->weight, &seg->p[2]);
+        get_conic_shoulder_point (seg1->p, seg1->weight, &seg1->p[2]);
+      }
+      break;
+
+    case GSK_PATH_MOVE:
+    case GSK_PATH_CLOSE:
+    default:
+      g_assert_not_reached ();
+      break;
+    }
+
+  maintain_smoothness (self, point + 1);
+  maintain_automatic (self, point + 1);
+
+  gtk_widget_queue_draw (GTK_WIDGET (self));
+}
+
+static void
+remove_point (CurveEditor *self,
+              int          point)
+{
+  Segment *seg;
+  graphene_point_t c, p;
+
+  seg = get_segment (self, point);
+  c = seg->p[2];
+  p = seg->p[3];
+
+  g_array_remove_index (self->segments, point);
+
+  seg = get_segment (self, point - 1);
+  seg->p[2] = c;
+  seg->p[3] = p;
+
+  maintain_smoothness (self, point);
+  maintain_automatic (self, point);
+}
+
+/* }}} */
+/* {{{ GskPath helpers */
+
+static void
+curve_editor_add_segment (CurveEditor    *self,
+                          GskPathBuilder *builder,
+                          int             point)
+{
+  Segment *seg;
+
+  seg = get_segment (self, point);
+
+  gsk_path_builder_move_to (builder, seg->p[0].x, seg->p[0].y);
+
+  switch (seg->op)
+    {
+    case GSK_PATH_LINE:
+      gsk_path_builder_line_to (builder, seg->p[3].x, seg->p[3].y);
+      break;
+
+    case GSK_PATH_CURVE:
+      gsk_path_builder_curve_to (builder,
+                                 seg->p[1].x, seg->p[1].y,
+                                 seg->p[2].x, seg->p[2].y,
+                                 seg->p[3].x, seg->p[3].y);
+      break;
+
+    case GSK_PATH_CONIC:
+      gsk_path_builder_conic_to (builder,
+                                 seg->p[1].x, seg->p[1].y,
+                                 seg->p[3].x, seg->p[3].y,
+                                 seg->weight);
+      break;
+
+    case GSK_PATH_MOVE:
+    case GSK_PATH_CLOSE:
+    default:
+      break;
+    }
+}
+
+static void
+curve_editor_add_path (CurveEditor    *self,
+                       GskPathBuilder *builder)
+{
+  int i;
+
+  for (i = 0; i < self->segments->len; i++)
+    {
+      Segment *seg = get_segment (self, i);
+
+      if (i == 0)
+        gsk_path_builder_move_to (builder, seg->p[0].x, seg->p[0].y);
+
+      switch (seg->op)
+        {
+        case GSK_PATH_MOVE:
+          gsk_path_builder_move_to (builder, seg->p[3].x, seg->p[3].y);
+          break;
+
+        case GSK_PATH_LINE:
+          gsk_path_builder_line_to (builder, seg->p[3].x, seg->p[3].y);
+          break;
+
+        case GSK_PATH_CURVE:
+          gsk_path_builder_curve_to (builder,
+                                     seg->p[1].x, seg->p[1].y,
+                                     seg->p[2].x, seg->p[2].y,
+                                     seg->p[3].x, seg->p[3].y);
+          break;
+
+        case GSK_PATH_CONIC:
+          gsk_path_builder_conic_to (builder,
+                                     seg->p[1].x, seg->p[1].y,
+                                     seg->p[3].x, seg->p[3].y,
+                                     seg->weight);
+          break;
+
+        case GSK_PATH_CLOSE:
+        default:
+          g_assert_not_reached ();
+        }
+    }
+
+  gsk_path_builder_close (builder);
+}
+
+static gboolean
+find_closest_segment (CurveEditor      *self,
+                      graphene_point_t *point,
+                      float             threshold,
+                      graphene_point_t *p,
+                      int              *segment,
+                      float            *pos)
+{
+  graphene_point_t pp;
+  float t;
+  int seg;
+  gboolean found = FALSE;
+  int i;
+
+  for (i = 0; i < self->segments->len; i++)
+    {
+      GskPathBuilder *builder;
+      GskPath *path;
+      GskPathMeasure *measure;
+      float t1;
+      graphene_point_t pp1;
+
+      builder = gsk_path_builder_new ();
+      curve_editor_add_segment (self, builder, i);
+      path = gsk_path_builder_free_to_path (builder);
+      measure = gsk_path_measure_new (path);
+
+      if (gsk_path_measure_get_closest_point_full (measure, point, threshold, &threshold, &pp1, &t1, NULL))
+        {
+          seg = i;
+          t = t1 / gsk_path_measure_get_length (measure);
+          pp = pp1;
+          found = TRUE;
+        }
+
+      gsk_path_measure_unref (measure);
+      gsk_path_unref (path);
+    }
+
+  if (found)
+    {
+      if (segment)
+        *segment = seg;
+      if (pos)
+        *pos = t;
+      if (p)
+        *p = pp;
+    }
+
+  return found;
+}
+
+/* }}} */
+/* {{{ Drag implementation */
+
+static void
+drag_begin (GtkGestureDrag *gesture,
+            double          start_x,
+            double          start_y,
+            CurveEditor     *self)
+{
+  int i, j;
+  graphene_point_t p = GRAPHENE_POINT_INIT (start_x, start_y);
+  float t;
+  int idx;
+
+  if (!self->edit)
+    return;
+
+  for (i = 0; i < self->segments->len; i++)
+    {
+      Segment *seg = get_segment (self, i);
+
+      for (j = 0; j < 3; j++)
+        {
+          if (graphene_point_distance (&seg->p[j], &p, NULL, NULL) < CLICK_RADIUS)
+            {
+              if (point_is_visible (self, i, j))
+                {
+                  self->dragged = i;
+                  seg->dragged = j;
+                  gtk_widget_queue_draw (GTK_WIDGET (self));
+                }
+              return;
+            }
+        }
+    }
+
+  if (find_closest_segment (self, &p, CLICK_RADIUS, NULL, &idx, &t))
+    {
+      /* Can't bend a straight line */
+      get_segment (self, idx)->op = GSK_PATH_CURVE;
+      self->molded = idx;
+      return;
+    }
+
+  gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_DENIED);
+}
+
+static void
+drag_line_point (CurveEditor *self,
+                 double       x,
+                 double       y)
+{
+  /* dragged point is on curve */
+  Segment *seg, *seg1, *seg2, *seg11;
+  const graphene_point_t *d, *p;
+  graphene_point_t *c;
+  float l1, l2, dx, dy;
+
+  seg = get_segment (self, self->dragged);
+  d = get_line_point (self, self->dragged);
+
+  /* before moving the point, record the distances to its neighbors, since
+   * we may want to preserve those
+   */
+  l1 = graphene_point_distance (d, get_left_control_point (self, self->dragged), NULL, NULL);
+  l2 = graphene_point_distance (d, get_right_control_point (self, self->dragged), NULL, NULL);
+
+  dx = x - d->x;
+  dy = y - d->y;
+
+  /* first move the point itself */
+  set_segment_start (self, self->dragged, &GRAPHENE_POINT_INIT (x, y));
+
+  /* adjust control points as needed */
+  seg1 = get_segment (self, self->dragged - 1);
+  seg2 = get_segment (self, self->dragged + 1);
+
+  if (seg1->op == GSK_PATH_LINE)
+    {
+      /* the other endpoint of the line */
+      p = get_line_point (self, self->dragged - 1);
+      c = get_right_control_point (self, self->dragged);
+
+      if (seg->op == GSK_PATH_CURVE && seg->type != CUSP)
+        {
+          opposite_point (d, p, l2, c);
+        }
+      else if (seg->op == GSK_PATH_CONIC && seg->type != CUSP)
+        {
+          graphene_point_t u;
+          line_intersection (&seg1->p[0], &seg1->p[3], &seg->p[3], &seg->p[1], &u);
+          if (u.x != NAN)
+            seg->p[1] = u;
+          else
+            {
+              seg->p[1].x += dx;
+              seg->p[1].y += dy;
+            }
+
+          maintain_conic (self, self->dragged);
+        }
+      else
+        {
+          c->x += dx;
+          c->y += dy;
+        }
+
+      /* always move the other control point along */
+      c = get_left_control_point (self, self->dragged);
+      c->x += dx;
+      c->y += dy;
+
+      /* handle the far end of the line */
+      seg11 = get_segment (self, self->dragged - 2);
+
+      if (seg11->op == GSK_PATH_CURVE && seg1->type != CUSP)
+        {
+          double l;
+          const graphene_point_t *p2;
+          graphene_point_t *c2;
+
+          p2 = get_line_point (self, self->dragged - 1);
+          c2 = get_left_control_point (self, self->dragged - 1);
+          /* adjust the control point before the line segment */
+          l = graphene_point_distance (c2, p2, NULL, NULL);
+          opposite_point (p2, d, l, c2);
+        }
+      else if (seg11->op == GSK_PATH_CONIC && seg1->type != CUSP)
+        {
+          graphene_point_t u;
+          line_intersection (&seg11->p[0], &seg11->p[1], &seg1->p[3], &seg1->p[0], &u);
+          if (u.x != NAN)
+            seg11->p[1] = u;
+
+          maintain_conic (self, self->dragged - 2);
+        }
+    }
+
+  if (seg->op == GSK_PATH_LINE)
+    {
+      /* the other endpoint of the line */
+      p = get_line_point (self, self->dragged + 1);
+      c = get_left_control_point (self, self->dragged);
+
+      if (seg1->op == GSK_PATH_CURVE && seg->type != CUSP)
+        {
+          /* adjust the control point before the line segment */
+          opposite_point (d, p, l1, c);
+        }
+      else if (seg1->op == GSK_PATH_CONIC && seg->type != CUSP)
+        {
+          graphene_point_t u;
+          line_intersection (&seg1->p[0], &seg1->p[1], &seg->p[0], &seg->p[3], &u);
+          if (u.x != NAN)
+            seg1->p[1] = u;
+          else
+            {
+              seg1->p[1].x += dx;
+              seg1->p[1].y += dy;
+            }
+
+          maintain_conic (self, self->dragged);
+        }
+      else if (seg1->op == GSK_PATH_CURVE)
+        {
+          c->x += dx;
+          c->y += dy;
+        }
+
+      /* always move the other control point along */
+      c = get_right_control_point (self, self->dragged);
+      c->x += dx;
+      c->x += dy;
+
+      /* handle the other end of the line */
+      if (seg2->op == GSK_PATH_CURVE && seg2->type != CUSP)
+        {
+          double l;
+
+          /* adjust the control point after the line segment */
+          c = get_right_control_point (self, self->dragged + 1);
+          l = graphene_point_distance (c, p, NULL, NULL);
+          opposite_point (p, d, l, c);
+        }
+      else if (seg2->op == GSK_PATH_CONIC && seg2->type != CUSP)
+        {
+          graphene_point_t u;
+          line_intersection (&seg->p[0], &seg->p[3], &seg2->p[1], &seg2->p[3], &u);
+          if (u.x != NAN)
+            seg2->p[1] = u;
+
+          maintain_conic (self, self->dragged + 1);
+        }
+    }
+
+  if (seg1->op != GSK_PATH_LINE && seg->op != GSK_PATH_LINE)
+    {
+      if (seg1->op == GSK_PATH_CURVE)
+        {
+          c = &seg1->p[2];
+          c->x += dx;
+          c->y += dy;
+        }
+      else if (seg1->op == GSK_PATH_CONIC && seg->type != CUSP)
+        {
+          graphene_point_t a, b;
+
+          a.x = seg1->p[1].x + dx;
+          a.y = seg1->p[1].y + dy;
+          line_intersection (&seg->p[0], &a, &seg1->p[0], &seg1->p[1], &b);
+          seg1->p[1] = b;
+        }
+
+      if (seg->op == GSK_PATH_CURVE)
+        {
+          c = &seg->p[1];
+          c->x += dx;
+          c->y += dy;
+        }
+      else if (seg->op == GSK_PATH_CONIC && seg->type != CUSP)
+        {
+          graphene_point_t a, b;
+
+          a.x = seg->p[1].x + dx;
+          a.y = seg->p[1].y + dy;
+          line_intersection (&seg->p[3], &seg->p[1], &a, &seg->p[0], &b);
+          seg->p[1] = b;
+        }
+    }
+
+  maintain_smoothness (self, self->dragged);
+  maintain_automatic (self, self->dragged);
+  maintain_conic (self, self->dragged);
+  maintain_conic (self, self->dragged - 1);
+}
+
+static void
+drag_conic_point (CurveEditor *self,
+                  float        x,
+                  float        y)
+{
+  Segment *seg, *seg1, *seg2;
+  graphene_point_t *d, *c1;
+  float l;
+
+  seg = get_segment (self, self->dragged);
+  g_assert (seg->op == GSK_PATH_CONIC);
+  d = &seg->p[seg->dragged];
+
+  seg1 = get_segment (self, self->dragged + 1);
+  seg2 = get_segment (self, self->dragged - 1);
+
+  if (seg->dragged == 1)
+    {
+      if (seg->type != CUSP && seg2->op == GSK_PATH_LINE)
+        {
+          /* control point must be on the line of seg2 */
+
+          if (seg1->type != CUSP && seg1->op == GSK_PATH_LINE)
+            {
+              graphene_point_t c;
+
+              line_intersection (&seg1->p[0], &seg1->p[3], &seg2->p[3], &seg2->p[0], &c);
+              if (c.x != NAN)
+                *d = c; /* unmoveable */
+              else
+                {
+                  closest_point (&GRAPHENE_POINT_INIT (x, y), &seg1->p[0], &seg1->p[3], &c);
+                  *d = c;
+                }
+            }
+          else
+            {
+              graphene_point_t c;
+
+              closest_point (&GRAPHENE_POINT_INIT (x, y), &seg2->p[0], &seg2->p[3], &c);
+              *d = c;
+
+              if (seg1->type != CUSP)
+                {
+                  l = graphene_point_distance (&seg1->p[0], &seg1->p[1], NULL, NULL);
+                  opposite_point (&seg1->p[0], d, l, &seg1->p[1]);
+                }
+            }
+        }
+      else if (seg1->type != CUSP && seg1->op == GSK_PATH_LINE)
+        {
+          graphene_point_t c;
+
+          closest_point (&GRAPHENE_POINT_INIT (x, y), &seg1->p[0], &seg1->p[3], &c);
+          *d = c;
+
+          if (seg2->type != CUSP)
+            {
+              if (seg2->op == GSK_PATH_CURVE)
+                c1 = &seg2->p[2];
+              else if (seg2->op == GSK_PATH_CONIC)
+                c1 = &seg2->p[1];
+              else
+                g_assert_not_reached ();
+              l = graphene_point_distance (&seg2->p[3], c1, NULL, NULL);
+              opposite_point (&seg2->p[3], d, l, c1);
+            }
+        }
+      else
+        {
+          /* unconstrained */
+          d->x = x;
+          d->y = y;
+
+          if (seg1->type != CUSP)
+            {
+              l = graphene_point_distance (&seg1->p[0], &seg1->p[1], NULL, NULL);
+              opposite_point (&seg1->p[0], d, l, &seg1->p[1]);
+            }
+
+          if (seg2->type != CUSP)
+            {
+              if (seg2->op == GSK_PATH_CURVE)
+                c1 = &seg2->p[2];
+              else if (seg2->op == GSK_PATH_CONIC)
+                c1 = &seg2->p[1];
+              else
+                g_assert_not_reached ();
+              l = graphene_point_distance (&seg2->p[3], c1, NULL, NULL);
+              opposite_point (&seg2->p[3], d, l, c1);
+            }
+        }
+    }
+  else if (seg->dragged == 2)
+    {
+      /* dragging the shoulder point */
+      graphene_point_t m;
+      float t;
+
+      graphene_point_interpolate (&seg->p[0], &seg->p[3], 0.5, &m);
+      find_point_on_line (&m, &seg->p[1], &GRAPHENE_POINT_INIT (x, y), &t);
+      t = CLAMP (t, 0, 0.9);
+      seg->weight = - t / (t - 1);
+    }
+
+  maintain_conic (self, self->dragged);
+}
+
+static void
+drag_control_point (CurveEditor *self,
+                    float        x,
+                    float        y)
+{
+  /* dragged point is a control point */
+  Segment *seg, *seg1;
+  const graphene_point_t *p, *p1;
+  graphene_point_t *c, *d;
+  PointType type;
+
+  seg = get_segment (self, self->dragged);
+  g_assert (seg->op == GSK_PATH_CURVE);
+  d = &seg->p[seg->dragged];
+
+  if (seg->dragged == 2)
+    {
+      seg1 = get_segment (self, self->dragged + 1);
+      p = &seg1->p[0];
+      c = &seg1->p[1];
+      type = seg1->type;
+      p1 = get_line_point (self, self->dragged + 2);
+    }
+  else if (seg->dragged == 1)
+    {
+      seg1 = get_segment (self, self->dragged - 1);
+      if (seg1->op == GSK_PATH_CONIC)
+        c = &seg1->p[1];
+      else
+        c = &seg1->p[2];
+      p = &seg->p[0];
+      type = seg->type;
+      p1 = &seg1->p[0];
+    }
+  else
+    g_assert_not_reached ();
+
+  if (type != CUSP)
+    {
+      if (seg1->op == GSK_PATH_CURVE)
+        {
+          double l;
+
+          /* first move the point itself */
+          d->x = x;
+          d->y = y;
+
+          /* then adjust the other control point */
+          if (type == SYMMETRIC)
+            l = graphene_point_distance (d, p, NULL, NULL);
+          else
+            l = graphene_point_distance (c, p, NULL, NULL);
+
+          opposite_point (p, d, l, c);
+        }
+      else if (seg1->op == GSK_PATH_CONIC)
+        {
+          graphene_point_t u;
+
+          d->x = x;
+          d->y = y;
+          line_intersection (p1, c, p, d, &u);
+          *c = u;
+
+          maintain_conic (self, self->dragged - 1);
+          maintain_conic (self, self->dragged + 1);
+        }
+      else if (seg1->op == GSK_PATH_LINE)
+        {
+          graphene_point_t m = GRAPHENE_POINT_INIT (x, y);
+          closest_point (&m, p, p1, d);
+        }
+      else
+        {
+          d->x = x;
+          d->y = y;
+        }
+    }
+  else
+    {
+      d->x = x;
+      d->y = y;
+    }
+}
+
+static void
+drag_point (CurveEditor *self,
+            double       x,
+            double       y)
+{
+  Segment *seg = get_segment (self, self->dragged);
+
+  if (seg->dragged == 0)
+    drag_line_point (self, x, y);
+  else if (seg->op == GSK_PATH_CONIC)
+    drag_conic_point (self, x, y);
+  else
+    drag_control_point (self, x, y);
+}
+
+static void
+drag_curve (CurveEditor *self,
+            double       x,
+            double       y)
+{
+  graphene_point_t *S, *E;
+  graphene_point_t B, C1, C2;
+  double l;
+  Segment *seg, *seg1, *seg2;
+
+  seg = get_segment (self, self->molded);
+  seg1 = get_segment (self, self->molded + 1);
+  seg2 = get_segment (self, self->molded - 1);
+
+  if (seg->op == GSK_PATH_CONIC)
+    {
+      /* FIXME */
+      return;
+    }
+
+  S = &seg->p[0];
+  B = GRAPHENE_POINT_INIT (x, y);
+  E = &seg->p[3];
+
+  bezier_through (S, &B, E, &C1, &C2);
+
+  seg->p[1] = C1;
+  seg->p[2] = C2;
+
+  /* When the neighboring segments are lines, we can't actually
+   * use C1 and C2 as-is, since we need control points to lie
+   * on the line. So we just use their distance. This makes our
+   * point B not quite match anymore, but we're overconstrained.
+   */
+  if (seg2->op == GSK_PATH_LINE)
+    {
+      l = graphene_point_distance (&seg->p[3], &C1, NULL, NULL);
+      if (three_point_angle (&seg2->p[3], &seg2->p[0], &B) > 0)
+        scale_point (&seg2->p[3], &seg2->p[0], l, &seg->p[1]);
+      else
+        opposite_point (&seg2->p[3], &seg2->p[0], l, &seg->p[1]);
+    }
+
+  if (seg1->op == GSK_PATH_LINE)
+    {
+      l = graphene_point_distance (&seg->p[0], &C2, NULL, NULL);
+      if (three_point_angle (&seg1->p[0], &seg1->p[3], &B) > 0)
+        scale_point (&seg1->p[0], &seg1->p[3], l, &seg->p[2]);
+      else
+        opposite_point (&seg1->p[0], &seg1->p[3], l, &seg->p[2]);
+    }
+
+  /* Maintain smoothness and symmetry */
+  if (seg->type != CUSP)
+    {
+      if (seg->type == SYMMETRIC)
+        l = graphene_point_distance (&seg->p[0], &seg->p[1], NULL, NULL);
+      else
+        l = graphene_point_distance (&seg->p[0], &seg2->p[2], NULL, NULL);
+      opposite_point (&seg->p[0], &seg->p[1], l, &seg2->p[2]);
+    }
+
+  if (seg1->type != CUSP)
+    {
+      if (seg1->type == SYMMETRIC)
+        l = graphene_point_distance (&seg->p[3], &seg->p[2], NULL, NULL);
+      else
+        l = graphene_point_distance (&seg->p[3], &seg1->p[1], NULL, NULL);
+      opposite_point (&seg->p[3], &seg->p[2], l, &seg1->p[1]);
+    }
+}
+
+static void
+drag_update (GtkGestureDrag *gesture,
+             double          offset_x,
+             double          offset_y,
+             CurveEditor     *self)
+{
+  double x, y;
+
+  gtk_gesture_drag_get_start_point (gesture, &x, &y);
+
+  x += offset_x;
+  y += offset_y;
+
+  if (self->dragged != -1)
+    {
+      gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_CLAIMED);
+      drag_point (self, x, y);
+      gtk_widget_queue_draw (GTK_WIDGET (self));
+    }
+  else if (self->molded != -1)
+    {
+      gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_CLAIMED);
+      drag_curve (self, x, y);
+      gtk_widget_queue_draw (GTK_WIDGET (self));
+    }
+}
+
+static void
+drag_end (GtkGestureDrag *gesture,
+          double          offset_x,
+          double          offset_y,
+          CurveEditor     *self)
+{
+  drag_update (gesture, offset_x, offset_y, self);
+  self->dragged = -1;
+  self->molded = -1;
+}
+
+/* }}} */
+/* {{{ Action callbacks */
+
+static void
+set_point_type (GSimpleAction *action,
+                GVariant      *value,
+                gpointer       data)
+{
+  CurveEditor *self = CURVE_EDITOR (data);
+
+  get_segment (self, self->context)->type = point_type_from_string (g_variant_get_string (value, NULL));
+
+  maintain_smoothness (self, self->context);
+  maintain_symmetry (self, self->context);
+  maintain_automatic (self, self->context);
+
+  gtk_widget_queue_draw (GTK_WIDGET (self));
+}
+
+static void
+set_operation (GSimpleAction *action,
+               GVariant      *value,
+               gpointer       data)
+{
+  CurveEditor *self = CURVE_EDITOR (data);
+  Segment *seg = get_segment (self, self->context);
+
+  seg->op = op_from_string (g_variant_get_string (value, NULL));
+
+  if (seg->op == GSK_PATH_CONIC && seg->weight == 0)
+    seg->weight = 1;
+
+  maintain_conic (self, self->context);
+
+  maintain_smoothness (self, self->context);
+  maintain_smoothness (self, self->context + 1);
+  maintain_symmetry (self, self->context);
+  maintain_symmetry (self, self->context + 1);
+
+  gtk_widget_queue_draw (GTK_WIDGET (self));
+}
+
+static void
+insert_new_point (GSimpleAction *action,
+                  GVariant      *value,
+                  gpointer       data)
+{
+  CurveEditor *self = CURVE_EDITOR (data);
+
+  insert_point (self, self->context, self->context_pos);
+
+  gtk_widget_queue_draw (GTK_WIDGET (self));
+}
+
+static void
+remove_current_point (GSimpleAction *action,
+                      GVariant      *value,
+                      gpointer       data)
+{
+  CurveEditor *self = CURVE_EDITOR (data);
+
+  remove_point (self, self->context);
+
+  gtk_widget_queue_draw (GTK_WIDGET (self));
+}
+
+static void
+toggle_edit_point (GSimpleAction *action,
+                   GVariant      *value,
+                   gpointer       data)
+{
+  CurveEditor *self = CURVE_EDITOR (data);
+
+  if (self->edited_point == self->context)
+    self->edited_point = -1;
+  else
+    {
+      self->edited_point = self->context;
+      self->edited_segment = -1;
+    }
+
+  gtk_widget_queue_draw (GTK_WIDGET (self));
+}
+
+static void
+toggle_edit_segment (GSimpleAction *action,
+                     GVariant      *value,
+                     gpointer       data)
+{
+  CurveEditor *self = CURVE_EDITOR (data);
+
+  if (self->edited_segment == self->context)
+    self->edited_segment = -1;
+  else
+    {
+      self->edited_segment = self->context;
+      self->edited_point = -1;
+    }
+
+  gtk_widget_queue_draw (GTK_WIDGET (self));
+}
+
+static void
+reset_weight (GSimpleAction *action,
+              GVariant      *value,
+              gpointer       data)
+{
+  CurveEditor *self = CURVE_EDITOR (data);
+  Segment *seg = get_segment (self, self->context);
+
+  seg->weight = 1;
+  maintain_conic (self, self->context);
+  gtk_widget_queue_draw (GTK_WIDGET (self));
+}
+
+/* }}} */
+/* {{{ Event handlers */
+
+static void
+pressed (GtkGestureClick *gesture,
+         int              n_press,
+         double           x,
+         double           y,
+         CurveEditor      *self)
+{
+  graphene_point_t m = GRAPHENE_POINT_INIT (x, y);
+  int i;
+  int button = gtk_gesture_single_get_current_button (GTK_GESTURE_SINGLE (gesture));
+  float t;
+
+  if (!self->edit)
+    return;
+
+  if (button == GDK_BUTTON_SECONDARY)
+    {
+      for (i = 0; i < self->segments->len; i++)
+        {
+          Segment *seg = get_segment (self, i);
+          const graphene_point_t *p = get_line_point (self, i);
+
+          if (graphene_point_distance (p, &m, NULL, NULL) < CLICK_RADIUS)
+            {
+              GAction *action;
+
+              self->context = i;
+
+              action = g_action_map_lookup_action (self->actions, "set-segment-type");
+              g_simple_action_set_enabled (G_SIMPLE_ACTION (action), FALSE);
+
+              action = g_action_map_lookup_action (self->actions, "add-point");
+              g_simple_action_set_enabled (G_SIMPLE_ACTION (action), FALSE);
+
+              action = g_action_map_lookup_action (self->actions, "remove-point");
+              g_simple_action_set_enabled (G_SIMPLE_ACTION (action), TRUE);
+
+              action = g_action_map_lookup_action (self->actions, "reset-weight");
+              g_simple_action_set_enabled (G_SIMPLE_ACTION (action), FALSE);
+
+              action = g_action_map_lookup_action (self->actions, "set-point-type");
+              g_simple_action_set_enabled (G_SIMPLE_ACTION (action), TRUE);
+              g_simple_action_set_state (G_SIMPLE_ACTION (action), g_variant_new_string 
(point_type_to_string (seg->type)));
+
+              action = g_action_map_lookup_action (self->actions, "edit-point");
+              g_simple_action_set_enabled (G_SIMPLE_ACTION (action), TRUE);
+              g_simple_action_set_state (G_SIMPLE_ACTION (action), g_variant_new_boolean (self->edited_point 
== i));
+
+              action = g_action_map_lookup_action (self->actions, "edit-segment");
+              g_simple_action_set_enabled (G_SIMPLE_ACTION (action), FALSE);
+
+              gtk_popover_set_pointing_to (GTK_POPOVER (self->menu),
+                                           &(const GdkRectangle){ x, y, 1, 1 });
+              gtk_popover_popup (GTK_POPOVER (self->menu));
+              return;
+            }
+        }
+
+      if (find_closest_segment (self, &m, CLICK_RADIUS, NULL, &i, &t))
+        {
+          GAction *action;
+
+          self->context = i;
+          self->context_pos = t;
+
+          action = g_action_map_lookup_action (self->actions, "set-point-type");
+          g_simple_action_set_enabled (G_SIMPLE_ACTION (action), FALSE);
+
+          action = g_action_map_lookup_action (self->actions, "edit-point");
+          g_simple_action_set_enabled (G_SIMPLE_ACTION (action), FALSE);
+
+          action = g_action_map_lookup_action (self->actions, "remove-point");
+          g_simple_action_set_enabled (G_SIMPLE_ACTION (action), FALSE);
+
+          action = g_action_map_lookup_action (self->actions, "add-point");
+          g_simple_action_set_enabled (G_SIMPLE_ACTION (action), TRUE);
+
+          action = g_action_map_lookup_action (self->actions, "edit-segment");
+          g_simple_action_set_enabled (G_SIMPLE_ACTION (action), TRUE);
+          g_simple_action_set_state (G_SIMPLE_ACTION (action), g_variant_new_boolean (self->edited_segment 
== i));
+
+          action = g_action_map_lookup_action (self->actions, "reset-weight");
+          g_simple_action_set_enabled (G_SIMPLE_ACTION (action),
+                                       get_segment (self, i)->op == GSK_PATH_CONIC);
+
+          action = g_action_map_lookup_action (self->actions, "set-segment-type");
+          g_simple_action_set_enabled (G_SIMPLE_ACTION (action), TRUE);
+          g_simple_action_set_state (G_SIMPLE_ACTION (action), g_variant_new_string (op_to_string 
(get_segment (self, i)->op)));
+
+          gtk_popover_set_pointing_to (GTK_POPOVER (self->menu),
+                                           &(const GdkRectangle){ x, y, 1, 1 });
+          gtk_popover_popup (GTK_POPOVER (self->menu));
+          return;
+        }
+    }
+}
+
+static void
+released (GtkGestureClick *gesture,
+          int              n_press,
+          double           x,
+          double           y,
+          CurveEditor      *self)
+{
+  graphene_point_t m = GRAPHENE_POINT_INIT (x, y);
+  int button = gtk_gesture_single_get_current_button (GTK_GESTURE_SINGLE (gesture));
+  int i;
+
+  if (!self->edit)
+    return;
+
+  for (i = 0; i < self->segments->len; i++)
+    {
+      const graphene_point_t *p = get_line_point (self, i);
+
+      if (graphene_point_distance (p, &m, NULL, NULL) < CLICK_RADIUS)
+        {
+          if (button == GDK_BUTTON_PRIMARY)
+            {
+              if (self->edited_point == i)
+                self->edited_point = -1;
+              else
+                {
+                  self->edited_point = i;
+                  self->edited_segment = -1;
+                }
+              gtk_widget_queue_draw (GTK_WIDGET (self));
+              return;
+            }
+        }
+    }
+
+  if (button == GDK_BUTTON_PRIMARY)
+    {
+      float t;
+      int point;
+
+      if (find_closest_segment (self, &m, CLICK_RADIUS, NULL, &point, &t))
+        {
+          self->dragged = -1;
+          self->molded = -1;
+          insert_point (self, point, t);
+        }
+    }
+}
+
+static void
+motion (GtkEventControllerMotion *controller,
+        double                    x,
+        double                    y,
+        CurveEditor              *self)
+{
+  graphene_point_t m = GRAPHENE_POINT_INIT (x, y);
+  int i, j;
+  gboolean changed = FALSE;
+
+  if (self->edit)
+    {
+      for (i = 0; i < self->segments->len; i++)
+        {
+          Segment *seg = get_segment (self, i);
+          int hovered = -1;
+
+          for (j = 0; j < 3; j++)
+            {
+              const graphene_point_t *q = &seg->p[j];
+
+              if (!point_is_visible (self, i, j))
+                continue;
+
+              if (graphene_point_distance (q, &m, NULL, NULL) < CLICK_RADIUS)
+                {
+                  hovered = j;
+                  break;
+                }
+            }
+          if (seg->hovered != hovered)
+            {
+              seg->hovered = hovered;
+              changed = TRUE;
+            }
+        }
+    }
+
+  if (changed)
+    gtk_widget_queue_draw (GTK_WIDGET (self));
+}
+
+static void
+leave (GtkEventController *controller,
+       CurveEditor        *self)
+{
+  int i;
+  gboolean changed = FALSE;
+
+  for (i = 0; i < self->segments->len; i++)
+    {
+      Segment *seg = get_segment (self, i);
+      if (seg->hovered != -1)
+        {
+          seg->hovered = -1;
+          changed = TRUE;
+        }
+    }
+
+  if (changed)
+    gtk_widget_queue_draw (GTK_WIDGET (self));
+}
+
+/* }}} */
+/* {{{ Snapshot */
+
+static void
+add_diamond (GskPathBuilder   *builder,
+             graphene_point_t *center,
+             float             radius)
+{
+  float r = radius * 2 / (1 + M_SQRT2);
+
+  gsk_path_builder_move_to (builder, center->x, center->y - r * M_SQRT2);
+  gsk_path_builder_line_to (builder, center->x + r * M_SQRT2, center->y);
+  gsk_path_builder_line_to (builder, center->x, center->y + r * M_SQRT2);
+  gsk_path_builder_line_to (builder, center->x - r * M_SQRT2, center->y);
+  gsk_path_builder_close (builder);
+}
+
+static void
+add_square (GskPathBuilder   *builder,
+            graphene_point_t *center,
+            float             radius)
+{
+  float r = radius * 2 / (1 + M_SQRT2);
+
+  gsk_path_builder_move_to (builder, center->x - r, center->y - r);
+  gsk_path_builder_line_to (builder, center->x + r, center->y - r);
+  gsk_path_builder_line_to (builder, center->x + r, center->y + r);
+  gsk_path_builder_line_to (builder, center->x - r, center->y + r);
+  gsk_path_builder_close (builder);
+}
+
+static void
+curve_editor_snapshot (GtkWidget   *widget,
+                       GtkSnapshot *snapshot)
+{
+  CurveEditor *self = (CurveEditor *)widget;
+  GskPathBuilder *builder;
+  GskPath *path;
+  GskStroke *stroke;
+  int i, j, k;
+  float width;
+  float height;
+
+  if (self->segments->len == 0)
+    return;
+
+  width = gtk_widget_get_width (widget);
+  height = gtk_widget_get_width (widget);
+
+  /* Add the curve itself */
+
+  builder = gsk_path_builder_new ();
+
+  curve_editor_add_path (self, builder);
+
+  path = gsk_path_builder_free_to_path (builder);
+
+  stroke = gsk_stroke_copy (self->stroke);
+  if (self->show_outline)
+    gsk_stroke_set_line_width (stroke, 1.0);
+  gtk_snapshot_push_stroke (snapshot, path, stroke);
+  gsk_stroke_free (stroke);
+
+  gtk_snapshot_append_color (snapshot,
+                             &self->color,
+                             &GRAPHENE_RECT_INIT (0, 0, width, height ));
+
+  gtk_snapshot_pop (snapshot);
+
+  if (self->show_outline)
+    {
+      GskPath *path2;
+
+      path2 = gsk_path_stroke (path, self->stroke);
+
+      stroke = gsk_stroke_copy (self->stroke);
+      gsk_stroke_set_line_width (stroke, 1.0);
+      gsk_stroke_set_dash (stroke, NULL, 0);
+      gtk_snapshot_push_stroke (snapshot, path2, stroke);
+      gsk_stroke_free (stroke);
+
+      gtk_snapshot_append_color (snapshot,
+                                 &(GdkRGBA){ 0, 0, 0, 1 },
+                                 &GRAPHENE_RECT_INIT (0, 0, width, height ));
+
+      gtk_snapshot_pop (snapshot);
+
+      gsk_path_unref (path2);
+    }
+
+  gsk_path_unref (path);
+
+  if (self->edit)
+    {
+      builder = gsk_path_builder_new ();
+
+      if (self->edited_point != -1)
+        {
+          /* Add the skeleton */
+          Segment *seg = get_segment (self, self->edited_point);
+          Segment *seg1 = get_segment (self, self->edited_point - 1);
+          const graphene_point_t *p = get_line_point (self, self->edited_point);
+
+          if (seg1->op == GSK_PATH_CURVE)
+            {
+              graphene_point_t *c = &seg1->p[2];
+              gsk_path_builder_move_to (builder, c->x, c->y);
+              gsk_path_builder_line_to (builder, p->x, p->y);
+            }
+          else if (seg1->op == GSK_PATH_CONIC)
+            {
+              graphene_point_t *c = &seg1->p[1];
+              gsk_path_builder_move_to (builder, c->x, c->y);
+              gsk_path_builder_line_to (builder, p->x, p->y);
+            }
+
+          if (seg->op == GSK_PATH_CURVE)
+            {
+              graphene_point_t *c = &seg->p[1];
+              gsk_path_builder_move_to (builder, c->x, c->y);
+              gsk_path_builder_line_to (builder, p->x, p->y);
+            }
+          else if (seg->op == GSK_PATH_CONIC)
+            {
+              graphene_point_t *c = &seg->p[1];
+              gsk_path_builder_move_to (builder, p->x, p->y);
+              gsk_path_builder_line_to (builder, c->x, c->y);
+            }
+        }
+
+      if (self->edited_segment != -1)
+        {
+          Segment *seg = get_segment (self, self->edited_segment);
+
+          if (seg->op == GSK_PATH_CURVE)
+            {
+              gsk_path_builder_move_to (builder, seg->p[0].x, seg->p[0].y);
+              gsk_path_builder_line_to (builder, seg->p[1].x, seg->p[1].y);
+              gsk_path_builder_line_to (builder, seg->p[2].x, seg->p[2].y);
+              gsk_path_builder_line_to (builder, seg->p[3].x, seg->p[3].y);
+            }
+          else if (seg->op == GSK_PATH_CONIC)
+            {
+              gsk_path_builder_move_to (builder, seg->p[0].x, seg->p[0].y);
+              gsk_path_builder_line_to (builder, seg->p[1].x, seg->p[1].y);
+              gsk_path_builder_line_to (builder, seg->p[3].x, seg->p[3].y);
+            }
+        }
+
+      path = gsk_path_builder_free_to_path (builder);
+
+      if (self->edited_point != -1 || self->edited_segment != -1)
+        {
+          stroke = gsk_stroke_new (1);
+          gtk_snapshot_push_stroke (snapshot, path, stroke);
+          gsk_stroke_free (stroke);
+
+          gtk_snapshot_append_color (snapshot,
+                                     &(GdkRGBA){ 0, 0, 0, 1 },
+                                     &GRAPHENE_RECT_INIT (0, 0, width, height ));
+
+          gtk_snapshot_pop (snapshot);
+        }
+
+      gsk_path_unref (path);
+
+      /* Draw the circles, in several passes, one for each color */
+
+      const char *colors[] = {
+        "red", /* hovered */
+        "white"   /* smooth curve points */
+      };
+      GdkRGBA color;
+
+       for (k = 0; k < 2; k++)
+        {
+          builder = gsk_path_builder_new ();
+
+          for (i = 0; i < self->segments->len; i++)
+            {
+              Segment *seg = get_segment (self, i);
+
+              for (j = 0; j < 3; j++)
+                {
+                  graphene_point_t *p = &seg->p[j];
+
+                  if (!point_is_visible (self, i, j))
+                    continue;
+
+                  if ((k == 0 && j != seg->hovered) ||
+                      (k == 1 && j == seg->hovered))
+                    continue;
+
+                  if (j != 0)
+                    {
+                      gsk_path_builder_add_circle (builder, p, DRAW_RADIUS);
+                    }
+                  else
+                    {
+                      switch (seg->type)
+                        {
+                        case CUSP:
+                          add_diamond (builder, p, DRAW_RADIUS);
+                          break;
+
+                        case SMOOTH:
+                          add_square (builder, p, DRAW_RADIUS);
+                          break;
+                        case SYMMETRIC:
+                        case AUTO:
+                          gsk_path_builder_add_circle (builder, p, DRAW_RADIUS);
+                          break;
+                        default:
+                          g_assert_not_reached ();
+                        }
+                    }
+                }
+            }
+
+          path = gsk_path_builder_free_to_path (builder);
+
+          gtk_snapshot_push_fill (snapshot, path, GSK_FILL_RULE_WINDING);
+          gdk_rgba_parse (&color, colors[k]);
+          gtk_snapshot_append_color (snapshot,
+                                     &color,
+                                     &GRAPHENE_RECT_INIT (0, 0, width, height));
+          gtk_snapshot_pop (snapshot);
+
+          stroke = gsk_stroke_new (1.0);
+          gtk_snapshot_push_stroke (snapshot, path, stroke);
+          gsk_stroke_free (stroke);
+
+          gtk_snapshot_append_color (snapshot,
+                                     &(GdkRGBA){ 0, 0, 0, 1 },
+                                     &GRAPHENE_RECT_INIT (0, 0, width, height));
+          gtk_snapshot_pop (snapshot);
+
+          gsk_path_unref (path);
+        }
+    }
+}
+
+/* }}} */
+/* {{{ GtkWidget boilerplate */
+
+static void
+curve_editor_measure (GtkWidget      *widget,
+                      GtkOrientation  orientation,
+                      int             for_size,
+                      int            *minimum_size,
+                      int            *natural_size,
+                      int            *minimum_baseline,
+                      int            *natural_baseline)
+{
+  *minimum_size = 100;
+  *natural_size = 200;
+}
+
+static void
+curve_editor_size_allocate (GtkWidget *widget,
+                           int        width,
+                           int        height,
+                           int        baseline)
+{
+  CurveEditor *self = CURVE_EDITOR (widget);
+
+  gtk_popover_present (GTK_POPOVER (self->menu));
+}
+
+/* }}} */
+/* {{{ GObject boilerplate */
+
+static void
+curve_editor_dispose (GObject *object)
+{
+  CurveEditor *self = CURVE_EDITOR (object);
+
+  g_clear_pointer (&self->segments, g_array_unref);
+  g_clear_pointer (&self->menu, gtk_widget_unparent);
+  g_clear_object (&self->actions);
+
+  G_OBJECT_CLASS (curve_editor_parent_class)->dispose (object);
+}
+
+static void
+curve_editor_class_init (CurveEditorClass *class)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (class);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (class);
+
+  object_class->dispose = curve_editor_dispose;
+
+  widget_class->snapshot = curve_editor_snapshot;
+  widget_class->measure = curve_editor_measure;
+  widget_class->size_allocate = curve_editor_size_allocate;
+}
+
+/* }}} */
+/* {{{ Setup */
+
+static void
+curve_editor_init (CurveEditor *self)
+{
+  GtkEventController *controller;
+  GMenu *menu;
+  GMenu *section;
+  GMenuItem *item;
+  GSimpleAction *action;
+
+  self->segments = g_array_new (FALSE, FALSE, sizeof (Segment));
+  self->dragged = -1;
+  self->molded = -1;
+  self->edited_point = -1;
+  self->edited_segment = -1;
+  self->edit = FALSE;
+  self->stroke = gsk_stroke_new (1.0);
+  self->color = (GdkRGBA){ 0, 0, 0, 1 };
+
+  controller = GTK_EVENT_CONTROLLER (gtk_gesture_drag_new ());
+  gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (controller), GDK_BUTTON_PRIMARY);
+  g_signal_connect (controller, "drag-begin", G_CALLBACK (drag_begin), self);
+  g_signal_connect (controller, "drag-update", G_CALLBACK (drag_update), self);
+  g_signal_connect (controller, "drag-end", G_CALLBACK (drag_end), self);
+  gtk_widget_add_controller (GTK_WIDGET (self), controller);
+
+  controller = GTK_EVENT_CONTROLLER (gtk_gesture_click_new ());
+  gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (controller), 0);
+  g_signal_connect (controller, "pressed", G_CALLBACK (pressed), self);
+  g_signal_connect (controller, "released", G_CALLBACK (released), self);
+  gtk_widget_add_controller (GTK_WIDGET (self), controller);
+
+  controller = gtk_event_controller_motion_new ();
+  g_signal_connect (controller, "motion", G_CALLBACK (motion), self);
+  g_signal_connect (controller, "leave", G_CALLBACK (leave), self);
+  gtk_widget_add_controller (GTK_WIDGET (self), controller);
+
+  self->actions = G_ACTION_MAP (g_simple_action_group_new ());
+
+  action = g_simple_action_new_stateful ("set-point-type", G_VARIANT_TYPE_STRING, g_variant_new_string 
("smooth"));
+  g_signal_connect (action, "change-state", G_CALLBACK (set_point_type), self);
+  g_action_map_add_action (G_ACTION_MAP (self->actions), G_ACTION (action));
+  gtk_widget_insert_action_group (GTK_WIDGET (self), "point", G_ACTION_GROUP (self->actions));
+
+  action = g_simple_action_new_stateful ("set-segment-type", G_VARIANT_TYPE_STRING, g_variant_new_string 
("curve"));
+  g_signal_connect (action, "change-state", G_CALLBACK (set_operation), self);
+  g_action_map_add_action (G_ACTION_MAP (self->actions), G_ACTION (action));
+
+  action = g_simple_action_new_stateful ("edit-point", NULL, g_variant_new_boolean (FALSE));
+  g_signal_connect (action, "change-state", G_CALLBACK (toggle_edit_point), self);
+  g_action_map_add_action (G_ACTION_MAP (self->actions), G_ACTION (action));
+
+  action = g_simple_action_new_stateful ("edit-segment", NULL, g_variant_new_boolean (FALSE));
+  g_signal_connect (action, "change-state", G_CALLBACK (toggle_edit_segment), self);
+  g_action_map_add_action (G_ACTION_MAP (self->actions), G_ACTION (action));
+
+  action = g_simple_action_new ("add-point", NULL);
+  g_signal_connect (action, "activate", G_CALLBACK (insert_new_point), self);
+  g_action_map_add_action (G_ACTION_MAP (self->actions), G_ACTION (action));
+
+  action = g_simple_action_new ("remove-point", NULL);
+  g_signal_connect (action, "activate", G_CALLBACK (remove_current_point), self);
+  g_action_map_add_action (G_ACTION_MAP (self->actions), G_ACTION (action));
+
+  action = g_simple_action_new ("reset-weight", NULL);
+  g_signal_connect (action, "activate", G_CALLBACK (reset_weight), self);
+  g_action_map_add_action (G_ACTION_MAP (self->actions), G_ACTION (action));
+
+  gtk_widget_insert_action_group (GTK_WIDGET (self), "path", G_ACTION_GROUP (self->actions));
+
+  menu = g_menu_new ();
+
+  section = g_menu_new ();
+
+  item = g_menu_item_new ("Cusp", "path.set-point-type::cusp");
+  g_menu_item_set_attribute_value (item, "hidden-when", g_variant_new_string ("action-disabled"));
+  g_menu_append_item (section, item);
+  g_object_unref (item);
+
+  item = g_menu_item_new ("Smooth", "path.set-point-type::smooth");
+  g_menu_item_set_attribute_value (item, "hidden-when", g_variant_new_string ("action-disabled"));
+  g_menu_append_item (section, item);
+  g_object_unref (item);
+
+  item = g_menu_item_new ("Symmetric", "path.set-point-type::symmetric");
+  g_menu_item_set_attribute_value (item, "hidden-when", g_variant_new_string ("action-disabled"));
+  g_menu_append_item (section, item);
+  g_object_unref (item);
+
+  item = g_menu_item_new ("Automatic", "path.set-point-type::auto");
+  g_menu_item_set_attribute_value (item, "hidden-when", g_variant_new_string ("action-disabled"));
+  g_menu_append_item (section, item);
+  g_object_unref (item);
+
+  g_menu_append_section (menu, NULL, G_MENU_MODEL (section));
+  g_object_unref (section);
+
+  section = g_menu_new ();
+
+  item = g_menu_item_new ("Line", "path.set-segment-type::line");
+  g_menu_item_set_attribute_value (item, "hidden-when", g_variant_new_string ("action-disabled"));
+  g_menu_append_item (section, item);
+  g_object_unref (item);
+
+  item = g_menu_item_new ("Curve", "path.set-segment-type::curve");
+  g_menu_item_set_attribute_value (item, "hidden-when", g_variant_new_string ("action-disabled"));
+  g_menu_append_item (section, item);
+  g_object_unref (item);
+
+  item = g_menu_item_new ("Conic", "path.set-segment-type::conic");
+  g_menu_item_set_attribute_value (item, "hidden-when", g_variant_new_string ("action-disabled"));
+  g_menu_append_item (section, item);
+  g_object_unref (item);
+
+  g_menu_append_section (menu, NULL, G_MENU_MODEL (section));
+  g_object_unref (section);
+
+  section = g_menu_new ();
+
+  item = g_menu_item_new ("Edit", "path.edit-point");
+  g_menu_item_set_attribute_value (item, "hidden-when", g_variant_new_string ("action-disabled"));
+  g_menu_append_item (section, item);
+  g_object_unref (item);
+
+  item = g_menu_item_new ("Edit", "path.edit-segment");
+  g_menu_item_set_attribute_value (item, "hidden-when", g_variant_new_string ("action-disabled"));
+  g_menu_append_item (section, item);
+  g_object_unref (item);
+
+  g_menu_append_section (menu, NULL, G_MENU_MODEL (section));
+  g_object_unref (section);
+
+  section = g_menu_new ();
+
+  item = g_menu_item_new ("Add", "path.add-point");
+  g_menu_item_set_attribute_value (item, "hidden-when", g_variant_new_string ("action-disabled"));
+  g_menu_append_item (section, item);
+  g_object_unref (item);
+
+  item = g_menu_item_new ("Remove", "path.remove-point");
+  g_menu_item_set_attribute_value (item, "hidden-when", g_variant_new_string ("action-disabled"));
+  g_menu_append_item (section, item);
+  g_object_unref (item);
+
+  item = g_menu_item_new ("Reset", "path.reset-weight");
+  g_menu_item_set_attribute_value (item, "hidden-when", g_variant_new_string ("action-disabled"));
+  g_menu_append_item (section, item);
+  g_object_unref (item);
+
+  g_menu_append_section (menu, NULL, G_MENU_MODEL (section));
+  g_object_unref (section);
+
+  self->menu = gtk_popover_menu_new_from_model (G_MENU_MODEL (menu));
+  g_object_unref (menu);
+
+  gtk_widget_set_parent (self->menu, GTK_WIDGET (self));
+}
+
+/* }}} */
+/* {{{ API */
+
+GtkWidget *
+curve_editor_new (void)
+{
+  return g_object_new (curve_editor_get_type (), NULL);
+}
+
+void
+curve_editor_set_edit (CurveEditor *self,
+                       gboolean     edit)
+{
+  self->edit = edit;
+  self->edited_point = -1;
+  self->edited_segment = -1;
+
+  gtk_widget_queue_draw (GTK_WIDGET (self));
+}
+
+static gboolean
+copy_segments (GskPathOperation        op,
+               const graphene_point_t *pts,
+               gsize                   n_pts,
+               float                   weight,
+               gpointer                data)
+{
+  CurveEditor *self = data;
+  Segment seg;
+
+  seg.op = op;
+  seg.hovered = -1;
+  seg.dragged = -1;
+
+  switch (op)
+    {
+    case GSK_PATH_MOVE:
+      break;
+
+    case GSK_PATH_CLOSE:
+      seg.p[0] = pts[0];
+      seg.p[3] = pts[1];
+      g_array_append_val (self->segments, seg);
+      break;
+
+    case GSK_PATH_LINE:
+      seg.p[0] = pts[0];
+      seg.p[3] = pts[1];
+      g_array_append_val (self->segments, seg);
+      break;
+
+    case GSK_PATH_CURVE:
+      seg.p[0] = pts[0];
+      seg.p[1] = pts[1];
+      seg.p[2] = pts[2];
+      seg.p[3] = pts[3];
+      g_array_append_val (self->segments, seg);
+      break;
+
+    case GSK_PATH_CONIC:
+      {
+        seg.p[0] = pts[0];
+        seg.p[1] = pts[1];
+        seg.p[3] = pts[2];
+        seg.weight = weight;
+
+        get_conic_shoulder_point (pts, weight, &seg.p[2]);
+
+        g_array_append_val (self->segments, seg);
+      }
+      break;
+
+    default:
+      g_assert_not_reached ();
+    }
+
+  return TRUE;
+}
+void
+curve_editor_set_path (CurveEditor *self,
+                       GskPath     *path)
+{
+  int i;
+  Segment *first, *last;
+
+  g_array_set_size (self->segments, 0);
+
+  gsk_path_foreach (path, GSK_PATH_FOREACH_ALLOW_CURVE | GSK_PATH_FOREACH_ALLOW_CONIC, copy_segments, self);
+
+  first = get_segment (self, 0);
+  last = get_segment (self, self->segments->len - 1);
+  if (last->op == GSK_PATH_CLOSE)
+    {
+      if (graphene_point_near (&last->p[0], &last->p[3], 0.001))
+        g_array_remove_index (self->segments, self->segments->len - 1);
+      else
+        last->op = GSK_PATH_LINE;
+    }
+  else
+    {
+      Segment seg;
+
+      seg.op = GSK_PATH_MOVE;
+      seg.p[0] = last->p[3];
+      seg.p[3] = first->p[0];
+      g_array_append_val (self->segments, seg);
+    }
+
+  for (i = 0; i < self->segments->len; i++)
+    check_smoothness (self, i);
+
+  gtk_widget_queue_draw (GTK_WIDGET (self));
+}
+
+GskPath *
+curve_editor_get_path (CurveEditor *self)
+{
+  GskPathBuilder *builder;
+
+  builder = gsk_path_builder_new ();
+
+  curve_editor_add_path (self, builder);
+
+  return gsk_path_builder_free_to_path (builder);
+}
+
+void
+curve_editor_set_stroke (CurveEditor *self,
+                         GskStroke   *stroke)
+{
+  gsk_stroke_free (self->stroke);
+  self->stroke = gsk_stroke_copy (stroke);
+
+  gtk_widget_queue_draw (GTK_WIDGET (self));
+}
+
+const GskStroke *
+curve_editor_get_stroke (CurveEditor *self)
+{
+  return self->stroke;
+}
+
+void
+curve_editor_set_color (CurveEditor *self,
+                        GdkRGBA     *color)
+{
+  self->color = *color;
+
+  gtk_widget_queue_draw (GTK_WIDGET (self));
+}
+
+const GdkRGBA *
+curve_editor_get_color (CurveEditor *self)
+{
+  return &self->color;
+}
+
+void
+curve_editor_set_show_outline (CurveEditor *self,
+                               gboolean     show_outline)
+{
+  self->show_outline = show_outline;
+
+  gtk_widget_queue_draw (GTK_WIDGET (self));
+}
+
+gboolean
+curve_editor_get_show_outline (CurveEditor *self)
+{
+  return self->show_outline;
+}
+
+/* }}} */
+
+/* vim:set foldmethod=marker expandtab: */
diff --git a/demos/gtk-demo/curve-editor.h b/demos/gtk-demo/curve-editor.h
new file mode 100644
index 0000000000..e62738ccbc
--- /dev/null
+++ b/demos/gtk-demo/curve-editor.h
@@ -0,0 +1,36 @@
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define CURVE_TYPE_EDITOR (curve_editor_get_type ())
+G_DECLARE_FINAL_TYPE (CurveEditor, curve_editor, CURVE, EDITOR, GtkWidget)
+
+GtkWidget *       curve_editor_new              (void);
+
+void              curve_editor_set_edit         (CurveEditor *self,
+                                                 gboolean     edit);
+
+void              curve_editor_set_path         (CurveEditor *self,
+                                                 GskPath     *path);
+
+GskPath *         curve_editor_get_path         (CurveEditor *self);
+
+void              curve_editor_set_stroke       (CurveEditor *self,
+                                                 GskStroke   *stroke);
+
+const GskStroke * curve_editor_get_stroke       (CurveEditor *self);
+
+
+void              curve_editor_set_color        (CurveEditor *self,
+                                                 GdkRGBA     *color);
+
+const GdkRGBA *   curve_editor_get_color        (CurveEditor *self);
+
+gboolean          curve_editor_get_show_outline (CurveEditor *self);
+
+void              curve_editor_set_show_outline (CurveEditor *self,
+                                                 gboolean     show_outline);
+
+G_END_DECLS
diff --git a/demos/gtk-demo/curve.c b/demos/gtk-demo/curve.c
new file mode 100644
index 0000000000..5974838b88
--- /dev/null
+++ b/demos/gtk-demo/curve.c
@@ -0,0 +1,272 @@
+/* Path/Curve Editor
+ *
+ * This demo shows an elaborate curve editor that you would expect to find
+ * in a vector graphics editor. It is built on top of GTK's path APIs.
+ */
+
+#include <gtk/gtk.h>
+#include "curve-editor.h"
+
+
+static GskPath *
+make_circle_path (void)
+{
+  float w = 310;
+  float h = 310;
+  float cx = w / 2;
+  float cy = h / 2;
+  float pad = 20;
+  float r = (w - 2 * pad) / 2;
+  float k = 0.55228;
+  float kr = k  * r;
+  GskPathBuilder *builder;
+
+  builder = gsk_path_builder_new ();
+
+  gsk_path_builder_move_to (builder,  cx, pad);
+  gsk_path_builder_curve_to (builder, cx + kr, pad,
+                                      w - pad, cy - kr,
+                                      w - pad, cy);
+  gsk_path_builder_curve_to (builder, w - pad, cy + kr,
+                                      cx + kr, h - pad,
+                                      cx, h - pad);
+  gsk_path_builder_curve_to (builder, cx - kr, h - pad,
+                                      pad, cy + kr,
+                                      pad, cy);
+  gsk_path_builder_curve_to (builder, pad, cy - kr,
+                                      cx - kr, pad,
+                                      cx, pad);
+  gsk_path_builder_close (builder);
+
+  return gsk_path_builder_free_to_path (builder);
+}
+
+static void
+edit_changed (GtkToggleButton *button,
+              GParamSpec      *pspec,
+              CurveEditor     *editor)
+{
+  curve_editor_set_edit (editor, gtk_toggle_button_get_active (button));
+}
+
+static void
+reset (GtkButton   *button,
+       CurveEditor *editor)
+{
+  GskPath *path;
+
+  path = make_circle_path ();
+  curve_editor_set_path (editor, path);
+  gsk_path_unref (path);
+}
+
+static void
+line_width_changed (GtkSpinButton *spin,
+                    CurveEditor   *editor)
+{
+  GskStroke *stroke;
+
+  stroke = gsk_stroke_copy (curve_editor_get_stroke (editor));
+  gsk_stroke_set_line_width (stroke, gtk_spin_button_get_value (spin));
+  curve_editor_set_stroke (editor, stroke);
+  gsk_stroke_free (stroke);
+}
+
+static void
+cap_changed (GtkDropDown *combo,
+             GParamSpec  *pspec,
+             CurveEditor *editor)
+{
+  GskStroke *stroke;
+
+  stroke = gsk_stroke_copy (curve_editor_get_stroke (editor));
+  gsk_stroke_set_line_cap (stroke, (GskLineCap)gtk_drop_down_get_selected (combo));
+  curve_editor_set_stroke (editor, stroke);
+  gsk_stroke_free (stroke);
+}
+
+static void
+join_changed (GtkDropDown *combo,
+              GParamSpec  *pspec,
+              CurveEditor *editor)
+{
+  GskStroke *stroke;
+
+  stroke = gsk_stroke_copy (curve_editor_get_stroke (editor));
+  gsk_stroke_set_line_join (stroke, (GskLineJoin)gtk_drop_down_get_selected (combo));
+  curve_editor_set_stroke (editor, stroke);
+  gsk_stroke_free (stroke);
+}
+
+static void
+color_changed (GtkColorChooser *chooser,
+               GParamSpec      *pspec,
+               CurveEditor     *editor)
+{
+  GdkRGBA color;
+
+  gtk_color_chooser_get_rgba (chooser, &color);
+  curve_editor_set_color (editor, &color);
+}
+
+static void
+stroke_toggled (GtkCheckButton *button,
+                CurveEditor    *editor)
+{
+  curve_editor_set_show_outline (editor, gtk_check_button_get_active (button));
+  gtk_widget_queue_draw (GTK_WIDGET (editor));
+}
+
+static void
+limit_changed (GtkSpinButton *spin,
+               CurveEditor   *editor)
+{
+  GskStroke *stroke;
+
+  stroke = gsk_stroke_copy (curve_editor_get_stroke (editor));
+  gsk_stroke_set_miter_limit (stroke, gtk_spin_button_get_value (spin));
+  curve_editor_set_stroke (editor, stroke);
+  gsk_stroke_free (stroke);
+}
+
+static void
+dashes_changed (GtkEntry    *entry,
+                GParamSpec  *spec,
+                CurveEditor *editor)
+{
+  const char *text;
+  char **split;
+  GArray *dash;
+  GskStroke *stroke;
+
+  text = gtk_editable_get_text (GTK_EDITABLE (entry));
+  split = g_strsplit (text, " ", 0);
+
+  dash = g_array_new (FALSE, FALSE, sizeof (float));
+  for (int i = 0; split[i] != NULL; i++)
+    {
+      double d;
+      char *endp = 0;
+
+      d = g_strtod (split[i], &endp);
+      if (*endp == '\0')
+        g_array_append_vals (dash, (float[1]) { d }, 1);
+    }
+
+  g_strfreev (split);
+
+  stroke = gsk_stroke_copy (curve_editor_get_stroke (editor));
+  gsk_stroke_set_dash (stroke, (const float *)dash->data, dash->len);
+  curve_editor_set_stroke (editor, stroke);
+  gsk_stroke_free (stroke);
+
+  g_array_free (dash, TRUE);
+}
+
+GtkWidget *
+do_curve (GtkWidget *do_widget)
+{
+  static GtkWidget *window = NULL;
+  GtkWidget *demo;
+  GtkWidget *edit_toggle;
+  GtkWidget *reset_button;
+  GtkWidget *titlebar;
+  GtkWidget *stroke_toggle;
+  GtkWidget *line_width_spin;
+  GtkWidget *stroke_button;
+  GtkWidget *popover;
+  GtkWidget *grid;
+  GtkWidget *cap_combo;
+  GtkWidget *join_combo;
+  GtkWidget *color_button;
+  GtkWidget *limit_spin;
+  GtkWidget *dash_entry;
+
+  if (!window)
+    {
+      window = gtk_window_new ();
+      gtk_window_set_title (GTK_WINDOW (window), "Curve Editor");
+      g_object_add_weak_pointer (G_OBJECT (window), (gpointer *)&window);
+      gtk_window_set_default_size (GTK_WINDOW (window), 310, 350);
+
+      edit_toggle = gtk_toggle_button_new ();
+      gtk_button_set_icon_name (GTK_BUTTON (edit_toggle), "document-edit-symbolic");
+
+      reset_button = gtk_button_new_from_icon_name ("edit-undo-symbolic");
+
+      stroke_button = gtk_menu_button_new ();
+      gtk_menu_button_set_icon_name (GTK_MENU_BUTTON (stroke_button), "open-menu-symbolic");
+      popover = gtk_popover_new ();
+      gtk_menu_button_set_popover (GTK_MENU_BUTTON (stroke_button), popover);
+
+      grid = gtk_grid_new ();
+      gtk_grid_set_row_spacing (GTK_GRID (grid), 6);
+      gtk_grid_set_column_spacing (GTK_GRID (grid), 6);
+      gtk_popover_set_child (GTK_POPOVER (popover), grid);
+
+      gtk_grid_attach (GTK_GRID (grid), gtk_label_new ("Color:"), 0, 0, 1, 1);
+      color_button = gtk_color_button_new_with_rgba (&(GdkRGBA){ 0., 0., 0., 1.});
+      gtk_grid_attach (GTK_GRID (grid), color_button, 1, 0, 1, 1);
+
+      gtk_grid_attach (GTK_GRID (grid), gtk_label_new ("Line width:"), 0, 1, 1, 1);
+      line_width_spin = gtk_spin_button_new_with_range (1, 20, 1);
+      gtk_spin_button_set_value (GTK_SPIN_BUTTON (line_width_spin), 1);
+      gtk_grid_attach (GTK_GRID (grid), line_width_spin, 1, 1, 1, 1);
+
+      gtk_grid_attach (GTK_GRID (grid), gtk_label_new ("Line cap:"), 0, 2, 1, 1);
+      cap_combo = gtk_drop_down_new_from_strings ((const char *[]){"Butt", "Round", "Square", NULL});
+      gtk_grid_attach (GTK_GRID (grid), cap_combo, 1, 2, 1, 1);
+
+      gtk_grid_attach (GTK_GRID (grid), gtk_label_new ("Line join:"), 0, 3, 1, 1);
+      join_combo = gtk_drop_down_new_from_strings ((const char *[]){"Miter", "Miter-clip", "Round", "Bevel", 
"Arcs", NULL});
+      gtk_grid_attach (GTK_GRID (grid), join_combo, 1, 3, 1, 1);
+
+      gtk_grid_attach (GTK_GRID (grid), gtk_label_new ("Miter limit:"), 0, 4, 1, 1);
+      limit_spin = gtk_spin_button_new_with_range (0, 10, 1);
+      gtk_spin_button_set_digits (GTK_SPIN_BUTTON (limit_spin), 1);
+      gtk_spin_button_set_value (GTK_SPIN_BUTTON (limit_spin), 4);
+      gtk_grid_attach (GTK_GRID (grid), limit_spin, 1, 4, 1, 1);
+
+      gtk_grid_attach (GTK_GRID (grid), gtk_label_new ("Dashes:"), 0, 5, 1, 1);
+      dash_entry = gtk_entry_new ();
+      gtk_grid_attach (GTK_GRID (grid), dash_entry, 1, 5, 1, 1);
+
+      stroke_toggle = gtk_check_button_new_with_label ("Show outline");
+      gtk_grid_attach (GTK_GRID (grid), stroke_toggle, 1, 6, 1, 1);
+
+      titlebar = gtk_header_bar_new ();
+      gtk_header_bar_pack_start (GTK_HEADER_BAR (titlebar), edit_toggle);
+      gtk_header_bar_pack_start (GTK_HEADER_BAR (titlebar), reset_button);
+      gtk_header_bar_pack_start (GTK_HEADER_BAR (titlebar), stroke_button);
+
+      gtk_window_set_titlebar (GTK_WINDOW (window), titlebar);
+
+      demo = curve_editor_new ();
+
+      g_signal_connect (stroke_toggle, "toggled", G_CALLBACK (stroke_toggled), demo);
+      g_signal_connect (edit_toggle, "notify::active", G_CALLBACK (edit_changed), demo);
+      g_signal_connect (reset_button, "clicked", G_CALLBACK (reset), demo);
+      g_signal_connect (cap_combo, "notify::selected", G_CALLBACK (cap_changed), demo);
+      g_signal_connect (join_combo, "notify::selected", G_CALLBACK (join_changed), demo);
+      g_signal_connect (color_button, "notify::rgba", G_CALLBACK (color_changed), demo);
+      g_signal_connect (line_width_spin, "value-changed", G_CALLBACK (line_width_changed), demo);
+      g_signal_connect (limit_spin, "value-changed", G_CALLBACK (limit_changed), demo);
+      g_signal_connect (dash_entry, "notify::text", G_CALLBACK (dashes_changed), demo);
+
+      reset (NULL, CURVE_EDITOR (demo));
+
+      gtk_spin_button_set_value (GTK_SPIN_BUTTON (line_width_spin), 6);
+      gtk_color_chooser_set_rgba (GTK_COLOR_CHOOSER (color_button), &(GdkRGBA) { 1, 0, 0, 1 });
+      gtk_drop_down_set_selected (GTK_DROP_DOWN (cap_combo), GSK_LINE_CAP_ROUND);
+      gtk_editable_set_text (GTK_EDITABLE (dash_entry), "0 8");
+
+      gtk_window_set_child (GTK_WINDOW (window), demo);
+    }
+
+  if (!gtk_widget_get_visible (window))
+    gtk_widget_show (window);
+  else
+    gtk_window_destroy (GTK_WINDOW (window));
+
+  return window;
+}
diff --git a/demos/gtk-demo/demo.gresource.xml b/demos/gtk-demo/demo.gresource.xml
index 5492aed444..be87cbcef3 100644
--- a/demos/gtk-demo/demo.gresource.xml
+++ b/demos/gtk-demo/demo.gresource.xml
@@ -249,6 +249,10 @@
   <gresource prefix="/video-player">
     <file>bbb.png</file>
   </gresource>
+  <gresource prefix="/curve">
+    <file>curve-editor.c</file>
+    <file>curve-editor.h</file>
+  </gresource>
   <gresource prefix="/sources">
     <file>application_demo.c</file>
     <file>assistant.c</file>
@@ -266,6 +270,7 @@
     <file>css_pixbufs.c</file>
     <file>css_shadows.c</file>
     <file>cursors.c</file>
+    <file>curve.c</file>
     <file>dialog.c</file>
     <file>drawingarea.c</file>
     <file>dropdown.c</file>
diff --git a/demos/gtk-demo/meson.build b/demos/gtk-demo/meson.build
index 8b7eb3ef64..98d4fd0f39 100644
--- a/demos/gtk-demo/meson.build
+++ b/demos/gtk-demo/meson.build
@@ -17,6 +17,7 @@ demos = files([
   'css_pixbufs.c',
   'css_shadows.c',
   'cursors.c',
+  'curve.c',
   'dialog.c',
   'drawingarea.c',
   'dnd.c',
@@ -131,6 +132,7 @@ extra_demo_sources = files([
   'script-names.c',
   'unicode-names.c',
   'suggestionentry.c',
+  'curve-editor.c',
 ])
 
 if harfbuzz_dep.found() and pangoft_dep.found()


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]