diff options
author | Matthias Clasen <mclasen@redhat.com> | 2020-11-19 13:41:21 -0500 |
---|---|---|
committer | Matthias Clasen <mclasen@redhat.com> | 2020-12-22 23:50:37 -0500 |
commit | cf3db3c7a14b0cb25b93881d6c2d3efdb3856038 (patch) | |
tree | 05509b7df99f016414dd48e3762eceeb327f65f4 | |
parent | 1ff73f17644296a3377eeb37b5ffe11c149c0e7b (diff) | |
download | gtk+-cf3db3c7a14b0cb25b93881d6c2d3efdb3856038.tar.gz |
Add a path editor demo
Add a simple demo for editing a poly-Bezier curve.
-rw-r--r-- | tests/curve-editor.c | 2619 | ||||
-rw-r--r-- | tests/curve-editor.h | 38 | ||||
-rw-r--r-- | tests/curve.c | 217 | ||||
-rw-r--r-- | tests/meson.build | 1 |
4 files changed, 2875 insertions, 0 deletions
diff --git a/tests/curve-editor.c b/tests/curve-editor.c new file mode 100644 index 0000000000..715b6e6ff6 --- /dev/null +++ b/tests/curve-editor.c @@ -0,0 +1,2619 @@ +#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[3]; + 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; + + 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; + + stroke = gsk_stroke_copy (self->stroke); + gsk_stroke_set_line_width (stroke, 1.0); + + path2 = gsk_path_stroke (path, self->stroke); + 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/tests/curve-editor.h b/tests/curve-editor.h new file mode 100644 index 0000000000..c9fcdf168a --- /dev/null +++ b/tests/curve-editor.h @@ -0,0 +1,38 @@ +#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/tests/curve.c b/tests/curve.c new file mode 100644 index 0000000000..24f93d9dc3 --- /dev/null +++ b/tests/curve.c @@ -0,0 +1,217 @@ +#include <gtk/gtk.h> +#include "curve-editor.h" + + +static GskPath * +make_circle_path (void) +{ + float w = 200; + float h = 200; + 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, + 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); +} + +int +main (int argc, char *argv[]) +{ + GtkWindow *window; + 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; + + gtk_init (); + + window = GTK_WINDOW (gtk_window_new ()); + gtk_window_set_default_size (GTK_WINDOW (window), 250, 250); + + 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", 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); + + stroke_toggle = gtk_check_button_new_with_label ("Show outline"); + gtk_grid_attach (GTK_GRID (grid), stroke_toggle, 1, 5, 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, "color-set", 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); + + reset (NULL, CURVE_EDITOR (demo)); + + gtk_window_set_child (window, demo); + + gtk_window_present (window); + + while (g_list_model_get_n_items (gtk_window_get_toplevels ()) > 0) + g_main_context_iteration (NULL, TRUE); + + return 0; +} diff --git a/tests/meson.build b/tests/meson.build index 432b39e3ac..b0341334ff 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -6,6 +6,7 @@ gtk_tests = [ ['motion-compression'], ['ottie'], ['overlayscroll'], + ['curve', ['curve.c', 'curve-editor.c']], ['curve2'], ['testupload'], ['testtransform'], |