From 4ac78e22415387174ba56651f91f66e188bcfb30 Mon Sep 17 00:00:00 2001 From: Cecil Date: Tue, 23 Oct 2018 19:36:49 -0600 Subject: [PATCH] for #410, add emeus-vfl-parser and shoes-vfl-parser * independent of solver except for using standard terminalogy which Shoes doesn't provide. Like 'super' where we have 'canvas' Clever person could figure out how to get vfl constraints mapped to cassowary-ruby gem. --- Rakefile | 3 +- Tests/layout/cs.rb | 4 +- Tests/layout/test-unit.out | 54 + Tests/layout/vfl1.rb | 16 +- Tests/layout/vfl2.rb | 66 ++ Tests/layout/vfl3.rb | 32 + make/subsys.rb | 21 +- shoes/layout/emeus-vfl-parser-private.h | 60 ++ shoes/layout/emeus-vfl-parser.c | 1192 +++++++++++++++++++++++ shoes/layout/layouts.h | 7 + shoes/layout/shoes-vfl-parser.c | 250 +++++ shoes/layout/test-vfl-parser.sav | 192 ++++ shoes/types/layout.c | 101 +- shoes/types/layout.h | 17 +- 14 files changed, 1971 insertions(+), 44 deletions(-) create mode 100644 Tests/layout/test-unit.out create mode 100644 Tests/layout/vfl2.rb create mode 100644 Tests/layout/vfl3.rb create mode 100644 shoes/layout/emeus-vfl-parser-private.h create mode 100644 shoes/layout/emeus-vfl-parser.c create mode 100644 shoes/layout/layouts.h create mode 100644 shoes/layout/shoes-vfl-parser.c create mode 100644 shoes/layout/test-vfl-parser.sav diff --git a/Rakefile b/Rakefile index c0a2f937..79bc62e2 100644 --- a/Rakefile +++ b/Rakefile @@ -417,7 +417,8 @@ end #These tasks create object files: SubDirs = ["#{rtp}/zzbase.done", "#{rtp}/http/zzdownload.done", "#{rtp}/plot/zzplot.done", "#{rtp}/console/zzconsole.done", - "#{rtp}/types/zzwidgets.done", "#{rtp}/native/zznative.done"] + "#{rtp}/types/zzwidgets.done", "#{rtp}/layout/zzlayout.done", + "#{rtp}/native/zznative.done"] # Windows doesn't use console - don't try to build it. Delete from dependcies case TGT_DIR diff --git a/Tests/layout/cs.rb b/Tests/layout/cs.rb index a4f620eb..f47974a9 100644 --- a/Tests/layout/cs.rb +++ b/Tests/layout/cs.rb @@ -33,7 +33,7 @@ def initialize(identifier) # Button1 starts 50 from the left margin. solver.add_constraint(b1.left.cn_equal left_limit + 50) -# Button2 ends 50 from the right margin (???) +# Button2 ends 50 from the right margin solver.add_constraint((left_limit + right_limit).cn_equal b2.left + b2.width + 50) # Button2 starts at least 100 from the end of Button1. This is the @@ -46,7 +46,7 @@ def initialize(identifier) # Button1's preferred width is 87 solver.add_constraint(b1.width.cn_equal 87, Strength::StrongStrength) - +puts "b1: #{b1.inspect}" # Button2's minimum width is 113 solver.add_constraint(b2.width.cn_geq 113) diff --git a/Tests/layout/test-unit.out b/Tests/layout/test-unit.out new file mode 100644 index 00000000..6ebbbc85 --- /dev/null +++ b/Tests/layout/test-unit.out @@ -0,0 +1,54 @@ +Vfl internal layout setup called +shoes_layout_internal_add called +shoes_layout_internal_add called +shoes_layout_internal_add called +shoes_layout_internal_size called pass: 0 +shoes_layout_internal_size called pass: 1 +In shoes_vfl_test +Parsing [invalid]: 'V|[backgroundBox]|' +Error: Expected ':' after vertical orientation +Parsing [invalid]: '[backgroundBox)' +Error: A predicate must follow a view name +Parsing [invalid]: '[backgroundBox(]' +Error: A predicate on a view must end with ')' +Parsing [invalid]: '[view]' +Error: Unable to find view with name 'view' +Parsing [invalid]: '[view]-' +Error: Unterminated spacing +Parsing [invalid]: '-[view]' +Error: Spacing cannot be set without a view +Parsing [invalid]: '[[' +Error: View identifiers must be valid C identifiers +Parsing [invalid]: '[9ab]' +Error: View identifiers must be valid C identifiers +Parsing [invalid]: '[-a]' +Error: View identifiers must be valid C identifiers +Parsing [invalid]: '[view(>30)]' +Error: Unknown relation; must be one of '==', '>=', or '<=' +Parsing [invalid]: '[view(>=30@foo)]' +Error: Priority must be a positive number or one of 'weak', 'medium', 'strong', and 'required' +Parsing [invalid]: '[view(view + wrong)]' +Error: Expected positive number as a constant +Parsing [invalid]: '[view(view.wrong)]' +Error: Attribute must be on one of 'width', 'height', 'centerX', 'centerY', 'top', 'bottom', 'left', 'right', 'start', 'end', 'baseline' +Parsing [valid]: '[button]-[textField]' +Parsing [valid]: '[button(>=50)]' +Parsing [valid]: '|-50-[purpleBox]-50-|' +Parsing [valid]: '|-[view]' +Parsing [valid]: '[view]|' +Parsing [valid]: 'V:[topField]-10-[bottomField]' +Parsing [valid]: '[maroonView][blueView]' +Parsing [valid]: '[button(100@strong)]' +Parsing [valid]: '[button1(==button2)]' +Parsing [valid]: '[flexibleButton(>=70,<=100)]' +Parsing [valid]: '|-[find]-[findNext]-[findField(>=20)]-|' +Parsing [valid]: 'H:|-8-[view1(==view2)]-12-[view2]-8-|' +Parsing [valid]: 'H:|-8-[view3]-8-|' +Parsing [valid]: 'V:|-8-[view1]-12-[view3(==view1,view2)]-8-|' +Parsing [valid]: '|-(>=0)-[view]-(>=0)-|' +Parsing [valid]: '[view(==0@500)]' +Parsing [valid]: '[view1]-(==0@500)-[view2]' +Parsing [valid]: '[view1(view2 * 2.0 + 20)]' +Parsing [valid]: '|-(metric1/2-20.0)-' +Parsing [valid]: '[view1(view1.height)]' +shoes_layout_internal_finish called diff --git a/Tests/layout/vfl1.rb b/Tests/layout/vfl1.rb index 05f8b1be..5e7ed471 100644 --- a/Tests/layout/vfl1.rb +++ b/Tests/layout/vfl1.rb @@ -2,13 +2,19 @@ Shoes.app width: 350, height: 400, resizeable: true do stack do para "Before layout" - @lay = layout use: :foobar, width: 300, height: 300 do - para "First Para", name: 'Label_1' - button "one", name: 'Button_1' - button "two", name: 'Button_2' + @lay = layout use: :Vfl, width: 300, height: 300 do + para "OverConstrained", name: 'Label1' + button "one", name: 'Button1' + button "two", name: 'Button2' end @lay.start { - @lay.rules 'foo.vfl' + metrics = {} + lines = [ + "H:|-8-[Button1(==Button2)]-12-[Button2]-8-|", + "H:|-8-[Label1]-8-|", + "V:|-8-[Button1,Button2]-12-[Label1(==Button1,Button2)]-8-|" + ] + @lay.rules lines: lines, metrics: metrics @lay.finish } end diff --git a/Tests/layout/vfl2.rb b/Tests/layout/vfl2.rb new file mode 100644 index 00000000..48224ca4 --- /dev/null +++ b/Tests/layout/vfl2.rb @@ -0,0 +1,66 @@ +class Sample + attr_accessor :widgets, :canvas + + def initialize() + @widgets = {} + end + + def setup(canvas, attr) + @canvas = canvas + end + + def add(canvas, widget, attrs) + name = attrs && attrs[:name] + @widgets[name] = widget + end + + def contents + return @widgets + end + + def remove(canvas, widget, pos) + return true + end + + def size(canvas, pass) + end + + def clear() + end + + def finish() + end +end + +Shoes.app width: 350, height: 400, resizeable: true do + stack do + para "Test vfl parser" + @cls = Sample.new() + @lay = layout use: @cls, width: 300, height: 300 do + para "OverConstrained", name: 'para1' + edit_line "one", name: 'el1' + button "two", name: 'but1' + button "three", name: "but2" + button "four", name: "but3" + end + @lay.start { + metrics = { + el1: 80.7, # what does this mean? + } + lines = [ + "H:|-[para1(but1)]-[but1]-|", + "H:|-[el1(but2)]-[but2]-|", + "H:[but3(but2)]-|", + "V:|-[para1(el1)]-[el1]-|", + "V:|-[but1(but2,but3)]-[but2]-[but3]-|" + ] + if @lay.vfl_parse lines: lines, views: @cls.contents, metrics: metrics + constraints = @lay.vfl_constraints + # display only! + constraints.each { |c| $stderr.puts c.inspect } + @lay.finish constraints + end + } + end + para "After layout" +end diff --git a/Tests/layout/vfl3.rb b/Tests/layout/vfl3.rb new file mode 100644 index 00000000..39a04eb3 --- /dev/null +++ b/Tests/layout/vfl3.rb @@ -0,0 +1,32 @@ + +Shoes.app width: 350, height: 400, resizeable: true do + stack do + para "Vfl layout" + @lay = layout use: :Vfl, width: 300, height: 300 do + para "OverConstrained", name: 'para1' + edit_line "one", name: 'el1' + button "two", name: 'but1' + button "three", name: "but2" + button "four", name: "but3" + end + @lay.start { + metrics = { + 'el1' => 80 # what does this mean or do or should be? + } + lines = [ + "H:|-[para1(but1)]-[but1]-|", + "H:|-[el1(but2)]-[but2]-|", + "H:[but3(but2)]-|", + "V:|-[para1(el1)]-[el1]-|", + "V:|-[but1(but2,but3)]-[but2]-[but3]-|" + ] + if @lay.vfl_parse lines: lines, metrics: metrics + constraints = @lay.vfl_constraints + # display purposes only? + constraints.each { |c| $stderr.puts c.inspect } + @lay.finish constraints + end + } + end + para "After layout" +end diff --git a/make/subsys.rb b/make/subsys.rb index a988e6b7..e11d2fd3 100644 --- a/make/subsys.rb +++ b/make/subsys.rb @@ -119,6 +119,23 @@ touch "#{tp}/plot/zzplot.done" end +# Layout +mkdir_p "#{tp}/layout", verbose: false +lay_src = FileList['shoes/layout/*.c'] +lay_hdr = FileList['shoes/layout/*.h'] +lay_obj = [] +lay_src.each do |c| + fnm = File.basename(c, ".*") + o = "#{tp}/layout/#{fnm}.o" + lay_obj << o + file o => [c] + lay_hdr do + sh "#{CC} -o #{o} -I. -c #{LINUX_CFLAGS} #{c}" + end +end +file "#{tp}/layout/zzlayout.done" => lay_obj do + touch "#{tp}/layout/zzlayout.done" +end + # Console mkdir_p "#{tp}/console", verbose: false #if RUBY_PLATFORM =~ /darwin/ @@ -152,7 +169,7 @@ end end -# Too keep the main Rakefile almost legible, create some file tasks here for detecting +# To keep the main Rakefile almost legible, create some file tasks here for detecting # updates to static/manual-en.txt and lib/shoes.rb, lib/shoes/*.rb after shoes is 'setup' # but BEFORE new_so/new_link is called - an OSX requirement # Also handle sample/*/*.rb changes @@ -171,7 +188,7 @@ touch "#{tp}/copyonly/zzshoesrb.done" end -# update lib/shoes/*.rb if changed. And ssl certs file. +# update lib/shoes/*.rb if changed. And ssl certs file. if CROSS && TGT_DIR != 'minlin' && TGT_DIR != 'minbsd' && (! TGT_DIR[/minosx/]) shoesrblib = FileList["lib/shoes/*.rb"] shoesrblib << "lib/shoes/cacert.pem" diff --git a/shoes/layout/emeus-vfl-parser-private.h b/shoes/layout/emeus-vfl-parser-private.h new file mode 100644 index 00000000..6869b178 --- /dev/null +++ b/shoes/layout/emeus-vfl-parser-private.h @@ -0,0 +1,60 @@ +#pragma once + +#include +#include "emeus-types-private.h" + +G_BEGIN_DECLS + +#define VFL_ERROR (vfl_error_quark ()) + +typedef enum { + VFL_ERROR_INVALID_SYMBOL, + VFL_ERROR_INVALID_ATTRIBUTE, + VFL_ERROR_INVALID_VIEW, + VFL_ERROR_INVALID_METRIC, + VFL_ERROR_INVALID_PRIORITY, + VFL_ERROR_INVALID_RELATION +} VflError; + +typedef struct _VflParser VflParser; + +typedef struct { + const char *view1; + const char *attr1; + OperatorType relation; + const char *view2; + const char *attr2; + double constant; + double multiplier; + double strength; +} VflConstraint; + +GQuark vfl_error_quark (void); + +VflParser *vfl_parser_new (int hspacing, + int vspacing, + GHashTable *metrics, + GHashTable *views); +void vfl_parser_free (VflParser *parser); + +void vfl_parser_set_default_spacing (VflParser *parser, + int hspacing, + int vspacing); + +void vfl_parser_set_metrics (VflParser *parser, + GHashTable *metrics); +void vfl_parser_set_views (VflParser *parser, + GHashTable *views); + +bool vfl_parser_parse_line (VflParser *parser, + const char *line, + gssize len, + GError **error); + +int vfl_parser_get_error_offset (VflParser *parser); +int vfl_parser_get_error_range (VflParser *parser); + +VflConstraint *vfl_parser_get_constraints (VflParser *parser, + int *n_constraints); + +G_END_DECLS diff --git a/shoes/layout/emeus-vfl-parser.c b/shoes/layout/emeus-vfl-parser.c new file mode 100644 index 00000000..b5011827 --- /dev/null +++ b/shoes/layout/emeus-vfl-parser.c @@ -0,0 +1,1192 @@ +// CJC: slightly modified from emeus/src.emeus-vfl-parser.c +#include "config.h" + +#include "emeus-vfl-parser-private.h" + +#include +#include +#include +typedef enum { + VFL_HORIZONTAL, + VFL_VERTICAL +} VflOrientation; + +typedef struct { + OperatorType relation; + + double constant; + double multiplier; + const char *subject; + char *object; + const char *attr; + + double priority; +} VflPredicate; + +typedef struct { + double size; + bool is_set; + bool is_default; + bool is_predicate; + 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 _VflParser +{ + 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 +vfl_error_quark (void) +{ + return g_quark_from_static_string ("vfl-error-quark"); +} + +VflParser * +vfl_parser_new (int hspacing, + int vspacing, + GHashTable *metrics, + GHashTable *views) +{ + VflParser *res = g_slice_new0 (VflParser); + + res->default_spacing[VFL_HORIZONTAL] = hspacing < 0 ? 8 : hspacing; + res->default_spacing[VFL_VERTICAL] = vspacing < 0 ? 8 : vspacing; + + res->orientation = VFL_HORIZONTAL; + + res->metrics_set = metrics; + res->views_set = views; + + return res; +} + +void +vfl_parser_set_default_spacing (VflParser *parser, + int hspacing, + int vspacing) +{ + parser->default_spacing[VFL_HORIZONTAL] = hspacing; + parser->default_spacing[VFL_VERTICAL] = vspacing; +} + +void +vfl_parser_set_metrics (VflParser *parser, + GHashTable *metrics) +{ + parser->metrics_set = metrics; +} + +void +vfl_parser_set_views (VflParser *parser, + GHashTable *views) +{ + parser->views_set = views; +} + +static int +get_default_spacing (VflParser *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 bool +parse_relation (const char *str, + OperatorType *relation, + char **endptr, + GError **error) +{ + const char *cur = str; + + if (*cur == '=') + { + cur += 1; + + if (*cur == '=') + { + *relation = OPERATOR_TYPE_EQ; + *endptr = (char *) cur + 1; + return true; + } + + goto out; + } + else if (*cur == '>') + { + cur += 1; + + if (*cur == '=') + { + *relation = OPERATOR_TYPE_GE; + *endptr = (char *) cur + 1; + return true; + } + + goto out; + } + else if (*cur == '<') + { + cur += 1; + + if (*cur == '=') + { + *relation = OPERATOR_TYPE_LE; + *endptr = (char *) cur + 1; + return true; + } + + goto out; + } + +out: + g_set_error (error, VFL_ERROR, VFL_ERROR_INVALID_RELATION, + "Unknown relation; must be one of '==', '>=', or '<='"); + return false; +} + +static bool +has_metric (VflParser *parser, + const char *name) +{ + if (parser->metrics_set == NULL) + return false; + + return g_hash_table_contains (parser->metrics_set, name); +} + +static bool +has_view (VflParser *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 bool +parse_predicate (VflParser *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 == '<') + { + OperatorType 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 = OPERATOR_TYPE_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, VFL_ERROR, VFL_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, VFL_ERROR, VFL_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, VFL_ERROR, VFL_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, VFL_ERROR, VFL_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, VFL_ERROR, VFL_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, VFL_ERROR, VFL_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 = STRENGTH_WEAK; + end += 4; + } + else if (strncmp (end, "medium", 6) == 0) + { + priority = STRENGTH_MEDIUM; + end += 6; + } + else if (strncmp (end, "strong", 6) == 0) + { + priority = STRENGTH_STRONG; + end += 6; + } + else if (strncmp (end, "required", 8) == 0) + { + priority = 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, VFL_ERROR, VFL_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 = STRENGTH_REQUIRED; + + if (endptr != NULL) + *endptr = (char *) end; + + return true; +} + +static bool +parse_view (VflParser *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, VFL_ERROR, VFL_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, VFL_ERROR, VFL_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, VFL_ERROR, VFL_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, VFL_ERROR, VFL_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, VFL_ERROR, VFL_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 EMEUS_ENABLE_DEBUG + g_debug ("*** Found predicate: %s.%s %s %g %s\n", + cur_predicate.object != NULL ? cur_predicate.object : view->name, + cur_predicate.attr, + cur_predicate.relation == OPERATOR_TYPE_EQ ? "==" : + cur_predicate.relation == OPERATOR_TYPE_LE ? "<=" : + cur_predicate.relation == OPERATOR_TYPE_GE ? ">=" : + "unknown relation", + cur_predicate.constant, + cur_predicate.priority == STRENGTH_WEAK ? "weak" : + cur_predicate.priority == STRENGTH_MEDIUM ? "medium" : + cur_predicate.priority == STRENGTH_STRONG ? "strong" : + cur_predicate.priority == STRENGTH_REQUIRED ? "required" : + "unknown strength"); +#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, VFL_ERROR, VFL_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, VFL_ERROR, VFL_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 +vfl_parser_clear (VflParser *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 +vfl_parser_free (VflParser *parser) +{ + if (parser == NULL) + return; + + vfl_parser_clear (parser); + + g_slice_free (VflParser, parser); +} + +bool +vfl_parser_parse_line (VflParser *parser, + const char *buffer, + gssize len, + GError **error) +{ + 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, VFL_ERROR, VFL_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, VFL_ERROR, VFL_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, VFL_ERROR, VFL_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, VFL_ERROR, VFL_ERROR_INVALID_SYMBOL, + "Unterminated spacing"); + return false; + } + + if (parser->current_view == NULL) + { + parser->error_offset = cur - parser->cursor; + g_set_error (error, VFL_ERROR, VFL_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->is_set = true; + spacing->is_default = true; + spacing->is_predicate = false; + 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->is_set = true; + spacing->is_default = false; + spacing->is_predicate = true; + 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, VFL_ERROR, VFL_ERROR_INVALID_SYMBOL, + "Expected ')' at the end of a predicate, not '%c'", *tmp); + return false; + } + + cur = tmp + 1; + if (*cur != '-') + { + g_set_error (error, VFL_ERROR, VFL_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->is_set = true; + spacing->is_default = false; + spacing->is_predicate = false; + spacing->size = g_ascii_strtod (cur, &tmp); + + if (tmp == cur) + { + parser->error_offset = cur - parser->cursor; + g_set_error (error, VFL_ERROR, VFL_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, VFL_ERROR, VFL_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, VFL_ERROR, VFL_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; +} + +VflConstraint * +vfl_parser_get_constraints (VflParser *parser, + int *n_constraints) +{ + GArray *constraints; + VflView *iter; + + constraints = g_array_new (FALSE, FALSE, sizeof (VflConstraint)); + + iter = parser->views; + while (iter != NULL) + { + VflConstraint 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.is_set) + { + 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.is_predicate) + { + const VflPredicate *p = &(iter->spacing.predicate); + + c.constant = p->constant * -1.0; + c.relation = p->relation; + c.strength = p->priority; + } + else if (iter->spacing.is_default) + { + c.constant = get_default_spacing (parser) * -1.0; + c.relation = OPERATOR_TYPE_EQ; + c.strength = STRENGTH_REQUIRED; + } + else + { + c.constant = iter->spacing.size * -1.0; + c.relation = OPERATOR_TYPE_EQ; + c.strength = 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 = OPERATOR_TYPE_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 = STRENGTH_REQUIRED; + + g_array_append_val (constraints, c); + } + + iter = iter->next_view; + } + + if (n_constraints != NULL) + *n_constraints = constraints->len; + +//#ifdef EMEUS_ENABLE_DEBUG +#if 0 + for (int i = 0; i < constraints->len; i++) + { + const VflConstraint *c = &g_array_index (constraints, VflConstraint, i); + + fprintf(stderr,"{\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 (VflConstraint *) g_array_free (constraints, FALSE); +} + +int +vfl_parser_get_error_offset (VflParser *parser) +{ + return parser->error_offset; +} + +int +vfl_parser_get_error_range (VflParser *parser) +{ + return parser->error_range; +} diff --git a/shoes/layout/layouts.h b/shoes/layout/layouts.h new file mode 100644 index 00000000..d7abd69e --- /dev/null +++ b/shoes/layout/layouts.h @@ -0,0 +1,7 @@ +#ifndef SHOES_LAYOUTS_H +#define SHOES_LAYOUTS_H + +#include "shoes/layout/emeus-vfl-parser-private.h" +VALUE shoes_vfl_rules(shoes_layout *lay, shoes_canvas *canvas, VALUE args); +void shoes_vfl_add_ele(shoes_canvas *canvas, VALUE ele); +#endif diff --git a/shoes/layout/shoes-vfl-parser.c b/shoes/layout/shoes-vfl-parser.c new file mode 100644 index 00000000..ffe58b90 --- /dev/null +++ b/shoes/layout/shoes-vfl-parser.c @@ -0,0 +1,250 @@ +#include "shoes/app.h" +#include "shoes/canvas.h" +#include "shoes/ruby.h" +#include "shoes/internal.h" +#include "shoes/world.h" +#include "shoes/native/native.h" +#include "shoes/version.h" +#include "shoes/types/types.h" +#include "shoes/types/settings.h" +#include "shoes/types/layout.h" +#include "shoes/layout/layouts.h" + +// Test data from emeus/examples/simple-grid.c + + +/* Layout: + * + * +-----------------------------+ + * | +-----------+ +-----------+ | + * | | Child 1 | | Child 2 | | + * | +-----------+ +-----------+ | + * | +-------------------------+ | + * | | Child 3 | | + * | +-------------------------+ | + * +-----------------------------+ + * + * Visual format: + * + * H:|-8-[view1(==view2)]-12-[view2]-8-| + * H:|-8-[view3]-8-| + * V:|-8-[view1,view2]-12-[view3(==view1,view2)]-8-| + */ + char *lines[3] = { + "H:|-8-[view1(==view2)]-12-[view2]-8-|", + "H:|-8-[view3]-8-|", + "V:|-8-[view1,view2]-12-[view3(==view1,view2)]-8-|" + }; + /* The third line fails to parse: A predicate must follow a view name + * + * vfl calls these 'views'. In shoes this would be the list of all the + * widget :names in a the layout. + */ + char *names[3] = { "view1", "view2", "view3" }; + + /* Constraints: + * + * super.start = child1.start - 8 + * child1.width = child2.width + * child1.end = child2.start - 12 + * child2.end = super.end - 8 + * super.start = child3.start - 8 + * child3.end = super.end - 8 + * super.top = child1.top - 8 + * super.top = child2.top - 8 + * child1.bottom = child3.top - 12 + * child2.bottom = child3.top - 12 + * child3.height = child1.height + * child3.height = child2.height + * child3.bottom = super.bottom - 8 + * + */ + + +void print_constraint(VflConstraint *c) { + fprintf(stderr,"{\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); +} + +extern ID s_name; + +static VALUE wrap_constraint(VflConstraint *c) { + VALUE hsh = rb_hash_new(); + VALUE val, key; + key = ID2SYM(rb_intern("view1")); + val = rb_str_new2(c->view1); + rb_hash_aset(hsh, key, val); + + key = ID2SYM(rb_intern("attr1")); + val = rb_str_new2(c->attr1); + rb_hash_aset(hsh, key, val); + + key = ID2SYM(rb_intern("relation")); + if (c->relation == OPERATOR_TYPE_LE) + val = rb_str_new2("LE"); + else if (c->relation == OPERATOR_TYPE_GE) + val = rb_str_new2("GE"); + else + val = rb_str_new2("EQ"); + + rb_hash_aset(hsh, key, val); + + key = ID2SYM(rb_intern("view2")); + if (c->view2 != NULL) + val = rb_str_new2(c->view2); + else + val = Qnil; + rb_hash_aset(hsh, key, val); + + key = ID2SYM(rb_intern("attr2")); + if (c->view2 != NULL) + val = rb_str_new2(c->attr2); + else + val = Qnil; + rb_hash_aset(hsh, key, val); + + key = ID2SYM(rb_intern("constant")); + val = DBL2NUM(c->constant); + rb_hash_aset(hsh, key, val); + + key = ID2SYM(rb_intern("multiplier")); + val = DBL2NUM(c->multiplier); + rb_hash_aset(hsh, key, val); + + key = ID2SYM(rb_intern("strength")); + val = DBL2NUM(c->strength); + rb_hash_aset(hsh, key, val); + + return hsh; + +} + +void shoes_vfl_add_ele(shoes_canvas *canvas, VALUE ele) { + shoes_layout *lay; + Data_Get_Struct(canvas->layout_mgr, shoes_layout, lay); + shoes_abstract *widget; + Data_Get_Struct(ele, shoes_abstract, widget); + VALUE name = ATTR(widget->attr, name); + char *str = RSTRING_PTR(name); + rb_hash_aset(lay->views, name, ele); +} + +// args is a verified Ruby hash. +VALUE shoes_vfl_rules(shoes_layout *lay, shoes_canvas *canvas, VALUE args) { + GError *error = NULL; + int hspacing = 10; + int vspacing = 10; + GHashTable *views, *metrics; + VflConstraint *constraints; + int n_constraints; + + //fprintf(stderr, "in vfl parse test\n"); + // create a parser + VflParser *parser = vfl_parser_new (-1, -1, NULL, NULL); + + // get view names (shoes ele's) + VALUE keys; + ID s_views = rb_intern("views"); + VALUE hashv = rb_hash_aref(args, ID2SYM(s_views)); + if (! NIL_P(hashv)) { + if (TYPE(hashv) == T_HASH) { + keys = rb_funcall(hashv, rb_intern("keys"), 0); + lay->views = hashv; + } else + rb_raise(rb_eArgError, "views: is not a hash"); + } else { + // unspecifed, try the one in the Layout obj - should be nil + keys = rb_funcall(lay->views, rb_intern("keys"), 0); + } + views = g_hash_table_new (g_str_hash, g_str_equal); + for (int i = 0; i < RARRAY_LEN(keys); i++) { + VALUE ent = rb_ary_entry(keys, i); + char *str = strdup(RSTRING_PTR(ent)); + //printf("view |%s|\n", str); + g_hash_table_add (views, str); + } + vfl_parser_set_views (parser, views); + + // Move any metrics from Ruby to glib hash 'key' -> double + ID s_metrics = rb_intern("metrics"); + hashv = rb_hash_aref(args, ID2SYM(s_metrics)); + if (! NIL_P(hashv)) { + if (TYPE(hashv) == T_HASH) { + keys = rb_funcall(hashv, rb_intern("keys"), 0); + lay->metrics = hashv; + } + else + rb_raise(rb_eArgError, "views: is not a hash"); + } else { + // unspecifed, try the one in the Layout obj - should be nil + keys = rb_funcall(lay->metrics, rb_intern("keys"), 0); + } + metrics = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, g_free); + for (int i = 0; i < RARRAY_LEN(keys); i++) { + const char *str; + ID metk; + VALUE metv; + double *v = g_new(double, 1); + + VALUE ent = rb_ary_entry(keys, i); + if (TYPE(ent) == T_SYMBOL) + str = rb_id2name(SYM2ID(ent)); + else if (TYPE(ent) == T_STRING) + str = RSTRING_PTR(ent); + else + rb_raise(rb_eArgError, "vfl metric key not string or symbol"); + metv = rb_hash_aref(lay->metrics, ent); + if (NIL_P(metv)) { + rb_raise(rb_eArgError, "vfl metric should not be nil for %s", str); + } else { + *v = NUM2DBL(metv); + //fprintf(stderr,"metrics key: %s => %g\n", str, *v); + } + g_hash_table_insert(metrics, (char *)str, v); + } + vfl_parser_set_metrics (parser, metrics); + + // get lines, check for array + ID s_lines = rb_intern("lines"); + VALUE lnary = rb_hash_aref(args, ID2SYM(s_lines)); + if (TYPE(lnary) != T_ARRAY) + rb_raise(rb_eArgError, "missing lines: array?"); + + // feed lines to parser + lay->constraints = rb_ary_new(); + for (int i = 0; i < RARRAY_LEN(lnary); i++) { + VALUE ln = rb_ary_entry(lnary, i); + char *line = RSTRING_PTR(ln); + //fprintf(stderr, "parse: %s\n", line); + vfl_parser_parse_line (parser, line, -1, &error); + if (error != NULL) { + char err[100]; + rb_raise(rb_eArgError, "vfl err: %s", error->message); + } else { + // get constraints out + constraints = vfl_parser_get_constraints (parser, &n_constraints); + //fprintf(stderr, "n_constraints: %d\n", n_constraints); + for (int i = 0; i < n_constraints; i++) { + VflConstraint *c = &constraints[i]; + rb_ary_push(lay->constraints, wrap_constraint(c)); + } + } + } + return Qtrue; +} diff --git a/shoes/layout/test-vfl-parser.sav b/shoes/layout/test-vfl-parser.sav new file mode 100644 index 00000000..9464d278 --- /dev/null +++ b/shoes/layout/test-vfl-parser.sav @@ -0,0 +1,192 @@ +#include "emeus-test-utils.h" + +#include "emeus-expression-private.h" +#include "emeus-simplex-solver-private.h" +#include "emeus-types-private.h" +#include "emeus-vfl-parser-private.h" + +#include "shoes/app.h" +#include "shoes/canvas.h" +#include "shoes/ruby.h" +#include "shoes/internal.h" +#include "shoes/world.h" +#include "shoes/native/native.h" +#include "shoes/version.h" +#include "shoes/types/types.h" +#include "shoes/types/settings.h" +#include "shoes/types/layout.h" + + +static struct { + const char *id; + const char *expression; + const char *views[5]; + const char *metrics[5]; +} vfl_valid[] = { + { "standard-space", "[button]-[textField]", { "button", "textField", NULL, }, { NULL, } }, + { "width-constraint", "[button(>=50)]", { "button", NULL, }, { NULL } }, + { "connection-superview", "|-50-[purpleBox]-50-|", { "purpleBox", NULL, }, { NULL, } }, + { "leading-superview", "|-[view]", { "view", NULL, }, { NULL, } }, + { "trailing-superview", "[view]|", { "view", NULL, }, { NULL, } }, + { "vertical-layout", "V:[topField]-10-[bottomField]", { "topField", "bottomField", NULL, }, { NULL, } }, + { "flush-views", "[maroonView][blueView]", { "maroonView", "blueView", NULL, }, { NULL, } }, + { "priority", "[button(100@strong)]", { "button", NULL, }, { NULL, } }, + { "equal-widths", "[button1(==button2)]", { "button1", "button2", NULL, }, { NULL, } }, + { "multiple-predicates", "[flexibleButton(>=70,<=100)]", { "flexibleButton", NULL, }, { NULL, } }, + { "complete-line", "|-[find]-[findNext]-[findField(>=20)]-|", { "find", "findNext", "findField", NULL, }, { NULL, } }, + { "grid-1", "H:|-8-[view1(==view2)]-12-[view2]-8-|", { "view1", "view2", NULL, }, { NULL, } }, + { "grid-2", "H:|-8-[view3]-8-|", { "view3", NULL, }, { NULL, } }, + { "grid-3", "V:|-8-[view1]-12-[view3(==view1,view2)]-8-|", { "view1", "view2", "view3", NULL, }, { NULL, } }, + { "predicate-spacing-1", "|-(>=0)-[view]-(>=0)-|", { "view", NULL, }, { NULL, } }, + { "predicate-numeric-priority", "[view(==0@500)]", { "view", NULL, }, { NULL, }, }, + { "predicate-spacing-priority", "[view1]-(==0@500)-[view2]", { "view1", "view2", NULL, }, { NULL, }, }, + { "predicate-view-operator", "[view1(view2 * 2.0 + 20)]", { "view1", "view2", NULL, }, { NULL, }, }, + { "predicate-metric-operator", "|-(metric1/2-20.0)-", { NULL, }, { "metric1", NULL, }, }, + { "predicate-attribute", "[view1(view1.height)]", { "view1", NULL, }, { NULL, }, }, +}; + +static struct { + const char *id; + const char *expression; + const char *views[5]; + VflError error_id; +} vfl_invalid[] = { + { "orientation-invalid", "V|[backgroundBox]|", { "backgroundBox", NULL, }, VFL_ERROR_INVALID_SYMBOL, }, + { "missing-view-terminator", "[backgroundBox)", { "backgroundBox", NULL, }, VFL_ERROR_INVALID_SYMBOL, }, + { "invalid-predicate", "[backgroundBox(]", { "backgroundBox", NULL, }, VFL_ERROR_INVALID_SYMBOL, }, + { "view-not-found", "[view]", { NULL, }, VFL_ERROR_INVALID_VIEW, }, + { "trailing-spacing", "[view]-", { "view", NULL, }, VFL_ERROR_INVALID_SYMBOL, }, + { "leading-spacing", "-[view]", { "view", NULL, }, VFL_ERROR_INVALID_SYMBOL, }, + { "view-invalid-identifier-1", "[[", { NULL, }, VFL_ERROR_INVALID_VIEW, }, + { "view-invalid-identifier-2", "[9ab]", { NULL, }, VFL_ERROR_INVALID_VIEW, }, + { "view-invalid-identifier-3", "[-a]", { NULL, }, VFL_ERROR_INVALID_VIEW, }, + { "predicate-wrong-relation", "[view(>30)]", { "view", NULL, }, VFL_ERROR_INVALID_RELATION, }, + { "predicate-wrong-priority", "[view(>=30@foo)]", { "view", NULL, }, VFL_ERROR_INVALID_PRIORITY, }, + { "predicate-wrong-operator", "[view(view + wrong)]", { "view", NULL, }, VFL_ERROR_INVALID_SYMBOL, }, + { "predicate-wrong-attribute", "[view(view.wrong)]", { "view", NULL, }, VFL_ERROR_INVALID_ATTRIBUTE, }, +}; + + +static void +vfl_parser_valid (gconstpointer data) +{ + int idx = GPOINTER_TO_INT (data); + VflParser *parser = vfl_parser_new (-1, -1, NULL, NULL); + GError *error = NULL; + VflConstraint *constraints; + int n_constraints = 0; + GHashTable *views, *metrics; + + fprintf(stderr, "Parsing [valid]: '%s'\n", vfl_valid[idx].expression); + + views = g_hash_table_new (g_str_hash, g_str_equal); + for (int i = 0; vfl_valid[idx].views[i] != NULL; i++) + g_hash_table_add (views, (char *) vfl_valid[idx].views[i]); + + vfl_parser_set_views (parser, views); + + metrics = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, g_free); + for (int i = 0; vfl_valid[idx].metrics[i] != NULL; i++) + { + double *v = g_new (double, 1); + *v = g_random_double_range (0, 10); + g_hash_table_insert (metrics, (char *) vfl_valid[idx].metrics[i], v); + } + + vfl_parser_set_metrics (parser, metrics); + + vfl_parser_parse_line (parser, vfl_valid[idx].expression, -1, &error); + g_assert_no_error (error); + + constraints = vfl_parser_get_constraints (parser, &n_constraints); + g_assert_nonnull (constraints); + g_assert_cmpint (n_constraints, !=, 0); + + g_free (constraints); + + vfl_parser_free (parser); + + g_hash_table_unref (metrics); + g_hash_table_unref (views); +} + +static void +vfl_parser_invalid (gconstpointer data) +{ + int idx = GPOINTER_TO_INT (data); + VflParser *parser = vfl_parser_new (-1, -1, NULL, NULL); + GError *error = NULL; + GHashTable *views; + + fprintf(stderr, "Parsing [invalid]: '%s'\n", vfl_invalid[idx].expression); + + views = g_hash_table_new (g_str_hash, g_str_equal); + for (int i = 0; vfl_invalid[idx].views[i] != NULL; i++) + g_hash_table_add (views, (char *) vfl_invalid[idx].views[i]); + + vfl_parser_set_views (parser, views); + + vfl_parser_parse_line (parser, vfl_invalid[idx].expression, -1, &error); + + g_assert_nonnull (error); + fprintf(stderr, "Error: %s\n", error->message); + + g_assert_error (error, VFL_ERROR, vfl_invalid[idx].error_id); + + g_error_free (error); + vfl_parser_free (parser); + g_hash_table_unref (views); +} + +#if 1 +#include +void shoes_vfl_test(shoes_layout *lay, shoes_canvas *canvas, char *str) { + fprintf(stderr, "In shoes_vfl_test\n"); + int argc = 0; + char *argv[] = {"", (char *)0 }; + for (int i = 0; i < G_N_ELEMENTS (vfl_invalid); i++) + { + char *path = g_strconcat ("/vfl/invalid/", vfl_invalid[i].id, NULL); + + //g_test_add_data_func (path, GINT_TO_POINTER (i), vfl_parser_invalid); + vfl_parser_invalid(GINT_TO_POINTER (i)); + + g_free (path); + } + for (int i = 0; i < G_N_ELEMENTS (vfl_valid); i++) + { + char *path = g_strconcat ("/vfl/valid/", vfl_valid[i].id, NULL); + + //g_test_add_data_func (path, GINT_TO_POINTER (i), vfl_parser_valid); + vfl_parser_valid(GINT_TO_POINTER (i)); + g_free (path); + } + +#else +int +main (int argc, char *argv[]) +{ + g_test_init (&argc, &argv, NULL); // does a gtk_init() - not good for shoes. + + for (int i = 0; i < G_N_ELEMENTS (vfl_invalid); i++) + { + char *path = g_strconcat ("/vfl/invalid/", vfl_invalid[i].id, NULL); + + g_test_add_data_func (path, GINT_TO_POINTER (i), vfl_parser_invalid); + + g_free (path); + } + + for (int i = 0; i < G_N_ELEMENTS (vfl_valid); i++) + { + char *path = g_strconcat ("/vfl/valid/", vfl_valid[i].id, NULL); + + g_test_add_data_func (path, GINT_TO_POINTER (i), vfl_parser_valid); + + g_free (path); + } + + return g_test_run (); +#endif +} + diff --git a/shoes/types/layout.c b/shoes/types/layout.c index 0f1660fe..699ea565 100644 --- a/shoes/types/layout.c +++ b/shoes/types/layout.c @@ -11,6 +11,7 @@ #include "shoes/types/types.h" #include "shoes/types/settings.h" #include "shoes/types/layout.h" +#include "shoes/layout/layouts.h" // // Shoes::Layout needs to be a slot-like class - mostly same api // @@ -19,7 +20,7 @@ extern VALUE cButton, cBackground; // user written managers must implment these methods: static ID s_manager, s_setup, s_addw, s_clear; extern ID s_remove, s_finish, s_size; - +ID s_name; /* FUNC_M generate two functions here * shoes_canvas_c_layout(int argc, VALUE *argv, VALUE self) { ..} * + means call shoes_canvas_repaint_all() at end @@ -39,6 +40,15 @@ void shoes_layout_init() { rb_define_method(cLayout, "width", CASTHOOK(shoes_layout_get_width), 0); rb_define_method(cLayout, "start", CASTHOOK(shoes_layout_start), -1); rb_define_method(cLayout, "style", CASTHOOK(shoes_layout_style), -1); + // VFL parser is not tied to a particular Shoes internal layout. + rb_define_method(cLayout, "vfl_parse", CASTHOOK(shoes_layout_parse_vfl), -1); + //rb_define_method(cLayout, "vfl_metrics", CASTHOOK(shoes_layout_get_metrics), -1); + //rb_define_method(cLayout, "vft_views", CASTHOOK(shoes_layout_get_views), -1); + rb_define_method(cLayout, "vfl_constraints", CASTHOOK(shoes_layout_get_constraints), -1); + + + + /* Vfl needs, others could use */ /* RUBY_M generates defines (allow Ruby to call the FUNC_M funtions rb_define_method(cCanvas, "layout", CASTHOOK(shoes_canvas_c_layout), -1); @@ -47,23 +57,27 @@ void shoes_layout_init() { RUBY_M("+layout", layout, -1); } -void shoes_layout_mark(shoes_layout *ly) { - rb_gc_mark_maybe(ly->canvas); - rb_gc_mark_maybe(ly->delegate); +void shoes_layout_mark(shoes_layout *lay) { + rb_gc_mark_maybe(lay->canvas); + rb_gc_mark_maybe(lay->delegate); + rb_gc_mark_maybe(lay->views); + rb_gc_mark_maybe(lay->metrics); } -static void shoes_layout_free(shoes_layout *ly) { - RUBY_CRITICAL(SHOE_FREE(ly)); +static void shoes_layout_free(shoes_layout *lay) { + RUBY_CRITICAL(SHOE_FREE(lay)); } VALUE shoes_layout_alloc(VALUE klass) { VALUE obj; - shoes_layout *ly = SHOE_ALLOC(shoes_layout); - SHOE_MEMZERO(ly, shoes_layout, 1); - obj = Data_Wrap_Struct(klass, shoes_layout_mark, shoes_layout_free, ly); + shoes_layout *lay = SHOE_ALLOC(shoes_layout); + SHOE_MEMZERO(lay, shoes_layout, 1); + obj = Data_Wrap_Struct(klass, shoes_layout_mark, shoes_layout_free, lay); // set fields ? - ly->delegate = Qnil; - ly->canvas = Qnil; + lay->delegate = Qnil; + lay->canvas = Qnil; + lay->views = rb_hash_new(); + lay->metrics = rb_hash_new(); return obj; } @@ -88,6 +102,7 @@ VALUE shoes_layout_new(VALUE attr, VALUE parent) { s_setup = rb_intern("setup"); s_addw = rb_intern("add"); s_clear = rb_intern("clear"); + s_name = rb_intern("name"); // get manager from attr, put in delegate. mgr = ATTR(attr, manager); if (SYMBOL_P(mgr) || NIL_P(mgr)) { @@ -229,28 +244,47 @@ VALUE shoes_layout_start(int argc, VALUE *argv, VALUE self) { } VALUE shoes_layout_add_rules(int argc, VALUE *argv, VALUE self) { - // TODO: call parse_args once we figure out what it is. VALUE arg; - if (argc < 1) - arg = Qnil; - else - arg = argv[1]; + rb_arg_list args; + rb_parse_args(argc, argv, "h", &args); + arg = args.a[0]; shoes_layout *lay; Data_Get_Struct(self, shoes_layout, lay); shoes_canvas *canvas; Data_Get_Struct(lay->canvas, shoes_canvas, canvas); + VALUE rtn; if (! NIL_P(lay->delegate)) { ID s_rules = rb_intern("rules"); if (rb_respond_to(lay->delegate, s_rules)) - rb_funcall(lay->delegate, s_rules, 1, arg); + rtn = rb_funcall(lay->delegate, s_rules, 1, arg); else rb_raise(rb_eArgError, "'rules' not implmented in Layout"); } else { - shoes_layout_internal_rules(lay, canvas, arg); + rtn = shoes_layout_internal_rules(lay, canvas, arg); } - return Qtrue; + return rtn; +} + +VALUE shoes_layout_parse_vfl(int argc, VALUE *argv, VALUE self) { + VALUE arg; + rb_arg_list args; + rb_parse_args(argc, argv, "h", &args); + arg = args.a[0]; + shoes_layout *lay; + Data_Get_Struct(self, shoes_layout, lay); + shoes_canvas *canvas; + Data_Get_Struct(lay->canvas, shoes_canvas, canvas); + VALUE rtn = shoes_vfl_rules(lay, canvas, arg); + return rtn; +} + +VALUE shoes_layout_get_constraints(int argc, VALUE *argv, VALUE self) { + shoes_layout *lay; + Data_Get_Struct(self, shoes_layout, lay); + return lay->constraints; } + // --------------- called from canvas internals ------------ void shoes_layout_size(shoes_canvas *canvas, int pass) { @@ -286,19 +320,19 @@ void shoes_layout_add_ele(shoes_canvas *canvas, VALUE ele) { } // Find a delegate or use the internal? if (canvas->layout_mgr != Qnil) { - shoes_layout *ly; - Data_Get_Struct(canvas->layout_mgr, shoes_layout, ly); + shoes_layout *lay; + Data_Get_Struct(canvas->layout_mgr, shoes_layout, lay); shoes_canvas *cvs; - Data_Get_Struct(ly->canvas, shoes_canvas, cvs); - if (! NIL_P(ly->delegate)) { - VALUE del = ly->delegate; + Data_Get_Struct(lay->canvas, shoes_canvas, cvs); + if (! NIL_P(lay->delegate)) { + VALUE del = lay->delegate; shoes_abstract *widget; Data_Get_Struct(ele, shoes_abstract, widget); - rb_funcall(del, s_addw, 3, ly->canvas, ele, widget->attr); + rb_funcall(del, s_addw, 3, lay->canvas, ele, widget->attr); return; } } - // here if no delgate or no manager object + // here no manager object shoes_layout_internal_add(canvas, ele); return; } @@ -335,15 +369,24 @@ void shoes_layout_internal_setup(shoes_layout *lay, shoes_canvas *canvas, } void shoes_layout_internal_add(shoes_canvas *canvas, VALUE ele) { - fprintf(stderr, "shoes_layout_internal_add called\n"); + shoes_layout *lay; + Data_Get_Struct(canvas->layout_mgr, shoes_layout, lay); + if (lay->mgr == Layout_VFL) + shoes_vfl_add_ele(canvas, ele); + else + fprintf(stderr, "shoes_layout_internal_add called\n"); } void shoes_layout_internal_clear(shoes_canvas *canvas) { fprintf(stderr, "shoes_layout_internal_clear called\n"); } -void shoes_layout_internal_rules(shoes_layout *lay, shoes_canvas *canvas, VALUE arg) { - fprintf(stderr, "shoes_layout_internal_rules called\n"); +VALUE shoes_layout_internal_rules(shoes_layout *lay, shoes_canvas *canvas, VALUE arg) { + if (lay->mgr == Layout_VFL) { + shoes_vfl_rules(lay, canvas, arg); + } else + fprintf(stderr, "shoes_layout_internal_rules called\n"); + return Qnil; } void shoes_layout_internal_finish(shoes_canvas *canvas) { diff --git a/shoes/types/layout.h b/shoes/types/layout.h index 86d47674..272a27b8 100644 --- a/shoes/types/layout.h +++ b/shoes/types/layout.h @@ -17,7 +17,12 @@ typedef struct { // fields below belong to the C crafted layout manager(s), what ever that // turns out to be. Layout_Types mgr; - VALUE fields; // whatever the manager needs it to be. + VALUE views; // VFL speak. Aka Widgets, Shoes elements + VALUE metrics; // VFL speak. + VALUE constraints; // VFL parse results, ruby-ized. not used ? + /* emeus + * emeus_create_constraints_from_description uses the parser + */ } shoes_layout; // all drawables do/should implement this at the top - slightly safer @@ -31,7 +36,7 @@ typedef struct { extern VALUE cLayout; void shoes_layout_init(); VALUE shoes_layout_new(VALUE attr, VALUE parent); -// slot like methods: +// user visible, slot like methods: VALUE shoes_layout_insert(int argc, VALUE *argv, VALUE self); VALUE shoes_layout_delete_at(int argc, VALUE *argv, VALUE self); VALUE shoes_layout_clear(int argc, VALUE *argv, VALUE self); @@ -41,14 +46,16 @@ VALUE shoes_layout_get_height(VALUE self); VALUE shoes_layout_get_width(VALUE self); VALUE shoes_layout_start(int argc, VALUE *argv, VALUE self); VALUE shoes_layout_style(int argc, VALUE *argv, VALUE self); +VALUE shoes_layout_parse_vfl(int argc, VALUE *argv, VALUE self); +VALUE shoes_layout_get_constraints(int argc, VALUE *argv, VALUE self); -// canvas calls these, delegate to usr or the secret layout +// canvas calls these, delegate to usr written (ruby) or the new C crafted layouts void shoes_layout_size(shoes_canvas *canvas, int pass); void shoes_layout_cleared(shoes_canvas *canvas); void shoes_layout_add_ele(shoes_canvas *canvas, VALUE ele); VALUE shoes_layout_delete_ele(shoes_canvas *canvas, VALUE ele); -// TODO: delegate methods for the future internal manager(s). +// TODO: delegate methods for the new C internal layot(s). void shoes_layout_internal_setup(shoes_layout *lay, shoes_canvas *canvas, VALUE attr); void shoes_layout_internal_add(shoes_canvas *canvas, VALUE ele); @@ -57,5 +64,5 @@ VALUE shoes_layout_internal_delete_at(shoes_layout *lay, shoes_canvas *canvas, void shoes_layout_internal_clear(shoes_canvas *canvas); void shoes_layout_internal_size(shoes_layout *lay, shoes_canvas *canvas, int pass); void shoes_layout_internal_finish(shoes_canvas *canvas); -void shoes_layout_internal_rules(shoes_layout *lay, shoes_canvas *canvas, VALUE arg); +VALUE shoes_layout_internal_rules(shoes_layout *lay, shoes_canvas *canvas, VALUE arg); #endif