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