Summary: This is meant to show a possible route format for a persistent form of Yoga. Where previous layouts can remain intact while still taking advantage of incremental layout by reusing previous subtrees. ```c YGNodeRef YGNodeClone(const YGNodeRef node); ``` The core of this functionality is a new API to clone an existing node. This makes a new detached node with all the same values as the previous one. Conceptually this makes the original node "frozen" from that point on. It's now immutable. (This is not yet enforced at runtime in this PR but something we should add.) Since the original is frozen, we reuse the children set from the original node. Their parent pointers still point back to the original tree though. The cloned node is still mutable. It can have its styles updated, and nodes can be inserted or deleted. If an insertion/deletion happens on a cloned node whose children were reused, it'll first shallow clone its children automatically. As a convenience I also added an API to clear all children: ```c void YGNodeRemoveAllChildren(const YGNodeRef node); ``` During insert/delete, or as a result of layout a set of reused children may need to be first cloned. A kind of copy-on-write. When that happens, the host may want to respond. E.g. by updating the `context` such as by cloning any wrapper objects and attaching them to the new node. ```c typedef void (*YGNodeClonedFunc)(YGNodeRef oldNode, YGNodeRef newNode, YGNodeRef parent, int childIndex); void YGConfigSetNodeClonedFunc(YGConfigRef config, YGNodeClonedFunc callback); ``` This PR doesn't change any existing semantics for trees that are not first cloned. It's possible for a single node to exist in two trees at once and be used by multiple threads. Therefore it's not safe to recursively free a whole tree when you use persistence. To solve this, any user of the library has to manually manage ref counting or tracing GC. E.g. by replicating the tree structure in a wrapper. In a follow up we could consider moving ref counting into Yoga. Closes https://github.com/facebook/yoga/pull/636 Reviewed By: emilsjolander Differential Revision: D5941921 Pulled By: sebmarkbage fbshipit-source-id: c8e93421824c112d09c4773bed4e3141b6491ccf
252 lines
8.9 KiB
C++
252 lines
8.9 KiB
C++
/**
|
|
* Copyright (c) 2014-present, Facebook, Inc.
|
|
* All rights reserved.
|
|
*
|
|
* This source code is licensed under the BSD-style license found in the
|
|
* LICENSE file in the root directory of this source tree. An additional grant
|
|
* of patent rights can be found in the PATENTS file in the same directory.
|
|
*/
|
|
|
|
// @Generated by gentest/gentest.rb from gentest/fixtures/YGPercentageTest.html
|
|
|
|
#include <gtest/gtest.h>
|
|
#include <yoga/Yoga.h>
|
|
|
|
TEST(YogaTest, cloning_shared_root) {
|
|
const YGConfigRef config = YGConfigNew();
|
|
|
|
const YGNodeRef root = YGNodeNewWithConfig(config);
|
|
YGNodeStyleSetWidth(root, 100);
|
|
YGNodeStyleSetHeight(root, 100);
|
|
|
|
const YGNodeRef root_child0 = YGNodeNewWithConfig(config);
|
|
YGNodeStyleSetFlexGrow(root_child0, 1);
|
|
YGNodeStyleSetFlexBasis(root_child0, 50);
|
|
YGNodeInsertChild(root, root_child0, 0);
|
|
|
|
const YGNodeRef root_child1 = YGNodeNewWithConfig(config);
|
|
YGNodeStyleSetFlexGrow(root_child1, 1);
|
|
YGNodeInsertChild(root, root_child1, 1);
|
|
YGNodeCalculateLayout(root, YGUndefined, YGUndefined, YGDirectionLTR);
|
|
|
|
ASSERT_FLOAT_EQ(0, YGNodeLayoutGetLeft(root));
|
|
ASSERT_FLOAT_EQ(0, YGNodeLayoutGetTop(root));
|
|
ASSERT_FLOAT_EQ(100, YGNodeLayoutGetWidth(root));
|
|
ASSERT_FLOAT_EQ(100, YGNodeLayoutGetHeight(root));
|
|
|
|
ASSERT_FLOAT_EQ(0, YGNodeLayoutGetLeft(root_child0));
|
|
ASSERT_FLOAT_EQ(0, YGNodeLayoutGetTop(root_child0));
|
|
ASSERT_FLOAT_EQ(100, YGNodeLayoutGetWidth(root_child0));
|
|
ASSERT_FLOAT_EQ(75, YGNodeLayoutGetHeight(root_child0));
|
|
|
|
ASSERT_FLOAT_EQ(0, YGNodeLayoutGetLeft(root_child1));
|
|
ASSERT_FLOAT_EQ(75, YGNodeLayoutGetTop(root_child1));
|
|
ASSERT_FLOAT_EQ(100, YGNodeLayoutGetWidth(root_child1));
|
|
ASSERT_FLOAT_EQ(25, YGNodeLayoutGetHeight(root_child1));
|
|
|
|
const YGNodeRef root2 = YGNodeClone(root);
|
|
YGNodeStyleSetWidth(root2, 100);
|
|
|
|
ASSERT_EQ(2, YGNodeGetChildCount(root2));
|
|
// The children should have referential equality at this point.
|
|
ASSERT_EQ(root_child0, YGNodeGetChild(root2, 0));
|
|
ASSERT_EQ(root_child1, YGNodeGetChild(root2, 1));
|
|
|
|
YGNodeCalculateLayout(root2, YGUndefined, YGUndefined, YGDirectionLTR);
|
|
|
|
ASSERT_EQ(2, YGNodeGetChildCount(root2));
|
|
// Relayout with no changed input should result in referential equality.
|
|
ASSERT_EQ(root_child0, YGNodeGetChild(root2, 0));
|
|
ASSERT_EQ(root_child1, YGNodeGetChild(root2, 1));
|
|
|
|
YGNodeStyleSetWidth(root2, 150);
|
|
YGNodeStyleSetHeight(root2, 200);
|
|
YGNodeCalculateLayout(root2, YGUndefined, YGUndefined, YGDirectionLTR);
|
|
|
|
ASSERT_EQ(2, YGNodeGetChildCount(root2));
|
|
// Relayout with changed input should result in cloned children.
|
|
const YGNodeRef root2_child0 = YGNodeGetChild(root2, 0);
|
|
const YGNodeRef root2_child1 = YGNodeGetChild(root2, 1);
|
|
ASSERT_NE(root_child0, root2_child0);
|
|
ASSERT_NE(root_child1, root2_child1);
|
|
|
|
// Everything in the root should remain unchanged.
|
|
ASSERT_FLOAT_EQ(0, YGNodeLayoutGetLeft(root));
|
|
ASSERT_FLOAT_EQ(0, YGNodeLayoutGetTop(root));
|
|
ASSERT_FLOAT_EQ(100, YGNodeLayoutGetWidth(root));
|
|
ASSERT_FLOAT_EQ(100, YGNodeLayoutGetHeight(root));
|
|
|
|
ASSERT_FLOAT_EQ(0, YGNodeLayoutGetLeft(root_child0));
|
|
ASSERT_FLOAT_EQ(0, YGNodeLayoutGetTop(root_child0));
|
|
ASSERT_FLOAT_EQ(100, YGNodeLayoutGetWidth(root_child0));
|
|
ASSERT_FLOAT_EQ(75, YGNodeLayoutGetHeight(root_child0));
|
|
|
|
ASSERT_FLOAT_EQ(0, YGNodeLayoutGetLeft(root_child1));
|
|
ASSERT_FLOAT_EQ(75, YGNodeLayoutGetTop(root_child1));
|
|
ASSERT_FLOAT_EQ(100, YGNodeLayoutGetWidth(root_child1));
|
|
ASSERT_FLOAT_EQ(25, YGNodeLayoutGetHeight(root_child1));
|
|
|
|
// The new root now has new layout.
|
|
ASSERT_FLOAT_EQ(0, YGNodeLayoutGetLeft(root2));
|
|
ASSERT_FLOAT_EQ(0, YGNodeLayoutGetTop(root2));
|
|
ASSERT_FLOAT_EQ(150, YGNodeLayoutGetWidth(root2));
|
|
ASSERT_FLOAT_EQ(200, YGNodeLayoutGetHeight(root2));
|
|
|
|
ASSERT_FLOAT_EQ(0, YGNodeLayoutGetLeft(root2_child0));
|
|
ASSERT_FLOAT_EQ(0, YGNodeLayoutGetTop(root2_child0));
|
|
ASSERT_FLOAT_EQ(150, YGNodeLayoutGetWidth(root2_child0));
|
|
ASSERT_FLOAT_EQ(125, YGNodeLayoutGetHeight(root2_child0));
|
|
|
|
ASSERT_FLOAT_EQ(0, YGNodeLayoutGetLeft(root2_child1));
|
|
ASSERT_FLOAT_EQ(125, YGNodeLayoutGetTop(root2_child1));
|
|
ASSERT_FLOAT_EQ(150, YGNodeLayoutGetWidth(root2_child1));
|
|
ASSERT_FLOAT_EQ(75, YGNodeLayoutGetHeight(root2_child1));
|
|
|
|
YGNodeFreeRecursive(root2);
|
|
|
|
YGNodeFreeRecursive(root);
|
|
|
|
YGConfigFree(config);
|
|
}
|
|
|
|
TEST(YogaTest, mutating_children_of_a_clone_clones) {
|
|
const YGConfigRef config = YGConfigNew();
|
|
|
|
const YGNodeRef root = YGNodeNewWithConfig(config);
|
|
ASSERT_EQ(0, YGNodeGetChildCount(root));
|
|
|
|
const YGNodeRef root2 = YGNodeClone(root);
|
|
ASSERT_EQ(0, YGNodeGetChildCount(root2));
|
|
|
|
const YGNodeRef root2_child0 = YGNodeNewWithConfig(config);
|
|
YGNodeInsertChild(root2, root2_child0, 0);
|
|
|
|
ASSERT_EQ(0, YGNodeGetChildCount(root));
|
|
ASSERT_EQ(1, YGNodeGetChildCount(root2));
|
|
|
|
const YGNodeRef root3 = YGNodeClone(root2);
|
|
ASSERT_EQ(1, YGNodeGetChildCount(root2));
|
|
ASSERT_EQ(1, YGNodeGetChildCount(root3));
|
|
ASSERT_EQ(YGNodeGetChild(root2, 0), YGNodeGetChild(root3, 0));
|
|
|
|
const YGNodeRef root3_child1 = YGNodeNewWithConfig(config);
|
|
YGNodeInsertChild(root3, root3_child1, 1);
|
|
ASSERT_EQ(1, YGNodeGetChildCount(root2));
|
|
ASSERT_EQ(2, YGNodeGetChildCount(root3));
|
|
ASSERT_EQ(root3_child1, YGNodeGetChild(root3, 1));
|
|
ASSERT_NE(YGNodeGetChild(root2, 0), YGNodeGetChild(root3, 0));
|
|
|
|
const YGNodeRef root4 = YGNodeClone(root3);
|
|
ASSERT_EQ(root3_child1, YGNodeGetChild(root4, 1));
|
|
|
|
YGNodeRemoveChild(root4, root3_child1);
|
|
ASSERT_EQ(2, YGNodeGetChildCount(root3));
|
|
ASSERT_EQ(1, YGNodeGetChildCount(root4));
|
|
ASSERT_NE(YGNodeGetChild(root3, 0), YGNodeGetChild(root4, 0));
|
|
|
|
YGNodeFreeRecursive(root4);
|
|
YGNodeFreeRecursive(root3);
|
|
YGNodeFreeRecursive(root2);
|
|
YGNodeFreeRecursive(root);
|
|
|
|
YGConfigFree(config);
|
|
}
|
|
|
|
TEST(YogaTest, cloning_two_levels) {
|
|
const YGConfigRef config = YGConfigNew();
|
|
|
|
const YGNodeRef root = YGNodeNewWithConfig(config);
|
|
YGNodeStyleSetWidth(root, 100);
|
|
YGNodeStyleSetHeight(root, 100);
|
|
|
|
const YGNodeRef root_child0 = YGNodeNewWithConfig(config);
|
|
YGNodeStyleSetFlexGrow(root_child0, 1);
|
|
YGNodeStyleSetFlexBasis(root_child0, 15);
|
|
YGNodeInsertChild(root, root_child0, 0);
|
|
|
|
const YGNodeRef root_child1 = YGNodeNewWithConfig(config);
|
|
YGNodeStyleSetFlexGrow(root_child1, 1);
|
|
YGNodeInsertChild(root, root_child1, 1);
|
|
|
|
const YGNodeRef root_child1_0 = YGNodeNewWithConfig(config);
|
|
YGNodeStyleSetFlexBasis(root_child1_0, 10);
|
|
YGNodeStyleSetFlexGrow(root_child1_0, 1);
|
|
YGNodeInsertChild(root_child1, root_child1_0, 0);
|
|
|
|
const YGNodeRef root_child1_1 = YGNodeNewWithConfig(config);
|
|
YGNodeStyleSetFlexBasis(root_child1_1, 25);
|
|
YGNodeInsertChild(root_child1, root_child1_1, 1);
|
|
|
|
YGNodeCalculateLayout(root, YGUndefined, YGUndefined, YGDirectionLTR);
|
|
|
|
ASSERT_FLOAT_EQ(40, YGNodeLayoutGetHeight(root_child0));
|
|
ASSERT_FLOAT_EQ(60, YGNodeLayoutGetHeight(root_child1));
|
|
ASSERT_FLOAT_EQ(35, YGNodeLayoutGetHeight(root_child1_0));
|
|
ASSERT_FLOAT_EQ(25, YGNodeLayoutGetHeight(root_child1_1));
|
|
|
|
const YGNodeRef root2_child0 = YGNodeClone(root_child0);
|
|
const YGNodeRef root2_child1 = YGNodeClone(root_child1);
|
|
const YGNodeRef root2 = YGNodeClone(root);
|
|
|
|
YGNodeStyleSetFlexGrow(root2_child0, 0);
|
|
YGNodeStyleSetFlexBasis(root2_child0, 40);
|
|
|
|
YGNodeRemoveAllChildren(root2);
|
|
YGNodeInsertChild(root2, root2_child0, 0);
|
|
YGNodeInsertChild(root2, root2_child1, 1);
|
|
ASSERT_EQ(2, YGNodeGetChildCount(root2));
|
|
|
|
YGNodeCalculateLayout(root2, YGUndefined, YGUndefined, YGDirectionLTR);
|
|
|
|
// Original root is unchanged
|
|
ASSERT_FLOAT_EQ(40, YGNodeLayoutGetHeight(root_child0));
|
|
ASSERT_FLOAT_EQ(60, YGNodeLayoutGetHeight(root_child1));
|
|
ASSERT_FLOAT_EQ(35, YGNodeLayoutGetHeight(root_child1_0));
|
|
ASSERT_FLOAT_EQ(25, YGNodeLayoutGetHeight(root_child1_1));
|
|
|
|
// New root has new layout at the top
|
|
ASSERT_FLOAT_EQ(40, YGNodeLayoutGetHeight(root2_child0));
|
|
ASSERT_FLOAT_EQ(60, YGNodeLayoutGetHeight(root2_child1));
|
|
|
|
// The deeper children are untouched.
|
|
ASSERT_EQ(YGNodeGetChild(root2_child1, 0), root_child1_0);
|
|
ASSERT_EQ(YGNodeGetChild(root2_child1, 1), root_child1_1);
|
|
|
|
YGNodeFreeRecursive(root2);
|
|
YGNodeFreeRecursive(root);
|
|
|
|
YGConfigFree(config);
|
|
}
|
|
|
|
TEST(YogaTest, cloning_and_freeing) {
|
|
const int32_t initialInstanceCount = YGNodeGetInstanceCount();
|
|
|
|
const YGConfigRef config = YGConfigNew();
|
|
|
|
const YGNodeRef root = YGNodeNewWithConfig(config);
|
|
YGNodeStyleSetWidth(root, 100);
|
|
YGNodeStyleSetHeight(root, 100);
|
|
const YGNodeRef root_child0 = YGNodeNewWithConfig(config);
|
|
YGNodeInsertChild(root, root_child0, 0);
|
|
const YGNodeRef root_child1 = YGNodeNewWithConfig(config);
|
|
YGNodeInsertChild(root, root_child1, 1);
|
|
|
|
YGNodeCalculateLayout(root, YGUndefined, YGUndefined, YGDirectionLTR);
|
|
|
|
const YGNodeRef root2 = YGNodeClone(root);
|
|
|
|
// Freeing the original root should be safe as long as we don't free its children.
|
|
YGNodeFree(root);
|
|
|
|
YGNodeCalculateLayout(root2, YGUndefined, YGUndefined, YGDirectionLTR);
|
|
|
|
YGNodeFreeRecursive(root2);
|
|
|
|
YGNodeFree(root_child0);
|
|
YGNodeFree(root_child1);
|
|
|
|
YGConfigFree(config);
|
|
|
|
ASSERT_EQ(initialInstanceCount, YGNodeGetInstanceCount());
|
|
}
|