This commit is contained in:
Andy Street
2014-09-18 15:15:21 -07:00
parent 7eef01f299
commit 6d93c20610
33 changed files with 5457 additions and 14 deletions

3
.gitignore vendored
View File

@@ -1 +1,4 @@
a.out
*.class
/**/java/out/*
/**/.idea/workspace.xml

View File

@@ -1,15 +1,26 @@
FILES=src/__tests__/Layout-test.c src/Layout.c src/Layout-test-utils.c
all: c test
all: c c_test java java_test
c:
@node ./src/transpile.js
c: transpile_all
test:
c_test: c
@gcc -std=c99 -Werror -Wno-padded $(FILES) -lm && ./a.out
@rm a.out
java: transpile_all src/java
@javac -cp ./lib/junit4.jar -sourcepath ./src/java/src:./src/java/tests src/java/tests/com/facebook/csslayout/*.java
java_test: java
@java -cp ./src/java/src:./src/java/tests:./lib/junit4.jar org.junit.runner.JUnitCore \
com.facebook.csslayout.LayoutEngineTest \
com.facebook.csslayout.LayoutCachingTest \
com.facebook.csslayout.CSSNodeTest
transpile_all: ./src/transpile.js
@node ./src/transpile.js
debug:
@gcc -ggdb $(FILES) -lm && lldb ./a.out
@rm a.out

BIN
lib/junit4.jar Normal file

Binary file not shown.

110
src/JavaTranspiler.js Normal file
View File

@@ -0,0 +1,110 @@
function __transpileToJavaCommon(code) {
return code
.replace(/CSS_UNDEFINED/g, 'CSSConstants.UNDEFINED')
.replace(/css_flex_direction_t/g, 'CSSFlexDirection')
.replace(/CSS_FLEX_DIRECTION_/g, 'CSSFlexDirection.')
.replace(/css_align_t/g, 'CSSAlign')
.replace(/CSS_ALIGN_/g, 'CSSAlign.')
.replace(/CSS_POSITION_/g, 'CSSPositionType.')
.replace(/css_justify_t/g, 'CSSJustify')
.replace(/CSS_JUSTIFY_/g, 'CSSJustify.')
.replace(/css_dim_t/g, 'MeasureOutput')
.replace(/bool/g, 'boolean')
.replace(/^(\s+)([^\s]+)\s+\+=/gm, '$1$2 = $2 +') // Expand +=
.replace(/leading\[([^\]]+)\]/g, 'getLeading($1)')
.replace(/trailing\[([^\]]+)\]/g, 'getTrailing($1)')
.replace(/pos\[([^\]]+)\]/g, 'getPos($1)')
.replace(/dim\[([^\]]+)\]/g, 'getDim($1)')
.replace(/isUndefined/g, 'FloatUtil.isUndefined')
// Since Java doesn't store its attributes in arrays, we need to use setters/getters to access
// the appropriate layout/style fields
.replace(
/(\w+)\.layout\[((?:getLeading|getPos)\([^\)]+\))\]\s+=\s+([^;]+);/gm,
'setLayoutPosition($1, $2, $3);')
.replace(/(\w+)\.layout\[((?:getLeading|getPos)\([^\]]+\))\]/g, 'getLayoutPosition($1, $2)')
.replace(
/(\w+)\.layout\[(getDim\([^\)]+\))\]\s+=\s+([^;]+);/gm,
'setLayoutDimension($1, $2, $3);')
.replace(/(\w+)\.layout\[(getDim\([^\]]+\))\]/g, 'getLayoutDimension($1, $2)')
.replace(/(\w+)\.style\[((?:getLeading|getPos)\([^\]]+\))\]/g, 'getStylePosition($1, $2)')
.replace(/(\w+)\.style\[(getDim\([^\]]+\))\]/g, 'getStyleDimension($1, $2)');
}
function __transpileSingleTestToJava(code) {
return __transpileToJavaCommon(code)
.replace(/new_test_css_node/g, 'new TestCSSNode')
.replace( // style.dimensions[CSS_WIDTH] => style.width
/(style|layout)\.dimensions\[CSS_(WIDTH|HEIGHT)\]/g,
function (str, match1, match2) {
return match1 + '.' + match2.toLowerCase();
})
.replace( // layout.position[CSS_TOP] => layout.y
/layout\.position\[CSS_(TOP|LEFT)\]/g,
function (str, match1) {
return 'layout.' + (match1 == 'TOP' ? 'y' : 'x');
})
.replace( // style.position[CSS_TOP] => style.positionTop
/style\.(position|margin|border|padding)\[CSS_(TOP|BOTTOM|LEFT|RIGHT)\]/g,
function (str, match1, match2) {
return 'style.' + match1 + match2[0] + match2.substring(1).toLowerCase();
})
.replace(/get_child\(.*context\,\s([^\)]+)\)/g, 'getChildAt($1)')
.replace(/init_css_node_children/g, 'addChildren')
.replace(/css_node_t(\s)\*/g, 'TestCSSNode$1')
.replace(/\->/g, '.')
.replace(/(\d+\.\d+)/g, '$1f')
.replace( // style.flex_direction => style.flexDirection
/style\.([^_\s]+)_(\w)(\w+)/g,
function (str, match1, match2, match3) {
return 'style.' + match1 + match2.toUpperCase() + match3;
})
.replace(/(\w+)\.measure\s+=\s+.+/, '$1.setMeasureFunction(sTestMeasureFunction);');
}
function indent(code) {
return code
.split('\n')
.map(function(line) { return ' ' + line; })
.join('\n');
}
var JavaTranspiler = {
transpileLayoutEngine: function(code) {
return indent(
__transpileToJavaCommon(code)
.replace(/function\s+layoutNode.*/, '')
.replace('node.style.measure', 'node.measure')
.replace(/\.children\.length/g, '.getChildCount()')
.replace(/node.children\[i\]/g, 'node.getChildAt(i)')
.replace(/fmaxf/g, 'Math.max')
.replace(/\/\*\([^\/]+\*\/\n/g, '') // remove comments for other languages
.replace(/var\/\*([^\/]+)\*\//g, '$1')
.replace(/ === /g, ' == ')
.replace(/\n /g, '\n')
.replace(/\/[*]!([^*]+)[*]\//g, '$1')
.replace(/css_node_t\*/g, 'CSSNode'));
},
transpileCConstDefs: function(cConstDefs) {
return indent(
cConstDefs
.replace(/#define\s+(\w+)\s+(\"[^\"]+\")/g, 'public static final String $1 = $2;')
.replace(/#define\s+(\w+)\s+(.+)/g, 'public static final float $1 = $2f;'));
},
transpileCTestsArray: function(allTestsInC) {
var allTestsInJava = [];
for (var i = 0; i < allTestsInC.length; i++) {
allTestsInJava[i] =
" @Test\n" +
" public void testCase" + i + "()\n" +
__transpileSingleTestToJava(allTestsInC[i]);
}
return allTestsInJava.join('\n\n');
},
}
if (typeof module !== 'undefined') {
module.exports = JavaTranspiler;
}

View File

@@ -398,7 +398,7 @@ static void layoutNodeImpl(css_node_t *node, float parentMaxWidth) {
// Pre-fill dimensions when using absolute position and both offsets for the axis are defined (either both
// left and right or top and bottom).
for (int ii = 0; ii < 2; ii++) {
css_flex_direction_t axis = ii ? CSS_FLEX_DIRECTION_ROW : CSS_FLEX_DIRECTION_COLUMN;
css_flex_direction_t axis = (ii != 0) ? CSS_FLEX_DIRECTION_ROW : CSS_FLEX_DIRECTION_COLUMN;
if (!isUndefined(node->layout.dimensions[dim[axis]]) &&
!isDimDefined(child, axis) &&
isPosDefined(child, leading[axis]) &&
@@ -491,7 +491,7 @@ static void layoutNodeImpl(css_node_t *node, float parentMaxWidth) {
// If there are flexible children in the mix, they are going to fill the
// remaining space
if (flexibleChildrenCount) {
if (flexibleChildrenCount != 0) {
float flexibleMainDim = remainingMainDim / totalFlexible;
// The non flexible children can overflow the container, in this case
@@ -675,7 +675,7 @@ static void layoutNodeImpl(css_node_t *node, float parentMaxWidth) {
// Pre-fill dimensions when using absolute position and both offsets for the axis are defined (either both
// left and right or top and bottom).
for (int ii = 0; ii < 2; ii++) {
css_flex_direction_t axis = ii ? CSS_FLEX_DIRECTION_ROW : CSS_FLEX_DIRECTION_COLUMN;
css_flex_direction_t axis = (ii != 0) ? CSS_FLEX_DIRECTION_ROW : CSS_FLEX_DIRECTION_COLUMN;
if (!isUndefined(node->layout.dimensions[dim[axis]]) &&
!isDimDefined(child, axis) &&
isPosDefined(child, leading[axis]) &&
@@ -692,7 +692,7 @@ static void layoutNodeImpl(css_node_t *node, float parentMaxWidth) {
}
}
for (int ii = 0; ii < 2; ii++) {
css_flex_direction_t axis = ii ? CSS_FLEX_DIRECTION_ROW : CSS_FLEX_DIRECTION_COLUMN;
css_flex_direction_t axis = (ii != 0) ? CSS_FLEX_DIRECTION_ROW : CSS_FLEX_DIRECTION_COLUMN;
if (isPosDefined(child, trailing[axis]) &&
!isPosDefined(child, leading[axis])) {
child->layout.position[leading[axis]] =

View File

@@ -237,7 +237,7 @@ var computeLayout = (function() {
// Let's not measure the text if we already know both dimensions
if (isRowUndefined || isColumnUndefined) {
var/*css_dim_t*/ measure_dim = node.style.measure(
/*!node->context,*/
/*(c)!node->context,*/
width
);
if (isRowUndefined) {
@@ -273,7 +273,7 @@ var computeLayout = (function() {
// Pre-fill dimensions when using absolute position and both offsets for the axis are defined (either both
// left and right or top and bottom).
for (var/*int*/ ii = 0; ii < 2; ii++) {
var/*css_flex_direction_t*/ axis = ii ? CSS_FLEX_DIRECTION_ROW : CSS_FLEX_DIRECTION_COLUMN;
var/*css_flex_direction_t*/ axis = (ii != 0) ? CSS_FLEX_DIRECTION_ROW : CSS_FLEX_DIRECTION_COLUMN;
if (!isUndefined(node.layout[dim[axis]]) &&
!isDimDefined(child, axis) &&
isPosDefined(child, leading[axis]) &&
@@ -366,7 +366,7 @@ var computeLayout = (function() {
// If there are flexible children in the mix, they are going to fill the
// remaining space
if (flexibleChildrenCount) {
if (flexibleChildrenCount != 0) {
var/*float*/ flexibleMainDim = remainingMainDim / totalFlexible;
// The non flexible children can overflow the container, in this case
@@ -550,7 +550,7 @@ var computeLayout = (function() {
// Pre-fill dimensions when using absolute position and both offsets for the axis are defined (either both
// left and right or top and bottom).
for (var/*int*/ ii = 0; ii < 2; ii++) {
var/*css_flex_direction_t*/ axis = ii ? CSS_FLEX_DIRECTION_ROW : CSS_FLEX_DIRECTION_COLUMN;
var/*css_flex_direction_t*/ axis = (ii != 0) ? CSS_FLEX_DIRECTION_ROW : CSS_FLEX_DIRECTION_COLUMN;
if (!isUndefined(node.layout[dim[axis]]) &&
!isDimDefined(child, axis) &&
isPosDefined(child, leading[axis]) &&
@@ -567,7 +567,7 @@ var computeLayout = (function() {
}
}
for (var/*int*/ ii = 0; ii < 2; ii++) {
var/*css_flex_direction_t*/ axis = ii ? CSS_FLEX_DIRECTION_ROW : CSS_FLEX_DIRECTION_COLUMN;
var/*css_flex_direction_t*/ axis = (ii != 0) ? CSS_FLEX_DIRECTION_ROW : CSS_FLEX_DIRECTION_COLUMN;
if (isPosDefined(child, trailing[axis]) &&
!isPosDefined(child, leading[axis])) {
child.layout[leading[axis]] =

1
src/java/.idea/.name generated Normal file
View File

@@ -0,0 +1 @@
css-layout

23
src/java/.idea/compiler.xml generated Normal file
View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<option name="DEFAULT_COMPILER" value="Javac" />
<resourceExtensions />
<wildcardResourcePatterns>
<entry name="!?*.java" />
<entry name="!?*.form" />
<entry name="!?*.class" />
<entry name="!?*.groovy" />
<entry name="!?*.scala" />
<entry name="!?*.flex" />
<entry name="!?*.kt" />
<entry name="!?*.clj" />
</wildcardResourcePatterns>
<annotationProcessing>
<profile default="true" name="Default" enabled="false">
<processorPath useClasspath="true" />
</profile>
</annotationProcessing>
</component>
</project>

View File

@@ -0,0 +1,5 @@
<component name="CopyrightManager">
<settings default="">
<module2copyright />
</settings>
</component>

5
src/java/.idea/encodings.xml generated Normal file
View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" useUTFGuessing="true" native2AsciiForPropertiesFiles="false" />
</project>

10
src/java/.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EntryPointsManager">
<entry_points version="2.0" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_6" assert-keyword="true" jdk-15="true" project-jdk-name="1.6" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

9
src/java/.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/java.iml" filepath="$PROJECT_DIR$/java.iml" />
</modules>
</component>
</project>

View File

@@ -0,0 +1,5 @@
<component name="DependencyValidationManager">
<state>
<option name="SKIP_IMPORT_STATEMENTS" value="false" />
</state>
</component>

125
src/java/.idea/uiDesigner.xml generated Normal file
View File

@@ -0,0 +1,125 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Palette2">
<group name="Swing">
<item class="com.intellij.uiDesigner.HSpacer" tooltip-text="Horizontal Spacer" icon="/com/intellij/uiDesigner/icons/hspacer.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="1" hsize-policy="6" anchor="0" fill="1" />
</item>
<item class="com.intellij.uiDesigner.VSpacer" tooltip-text="Vertical Spacer" icon="/com/intellij/uiDesigner/icons/vspacer.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="1" anchor="0" fill="2" />
</item>
<item class="javax.swing.JPanel" icon="/com/intellij/uiDesigner/icons/panel.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3" />
</item>
<item class="javax.swing.JScrollPane" icon="/com/intellij/uiDesigner/icons/scrollPane.png" removable="false" auto-create-binding="false" can-attach-label="true">
<default-constraints vsize-policy="7" hsize-policy="7" anchor="0" fill="3" />
</item>
<item class="javax.swing.JButton" icon="/com/intellij/uiDesigner/icons/button.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="0" fill="1" />
<initial-values>
<property name="text" value="Button" />
</initial-values>
</item>
<item class="javax.swing.JRadioButton" icon="/com/intellij/uiDesigner/icons/radioButton.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
<initial-values>
<property name="text" value="RadioButton" />
</initial-values>
</item>
<item class="javax.swing.JCheckBox" icon="/com/intellij/uiDesigner/icons/checkBox.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
<initial-values>
<property name="text" value="CheckBox" />
</initial-values>
</item>
<item class="javax.swing.JLabel" icon="/com/intellij/uiDesigner/icons/label.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="0" anchor="8" fill="0" />
<initial-values>
<property name="text" value="Label" />
</initial-values>
</item>
<item class="javax.swing.JTextField" icon="/com/intellij/uiDesigner/icons/textField.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JPasswordField" icon="/com/intellij/uiDesigner/icons/passwordField.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JFormattedTextField" icon="/com/intellij/uiDesigner/icons/formattedTextField.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JTextArea" icon="/com/intellij/uiDesigner/icons/textArea.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTextPane" icon="/com/intellij/uiDesigner/icons/textPane.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JEditorPane" icon="/com/intellij/uiDesigner/icons/editorPane.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JComboBox" icon="/com/intellij/uiDesigner/icons/comboBox.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="2" anchor="8" fill="1" />
</item>
<item class="javax.swing.JTable" icon="/com/intellij/uiDesigner/icons/table.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JList" icon="/com/intellij/uiDesigner/icons/list.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="2" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTree" icon="/com/intellij/uiDesigner/icons/tree.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTabbedPane" icon="/com/intellij/uiDesigner/icons/tabbedPane.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
<preferred-size width="200" height="200" />
</default-constraints>
</item>
<item class="javax.swing.JSplitPane" icon="/com/intellij/uiDesigner/icons/splitPane.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
<preferred-size width="200" height="200" />
</default-constraints>
</item>
<item class="javax.swing.JSpinner" icon="/com/intellij/uiDesigner/icons/spinner.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
</item>
<item class="javax.swing.JSlider" icon="/com/intellij/uiDesigner/icons/slider.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
</item>
<item class="javax.swing.JSeparator" icon="/com/intellij/uiDesigner/icons/separator.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3" />
</item>
<item class="javax.swing.JProgressBar" icon="/com/intellij/uiDesigner/icons/progressbar.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1" />
</item>
<item class="javax.swing.JToolBar" icon="/com/intellij/uiDesigner/icons/toolbar.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1">
<preferred-size width="-1" height="20" />
</default-constraints>
</item>
<item class="javax.swing.JToolBar$Separator" icon="/com/intellij/uiDesigner/icons/toolbarSeparator.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="0" anchor="0" fill="1" />
</item>
<item class="javax.swing.JScrollBar" icon="/com/intellij/uiDesigner/icons/scrollbar.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="0" anchor="0" fill="2" />
</item>
</group>
</component>
</project>

7
src/java/.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="" />
</component>
</project>

22
src/java/java.iml Normal file
View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="module-library">
<library>
<CLASSES>
<root url="jar://$APPLICATION_HOME_DIR$/lib/junit-4.10.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</orderEntry>
</component>
</module>

View File

@@ -0,0 +1,9 @@
package com.facebook.csslayout;
public enum CSSAlign {
AUTO,
FLEX_START,
CENTER,
FLEX_END,
STRETCH,
}

View File

@@ -0,0 +1,6 @@
package com.facebook.csslayout;
public class CSSConstants {
public static final float UNDEFINED = Float.NaN;
}

View File

@@ -0,0 +1,6 @@
package com.facebook.csslayout;
public enum CSSFlexDirection {
COLUMN,
ROW,
}

View File

@@ -0,0 +1,9 @@
package com.facebook.csslayout;
public enum CSSJustify {
FLEX_START,
CENTER,
FLEX_END,
SPACE_BETWEEN,
SPACE_AROUND,
}

View File

@@ -0,0 +1,39 @@
package com.facebook.csslayout;
/**
* Where the output of {@link LayoutEngine#layoutNode(CSSNode, float)} will go in the CSSNode.
*/
public class CSSLayout {
public float x;
public float y;
public float width = CSSConstants.UNDEFINED;
public float height = CSSConstants.UNDEFINED;
/**
* This should always get called before calling {@link LayoutEngine#layoutNode(CSSNode, float)}
*/
public void resetResult() {
x = 0;
y = 0;
width = CSSConstants.UNDEFINED;
height = CSSConstants.UNDEFINED;
}
public void copy(CSSLayout layout) {
x = layout.x;
y = layout.y;
width = layout.width;
height = layout.height;
}
@Override
public String toString() {
return "layout: {" +
"x: " + x + ", " +
"y: " + y + ", " +
"width: " + width + ", " +
"height: " + height +
"}";
}
}

View File

@@ -0,0 +1,338 @@
package com.facebook.csslayout;
import java.util.ArrayList;
/**
* A CSS Node. It has a style object you can manipulate at {@link #style}. After calling
* {@link #calculateLayout()}, {@link #layout} will be filled with the results of the layout.
*/
public class CSSNode {
private static enum LayoutState {
/**
* Some property of this node or its children has changes and the current values in
* {@link #layout} are not valid.
*/
DIRTY,
/**
* This node has a new layout relative to the last time {@link #markLayoutApplied()} was called.
*/
HAS_NEW_LAYOUT,
/**
* {@link #layout} is valid for the node's properties and this layout has been marked as
* having been applied.
*/
UP_TO_DATE,
}
// Only one copy kept around to keep from allocating a bunch of MeasureOutput objects
// NOT THREAD SAFE! NOT RE-ENTRANT SAFE!
private static final MeasureOutput MEASURE_OUTPUT = new MeasureOutput();
public static interface MeasureFunction {
/**
* Should measure the given node and put the result in the given MeasureOutput.
*
* NB: measure is NOT guaranteed to be threadsafe/re-entrant safe!
*/
public void measure(CSSNode node, float width, MeasureOutput measureOutput);
}
// VisibleForTesting
/*package*/ final CSSStyle style = new CSSStyle();
/*package*/ final CSSLayout layout = new CSSLayout();
/*package*/ final CachedCSSLayout lastLayout = new CachedCSSLayout();
// 4 is kinda arbitrary, but the default of 10 seems really high for an average View.
private final ArrayList<CSSNode> mChildren = new ArrayList<CSSNode>(4);
private CSSNode mParent;
private MeasureFunction mMeasureFunction = null;
private LayoutState mLayoutState = LayoutState.DIRTY;
public int getChildCount() {
return mChildren.size();
}
public CSSNode getChildAt(int i) {
return mChildren.get(i);
}
public void addChildAt(CSSNode child, int i) {
if (child.mParent != null) {
throw new IllegalStateException("Child already has a parent, it must be removed first.");
}
mChildren.add(i, child);
child.mParent = this;
dirty();
}
public void removeChildAt(int i) {
mChildren.remove(i).mParent = null;
dirty();
}
public CSSNode getParent() {
return mParent;
}
public void setMeasureFunction(MeasureFunction measureFunction) {
dirtyIfDifferent(mMeasureFunction, measureFunction);
mMeasureFunction = measureFunction;
}
public boolean isMeasureDefined() {
return mMeasureFunction != null;
}
/*package*/ MeasureOutput measure(float width) {
if (!isMeasureDefined()) {
throw new RuntimeException("Measure function isn't defined!");
}
MEASURE_OUTPUT.height = CSSConstants.UNDEFINED;
MEASURE_OUTPUT.width = CSSConstants.UNDEFINED;
mMeasureFunction.measure(this, width, MEASURE_OUTPUT);
return MEASURE_OUTPUT;
}
/**
* Performs the actual layout and saves the results in {@link #layout}
*/
public void calculateLayout() {
resetLayoutResultRecursive();
LayoutEngine.layoutNode(this, CSSConstants.UNDEFINED);
}
/**
* See {@link LayoutState#DIRTY}.
*/
/*package*/ boolean isDirty() {
return mLayoutState == LayoutState.DIRTY;
}
/**
* See {@link LayoutState#HAS_NEW_LAYOUT}.
*/
public boolean hasNewLayout() {
return mLayoutState == LayoutState.HAS_NEW_LAYOUT;
}
protected void dirty() {
if (mLayoutState == LayoutState.DIRTY) {
return;
} else if (mLayoutState == LayoutState.HAS_NEW_LAYOUT) {
throw new IllegalStateException(
"Previous layout was ignored! markLayoutApplied() never called");
}
mLayoutState = LayoutState.DIRTY;
if (mParent != null) {
mParent.dirty();
}
}
/*package*/ void markHasNewLayout() {
mLayoutState = LayoutState.HAS_NEW_LAYOUT;
}
/**
* Tells the node that the current values in {@link #layout} have been applied. Subsequent calls
* to {@link #hasNewLayout()} will return false until this node is laid out with new parameters.
* You must call this each time the layout is generated if the node has a new layout.
*/
public void markLayoutApplied() {
if (!hasNewLayout()) {
throw new IllegalStateException("Expected node to have a new layout to apply!");
}
mLayoutState = LayoutState.UP_TO_DATE;
}
@Override
public String toString() {
String layoutString = layout.toString();
if (getChildCount() == 0) {
return layoutString;
}
layoutString += ", children: [\n";
for (int i = 0; i < getChildCount(); i++) {
String childString = getChildAt(i).toString();
childString = childString.replaceAll("\n", "\n\t");
layoutString += "\t" + childString + "\n";
}
layoutString += "]";
return layoutString;
}
private void resetLayoutResultRecursive() {
layout.resetResult();
for (int i = 0; i < getChildCount(); i++) {
getChildAt(i).resetLayoutResultRecursive();
}
}
protected void dirtyIfDifferent(float f1, float f2) {
if (!FloatUtil.floatsEqual(f1, f2)) {
dirty();
}
}
private static <T> boolean objectsEqual(T o1, T o2) {
if (o1 == null) {
return o2 == null;
}
return o1.equals(o2);
}
protected <T> void dirtyIfDifferent(T o1, T o2) {
if (!objectsEqual(o1, o2)) {
dirty();
}
}
public void setFlexDirection(CSSFlexDirection flexDirection) {
dirtyIfDifferent(style.flexDirection, flexDirection);
style.flexDirection = flexDirection;
}
public void setJustifyContent(CSSJustify justifyContent) {
dirtyIfDifferent(style.justifyContent, justifyContent);
style.justifyContent = justifyContent;
}
public void setAlignItems(CSSAlign alignItems) {
dirtyIfDifferent(style.alignItems, alignItems);
style.alignItems = alignItems;
}
public void setAlignSelf(CSSAlign alignSelf) {
dirtyIfDifferent(style.alignSelf, alignSelf);
style.alignSelf = alignSelf;
}
public void setPositionType(CSSPositionType positionType) {
dirtyIfDifferent(style.positionType, positionType);
style.positionType = positionType;
}
public void setFlex(float flex) {
dirtyIfDifferent(style.flex, flex);
style.flex = flex;
}
public void setMarginTop(float marginTop) {
dirtyIfDifferent(style.marginTop, marginTop);
style.marginTop = marginTop;
}
public void setMarginBottom(float marginBottom) {
dirtyIfDifferent(style.marginBottom, marginBottom);
style.marginBottom = marginBottom;
}
public void setMarginLeft(float marginLeft) {
dirtyIfDifferent(style.marginLeft, marginLeft);
style.marginLeft = marginLeft;
}
public void setMarginRight(float marginRight) {
dirtyIfDifferent(style.marginRight, marginRight);
style.marginRight = marginRight;
}
public void setPaddingTop(float paddingTop) {
dirtyIfDifferent(style.paddingTop, paddingTop);
style.paddingTop = paddingTop;
}
public void setPaddingBottom(float paddingBottom) {
dirtyIfDifferent(style.paddingBottom, paddingBottom);
style.paddingBottom = paddingBottom;
}
public void setPaddingLeft(float paddingLeft) {
dirtyIfDifferent(style.paddingLeft, paddingLeft);
style.paddingLeft = paddingLeft;
}
public void setPaddingRight(float paddingRight) {
dirtyIfDifferent(style.paddingRight, paddingRight);
style.paddingRight = paddingRight;
}
public void setPositionTop(float positionTop) {
dirtyIfDifferent(style.positionTop, positionTop);
style.positionTop = positionTop;
}
public void setPositionBottom(float positionBottom) {
dirtyIfDifferent(style.positionBottom, positionBottom);
style.positionBottom = positionBottom;
}
public void setPositionLeft(float positionLeft) {
dirtyIfDifferent(style.positionLeft, positionLeft);
style.positionLeft = positionLeft;
}
public void setPositionRight(float positionRight) {
dirtyIfDifferent(style.positionRight, positionRight);
style.positionRight = positionRight;
}
public void setBorderTop(float borderTop) {
dirtyIfDifferent(style.borderTop, borderTop);
style.borderTop = borderTop;
}
public void setBorderBottom(float borderBottom) {
dirtyIfDifferent(style.borderBottom, borderBottom);
style.borderBottom = borderBottom;
}
public void setBorderLeft(float borderLeft) {
dirtyIfDifferent(style.borderLeft, borderLeft);
style.borderLeft = borderLeft;
}
public void setBorderRight(float borderRight) {
dirtyIfDifferent(style.borderRight, borderRight);
style.borderRight = borderRight;
}
public void setStyleWidth(float width) {
dirtyIfDifferent(style.width, width);
style.width = width;
}
public void setStyleHeight(float height) {
dirtyIfDifferent(style.height, height);
style.height = height;
}
public float getLayoutX() {
return layout.x;
}
public float getLayoutY() {
return layout.y;
}
public float getLayoutWidth() {
return layout.width;
}
public float getLayoutHeight() {
return layout.height;
}
}

View File

@@ -0,0 +1,6 @@
package com.facebook.csslayout;
public enum CSSPositionType {
RELATIVE,
ABSOLUTE,
}

View File

@@ -0,0 +1,37 @@
package com.facebook.csslayout;
/**
* The CSS style definition for a {@link CSSNode}.
*/
public class CSSStyle {
public CSSFlexDirection flexDirection = CSSFlexDirection.COLUMN;
public CSSJustify justifyContent = CSSJustify.FLEX_START;
public CSSAlign alignItems = CSSAlign.FLEX_START;
public CSSAlign alignSelf = CSSAlign.AUTO;
public CSSPositionType positionType = CSSPositionType.RELATIVE;
public float flex;
public float marginTop;
public float marginBottom;
public float marginLeft;
public float marginRight;
public float paddingTop;
public float paddingBottom;
public float paddingLeft;
public float paddingRight;
public float positionTop = CSSConstants.UNDEFINED;
public float positionBottom = CSSConstants.UNDEFINED;
public float positionLeft = CSSConstants.UNDEFINED;
public float positionRight = CSSConstants.UNDEFINED;
public float borderTop;
public float borderBottom;
public float borderLeft;
public float borderRight;
public float width = CSSConstants.UNDEFINED;
public float height = CSSConstants.UNDEFINED;
}

View File

@@ -0,0 +1,13 @@
package com.facebook.csslayout;
/**
* CSSLayout with additional information about the conditions under which it was generated.
* {@link #requestedWidth} and {@link #requestedHeight} are the width and height the parent set on
* this node before calling layout visited us.
*/
public class CachedCSSLayout extends CSSLayout {
public float requestedWidth = CSSConstants.UNDEFINED;
public float requestedHeight = CSSConstants.UNDEFINED;
public float parentMaxWidth = CSSConstants.UNDEFINED;
}

View File

@@ -0,0 +1,17 @@
// Copyright 2004-present Facebook. All Rights Reserved.
package com.facebook.csslayout;
public class FloatUtil {
public static boolean isUndefined(float f) {
return Float.compare(f, CSSConstants.UNDEFINED) == 0;
}
public static boolean floatsEqual(float f1, float f2) {
if (isUndefined(f1)) {
return isUndefined(f2);
}
return Math.abs(f2 - f1) < .00001;
}
}

View File

@@ -0,0 +1,650 @@
package com.facebook.csslayout;
/**
* Calculates layouts based on CSS style. See {@link #layoutNode(CSSNode, float)}.
*/
public class LayoutEngine {
private static enum PositionIndex {
TOP,
LEFT,
BOTTOM,
RIGHT,
}
private static enum DimensionIndex {
WIDTH,
HEIGHT,
}
private static void setLayoutPosition(CSSNode node, PositionIndex position, float value) {
switch (position) {
case TOP:
node.layout.y = value;
break;
case LEFT:
node.layout.x = value;
break;
default:
throw new RuntimeException("Didn't get TOP or LEFT!");
}
}
private static float getLayoutPosition(CSSNode node, PositionIndex position) {
switch (position) {
case TOP:
return node.layout.y;
case LEFT:
return node.layout.x;
default:
throw new RuntimeException("Didn't get TOP or LEFT!");
}
}
private static void setLayoutDimension(CSSNode node, DimensionIndex dimension, float value) {
switch (dimension) {
case WIDTH:
node.layout.width = value;
break;
case HEIGHT:
node.layout.height = value;
break;
default:
throw new RuntimeException("Someone added a third dimension...");
}
}
private static float getLayoutDimension(CSSNode node, DimensionIndex dimension) {
switch (dimension) {
case WIDTH:
return node.layout.width;
case HEIGHT:
return node.layout.height;
default:
throw new RuntimeException("Someone added a third dimension...");
}
}
private static float getStylePosition(CSSNode node, PositionIndex position) {
switch (position) {
case TOP:
return node.style.positionTop;
case BOTTOM:
return node.style.positionBottom;
case LEFT:
return node.style.positionLeft;
case RIGHT:
return node.style.positionRight;
default:
throw new RuntimeException("Someone added a new cardinal direction...");
}
}
private static float getStyleDimension(CSSNode node, DimensionIndex dimension) {
switch (dimension) {
case WIDTH:
return node.style.width;
case HEIGHT:
return node.style.height;
default:
throw new RuntimeException("Someone added a third dimension...");
}
}
private static PositionIndex getLeading(CSSFlexDirection axis) {
return axis == CSSFlexDirection.COLUMN ? PositionIndex.TOP : PositionIndex.LEFT;
}
private static PositionIndex getTrailing(CSSFlexDirection axis) {
return axis == CSSFlexDirection.COLUMN ? PositionIndex.BOTTOM : PositionIndex.RIGHT;
}
private static PositionIndex getPos(CSSFlexDirection axis) {
return axis == CSSFlexDirection.COLUMN ? PositionIndex.TOP : PositionIndex.LEFT;
}
private static DimensionIndex getDim(CSSFlexDirection axis) {
return axis == CSSFlexDirection.COLUMN ? DimensionIndex.HEIGHT : DimensionIndex.WIDTH;
}
private static boolean isDimDefined(CSSNode node, CSSFlexDirection axis) {
return !FloatUtil.isUndefined(getStyleDimension(node, getDim(axis)));
}
private static boolean isPosDefined(CSSNode node, PositionIndex position) {
return !FloatUtil.isUndefined(getStylePosition(node, position));
}
private static float getPosition(CSSNode node, PositionIndex position) {
float result = getStylePosition(node, position);
return FloatUtil.isUndefined(result) ? 0 : result;
}
private static float getMargin(CSSNode node, PositionIndex position) {
switch (position) {
case TOP:
return node.style.marginTop;
case BOTTOM:
return node.style.marginBottom;
case LEFT:
return node.style.marginLeft;
case RIGHT:
return node.style.marginRight;
default:
throw new RuntimeException("Someone added a new cardinal direction...");
}
}
private static float getPadding(CSSNode node, PositionIndex position) {
switch (position) {
case TOP:
return node.style.paddingTop;
case BOTTOM:
return node.style.paddingBottom;
case LEFT:
return node.style.paddingLeft;
case RIGHT:
return node.style.paddingRight;
default:
throw new RuntimeException("Someone added a new cardinal direction...");
}
}
private static float getBorder(CSSNode node, PositionIndex position) {
switch (position) {
case TOP:
return node.style.borderTop;
case BOTTOM:
return node.style.borderBottom;
case LEFT:
return node.style.borderLeft;
case RIGHT:
return node.style.borderRight;
default:
throw new RuntimeException("Someone added a new cardinal direction...");
}
}
private static float getPaddingAndBorder(CSSNode node, PositionIndex position) {
return getPadding(node, position) + getBorder(node, position);
}
private static float getMarginAxis(CSSNode node, CSSFlexDirection axis) {
return getMargin(node, getLeading(axis)) + getMargin(node, getTrailing(axis));
}
private static float getPaddingAndBorderAxis(CSSNode node, CSSFlexDirection axis) {
return getPaddingAndBorder(
node,
getLeading(axis)) + getPaddingAndBorder(node, getTrailing(axis));
}
private static void setDimensionFromStyle(CSSNode node, CSSFlexDirection axis) {
// The parent already computed us a width or height. We just skip it
if (!FloatUtil.isUndefined(getLayoutDimension(node, getDim(axis)))) {
return;
}
// We only run if there's a width or height defined
if (!isDimDefined(node, axis)) {
return;
}
// The dimensions can never be smaller than the padding and border
float maxLayoutDimension = Math.max(
getStyleDimension(node, getDim(axis)),
getPaddingAndBorderAxis(node, axis));
setLayoutDimension(node, getDim(axis), maxLayoutDimension);
}
private static float getRelativePosition(CSSNode node, CSSFlexDirection axis) {
float lead = getStylePosition(node, getLeading(axis));
if (!FloatUtil.isUndefined(lead)) {
return lead;
}
return -getPosition(node, getTrailing(axis));
}
private static float getFlex(CSSNode node) {
return node.style.flex;
}
private static CSSFlexDirection getFlexDirection(CSSNode node) {
return node.style.flexDirection;
}
private static CSSPositionType getPositionType(CSSNode node) {
return node.style.positionType;
}
private static CSSAlign getAlignItem(CSSNode node, CSSNode child) {
if (child.style.alignSelf != CSSAlign.AUTO) {
return child.style.alignSelf;
}
return node.style.alignItems;
}
private static CSSJustify getJustifyContent(CSSNode node) {
return node.style.justifyContent;
}
private static boolean isFlex(CSSNode node) {
return getPositionType(node) == CSSPositionType.RELATIVE && getFlex(node) > 0;
}
private static boolean isMeasureDefined(CSSNode node) {
return node.isMeasureDefined();
}
private static float getDimWithMargin(CSSNode node, CSSFlexDirection axis) {
return getLayoutDimension(node, getDim(axis)) +
getMargin(node, getLeading(axis)) +
getMargin(node, getTrailing(axis));
}
private static boolean needsRelayout(CSSNode node, float parentMaxWidth) {
return node.isDirty() ||
!FloatUtil.floatsEqual(node.lastLayout.requestedHeight, node.layout.height) ||
!FloatUtil.floatsEqual(node.lastLayout.requestedWidth, node.layout.width) ||
!FloatUtil.floatsEqual(node.lastLayout.parentMaxWidth, parentMaxWidth);
}
/*package*/ static void layoutNode(CSSNode node, float parentMaxWidth) {
if (needsRelayout(node, parentMaxWidth)) {
node.lastLayout.requestedWidth = node.layout.width;
node.lastLayout.requestedHeight = node.layout.height;
node.lastLayout.parentMaxWidth = parentMaxWidth;
layoutNodeImpl(node, parentMaxWidth);
node.markHasNewLayout();
node.lastLayout.copy(node.layout);
} else {
node.layout.copy(node.lastLayout);
}
}
private static void layoutNodeImpl(CSSNode node, float parentMaxWidth) {
/** START_GENERATED **/
CSSFlexDirection mainAxis = getFlexDirection(node);
CSSFlexDirection crossAxis = mainAxis == CSSFlexDirection.ROW ?
CSSFlexDirection.COLUMN :
CSSFlexDirection.ROW;
// Handle width and height style attributes
setDimensionFromStyle(node, mainAxis);
setDimensionFromStyle(node, crossAxis);
// The position is set by the parent, but we need to complete it with a
// delta composed of the margin and left/top/right/bottom
setLayoutPosition(node, getLeading(mainAxis), getLayoutPosition(node, getLeading(mainAxis)) + getMargin(node, getLeading(mainAxis)) +
getRelativePosition(node, mainAxis));
setLayoutPosition(node, getLeading(crossAxis), getLayoutPosition(node, getLeading(crossAxis)) + getMargin(node, getLeading(crossAxis)) +
getRelativePosition(node, crossAxis));
if (isMeasureDefined(node)) {
float width = CSSConstants.UNDEFINED;
if (isDimDefined(node, CSSFlexDirection.ROW)) {
width = node.style.width;
} else if (!FloatUtil.isUndefined(getLayoutDimension(node, getDim(CSSFlexDirection.ROW)))) {
width = getLayoutDimension(node, getDim(CSSFlexDirection.ROW));
} else {
width = parentMaxWidth -
getMarginAxis(node, CSSFlexDirection.ROW);
}
width -= getPaddingAndBorderAxis(node, CSSFlexDirection.ROW);
// We only need to give a dimension for the text if we haven't got any
// for it computed yet. It can either be from the style attribute or because
// the element is flexible.
boolean isRowUndefined = !isDimDefined(node, CSSFlexDirection.ROW) &&
FloatUtil.isUndefined(getLayoutDimension(node, getDim(CSSFlexDirection.ROW)));
boolean isColumnUndefined = !isDimDefined(node, CSSFlexDirection.COLUMN) &&
FloatUtil.isUndefined(getLayoutDimension(node, getDim(CSSFlexDirection.COLUMN)));
// Let's not measure the text if we already know both dimensions
if (isRowUndefined || isColumnUndefined) {
MeasureOutput measure_dim = node.measure(
width
);
if (isRowUndefined) {
node.layout.width = measure_dim.width +
getPaddingAndBorderAxis(node, CSSFlexDirection.ROW);
}
if (isColumnUndefined) {
node.layout.height = measure_dim.height +
getPaddingAndBorderAxis(node, CSSFlexDirection.COLUMN);
}
}
return;
}
// Pre-fill some dimensions straight from the parent
for (int i = 0; i < node.getChildCount(); ++i) {
CSSNode child = node.getChildAt(i);
// Pre-fill cross axis dimensions when the child is using stretch before
// we call the recursive layout pass
if (getAlignItem(node, child) == CSSAlign.STRETCH &&
getPositionType(child) == CSSPositionType.RELATIVE &&
!FloatUtil.isUndefined(getLayoutDimension(node, getDim(crossAxis))) &&
!isDimDefined(child, crossAxis) &&
!isPosDefined(child, getLeading(crossAxis))) {
setLayoutDimension(child, getDim(crossAxis), Math.max(
getLayoutDimension(node, getDim(crossAxis)) -
getPaddingAndBorderAxis(node, crossAxis) -
getMarginAxis(child, crossAxis),
// You never want to go smaller than padding
getPaddingAndBorderAxis(child, crossAxis)
));
} else if (getPositionType(child) == CSSPositionType.ABSOLUTE) {
// Pre-fill dimensions when using absolute position and both offsets for the axis are defined (either both
// left and right or top and bottom).
for (int ii = 0; ii < 2; ii++) {
CSSFlexDirection axis = (ii != 0) ? CSSFlexDirection.ROW : CSSFlexDirection.COLUMN;
if (!FloatUtil.isUndefined(getLayoutDimension(node, getDim(axis))) &&
!isDimDefined(child, axis) &&
isPosDefined(child, getLeading(axis)) &&
isPosDefined(child, getTrailing(axis))) {
setLayoutDimension(child, getDim(axis), Math.max(
getLayoutDimension(node, getDim(axis)) -
getPaddingAndBorderAxis(node, axis) -
getMarginAxis(child, axis) -
getPosition(child, getLeading(axis)) -
getPosition(child, getTrailing(axis)),
// You never want to go smaller than padding
getPaddingAndBorderAxis(child, axis)
));
}
}
}
}
// <Loop A> Layout non flexible children and count children by type
// mainContentDim is accumulation of the dimensions and margin of all the
// non flexible children. This will be used in order to either set the
// dimensions of the node if none already exist, or to compute the
// remaining space left for the flexible children.
float mainContentDim = 0;
// There are three kind of children, non flexible, flexible and absolute.
// We need to know how many there are in order to distribute the space.
int flexibleChildrenCount = 0;
float totalFlexible = 0;
int nonFlexibleChildrenCount = 0;
for (int i = 0; i < node.getChildCount(); ++i) {
CSSNode child = node.getChildAt(i);
// It only makes sense to consider a child flexible if we have a computed
// dimension for the node.
if (!FloatUtil.isUndefined(getLayoutDimension(node, getDim(mainAxis))) && isFlex(child)) {
flexibleChildrenCount++;
totalFlexible = totalFlexible + getFlex(child);
// Even if we don't know its exact size yet, we already know the padding,
// border and margin. We'll use this partial information to compute the
// remaining space.
mainContentDim = mainContentDim + getPaddingAndBorderAxis(child, mainAxis) +
getMarginAxis(child, mainAxis);
} else {
float maxWidth = CSSConstants.UNDEFINED;
if (mainAxis == CSSFlexDirection.ROW) {
// do nothing
} else if (isDimDefined(node, CSSFlexDirection.ROW)) {
maxWidth = getLayoutDimension(node, getDim(CSSFlexDirection.ROW)) -
getPaddingAndBorderAxis(node, CSSFlexDirection.ROW);
} else {
maxWidth = parentMaxWidth -
getMarginAxis(node, CSSFlexDirection.ROW) -
getPaddingAndBorderAxis(node, CSSFlexDirection.ROW);
}
// This is the main recursive call. We layout non flexible children.
layoutNode(child, maxWidth);
// Absolute positioned elements do not take part of the layout, so we
// don't use them to compute mainContentDim
if (getPositionType(child) == CSSPositionType.RELATIVE) {
nonFlexibleChildrenCount++;
// At this point we know the final size and margin of the element.
mainContentDim = mainContentDim + getDimWithMargin(child, mainAxis);
}
}
}
// <Loop B> Layout flexible children and allocate empty space
// In order to position the elements in the main axis, we have two
// controls. The space between the beginning and the first element
// and the space between each two elements.
float leadingMainDim = 0;
float betweenMainDim = 0;
// If the dimensions of the current node is defined by its children, they
// are all going to be packed together and we don't need to compute
// anything.
if (!FloatUtil.isUndefined(getLayoutDimension(node, getDim(mainAxis)))) {
// The remaining available space that needs to be allocated
float remainingMainDim = getLayoutDimension(node, getDim(mainAxis)) -
getPaddingAndBorderAxis(node, mainAxis) -
mainContentDim;
// If there are flexible children in the mix, they are going to fill the
// remaining space
if (flexibleChildrenCount != 0) {
float flexibleMainDim = remainingMainDim / totalFlexible;
// The non flexible children can overflow the container, in this case
// we should just assume that there is no space available.
if (flexibleMainDim < 0) {
flexibleMainDim = 0;
}
// We iterate over the full array and only apply the action on flexible
// children. This is faster than actually allocating a new array that
// contains only flexible children.
for (int i = 0; i < node.getChildCount(); ++i) {
CSSNode child = node.getChildAt(i);
if (isFlex(child)) {
// At this point we know the final size of the element in the main
// dimension
setLayoutDimension(child, getDim(mainAxis), flexibleMainDim * getFlex(child) +
getPaddingAndBorderAxis(child, mainAxis));
float maxWidth = CSSConstants.UNDEFINED;
if (mainAxis == CSSFlexDirection.ROW) {
// do nothing
} else if (isDimDefined(node, CSSFlexDirection.ROW)) {
maxWidth = getLayoutDimension(node, getDim(CSSFlexDirection.ROW)) -
getPaddingAndBorderAxis(node, CSSFlexDirection.ROW);
} else {
maxWidth = parentMaxWidth -
getMarginAxis(node, CSSFlexDirection.ROW) -
getPaddingAndBorderAxis(node, CSSFlexDirection.ROW);
}
// And we recursively call the layout algorithm for this child
layoutNode(child, maxWidth);
}
}
// We use justifyContent to figure out how to allocate the remaining
// space available
} else {
CSSJustify justifyContent = getJustifyContent(node);
if (justifyContent == CSSJustify.FLEX_START) {
// Do nothing
} else if (justifyContent == CSSJustify.CENTER) {
leadingMainDim = remainingMainDim / 2;
} else if (justifyContent == CSSJustify.FLEX_END) {
leadingMainDim = remainingMainDim;
} else if (justifyContent == CSSJustify.SPACE_BETWEEN) {
remainingMainDim = Math.max(remainingMainDim, 0);
betweenMainDim = remainingMainDim /
(flexibleChildrenCount + nonFlexibleChildrenCount - 1);
} else if (justifyContent == CSSJustify.SPACE_AROUND) {
// Space on the edges is half of the space between elements
betweenMainDim = remainingMainDim /
(flexibleChildrenCount + nonFlexibleChildrenCount);
leadingMainDim = betweenMainDim / 2;
}
}
}
// <Loop C> Position elements in the main axis and compute dimensions
// At this point, all the children have their dimensions set. We need to
// find their position. In order to do that, we accumulate data in
// variables that are also useful to compute the total dimensions of the
// container!
float crossDim = 0;
float mainDim = leadingMainDim +
getPaddingAndBorder(node, getLeading(mainAxis));
for (int i = 0; i < node.getChildCount(); ++i) {
CSSNode child = node.getChildAt(i);
if (getPositionType(child) == CSSPositionType.ABSOLUTE &&
isPosDefined(child, getLeading(mainAxis))) {
// In case the child is position absolute and has left/top being
// defined, we override the position to whatever the user said
// (and margin/border).
setLayoutPosition(child, getPos(mainAxis), getPosition(child, getLeading(mainAxis)) +
getBorder(node, getLeading(mainAxis)) +
getMargin(child, getLeading(mainAxis)));
} else {
// If the child is position absolute (without top/left) or relative,
// we put it at the current accumulated offset.
setLayoutPosition(child, getPos(mainAxis), getLayoutPosition(child, getPos(mainAxis)) + mainDim);
}
// Now that we placed the element, we need to update the variables
// We only need to do that for relative elements. Absolute elements
// do not take part in that phase.
if (getPositionType(child) == CSSPositionType.RELATIVE) {
// The main dimension is the sum of all the elements dimension plus
// the spacing.
mainDim = mainDim + betweenMainDim + getDimWithMargin(child, mainAxis);
// The cross dimension is the max of the elements dimension since there
// can only be one element in that cross dimension.
crossDim = Math.max(crossDim, getDimWithMargin(child, crossAxis));
}
}
// If the user didn't specify a width or height, and it has not been set
// by the container, then we set it via the children.
if (FloatUtil.isUndefined(getLayoutDimension(node, getDim(mainAxis)))) {
setLayoutDimension(node, getDim(mainAxis), Math.max(
// We're missing the last padding at this point to get the final
// dimension
mainDim + getPaddingAndBorder(node, getTrailing(mainAxis)),
// We can never assign a width smaller than the padding and borders
getPaddingAndBorderAxis(node, mainAxis)
));
}
if (FloatUtil.isUndefined(getLayoutDimension(node, getDim(crossAxis)))) {
setLayoutDimension(node, getDim(crossAxis), Math.max(
// For the cross dim, we add both sides at the end because the value
// is aggregate via a max function. Intermediate negative values
// can mess this computation otherwise
crossDim + getPaddingAndBorderAxis(node, crossAxis),
getPaddingAndBorderAxis(node, crossAxis)
));
}
// <Loop D> Position elements in the cross axis
for (int i = 0; i < node.getChildCount(); ++i) {
CSSNode child = node.getChildAt(i);
if (getPositionType(child) == CSSPositionType.ABSOLUTE &&
isPosDefined(child, getLeading(crossAxis))) {
// In case the child is absolutely positionned and has a
// top/left/bottom/right being set, we override all the previously
// computed positions to set it correctly.
setLayoutPosition(child, getPos(crossAxis), getPosition(child, getLeading(crossAxis)) +
getBorder(node, getLeading(crossAxis)) +
getMargin(child, getLeading(crossAxis)));
} else {
float leadingCrossDim = getPaddingAndBorder(node, getLeading(crossAxis));
// For a relative children, we're either using alignItems (parent) or
// alignSelf (child) in order to determine the position in the cross axis
if (getPositionType(child) == CSSPositionType.RELATIVE) {
CSSAlign alignItem = getAlignItem(node, child);
if (alignItem == CSSAlign.FLEX_START) {
// Do nothing
} else if (alignItem == CSSAlign.STRETCH) {
// You can only stretch if the dimension has not already been set
// previously.
if (!isDimDefined(child, crossAxis)) {
setLayoutDimension(child, getDim(crossAxis), Math.max(
getLayoutDimension(node, getDim(crossAxis)) -
getPaddingAndBorderAxis(node, crossAxis) -
getMarginAxis(child, crossAxis),
// You never want to go smaller than padding
getPaddingAndBorderAxis(child, crossAxis)
));
}
} else {
// The remaining space between the parent dimensions+padding and child
// dimensions+margin.
float remainingCrossDim = getLayoutDimension(node, getDim(crossAxis)) -
getPaddingAndBorderAxis(node, crossAxis) -
getDimWithMargin(child, crossAxis);
if (alignItem == CSSAlign.CENTER) {
leadingCrossDim = leadingCrossDim + remainingCrossDim / 2;
} else { // CSSAlign.FLEX_END
leadingCrossDim = leadingCrossDim + remainingCrossDim;
}
}
}
// And we apply the position
setLayoutPosition(child, getPos(crossAxis), getLayoutPosition(child, getPos(crossAxis)) + leadingCrossDim);
}
}
// <Loop E> Calculate dimensions for absolutely positioned elements
for (int i = 0; i < node.getChildCount(); ++i) {
CSSNode child = node.getChildAt(i);
if (getPositionType(child) == CSSPositionType.ABSOLUTE) {
// Pre-fill dimensions when using absolute position and both offsets for the axis are defined (either both
// left and right or top and bottom).
for (int ii = 0; ii < 2; ii++) {
CSSFlexDirection axis = (ii != 0) ? CSSFlexDirection.ROW : CSSFlexDirection.COLUMN;
if (!FloatUtil.isUndefined(getLayoutDimension(node, getDim(axis))) &&
!isDimDefined(child, axis) &&
isPosDefined(child, getLeading(axis)) &&
isPosDefined(child, getTrailing(axis))) {
setLayoutDimension(child, getDim(axis), Math.max(
getLayoutDimension(node, getDim(axis)) -
getPaddingAndBorderAxis(node, axis) -
getMarginAxis(child, axis) -
getPosition(child, getLeading(axis)) -
getPosition(child, getTrailing(axis)),
// You never want to go smaller than padding
getPaddingAndBorderAxis(child, axis)
));
}
}
for (int ii = 0; ii < 2; ii++) {
CSSFlexDirection axis = (ii != 0) ? CSSFlexDirection.ROW : CSSFlexDirection.COLUMN;
if (isPosDefined(child, getTrailing(axis)) &&
!isPosDefined(child, getLeading(axis))) {
setLayoutPosition(child, getLeading(axis), getLayoutDimension(node, getDim(axis)) -
getLayoutDimension(child, getDim(axis)) -
getPosition(child, getTrailing(axis)));
}
}
}
}
}
/** END_GENERATED **/
}

View File

@@ -0,0 +1,10 @@
package com.facebook.csslayout;
/**
* POJO to hold the output of the measure function.
*/
public class MeasureOutput {
public float width;
public float height;
}

View File

@@ -0,0 +1,42 @@
package com.facebook.csslayout;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
/**
* Tests for {@link CSSNode}.
*/
public class CSSNodeTest {
@Test
public void testAddChildGetParent() {
CSSNode parent = new CSSNode();
CSSNode child = new CSSNode();
assertNull(child.getParent());
assertEquals(0, parent.getChildCount());
parent.addChildAt(child, 0);
assertEquals(1, parent.getChildCount());
assertEquals(child, parent.getChildAt(0));
assertEquals(parent, child.getParent());
parent.removeChildAt(0);
assertNull(child.getParent());
assertEquals(0, parent.getChildCount());
}
@Test(expected = IllegalStateException.class)
public void testCannotAddChildToMultipleParents() {
CSSNode parent1 = new CSSNode();
CSSNode parent2 = new CSSNode();
CSSNode child = new CSSNode();
parent1.addChildAt(child, 0);
parent2.addChildAt(child, 0);
}
}

View File

@@ -0,0 +1,180 @@
package com.facebook.csslayout;
import org.junit.Test;
import static junit.framework.Assert.*;
/**
* Tests for {@link LayoutEngine} and {@link CSSNode} to make sure layouts are only generated when
* needed.
*/
public class LayoutCachingTest {
private void assertTreeHasNewLayout(boolean expectedHasNewLayout, CSSNode root) {
assertEquals(expectedHasNewLayout, root.hasNewLayout());
for (int i = 0; i < root.getChildCount(); i++) {
assertTreeHasNewLayout(expectedHasNewLayout, root.getChildAt(i));
}
}
private void markLayoutAppliedForTree(CSSNode root) {
root.markLayoutApplied();
for (int i = 0; i < root.getChildCount(); i++) {
markLayoutAppliedForTree(root.getChildAt(i));
}
}
@Test
public void testCachesFullTree() {
CSSNode root = new CSSNode();
CSSNode c0 = new CSSNode();
CSSNode c1 = new CSSNode();
CSSNode c0c0 = new CSSNode();
root.addChildAt(c0, 0);
root.addChildAt(c1, 1);
c0.addChildAt(c0c0, 0);
root.calculateLayout();
assertTreeHasNewLayout(true, root);
markLayoutAppliedForTree(root);
root.calculateLayout();
assertTreeHasNewLayout(false, root);
}
@Test
public void testInvalidatesCacheWhenChildAdded() {
CSSNode root = new CSSNode();
CSSNode c0 = new CSSNode();
CSSNode c1 = new CSSNode();
CSSNode c0c0 = new CSSNode();
CSSNode c0c1 = new CSSNode();
c0c1.setStyleWidth(200);
c0c1.setStyleHeight(200);
root.addChildAt(c0, 0);
root.addChildAt(c1, 1);
c0.addChildAt(c0c0, 0);
root.calculateLayout();
assertTreeHasNewLayout(true, root);
markLayoutAppliedForTree(root);
root.calculateLayout();
assertTreeHasNewLayout(false, root);
c0.addChildAt(c0c1, 1);
root.calculateLayout();
assertTrue(root.hasNewLayout());
assertTrue(c0.hasNewLayout());
assertTrue(c0c1.hasNewLayout());
assertFalse(c0c0.hasNewLayout());
assertFalse(c1.hasNewLayout());
}
@Test
public void testInvalidatesCacheWhenEnumPropertyChanges() {
CSSNode root = new CSSNode();
CSSNode c0 = new CSSNode();
CSSNode c1 = new CSSNode();
CSSNode c0c0 = new CSSNode();
root.addChildAt(c0, 0);
root.addChildAt(c1, 1);
c0.addChildAt(c0c0, 0);
root.calculateLayout();
assertTreeHasNewLayout(true, root);
markLayoutAppliedForTree(root);
root.calculateLayout();
assertTreeHasNewLayout(false, root);
c1.setAlignSelf(CSSAlign.CENTER);
root.calculateLayout();
assertTrue(root.hasNewLayout());
assertTrue(c1.hasNewLayout());
assertFalse(c0.hasNewLayout());
assertFalse(c0c0.hasNewLayout());
}
@Test
public void testInvalidatesCacheWhenFloatPropertyChanges() {
CSSNode root = new CSSNode();
CSSNode c0 = new CSSNode();
CSSNode c1 = new CSSNode();
CSSNode c0c0 = new CSSNode();
root.addChildAt(c0, 0);
root.addChildAt(c1, 1);
c0.addChildAt(c0c0, 0);
root.calculateLayout();
assertTreeHasNewLayout(true, root);
markLayoutAppliedForTree(root);
root.calculateLayout();
assertTreeHasNewLayout(false, root);
c1.setMarginLeft(10);
root.calculateLayout();
assertTrue(root.hasNewLayout());
assertTrue(c1.hasNewLayout());
assertFalse(c0.hasNewLayout());
assertFalse(c0c0.hasNewLayout());
}
@Test
public void testInvalidatesFullTreeWhenParentWidthChanges() {
CSSNode root = new CSSNode();
CSSNode c0 = new CSSNode();
CSSNode c1 = new CSSNode();
CSSNode c0c0 = new CSSNode();
root.addChildAt(c0, 0);
root.addChildAt(c1, 1);
c0.addChildAt(c0c0, 0);
root.calculateLayout();
assertTreeHasNewLayout(true, root);
markLayoutAppliedForTree(root);
root.calculateLayout();
assertTreeHasNewLayout(false, root);
c0.setStyleWidth(200);
root.calculateLayout();
assertTrue(root.hasNewLayout());
assertTrue(c0.hasNewLayout());
assertTrue(c0c0.hasNewLayout());
assertFalse(c1.hasNewLayout());
}
@Test
public void testDoesNotInvalidateCacheWhenPropertyIsTheSame() {
CSSNode root = new CSSNode();
CSSNode c0 = new CSSNode();
CSSNode c1 = new CSSNode();
CSSNode c0c0 = new CSSNode();
root.addChildAt(c0, 0);
root.addChildAt(c1, 1);
c0.addChildAt(c0c0, 0);
root.setStyleWidth(200);
root.calculateLayout();
assertTreeHasNewLayout(true, root);
markLayoutAppliedForTree(root);
root.calculateLayout();
assertTreeHasNewLayout(false, root);
root.setStyleWidth(200);
root.calculateLayout();
assertTreeHasNewLayout(false, root);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,17 @@
package com.facebook.csslayout;
/**
* Generated constants used in {@link LayoutEngineTest}.
*/
public class TestConstants {
/** START_GENERATED **/
public static final float SMALL_WIDTH = 34.671875f;
public static final float SMALL_HEIGHT = 18f;
public static final float BIG_WIDTH = 172.421875f;
public static final float BIG_HEIGHT = 36f;
public static final float BIG_MIN_WIDTH = 100.453125f;
public static final String SMALL_TEXT = "small";
public static final String LONG_TEXT = "loooooooooong with space";
/** END_GENERATED **/
}

View File

@@ -1,6 +1,7 @@
var layoutTestUtils = require('./Layout-test-utils.js');
var computeLayout = require('./Layout.js');
var fs = require('fs');
var JavaTranspiler = require('./JavaTranspiler.js');
var currentTest = '';
var allTests = [];
@@ -223,6 +224,7 @@ function transpileAnnotatedJStoC(jsCode) {
.replace(/var\/\*([^\/]+)\*\//g, '$1')
.replace(/ === /g, ' == ')
.replace(/\n /g, '\n')
.replace(/\/\*\(c\)!([^*]+)\*\//g, '$1')
.replace(/\/[*]!([^*]+)[*]\//g, '$1')
.split('\n').slice(1, -1).join('\n');
}
@@ -250,6 +252,10 @@ function generateFile(fileName, generatedContent) {
}
generateFile(__dirname + '/__tests__/Layout-test.c', allTests.map(printLayout).join('\n\n'));
var allTestsInC = allTests.map(printLayout);
generateFile(__dirname + '/__tests__/Layout-test.c', allTestsInC.join('\n\n'));
generateFile(__dirname + '/Layout-test-utils.c', makeConstDefs());
generateFile(__dirname + '/Layout.c', transpileAnnotatedJStoC(computeLayout.toString()));
generateFile(__dirname + '/java/src/com/facebook/csslayout/LayoutEngine.java', JavaTranspiler.transpileLayoutEngine(computeLayout.toString()));
generateFile(__dirname + '/java/tests/com/facebook/csslayout/TestConstants.java', JavaTranspiler.transpileCConstDefs(makeConstDefs()));
generateFile(__dirname + '/java/tests/com/facebook/csslayout/LayoutEngineTest.java', JavaTranspiler.transpileCTestsArray(allTestsInC));