From 02402b2881de493c5b1152cec30391cb0f515717 Mon Sep 17 00:00:00 2001 From: Matthias Clasen Date: Mon, 26 Jun 2023 08:41:08 -0400 Subject: [PATCH] tools: Add gtk4-path-tool This comes in handy for testing, among other things. For now, this supports decomposing, transforming, reversing, restricting, rendering and preview. --- docs/reference/gtk/gtk4-path-tool.rst | 140 ++++++++++++++++ docs/reference/gtk/meson.build | 1 + tools/gtk-path-tool-decompose.c | 142 +++++++++++++++++ tools/gtk-path-tool-info.c | 80 ++++++++++ tools/gtk-path-tool-render.c | 143 +++++++++++++++++ tools/gtk-path-tool-restrict.c | 93 +++++++++++ tools/gtk-path-tool-reverse.c | 76 +++++++++ tools/gtk-path-tool-show.c | 134 ++++++++++++++++ tools/gtk-path-tool-transform.c | 89 +++++++++++ tools/gtk-path-tool-utils.c | 139 ++++++++++++++++ tools/gtk-path-tool.c | 148 +++++++++++++++++ tools/gtk-path-tool.h | 16 ++ tools/meson.build | 10 ++ tools/path-view.c | 221 ++++++++++++++++++++++++++ tools/path-view.h | 9 ++ 15 files changed, 1441 insertions(+) create mode 100644 docs/reference/gtk/gtk4-path-tool.rst create mode 100644 tools/gtk-path-tool-decompose.c create mode 100644 tools/gtk-path-tool-info.c create mode 100644 tools/gtk-path-tool-render.c create mode 100644 tools/gtk-path-tool-restrict.c create mode 100644 tools/gtk-path-tool-reverse.c create mode 100644 tools/gtk-path-tool-show.c create mode 100644 tools/gtk-path-tool-transform.c create mode 100644 tools/gtk-path-tool-utils.c create mode 100644 tools/gtk-path-tool.c create mode 100644 tools/gtk-path-tool.h create mode 100644 tools/path-view.c create mode 100644 tools/path-view.h diff --git a/docs/reference/gtk/gtk4-path-tool.rst b/docs/reference/gtk/gtk4-path-tool.rst new file mode 100644 index 0000000000..a2cca294e5 --- /dev/null +++ b/docs/reference/gtk/gtk4-path-tool.rst @@ -0,0 +1,140 @@ +.. _gtk4-path-tool(1): + +================= +gtk4-path-tool +================= + +----------------------- +GskPath Utility +----------------------- + +SYNOPSIS +-------- +| **gtk4-path-tool** [OPTIONS...] +| +| **gtk4-path-tool** decompose [OPTIONS...] +| **gtk4-path-tool** transform [OPTIONS...] +| **gtk4-path-tool** reverse [OPTIONS...] +| **gtk4-path-tool** restrict [OPTIONS...] +| **gtk4-path-tool** show [OPTIONS...] +| **gtk4-path-tool** render [OPTIONS...] +| **gtk4-path-tool** info [OPTIONS...] + +DESCRIPTION +----------- + +``gtk4-path-tool`` can perform various tasks on paths. Paths are specified +in SVG syntax, as strings like "M 100 100 C 100 200 200 200 200 100 Z". + +To read a path from a file, use a filename that starts with a '.' or a '/'. +To read a path from stdin, use '-'. + +COMMANDS +-------- + +Decomposing +^^^^^^^^^^^ + +The ``decompose`` command approximates the path by one with simpler elements. +When used without options, the curves of the path are approximated by line +segments. + +``--allow-curves`` + + Allow cubic Bézier curves to be used in the generated path. + +``--allow-conics`` + + Allow rational quadratic Bézier curves to be used in the generated path. + +Transforming +^^^^^^^^^^^^ + +The ``transform`` command applies a transformation to the path. The transform +can be 3D transform, but the resulting path is projected to the z=0 plane. + +``--transform=TRANSFORM`` + + The transform to apply. Transforms are specified in CSS syntax, for example: + "translate(10,10) rotate(45)" + +Reversing +^^^^^^^^^ + +The ``reverse`` command creates a path that traces the same outlines as +the original path, in the opposite direction. + +Restricting +^^^^^^^^^^^ + +The ``restrict`` command creates a path that traces a segment of the original +path. Note that the start and the end of the segment are specified as +path length from the beginning of the path. + +``--start=LENGTH`` + + The distance from the beginning of the path where the segment begins. The + default values is 0. + +``--end=LENGTH`` + + The distance from the beginning of the path where the segment ends. The + default value is the length of path. + +Showing +^^^^^^^ + +The ``show`` command displays the given path in a window. The interior +of the path is filled. + +``--fill-rule=VALUE`` + + The fill rule that is used to determine what areas are inside the path. + The possible values are ``winding`` or ``even-odd``. The default is ``winding``. + +``--fg-color=COLOR`` + + The color that is used to fill the interior of the path. + If not specified, black is used. + +``--bg-color=COLOR`` + + The color that is used to render the background behind the path. + If not specified, white is used. + +Rendering +^^^^^^^^^ + +The ``render`` command renders the given path as a PNG image. +The interior of the path is filled. + +``--fill-rule=VALUE`` + + The fill rule that is used to determine what areas are inside the path. + The possible values are ``winding`` or ``even-odd``. The default is ``winding``. + +``--fg-color=COLOR`` + + The color that is used to fill the interior of the path. + If not specified, black is used. + +``--bg-color=COLOR`` + + The color that is used to render the background behind the path. + If not specified, white is used. + +``--output-file=FILE`` + + The file to save the PNG image to. + If not specified, "path.png" is used. + +Info +^^^^ + +The ``info`` command shows various information about the given path, +such as its bounding box and and its length. + +REFERENCES +---------- + +- SVG Path Specification, https://www.w3.org/TR/SVG2/paths.html diff --git a/docs/reference/gtk/meson.build b/docs/reference/gtk/meson.build index 0954a1b33f..077b5f7373 100644 --- a/docs/reference/gtk/meson.build +++ b/docs/reference/gtk/meson.build @@ -76,6 +76,7 @@ if get_option('man-pages') and rst2man.found() [ 'gtk4-launch', '1', ], [ 'gtk4-query-settings', '1', ], [ 'gtk4-update-icon-cache', '1', ], + [ 'gtk4-path-tool', '1', ], ] if get_option('demos') diff --git a/tools/gtk-path-tool-decompose.c b/tools/gtk-path-tool-decompose.c new file mode 100644 index 0000000000..dfe8dca3ea --- /dev/null +++ b/tools/gtk-path-tool-decompose.c @@ -0,0 +1,142 @@ +/* Copyright 2023 Red Hat, Inc. + * + * GTK+ is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * GLib is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with GTK+; see the file COPYING. If not, + * see . + * + * Author: Matthias Clasen + */ + +#include "config.h" + +#include +#include "gtk-path-tool.h" + +#include + +static gboolean +foreach_cb (GskPathOperation op, + const graphene_point_t *pts, + gsize n_pts, + float weight, + gpointer user_data) +{ + GskPathBuilder *builder = user_data; + + switch (op) + { + case GSK_PATH_MOVE: + gsk_path_builder_move_to (builder, pts[0].x, pts[0].y); + break; + + case GSK_PATH_CLOSE: + gsk_path_builder_close (builder); + break; + + case GSK_PATH_LINE: + gsk_path_builder_line_to (builder, pts[1].x, pts[1].y); + break; + + case GSK_PATH_QUAD: + gsk_path_builder_quad_to (builder, pts[1].x, pts[1].y, pts[2].x, pts[2].y); + break; + + case GSK_PATH_CUBIC: + gsk_path_builder_cubic_to (builder, pts[1].x, pts[1].y, + pts[2].x, pts[2].y, + pts[3].x, pts[3].y); + break; + + case GSK_PATH_CONIC: + gsk_path_builder_conic_to (builder, pts[1].x, pts[1].y, + pts[2].x, pts[2].y, + weight); + break; + + default: + g_assert_not_reached (); + } + + return TRUE; +} + +void +do_decompose (int *argc, const char ***argv) +{ + GError *error = NULL; + gboolean allow_quad = FALSE; + gboolean allow_curve = FALSE; + gboolean allow_conic = FALSE; + char **args = NULL; + GOptionContext *context; + GOptionEntry entries[] = { + { "allow-quad", 0, 0, G_OPTION_ARG_NONE, &allow_quad, N_("Allow quadratic Bézier curves"), NULL }, + { "allow-cubic", 0, 0, G_OPTION_ARG_NONE, &allow_curve, N_("Allow cubic Bézier curves"), NULL }, + { "allow-conic", 0, 0, G_OPTION_ARG_NONE, &allow_conic, N_("Allow rational quadratic Bézier curves"), NULL }, + { G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &args, NULL, N_("PATH") }, + { NULL, }, + }; + GskPathForeachFlags flags; + GskPath *path, *result; + GskPathBuilder *builder; + + g_set_prgname ("gtk4-path-tool decompose"); + + context = g_option_context_new (NULL); + g_option_context_set_translation_domain (context, GETTEXT_PACKAGE); + g_option_context_add_main_entries (context, entries, NULL); + g_option_context_set_summary (context, _("Decompose a path.")); + + if (!g_option_context_parse (context, argc, (char ***)argv, &error)) + { + g_printerr ("%s\n", error->message); + g_error_free (error); + exit (1); + } + + g_option_context_free (context); + + if (args == NULL) + { + g_printerr ("%s\n", _("No paths given.")); + exit (1); + } + + path = get_path (args[0]); + + flags = 0; + if (allow_quad) + flags |= GSK_PATH_FOREACH_ALLOW_QUAD; + if (allow_curve) + flags |= GSK_PATH_FOREACH_ALLOW_CUBIC; + if (allow_conic) + flags |= GSK_PATH_FOREACH_ALLOW_CONIC; + + builder = gsk_path_builder_new (); + + gsk_path_foreach (path, flags, foreach_cb, builder); + + result = gsk_path_builder_free_to_path (builder); + + if (result) + { + char *str = gsk_path_to_string (result); + g_print ("%s\n", str); + g_free (str); + } + else + { + g_printerr ("%s\n", _("That didn't work out.")); + exit (1); + } +} diff --git a/tools/gtk-path-tool-info.c b/tools/gtk-path-tool-info.c new file mode 100644 index 0000000000..b1ab3f5b08 --- /dev/null +++ b/tools/gtk-path-tool-info.c @@ -0,0 +1,80 @@ +/* Copyright 2023 Red Hat, Inc. + * + * GTK+ is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * GLib is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with GTK+; see the file COPYING. If not, + * see . + * + * Author: Matthias Clasen + */ + +#include "config.h" + +#include +#include "gtk-path-tool.h" + +#include + +void +do_info (int *argc, const char ***argv) +{ + GError *error = NULL; + char **args = NULL; + GOptionContext *context; + GOptionEntry entries[] = { + { G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &args, NULL, N_("PATH") }, + { NULL, }, + }; + GskPath *path; + GskPathMeasure *measure; + graphene_rect_t bounds; + + g_set_prgname ("gtk4-path-tool info"); + + context = g_option_context_new (NULL); + g_option_context_set_translation_domain (context, GETTEXT_PACKAGE); + g_option_context_add_main_entries (context, entries, NULL); + g_option_context_set_summary (context, _("Print information about a path.")); + + if (!g_option_context_parse (context, argc, (char ***)argv, &error)) + { + g_printerr ("%s\n", error->message); + g_error_free (error); + exit (1); + } + + g_option_context_free (context); + + if (args == NULL) + { + g_printerr ("%s\n", _("No paths given.")); + exit (1); + } + + path = get_path (args[0]); + measure = gsk_path_measure_new (path); + + if (gsk_path_is_empty (path)) + g_print ("%s\n", _("Path is empty.")); + else + { + if (gsk_path_is_closed (path)) + g_print ("%s\n", _("Path is closed")); + + g_print ("%s %g\n", _("Path length"), gsk_path_measure_get_length (measure)); + + if (gsk_path_get_bounds (path, &bounds)) + g_print ("%s: %g %g %g %g\n", _("Bounds"), + bounds.origin.x, bounds.origin.y, + bounds.size.width, bounds.size.height); + } +} diff --git a/tools/gtk-path-tool-render.c b/tools/gtk-path-tool-render.c new file mode 100644 index 0000000000..67448d2c82 --- /dev/null +++ b/tools/gtk-path-tool-render.c @@ -0,0 +1,143 @@ +/* Copyright 2023 Red Hat, Inc. + * + * GTK+ is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * GLib is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with GTK+; see the file COPYING. If not, + * see . + * + * Author: Matthias Clasen + */ + +#include "config.h" + +#include +#include +#include + +#include +#include +#include +#include +#include "gtk-path-tool.h" + + +void +do_render (int *argc, + const char ***argv) +{ + GError *error = NULL; + const char *fill = "winding"; + const char *fg_color = "black"; + const char *bg_color = "white"; + const char *output_file = NULL; + char **args = NULL; + GOptionContext *context; + const GOptionEntry entries[] = { + { "fill-rule", 0, 0, G_OPTION_ARG_STRING, &fill, N_("Fill rule (winding, even-odd)"), N_("VALUE") }, + { "fg-color", 0, 0, G_OPTION_ARG_STRING, &fg_color, N_("Foreground color"), N_("COLOR") }, + { "bg-color", 0, 0, G_OPTION_ARG_STRING, &bg_color, N_("Background color"), N_("COLOR") }, + { "output", 0, 0, G_OPTION_ARG_FILENAME, &output_file, N_("The output file"), N_("FILE") }, + { G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_STRING_ARRAY, &args, NULL, N_("PATH") }, + { NULL, } + }; + GskPath *path; + GskFillRule fill_rule; + GdkRGBA fg, bg; + graphene_rect_t bounds; + GskRenderNode *fg_node, *nodes[2], *node; + GdkSurface *surface; + GskRenderer *renderer; + GdkTexture *texture; + const char *filename; + + if (gdk_display_get_default () == NULL) + { + g_printerr ("%s\n", _("Could not initialize windowing system")); + exit (1); + } + + g_set_prgname ("gtk4-path-tool render"); + context = g_option_context_new (NULL); + g_option_context_set_translation_domain (context, GETTEXT_PACKAGE); + g_option_context_add_main_entries (context, entries, NULL); + g_option_context_set_summary (context, _("Render the path to a png image.")); + + if (!g_option_context_parse (context, argc, (char ***)argv, &error)) + { + g_printerr ("%s\n", error->message); + g_error_free (error); + exit (1); + } + + g_option_context_free (context); + + if (args == NULL) + { + g_printerr ("%s\n", _("No path specified")); + exit (1); + } + + if (g_strv_length (args) > 1) + { + g_printerr ("%s\n", _("Can only render a single path")); + exit (1); + } + + path = get_path (args[0]); + + fill_rule = get_enum_value (GSK_TYPE_FILL_RULE, _("fill rule"), fill); + get_color (&fg, fg_color); + get_color (&bg, bg_color); + + gsk_path_get_bounds (path, &bounds); + graphene_rect_inset (&bounds, -10, -10); + + nodes[0] = gsk_color_node_new (&bg, &bounds); + fg_node = gsk_color_node_new (&fg, &bounds); + nodes[1] = gsk_fill_node_new (fg_node, path, fill_rule); + + node = gsk_container_node_new (nodes, 2); + + gsk_render_node_unref (fg_node); + gsk_render_node_unref (nodes[0]); + gsk_render_node_unref (nodes[1]); + + surface = gdk_surface_new_toplevel (gdk_display_get_default ()); + renderer = gsk_renderer_new_for_surface (surface); + + texture = gsk_renderer_render_texture (renderer, node, &bounds); + + filename = output_file ? output_file : "path.png"; + if (!gdk_texture_save_to_png (texture, filename)) + { + char *msg = g_strdup_printf (_("Saving png to '%s' failed"), filename); + g_printerr ("%s\n", msg); + exit (1); + } + + if (output_file == NULL) + { + char *msg = g_strdup_printf (_("Output written to '%s'."), filename); + g_print ("%s\n", msg); + g_free (msg); + } + + g_object_unref (texture); + gsk_renderer_unrealize (renderer); + g_object_unref (renderer); + g_object_unref (surface); + gsk_render_node_unref (node); + + gsk_path_unref (path); + + g_strfreev (args); +} diff --git a/tools/gtk-path-tool-restrict.c b/tools/gtk-path-tool-restrict.c new file mode 100644 index 0000000000..b42f4d61e0 --- /dev/null +++ b/tools/gtk-path-tool-restrict.c @@ -0,0 +1,93 @@ +/* Copyright 2023 Red Hat, Inc. + * + * GTK+ is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * GLib is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with GTK+; see the file COPYING. If not, + * see . + * + * Author: Matthias Clasen + */ + +#include "config.h" + +#include +#include "gtk-path-tool.h" + +#include + +void +do_restrict (int *argc, const char ***argv) +{ + GError *error = NULL; + double start = G_MAXDOUBLE; + double end = G_MAXDOUBLE; + char **args = NULL; + GOptionContext *context; + GOptionEntry entries[] = { + { "start", 0, 0, G_OPTION_ARG_DOUBLE, &start, N_("Beginning of segment"), N_("LENGTH") }, + { "end", 0, 0, G_OPTION_ARG_DOUBLE, &start, N_("End of segment"), N_("LENGTH") }, + { G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &args, NULL, N_("PATH") }, + { NULL, }, + }; + GskPath *path, *result; + GskPathMeasure *measure; + GskPathBuilder *builder; + + g_set_prgname ("gtk4-path-tool restrict"); + + context = g_option_context_new (NULL); + g_option_context_set_translation_domain (context, GETTEXT_PACKAGE); + g_option_context_add_main_entries (context, entries, NULL); + g_option_context_set_summary (context, _("Restrict a path to a segment.")); + + if (!g_option_context_parse (context, argc, (char ***)argv, &error)) + { + g_printerr ("%s\n", error->message); + g_error_free (error); + exit (1); + } + + g_option_context_free (context); + + if (args == NULL) + { + g_printerr ("%s\n", _("No paths given.")); + exit (1); + } + + path = get_path (args[0]); + measure = gsk_path_measure_new (path); + + if (start == G_MAXDOUBLE) + start = 0; + + if (end == G_MAXDOUBLE) + end = gsk_path_measure_get_length (measure); + + builder = gsk_path_builder_new (); + + gsk_path_builder_add_segment (builder, measure, start, end); + + result = gsk_path_builder_free_to_path (builder); + + if (result) + { + char *str = gsk_path_to_string (result); + g_print ("%s\n", str); + g_free (str); + } + else + { + g_printerr ("%s\n", _("That didn't work out.")); + exit (1); + } +} diff --git a/tools/gtk-path-tool-reverse.c b/tools/gtk-path-tool-reverse.c new file mode 100644 index 0000000000..1c1d212280 --- /dev/null +++ b/tools/gtk-path-tool-reverse.c @@ -0,0 +1,76 @@ +/* Copyright 2023 Red Hat, Inc. + * + * GTK+ is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * GLib is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with GTK+; see the file COPYING. If not, + * see . + * + * Author: Matthias Clasen + */ + +#include "config.h" + +#include +#include "gtk-path-tool.h" + +#include + +void +do_reverse (int *argc, const char ***argv) +{ + GError *error = NULL; + char **args = NULL; + GOptionContext *context; + GOptionEntry entries[] = { + { G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &args, NULL, N_("PATH") }, + { NULL, }, + }; + GskPath *path, *result; + + g_set_prgname ("gtk4-path-tool reverse"); + + context = g_option_context_new (NULL); + g_option_context_set_translation_domain (context, GETTEXT_PACKAGE); + g_option_context_add_main_entries (context, entries, NULL); + g_option_context_set_summary (context, _("Reverse a path.")); + + if (!g_option_context_parse (context, argc, (char ***)argv, &error)) + { + g_printerr ("%s\n", error->message); + g_error_free (error); + exit (1); + } + + g_option_context_free (context); + + if (args == NULL) + { + g_printerr ("%s\n", _("No paths given.")); + exit (1); + } + + path = get_path (args[0]); + + result = gsk_path_reverse (path); + + if (result) + { + char *str = gsk_path_to_string (result); + g_print ("%s\n", str); + g_free (str); + } + else + { + g_printerr ("%s\n", _("That didn't work out.")); + exit (1); + } +} diff --git a/tools/gtk-path-tool-show.c b/tools/gtk-path-tool-show.c new file mode 100644 index 0000000000..ec7484b473 --- /dev/null +++ b/tools/gtk-path-tool-show.c @@ -0,0 +1,134 @@ +/* Copyright 2023 Red Hat, Inc. + * + * GTK+ is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * GLib is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with GTK+; see the file COPYING. If not, + * see . + * + * Author: Matthias Clasen + */ + +#include "config.h" + +#include +#include +#include + +#include +#include +#include +#include +#include "gtk-path-tool.h" + +#include "path-view.h" + +static void +show_path (GskPath *path, + GskFillRule fill_rule, + const GdkRGBA *fg_color, + const GdkRGBA *bg_color) +{ + GtkWidget *window, *sw, *child; + + window = gtk_window_new (); + gtk_window_set_title (GTK_WINDOW (window), _("Path Preview")); + +// gtk_window_set_default_size (GTK_WINDOW (window), 700, 500); + + sw = gtk_scrolled_window_new (); + gtk_scrolled_window_set_propagate_natural_width (GTK_SCROLLED_WINDOW (sw), TRUE); + gtk_scrolled_window_set_propagate_natural_height (GTK_SCROLLED_WINDOW (sw), TRUE); + gtk_window_set_child (GTK_WINDOW (window), sw); + + child = path_view_new (path); + g_object_set (child, + "fill-rule", fill_rule, + "fg-color", fg_color, + "bg-color", bg_color, + NULL); + gtk_widget_set_hexpand (child, TRUE); + gtk_widget_set_vexpand (child, TRUE); + gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (sw), child); + + gtk_window_present (GTK_WINDOW (window)); + + while (g_list_model_get_n_items (gtk_window_get_toplevels ()) > 0) + g_main_context_iteration (NULL, TRUE); +} + +void +do_show (int *argc, + const char ***argv) +{ + GError *error = NULL; + const char *fill = "winding"; + const char *fg_color = "black"; + const char *bg_color = "white"; + char **args = NULL; + GOptionContext *context; + const GOptionEntry entries[] = { + { "fill-rule", 0, 0, G_OPTION_ARG_STRING, &fill, N_("Fill rule (winding, even-odd)"), N_("VALUE") }, + { "fg-color", 0, 0, G_OPTION_ARG_STRING, &fg_color, N_("Foreground color"), N_("COLOR") }, + { "bg-color", 0, 0, G_OPTION_ARG_STRING, &bg_color, N_("Background color"), N_("COLOR") }, + { G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_STRING_ARRAY, &args, NULL, N_("PATH") }, + { NULL, } + }; + GskPath *path; + GskFillRule fill_rule; + GdkRGBA fg; + GdkRGBA bg; + + if (gdk_display_get_default () == NULL) + { + g_printerr ("%s\n", _("Could not initialize windowing system")); + exit (1); + } + + g_set_prgname ("gtk4-path-tool show"); + context = g_option_context_new (NULL); + g_option_context_set_translation_domain (context, GETTEXT_PACKAGE); + g_option_context_add_main_entries (context, entries, NULL); + g_option_context_set_summary (context, _("Display the path.")); + + if (!g_option_context_parse (context, argc, (char ***)argv, &error)) + { + g_printerr ("%s\n", error->message); + g_error_free (error); + exit (1); + } + + g_option_context_free (context); + + if (args == NULL) + { + g_printerr ("%s\n", _("No path specified")); + exit (1); + } + + if (g_strv_length (args) > 1) + { + g_printerr ("%s\n", _("Can only show a single path")); + exit (1); + } + + path = get_path (args[0]); + + fill_rule = get_enum_value (GSK_TYPE_FILL_RULE, _("fill rule"), fill); + get_color (&fg, fg_color); + get_color (&bg, bg_color); + + show_path (path, fill_rule, &fg, &bg); + + gsk_path_unref (path); + + g_strfreev (args); +} diff --git a/tools/gtk-path-tool-transform.c b/tools/gtk-path-tool-transform.c new file mode 100644 index 0000000000..8724db3f8c --- /dev/null +++ b/tools/gtk-path-tool-transform.c @@ -0,0 +1,89 @@ +/* Copyright 2023 Red Hat, Inc. + * + * GTK+ is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * GLib is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with GTK+; see the file COPYING. If not, + * see . + * + * Author: Matthias Clasen + */ + +#include "config.h" + +#include +#include "gtk-path-tool.h" + +#include + +void +do_transform (int *argc, const char ***argv) +{ + GError *error = NULL; + const char *transform_arg = NULL; + char **args = NULL; + GOptionContext *context; + GOptionEntry entries[] = { + { "transform", 0, 0, G_OPTION_ARG_STRING, &transform_arg, N_("Transform to apply (in CSS syntax)"), N_("TRANSFORM") }, + { G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &args, NULL, N_("PATH") }, + { NULL, }, + }; + GskPath *path, *result; + GskTransform *transform = NULL; + + g_set_prgname ("gtk4-path-tool transform"); + + context = g_option_context_new (NULL); + g_option_context_set_translation_domain (context, GETTEXT_PACKAGE); + g_option_context_add_main_entries (context, entries, NULL); + g_option_context_set_summary (context, _("Transform a path.")); + + if (!g_option_context_parse (context, argc, (char ***)argv, &error)) + { + g_printerr ("%s\n", error->message); + g_error_free (error); + exit (1); + } + + g_option_context_free (context); + + if (args == NULL) + { + g_printerr ("%s\n", _("No paths given.")); + exit (1); + } + + path = get_path (args[0]); + + if (transform_arg) + { + if (!gsk_transform_parse (transform_arg, &transform)) + { + char *msg = g_strdup_printf (_("Could not parse '%s' as transform."), transform_arg); + g_printerr ("%s\n", msg); + exit (1); + } + } + + result = gsk_path_transform (path, transform); + + if (result) + { + char *str = gsk_path_to_string (result); + g_print ("%s\n", str); + g_free (str); + } + else + { + g_printerr ("%s\n", _("That didn't work out.")); + exit (1); + } +} diff --git a/tools/gtk-path-tool-utils.c b/tools/gtk-path-tool-utils.c new file mode 100644 index 0000000000..76888cadd6 --- /dev/null +++ b/tools/gtk-path-tool-utils.c @@ -0,0 +1,139 @@ +/* Copyright 2023 Red Hat, Inc. + * + * GTK+ is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * GLib is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with GTK+; see the file COPYING. If not, + * see . + * + * Author: Matthias Clasen + */ + +#include "config.h" + +#include +#include +#ifdef G_OS_UNIX +#include +#endif +#include "gtk-path-tool.h" + +#include + +GskPath * +get_path (const char *arg) +{ + char *buffer = NULL; + gsize len; + GError *error = NULL; + GskPath *path; + + if (arg[0] == '.' || arg[0] == '/') + { + if (!g_file_get_contents (arg, &buffer, &len, &error)) + { + g_printerr ("%s\n", error->message); + exit (1); + } + } +#ifdef G_OS_UNIX + else if (strcmp (arg, "-") == 0) + { + GInputStream *in; + GOutputStream *out; + + in = g_unix_input_stream_new (0, FALSE); + out = g_memory_output_stream_new (NULL, 0, g_realloc, g_free); + + if (g_output_stream_splice (out, in, 0, NULL, &error) < 0) + { + g_printerr (_("Failed to read from standard input: %s\n"), error->message); + exit (1); + } + + if (!g_output_stream_close (out, NULL, &error)) + { + g_printerr (_("Error reading from standard input: %s\n"), error->message); + exit (1); + } + + buffer = g_memory_output_stream_steal_data (G_MEMORY_OUTPUT_STREAM (out)); + + g_object_unref (out); + g_object_unref (in); + } +#endif + else + buffer = g_strdup (arg); + + g_strstrip (buffer); + + path = gsk_path_parse (buffer); + + if (path == NULL) + { + g_printerr (_("Failed to parse '%s' as path.\n"), arg); + exit (1); + } + + g_free (buffer); + + return path; +} + +int +get_enum_value (GType type, + const char *type_nick, + const char *str) +{ + GEnumClass *class = g_type_class_ref (type); + GEnumValue *value; + int val; + + value = g_enum_get_value_by_nick (class, str); + if (value) + val = value->value; + else + { + GString *s; + + s = g_string_new (""); + g_string_append_printf (s, _("Failed to parse '%s' as %s."), str, type_nick); + g_string_append (s, "\n"); + g_string_append (s, _("Possible values: ")); + + for (unsigned int i = 0; i < class->n_values; i++) + { + if (i > 0) + g_string_append (s, ", "); + g_string_append (s, class->values[i].value_nick); + } + g_printerr ("%s\n", s->str); + g_string_free (s, TRUE); + exit (1); + } + + g_type_class_unref (class); + + return val; +} + +void +get_color (GdkRGBA *rgba, + const char *str) +{ + if (!gdk_rgba_parse (rgba, str)) + { + char *msg = g_strdup_printf (_("Could not parse '%s' as color"), str); + g_printerr ("%s\n", msg); + exit (1); + } +} diff --git a/tools/gtk-path-tool.c b/tools/gtk-path-tool.c new file mode 100644 index 0000000000..820e3da154 --- /dev/null +++ b/tools/gtk-path-tool.c @@ -0,0 +1,148 @@ +/* Copyright 2023 Red Hat, Inc. + * + * GTK+ is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * GLib is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with GTK+; see the file COPYING. If not, + * see . + * + * Author: Matthias Clasen + */ + +#include "config.h" + +#include +#include +#include + +#include +#include +#include +#include +#include "gtk-path-tool.h" + +static void G_GNUC_NORETURN +usage (void) +{ + g_print (_("Usage:\n" + " gtk4-path-tool [COMMAND] [OPTION…] PATH\n" + "\n" + "Perform various tasks on paths.\n" + "\n" + "Commands:\n" + " decompose Decompose the path\n" + " transform Transform the path\n" + " reverse Reverse the path\n" + " restrict Restrict the path to a segment\n" + " show Display the path in a window\n" + " render Render the path as an image\n" + " info Print information about the path\n" + "\n")); + exit (1); +} + +/* A simplified version of g_log_writer_default_would_drop(), to avoid + * bumping up the required version of GLib to 2.68 + */ +static gboolean +would_drop (GLogLevelFlags level, + const char *domain) +{ +#if GLIB_CHECK_VERSION (2, 68, 0) + return g_log_writer_default_would_drop (level, domain); +#else + return (level & (G_LOG_LEVEL_ERROR | + G_LOG_LEVEL_CRITICAL | + G_LOG_LEVEL_WARNING)) == 0; +#endif +} + +static GLogWriterOutput +log_writer_func (GLogLevelFlags level, + const GLogField *fields, + gsize n_fields, + gpointer user_data) +{ + gsize i; + const char *domain = NULL; + const char *message = NULL; + + for (i = 0; i < n_fields; i++) + { + if (g_strcmp0 (fields[i].key, "GLIB_DOMAIN") == 0) + domain = fields[i].value; + else if (g_strcmp0 (fields[i].key, "MESSAGE") == 0) + message = fields[i].value; + } + + if (message != NULL && !would_drop (level, domain)) + { + const char *prefix; + switch (level & G_LOG_LEVEL_MASK) + { + case G_LOG_LEVEL_ERROR: + prefix = "ERROR"; + break; + case G_LOG_LEVEL_CRITICAL: + prefix = "CRITICAL"; + break; + case G_LOG_LEVEL_WARNING: + prefix = "WARNING"; + break; + default: + prefix = "INFO"; + break; + } + g_printerr ("%s-%s: %s\n", domain, prefix, message); + } + + return G_LOG_WRITER_HANDLED; +} + +int +main (int argc, const char *argv[]) +{ + g_set_prgname ("gtk4-path-tool"); + + g_log_set_writer_func (log_writer_func, NULL, NULL); + + gtk_init_check (); + + gtk_test_register_all_types (); + + if (argc < 2) + usage (); + + if (strcmp (argv[1], "--help") == 0) + usage (); + + argv++; + argc--; + + if (strcmp (argv[0], "decompose") == 0) + do_decompose (&argc, &argv); + else if (strcmp (argv[0], "info") == 0) + do_info (&argc, &argv); + else if (strcmp (argv[0], "render") == 0) + do_render (&argc, &argv); + else if (strcmp (argv[0], "restrict") == 0) + do_restrict (&argc, &argv); + else if (strcmp (argv[0], "reverse") == 0) + do_reverse (&argc, &argv); + else if (strcmp (argv[0], "show") == 0) + do_show (&argc, &argv); + else if (strcmp (argv[0], "transform") == 0) + do_transform (&argc, &argv); + else + usage (); + + return 0; +} diff --git a/tools/gtk-path-tool.h b/tools/gtk-path-tool.h new file mode 100644 index 0000000000..d2949d8cc1 --- /dev/null +++ b/tools/gtk-path-tool.h @@ -0,0 +1,16 @@ +#pragma once + +void do_info (int *argc, const char ***argv); +void do_decompose (int *argc, const char ***argv); +void do_transform (int *argc, const char ***argv); +void do_reverse (int *argc, const char ***argv); +void do_restrict (int *argc, const char ***argv); +void do_render (int *argc, const char ***argv); +void do_show (int *argc, const char ***argv); + +GskPath *get_path (const char *arg); +int get_enum_value (GType type, + const char *type_nick, + const char *str); +void get_color (GdkRGBA *rgba, + const char *str); diff --git a/tools/meson.build b/tools/meson.build index af77a7abb6..698c996a13 100644 --- a/tools/meson.build +++ b/tools/meson.build @@ -23,6 +23,16 @@ if win32_enabled endif gtk_tools = [ + ['gtk4-path-tool', ['gtk-path-tool.c', + 'gtk-path-tool-decompose.c', + 'gtk-path-tool-info.c', + 'gtk-path-tool-render.c', + 'gtk-path-tool-restrict.c', + 'gtk-path-tool-reverse.c', + 'gtk-path-tool-show.c', + 'gtk-path-tool-transform.c', + 'gtk-path-tool-utils.c', + 'path-view.c'], [libgtk_dep]], ['gtk4-query-settings', ['gtk-query-settings.c'], [libgtk_dep]], ['gtk4-builder-tool', ['gtk-builder-tool.c', 'gtk-builder-tool-simplify.c', diff --git a/tools/path-view.c b/tools/path-view.c new file mode 100644 index 0000000000..79f14c45cd --- /dev/null +++ b/tools/path-view.c @@ -0,0 +1,221 @@ +/* Copyright 2023 Red Hat, Inc. + * + * GTK+ is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * GLib is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with GTK+; see the file COPYING. If not, + * see . + * + * Author: Matthias Clasen + */ + +#include "config.h" + +#include "path-view.h" + +struct _PathView +{ + GtkWidget parent_instance; + + GskPath *path; + graphene_rect_t bounds; + GskFillRule fill_rule; + GdkRGBA fg; + GdkRGBA bg; + int padding; +}; + +enum { + PROP_PATH = 1, + PROP_FILL_RULE, + PROP_FG_COLOR, + PROP_BG_COLOR, + N_PROPERTIES +}; + +static GParamSpec *properties[N_PROPERTIES] = { NULL, }; + +struct _PathViewClass +{ + GtkWidgetClass parent_class; +}; + +G_DEFINE_TYPE (PathView, path_view, GTK_TYPE_WIDGET) + +static void +path_view_init (PathView *self) +{ + self->fill_rule = GSK_FILL_RULE_WINDING; + self->fg = (GdkRGBA) { 0, 0, 0, 1}; + self->bg = (GdkRGBA) { 1, 1, 1, 1}; + self->padding = 10; +} + +static void +path_view_dispose (GObject *object) +{ + PathView *self = PATH_VIEW (object); + + g_clear_pointer (&self->path, gsk_path_unref); + + G_OBJECT_CLASS (path_view_parent_class)->dispose (object); +} + +static void +path_view_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + PathView *self = PATH_VIEW (object); + + switch (prop_id) + { + case PROP_PATH: + g_value_set_boxed (value, self->path); + break; + + case PROP_FILL_RULE: + g_value_set_enum (value, self->fill_rule); + break; + + case PROP_FG_COLOR: + g_value_set_boxed (value, &self->fg); + break; + + case PROP_BG_COLOR: + g_value_set_boxed (value, &self->bg); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +path_view_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + PathView *self = PATH_VIEW (object); + + switch (prop_id) + { + case PROP_PATH: + g_clear_pointer (&self->path, gsk_path_unref); + self->path = g_value_dup_boxed (value); + gsk_path_get_bounds (self->path, &self->bounds); + gtk_widget_queue_resize (GTK_WIDGET (self)); + break; + + case PROP_FILL_RULE: + self->fill_rule = g_value_get_enum (value); + gtk_widget_queue_draw (GTK_WIDGET (self)); + break; + + case PROP_FG_COLOR: + self->fg = *(GdkRGBA *) g_value_get_boxed (value); + gtk_widget_queue_draw (GTK_WIDGET (self)); + break; + + case PROP_BG_COLOR: + self->bg = *(GdkRGBA *) g_value_get_boxed (value); + gtk_widget_queue_draw (GTK_WIDGET (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + + +static void +path_view_measure (GtkWidget *widget, + GtkOrientation orientation, + int for_size, + int *minimum, + int *natural, + int *minimum_baseline, + int *natural_baseline) +{ + PathView *self = PATH_VIEW (widget); + + if (orientation == GTK_ORIENTATION_HORIZONTAL) + *minimum = *natural = (int) ceilf (self->bounds.size.width) + 2 * self->padding; + else + *minimum = *natural = (int) ceilf (self->bounds.size.height) + 2 * self->padding; +} + +static void +path_view_snapshot (GtkWidget *widget, + GtkSnapshot *snapshot) +{ + PathView *self = PATH_VIEW (widget); + graphene_rect_t bounds = self->bounds; + + graphene_rect_inset (&bounds, - self->padding, - self->padding); + + gtk_snapshot_save (snapshot); + gtk_snapshot_append_color (snapshot, &self->bg, &self->bounds); + gtk_snapshot_translate (snapshot, &GRAPHENE_POINT_INIT (self->padding, self->padding)); + gtk_snapshot_push_fill (snapshot, self->path, self->fill_rule); + gtk_snapshot_append_color (snapshot, &self->fg, &self->bounds); + gtk_snapshot_pop (snapshot); + gtk_snapshot_restore (snapshot); +} + +static void +path_view_class_init (PathViewClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (class); + + object_class->dispose = path_view_dispose; + object_class->get_property = path_view_get_property; + object_class->set_property = path_view_set_property; + + widget_class->measure = path_view_measure; + widget_class->snapshot = path_view_snapshot; + + properties[PROP_PATH] + = g_param_spec_boxed ("path", NULL, NULL, + GSK_TYPE_PATH, + G_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY); + + properties[PROP_FILL_RULE] + = g_param_spec_enum ("fill-rule", NULL, NULL, + GSK_TYPE_FILL_RULE, + GSK_FILL_RULE_WINDING, + G_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY); + + properties[PROP_FG_COLOR] + = g_param_spec_boxed ("fg-color", NULL, NULL, + GDK_TYPE_RGBA, + G_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY); + + properties[PROP_BG_COLOR] + = g_param_spec_boxed ("bg-color", NULL, NULL, + GDK_TYPE_RGBA, + G_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, N_PROPERTIES, properties); +} + +GtkWidget * +path_view_new (GskPath *path) +{ + return g_object_new (PATH_TYPE_VIEW, + "path", path, + NULL); +} diff --git a/tools/path-view.h b/tools/path-view.h new file mode 100644 index 0000000000..2998d727b8 --- /dev/null +++ b/tools/path-view.h @@ -0,0 +1,9 @@ +#pragma once + +#include + +#define PATH_TYPE_VIEW (path_view_get_type ()) + +G_DECLARE_FINAL_TYPE (PathView, path_view, PATH, VIEW, GtkWidget) + +GtkWidget * path_view_new (GskPath *path);