Allow setting the type of segments to move, line or curve, from the context menu, and do the necessary adjustments to maintain smoothness, in particular between curve and line segments.
930 lines
26 KiB
C
930 lines
26 KiB
C
/* TODO
|
|
* - point insert/remove
|
|
* - rename to CurveEditor
|
|
* - add properties
|
|
*/
|
|
|
|
#include <gtk/gtk.h>
|
|
|
|
/* 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);
|
|
}
|
|
|
|
/* 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);
|
|
}
|
|
|
|
#define RADIUS 5
|
|
|
|
G_DECLARE_FINAL_TYPE (DemoWidget, demo_widget, DEMO, WIDGET, GtkWidget)
|
|
|
|
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 struct
|
|
{
|
|
Operation op;
|
|
gboolean edit;
|
|
gboolean smooth;
|
|
} PointData;
|
|
|
|
struct _DemoWidget
|
|
{
|
|
GtkWidget parent_instance;
|
|
graphene_point_t *points;
|
|
int n_points;
|
|
PointData *point_data; /* length is n_points / 3 */
|
|
int dragged;
|
|
int context;
|
|
gboolean symmetric;
|
|
gboolean edit;
|
|
|
|
GtkWidget *menu;
|
|
GActionMap *actions;
|
|
};
|
|
|
|
struct _DemoWidgetClass
|
|
{
|
|
GtkWidgetClass parent_class;
|
|
};
|
|
|
|
G_DEFINE_TYPE (DemoWidget, demo_widget, GTK_TYPE_WIDGET)
|
|
|
|
static float
|
|
dist (graphene_point_t *a, graphene_point_t *b)
|
|
{
|
|
graphene_vec2_t v;
|
|
|
|
graphene_vec2_init (&v, a->x - b->x, a->y - b->y);
|
|
return graphene_vec2_length (&v);
|
|
}
|
|
|
|
static void
|
|
drag_begin (GtkGestureDrag *gesture,
|
|
double start_x,
|
|
double start_y,
|
|
DemoWidget *self)
|
|
{
|
|
int i;
|
|
graphene_point_t p = GRAPHENE_POINT_INIT (start_x, start_y);
|
|
|
|
if (self->edit)
|
|
for (i = 0; i < self->n_points; i++)
|
|
{
|
|
if (dist (&self->points[i], &p) < RADIUS)
|
|
{
|
|
self->dragged = i;
|
|
self->symmetric = (gtk_event_controller_get_current_event_state (GTK_EVENT_CONTROLLER (gesture)) & GDK_CONTROL_MASK) == 0;
|
|
|
|
gtk_widget_queue_draw (GTK_WIDGET (self));
|
|
return;
|
|
}
|
|
}
|
|
|
|
gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_DENIED);
|
|
}
|
|
|
|
static void
|
|
drag_update (GtkGestureDrag *gesture,
|
|
double offset_x,
|
|
double offset_y,
|
|
DemoWidget *self)
|
|
{
|
|
double x, y;
|
|
double dx, dy;
|
|
graphene_point_t *c, *p, *d;
|
|
double l1, l2;
|
|
|
|
if (self->dragged == -1)
|
|
return;
|
|
|
|
gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_CLAIMED);
|
|
|
|
gtk_gesture_drag_get_start_point (gesture, &x, &y);
|
|
|
|
x += offset_x;
|
|
y += offset_y;
|
|
|
|
d = &self->points[self->dragged];
|
|
|
|
/* before moving the point, record the distances to its neighbors, since
|
|
* we may want to preserve those
|
|
*/
|
|
c = &self->points[(self->dragged - 1 + self->n_points) % self->n_points];
|
|
l1 = dist (d, c);
|
|
c = &self->points[(self->dragged + 1) % self->n_points];
|
|
l2 = dist (d, c);
|
|
|
|
dx = x - d->x;
|
|
dy = y - d->y;
|
|
|
|
if (self->dragged % 3 == 0)
|
|
{
|
|
/* dragged point is on curve */
|
|
|
|
Operation op, op1, op2;
|
|
|
|
/* first move the point itself */
|
|
d->x = x;
|
|
d->y = y;
|
|
|
|
/* adjust control points as needed */
|
|
op = self->point_data[self->dragged / 3].op;
|
|
op1 = self->point_data[((self->dragged - 1 + self->n_points) % self->n_points) / 3].op;
|
|
|
|
if (op1 == LINE)
|
|
{
|
|
/* the other endpoint of the line */
|
|
p = &self->points[(self->dragged - 3 + self->n_points) % self->n_points];
|
|
|
|
if (op == CURVE && self->point_data[self->dragged / 3].smooth)
|
|
{
|
|
/* adjust the control point after the line segment */
|
|
c = &self->points[(self->dragged + 1) % self->n_points];
|
|
opposite_point (d, p, l2, c);
|
|
}
|
|
else
|
|
{
|
|
c = &self->points[(self->dragged + 1) % self->n_points];
|
|
c->x += dx;
|
|
c->y += dy;
|
|
}
|
|
|
|
c = &self->points[(self->dragged - 1 + self->n_points) % self->n_points];
|
|
c->x += dx;
|
|
c->y += dy;
|
|
|
|
op2 = self->point_data[((self->dragged - 4 + self->n_points) % self->n_points) / 3].op;
|
|
if (op2 == CURVE && self->point_data[((self->dragged - 3 + self->n_points) % self->n_points) / 3].smooth)
|
|
{
|
|
double l;
|
|
|
|
/* adjust the control point before the line segment */
|
|
c = &self->points[((self->dragged - 4 + self->n_points) % self->n_points)];
|
|
|
|
l = dist (c, p);
|
|
opposite_point (p, d, l, c);
|
|
}
|
|
}
|
|
|
|
if (op == LINE)
|
|
{
|
|
/* the other endpoint of the line */
|
|
p = &self->points[(self->dragged + 3) % self->n_points];
|
|
|
|
if (op1 == CURVE && self->point_data[self->dragged / 3].smooth)
|
|
{
|
|
/* adjust the control point before the line segment */
|
|
c = &self->points[(self->dragged - 1 + self->n_points) % self->n_points];
|
|
opposite_point (d, p, l1, c);
|
|
}
|
|
else
|
|
{
|
|
c = &self->points[(self->dragged -1 + self->n_points) % self->n_points];
|
|
c->x += dx;
|
|
c->y += dy;
|
|
}
|
|
|
|
c = &self->points[(self->dragged + 1) % self->n_points];
|
|
c->x += dx;
|
|
c->y += dy;
|
|
|
|
op2 = self->point_data[((self->dragged + 3) % self->n_points) / 3].op;
|
|
if (op2 == CURVE && self->point_data[((self->dragged + 3) % self->n_points) / 3].smooth)
|
|
{
|
|
double l;
|
|
|
|
/* adjust the control point after the line segment */
|
|
c = &self->points[((self->dragged + 4) % self->n_points)];
|
|
|
|
l = dist (c, p);
|
|
opposite_point (p, d, l, c);
|
|
}
|
|
}
|
|
|
|
if (op1 != LINE && op != LINE)
|
|
{
|
|
self->points[(self->dragged - 1 + self->n_points) % self->n_points].x += dx;
|
|
self->points[(self->dragged - 1 + self->n_points) % self->n_points].y += dy;
|
|
|
|
self->points[(self->dragged + 1) % self->n_points].x += dx;
|
|
self->points[(self->dragged + 1) % self->n_points].y += dy;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/* dragged point is a control point */
|
|
|
|
int point;
|
|
graphene_point_t *p1;
|
|
Operation op, op1;
|
|
|
|
if (self->dragged % 3 == 1)
|
|
{
|
|
point = (self->dragged - 1 + self->n_points) % self->n_points;
|
|
c = &self->points[(self->dragged - 2 + self->n_points) % self->n_points];
|
|
p = &self->points[point];
|
|
|
|
op = self->point_data[point / 3].op;
|
|
op1 = self->point_data[((self->dragged - 4 + self->n_points) % self->n_points) / 3].op;
|
|
p1 = &self->points[(self->dragged - 4 + self->n_points) % self->n_points];
|
|
}
|
|
else if (self->dragged % 3 == 2)
|
|
{
|
|
point = (self->dragged + 1) % self->n_points;
|
|
c = &self->points[(self->dragged + 2) % self->n_points];
|
|
p = &self->points[point];
|
|
|
|
op = self->point_data[self->dragged / 3].op;
|
|
op1 = self->point_data[point / 3].op;
|
|
p1 = &self->points[(self->dragged + 4) % self->n_points];
|
|
}
|
|
else
|
|
g_assert_not_reached ();
|
|
|
|
if (op == CURVE && self->point_data[point / 3].smooth)
|
|
{
|
|
if (op1 == CURVE)
|
|
{
|
|
double l;
|
|
|
|
/* first move the point itself */
|
|
d->x = x;
|
|
d->y = y;
|
|
|
|
/* then adjust the other control point */
|
|
if (self->symmetric)
|
|
l = dist (d, p);
|
|
else
|
|
l = dist (c, p);
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
gtk_widget_queue_draw (GTK_WIDGET (self));
|
|
}
|
|
|
|
static void
|
|
drag_end (GtkGestureDrag *gesture,
|
|
double offset_x,
|
|
double offset_y,
|
|
DemoWidget *self)
|
|
{
|
|
drag_update (gesture, offset_x, offset_y, self);
|
|
self->dragged = -1;
|
|
self->symmetric = FALSE;
|
|
}
|
|
|
|
static void
|
|
maintain_smoothness (DemoWidget *self,
|
|
int point)
|
|
{
|
|
gboolean smooth;
|
|
Operation op, op1;
|
|
|
|
smooth = self->point_data[point / 3].smooth;
|
|
|
|
op = self->point_data[point / 3].op;
|
|
op1 = self->point_data[((point - 1 + self->n_points) % self->n_points) / 3].op;
|
|
|
|
if (smooth)
|
|
{
|
|
graphene_point_t *p;
|
|
|
|
p = &self->points[point];
|
|
|
|
if (op == CURVE && op1 == CURVE)
|
|
{
|
|
graphene_point_t *c, *c2;
|
|
float d;
|
|
|
|
c = &self->points[(point - 1 + self->n_points) % self->n_points];
|
|
c2 = &self->points[(point + 1) % self->n_points];
|
|
|
|
d = dist (c, p);
|
|
opposite_point (p, c2, d, c);
|
|
}
|
|
else if (op == CURVE && op1 == LINE)
|
|
{
|
|
graphene_point_t *c, *p2;
|
|
float d;
|
|
|
|
c = &self->points[(point + 1) % self->n_points];
|
|
p2 = &self->points[(point - 3 + self->n_points) % self->n_points];
|
|
|
|
d = dist (c, p);
|
|
opposite_point (p, p2, d, c);
|
|
}
|
|
else if (op == LINE && op1 == CURVE)
|
|
{
|
|
graphene_point_t *c, *p2;
|
|
float d;
|
|
|
|
c = &self->points[(point - 1 + self->n_points) % self->n_points];
|
|
p2 = &self->points[(point + 3) % self->n_points];
|
|
|
|
d = dist (c, p);
|
|
opposite_point (p, p2, d, c);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void
|
|
toggle_smooth (GSimpleAction *action,
|
|
GVariant *value,
|
|
gpointer data)
|
|
{
|
|
DemoWidget *self = DEMO_WIDGET (data);
|
|
|
|
self->point_data[self->context / 3].smooth = g_variant_get_boolean (value);
|
|
|
|
maintain_smoothness (self, self->context);
|
|
|
|
gtk_widget_queue_draw (GTK_WIDGET (self));
|
|
}
|
|
|
|
static void
|
|
set_operation (GSimpleAction *action,
|
|
GVariant *value,
|
|
gpointer data)
|
|
{
|
|
DemoWidget *self = DEMO_WIDGET (data);
|
|
|
|
self->point_data[self->context / 3].op = op_from_string (g_variant_get_string (value, NULL));
|
|
|
|
maintain_smoothness (self, self->context);
|
|
maintain_smoothness (self, (self->context + 3) % self->n_points);
|
|
|
|
gtk_widget_queue_draw (GTK_WIDGET (self));
|
|
}
|
|
|
|
static void
|
|
pressed (GtkGestureClick *gesture,
|
|
int n_press,
|
|
double x,
|
|
double y,
|
|
DemoWidget *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)
|
|
return;
|
|
|
|
for (i = 0; i < self->n_points; i++)
|
|
{
|
|
if (i % 3 != 0)
|
|
continue;
|
|
|
|
if (dist (&self->points[i], &m) < RADIUS)
|
|
{
|
|
GAction *action;
|
|
|
|
self->context = i;
|
|
|
|
action = g_action_map_lookup_action (self->actions, "smooth");
|
|
g_simple_action_set_state (G_SIMPLE_ACTION (action), g_variant_new_boolean (self->point_data[i / 3].smooth));
|
|
|
|
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 (self->point_data[i / 3].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,
|
|
DemoWidget *self)
|
|
{
|
|
graphene_point_t m = GRAPHENE_POINT_INIT (x, y);
|
|
int i;
|
|
|
|
if (!self->edit)
|
|
return;
|
|
|
|
for (i = 0; i < self->n_points; i++)
|
|
{
|
|
if (dist (&self->points[i], &m) < RADIUS)
|
|
{
|
|
if (i % 3 == 0)
|
|
{
|
|
int button = gtk_gesture_single_get_current_button (GTK_GESTURE_SINGLE (gesture));
|
|
if (button == GDK_BUTTON_PRIMARY)
|
|
{
|
|
self->point_data[i / 3].edit = !self->point_data[i / 3].edit;
|
|
}
|
|
else if (button == GDK_BUTTON_SECONDARY)
|
|
{
|
|
self->context = i;
|
|
self->point_data[i / 3].smooth = !self->point_data[i / 3].smooth;
|
|
if (self->point_data[i / 3].smooth)
|
|
{
|
|
graphene_point_t *p, *c, *c2;
|
|
float d;
|
|
|
|
p = &self->points[i];
|
|
c = &self->points[(i - 1 + self->n_points) % self->n_points];
|
|
c2 = &self->points[(i + 1 + self->n_points) % self->n_points];
|
|
|
|
d = dist (c, p);
|
|
opposite_point (p, c2, d, c);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static void
|
|
init_points (DemoWidget *self)
|
|
{
|
|
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;
|
|
int i;
|
|
|
|
g_free (self->points);
|
|
g_free (self->point_data);
|
|
|
|
self->n_points = 12;
|
|
self->points = g_new (graphene_point_t, self->n_points);
|
|
self->point_data = g_new (PointData, self->n_points / 3);
|
|
|
|
|
|
self->points[0] = GRAPHENE_POINT_INIT (cx, pad);
|
|
self->points[1] = GRAPHENE_POINT_INIT (cx + kr, pad);
|
|
self->points[2] = GRAPHENE_POINT_INIT (w - pad, cy - kr);
|
|
|
|
self->points[3] = GRAPHENE_POINT_INIT (w - pad, cy);
|
|
self->points[4] = GRAPHENE_POINT_INIT (w - pad, cy + kr);
|
|
self->points[5] = GRAPHENE_POINT_INIT (cx + kr, h - pad);
|
|
|
|
self->points[6] = GRAPHENE_POINT_INIT (cx, h - pad);
|
|
self->points[7] = GRAPHENE_POINT_INIT (cx - kr, h - pad);
|
|
self->points[8] = GRAPHENE_POINT_INIT (pad, cy + kr);
|
|
|
|
self->points[9] = GRAPHENE_POINT_INIT (pad, cy);
|
|
self->points[10] = GRAPHENE_POINT_INIT (pad, cy - kr);
|
|
self->points[11] = GRAPHENE_POINT_INIT (cx - kr, pad);
|
|
|
|
for (i = 0; i < self->n_points / 3; i++)
|
|
{
|
|
self->point_data[i].edit = FALSE;
|
|
self->point_data[i].smooth = TRUE;
|
|
self->point_data[i].op = CURVE;
|
|
}
|
|
}
|
|
|
|
static void
|
|
demo_widget_init (DemoWidget *self)
|
|
{
|
|
GtkGesture *gesture;
|
|
GMenu *menu;
|
|
GMenu *section;
|
|
GMenuItem *item;
|
|
GSimpleAction *action;
|
|
|
|
self->dragged = -1;
|
|
self->edit = FALSE;
|
|
|
|
gesture = gtk_gesture_drag_new ();
|
|
gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (gesture), GDK_BUTTON_PRIMARY);
|
|
g_signal_connect (gesture, "drag-begin", G_CALLBACK (drag_begin), self);
|
|
g_signal_connect (gesture, "drag-update", G_CALLBACK (drag_update), self);
|
|
g_signal_connect (gesture, "drag-end", G_CALLBACK (drag_end), self);
|
|
gtk_widget_add_controller (GTK_WIDGET (self), GTK_EVENT_CONTROLLER (gesture));
|
|
|
|
gesture = gtk_gesture_click_new ();
|
|
gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (gesture), 0);
|
|
g_signal_connect (gesture, "pressed", G_CALLBACK (pressed), self);
|
|
g_signal_connect (gesture, "released", G_CALLBACK (released), self);
|
|
gtk_widget_add_controller (GTK_WIDGET (self), GTK_EVENT_CONTROLLER (gesture));
|
|
|
|
init_points (self);
|
|
|
|
self->actions = G_ACTION_MAP (g_simple_action_group_new ());
|
|
|
|
action = g_simple_action_new_stateful ("smooth", NULL, g_variant_new_boolean (FALSE));
|
|
g_signal_connect (action, "change-state", G_CALLBACK (toggle_smooth), 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));
|
|
|
|
gtk_widget_insert_action_group (GTK_WIDGET (self), "point", G_ACTION_GROUP (self->actions));
|
|
|
|
menu = g_menu_new ();
|
|
|
|
item = g_menu_item_new ("Smooth", "point.smooth");
|
|
g_menu_append_item (menu, item);
|
|
g_object_unref (item);
|
|
|
|
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);
|
|
|
|
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));
|
|
}
|
|
|
|
static void
|
|
demo_widget_snapshot (GtkWidget *widget,
|
|
GtkSnapshot *snapshot)
|
|
{
|
|
DemoWidget *self = (DemoWidget *)widget;
|
|
GskPathBuilder *builder;
|
|
GskPath *path;
|
|
GskStroke *stroke;
|
|
int i, j;
|
|
float width;
|
|
float height;
|
|
|
|
width = gtk_widget_get_width (widget);
|
|
height = gtk_widget_get_width (widget);
|
|
|
|
builder = gsk_path_builder_new ();
|
|
|
|
if (self->edit)
|
|
{
|
|
/* Add the skeleton */
|
|
|
|
gsk_path_builder_move_to (builder, self->points[0].x, self->points[0].y);
|
|
for (i = 1; i < self->n_points; i++)
|
|
{
|
|
gboolean edit;
|
|
gboolean line;
|
|
|
|
if (i % 3 == 2)
|
|
edit = self->point_data[((i + 3) % self->n_points) / 3].edit;
|
|
else
|
|
edit = self->point_data[i / 3].edit;
|
|
|
|
if (i % 3 == 0)
|
|
line = self->point_data[((i - 1 + self->n_points) % self->n_points) / 3].op != CURVE;
|
|
else
|
|
line = self->point_data[i / 3].op != CURVE;
|
|
|
|
if (edit)
|
|
{
|
|
if (i % 3 == 2 || line)
|
|
gsk_path_builder_move_to (builder, self->points[i].x, self->points[i].y);
|
|
else
|
|
gsk_path_builder_line_to (builder, self->points[i].x, self->points[i].y);
|
|
}
|
|
}
|
|
if (self->point_data[0].edit)
|
|
gsk_path_builder_line_to (builder, self->points[0].x, self->points[0].y);
|
|
}
|
|
|
|
/* Add the curve itself */
|
|
|
|
gsk_path_builder_move_to (builder, self->points[0].x, self->points[0].y);
|
|
for (i = 1; i < self->n_points; i += 3)
|
|
{
|
|
switch (self->point_data[i / 3].op)
|
|
{
|
|
case MOVE:
|
|
gsk_path_builder_move_to (builder,
|
|
self->points[(i + 2) % self->n_points].x, self->points[(i + 2) % self->n_points].y);
|
|
break;
|
|
|
|
case LINE:
|
|
gsk_path_builder_line_to (builder,
|
|
self->points[(i + 2) % self->n_points].x, self->points[(i + 2) % self->n_points].y);
|
|
break;
|
|
|
|
case CURVE:
|
|
gsk_path_builder_curve_to (builder,
|
|
self->points[i].x, self->points[i].y,
|
|
self->points[(i + 1) % self->n_points].x, self->points[(i + 1) % self->n_points].y,
|
|
self->points[(i + 2) % self->n_points].x, self->points[(i + 2) % self->n_points].y);
|
|
break;
|
|
default:
|
|
g_assert_not_reached ();
|
|
}
|
|
}
|
|
|
|
/* Stroke everything we have so far */
|
|
|
|
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);
|
|
|
|
if (self->edit)
|
|
{
|
|
/* Draw the circles, in several passes, one for each color */
|
|
|
|
const char *colors[] = {
|
|
"red",
|
|
"green",
|
|
"blue"
|
|
};
|
|
GdkRGBA color;
|
|
|
|
for (j = 0; j < 3; j++)
|
|
{
|
|
builder = gsk_path_builder_new ();
|
|
|
|
for (i = 0; i < self->n_points; i++)
|
|
{
|
|
switch (j)
|
|
{
|
|
case 0:
|
|
if (!(i % 3 == 0 &&
|
|
self->point_data[i / 3].smooth))
|
|
continue;
|
|
break;
|
|
|
|
case 1:
|
|
if (!(i % 3 == 0 &&
|
|
!self->point_data[i / 3].smooth))
|
|
continue;
|
|
break;
|
|
|
|
case 2:
|
|
if (i % 3 == 1)
|
|
{
|
|
if (!(self->point_data[i / 3].edit &&
|
|
self->point_data[i / 3].op == CURVE))
|
|
continue;
|
|
}
|
|
else if (i % 3 == 2)
|
|
{
|
|
if (!(self->point_data[((i + 3) % self->n_points) / 3].edit &&
|
|
self->point_data[i / 3].op == CURVE))
|
|
continue;
|
|
}
|
|
else
|
|
continue;
|
|
break;
|
|
|
|
default:
|
|
g_assert_not_reached ();
|
|
}
|
|
|
|
gsk_path_builder_add_circle (builder, &self->points[i], RADIUS);
|
|
}
|
|
|
|
path = gsk_path_builder_free_to_path (builder);
|
|
|
|
gtk_snapshot_push_fill (snapshot, path, GSK_FILL_RULE_WINDING);
|
|
gdk_rgba_parse (&color, colors[j]);
|
|
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);
|
|
|
|
gdk_rgba_parse (&color, "black");
|
|
gtk_snapshot_append_color (snapshot, &color, &GRAPHENE_RECT_INIT (0, 0, width, height));
|
|
gtk_snapshot_pop (snapshot);
|
|
|
|
gsk_path_unref (path);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void
|
|
demo_widget_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
|
|
demo_widget_size_allocate (GtkWidget *widget,
|
|
int width,
|
|
int height,
|
|
int baseline)
|
|
{
|
|
DemoWidget *self = DEMO_WIDGET (widget);
|
|
|
|
gtk_native_check_resize (GTK_NATIVE (self->menu));
|
|
}
|
|
|
|
static void
|
|
demo_widget_dispose (GObject *object)
|
|
{
|
|
DemoWidget *self = DEMO_WIDGET (object);
|
|
|
|
g_clear_pointer (&self->points, g_free);
|
|
g_clear_pointer (&self->point_data, g_free);
|
|
g_clear_pointer (&self->menu, gtk_widget_unparent);
|
|
|
|
G_OBJECT_CLASS (demo_widget_parent_class)->dispose (object);
|
|
}
|
|
|
|
static void
|
|
demo_widget_class_init (DemoWidgetClass *class)
|
|
{
|
|
GObjectClass *object_class = G_OBJECT_CLASS (class);
|
|
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (class);
|
|
|
|
object_class->dispose = demo_widget_dispose;
|
|
|
|
widget_class->snapshot = demo_widget_snapshot;
|
|
widget_class->measure = demo_widget_measure;
|
|
widget_class->size_allocate = demo_widget_size_allocate;
|
|
}
|
|
|
|
static GtkWidget *
|
|
demo_widget_new (void)
|
|
{
|
|
return g_object_new (demo_widget_get_type (), NULL);
|
|
}
|
|
|
|
static void
|
|
edit_changed (GtkToggleButton *button,
|
|
GParamSpec *pspec,
|
|
DemoWidget *self)
|
|
{
|
|
int i;
|
|
|
|
self->edit = gtk_toggle_button_get_active (button);
|
|
if (!self->edit)
|
|
{
|
|
for (i = 0; i < self->n_points / 3; i++)
|
|
self->point_data[i].edit = FALSE;
|
|
}
|
|
gtk_widget_queue_draw (GTK_WIDGET (self));
|
|
}
|
|
|
|
static void
|
|
reset (GtkButton *button,
|
|
DemoWidget *self)
|
|
{
|
|
init_points (self);
|
|
gtk_widget_queue_draw (GTK_WIDGET (self));
|
|
}
|
|
|
|
int
|
|
main (int argc, char *argv[])
|
|
{
|
|
GtkWindow *window;
|
|
GtkWidget *demo;
|
|
GtkWidget *edit_toggle;
|
|
GtkWidget *reset_button;
|
|
GtkWidget *titlebar;
|
|
|
|
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");
|
|
|
|
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_window_set_titlebar (GTK_WINDOW (window), titlebar);
|
|
|
|
demo = demo_widget_new ();
|
|
|
|
g_signal_connect (edit_toggle, "notify::active", G_CALLBACK (edit_changed), demo);
|
|
g_signal_connect (reset_button, "clicked", G_CALLBACK (reset), 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;
|
|
}
|