From 1e725b66a5d1031f87f687c689724a8ab920a1b0 Mon Sep 17 00:00:00 2001 From: Emmanuele Bassi Date: Sat, 29 Jun 2019 18:03:42 +0100 Subject: [PATCH] Add VFL parser for constraints Constraints can be expressed with a compact syntax, called VFL (visual format language). --- gtk/gtkconstraintvflparser.c | 1226 +++++++++++++++++++++++++++ gtk/gtkconstraintvflparserprivate.h | 89 ++ gtk/meson.build | 1 + 3 files changed, 1316 insertions(+) create mode 100644 gtk/gtkconstraintvflparser.c create mode 100644 gtk/gtkconstraintvflparserprivate.h diff --git a/gtk/gtkconstraintvflparser.c b/gtk/gtkconstraintvflparser.c new file mode 100644 index 0000000000..0d46784991 --- /dev/null +++ b/gtk/gtkconstraintvflparser.c @@ -0,0 +1,1226 @@ +/* gtkconstraintvflparser.c: VFL constraint definition parser + * + * Copyright 2017 Endless + * Copyright 2019 GNOME Foundation + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * This library 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.1 of the License, or (at your option) any later version. + * + * This library 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 this library; if not, see . + */ + +#include "config.h" + +#include "gtkconstraintvflparserprivate.h" + +#include + +typedef enum { + VFL_HORIZONTAL, + VFL_VERTICAL +} VflOrientation; + +typedef struct { + GtkConstraintRelation relation; + + double constant; + double multiplier; + const char *subject; + char *object; + const char *attr; + + double priority; +} VflPredicate; + +typedef enum { + SPACING_SET = 1 << 0, + SPACING_DEFAULT = 1 << 1, + SPACING_PREDICATE = 1 << 2 +} VflSpacingFlags; + +typedef struct { + double size; + VflSpacingFlags flags; + VflPredicate predicate; +} VflSpacing; + +typedef struct _VflView VflView; + +struct _VflView +{ + char *name; + + /* Decides which attributes are admissible */ + VflOrientation orientation; + + /* A set of predicates, which will be used to + * set up constraints + */ + GArray *predicates; + + VflSpacing spacing; + + VflView *prev_view; + VflView *next_view; +}; + +struct _GtkConstraintVflParser +{ + char *buffer; + gsize buffer_len; + + int error_offset; + int error_range; + + int default_spacing[2]; + + /* Set */ + GHashTable *metrics_set; + /* Set */ + GHashTable *views_set; + + const char *cursor; + + /* Decides which attributes are admissible */ + VflOrientation orientation; + + VflView *leading_super; + VflView *trailing_super; + + VflView *current_view; + VflView *views; +}; + +GQuark +gtk_constraint_vfl_parser_error_quark (void) +{ + return g_quark_from_static_string ("gtk-constraint-vfl-parser-error-quark"); +} + +GtkConstraintVflParser * +gtk_constraint_vfl_parser_new (void) +{ + GtkConstraintVflParser *res = g_new0 (GtkConstraintVflParser, 1); + + res->default_spacing[VFL_HORIZONTAL] = 8; + res->default_spacing[VFL_VERTICAL] = 8; + + res->orientation = VFL_HORIZONTAL; + + return res; +} + +void +gtk_constraint_vfl_parser_set_default_spacing (GtkConstraintVflParser *parser, + int hspacing, + int vspacing) +{ + parser->default_spacing[VFL_HORIZONTAL] = hspacing < 0 ? 8 : hspacing; + parser->default_spacing[VFL_VERTICAL] = vspacing < 0 ? 8 : vspacing; +} + +void +gtk_constraint_vfl_parser_set_metrics (GtkConstraintVflParser *parser, + GHashTable *metrics) +{ + parser->metrics_set = metrics; +} + +void +gtk_constraint_vfl_parser_set_views (GtkConstraintVflParser *parser, + GHashTable *views) +{ + parser->views_set = views; +} + +static int +get_default_spacing (GtkConstraintVflParser *parser) +{ + return parser->default_spacing[parser->orientation]; +} + +/* Default attributes, if unnamed, depending on the orientation */ +static const char *default_attribute[2] = { + [VFL_HORIZONTAL] = "width", + [VFL_VERTICAL] = "height", +}; + +static gboolean +parse_relation (const char *str, + GtkConstraintRelation *relation, + char **endptr, + GError **error) +{ + const char *cur = str; + + if (*cur == '=') + { + cur += 1; + + if (*cur == '=') + { + *relation = GTK_CONSTRAINT_RELATION_EQ; + *endptr = (char *) cur + 1; + return TRUE; + } + + goto out; + } + else if (*cur == '>') + { + cur += 1; + + if (*cur == '=') + { + *relation = GTK_CONSTRAINT_RELATION_GE; + *endptr = (char *) cur + 1; + return TRUE; + } + + goto out; + } + else if (*cur == '<') + { + cur += 1; + + if (*cur == '=') + { + *relation = GTK_CONSTRAINT_RELATION_LE; + *endptr = (char *) cur + 1; + return TRUE; + } + + goto out; + } + +out: + g_set_error (error, GTK_CONSTRAINT_VFL_PARSER_ERROR, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_RELATION, + "Unknown relation; must be one of '==', '>=', or '<='"); + return FALSE; +} + +static gboolean +has_metric (GtkConstraintVflParser *parser, + const char *name) +{ + if (parser->metrics_set == NULL) + return FALSE; + + return g_hash_table_contains (parser->metrics_set, name); +} + +static gboolean +has_view (GtkConstraintVflParser *parser, + const char *name) +{ + if (parser->views_set == NULL) + return FALSE; + + if (!g_hash_table_contains (parser->views_set, name)) + return FALSE; + + return g_hash_table_lookup (parser->views_set, name) != NULL; +} + +/* Valid attributes */ +static const struct { + int len; + const char *name; +} valid_attributes[] = { + { 5, "width" }, + { 6, "height" }, + { 7, "centerX" }, + { 7, "centerY" }, + { 3, "top" }, + { 6, "bottom" }, + { 4, "left" }, + { 5, "right" }, + { 5, "start" }, + { 3, "end" }, + { 8, "baseline" } +}; + +static char * +get_offset_to (const char *str, + const char *tokens) +{ + char *offset = NULL; + int n_tokens = strlen (tokens); + + for (int i = 0; i < n_tokens; i++) + { + if ((offset = strchr (str, tokens[i])) != NULL) + break; + } + + return offset; +} + +static gboolean +parse_predicate (GtkConstraintVflParser *parser, + const char *cursor, + VflPredicate *predicate, + char **endptr, + GError **error) +{ + VflOrientation orientation = parser->orientation; + const char *end = cursor; + + predicate->object = NULL; + predicate->multiplier = 1.0; + + /* = ()? () ('.')? ()? ('@')? + * = '==' | '<=' | '>=' + * = | + * = | + * = [A-Za-z_]([A-Za-z0-9_]*) + * = [A-Za-z_]([A-Za-z0-9_]*) + * = (['*'|'/'])? (['+'|'-'])? + * = | 'weak' | 'medium' | 'strong' | 'required' + */ + + /* Parse relation */ + if (*end == '=' || *end == '>' || *end == '<') + { + GtkConstraintRelation relation; + char *tmp; + + if (!parse_relation (end, &relation, &tmp, error)) + { + parser->error_offset = end - parser->cursor; + parser->error_range = 0; + return FALSE; + } + + predicate->relation = relation; + + end = tmp; + } + else + predicate->relation = GTK_CONSTRAINT_RELATION_EQ; + + /* Parse object of predicate */ + if (g_ascii_isdigit (*end)) + { + char *tmp; + + /* */ + predicate->object = NULL; + predicate->attr = default_attribute[orientation]; + predicate->constant = g_ascii_strtod (end, &tmp); + + end = tmp; + } + else if (g_ascii_isalpha (*end) || *end == '_') + { + const char *name_start = end; + + while (g_ascii_isalnum (*end) || *end == '_') + end += 1; + + char *name = g_strndup (name_start, end - name_start); + + /* We only accept view names if the subject of the predicate + * is a view, i.e. we do not allow view names inside a spacing + * predicate + */ + if (predicate->subject == NULL) + { + if (parser->metrics_set == NULL || !has_metric (parser, name)) + { + parser->error_offset = name_start - parser->cursor; + parser->error_range = end - name_start; + g_set_error (error, GTK_CONSTRAINT_VFL_PARSER_ERROR, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_METRIC, + "Unable to find metric with name '%s'", name); + g_free (name); + return FALSE; + } + + double *val = g_hash_table_lookup (parser->metrics_set, name); + + predicate->object = NULL; + predicate->attr = default_attribute[orientation]; + predicate->constant = *val; + + g_free (name); + + goto parse_operators; + } + + if (has_metric (parser, name)) + { + double *val = g_hash_table_lookup (parser->metrics_set, name); + + predicate->object = NULL; + predicate->attr = default_attribute[orientation]; + predicate->constant = *val; + + g_free (name); + + goto parse_operators; + } + + if (has_view (parser, name)) + { + /* Transfer name's ownership to the predicate */ + predicate->object = name; + predicate->attr = default_attribute[orientation]; + predicate->constant = 0; + + goto parse_attribute; + } + + parser->error_offset = name_start - parser->cursor; + parser->error_range = end - name_start; + g_set_error (error, GTK_CONSTRAINT_VFL_PARSER_ERROR, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_VIEW, + "Unable to find view with name '%s'", name); + g_free (name); + return FALSE; + } + else + { + parser->error_offset = end - parser->cursor; + parser->error_range = 0; + g_set_error (error, GTK_CONSTRAINT_VFL_PARSER_ERROR, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_SYMBOL, + "Expected constant, view name, or metric"); + return FALSE; + } + +parse_attribute: + if (*end == '.') + { + end += 1; + predicate->attr = NULL; + + for (int i = 0; i < G_N_ELEMENTS (valid_attributes); i++) + { + if (g_ascii_strncasecmp (valid_attributes[i].name, end, valid_attributes[i].len) == 0) + { + predicate->attr = valid_attributes[i].name; + end += valid_attributes[i].len; + } + } + + if (predicate->attr == NULL) + { + char *range_end = get_offset_to (end, "*/+-@,)]"); + + if (range_end != NULL) + parser->error_range = range_end - end - 1; + else + parser->error_range = 0; + + g_free (predicate->object); + + parser->error_offset = end - parser->cursor; + g_set_error (error, GTK_CONSTRAINT_VFL_PARSER_ERROR, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_ATTRIBUTE, + "Attribute must be on one of 'width', 'height', " + "'centerX', 'centerY', 'top', 'bottom', " + "'left', 'right', 'start', 'end', 'baseline'"); + return FALSE; + } + } + +parse_operators: + /* Parse multiplier operator */ + while (g_ascii_isspace (*end)) + end += 1; + + if ((*end == '*') || (*end == '/')) + { + double multiplier; + const char *operator; + + operator = end; + end += 1; + + while (g_ascii_isspace (*end)) + end += 1; + + if (g_ascii_isdigit (*end)) + { + char *tmp; + + multiplier = g_ascii_strtod (end, &tmp); + end = tmp; + } + else + { + g_free (predicate->object); + + parser->error_offset = end - parser->cursor; + parser->error_range = 0; + g_set_error (error, GTK_CONSTRAINT_VFL_PARSER_ERROR, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_SYMBOL, + "Expected a positive number as a multiplier"); + return FALSE; + } + + if (predicate->object != NULL) + { + if (*operator == '*') + predicate->multiplier = multiplier; + else + predicate->multiplier = 1.0 / multiplier; + } + else + { + /* If the subject is a constant then apply multiplier directly */ + if (*operator == '*') + predicate->constant *= multiplier; + else + predicate->constant *= 1.0 / multiplier; + } + } + + /* Parse constant operator */ + while (g_ascii_isspace (*end)) + end += 1; + + if ((*end == '+') || (*end == '-')) + { + double constant; + const char *operator; + + operator = end; + end += 1; + + while (g_ascii_isspace (*end)) + end += 1; + + if (g_ascii_isdigit (*end)) + { + char *tmp; + + constant = g_ascii_strtod (end, &tmp); + end = tmp; + } + else + { + g_free (predicate->object); + + parser->error_offset = end - parser->cursor; + parser->error_range = 0; + g_set_error (error, GTK_CONSTRAINT_VFL_PARSER_ERROR, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_SYMBOL, + "Expected positive number as a constant"); + return FALSE; + } + + if (*operator == '+') + predicate->constant += constant; + else + predicate->constant += -1.0 * constant; + } + + /* Parse priority */ + if (*end == '@') + { + double priority; + end += 1; + + if (g_ascii_isdigit (*end)) + { + char *tmp; + + priority = g_ascii_strtod (end, &tmp); + end = tmp; + } + else if (strncmp (end, "weak", 4) == 0) + { + priority = GTK_CONSTRAINT_STRENGTH_WEAK; + end += 4; + } + else if (strncmp (end, "medium", 6) == 0) + { + priority = GTK_CONSTRAINT_STRENGTH_MEDIUM; + end += 6; + } + else if (strncmp (end, "strong", 6) == 0) + { + priority = GTK_CONSTRAINT_STRENGTH_STRONG; + end += 6; + } + else if (strncmp (end, "required", 8) == 0) + { + priority = GTK_CONSTRAINT_STRENGTH_REQUIRED; + end += 8; + } + else + { + char *range_end = get_offset_to (end, ",)]"); + + g_free (predicate->object); + + if (range_end != NULL) + parser->error_range = range_end - end - 1; + else + parser->error_range = 0; + + parser->error_offset = end - parser->cursor; + g_set_error (error, GTK_CONSTRAINT_VFL_PARSER_ERROR, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_PRIORITY, + "Priority must be a positive number or one of " + "'weak', 'medium', 'strong', and 'required'"); + return FALSE; + } + + predicate->priority = priority; + } + else + predicate->priority = GTK_CONSTRAINT_STRENGTH_REQUIRED; + + if (endptr != NULL) + *endptr = (char *) end; + + return TRUE; +} + +static gboolean +parse_view (GtkConstraintVflParser *parser, + const char *cursor, + VflView *view, + char **endptr, + GError **error) +{ + const char *end = cursor; + + /* = '[' ()? ']' + * = [A-Za-z_]([A-Za-z0-9_]+) + */ + + g_assert (*end == '['); + + /* Skip '[' */ + end += 1; + + if (!(g_ascii_isalpha (*end) || *end == '_')) + { + parser->error_offset = end - parser->cursor; + parser->error_range = 0; + g_set_error (error, GTK_CONSTRAINT_VFL_PARSER_ERROR, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_VIEW, + "View identifiers must be valid C identifiers"); + return FALSE; + } + + while (g_ascii_isalnum (*end)) + end += 1; + + if (*end == '\0') + { + parser->error_offset = end - parser->cursor; + g_set_error (error, GTK_CONSTRAINT_VFL_PARSER_ERROR, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_SYMBOL, + "A view must end with ']'"); + return FALSE; + } + + char *name = g_strndup (cursor + 1, end - cursor - 1); + if (!has_view (parser, name)) + { + parser->error_offset = (cursor + 1) - parser->cursor; + parser->error_range = end - cursor - 1; + g_set_error (error, GTK_CONSTRAINT_VFL_PARSER_ERROR, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_VIEW, + "Unable to find view with name '%s'", name); + g_free (name); + return FALSE; + } + + view->name = name; + view->predicates = g_array_new (FALSE, FALSE, sizeof (VflPredicate)); + + if (*end == ']') + { + if (endptr != NULL) + *endptr = (char *) end + 1; + + return TRUE; + } + + /* = '(' (',' )* ')' */ + if (*end != '(') + { + parser->error_offset = end - parser->cursor; + g_set_error (error, GTK_CONSTRAINT_VFL_PARSER_ERROR, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_SYMBOL, + "A predicate must follow a view name"); + return FALSE; + } + + end += 1; + + while (*end != '\0') + { + VflPredicate cur_predicate; + char *tmp; + + if (*end == ']' || *end == '\0') + { + parser->error_offset = end - parser->cursor; + g_set_error (error, GTK_CONSTRAINT_VFL_PARSER_ERROR, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_SYMBOL, + "A predicate on a view must end with ')'"); + return FALSE; + } + + memset (&cur_predicate, 0, sizeof (VflPredicate)); + + cur_predicate.subject = view->name; + if (!parse_predicate (parser, end, &cur_predicate, &tmp, error)) + return FALSE; + + end = tmp; + +#ifdef G_ENABLE_DEBUG + g_debug ("*** Found predicate: %s.%s %s %g %s (%g)\n", + cur_predicate.object != NULL ? cur_predicate.object : view->name, + cur_predicate.attr, + cur_predicate.relation == GTK_CONSTRAINT_RELATION_EQ ? "==" : + cur_predicate.relation == GTK_CONSTRAINT_RELATION_LE ? "<=" : + cur_predicate.relation == GTK_CONSTRAINT_RELATION_GE ? ">=" : + "unknown relation", + cur_predicate.constant, + cur_predicate.priority == GTK_CONSTRAINT_STRENGTH_WEAK ? "weak" : + cur_predicate.priority == GTK_CONSTRAINT_STRENGTH_MEDIUM ? "medium" : + cur_predicate.priority == GTK_CONSTRAINT_STRENGTH_STRONG ? "strong" : + cur_predicate.priority == GTK_CONSTRAINT_STRENGTH_REQUIRED ? "required" : + "explicit strength", + cur_predicate.priority); +#endif + + g_array_append_val (view->predicates, cur_predicate); + + /* If the predicate is a list, iterate again */ + if (*end == ',') + { + end += 1; + continue; + } + + /* We reached the end of the predicate */ + if (*end == ')') + { + end += 1; + break; + } + + parser->error_offset = end - parser->cursor; + g_set_error (error, GTK_CONSTRAINT_VFL_PARSER_ERROR, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_SYMBOL, + "Expected ')' at the end of a predicate, not '%c'", *end); + return FALSE; + } + + if (*end != ']') + { + parser->error_offset = end - parser->cursor; + g_set_error (error, GTK_CONSTRAINT_VFL_PARSER_ERROR, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_SYMBOL, + "Expected ']' at the end of a view, not '%c'", *end); + return FALSE; + } + + if (endptr != NULL) + *endptr = (char *) end + 1; + + return TRUE; +} + +static void +vfl_view_free (VflView *view) +{ + if (view == NULL) + return; + + g_free (view->name); + + if (view->predicates != NULL) + { + for (int i = 0; i < view->predicates->len; i++) + { + VflPredicate *p = &g_array_index (view->predicates, VflPredicate, i); + + g_free (p->object); + } + + g_array_free (view->predicates, TRUE); + view->predicates = NULL; + } + + view->prev_view = NULL; + view->next_view = NULL; + + g_slice_free (VflView, view); +} + +static void +gtk_constraint_vfl_parser_clear (GtkConstraintVflParser *parser) +{ + parser->error_offset = 0; + parser->error_range = 0; + + VflView *iter = parser->views; + while (iter != NULL) + { + VflView *next = iter->next_view; + + vfl_view_free (iter); + + iter = next; + } + + parser->views = NULL; + parser->current_view = NULL; + parser->leading_super = NULL; + parser->trailing_super = NULL; + + parser->cursor = NULL; + + g_free (parser->buffer); + parser->buffer_len = 0; +} + +void +gtk_constraint_vfl_parser_free (GtkConstraintVflParser *parser) +{ + if (parser == NULL) + return; + + gtk_constraint_vfl_parser_clear (parser); + + g_free (parser); +} + +gboolean +gtk_constraint_vfl_parser_parse_line (GtkConstraintVflParser *parser, + const char *buffer, + gssize len, + GError **error) +{ + gtk_constraint_vfl_parser_clear (parser); + + if (len > 0) + { + parser->buffer = g_strndup (buffer, len); + parser->buffer_len = len; + } + else + { + parser->buffer = g_strdup (buffer); + parser->buffer_len = strlen (buffer); + } + + parser->cursor = parser->buffer; + + const char *cur = parser->cursor; + + /* Skip leading whitespace */ + while (g_ascii_isspace (*cur)) + cur += 1; + + /* Check orientation; if none is specified, then we assume horizontal */ + parser->orientation = VFL_HORIZONTAL; + if (*cur == 'H') + { + cur += 1; + + if (*cur != ':') + { + parser->error_offset = cur - parser->cursor; + g_set_error (error, GTK_CONSTRAINT_VFL_PARSER_ERROR, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_SYMBOL, + "Expected ':' after horizontal orientation"); + return FALSE; + } + + parser->orientation = VFL_HORIZONTAL; + cur += 1; + } + else if (*cur == 'V') + { + cur += 1; + + if (*cur != ':') + { + parser->error_offset = cur - parser->cursor; + g_set_error (error, GTK_CONSTRAINT_VFL_PARSER_ERROR, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_SYMBOL, + "Expected ':' after vertical orientation"); + return FALSE; + } + + parser->orientation = VFL_VERTICAL; + cur += 1; + } + + while (*cur != '\0') + { + /* Super-view */ + if (*cur == '|') + { + if (parser->views == NULL && parser->leading_super == NULL) + { + parser->leading_super = g_slice_new0 (VflView); + + parser->leading_super->name = g_strdup ("super"); + parser->leading_super->orientation = parser->orientation; + + parser->current_view = parser->leading_super; + parser->views = parser->leading_super; + } + else if (parser->trailing_super == NULL) + { + parser->trailing_super = g_slice_new0 (VflView); + + parser->trailing_super->name = g_strdup ("super"); + parser->trailing_super->orientation = parser->orientation; + + parser->current_view->next_view = parser->trailing_super; + parser->trailing_super->prev_view = parser->current_view; + + parser->current_view = parser->trailing_super; + + /* If we reached the second '|' then we completed a line + * of layout, and we can stop + */ + break; + } + else + { + parser->error_offset = cur - parser->cursor; + g_set_error (error, GTK_CONSTRAINT_VFL_PARSER_ERROR, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_SYMBOL, + "Super views can only appear at the beginning " + "and end of the layout, and not multiple times"); + return FALSE; + } + + cur += 1; + + continue; + } + + /* Spacing */ + if (*cur == '-') + { + if (*(cur + 1) == '\0') + { + parser->error_offset = cur - parser->cursor; + g_set_error (error, GTK_CONSTRAINT_VFL_PARSER_ERROR, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_SYMBOL, + "Unterminated spacing"); + return FALSE; + } + + if (parser->current_view == NULL) + { + parser->error_offset = cur - parser->cursor; + g_set_error (error, GTK_CONSTRAINT_VFL_PARSER_ERROR, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_SYMBOL, + "Spacing cannot be set without a view"); + return FALSE; + } + + if (*(cur + 1) == '|' || *(cur + 1) == '[') + { + VflSpacing *spacing = &(parser->current_view->spacing); + + /* Default spacer */ + spacing->flags = SPACING_SET | SPACING_DEFAULT; + spacing->size = 0; + + cur += 1; + + continue; + } + else if (*(cur + 1) == '(') + { + VflPredicate *predicate; + VflSpacing *spacing; + char *tmp; + + /* Predicate */ + cur += 1; + + spacing = &(parser->current_view->spacing); + spacing->flags = SPACING_SET | SPACING_PREDICATE; + spacing->size = 0; + + /* Spacing predicates have no subject */ + predicate = &(spacing->predicate); + predicate->subject = NULL; + + cur += 1; + if (!parse_predicate (parser, cur, predicate, &tmp, error)) + return FALSE; + + if (*tmp != ')') + { + g_set_error (error, GTK_CONSTRAINT_VFL_PARSER_ERROR, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_SYMBOL, + "Expected ')' at the end of a predicate, not '%c'", *tmp); + return FALSE; + } + + cur = tmp + 1; + if (*cur != '-') + { + g_set_error (error, GTK_CONSTRAINT_VFL_PARSER_ERROR, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_SYMBOL, + "Explicit spacing must be followed by '-'"); + return FALSE; + } + + cur += 1; + + continue; + } + else if (g_ascii_isdigit (*(cur + 1))) + { + VflSpacing *spacing; + char *tmp; + + /* Explicit spacing */ + cur += 1; + + spacing = &(parser->current_view->spacing); + spacing->flags = SPACING_SET; + spacing->size = g_ascii_strtod (cur, &tmp); + + if (tmp == cur) + { + parser->error_offset = cur - parser->cursor; + g_set_error (error, GTK_CONSTRAINT_VFL_PARSER_ERROR, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_SYMBOL, + "Spacing must be a number"); + return FALSE; + } + + if (*tmp != '-') + { + parser->error_offset = cur - parser->cursor; + parser->error_range = tmp - cur; + g_set_error (error, GTK_CONSTRAINT_VFL_PARSER_ERROR, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_SYMBOL, + "Explicit spacing must be followed by '-'"); + return FALSE; + } + + cur = tmp + 1; + + continue; + } + else + { + parser->error_offset = cur - parser->cursor; + g_set_error (error, GTK_CONSTRAINT_VFL_PARSER_ERROR, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_SYMBOL, + "Spacing can either be '-' or a number"); + return FALSE; + } + } + + if (*cur == '[') + { + VflView *view = g_slice_new0 (VflView); + char *tmp; + + view->orientation = parser->orientation; + + if (!parse_view (parser, cur, view, &tmp, error)) + { + vfl_view_free (view); + return FALSE; + } + + cur = tmp; + + if (parser->views == NULL) + parser->views = view; + + view->prev_view = parser->current_view; + + if (parser->current_view != NULL) + parser->current_view->next_view = view; + + parser->current_view = view; + + continue; + } + + cur += 1; + } + + return TRUE; +} + +GtkConstraintVfl * +gtk_constraint_vfl_parser_get_constraints (GtkConstraintVflParser *parser, + int *n_constraints) +{ + GArray *constraints; + VflView *iter; + + constraints = g_array_new (FALSE, FALSE, sizeof (GtkConstraintVfl)); + + iter = parser->views; + while (iter != NULL) + { + GtkConstraintVfl c; + + if (iter->predicates != NULL) + { + for (int i = 0; i < iter->predicates->len; i++) + { + const VflPredicate *p = &g_array_index (iter->predicates, VflPredicate, i); + + c.view1 = iter->name; + c.attr1 = iter->orientation == VFL_HORIZONTAL ? "width" : "height"; + if (p->object != NULL) + { + c.view2 = p->object; + c.attr2 = p->attr; + } + else + { + c.view2 = NULL; + c.attr2 = NULL; + } + c.relation = p->relation; + c.constant = p->constant; + c.multiplier = p->multiplier; + c.strength = p->priority; + + g_array_append_val (constraints, c); + } + } + + if ((iter->spacing.flags & SPACING_SET) != 0) + { + c.view1 = iter->name; + + /* If this is the first view, we need to anchor the leading edge */ + if (iter == parser->leading_super) + c.attr1 = iter->orientation == VFL_HORIZONTAL ? "start" : "top"; + else + c.attr1 = iter->orientation == VFL_HORIZONTAL ? "end" : "bottom"; + + c.view2 = iter->next_view != NULL ? iter->next_view->name : "super"; + + if (iter == parser->trailing_super || iter->next_view == parser->trailing_super) + c.attr2 = iter->orientation == VFL_HORIZONTAL ? "end" : "bottom"; + else + c.attr2 = iter->orientation == VFL_HORIZONTAL ? "start" : "top"; + + if ((iter->spacing.flags & SPACING_PREDICATE) != 0) + { + const VflPredicate *p = &(iter->spacing.predicate); + + c.constant = p->constant * -1.0; + c.relation = p->relation; + c.strength = p->priority; + } + else if ((iter->spacing.flags & SPACING_DEFAULT) != 0) + { + c.constant = get_default_spacing (parser) * -1.0; + c.relation = GTK_CONSTRAINT_RELATION_EQ; + c.strength = GTK_CONSTRAINT_STRENGTH_REQUIRED; + } + else + { + c.constant = iter->spacing.size * -1.0; + c.relation = GTK_CONSTRAINT_RELATION_EQ; + c.strength = GTK_CONSTRAINT_STRENGTH_REQUIRED; + } + + c.multiplier = 1.0; + + g_array_append_val (constraints, c); + } + else if (iter->next_view != NULL) + { + c.view1 = iter->name; + + /* If this is the first view, we need to anchor the leading edge */ + if (iter == parser->leading_super) + c.attr1 = iter->orientation == VFL_HORIZONTAL ? "start" : "top"; + else + c.attr1 = iter->orientation == VFL_HORIZONTAL ? "end" : "bottom"; + + c.relation = GTK_CONSTRAINT_RELATION_EQ; + + c.view2 = iter->next_view->name; + + if (iter->next_view == parser->trailing_super) + c.attr2 = iter->orientation == VFL_HORIZONTAL ? "end" : "bottom"; + else + c.attr2 = iter->orientation == VFL_HORIZONTAL ? "start" : "top"; + + c.constant = 0.0; + c.multiplier = 1.0; + c.strength = GTK_CONSTRAINT_STRENGTH_REQUIRED; + + g_array_append_val (constraints, c); + } + + iter = iter->next_view; + } + + if (n_constraints != NULL) + *n_constraints = constraints->len; + +#ifdef G_ENABLE_DEBUG + for (int i = 0; i < constraints->len; i++) + { + const GtkConstraintVfl *c = &g_array_index (constraints, GtkConstraintVfl, i); + + g_debug ("{\n" + " .view1: '%s',\n" + " .attr1: '%s',\n" + " .relation: '%d',\n" + " .view2: '%s',\n" + " .attr2: '%s',\n" + " .constant: %g,\n" + " .multiplier: %g,\n" + " .strength: %g\n" + "}\n", + c->view1, c->attr1, + c->relation, + c->view2 != NULL ? c->view2 : "none", c->attr2 != NULL ? c->attr2 : "none", + c->constant, + c->multiplier, + c->strength); + } +#endif + + return (GtkConstraintVfl *) g_array_free (constraints, FALSE); +} + +int +gtk_constraint_vfl_parser_get_error_offset (GtkConstraintVflParser *parser) +{ + return parser->error_offset; +} + +int +gtk_constraint_vfl_parser_get_error_range (GtkConstraintVflParser *parser) +{ + return parser->error_range; +} diff --git a/gtk/gtkconstraintvflparserprivate.h b/gtk/gtkconstraintvflparserprivate.h new file mode 100644 index 0000000000..a89b474893 --- /dev/null +++ b/gtk/gtkconstraintvflparserprivate.h @@ -0,0 +1,89 @@ +/* gtkconstraintvflparserprivate.h: VFL constraint definition parser + * + * Copyright 2017 Endless + * Copyright 2019 GNOME Foundation + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * This library 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.1 of the License, or (at your option) any later version. + * + * This library 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 this library; if not, see . + */ + +#pragma once + +#include "gtkconstrainttypesprivate.h" + +G_BEGIN_DECLS + +#define GTK_CONSTRAINT_VFL_PARSER_ERROR (gtk_constraint_vfl_parser_error_quark ()) + +typedef enum { + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_SYMBOL, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_ATTRIBUTE, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_VIEW, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_METRIC, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_PRIORITY, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_RELATION +} VflError; + +typedef struct _GtkConstraintVflParser GtkConstraintVflParser; + +typedef struct { + const char *view1; + const char *attr1; + GtkConstraintRelation relation; + const char *view2; + const char *attr2; + double constant; + double multiplier; + double strength; +} GtkConstraintVfl; + +GQuark gtk_constraint_vfl_parser_error_quark (void); + +GtkConstraintVflParser * +gtk_constraint_vfl_parser_new (void); + +void +gtk_constraint_vfl_parser_free (GtkConstraintVflParser *parser); + +void +gtk_constraint_vfl_parser_set_default_spacing (GtkConstraintVflParser *parser, + int hspacing, + int vspacing); + +void +gtk_constraint_vfl_parser_set_metrics (GtkConstraintVflParser *parser, + GHashTable *metrics); + +void +gtk_constraint_vfl_parser_set_views (GtkConstraintVflParser *parser, + GHashTable *views); + +gboolean +gtk_constraint_vfl_parser_parse_line (GtkConstraintVflParser *parser, + const char *line, + gssize len, + GError **error); + +int +gtk_constraint_vfl_parser_get_error_offset (GtkConstraintVflParser *parser); + +int +gtk_constraint_vfl_parser_get_error_range (GtkConstraintVflParser *parser); + +GtkConstraintVfl * +gtk_constraint_vfl_parser_get_constraints (GtkConstraintVflParser *parser, + int *n_constraints); + +G_END_DECLS diff --git a/gtk/meson.build b/gtk/meson.build index a79e3e7ecc..0b60ce22e4 100644 --- a/gtk/meson.build +++ b/gtk/meson.build @@ -39,6 +39,7 @@ gtk_private_sources = files([ 'gtkcolorswatch.c', 'gtkconstraintexpression.c', 'gtkconstraintsolver.c', + 'gtkconstraintvflparser.c', 'gtkcssanimatedstyle.c', 'gtkcssanimation.c', 'gtkcssarrayvalue.c',