diff options
author | Matthias Clasen <mclasen@redhat.com> | 2020-11-19 13:41:21 -0500 |
---|---|---|
committer | Matthias Clasen <mclasen@redhat.com> | 2020-11-29 22:05:54 -0500 |
commit | 0b1637f46585354228f4bf97dbb5c6af451a13b1 (patch) | |
tree | bddf6df5e8531e4a0eaadb5367c371423e5ee2c5 | |
parent | eb247837133b84d2069afb974a30f87461ad54c0 (diff) | |
download | gtk+-0b1637f46585354228f4bf97dbb5c6af451a13b1.tar.gz |
Add a path editor demomatthiasc/lottie-tests
Add a simple demo for editing a poly-Bezier curve.
It does not handle conic segments yet.
-rw-r--r-- | tests/curve-editor.c | 1816 | ||||
-rw-r--r-- | tests/curve-editor.h | 33 | ||||
-rw-r--r-- | tests/curve.c | 184 | ||||
-rw-r--r-- | tests/meson.build | 1 |
4 files changed, 2034 insertions, 0 deletions
diff --git a/tests/curve-editor.c b/tests/curve-editor.c new file mode 100644 index 0000000000..adf42d7c7d --- /dev/null +++ b/tests/curve-editor.c @@ -0,0 +1,1816 @@ +#include "curve-editor.h" + +#include <gtk/gtk.h> + +#define DRAW_RADIUS 5 +#define CLICK_RADIUS 8 + +/* {{{ Types and structures */ +typedef enum +{ + MOVE, + LINE, + CURVE +} Operation; + +static const char * +op_to_string (Operation op) +{ + switch (op) + { + case MOVE: + return "move"; + case LINE: + return "line"; + case CURVE: + return "curve"; + default: + g_assert_not_reached (); + } +} + +static Operation +op_from_string (const char *s) +{ + if (strcmp (s, "move") == 0) + return MOVE; + else if (strcmp (s, "line") == 0) + return LINE; + else if (strcmp (s, "curve") == 0) + return CURVE; + 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 (); +} + +/* We don't store Bezier segments, but an array of points on + * the line. Each point comes with its two neighboring control + * points, so each Bezier segment contains p[1] and p[2] from + * one point, and p[0] and p[1] from the next. + * + * The control points are irrelevant for MOVE and LINE segments. + */ +typedef struct +{ + /* 0 and 2 are control points, 1 is the point on the line */ + graphene_point_t p[3]; + PointType type; + gboolean edit; + int dragged; + int hovered; + /* refers to the segment following the point */ + Operation op; +} PointData; + +struct _CurveEditor +{ + GtkWidget parent_instance; + GArray *points; + int dragged; + int context; + gboolean edit; + int molded; + + GtkWidget *menu; + GActionMap *actions; + GskStroke *stroke; + GdkRGBA color; +}; + +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); +} + +/* 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); +} +/* }}} */ +/* {{{ Utilities */ +static PointData * +get_point (CurveEditor *self, + int point) +{ + point = point % self->points->len; + if (point < 0) + point += self->points->len; + return &g_array_index (self->points, PointData, point); +} + +static gboolean +point_is_visible (CurveEditor *self, + int point, + int point1) +{ + PointData *pd; + + if (!self->edit) + return FALSE; + + pd = get_point (self, point); + switch (point1) + { + case 0: + if (!pd->edit) + return FALSE; + else + return get_point (self, point - 1)->op == CURVE; + case 1: /* point on curve */ + return TRUE; + case 2: + if (!pd->edit) + return FALSE; + else + return pd->op == CURVE; + default: + g_assert_not_reached (); + } +} + +static void +maintain_smoothness (CurveEditor *self, + int point) +{ + PointData *pd; + Operation op, op1; + graphene_point_t *p, *c, *c2, *p2; + float d; + + pd = get_point (self, point); + + if (pd->type == CUSP) + return; + + op = pd->op; + op1 = get_point (self, point - 1)->op; + + p = &pd->p[1]; + c = &pd->p[0]; + c2 = &pd->p[2]; + + if (op == CURVE && op1 == CURVE) + { + d = graphene_point_distance (c, p, NULL, NULL); + opposite_point (p, c2, d, c); + } + else if (op == CURVE && op1 == LINE) + { + p2 = &get_point (self, point - 1)->p[1]; + d = graphene_point_distance (c2, p, NULL, NULL); + opposite_point (p, p2, d, c2); + } + else if (op == LINE && op1 == CURVE) + { + p2 = &get_point (self, point + 1)->p[1]; + d = graphene_point_distance (c, p, NULL, NULL); + opposite_point (p, p2, d, c); + } +} + +static void +maintain_symmetry (CurveEditor *self, + int point) +{ + PointData *pd; + graphene_point_t *p, *c, *c2; + double l1, l2, l; + + pd = get_point (self, point); + + if (pd->type != SYMMETRIC) + return; + + c = &pd->p[0]; + p = &pd->p[1]; + c2 = &pd->p[2]; + + 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) +{ + PointData *pd, *pd1, *pd2; + double l1, l2; + graphene_point_t a; + + pd = get_point (self, point); + + if (pd->type != AUTO) + return; + + pd1 = get_point (self, point - 1); + pd2 = get_point (self, point + 1); + + l1 = graphene_point_distance (&pd->p[1], &pd1->p[1], NULL, NULL); + l2 = graphene_point_distance (&pd->p[1], &pd2->p[1], NULL, NULL); + + a.x = pd2->p[1].x + (pd->p[1].x - pd1->p[1].x); + a.y = pd2->p[1].y + (pd->p[1].y - pd1->p[1].y); + + scale_point (&pd->p[1], &a, l2/3, &pd->p[2]); + opposite_point (&pd->p[1], &a, l1/3, &pd->p[0]); +} + +static void +maintain_automatic (CurveEditor *self, + int point) +{ + if (get_point (self, point)->op != CURVE || + get_point (self, point - 1)->op != CURVE) + return; + + update_automatic (self, point); + update_automatic (self, point - 1); + update_automatic (self, point + 1); +} + +/* Check if the points arount point currently satisfy + * smoothness conditions. Set PointData.type accordingly. + */ +static void +check_smoothness (CurveEditor *self, + int point) +{ + Operation op, op1; + graphene_point_t *p1, *p2; + PointData *pd; + + pd = get_point (self, point); + op = pd->op; + op1 = get_point (self, point - 1)->op; + + if (op == CURVE) + p2 = &pd->p[2]; + else if (op == LINE) + p2 = &get_point (self, point + 1)->p[1]; + else + p2 = NULL; + + if (op1 == CURVE) + p1 = &pd->p[0]; + else if (op1 == LINE) + p1 = &get_point (self, point - 1)->p[1]; + else + p1 = NULL; + + if (!p1 || !p2 || !collinear (&pd->p[1], p1, p2)) + pd->type = CUSP; + else + pd->type = SMOOTH; +} + +static void +insert_point (CurveEditor *self, + int point, + double pos) +{ + PointData *pd, *pd1, *pd2; + graphene_point_t points[4]; + PointData np; + + pd = get_point (self, point); + if (pd->op == MOVE) + return; + + pd1 = get_point (self, point + 1); + points[0] = pd->p[1]; + points[1] = pd->p[2]; + points[2] = pd1->p[0]; + points[3] = pd1->p[1]; + + g_array_insert_val (self->points, point + 1, np); + + pd = get_point (self, point); + pd1 = get_point (self, point + 1); + pd2 = get_point (self, point + 2); + + pd1->type = SMOOTH; + pd1->hovered = -1; + pd1->dragged = -1; + + if (pd->op == LINE) + { + pd1->op = LINE; + graphene_point_interpolate (&points[0], &points[3], pos, &pd1->p[1]); + } + else if (pd->op == CURVE) + { + graphene_point_t left[4]; + graphene_point_t right[4]; + int left_pos = 0; + int right_pos = 0; + + pd1->op = CURVE; + + split_bezier (points, 4, pos, left, &left_pos, right, &right_pos); + + pd->p[1] = left[0]; + pd->p[2] = left[1]; + pd1->p[0] = left[2]; + pd1->p[1] = left[3]; + pd1->p[2] = right[2]; + pd2->p[0] = right[1]; + pd2->p[1] = right[0]; + } + else + g_assert_not_reached (); + + 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) +{ + g_array_remove_index (self->points, point); + + maintain_smoothness (self, point); + maintain_automatic (self, point); +} + +/* }}} */ +/* {{{ GskPath helpers */ +static void +curve_editor_add_segment (CurveEditor *self, + GskPathBuilder *builder, + int point) +{ + PointData *pd1, *pd; + + pd1 = get_point (self, point); + pd = get_point (self, point + 1); + + gsk_path_builder_move_to (builder, pd1->p[1].x, pd1->p[1].y); + + switch (pd1->op) + { + case LINE: + gsk_path_builder_line_to (builder, pd->p[1].x, pd->p[1].y); + break; + + case CURVE: + gsk_path_builder_curve_to (builder, + pd1->p[2].x, pd1->p[2].y, + pd->p[0].x, pd->p[0].y, + pd->p[1].x, pd->p[1].y); + break; + + case MOVE: + default: + break; + } +} + +static void +curve_editor_add_path (CurveEditor *self, + GskPathBuilder *builder) +{ + int i; + + for (i = 0; i < self->points->len; i++) + { + PointData *pd1, *pd; + + pd1 = get_point (self, i); + pd = get_point (self, i + 1); + + if (i == 0) + gsk_path_builder_move_to (builder, pd1->p[1].x, pd1->p[1].y); + + switch (pd1->op) + { + case MOVE: + gsk_path_builder_move_to (builder, pd->p[1].x, pd->p[1].y); + break; + + case LINE: + gsk_path_builder_line_to (builder, pd->p[1].x, pd->p[1].y); + break; + + case CURVE: + gsk_path_builder_curve_to (builder, + pd1->p[2].x, pd1->p[2].y, + pd->p[0].x, pd->p[0].y, + pd->p[1].x, pd->p[1].y); + break; + + 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->points->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 point; + + if (!self->edit) + return; + + for (i = 0; i < self->points->len; i++) + { + PointData *pd = get_point (self, i); + + for (j = 0; j < 3; j++) + { + if (graphene_point_distance (&pd->p[j], &p, NULL, NULL) < CLICK_RADIUS) + { + if (point_is_visible (self, i, j)) + { + self->dragged = i; + pd->dragged = j; + gtk_widget_queue_draw (GTK_WIDGET (self)); + } + return; + } + } + } + + if (find_closest_segment (self, &p, CLICK_RADIUS, NULL, &point, &t)) + { + /* Can't bend a straight line */ + get_point (self, point)->op = CURVE; + self->molded = point; + return; + } + + gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_DENIED); +} + +static void +drag_control_point (CurveEditor *self, + double x, + double y) +{ + double dx, dy; + graphene_point_t *c, *p, *d; + double l1, l2; + PointData *pd; + + pd = get_point (self, self->dragged); + d = &pd->p[pd->dragged]; + + /* before moving the point, record the distances to its neighbors, since + * we may want to preserve those + */ + l1 = graphene_point_distance (&pd->p[1], &pd->p[0], NULL, NULL); + l2 = graphene_point_distance (&pd->p[1], &pd->p[2], NULL, NULL); + + dx = x - d->x; + dy = y - d->y; + + if (pd->dragged == 1) + { + /* dragged point is on curve */ + + Operation op, op1, op11, op2; + PointData *pd1, *pd2; + + /* first move the point itself */ + d->x = x; + d->y = y; + + /* adjust control points as needed */ + pd1 = get_point (self, self->dragged - 1); + pd2 = get_point (self, self->dragged + 1); + + op = pd->op; + op1 = pd1->op; + op2 = pd2->op; + + if (op1 == LINE) + { + /* the other endpoint of the line */ + p = &pd1->p[1]; + + if (op == CURVE && pd->type != CUSP) + { + /* adjust the control point after the line segment */ + opposite_point (d, p, l2, &pd->p[2]); + } + else + { + pd->p[2].x += dx; + pd->p[2].y += dy; + } + + pd->p[0].x += dx; + pd->p[0].y += dy; + + op11 = get_point (self, self->dragged - 2)->op; + + if (op11 == CURVE && pd1->type != CUSP) + { + double l; + + /* adjust the control point before the line segment */ + l = graphene_point_distance (&pd1->p[0], p, NULL, NULL); + opposite_point (p, d, l, &pd1->p[0]); + } + } + + if (op == LINE) + { + /* the other endpoint of the line */ + p = &pd2->p[1]; + + if (op1 == CURVE && pd->type != CUSP) + { + /* adjust the control point before the line segment */ + opposite_point (d, p, l1, &pd->p[0]); + } + else + { + pd->p[0].x += dx; + pd->p[0].y += dy; + } + + pd->p[2].x += dx; + pd->p[2].y += dy; + + if (op2 == CURVE && pd2->type != CUSP) + { + double l; + + /* adjust the control point after the line segment */ + l = graphene_point_distance (&pd2->p[2], p, NULL, NULL); + opposite_point (p, d, l, &pd2->p[2]); + } + } + + if (op1 != LINE && op != LINE) + { + pd->p[0].x += dx; + pd->p[0].y += dy; + pd->p[2].x += dx; + pd->p[2].y += dy; + } + + maintain_automatic (self, self->dragged); + } + else + { + /* dragged point is a control point */ + + graphene_point_t *p1; + Operation op, op1; + + if (pd->dragged == 0) + { + c = &pd->p[2]; + p = &pd->p[1]; + + op = get_point (self, self->dragged - 1)->op; + op1 = get_point (self, self->dragged)->op; + p1 = &get_point (self, self->dragged + 1)->p[1]; + } + else if (pd->dragged == 2) + { + c = &pd->p[0]; + p = &pd->p[1]; + + op = get_point (self, self->dragged)->op; + op1 = get_point (self, self->dragged - 1)->op; + p1 = &get_point (self, self->dragged - 1)->p[1]; + } + else + g_assert_not_reached (); + + if (op == CURVE && pd->type != CUSP) + { + if (op1 == CURVE) + { + double l; + + /* first move the point itself */ + d->x = x; + d->y = y; + + /* then adjust the other control point */ + if (pd->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 (op1 == 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_curve (CurveEditor *self, + double x, + double y) +{ + PointData *pd, *pd1, *pd2, *pd3; + graphene_point_t *S, *E; + graphene_point_t B, C1, C2; + double l; + + pd = get_point (self, self->molded); + pd1 = get_point (self, self->molded + 1); + pd2 = get_point (self, self->molded - 1); + pd3 = get_point (self, self->molded + 2); + + S = &pd->p[1]; + B = GRAPHENE_POINT_INIT (x, y); + E = &pd1->p[1]; + + bezier_through (S, &B, E, &C1, &C2); + + pd->p[2] = C1; + pd1->p[0] = 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 (pd2->op == LINE) + { + l = graphene_point_distance (&pd->p[1], &pd->p[2], NULL, NULL); + if (three_point_angle (&pd->p[1], &pd2->p[1], &B) > 0) + scale_point (&pd->p[1], &pd2->p[1], l, &pd->p[2]); + else + opposite_point (&pd->p[1], &pd2->p[1], l, &pd->p[2]); + } + + if (pd1->op == LINE) + { + l = graphene_point_distance (&pd1->p[1], &pd1->p[0], NULL, NULL); + if (three_point_angle (&pd1->p[1], &pd3->p[1], &B) > 0) + scale_point (&pd1->p[1], &pd3->p[1], l, &pd1->p[0]); + else + opposite_point (&pd1->p[1], &pd3->p[1], l, &pd1->p[0]); + } + + /* Maintain smoothness and symmetry */ + if (pd->type != CUSP) + { + if (pd->type == SYMMETRIC) + l = graphene_point_distance (&pd->p[1], &pd->p[2], NULL, NULL); + else + l = graphene_point_distance (&pd->p[1], &pd->p[0], NULL, NULL); + opposite_point (&pd->p[1], &pd->p[2], l, &pd->p[0]); + } + + if (pd1->type != CUSP) + { + if (pd1->type == SYMMETRIC) + l = graphene_point_distance (&pd1->p[1], &pd1->p[0], NULL, NULL); + else + l = graphene_point_distance (&pd1->p[1], &pd1->p[2], NULL, NULL); + opposite_point (&pd1->p[1], &pd1->p[0], l, &pd1->p[2]); + } +} + +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_control_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_point (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); + + get_point (self, self->context)->op = op_from_string (g_variant_get_string (value, NULL)); + + 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 +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)); + } +/* }}} */ +/* {{{ 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)); + + if (!self->edit) + return; + + if (button == GDK_BUTTON_SECONDARY) + { + for (i = 0; i < self->points->len; i++) + { + PointData *pd = get_point (self, i); + + if (graphene_point_distance (&pd->p[1], &m, NULL, NULL) < CLICK_RADIUS) + { + GAction *action; + + self->context = i; + + action = g_action_map_lookup_action (self->actions, "type"); + g_simple_action_set_state (G_SIMPLE_ACTION (action), g_variant_new_string (point_type_to_string (pd->type))); + + action = g_action_map_lookup_action (self->actions, "operation"); + g_simple_action_set_state (G_SIMPLE_ACTION (action), g_variant_new_string (op_to_string (pd->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->points->len; i++) + { + PointData *pd = get_point (self, i); + + if (graphene_point_distance (&pd->p[1], &m, NULL, NULL) < CLICK_RADIUS) + { + if (button == GDK_BUTTON_PRIMARY) + { + pd->edit = !pd->edit; + 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->points->len; i++) + { + PointData *pd = get_point (self, i); + int hovered = -1; + + for (j = 0; j < 3; j++) + { + if (!point_is_visible (self, i, j)) + continue; + + if (graphene_point_distance (&pd->p[j], &m, NULL, NULL) < CLICK_RADIUS) + { + hovered = j; + break; + } + } + if (pd->hovered != hovered) + { + pd->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->points->len; i++) + { + PointData *pd = get_point (self, i); + if (pd->hovered != -1) + { + pd->hovered = -1; + changed = TRUE; + } + } + + if (changed) + gtk_widget_queue_draw (GTK_WIDGET (self)); +} +/* }}} */ +/* {{{ Snapshot */ +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->points->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); + gtk_snapshot_push_stroke (snapshot, path, self->stroke); + gsk_path_unref (path); + + gtk_snapshot_append_color (snapshot, + &self->color, + &GRAPHENE_RECT_INIT (0, 0, width, height )); + + gtk_snapshot_pop (snapshot); + + if (self->edit) + { + /* Add the skeleton */ + + builder = gsk_path_builder_new (); + + for (i = 0; i < self->points->len; i++) + { + PointData *pd = get_point (self, i); + gboolean need_move = TRUE; + + if (point_is_visible (self, i, 0)) + { + gsk_path_builder_move_to (builder, pd->p[0].x, pd->p[0].y); + gsk_path_builder_line_to (builder, pd->p[1].x, pd->p[1].y); + need_move = FALSE; + } + if (point_is_visible (self, i, 2)) + { + if (need_move) + gsk_path_builder_move_to (builder, pd->p[1].x, pd->p[1].y); + gsk_path_builder_line_to (builder, pd->p[2].x, pd->p[2].y); + } + } + + path = gsk_path_builder_free_to_path (builder); + stroke = gsk_stroke_new (1); + gtk_snapshot_push_stroke (snapshot, path, stroke); + gsk_stroke_free (stroke); + gsk_path_unref (path); + + gtk_snapshot_append_color (snapshot, + &(GdkRGBA){ 0, 0, 0, 1 }, + &GRAPHENE_RECT_INIT (0, 0, width, height )); + + gtk_snapshot_pop (snapshot); + + /* Draw the circles, in several passes, one for each color */ + + const char *colors[] = { + "white", /* hovered */ + "red", /* smooth curve points */ + "green", /* sharp curve points */ + "blue" /* control points */ + }; + GdkRGBA color; + + for (k = 0; k < 4; k++) + { + builder = gsk_path_builder_new (); + + for (i = 0; i < self->points->len; i++) + { + PointData *pd = get_point (self, i); + + for (j = 0; j < 3; j++) + { + switch (k) + { + case 0: + if (j != pd->hovered) + continue; + break; + + case 1: + if (j == pd->hovered) + continue; + + if (!(j == 1 && pd->type != CUSP)) + continue; + break; + + case 2: + if (j == pd->hovered) + continue; + + if (!(j == 1 && pd->type == CUSP)) + continue; + break; + + case 3: + if (j == pd->hovered) + continue; + + if (j == 1) + continue; + + if (!point_is_visible (self, i, j)) + continue; + break; + + default: + g_assert_not_reached (); + } + + gsk_path_builder_add_circle (builder, &pd->p[j], DRAW_RADIUS); + } + } + + 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_native_check_resize (GTK_NATIVE (self->menu)); +} +/* }}} */ +/* {{{ GObject boilerplate */ +static void +curve_editor_dispose (GObject *object) +{ + CurveEditor *self = CURVE_EDITOR (object); + + g_clear_pointer (&self->points, 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->points = g_array_new (FALSE, FALSE, sizeof (PointData)); + self->dragged = -1; + self->molded = -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 ("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 ("operation", 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 ("remove", 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)); + + gtk_widget_insert_action_group (GTK_WIDGET (self), "point", G_ACTION_GROUP (self->actions)); + + menu = g_menu_new (); + + section = g_menu_new (); + + item = g_menu_item_new ("Cusp", "point.type::cusp"); + g_menu_append_item (section, item); + g_object_unref (item); + + item = g_menu_item_new ("Smooth", "point.type::smooth"); + g_menu_append_item (section, item); + g_object_unref (item); + + item = g_menu_item_new ("Symmetric", "point.type::symmetric"); + g_menu_append_item (section, item); + g_object_unref (item); + + item = g_menu_item_new ("Automatic", "point.type::auto"); + 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 ("Move", "point.operation::move"); + g_menu_append_item (section, item); + g_object_unref (item); + + item = g_menu_item_new ("Line", "point.operation::line"); + g_menu_append_item (section, item); + g_object_unref (item); + + item = g_menu_item_new ("Curve", "point.operation::curve"); + 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 ("Remove", "point.remove"); + 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) +{ + int i; + + self->edit = edit; + if (!self->edit) + { + for (i = 0; i < self->points->len; i++) + { + PointData *pd = get_point (self, i); + pd->edit = FALSE; + pd->hovered = -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; + PointData *pd; + PointData *pd1; + PointData np; + + switch (op) + { + case GSK_PATH_MOVE: + if (self->points->len > 0) + { + pd = &g_array_index (self->points, PointData, self->points->len - 1); + pd->op = MOVE; + } + + np.p[1] = pts[0]; + g_array_append_val (self->points, np); + break; + + case GSK_PATH_CLOSE: + pd = &g_array_index (self->points, PointData, self->points->len - 1); + pd1 = &g_array_index (self->points, PointData, 0); + if (graphene_point_near (&pd->p[1], &pd1->p[1], 0.001)) + { + pd1->p[0] = pd->p[0]; + g_array_remove_index (self->points, self->points->len - 1); + } + break; + + case GSK_PATH_LINE: + pd = &g_array_index (self->points, PointData, self->points->len - 1); + pd->op = LINE; + np.p[1] = pts[1]; + g_array_append_val (self->points, np); + break; + + case GSK_PATH_CURVE: + pd = &g_array_index (self->points, PointData, self->points->len - 1); + pd->op = CURVE; + pd->p[2] = pts[1]; + np.p[0] = pts[2]; + np.p[1] = pts[3]; + g_array_append_val (self->points, np); + break; + + case GSK_PATH_CONIC: + /* FIXME */ + + default: + g_assert_not_reached (); + } + + return TRUE; +} +void +curve_editor_set_path (CurveEditor *self, + GskPath *path) +{ + int i; + + g_array_set_size (self->points, 0); + + gsk_path_foreach (path, copy_segments, self); + + for (i = 0; i < self->points->len; i++) + { + PointData *pd = get_point (self, i); + pd->hovered = -1; + pd->dragged = -1; + pd->edit= FALSE; + 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; +} +/* }}} */ +/* vim:set foldmethod=marker expandtab: */ diff --git a/tests/curve-editor.h b/tests/curve-editor.h new file mode 100644 index 0000000000..215adf449f --- /dev/null +++ b/tests/curve-editor.h @@ -0,0 +1,33 @@ +#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); + +G_END_DECLS diff --git a/tests/curve.c b/tests/curve.c new file mode 100644 index 0000000000..45b08c89ab --- /dev/null +++ b/tests/curve.c @@ -0,0 +1,184 @@ +#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 +width_changed (GtkRange *range, + CurveEditor *editor) +{ + GskStroke *stroke; + + stroke = gsk_stroke_copy (curve_editor_get_stroke (editor)); + gsk_stroke_set_line_width (stroke, gtk_range_get_value (range)); + 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); +} + +int +main (int argc, char *argv[]) +{ + GtkWindow *window; + GtkWidget *demo; + GtkWidget *edit_toggle; + GtkWidget *reset_button; + GtkWidget *titlebar; + GtkWidget *scale; + GtkWidget *stroke_button; + GtkWidget *popover; + GtkWidget *grid; + GtkWidget *cap_combo; + GtkWidget *join_combo; + GtkWidget *color_button; + + 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 ("Line Width:"), 0, 0, 1, 1); + scale = gtk_scale_new_with_range (GTK_ORIENTATION_HORIZONTAL, 1, 10, 1); + gtk_widget_set_size_request (scale, 60, -1); + gtk_grid_attach (GTK_GRID (grid), scale, 1, 0, 1, 1); + + gtk_grid_attach (GTK_GRID (grid), gtk_label_new ("Line cap:"), 0, 1, 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, 1, 1, 1); + + gtk_grid_attach (GTK_GRID (grid), gtk_label_new ("Line join:"), 0, 2, 1, 1); + join_combo = gtk_drop_down_new_from_strings ((const char *[]){"Miter", "Round", "Bevel", NULL}); + gtk_grid_attach (GTK_GRID (grid), join_combo, 1, 2, 1, 1); + + gtk_grid_attach (GTK_GRID (grid), gtk_label_new ("Stroke color:"), 0, 3, 1, 1); + color_button = gtk_color_button_new_with_rgba (&(GdkRGBA){ 0., 0., 0., 1.}); + gtk_grid_attach (GTK_GRID (grid), color_button, 1, 3, 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 (edit_toggle, "notify::active", G_CALLBACK (edit_changed), demo); + g_signal_connect (reset_button, "clicked", G_CALLBACK (reset), demo); + g_signal_connect (scale, "value-changed", G_CALLBACK (width_changed), 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); + + 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 42c492b67f..5a90ce07b1 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -1,5 +1,6 @@ gtk_tests = [ # testname, optional extra sources + ['curve', ['curve.c', 'curve-editor.c']], ['curve2'], ['testupload'], ['testtransform'], |