Files
yoga/gentest/gentest.js

792 lines
20 KiB
JavaScript
Raw Normal View History

/**
Fix Generation of Tests from Fixtures Summary: https://github.com/facebook/yoga/pull/1116 added a change to the test generator "gentests.rb" to support a newer version of chromedriver, along with a change to the enum generator (not touched in this diff) to produce code consistent with the current tests, which seem to have been manually edited since last generation. I had trouble running the test generator locally, because it relies on unversioned third-party dependencies, whose APIs change. Looking at source history, it seems like each time someone wants to run the script, they end up updating its syntax to match whatever versions they pull in. This change adds a Gemfile and lock so that that the version of "watir" is locked, and so that we will also automatically pull in a consistent "chomedriver" version via the "webdrivers" gem. It includes the updates from the PR to be consistent with already output tests, and I have also updated the copyright header generation to no longer create lint warnings on newly generated tests (some of the previous ones were fixed manually it looks like). The test generator would still produce bodies which would fail clang-format, and were manually edited (causing generation to emit new lint warnings), so I updated the generator to suppress clang-format in the body of the generated files. Three tests, around the interaction of minimum dimensions and flexible children produce different results in Chrome now compared to when the tests were added, so running `gentests.rb` creates tests which break UTs. This doesn't seem like any sort of rounding, or device specific difference, so I have disabled these tests for now. While digging around, it does look like Chrome periodically will fix bugs in its own layout implementation which cause differences, like https://bugs.chromium.org/p/chromium/issues/detail?id=927066 Reviewed By: rozele, Andrey-Mishanin Differential Revision: D39907416 fbshipit-source-id: f88714ff038b42f935901783452df25eabb6ebb1
2022-09-29 22:25:24 -07:00
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/* eslint-env browser */
/* global CPPEmitter:readable, JavaEmitter:readable, JavascriptEmitter:readable */
const DEFAULT_EXPERIMENTS = ['AbsolutePercentageAgainstPaddingEdge'];
window.onload = function () {
checkDefaultValues();
printTest(
new CPPEmitter(),
'cpp',
document.body.children[0],
document.body.children[1],
document.body.children[2],
);
printTest(
new JavaEmitter(),
'java',
document.body.children[0],
document.body.children[1],
document.body.children[2],
);
printTest(
new JavascriptEmitter(),
'js',
document.body.children[0],
document.body.children[1],
document.body.children[2],
);
};
function assert(condition, message) {
if (!condition) {
throw new Error(message);
}
}
Fix Generation of Tests from Fixtures Summary: https://github.com/facebook/yoga/pull/1116 added a change to the test generator "gentests.rb" to support a newer version of chromedriver, along with a change to the enum generator (not touched in this diff) to produce code consistent with the current tests, which seem to have been manually edited since last generation. I had trouble running the test generator locally, because it relies on unversioned third-party dependencies, whose APIs change. Looking at source history, it seems like each time someone wants to run the script, they end up updating its syntax to match whatever versions they pull in. This change adds a Gemfile and lock so that that the version of "watir" is locked, and so that we will also automatically pull in a consistent "chomedriver" version via the "webdrivers" gem. It includes the updates from the PR to be consistent with already output tests, and I have also updated the copyright header generation to no longer create lint warnings on newly generated tests (some of the previous ones were fixed manually it looks like). The test generator would still produce bodies which would fail clang-format, and were manually edited (causing generation to emit new lint warnings), so I updated the generator to suppress clang-format in the body of the generated files. Three tests, around the interaction of minimum dimensions and flexible children produce different results in Chrome now compared to when the tests were added, so running `gentests.rb` creates tests which break UTs. This doesn't seem like any sort of rounding, or device specific difference, so I have disabled these tests for now. While digging around, it does look like Chrome periodically will fix bugs in its own layout implementation which cause differences, like https://bugs.chromium.org/p/chromium/issues/detail?id=927066 Reviewed By: rozele, Andrey-Mishanin Differential Revision: D39907416 fbshipit-source-id: f88714ff038b42f935901783452df25eabb6ebb1
2022-09-29 22:25:24 -07:00
function printTest(e, ext, LTRContainer, RTLContainer, genericContainer) {
e.push([
Fix Generation of Tests from Fixtures Summary: https://github.com/facebook/yoga/pull/1116 added a change to the test generator "gentests.rb" to support a newer version of chromedriver, along with a change to the enum generator (not touched in this diff) to produce code consistent with the current tests, which seem to have been manually edited since last generation. I had trouble running the test generator locally, because it relies on unversioned third-party dependencies, whose APIs change. Looking at source history, it seems like each time someone wants to run the script, they end up updating its syntax to match whatever versions they pull in. This change adds a Gemfile and lock so that that the version of "watir" is locked, and so that we will also automatically pull in a consistent "chomedriver" version via the "webdrivers" gem. It includes the updates from the PR to be consistent with already output tests, and I have also updated the copyright header generation to no longer create lint warnings on newly generated tests (some of the previous ones were fixed manually it looks like). The test generator would still produce bodies which would fail clang-format, and were manually edited (causing generation to emit new lint warnings), so I updated the generator to suppress clang-format in the body of the generated files. Three tests, around the interaction of minimum dimensions and flexible children produce different results in Chrome now compared to when the tests were added, so running `gentests.rb` creates tests which break UTs. This doesn't seem like any sort of rounding, or device specific difference, so I have disabled these tests for now. While digging around, it does look like Chrome periodically will fix bugs in its own layout implementation which cause differences, like https://bugs.chromium.org/p/chromium/issues/detail?id=927066 Reviewed By: rozele, Andrey-Mishanin Differential Revision: D39907416 fbshipit-source-id: f88714ff038b42f935901783452df25eabb6ebb1
2022-09-29 22:25:24 -07:00
ext === 'js' ? '/**' : '/*',
' * Copyright (c) Meta Platforms, Inc. and affiliates.',
' *',
' * This source code is licensed under the MIT license found in the',
' * LICENSE file in the root directory of this source tree.',
' */',
Fix Generation of Tests from Fixtures Summary: https://github.com/facebook/yoga/pull/1116 added a change to the test generator "gentests.rb" to support a newer version of chromedriver, along with a change to the enum generator (not touched in this diff) to produce code consistent with the current tests, which seem to have been manually edited since last generation. I had trouble running the test generator locally, because it relies on unversioned third-party dependencies, whose APIs change. Looking at source history, it seems like each time someone wants to run the script, they end up updating its syntax to match whatever versions they pull in. This change adds a Gemfile and lock so that that the version of "watir" is locked, and so that we will also automatically pull in a consistent "chomedriver" version via the "webdrivers" gem. It includes the updates from the PR to be consistent with already output tests, and I have also updated the copyright header generation to no longer create lint warnings on newly generated tests (some of the previous ones were fixed manually it looks like). The test generator would still produce bodies which would fail clang-format, and were manually edited (causing generation to emit new lint warnings), so I updated the generator to suppress clang-format in the body of the generated files. Three tests, around the interaction of minimum dimensions and flexible children produce different results in Chrome now compared to when the tests were added, so running `gentests.rb` creates tests which break UTs. This doesn't seem like any sort of rounding, or device specific difference, so I have disabled these tests for now. While digging around, it does look like Chrome periodically will fix bugs in its own layout implementation which cause differences, like https://bugs.chromium.org/p/chromium/issues/detail?id=927066 Reviewed By: rozele, Andrey-Mishanin Differential Revision: D39907416 fbshipit-source-id: f88714ff038b42f935901783452df25eabb6ebb1
2022-09-29 22:25:24 -07:00
ext === 'cpp' ? '\n// clang-format off' : '',
'// @' +
'generated by gentest/gentest.rb from gentest/fixtures/' +
document.title +
'.html',
'',
]);
e.emitPrologue();
const LTRLayoutTree = calculateTree(LTRContainer);
const RTLLayoutTree = calculateTree(RTLContainer);
const genericLayoutTree = calculateTree(genericContainer);
for (let i = 0; i < genericLayoutTree.length; i++) {
e.emitTestPrologue(
genericLayoutTree[i].name,
genericLayoutTree[i].experiments,
genericLayoutTree[i].disabled,
);
if (genericLayoutTree[i].name == 'wrap_column') {
// Modify width and left values due to both safari and chrome not abiding by the
// specification. The undefined dimension of a parent should be defined by the total size
// of their children in that dimension.
// See diagram under flex-wrap header https://www.w3.org/TR/css-flexbox-1/
assert(
LTRLayoutTree[0].width == 30,
'wrap_column LTR root.width should be 30',
);
LTRLayoutTree[0].width = 60;
assert(
RTLLayoutTree[0].width == 30,
'wrap_column RTL root.width should be 30',
);
RTLLayoutTree[0].width = 60;
const children = RTLLayoutTree[0].children;
assert(
children[0].left == 0,
'wrap_column RTL root_child0.left should be 0',
);
children[0].left = 30;
assert(
children[1].left == 0,
'wrap_column RTL root_child0.left should be 0',
);
children[1].left = 30;
assert(
children[2].left == 0,
'wrap_column RTL root_child2.left should be 0',
);
children[2].left = 30;
assert(
children[3].left == -30,
'wrap_column RTL root_child3.left should be -30',
);
children[3].left = 0;
}
setupTestTree(
e,
undefined,
LTRLayoutTree[i],
genericLayoutTree[i],
'root',
null,
);
e.YGNodeCalculateLayout(
'root',
e.YGDirectionLTR,
genericLayoutTree[i].experiments,
);
e.push('');
assertTestTree(e, LTRLayoutTree[i], 'root', null);
e.push('');
e.YGNodeCalculateLayout(
'root',
e.YGDirectionRTL,
genericLayoutTree[i].experiments,
);
e.push('');
assertTestTree(e, RTLLayoutTree[i], 'root', null);
e.emitTestEpilogue(genericLayoutTree[i].experiments);
}
e.emitEpilogue();
e.print();
}
function assertTestTree(e, node, nodeName, _parentName) {
e.AssertEQ(node.left, e.YGNodeLayoutGetLeft(nodeName));
e.AssertEQ(node.top, e.YGNodeLayoutGetTop(nodeName));
e.AssertEQ(node.width, e.YGNodeLayoutGetWidth(nodeName));
e.AssertEQ(node.height, e.YGNodeLayoutGetHeight(nodeName));
for (let i = 0; i < node.children.length; i++) {
e.push('');
const childName = nodeName + '_child' + i;
assertTestTree(e, node.children[i], childName, nodeName);
}
}
function checkDefaultValues() {
// Sanity check of the Yoga default values by test-template.html
[
{style: 'flex-direction', value: 'column'},
{style: 'justify-content', value: 'flex-start'},
{style: 'align-content', value: 'flex-start'},
{style: 'align-items', value: 'stretch'},
{style: 'position', value: 'relative'},
{style: 'flex-wrap', value: 'nowrap'},
{style: 'overflow', value: 'visible'},
{style: 'flex-grow', value: '0'},
{style: 'flex-shrink', value: '0'},
{style: 'left', value: 'undefined'},
{style: 'top', value: 'undefined'},
{style: 'right', value: 'undefined'},
{style: 'bottom', value: 'undefined'},
{style: 'display', value: 'flex'},
].forEach(item => {
assert(
isDefaultStyleValue(item.style, item.value),
item.style + ' should be ' + item.value,
);
});
}
function setupTestTree(
e,
parent,
node,
genericNode,
nodeName,
parentName,
index,
) {
e.emitTestTreePrologue(nodeName);
for (const style in node.style) {
// Skip position info for root as it messes up tests
if (
node.declaredStyle[style] === '' &&
(style == 'position' ||
style == 'left' ||
style == 'top' ||
style == 'right' ||
style == 'bottom' ||
style == 'width' ||
style == 'height')
) {
continue;
}
if (!isDefaultStyleValue(style, node.style[style])) {
switch (style) {
case 'gap':
e.YGNodeStyleSetGap(
nodeName,
e.YGGutterAll,
pointValue(e, node.style[style]),
);
break;
case 'column-gap':
e.YGNodeStyleSetGap(
nodeName,
e.YGGutterColumn,
pointValue(e, node.style[style]),
);
break;
case 'row-gap':
e.YGNodeStyleSetGap(
nodeName,
e.YGGutterRow,
pointValue(e, node.style[style]),
);
break;
case 'direction':
e.YGNodeStyleSetDirection(
nodeName,
directionValue(e, node.style[style]),
);
break;
case 'flex-direction':
e.YGNodeStyleSetFlexDirection(
nodeName,
flexDirectionValue(e, node.style[style]),
);
break;
case 'justify-content':
e.YGNodeStyleSetJustifyContent(
nodeName,
justifyValue(e, node.style[style]),
);
break;
case 'align-content':
e.YGNodeStyleSetAlignContent(
nodeName,
alignValue(e, node.style[style]),
);
break;
case 'align-items':
e.YGNodeStyleSetAlignItems(
nodeName,
alignValue(e, node.style[style]),
);
break;
case 'align-self':
if (!parent || node.style[style] !== parent.style['align-items']) {
e.YGNodeStyleSetAlignSelf(
nodeName,
alignValue(e, node.style[style]),
);
}
break;
case 'position':
e.YGNodeStyleSetPositionType(
nodeName,
positionValue(e, node.style[style]),
);
break;
case 'flex-wrap':
e.YGNodeStyleSetFlexWrap(nodeName, wrapValue(e, node.style[style]));
break;
case 'overflow':
e.YGNodeStyleSetOverflow(
nodeName,
overflowValue(e, node.style[style]),
);
break;
case 'flex-grow':
e.YGNodeStyleSetFlexGrow(nodeName, node.style[style]);
break;
case 'flex-shrink':
e.YGNodeStyleSetFlexShrink(nodeName, node.style[style]);
break;
case 'flex-basis':
e.YGNodeStyleSetFlexBasis(nodeName, pointValue(e, node.style[style]));
break;
case 'left':
if (genericNode.rawStyle.indexOf('start:') >= 0) {
e.YGNodeStyleSetPosition(
nodeName,
e.YGEdgeStart,
pointValue(e, node.style[style]),
);
} else {
e.YGNodeStyleSetPosition(
nodeName,
e.YGEdgeLeft,
pointValue(e, node.style[style]),
);
}
break;
case 'top':
e.YGNodeStyleSetPosition(
nodeName,
e.YGEdgeTop,
pointValue(e, node.style[style]),
);
break;
case 'right':
if (genericNode.rawStyle.indexOf('end:') >= 0) {
e.YGNodeStyleSetPosition(
nodeName,
e.YGEdgeEnd,
pointValue(e, node.style[style]),
);
} else {
e.YGNodeStyleSetPosition(
nodeName,
e.YGEdgeRight,
pointValue(e, node.style[style]),
);
}
break;
case 'bottom':
e.YGNodeStyleSetPosition(
nodeName,
e.YGEdgeBottom,
pointValue(e, node.style[style]),
);
break;
case 'margin-left':
if (genericNode.rawStyle.indexOf('margin-start:') >= 0) {
e.YGNodeStyleSetMargin(
nodeName,
e.YGEdgeStart,
pointValue(e, node.style[style]),
);
} else {
e.YGNodeStyleSetMargin(
nodeName,
e.YGEdgeLeft,
pointValue(e, node.style[style]),
);
}
break;
case 'margin-top':
e.YGNodeStyleSetMargin(
nodeName,
e.YGEdgeTop,
pointValue(e, node.style[style]),
);
break;
case 'margin-right':
if (genericNode.rawStyle.indexOf('margin-end:') >= 0) {
e.YGNodeStyleSetMargin(
nodeName,
e.YGEdgeEnd,
pointValue(e, node.style[style]),
);
} else {
e.YGNodeStyleSetMargin(
nodeName,
e.YGEdgeRight,
pointValue(e, node.style[style]),
);
}
break;
case 'margin-bottom':
e.YGNodeStyleSetMargin(
nodeName,
e.YGEdgeBottom,
pointValue(e, node.style[style]),
);
break;
case 'padding-left':
if (genericNode.rawStyle.indexOf('padding-start:') >= 0) {
e.YGNodeStyleSetPadding(
nodeName,
e.YGEdgeStart,
pointValue(e, node.style[style]),
);
} else {
e.YGNodeStyleSetPadding(
nodeName,
e.YGEdgeLeft,
pointValue(e, node.style[style]),
);
}
break;
case 'padding-top':
e.YGNodeStyleSetPadding(
nodeName,
e.YGEdgeTop,
pointValue(e, node.style[style]),
);
break;
case 'padding-right':
if (genericNode.rawStyle.indexOf('padding-end:') >= 0) {
e.YGNodeStyleSetPadding(
nodeName,
e.YGEdgeEnd,
pointValue(e, node.style[style]),
);
} else {
e.YGNodeStyleSetPadding(
nodeName,
e.YGEdgeRight,
pointValue(e, node.style[style]),
);
}
break;
case 'padding-bottom':
e.YGNodeStyleSetPadding(
nodeName,
e.YGEdgeBottom,
pointValue(e, node.style[style]),
);
break;
case 'border-left-width':
if (genericNode.rawStyle.indexOf('border-start-width:') >= 0) {
e.YGNodeStyleSetBorder(
nodeName,
e.YGEdgeStart,
pointValue(e, node.style[style]),
);
} else {
e.YGNodeStyleSetBorder(
nodeName,
e.YGEdgeLeft,
pointValue(e, node.style[style]),
);
}
break;
case 'border-top-width':
e.YGNodeStyleSetBorder(
nodeName,
e.YGEdgeTop,
pointValue(e, node.style[style]),
);
break;
case 'border-right-width':
if (genericNode.rawStyle.indexOf('border-end-width:') >= 0) {
e.YGNodeStyleSetBorder(
nodeName,
e.YGEdgeEnd,
pointValue(e, node.style[style]),
);
} else {
e.YGNodeStyleSetBorder(
nodeName,
e.YGEdgeRight,
pointValue(e, node.style[style]),
);
}
break;
case 'border-bottom-width':
e.YGNodeStyleSetBorder(
nodeName,
e.YGEdgeBottom,
pointValue(e, node.style[style]),
);
break;
case 'width':
e.YGNodeStyleSetWidth(nodeName, pointValue(e, node.style[style]));
break;
case 'min-width':
e.YGNodeStyleSetMinWidth(nodeName, pointValue(e, node.style[style]));
break;
case 'max-width':
e.YGNodeStyleSetMaxWidth(nodeName, pointValue(e, node.style[style]));
break;
case 'height':
e.YGNodeStyleSetHeight(nodeName, pointValue(e, node.style[style]));
break;
case 'min-height':
e.YGNodeStyleSetMinHeight(nodeName, pointValue(e, node.style[style]));
break;
case 'max-height':
e.YGNodeStyleSetMaxHeight(nodeName, pointValue(e, node.style[style]));
break;
case 'display':
e.YGNodeStyleSetDisplay(nodeName, displayValue(e, node.style[style]));
break;
}
}
}
if (parentName) {
e.YGNodeInsertChild(parentName, nodeName, index);
}
for (let i = 0; i < node.children.length; i++) {
e.push('');
const childName = nodeName + '_child' + i;
setupTestTree(
e,
node,
node.children[i],
genericNode.children[i],
childName,
nodeName,
i,
);
}
}
function overflowValue(e, value) {
switch (value) {
case 'visible':
return e.YGOverflowVisible;
case 'hidden':
return e.YGOverflowHidden;
}
}
function wrapValue(e, value) {
switch (value) {
case 'wrap':
return e.YGWrapWrap;
case 'wrap-reverse':
return e.YGWrapWrapReverse;
case 'nowrap':
return e.YGWrapNoWrap;
}
}
function flexDirectionValue(e, value) {
switch (value) {
case 'row':
return e.YGFlexDirectionRow;
case 'row-reverse':
return e.YGFlexDirectionRowReverse;
case 'column':
return e.YGFlexDirectionColumn;
case 'column-reverse':
return e.YGFlexDirectionColumnReverse;
}
}
function justifyValue(e, value) {
switch (value) {
case 'center':
return e.YGJustifyCenter;
case 'space-around':
return e.YGJustifySpaceAround;
case 'space-between':
return e.YGJustifySpaceBetween;
case 'space-evenly':
return e.YGJustifySpaceEvenly;
case 'flex-start':
return e.YGJustifyFlexStart;
case 'flex-end':
return e.YGJustifyFlexEnd;
}
}
function positionValue(e, value) {
switch (value) {
case 'absolute':
return e.YGPositionTypeAbsolute;
default:
return e.YGPositionTypeRelative;
}
}
function directionValue(e, value) {
switch (value) {
case 'ltr':
return e.YGDirectionLTR;
case 'rtl':
return e.YGDirectionRTL;
case 'inherit':
return e.YGDirectionInherit;
}
}
function alignValue(e, value) {
switch (value) {
case 'auto':
return e.YGAlignAuto;
case 'center':
return e.YGAlignCenter;
case 'stretch':
return e.YGAlignStretch;
case 'flex-start':
return e.YGAlignFlexStart;
case 'flex-end':
return e.YGAlignFlexEnd;
case 'space-between':
return e.YGAlignSpaceBetween;
case 'space-around':
return e.YGAlignSpaceAround;
case 'baseline':
return e.YGAlignBaseline;
}
}
function pointValue(e, value) {
switch (value) {
case 'auto':
return e.YGAuto;
case 'undefined':
return e.YGUndefined;
default:
return value;
}
}
function displayValue(e, value) {
switch (value) {
case 'flex':
return e.YGDisplayFlex;
case 'none':
return e.YGDisplayNone;
}
}
const DEFAULT_STYLES = new Map();
function isDefaultStyleValue(style, value) {
let defaultStyle = DEFAULT_STYLES.get(style);
if (defaultStyle == null) {
switch (style) {
case 'position':
defaultStyle = new Set(['relative']);
break;
case 'left':
case 'top':
case 'right':
case 'bottom':
case 'start':
case 'end':
defaultStyle = new Set(['undefined']);
break;
case 'min-height':
case 'min-width':
defaultStyle = new Set(['0', '0px', 'auto']);
break;
default: {
const node = document.getElementById('default');
defaultStyle = new Set([getComputedStyle(node, null)[style]]);
break;
}
}
DEFAULT_STYLES.set(style, defaultStyle);
}
return DEFAULT_STYLES.get(style).has(value);
}
New round-to-pixel-grid algorithm that fixes possible subpixel gaps between sibling nodes Summary: This diff introduces new, little bit sophisticated round-to-pixel-grid algorithm. **Motivation:** Previous simple and straightforward solution works in most cases but sometimes produce the not-so-great result. A while ago Nick Lockwood described this problem and proposed the solution in RN's RCTShadowView class: For example, say you have the following structure: // +--------+---------+--------+ // | |+-------+| | // | || || | // | |+-------+| | // +--------+---------+--------+ Say the screen width is 320 pts so the three big views will get the following x bounds from our layout system: {0, 106.667}, {106.667, 213.333}, {213.333, 320} Assuming screen scale is 2, these numbers must be rounded to the nearest 0.5 to fit the pixel grid: {0, 106.5}, {106.5, 213.5}, {213.5, 320} You'll notice that the three widths are 106.5, 107, 106.5. This is great for the parent views but it gets trickier when we consider rounding for the subview. When we go to round the bounds for the subview in the middle, it's relative bounds are {0, 106.667} which gets rounded to {0, 106.5}. This will cause the subview to be one pixel smaller than it should be. This is why we need to pass in the absolute position in order to do the rounding relative to the screen's grid rather than the view's grid. After passing in the absolutePosition of {106.667, y}, we do the following calculations: absoluteLeft = round(absolutePosition.x + viewPosition.left) = round(106.667 + 0) = 106.5 absoluteRight = round(absolutePosition.x + viewPosition.left + viewSize.width) + round(106.667 + 0 + 106.667) = 213.5 width = 213.5 - 106.5 = 107 You'll notice that this is the same width we calculated for the parent view because we've taken its position into account. I believe this is awesome. I also believe that we have to decouple this logic from RN and put it into awesome Yoga. So I did it in this diff. **Fun fact:** The original implementation of this algorithm in RN had (and still have) a bug, which was found by Dustin dshahidehpour and fixed in D4133643. Therefore that diff was unlanded because it broke something unrelated inside RN text engine. I will fix that problem in RN later. **Why do we need to change test methodology?** Because the way we receive layout metrics from Chrome browser actually directly related to rounding problem. Previously we used `offsetHeight` and `offsetWidth` properties of the DOM node, which contain naively rounded values from `computedStyle` or `getBoundingClientRect`. (Which is we are trying to fix!) So, I added the new function that computes node size using two-step-rounding approach, conceptually similar to one that implemented in Yoga. Note: Chrome browser performs rounding layout as part of rendering process and actual values that can ve computed by counting actual pixel are different from these natively rounded ones. **Why do some tests now have different desired values?** These changes actually prove that my approach is correct and more useful for actual view rendering goals. So, let's take a look at test with changed values `rounding_fractial_input_3`: Previously: 64+25+24=114 (Incorrect!) Now: 65+24+25=114 (Correct!) Previously: 64+25+24=114 (Incorrect!) Now: 65+24+25=114 (Correct!) Reviewed By: emilsjolander Differential Revision: D4941266 fbshipit-source-id: 07500f5cc93c628219500e9e07291438e9d5d36c
2017-04-25 17:29:27 -07:00
function getRoundedSize(node) {
const boundingRect = node.getBoundingClientRect();
New round-to-pixel-grid algorithm that fixes possible subpixel gaps between sibling nodes Summary: This diff introduces new, little bit sophisticated round-to-pixel-grid algorithm. **Motivation:** Previous simple and straightforward solution works in most cases but sometimes produce the not-so-great result. A while ago Nick Lockwood described this problem and proposed the solution in RN's RCTShadowView class: For example, say you have the following structure: // +--------+---------+--------+ // | |+-------+| | // | || || | // | |+-------+| | // +--------+---------+--------+ Say the screen width is 320 pts so the three big views will get the following x bounds from our layout system: {0, 106.667}, {106.667, 213.333}, {213.333, 320} Assuming screen scale is 2, these numbers must be rounded to the nearest 0.5 to fit the pixel grid: {0, 106.5}, {106.5, 213.5}, {213.5, 320} You'll notice that the three widths are 106.5, 107, 106.5. This is great for the parent views but it gets trickier when we consider rounding for the subview. When we go to round the bounds for the subview in the middle, it's relative bounds are {0, 106.667} which gets rounded to {0, 106.5}. This will cause the subview to be one pixel smaller than it should be. This is why we need to pass in the absolute position in order to do the rounding relative to the screen's grid rather than the view's grid. After passing in the absolutePosition of {106.667, y}, we do the following calculations: absoluteLeft = round(absolutePosition.x + viewPosition.left) = round(106.667 + 0) = 106.5 absoluteRight = round(absolutePosition.x + viewPosition.left + viewSize.width) + round(106.667 + 0 + 106.667) = 213.5 width = 213.5 - 106.5 = 107 You'll notice that this is the same width we calculated for the parent view because we've taken its position into account. I believe this is awesome. I also believe that we have to decouple this logic from RN and put it into awesome Yoga. So I did it in this diff. **Fun fact:** The original implementation of this algorithm in RN had (and still have) a bug, which was found by Dustin dshahidehpour and fixed in D4133643. Therefore that diff was unlanded because it broke something unrelated inside RN text engine. I will fix that problem in RN later. **Why do we need to change test methodology?** Because the way we receive layout metrics from Chrome browser actually directly related to rounding problem. Previously we used `offsetHeight` and `offsetWidth` properties of the DOM node, which contain naively rounded values from `computedStyle` or `getBoundingClientRect`. (Which is we are trying to fix!) So, I added the new function that computes node size using two-step-rounding approach, conceptually similar to one that implemented in Yoga. Note: Chrome browser performs rounding layout as part of rendering process and actual values that can ve computed by counting actual pixel are different from these natively rounded ones. **Why do some tests now have different desired values?** These changes actually prove that my approach is correct and more useful for actual view rendering goals. So, let's take a look at test with changed values `rounding_fractial_input_3`: Previously: 64+25+24=114 (Incorrect!) Now: 65+24+25=114 (Correct!) Previously: 64+25+24=114 (Incorrect!) Now: 65+24+25=114 (Correct!) Reviewed By: emilsjolander Differential Revision: D4941266 fbshipit-source-id: 07500f5cc93c628219500e9e07291438e9d5d36c
2017-04-25 17:29:27 -07:00
return {
width: Math.round(boundingRect.right) - Math.round(boundingRect.left),
height: Math.round(boundingRect.bottom) - Math.round(boundingRect.top),
New round-to-pixel-grid algorithm that fixes possible subpixel gaps between sibling nodes Summary: This diff introduces new, little bit sophisticated round-to-pixel-grid algorithm. **Motivation:** Previous simple and straightforward solution works in most cases but sometimes produce the not-so-great result. A while ago Nick Lockwood described this problem and proposed the solution in RN's RCTShadowView class: For example, say you have the following structure: // +--------+---------+--------+ // | |+-------+| | // | || || | // | |+-------+| | // +--------+---------+--------+ Say the screen width is 320 pts so the three big views will get the following x bounds from our layout system: {0, 106.667}, {106.667, 213.333}, {213.333, 320} Assuming screen scale is 2, these numbers must be rounded to the nearest 0.5 to fit the pixel grid: {0, 106.5}, {106.5, 213.5}, {213.5, 320} You'll notice that the three widths are 106.5, 107, 106.5. This is great for the parent views but it gets trickier when we consider rounding for the subview. When we go to round the bounds for the subview in the middle, it's relative bounds are {0, 106.667} which gets rounded to {0, 106.5}. This will cause the subview to be one pixel smaller than it should be. This is why we need to pass in the absolute position in order to do the rounding relative to the screen's grid rather than the view's grid. After passing in the absolutePosition of {106.667, y}, we do the following calculations: absoluteLeft = round(absolutePosition.x + viewPosition.left) = round(106.667 + 0) = 106.5 absoluteRight = round(absolutePosition.x + viewPosition.left + viewSize.width) + round(106.667 + 0 + 106.667) = 213.5 width = 213.5 - 106.5 = 107 You'll notice that this is the same width we calculated for the parent view because we've taken its position into account. I believe this is awesome. I also believe that we have to decouple this logic from RN and put it into awesome Yoga. So I did it in this diff. **Fun fact:** The original implementation of this algorithm in RN had (and still have) a bug, which was found by Dustin dshahidehpour and fixed in D4133643. Therefore that diff was unlanded because it broke something unrelated inside RN text engine. I will fix that problem in RN later. **Why do we need to change test methodology?** Because the way we receive layout metrics from Chrome browser actually directly related to rounding problem. Previously we used `offsetHeight` and `offsetWidth` properties of the DOM node, which contain naively rounded values from `computedStyle` or `getBoundingClientRect`. (Which is we are trying to fix!) So, I added the new function that computes node size using two-step-rounding approach, conceptually similar to one that implemented in Yoga. Note: Chrome browser performs rounding layout as part of rendering process and actual values that can ve computed by counting actual pixel are different from these natively rounded ones. **Why do some tests now have different desired values?** These changes actually prove that my approach is correct and more useful for actual view rendering goals. So, let's take a look at test with changed values `rounding_fractial_input_3`: Previously: 64+25+24=114 (Incorrect!) Now: 65+24+25=114 (Correct!) Previously: 64+25+24=114 (Incorrect!) Now: 65+24+25=114 (Correct!) Reviewed By: emilsjolander Differential Revision: D4941266 fbshipit-source-id: 07500f5cc93c628219500e9e07291438e9d5d36c
2017-04-25 17:29:27 -07:00
};
}
function calculateTree(root, roundToPixelGrid) {
const rootLayout = [];
for (let i = 0; i < root.children.length; i++) {
const child = root.children[i];
const layout = {
name: child.id !== '' ? child.id : 'INSERT_NAME_HERE',
left: child.offsetLeft + child.parentNode.clientLeft,
top: child.offsetTop + child.parentNode.clientTop,
width: child.offsetWidth,
height: child.offsetHeight,
New round-to-pixel-grid algorithm that fixes possible subpixel gaps between sibling nodes Summary: This diff introduces new, little bit sophisticated round-to-pixel-grid algorithm. **Motivation:** Previous simple and straightforward solution works in most cases but sometimes produce the not-so-great result. A while ago Nick Lockwood described this problem and proposed the solution in RN's RCTShadowView class: For example, say you have the following structure: // +--------+---------+--------+ // | |+-------+| | // | || || | // | |+-------+| | // +--------+---------+--------+ Say the screen width is 320 pts so the three big views will get the following x bounds from our layout system: {0, 106.667}, {106.667, 213.333}, {213.333, 320} Assuming screen scale is 2, these numbers must be rounded to the nearest 0.5 to fit the pixel grid: {0, 106.5}, {106.5, 213.5}, {213.5, 320} You'll notice that the three widths are 106.5, 107, 106.5. This is great for the parent views but it gets trickier when we consider rounding for the subview. When we go to round the bounds for the subview in the middle, it's relative bounds are {0, 106.667} which gets rounded to {0, 106.5}. This will cause the subview to be one pixel smaller than it should be. This is why we need to pass in the absolute position in order to do the rounding relative to the screen's grid rather than the view's grid. After passing in the absolutePosition of {106.667, y}, we do the following calculations: absoluteLeft = round(absolutePosition.x + viewPosition.left) = round(106.667 + 0) = 106.5 absoluteRight = round(absolutePosition.x + viewPosition.left + viewSize.width) + round(106.667 + 0 + 106.667) = 213.5 width = 213.5 - 106.5 = 107 You'll notice that this is the same width we calculated for the parent view because we've taken its position into account. I believe this is awesome. I also believe that we have to decouple this logic from RN and put it into awesome Yoga. So I did it in this diff. **Fun fact:** The original implementation of this algorithm in RN had (and still have) a bug, which was found by Dustin dshahidehpour and fixed in D4133643. Therefore that diff was unlanded because it broke something unrelated inside RN text engine. I will fix that problem in RN later. **Why do we need to change test methodology?** Because the way we receive layout metrics from Chrome browser actually directly related to rounding problem. Previously we used `offsetHeight` and `offsetWidth` properties of the DOM node, which contain naively rounded values from `computedStyle` or `getBoundingClientRect`. (Which is we are trying to fix!) So, I added the new function that computes node size using two-step-rounding approach, conceptually similar to one that implemented in Yoga. Note: Chrome browser performs rounding layout as part of rendering process and actual values that can ve computed by counting actual pixel are different from these natively rounded ones. **Why do some tests now have different desired values?** These changes actually prove that my approach is correct and more useful for actual view rendering goals. So, let's take a look at test with changed values `rounding_fractial_input_3`: Previously: 64+25+24=114 (Incorrect!) Now: 65+24+25=114 (Correct!) Previously: 64+25+24=114 (Incorrect!) Now: 65+24+25=114 (Correct!) Reviewed By: emilsjolander Differential Revision: D4941266 fbshipit-source-id: 07500f5cc93c628219500e9e07291438e9d5d36c
2017-04-25 17:29:27 -07:00
children: calculateTree(child, roundToPixelGrid),
style: getYogaStyle(child),
declaredStyle: child.style,
rawStyle: child.getAttribute('style'),
experiments: child.dataset.experiments
? child.dataset.experiments.split(' ')
: DEFAULT_EXPERIMENTS,
disabled: child.dataset.disabled === 'true',
New round-to-pixel-grid algorithm that fixes possible subpixel gaps between sibling nodes Summary: This diff introduces new, little bit sophisticated round-to-pixel-grid algorithm. **Motivation:** Previous simple and straightforward solution works in most cases but sometimes produce the not-so-great result. A while ago Nick Lockwood described this problem and proposed the solution in RN's RCTShadowView class: For example, say you have the following structure: // +--------+---------+--------+ // | |+-------+| | // | || || | // | |+-------+| | // +--------+---------+--------+ Say the screen width is 320 pts so the three big views will get the following x bounds from our layout system: {0, 106.667}, {106.667, 213.333}, {213.333, 320} Assuming screen scale is 2, these numbers must be rounded to the nearest 0.5 to fit the pixel grid: {0, 106.5}, {106.5, 213.5}, {213.5, 320} You'll notice that the three widths are 106.5, 107, 106.5. This is great for the parent views but it gets trickier when we consider rounding for the subview. When we go to round the bounds for the subview in the middle, it's relative bounds are {0, 106.667} which gets rounded to {0, 106.5}. This will cause the subview to be one pixel smaller than it should be. This is why we need to pass in the absolute position in order to do the rounding relative to the screen's grid rather than the view's grid. After passing in the absolutePosition of {106.667, y}, we do the following calculations: absoluteLeft = round(absolutePosition.x + viewPosition.left) = round(106.667 + 0) = 106.5 absoluteRight = round(absolutePosition.x + viewPosition.left + viewSize.width) + round(106.667 + 0 + 106.667) = 213.5 width = 213.5 - 106.5 = 107 You'll notice that this is the same width we calculated for the parent view because we've taken its position into account. I believe this is awesome. I also believe that we have to decouple this logic from RN and put it into awesome Yoga. So I did it in this diff. **Fun fact:** The original implementation of this algorithm in RN had (and still have) a bug, which was found by Dustin dshahidehpour and fixed in D4133643. Therefore that diff was unlanded because it broke something unrelated inside RN text engine. I will fix that problem in RN later. **Why do we need to change test methodology?** Because the way we receive layout metrics from Chrome browser actually directly related to rounding problem. Previously we used `offsetHeight` and `offsetWidth` properties of the DOM node, which contain naively rounded values from `computedStyle` or `getBoundingClientRect`. (Which is we are trying to fix!) So, I added the new function that computes node size using two-step-rounding approach, conceptually similar to one that implemented in Yoga. Note: Chrome browser performs rounding layout as part of rendering process and actual values that can ve computed by counting actual pixel are different from these natively rounded ones. **Why do some tests now have different desired values?** These changes actually prove that my approach is correct and more useful for actual view rendering goals. So, let's take a look at test with changed values `rounding_fractial_input_3`: Previously: 64+25+24=114 (Incorrect!) Now: 65+24+25=114 (Correct!) Previously: 64+25+24=114 (Incorrect!) Now: 65+24+25=114 (Correct!) Reviewed By: emilsjolander Differential Revision: D4941266 fbshipit-source-id: 07500f5cc93c628219500e9e07291438e9d5d36c
2017-04-25 17:29:27 -07:00
};
const size = getRoundedSize(child);
layout.width = size.width;
layout.height = size.height;
New round-to-pixel-grid algorithm that fixes possible subpixel gaps between sibling nodes Summary: This diff introduces new, little bit sophisticated round-to-pixel-grid algorithm. **Motivation:** Previous simple and straightforward solution works in most cases but sometimes produce the not-so-great result. A while ago Nick Lockwood described this problem and proposed the solution in RN's RCTShadowView class: For example, say you have the following structure: // +--------+---------+--------+ // | |+-------+| | // | || || | // | |+-------+| | // +--------+---------+--------+ Say the screen width is 320 pts so the three big views will get the following x bounds from our layout system: {0, 106.667}, {106.667, 213.333}, {213.333, 320} Assuming screen scale is 2, these numbers must be rounded to the nearest 0.5 to fit the pixel grid: {0, 106.5}, {106.5, 213.5}, {213.5, 320} You'll notice that the three widths are 106.5, 107, 106.5. This is great for the parent views but it gets trickier when we consider rounding for the subview. When we go to round the bounds for the subview in the middle, it's relative bounds are {0, 106.667} which gets rounded to {0, 106.5}. This will cause the subview to be one pixel smaller than it should be. This is why we need to pass in the absolute position in order to do the rounding relative to the screen's grid rather than the view's grid. After passing in the absolutePosition of {106.667, y}, we do the following calculations: absoluteLeft = round(absolutePosition.x + viewPosition.left) = round(106.667 + 0) = 106.5 absoluteRight = round(absolutePosition.x + viewPosition.left + viewSize.width) + round(106.667 + 0 + 106.667) = 213.5 width = 213.5 - 106.5 = 107 You'll notice that this is the same width we calculated for the parent view because we've taken its position into account. I believe this is awesome. I also believe that we have to decouple this logic from RN and put it into awesome Yoga. So I did it in this diff. **Fun fact:** The original implementation of this algorithm in RN had (and still have) a bug, which was found by Dustin dshahidehpour and fixed in D4133643. Therefore that diff was unlanded because it broke something unrelated inside RN text engine. I will fix that problem in RN later. **Why do we need to change test methodology?** Because the way we receive layout metrics from Chrome browser actually directly related to rounding problem. Previously we used `offsetHeight` and `offsetWidth` properties of the DOM node, which contain naively rounded values from `computedStyle` or `getBoundingClientRect`. (Which is we are trying to fix!) So, I added the new function that computes node size using two-step-rounding approach, conceptually similar to one that implemented in Yoga. Note: Chrome browser performs rounding layout as part of rendering process and actual values that can ve computed by counting actual pixel are different from these natively rounded ones. **Why do some tests now have different desired values?** These changes actually prove that my approach is correct and more useful for actual view rendering goals. So, let's take a look at test with changed values `rounding_fractial_input_3`: Previously: 64+25+24=114 (Incorrect!) Now: 65+24+25=114 (Correct!) Previously: 64+25+24=114 (Incorrect!) Now: 65+24+25=114 (Correct!) Reviewed By: emilsjolander Differential Revision: D4941266 fbshipit-source-id: 07500f5cc93c628219500e9e07291438e9d5d36c
2017-04-25 17:29:27 -07:00
rootLayout.push(layout);
}
return rootLayout;
}
function getYogaStyle(node) {
// TODO: Relying on computed style means we cannot test shorthand props like
// "padding", "margin", "gap", or negative values.
return [
'direction',
'flex-direction',
'justify-content',
'align-content',
'align-items',
'align-self',
'position',
'flex-wrap',
'overflow',
'flex-grow',
'flex-shrink',
'flex-basis',
'left',
'top',
'right',
'bottom',
'margin-left',
'margin-top',
'margin-right',
'margin-bottom',
'padding-left',
'padding-top',
'padding-right',
'padding-bottom',
'border-left-width',
'border-top-width',
'border-right-width',
'border-bottom-width',
'width',
'min-width',
'max-width',
'height',
'min-height',
'max-height',
'column-gap',
'row-gap',
'display',
].reduce((map, key) => {
map[key] =
node.style[key] || getComputedStyle(node, null).getPropertyValue(key);
return map;
}, {});
}
const Emitter = function (lang, indent) {
this.lang = lang;
this.indent = indent;
this.indents = [];
this.lines = [];
};
Emitter.prototype = Object.create(Object.prototype, {
constructor: {value: Emitter},
pushIndent: {
value: function () {
this.indents.push(this.indent);
},
},
popIndent: {
value: function () {
this.indents.pop();
},
},
push: {
value: function (line) {
if (line instanceof Array) {
line.forEach(function (element) {
this.push(element);
}, this);
return;
} else if (line.length > 0) {
line = this.indents.join('') + line;
}
this.lines.push(line);
},
},
print: {
value: function () {
console.log(this.lines.join('\n'));
},
},
});