gtk/demos/gtk-demo/fontify.c
Matthias Clasen 6b708e9b60 textbuffer: Redo insert markup a little bit
Redo the tag insertion function to avoid quadratic
behavior, and at the same time, fix handling of
alpha for color attributes.

Update the copy of this function in gtk4-demo
as well.
2020-09-19 14:17:28 -04:00

574 lines
15 KiB
C

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "config.h"
#include <gtk/gtk.h>
#include <glib/gstdio.h>
#ifdef HAVE_GIO_UNIX
#include <gio/gunixoutputstream.h>
#include <fcntl.h>
#endif
/* This is the guts of gtk_text_buffer_insert_markup,
* copied here so we can make an incremental version.
*/
static void
insert_tags_for_attributes (GtkTextBuffer *buffer,
PangoAttrIterator *iter,
GtkTextIter *start,
GtkTextIter *end)
{
GtkTextTagTable *table;
GSList *attrs, *l;
GtkTextTag *tag;
char name[256];
float fg_alpha, bg_alpha;
table = gtk_text_buffer_get_tag_table (buffer);
#define LANGUAGE_ATTR(attr_name) \
{ \
const char *language = pango_language_to_string (((PangoAttrLanguage*)attr)->value); \
g_snprintf (name, 256, "language=%s", language); \
tag = gtk_text_tag_table_lookup (table, name); \
if (!tag) \
{ \
tag = gtk_text_tag_new (name); \
g_object_set (tag, #attr_name, language, NULL); \
gtk_text_tag_table_add (table, tag); \
g_object_unref (tag); \
} \
gtk_text_buffer_apply_tag (buffer, tag, start, end); \
}
#define STRING_ATTR(attr_name) \
{ \
const char *string = ((PangoAttrString*)attr)->value; \
g_snprintf (name, 256, #attr_name "=%s", string); \
tag = gtk_text_tag_table_lookup (table, name); \
if (!tag) \
{ \
tag = gtk_text_tag_new (name); \
g_object_set (tag, #attr_name, string, NULL); \
gtk_text_tag_table_add (table, tag); \
g_object_unref (tag); \
} \
gtk_text_buffer_apply_tag (buffer, tag, start, end); \
}
#define INT_ATTR(attr_name) \
{ \
int value = ((PangoAttrInt*)attr)->value; \
g_snprintf (name, 256, #attr_name "=%d", value); \
tag = gtk_text_tag_table_lookup (table, name); \
if (!tag) \
{ \
tag = gtk_text_tag_new (name); \
g_object_set (tag, #attr_name, value, NULL); \
gtk_text_tag_table_add (table, tag); \
g_object_unref (tag); \
} \
gtk_text_buffer_apply_tag (buffer, tag, start, end); \
}
#define FONT_ATTR(attr_name) \
{ \
PangoFontDescription *desc = ((PangoAttrFontDesc*)attr)->desc; \
char *str = pango_font_description_to_string (desc); \
g_snprintf (name, 256, "font-desc=%s", str); \
g_free (str); \
tag = gtk_text_tag_table_lookup (table, name); \
if (!tag) \
{ \
tag = gtk_text_tag_new (name); \
g_object_set (tag, #attr_name, desc, NULL); \
gtk_text_tag_table_add (table, tag); \
g_object_unref (tag); \
} \
gtk_text_buffer_apply_tag (buffer, tag, start, end); \
}
#define FLOAT_ATTR(attr_name) \
{ \
float value = ((PangoAttrFloat*)attr)->value; \
g_snprintf (name, 256, #attr_name "=%g", value); \
tag = gtk_text_tag_table_lookup (table, name); \
if (!tag) \
{ \
tag = gtk_text_tag_new (name); \
g_object_set (tag, #attr_name, value, NULL); \
gtk_text_tag_table_add (table, tag); \
g_object_unref (tag); \
} \
gtk_text_buffer_apply_tag (buffer, tag, start, end); \
}
#define RGBA_ATTR(attr_name, alpha_value) \
{ \
PangoColor *color; \
GdkRGBA rgba; \
color = &((PangoAttrColor*)attr)->color; \
rgba.red = color->red / 65535.; \
rgba.green = color->green / 65535.; \
rgba.blue = color->blue / 65535.; \
rgba.alpha = alpha_value; \
char *str = gdk_rgba_to_string (&rgba); \
g_snprintf (name, 256, #attr_name "=%s", str); \
g_free (str); \
tag = gtk_text_tag_table_lookup (table, name); \
if (!tag) \
{ \
tag = gtk_text_tag_new (name); \
g_object_set (tag, #attr_name, &rgba, NULL); \
gtk_text_tag_table_add (table, tag); \
g_object_unref (tag); \
} \
gtk_text_buffer_apply_tag (buffer, tag, start, end); \
}
fg_alpha = bg_alpha = 1.;
attrs = pango_attr_iterator_get_attrs (iter);
for (l = attrs; l; l = l->next)
{
PangoAttribute *attr = l->data;
switch ((int)attr->klass->type)
{
case PANGO_ATTR_FOREGROUND_ALPHA:
fg_alpha = ((PangoAttrInt*)attr)->value / 65535.;
break;
case PANGO_ATTR_BACKGROUND_ALPHA:
bg_alpha = ((PangoAttrInt*)attr)->value / 65535.;
break;
default:
break;
}
}
for (l = attrs; l; l = l->next)
{
PangoAttribute *attr = l->data;
switch (attr->klass->type)
{
case PANGO_ATTR_LANGUAGE:
LANGUAGE_ATTR (language);
break;
case PANGO_ATTR_FAMILY:
STRING_ATTR (family);
break;
case PANGO_ATTR_STYLE:
INT_ATTR (style);
break;
case PANGO_ATTR_WEIGHT:
INT_ATTR (weight);
break;
case PANGO_ATTR_VARIANT:
INT_ATTR (variant);
break;
case PANGO_ATTR_STRETCH:
INT_ATTR (stretch);
break;
case PANGO_ATTR_SIZE:
INT_ATTR (size);
break;
case PANGO_ATTR_FONT_DESC:
FONT_ATTR (font-desc);
break;
case PANGO_ATTR_FOREGROUND:
RGBA_ATTR (foreground_rgba, fg_alpha);
break;
case PANGO_ATTR_BACKGROUND:
RGBA_ATTR (background_rgba, bg_alpha);
break;
case PANGO_ATTR_UNDERLINE:
INT_ATTR (underline);
break;
case PANGO_ATTR_UNDERLINE_COLOR:
RGBA_ATTR (underline_rgba, fg_alpha);
break;
case PANGO_ATTR_OVERLINE:
INT_ATTR (overline);
break;
case PANGO_ATTR_OVERLINE_COLOR:
RGBA_ATTR (overline_rgba, fg_alpha);
break;
case PANGO_ATTR_STRIKETHROUGH:
INT_ATTR (strikethrough);
break;
case PANGO_ATTR_STRIKETHROUGH_COLOR:
RGBA_ATTR (strikethrough_rgba, fg_alpha);
break;
case PANGO_ATTR_RISE:
INT_ATTR (rise);
break;
case PANGO_ATTR_SCALE:
FLOAT_ATTR (scale);
break;
case PANGO_ATTR_FALLBACK:
INT_ATTR (fallback);
break;
case PANGO_ATTR_LETTER_SPACING:
INT_ATTR (letter_spacing);
break;
case PANGO_ATTR_FONT_FEATURES:
STRING_ATTR (font_features);
break;
case PANGO_ATTR_ALLOW_BREAKS:
INT_ATTR (allow_breaks);
break;
case PANGO_ATTR_SHOW:
INT_ATTR (show_spaces);
break;
case PANGO_ATTR_INSERT_HYPHENS:
INT_ATTR (insert_hyphens);
break;
case PANGO_ATTR_SHAPE:
case PANGO_ATTR_ABSOLUTE_SIZE:
case PANGO_ATTR_GRAVITY:
case PANGO_ATTR_GRAVITY_HINT:
case PANGO_ATTR_FOREGROUND_ALPHA:
case PANGO_ATTR_BACKGROUND_ALPHA:
break;
case PANGO_ATTR_INVALID:
default:
g_assert_not_reached ();
break;
}
}
g_slist_free_full (attrs, (GDestroyNotify)pango_attribute_destroy);
#undef LANGUAGE_ATTR
#undef STRING_ATTR
#undef INT_ATTR
#undef FONT_ATTR
#undef FLOAT_ATTR
#undef RGBA_ATTR
}
typedef struct
{
GMarkupParseContext *parser;
char *markup;
gsize pos;
gsize len;
GtkTextBuffer *buffer;
GtkTextIter iter;
GtkTextMark *mark;
PangoAttrList *attributes;
char *text;
PangoAttrIterator *attr;
} MarkupData;
static void
free_markup_data (MarkupData *mdata)
{
g_free (mdata->markup);
g_clear_pointer (&mdata->parser, g_markup_parse_context_free);
gtk_text_buffer_delete_mark (mdata->buffer, mdata->mark);
g_clear_pointer (&mdata->attr, pango_attr_iterator_destroy);
g_clear_pointer (&mdata->attributes, pango_attr_list_unref);
g_free (mdata->text);
g_object_unref (mdata->buffer);
g_free (mdata);
}
static gboolean
insert_markup_idle (gpointer data)
{
MarkupData *mdata = data;
gint64 begin;
begin = g_get_monotonic_time ();
do
{
int start, end;
int start_offset;
GtkTextIter start_iter;
if (g_get_monotonic_time () - begin > G_TIME_SPAN_MILLISECOND)
{
g_idle_add (insert_markup_idle, data);
return G_SOURCE_REMOVE;
}
pango_attr_iterator_range (mdata->attr, &start, &end);
if (end == G_MAXINT) /* last chunk */
end = start - 1; /* resulting in -1 to be passed to _insert */
start_offset = gtk_text_iter_get_offset (&mdata->iter);
gtk_text_buffer_insert (mdata->buffer, &mdata->iter, mdata->text + start, end - start);
gtk_text_buffer_get_iter_at_offset (mdata->buffer, &start_iter, start_offset);
insert_tags_for_attributes (mdata->buffer, mdata->attr, &start_iter, &mdata->iter);
gtk_text_buffer_get_iter_at_mark (mdata->buffer, &mdata->iter, mdata->mark);
}
while (pango_attr_iterator_next (mdata->attr));
free_markup_data (mdata);
return G_SOURCE_REMOVE;
}
static gboolean
parse_markup_idle (gpointer data)
{
MarkupData *mdata = data;
gint64 begin;
GError *error = NULL;
begin = g_get_monotonic_time ();
do {
if (g_get_monotonic_time () - begin > G_TIME_SPAN_MILLISECOND)
{
g_idle_add (parse_markup_idle, data);
return G_SOURCE_REMOVE;
}
if (!g_markup_parse_context_parse (mdata->parser,
mdata->markup + mdata->pos,
MIN (4096, mdata->len - mdata->pos),
&error))
{
g_warning ("Invalid markup string: %s", error->message);
g_error_free (error);
free_markup_data (mdata);
return G_SOURCE_REMOVE;
}
mdata->pos += 4096;
} while (mdata->pos < mdata->len);
if (!pango_markup_parser_finish (mdata->parser,
&mdata->attributes,
&mdata->text,
NULL,
&error))
{
g_warning ("Invalid markup string: %s", error->message);
g_error_free (error);
free_markup_data (mdata);
return G_SOURCE_REMOVE;
}
if (!mdata->attributes)
{
gtk_text_buffer_insert (mdata->buffer, &mdata->iter, mdata->text, -1);
free_markup_data (mdata);
return G_SOURCE_REMOVE;
}
mdata->attr = pango_attr_list_get_iterator (mdata->attributes);
insert_markup_idle (data);
return G_SOURCE_REMOVE;
}
/* Takes a ref on @buffer while it is operating,
* and consumes @markup.
*/
static void
insert_markup (GtkTextBuffer *buffer,
GtkTextIter *iter,
char *markup,
int len)
{
MarkupData *data;
g_return_if_fail (GTK_IS_TEXT_BUFFER (buffer));
data = g_new0 (MarkupData, 1);
data->buffer = g_object_ref (buffer);
data->iter = *iter;
data->markup = markup;
data->len = len;
data->parser = pango_markup_parser_new (0);
data->pos = 0;
/* create mark with right gravity */
data->mark = gtk_text_buffer_create_mark (buffer, NULL, iter, FALSE);
parse_markup_idle (data);
}
static void
fontify_finish (GObject *source,
GAsyncResult *result,
gpointer data)
{
GSubprocess *subprocess = G_SUBPROCESS (source);
GtkTextBuffer *buffer = data;
GBytes *stdout_buf = NULL;
GBytes *stderr_buf = NULL;
GError *error = NULL;
if (!g_subprocess_communicate_finish (subprocess,
result,
&stdout_buf,
&stderr_buf,
&error))
{
g_clear_pointer (&stdout_buf, g_bytes_unref);
g_clear_pointer (&stderr_buf, g_bytes_unref);
g_warning ("%s", error->message);
g_clear_error (&error);
g_object_unref (subprocess);
g_object_unref (buffer);
return;
}
if (g_subprocess_get_exit_status (subprocess) != 0)
{
if (stderr_buf)
g_warning ("%s", (char *)g_bytes_get_data (stderr_buf, NULL));
g_clear_pointer (&stderr_buf, g_bytes_unref);
}
g_object_unref (subprocess);
g_clear_pointer (&stderr_buf, g_bytes_unref);
if (stdout_buf)
{
char *markup;
gsize len;
char *p;
GtkTextIter start;
gtk_text_buffer_set_text (buffer, "", 0);
/* highlight puts a span with font and size around its output,
* which we don't want.
*/
markup = g_bytes_unref_to_data (stdout_buf, &len);
for (p = markup + strlen ("<span "); *p != '>'; p++) *p = ' ';
gtk_text_buffer_get_start_iter (buffer, &start);
insert_markup (buffer, &start, markup, len);
}
g_object_unref (buffer);
}
void
fontify (const char *format,
GtkTextBuffer *source_buffer)
{
GSubprocess *subprocess;
char *format_arg;
GtkSettings *settings;
char *theme;
gboolean prefer_dark;
const char *style_arg;
const char *text;
GtkTextIter start, end;
GBytes *bytes;
GError *error = NULL;
settings = gtk_settings_get_default ();
g_object_get (settings,
"gtk-theme-name", &theme,
"gtk-application-prefer-dark-theme", &prefer_dark,
NULL);
if (prefer_dark || strcmp (theme, "HighContrastInverse") == 0)
style_arg = "--style=edit-vim-dark";
else
style_arg = "--style=edit-kwrite";
g_free (theme);
format_arg = g_strconcat ("--syntax=", format, NULL);
subprocess = g_subprocess_new (G_SUBPROCESS_FLAGS_STDIN_PIPE |
G_SUBPROCESS_FLAGS_STDOUT_PIPE |
G_SUBPROCESS_FLAGS_STDERR_PIPE,
&error,
"highlight",
format_arg,
"--out-format=pango",
style_arg,
NULL);
g_free (format_arg);
if (!subprocess)
{
if (g_error_matches (error, G_SPAWN_ERROR, G_SPAWN_ERROR_NOENT))
{
static gboolean warned = FALSE;
if (!warned)
{
warned = TRUE;
g_message ("For syntax highlighting, install the “highlight” program");
}
}
else
g_warning ("%s", error->message);
g_clear_error (&error);
return;
}
gtk_text_buffer_get_bounds (source_buffer, &start, &end);
text = gtk_text_buffer_get_text (source_buffer, &start, &end, TRUE);
bytes = g_bytes_new_static (text, strlen (text));
#ifdef HAVE_GIO_UNIX
/* Work around https://gitlab.gnome.org/GNOME/glib/-/issues/2182 */
if (G_IS_UNIX_OUTPUT_STREAM (g_subprocess_get_stdin_pipe (subprocess)))
{
GOutputStream *stdin_pipe = g_subprocess_get_stdin_pipe (subprocess);
int fd = g_unix_output_stream_get_fd (G_UNIX_OUTPUT_STREAM (stdin_pipe));
fcntl (fd, F_SETFL, O_NONBLOCK);
}
#endif
g_subprocess_communicate_async (subprocess,
bytes,
NULL,
fontify_finish,
g_object_ref (source_buffer));
}