/* 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; if (gsk_path_get_closest_point (self->line_path, &GRAPHENE_POINT_INIT (x, y), INFINITY, &point, NULL)) { 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; }