From 6f462a72bf8d1165c6a3663f0ba20307856e2ce7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Nison?= Date: Mon, 2 Jan 2017 02:22:45 -0800 Subject: [PATCH] Adds Javascript Support Summary: - As mentioned in the title, this PR adds Javascript support to Yoga. Two different builds are included in this PR thanks to [nbind](https://github.com/charto/nbind), which conveniently allow to target both Node.js' native addons and browser environments via asmjs with approximately the same codebase. That should solve #215. - All tests successfully pass on both codepaths. You can run `yarn test:all` inside the `javascript` directory to test it. - Because of a bug in nbind, the [following PR](https://github.com/charto/nbind/pull/57) needs to be merged and a new version released before this one can be safely merged as well. - I had to use `double` types instead of `float` in the C++ bindings because of an Emscripten [bug](https://github.com/kripken/emscripten/issues/3592) where symbols aren't correctly exported when using floats. - There's some tweaks to do before this PR is 100% ready to merge, but I wanted to have your opinion first. What do you think of this? --- To do: - [x] Ensure th Closes https://github.com/facebook/yoga/pull/304 Reviewed By: mikearmstrong001 Differential Revision: D4375187 Pulled By: emilsjolander fbshipit-source-id: 47248558a9506b7c512b5ef281cd12fe1a60cab7 --- .travis.yml | 12 + docs/_data/nav_docs.yml | 1 + docs/_docs/api/javascript.md | 53 + gentest/gentest-javascript.js | 232 ++ gentest/gentest.js | 6 + gentest/gentest.rb | 4 + gentest/test-template.html | 1 + javascript/.gitignore | 13 + javascript/.hgignore | 15 + javascript/autogypi.json | 6 + javascript/binding.gyp | 30 + javascript/build/Release/nbind.js | 19 + javascript/final-flags.gypi | 30 + javascript/package.json | 49 + javascript/sources/Layout.hh | 30 + javascript/sources/Node.cc | 405 ++++ javascript/sources/Node.hh | 174 ++ javascript/sources/Size.hh | 36 + javascript/sources/entry-browser.js | 32 + javascript/sources/entry-common.js | 205 ++ javascript/sources/entry-node.js | 13 + javascript/sources/global.cc | 27 + javascript/sources/global.hh | 15 + javascript/sources/nbind.cc | 139 ++ javascript/tests/Benchmarks/YGBenchmark.js | 113 + .../Facebook.Yoga/YGAbsolutePositionTest.js | 312 +++ .../tests/Facebook.Yoga/YGAlignContentTest.js | 411 ++++ .../tests/Facebook.Yoga/YGAlignItemsTest.js | 171 ++ .../tests/Facebook.Yoga/YGAlignSelfTest.js | 174 ++ .../tests/Facebook.Yoga/YGBorderTest.js | 209 ++ .../Facebook.Yoga/YGFlexDirectionTest.js | 411 ++++ javascript/tests/Facebook.Yoga/YGFlexTest.js | 418 ++++ .../tests/Facebook.Yoga/YGFlexWrapTest.js | 354 +++ .../Facebook.Yoga/YGJustifyContentTest.js | 685 ++++++ .../tests/Facebook.Yoga/YGMarginTest.js | 436 ++++ .../tests/Facebook.Yoga/YGMeasureCacheTest.js | 35 + .../tests/Facebook.Yoga/YGMeasureTest.js | 33 + .../Facebook.Yoga/YGMinMaxDimensionTest.js | 684 ++++++ .../tests/Facebook.Yoga/YGPaddingTest.js | 254 +++ .../tests/Facebook.Yoga/YGRoundingTest.js | 809 +++++++ javascript/tests/index.html | 34 + javascript/tests/run-bench.js | 73 + javascript/tests/tools.js | 60 + javascript/webpack.config.js | 43 + javascript/yarn.lock | 2006 +++++++++++++++++ 45 files changed, 9272 insertions(+) create mode 100644 docs/_docs/api/javascript.md create mode 100644 gentest/gentest-javascript.js create mode 100644 javascript/.gitignore create mode 100644 javascript/.hgignore create mode 100644 javascript/autogypi.json create mode 100644 javascript/binding.gyp create mode 100644 javascript/build/Release/nbind.js create mode 100644 javascript/final-flags.gypi create mode 100644 javascript/package.json create mode 100644 javascript/sources/Layout.hh create mode 100644 javascript/sources/Node.cc create mode 100644 javascript/sources/Node.hh create mode 100644 javascript/sources/Size.hh create mode 100644 javascript/sources/entry-browser.js create mode 100644 javascript/sources/entry-common.js create mode 100644 javascript/sources/entry-node.js create mode 100644 javascript/sources/global.cc create mode 100644 javascript/sources/global.hh create mode 100644 javascript/sources/nbind.cc create mode 100644 javascript/tests/Benchmarks/YGBenchmark.js create mode 100644 javascript/tests/Facebook.Yoga/YGAbsolutePositionTest.js create mode 100644 javascript/tests/Facebook.Yoga/YGAlignContentTest.js create mode 100644 javascript/tests/Facebook.Yoga/YGAlignItemsTest.js create mode 100644 javascript/tests/Facebook.Yoga/YGAlignSelfTest.js create mode 100644 javascript/tests/Facebook.Yoga/YGBorderTest.js create mode 100644 javascript/tests/Facebook.Yoga/YGFlexDirectionTest.js create mode 100644 javascript/tests/Facebook.Yoga/YGFlexTest.js create mode 100644 javascript/tests/Facebook.Yoga/YGFlexWrapTest.js create mode 100644 javascript/tests/Facebook.Yoga/YGJustifyContentTest.js create mode 100644 javascript/tests/Facebook.Yoga/YGMarginTest.js create mode 100644 javascript/tests/Facebook.Yoga/YGMeasureCacheTest.js create mode 100644 javascript/tests/Facebook.Yoga/YGMeasureTest.js create mode 100644 javascript/tests/Facebook.Yoga/YGMinMaxDimensionTest.js create mode 100644 javascript/tests/Facebook.Yoga/YGPaddingTest.js create mode 100644 javascript/tests/Facebook.Yoga/YGRoundingTest.js create mode 100644 javascript/tests/index.html create mode 100644 javascript/tests/run-bench.js create mode 100644 javascript/tests/tools.js create mode 100644 javascript/webpack.config.js create mode 100644 javascript/yarn.lock diff --git a/.travis.yml b/.travis.yml index 608e2bab..965da701 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,14 +17,26 @@ before_install: - brew cask install java - brew outdated xctool || brew upgrade xctool - brew install mono + - export JAVA_HOME=$(/usr/libexec/java_home -v 1.8) - export PATH=$JAVA_HOME/bin:$PATH +install: + - cd javascript + - npm install + - cd $TRAVIS_BUILD_DIR + script: - buck test //:yoga - buck test //java:java - buck test //YogaKit:YogaKitTests --config cxx.default_platform=iphonesimulator-x86_64 --config cxx.cflags=-DTRAVIS_CI - sh csharp/tests/Facebook.Yoga/test_macos.sh + + - cd javascript + - npm run test:all + - npm run bench + - cd $TRAVIS_BUILD_DIR + - buck run //benchmark:benchmark - git checkout HEAD^ - buck run //benchmark:benchmark diff --git a/docs/_data/nav_docs.yml b/docs/_data/nav_docs.yml index 8c401cfb..92cfe6a5 100644 --- a/docs/_data/nav_docs.yml +++ b/docs/_data/nav_docs.yml @@ -22,3 +22,4 @@ - id: objc - id: java - id: csharp + - id: javascript diff --git a/docs/_docs/api/javascript.md b/docs/_docs/api/javascript.md new file mode 100644 index 00000000..0f176ba5 --- /dev/null +++ b/docs/_docs/api/javascript.md @@ -0,0 +1,53 @@ +--- +docid: javascript +title: Javascript +layout: docs +permalink: /docs/api/javascript/ +--- + +The `yoga-layout` module offers two different implementations of Yoga. The first one is a native Node module, and the second one is an asm.js fallback that can be used when native compilation isn't an option - such as web browsers. + +> Because this module is compiled from a C++ codebase, the function prototypes below will use the C++-syntax. Nevertheless, the corresponding methods all exist on the JS side, with the same arguments (`Node *` being a node object). + +### Lifecycle + +Create a node via `Yoga.Node.create()`, and destroy it by using its `free()` or `freeRecursive()` method. Note that unlike most objects in your Javascript applications, your nodes will not get automatically garbage collected, which means that it is especially important you keep track of them so you can free them when you no longer need them. + + + +### Children + +The following methods help manage the children of a node. + + + +### Style getters & setters + +The large part of Yoga's API consists of properties, setters, and getters for styles. These all follow the same general structure. Bellow are the function and enums used to control the various styles. For an in depth guide to how each style works see the getting started guide. + + + + + +### Layout results + +Once you have set up a tree of nodes with styles you will want to get the result of a layout calculation. Call `calculateLayout()` perform layout calculation. Once this function returns the results of the layout calculation is stored on each node. Traverse the tree and retrieve the values from each node. + + + +### Custom measurements + +Certain nodes need to ability to measure themselves, the most common example is nodes which represent text. Text has an intrinsic size and requires measuring itself to determine that size. This is not something Yoga can do as it requires relying on the host system's text rendering engine. + +- Call `markDirty()` if a node with a custom text measurement function needs to be re-measured during the next layout pass. +- Note that in the prototype below, `Size` is an object that contain both `width` and `height` properties. + +> A measure function can only be attached to a leaf node in the hierarchy. + + + +### Experiments + +Yoga has the concept of experiments. An experiment is a feature which is not yet stable. To enable a feature use the following functions. Once a feature has been tested and is ready to be released as a stable API we will remove its feature flag. + + diff --git a/gentest/gentest-javascript.js b/gentest/gentest-javascript.js new file mode 100644 index 00000000..8c65867c --- /dev/null +++ b/gentest/gentest-javascript.js @@ -0,0 +1,232 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +var JavascriptEmitter = function() { + Emitter.call(this, 'js', ' '); +}; + +function toJavascriptUpper(symbol) { + var out = ''; + for (var i = 0; i < symbol.length; i++) { + var c = symbol[i]; + if (c == c.toUpperCase() && i != 0 && symbol[i - 1] != symbol[i - 1].toUpperCase()) { + out += '_'; + } + out += c.toUpperCase(); + } + return out; +} + +JavascriptEmitter.prototype = Object.create(Emitter.prototype, { + constructor:{value:JavascriptEmitter}, + + emitPrologue:{value:function() { + this.push([ + 'var Yoga = Yoga || require("../../sources/entry-" + process.env.TEST_ENTRY);', + '' + ]); + }}, + + emitTestPrologue:{value:function(name, experiments) { + this.push('it(' + JSON.stringify(name) + ', function () {'); + this.pushIndent(); + + if (experiments.length > 0) { + for (var i in experiments) { + this.push('Yoga.setExperimentalFeatureEnabled(Yoga.FEATURE_' + toJavascriptUpper(experiments[i]) + ', true);'); + } + this.push(''); + } + }}, + + emitTestTreePrologue:{value:function(nodeName) { + this.push('var ' + nodeName + ' = Yoga.Node.create();'); + }}, + + emitTestEpilogue:{value:function(experiments) { + this.push(''); + this.push('if (typeof root !== "undefined")'); + this.pushIndent(); + this.push('root.freeRecursive();'); + this.popIndent(); + + this.push(''); + this.push('(typeof gc !== "undefined") && gc();'); + this.AssertEQ('0', 'Yoga.getInstanceCount()'); + + if (experiments.length > 0) { + this.push(''); + for (var i in experiments) { + this.push('Yoga.setExperimentalFeatureEnabled(Yoga.FEATURE_' + toJavascriptUpper(experiments[i]) + ', false);'); + } + } + + this.popIndent(); + this.push('});'); + }}, + + emitEpilogue:{value:function () { + this.push(''); + }}, + + AssertEQ:{value:function(v0, v1) { + this.push('console.assert(' + v0 + ' === ' + v1 + ', "' + v0 + ' === ' + v1 + ' (" + ' + v1 + ' + ")");'); + }}, + + YGAlignAuto:{value:'Yoga.ALIGN_AUTO'}, + YGAlignCenter:{value:'Yoga.ALIGN_CENTER'}, + YGAlignFlexEnd:{value:'Yoga.ALIGN_FLEX_END'}, + YGAlignFlexStart:{value:'Yoga.ALIGN_FLEX_START'}, + YGAlignStretch:{value:'Yoga.ALIGN_STRETCH'}, + + YGDirectionInherit:{value:'Yoga.DIRECTION_INHERIT'}, + YGDirectionLTR:{value:'Yoga.DIRECTION_LTR'}, + YGDirectionRTL:{value:'Yoga.DIRECTION_RTL'}, + + YGEdgeBottom:{value:'Yoga.EDGE_BOTTOM'}, + YGEdgeEnd:{value:'Yoga.EDGE_END'}, + YGEdgeLeft:{value:'Yoga.EDGE_LEFT'}, + YGEdgeRight:{value:'Yoga.EDGE_RIGHT'}, + YGEdgeStart:{value:'Yoga.EDGE_START'}, + YGEdgeTop:{value:'Yoga.EDGE_TOP'}, + + YGFlexDirectionColumn:{value:'Yoga.FLEX_DIRECTION_COLUMN'}, + YGFlexDirectionColumnReverse:{value:'Yoga.FLEX_DIRECTION_COLUMN_REVERSE'}, + YGFlexDirectionRow:{value:'Yoga.FLEX_DIRECTION_ROW'}, + YGFlexDirectionRowReverse:{value:'Yoga.FLEX_DIRECTION_ROW_REVERSE'}, + + YGJustifyCenter:{value:'Yoga.JUSTIFY_CENTER'}, + YGJustifyFlexEnd:{value:'Yoga.JUSTIFY_FLEX_END'}, + YGJustifyFlexStart:{value:'Yoga.JUSTIFY_FLEX_START'}, + YGJustifySpaceAround:{value:'Yoga.JUSTIFY_SPACE_AROUND'}, + YGJustifySpaceBetween:{value:'Yoga.JUSTIFY_SPACE_BETWEEN'}, + + YGOverflowHidden:{value:'Yoga.OVERFLOW_HIDDEN'}, + YGOverflowVisible:{value:'Yoga.OVERFLOW_VISIBLE'}, + + YGPositionTypeAbsolute:{value:'Yoga.POSITION_TYPE_ABSOLUTE'}, + YGPositionTypeRelative:{value:'Yoga.POSITION_TYPE_RELATIVE'}, + + YGWrapNoWrap:{value:'Yoga.WRAP_NO_WRAP'}, + YGWrapWrap:{value:'Yoga.WRAP_WRAP'}, + + YGUndefined:{value:'Yoga.UNDEFINED'}, + + YGNodeCalculateLayout:{value:function(node, dir) { + this.push(node + '.calculateLayout(Yoga.UNDEFINED, Yoga.UNDEFINED, ' + dir + ');'); + }}, + + YGNodeInsertChild:{value:function(parentName, nodeName, index) { + this.push(parentName + '.insertChild(' + nodeName + ', ' + index + ');'); + }}, + + YGNodeLayoutGetLeft:{value:function(nodeName) { + return nodeName + '.getComputedLeft()'; + }}, + + YGNodeLayoutGetTop:{value:function(nodeName) { + return nodeName + '.getComputedTop()'; + }}, + + YGNodeLayoutGetWidth:{value:function(nodeName) { + return nodeName + '.getComputedWidth()'; + }}, + + YGNodeLayoutGetHeight:{value:function(nodeName) { + return nodeName + '.getComputedHeight()'; + }}, + + YGNodeStyleSetAlignContent:{value:function(nodeName, value) { + this.push(nodeName + '.setAlignContent(' + value + ');'); + }}, + + YGNodeStyleSetAlignItems:{value:function(nodeName, value) { + this.push(nodeName + '.setAlignItems(' + value + ');'); + }}, + + YGNodeStyleSetAlignSelf:{value:function(nodeName, value) { + this.push(nodeName + '.setAlignSelf(' + value + ');'); + }}, + + YGNodeStyleSetBorder:{value:function(nodeName, edge, value) { + this.push(nodeName + '.setBorder(' + edge + ', ' + value + ');'); + }}, + + YGNodeStyleSetDirection:{value:function(nodeName, value) { + this.push(nodeName + '.setDirection(' + value + ');'); + }}, + + YGNodeStyleSetFlexBasis:{value:function(nodeName, value) { + this.push(nodeName + '.setFlexBasis(' + value + ');'); + }}, + + YGNodeStyleSetFlexDirection:{value:function(nodeName, value) { + this.push(nodeName + '.setFlexDirection(' + value + ');'); + }}, + + YGNodeStyleSetFlexGrow:{value:function(nodeName, value) { + this.push(nodeName + '.setFlexGrow(' + value + ');'); + }}, + + YGNodeStyleSetFlexShrink:{value:function(nodeName, value) { + this.push(nodeName + '.setFlexShrink(' + value + ');'); + }}, + + YGNodeStyleSetFlexWrap:{value:function(nodeName, value) { + this.push(nodeName + '.setFlexWrap(' + value + ');'); + }}, + + YGNodeStyleSetHeight:{value:function(nodeName, value) { + this.push(nodeName + '.setHeight(' + value + ');'); + }}, + + YGNodeStyleSetJustifyContent:{value:function(nodeName, value) { + this.push(nodeName + '.setJustifyContent(' + value + ');'); + }}, + + YGNodeStyleSetMargin:{value:function(nodeName, edge, value) { + this.push(nodeName + '.setMargin(' + edge + ', ' + value + ');'); + }}, + + YGNodeStyleSetMaxHeight:{value:function(nodeName, value) { + this.push(nodeName + '.setMaxHeight(' + value + ');'); + }}, + + YGNodeStyleSetMaxWidth:{value:function(nodeName, value) { + this.push(nodeName + '.setMaxWidth(' + value + ');'); + }}, + + YGNodeStyleSetMinHeight:{value:function(nodeName, value) { + this.push(nodeName + '.setMinHeight(' + value + ');'); + }}, + + YGNodeStyleSetMinWidth:{value:function(nodeName, value) { + this.push(nodeName + '.setMinWidth(' + value + ');'); + }}, + + YGNodeStyleSetOverflow:{value:function(nodeName, value) { + this.push(nodeName + '.setOverflow(' + value + ');'); + }}, + + YGNodeStyleSetPadding:{value:function(nodeName, edge, value) { + this.push(nodeName + '.setPadding(' + edge + ', ' + value + ');'); + }}, + + YGNodeStyleSetPosition:{value:function(nodeName, edge, value) { + this.push(nodeName + '.setPosition(' + edge + ', ' + value + ');'); + }}, + + YGNodeStyleSetPositionType:{value:function(nodeName, value) { + this.push(nodeName + '.setPositionType(' + value + ');'); + }}, + + YGNodeStyleSetWidth:{value:function(nodeName, value) { + this.push(nodeName + '.setWidth(' + value + ');'); + }}, +}); diff --git a/gentest/gentest.js b/gentest/gentest.js index 196a8c1e..5851b45e 100755 --- a/gentest/gentest.js +++ b/gentest/gentest.js @@ -27,6 +27,12 @@ window.onload = function() { document.body.children[0], document.body.children[1], document.body.children[2]); + + printTest( + new JavascriptEmitter(), + document.body.children[0], + document.body.children[1], + document.body.children[2]); } function assert(condition, message) { diff --git a/gentest/gentest.rb b/gentest/gentest.rb index 4293866d..71edff20 100644 --- a/gentest/gentest.rb +++ b/gentest/gentest.rb @@ -47,6 +47,10 @@ Dir['fixtures/*.html'].each do |file| f = File.open("../csharp/tests/Facebook.Yoga/#{name}.cs", 'w') f.write eval(logs[2].message.sub(/^[^"]*/, '')).sub('YogaTest', name) f.close + + f = File.open("../javascript/tests/Facebook.Yoga/#{name}.js", 'w') + f.write eval(logs[3].message.sub(/^[^"]*/, '')).sub('YogaTest', name) + f.close end File.delete('test.html') browser.close diff --git a/gentest/test-template.html b/gentest/test-template.html index 0b4102f7..cb01b8c4 100644 --- a/gentest/test-template.html +++ b/gentest/test-template.html @@ -7,6 +7,7 @@ +