From e2d2b57f0ea753b0de68f21bdf3ee5f4d7008d1c Mon Sep 17 00:00:00 2001 From: Benjamin Otte Date: Thu, 19 Nov 2020 22:34:37 +0100 Subject: [PATCH] demos: Add a text-on-path demo --- demos/gtk-demo/demo.gresource.xml | 4 + demos/gtk-demo/meson.build | 1 + demos/gtk-demo/path_text.c | 586 ++++++++++++++++++++++++++++++ demos/gtk-demo/path_text.ui | 38 ++ 4 files changed, 629 insertions(+) create mode 100644 demos/gtk-demo/path_text.c create mode 100644 demos/gtk-demo/path_text.ui diff --git a/demos/gtk-demo/demo.gresource.xml b/demos/gtk-demo/demo.gresource.xml index 6a1b760c4b..2c8f1caf88 100644 --- a/demos/gtk-demo/demo.gresource.xml +++ b/demos/gtk-demo/demo.gresource.xml @@ -338,6 +338,7 @@ path_fill.c path_spinner.c path_walk.c + path_text.c peg_solitaire.c pickers.c printing.c @@ -427,6 +428,9 @@ path_walk.ui path_world.txt + + path_text.ui + icons/16x16/actions/application-exit.png icons/16x16/actions/document-new.png diff --git a/demos/gtk-demo/meson.build b/demos/gtk-demo/meson.build index dd8b4fd894..fe60493016 100644 --- a/demos/gtk-demo/meson.build +++ b/demos/gtk-demo/meson.build @@ -75,6 +75,7 @@ demos = files([ 'path_fill.c', 'path_spinner.c', 'path_walk.c', + 'path_text.c', 'peg_solitaire.c', 'pickers.c', 'printing.c', diff --git a/demos/gtk-demo/path_text.c b/demos/gtk-demo/path_text.c new file mode 100644 index 0000000000..e6ddd70552 --- /dev/null +++ b/demos/gtk-demo/path_text.c @@ -0,0 +1,586 @@ +/* Path/Text + * + * This demo shows how to use GskPath to transform a path along another path. + * + * It also demonstrates that paths can be filled with more interesting + * content than just plain colors. + */ + +#include +#include + +#define GTK_TYPE_PATH_WIDGET (gtk_path_widget_get_type ()) +G_DECLARE_FINAL_TYPE (GtkPathWidget, gtk_path_widget, GTK, PATH_WIDGET, GtkWidget) + +#define POINT_SIZE 8 + +enum { + PROP_0, + PROP_TEXT, + PROP_EDITABLE, + N_PROPS +}; + +struct _GtkPathWidget +{ + GtkWidget parent_instance; + + char *text; + gboolean editable; + + graphene_point_t points[4]; + + guint active_point; + + GskPath *line_path; + GskPath *text_path; + + GdkPaintable *background; +}; + +struct _GtkPathWidgetClass +{ + GtkWidgetClass parent_class; +}; + +static GParamSpec *properties[N_PROPS] = { NULL, }; + +G_DEFINE_TYPE (GtkPathWidget, gtk_path_widget, GTK_TYPE_WIDGET) + +static GskPath * +create_path_from_text (GtkWidget *widget, + const char *text, + graphene_point_t *out_offset) +{ + PangoLayout *layout; + PangoFontDescription *desc; + GskPathBuilder *builder; + GskPath *result; + + layout = gtk_widget_create_pango_layout (widget, text); + desc = pango_font_description_from_string ("sans bold 36"); + pango_layout_set_font_description (layout, desc); + pango_font_description_free (desc); + + builder = gsk_path_builder_new (); + gsk_path_builder_add_layout (builder, layout); + result = gsk_path_builder_free_to_path (builder); + + if (out_offset) + graphene_point_init (out_offset, 0, - pango_layout_get_baseline (layout) / (double) PANGO_SCALE); + g_object_unref (layout); + + return result; +} + +typedef struct +{ + GskPathMeasure *measure; + GskPathBuilder *builder; + graphene_point_t offset; + double scale; +} GtkPathTransform; + +static void +gtk_path_transform_point (GskPathMeasure *measure, + const graphene_point_t *pt, + const graphene_point_t *offset, + float scale, + graphene_point_t *res) +{ + graphene_vec2_t tangent; + GskPathPoint point; + + if (gsk_path_measure_get_point (measure, (pt->x + offset->x) * scale, &point)) + { + GskPath *path = gsk_path_measure_get_path (measure); + + gsk_path_point_get_position (&point, path, res); + gsk_path_point_get_tangent (&point, path, GSK_PATH_TO_END, &tangent); + + res->x -= (pt->y + offset->y) * scale * graphene_vec2_get_y (&tangent); + res->y += (pt->y + offset->y) * scale * graphene_vec2_get_x (&tangent); + } +} + +static gboolean +gtk_path_transform_op (GskPathOperation op, + const graphene_point_t *pts, + gsize n_pts, + float weight, + gpointer data) +{ + GtkPathTransform *transform = data; + + switch (op) + { + case GSK_PATH_MOVE: + { + graphene_point_t res; + gtk_path_transform_point (transform->measure, &pts[0], &transform->offset, transform->scale, &res); + gsk_path_builder_move_to (transform->builder, res.x, res.y); + } + break; + + case GSK_PATH_LINE: + { + graphene_point_t res; + gtk_path_transform_point (transform->measure, &pts[1], &transform->offset, transform->scale, &res); + gsk_path_builder_line_to (transform->builder, res.x, res.y); + } + break; + + case GSK_PATH_QUAD: + { + graphene_point_t res[2]; + gtk_path_transform_point (transform->measure, &pts[1], &transform->offset, transform->scale, &res[0]); + gtk_path_transform_point (transform->measure, &pts[2], &transform->offset, transform->scale, &res[1]); + gsk_path_builder_quad_to (transform->builder, res[0].x, res[0].y, res[1].x, res[1].y); + } + break; + + case GSK_PATH_CUBIC: + { + graphene_point_t res[3]; + gtk_path_transform_point (transform->measure, &pts[1], &transform->offset, transform->scale, &res[0]); + gtk_path_transform_point (transform->measure, &pts[2], &transform->offset, transform->scale, &res[1]); + gtk_path_transform_point (transform->measure, &pts[3], &transform->offset, transform->scale, &res[2]); + gsk_path_builder_cubic_to (transform->builder, res[0].x, res[0].y, res[1].x, res[1].y, res[2].x, res[2].y); + } + break; + + case GSK_PATH_CONIC: + { + graphene_point_t res[2]; + gtk_path_transform_point (transform->measure, &pts[1], &transform->offset, transform->scale, &res[0]); + gtk_path_transform_point (transform->measure, &pts[3], &transform->offset, transform->scale, &res[1]); + gsk_path_builder_conic_to (transform->builder, res[0].x, res[0].y, res[1].x, res[1].y, weight); + } + break; + + case GSK_PATH_CLOSE: + gsk_path_builder_close (transform->builder); + break; + + default: + g_assert_not_reached(); + return FALSE; + } + + return TRUE; +} + +static GskPath * +gtk_path_transform (GskPath *line_path, + GskPath *path, + const graphene_point_t *offset) +{ + GskPathMeasure *measure = gsk_path_measure_new (line_path); + GtkPathTransform transform = { measure, gsk_path_builder_new (), *offset }; + graphene_rect_t bounds; + + gsk_path_get_bounds (path, &bounds); + if (bounds.origin.x + bounds.size.width > 0) + transform.scale = gsk_path_measure_get_length (measure) / (bounds.origin.x + bounds.size.width); + else + transform.scale = 1.0f; + + gsk_path_foreach (path, -1, gtk_path_transform_op, &transform); + + gsk_path_measure_unref (measure); + + return gsk_path_builder_free_to_path (transform.builder); +} + +static void +gtk_path_widget_clear_text_path (GtkPathWidget *self) +{ + g_clear_pointer (&self->text_path, gsk_path_unref); +} + +static void +gtk_path_widget_clear_paths (GtkPathWidget *self) +{ + gtk_path_widget_clear_text_path (self); + + g_clear_pointer (&self->line_path, gsk_path_unref); +} + +static void +gtk_path_widget_create_text_path (GtkPathWidget *self) +{ + GskPath *path; + graphene_point_t offset; + + gtk_path_widget_clear_text_path (self); + + path = create_path_from_text (GTK_WIDGET (self), self->text, &offset); + self->text_path = gtk_path_transform (self->line_path, path, &offset); + + gsk_path_unref (path); +} + +static void +gtk_path_widget_create_paths (GtkPathWidget *self) +{ + double width = gtk_widget_get_width (GTK_WIDGET (self)); + double height = gtk_widget_get_height (GTK_WIDGET (self)); + GskPathBuilder *builder; + + gtk_path_widget_clear_paths (self); + + if (width <= 0 || height <= 0) + return; + + builder = gsk_path_builder_new (); + gsk_path_builder_move_to (builder, + self->points[0].x * width, self->points[0].y * height); + gsk_path_builder_cubic_to (builder, + self->points[1].x * width, self->points[1].y * height, + self->points[2].x * width, self->points[2].y * height, + self->points[3].x * width, self->points[3].y * height); + self->line_path = gsk_path_builder_free_to_path (builder); + + gtk_path_widget_create_text_path (self); +} + +static void +gtk_path_widget_allocate (GtkWidget *widget, + int width, + int height, + int baseline) +{ + GtkPathWidget *self = GTK_PATH_WIDGET (widget); + + GTK_WIDGET_CLASS (gtk_path_widget_parent_class)->size_allocate (widget, width, height, baseline); + + gtk_path_widget_create_paths (self); +} + +static void +gtk_path_widget_snapshot (GtkWidget *widget, + GtkSnapshot *snapshot) +{ + GtkPathWidget *self = GTK_PATH_WIDGET (widget); + double width = gtk_widget_get_width (widget); + double height = gtk_widget_get_height (widget); + GskPath *path; + GskStroke *stroke; + gsize i; + + /* frosted glass the background */ + gtk_snapshot_push_blur (snapshot, 100); + gdk_paintable_snapshot (self->background, snapshot, width, height); + gtk_snapshot_append_color (snapshot, &(GdkRGBA) { 1, 1, 1, 0.6 }, &GRAPHENE_RECT_INIT (0, 0, width, height)); + gtk_snapshot_pop (snapshot); + + /* draw the text */ + if (self->text_path) + { + gtk_snapshot_push_fill (snapshot, self->text_path, GSK_FILL_RULE_WINDING); + gdk_paintable_snapshot (self->background, snapshot, width, height); + + /* ... with an emboss effect */ + stroke = gsk_stroke_new (2.0); + gtk_snapshot_translate (snapshot, &GRAPHENE_POINT_INIT(1, 1)); + gtk_snapshot_push_stroke (snapshot, self->text_path, stroke); + gtk_snapshot_append_color (snapshot, &(GdkRGBA) { 0, 0, 0, 0.2 }, &GRAPHENE_RECT_INIT (0, 0, width, height)); + gsk_stroke_free (stroke); + gtk_snapshot_pop (snapshot); + + gtk_snapshot_pop (snapshot); + } + + if (self->editable && self->line_path) + { + GskPathBuilder *builder; + + /* draw the control line */ + stroke = gsk_stroke_new (1.0); + gtk_snapshot_push_stroke (snapshot, self->line_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); + + /* draw the points */ + builder = gsk_path_builder_new (); + for (i = 0; i < 4; i++) + { + gsk_path_builder_add_circle (builder, &GRAPHENE_POINT_INIT (self->points[i].x * width, self->points[i].y * height), POINT_SIZE); + } + path = gsk_path_builder_free_to_path (builder); + + gtk_snapshot_push_fill (snapshot, path, GSK_FILL_RULE_WINDING); + gtk_snapshot_append_color (snapshot, &(GdkRGBA) { 1, 1, 1, 1 }, &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); + } +} + +static void +gtk_path_widget_set_text (GtkPathWidget *self, + const char *text) +{ + if (g_strcmp0 (self->text, text) == 0) + return; + + g_free (self->text); + self->text = g_strdup (text); + + gtk_path_widget_create_paths (self); + + gtk_widget_queue_draw (GTK_WIDGET (self)); + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_TEXT]); +} + +static void +gtk_path_widget_set_editable (GtkPathWidget *self, + gboolean editable) +{ + if (self->editable == editable) + return; + + self->editable = editable; + + gtk_widget_queue_draw (GTK_WIDGET (self)); + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_EDITABLE]); +} + +static void +gtk_path_widget_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) + +{ + GtkPathWidget *self = GTK_PATH_WIDGET (object); + + switch (prop_id) + { + case PROP_TEXT: + gtk_path_widget_set_text (self, g_value_get_string (value)); + break; + + case PROP_EDITABLE: + gtk_path_widget_set_editable (self, g_value_get_boolean (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gtk_path_widget_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtkPathWidget *self = GTK_PATH_WIDGET (object); + + switch (prop_id) + { + case PROP_TEXT: + g_value_set_string (value, self->text); + break; + + case PROP_EDITABLE: + g_value_set_boolean (value, self->editable); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gtk_path_widget_dispose (GObject *object) +{ + GtkPathWidget *self = GTK_PATH_WIDGET (object); + + gtk_path_widget_clear_paths (self); + + G_OBJECT_CLASS (gtk_path_widget_parent_class)->dispose (object); +} + +static void +gtk_path_widget_class_init (GtkPathWidgetClass *klass) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = gtk_path_widget_dispose; + object_class->set_property = gtk_path_widget_set_property; + object_class->get_property = gtk_path_widget_get_property; + + widget_class->size_allocate = gtk_path_widget_allocate; + widget_class->snapshot = gtk_path_widget_snapshot; + + properties[PROP_TEXT] = + g_param_spec_string ("text", + "text", + "Text transformed along a path", + NULL, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + properties[PROP_EDITABLE] = + g_param_spec_boolean ("editable", + "editable", + "If the path can be edited by the user", + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, N_PROPS, properties); +} + +static void +drag_begin (GtkGestureDrag *gesture, + double x, + double y, + GtkPathWidget *self) +{ + graphene_point_t mouse = GRAPHENE_POINT_INIT (x, y); + double width = gtk_widget_get_width (GTK_WIDGET (self)); + double height = gtk_widget_get_height (GTK_WIDGET (self)); + gsize i; + + for (i = 0; i < 4; i++) + { + if (graphene_point_distance (&GRAPHENE_POINT_INIT (self->points[i].x * width, self->points[i].y * height), &mouse, NULL, NULL) <= POINT_SIZE) + { + self->active_point = i; + break; + } + } + if (i == 4) + { + gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_DENIED); + return; + } + + gtk_widget_queue_draw (GTK_WIDGET (self)); +} + +static void +drag_update (GtkGestureDrag *drag, + double offset_x, + double offset_y, + GtkPathWidget *self) +{ + double width = gtk_widget_get_width (GTK_WIDGET (self)); + double height = gtk_widget_get_height (GTK_WIDGET (self)); + double start_x, start_y; + + gtk_gesture_drag_get_start_point (drag, &start_x, &start_y); + + self->points[self->active_point] = GRAPHENE_POINT_INIT ((start_x + offset_x) / width, + (start_y + offset_y) / height); + self->points[self->active_point].x = CLAMP (self->points[self->active_point].x, 0, 1); + self->points[self->active_point].y = CLAMP (self->points[self->active_point].y, 0, 1); + + gtk_path_widget_create_paths (self); + + gtk_widget_queue_draw (GTK_WIDGET (self)); +} + +static void +pointer_motion (GtkEventControllerMotion *controller, + double x, + double y, + GtkPathWidget *self) +{ + GskPathPoint point; + graphene_point_t pos; + + if (gsk_path_get_closest_point (self->line_path, + &GRAPHENE_POINT_INIT (x, y), + INFINITY, + &point)) + { + gsk_path_point_get_position (&point, self->line_path, &pos); + + gtk_widget_queue_draw (GTK_WIDGET (self)); + } +} + +static void +pointer_leave (GtkEventControllerMotion *controller, + GtkPathWidget *self) +{ + gtk_widget_queue_draw (GTK_WIDGET (self)); +} + +static void +gtk_path_widget_init (GtkPathWidget *self) +{ + GtkEventController *controller; + + controller = GTK_EVENT_CONTROLLER (gtk_gesture_drag_new ()); + 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_update), self); + gtk_widget_add_controller (GTK_WIDGET (self), controller); + + controller = GTK_EVENT_CONTROLLER (gtk_event_controller_motion_new ()); + g_signal_connect (controller, "enter", G_CALLBACK (pointer_motion), self); + g_signal_connect (controller, "motion", G_CALLBACK (pointer_motion), self); + g_signal_connect (controller, "leave", G_CALLBACK (pointer_leave), self); + gtk_widget_add_controller (GTK_WIDGET (self), controller); + + self->points[0] = GRAPHENE_POINT_INIT (0.1, 0.9); + self->points[1] = GRAPHENE_POINT_INIT (0.3, 0.1); + self->points[2] = GRAPHENE_POINT_INIT (0.7, 0.1); + self->points[3] = GRAPHENE_POINT_INIT (0.9, 0.9); + + self->background = GDK_PAINTABLE (gdk_texture_new_from_resource ("/sliding_puzzle/portland-rose.jpg")); + + gtk_path_widget_set_text (self, "It's almost working"); +} + +GtkWidget * +gtk_path_widget_new (void) +{ + GtkPathWidget *self; + + self = g_object_new (GTK_TYPE_PATH_WIDGET, NULL); + + return GTK_WIDGET (self); +} + +GtkWidget * +do_path_text (GtkWidget *do_widget) +{ + static GtkWidget *window = NULL; + + if (!window) + { + GtkBuilder *builder; + + g_type_ensure (GTK_TYPE_PATH_WIDGET); + + builder = gtk_builder_new_from_resource ("/path_text/path_text.ui"); + window = GTK_WIDGET (gtk_builder_get_object (builder, "window")); + gtk_window_set_display (GTK_WINDOW (window), + gtk_widget_get_display (do_widget)); + g_object_add_weak_pointer (G_OBJECT (window), (gpointer *) &window); + g_object_unref (builder); + } + + if (!gtk_widget_get_visible (window)) + gtk_window_present (GTK_WINDOW (window)); + else + gtk_window_destroy (GTK_WINDOW (window)); + + return window; +} diff --git a/demos/gtk-demo/path_text.ui b/demos/gtk-demo/path_text.ui new file mode 100644 index 0000000000..ffd96a7d6b --- /dev/null +++ b/demos/gtk-demo/path_text.ui @@ -0,0 +1,38 @@ + + + + Text along a Path + + + + + document-edit-symbolic + + + + + + + vertical + + + + + + Through the looking glass + + + + + + + + + true + true + + + + + +