diff --git a/javascript/sources/Node.cc b/javascript/sources/Node.cc index 108bc4b5..748a6cd0 100644 --- a/javascript/sources/Node.cc +++ b/javascript/sources/Node.cc @@ -23,6 +23,13 @@ static YGSize globalMeasureFunc(YGNodeRef nodeRef, float width, YGMeasureMode wi return ygSize; } +static void globalDirtiedFunc(YGNodeRef nodeRef) +{ + Node const & node = *reinterpret_cast(YGNodeGetContext(nodeRef)); + + node.callDirtiedFunc(); +} + /* static */ Node * Node::createDefault(void) { return new Node(nullptr); @@ -46,6 +53,7 @@ static YGSize globalMeasureFunc(YGNodeRef nodeRef, float width, YGMeasureMode wi Node::Node(Config * config) : m_node(config != nullptr ? YGNodeNewWithConfig(config->m_config) : YGNodeNew()) , m_measureFunc(nullptr) +, m_dirtiedFunc(nullptr) { YGNodeSetContext(m_node, reinterpret_cast(this)); } @@ -58,6 +66,7 @@ Node::~Node(void) void Node::reset(void) { m_measureFunc.reset(nullptr); + m_dirtiedFunc.reset(nullptr); YGNodeReset(m_node); } @@ -429,6 +438,24 @@ Size Node::callMeasureFunc(double width, int widthMode, double height, int heigh return m_measureFunc->call(width, widthMode, height, heightMode); } +void Node::setDirtiedFunc(nbind::cbFunction & dirtiedFunc) +{ + m_dirtiedFunc.reset(new nbind::cbFunction(dirtiedFunc)); + + YGNodeSetDirtiedFunc(m_node, &globalDirtiedFunc); +} + +void Node::unsetDirtiedFunc(void) { + m_dirtiedFunc.reset(nullptr); + + YGNodeSetDirtiedFunc(m_node, nullptr); +} + +void Node::callDirtiedFunc(void) const +{ + m_dirtiedFunc->call(); +} + void Node::markDirty(void) { YGNodeMarkDirty(m_node); diff --git a/javascript/sources/Node.hh b/javascript/sources/Node.hh index 593745b2..a3114505 100644 --- a/javascript/sources/Node.hh +++ b/javascript/sources/Node.hh @@ -161,6 +161,15 @@ class Node { Size callMeasureFunc(double width, int widthMode, double height, int heightMode) const; + public: // Dirtied func mutators + + void setDirtiedFunc(nbind::cbFunction & dirtiedFunc); + void unsetDirtiedFunc(void); + + public: // Dirtied func inspectors + + void callDirtiedFunc(void) const; + public: // Dirtiness accessors void markDirty(void); @@ -194,5 +203,5 @@ class Node { YGNodeRef m_node; std::unique_ptr m_measureFunc; - + std::unique_ptr m_dirtiedFunc; }; diff --git a/javascript/sources/nbind.cc b/javascript/sources/nbind.cc index c778fe8a..7f1aac0f 100644 --- a/javascript/sources/nbind.cc +++ b/javascript/sources/nbind.cc @@ -155,6 +155,9 @@ NBIND_CLASS(Node) method(setMeasureFunc); method(unsetMeasureFunc); + method(setDirtiedFunc); + method(unsetDirtiedFunc); + method(markDirty); method(isDirty); diff --git a/javascript/tests/Facebook.Yoga/YGDirtiedTest.js b/javascript/tests/Facebook.Yoga/YGDirtiedTest.js new file mode 100644 index 00000000..20320c43 --- /dev/null +++ b/javascript/tests/Facebook.Yoga/YGDirtiedTest.js @@ -0,0 +1,168 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +var Yoga = Yoga || require("../../sources/entry-" + process.env.TEST_ENTRY); + +it("dirtied", function() { + var root = Yoga.Node.create(); + root.setAlignItems(Yoga.ALIGN_FLEX_START); + root.setWidth(100); + root.setHeight(100); + + root.calculateLayout(undefined, undefined, Yoga.DIRECTION_LTR); + + let dirtied = 0; + root.setDirtiedFunc(function() { dirtied++; }); + // only nodes with a measure function can be marked dirty + root.setMeasureFunc(function() {}); + + console.assert(0 === dirtied, "0 === dirtied"); + + // dirtied func MUST be called in case of explicit dirtying. + root.markDirty(); + console.assert(1 === dirtied, "1 === dirtied"); + + // dirtied func MUST be called ONCE. + root.markDirty(); + console.assert(1 === dirtied, "1 === dirtied"); + + if (typeof root !== "undefined") + root.freeRecursive(); + + typeof gc !== "undefined" && gc(); + console.assert(0 === Yoga.getInstanceCount(), "0 === Yoga.getInstanceCount() (" + Yoga.getInstanceCount() + ")"); +}); + +it("dirtied_propagation", function() { + var root = Yoga.Node.create(); + root.setAlignItems(Yoga.ALIGN_FLEX_START); + root.setWidth(100); + root.setHeight(100); + + var root_child0 = Yoga.Node.create(); + root_child0.setAlignItems(Yoga.ALIGN_FLEX_START); + root_child0.setWidth(50); + root_child0.setHeight(20); + root_child0.setMeasureFunc(function() {}); + root.insertChild(root_child0, 0); + + var root_child1 = Yoga.Node.create(); + root_child1.setAlignItems(Yoga.ALIGN_FLEX_START); + root_child1.setWidth(50); + root_child1.setHeight(20); + root.insertChild(root_child1, 0); + + root.calculateLayout(undefined, undefined, Yoga.DIRECTION_LTR); + + let dirtied = 0; + root.setDirtiedFunc(function() { dirtied++; }); + + console.assert(0 === dirtied, "0 === dirtied"); + + // dirtied func MUST be called for the first time. + root_child0.markDirty(); + console.assert(1 === dirtied, "1 === dirtied"); + + // dirtied func must NOT be called for the second time. + root_child0.markDirty(); + console.assert(1 === dirtied, "1 === dirtied"); + + if (typeof root !== "undefined") + root.freeRecursive(); + + typeof gc !== "undefined" && gc(); + console.assert(0 === Yoga.getInstanceCount(), "0 === Yoga.getInstanceCount() (" + Yoga.getInstanceCount() + ")"); +}); + +it("dirtied_hierarchy", function() { + var root = Yoga.Node.create(); + root.setAlignItems(Yoga.ALIGN_FLEX_START); + root.setWidth(100); + root.setHeight(100); + + var root_child0 = Yoga.Node.create(); + root_child0.setAlignItems(Yoga.ALIGN_FLEX_START); + root_child0.setWidth(50); + root_child0.setHeight(20); + root_child0.setMeasureFunc(function() {}); + root.insertChild(root_child0, 0); + + var root_child1 = Yoga.Node.create(); + root_child1.setAlignItems(Yoga.ALIGN_FLEX_START); + root_child1.setWidth(50); + root_child1.setHeight(20); + root_child1.setMeasureFunc(function() {}); + root.insertChild(root_child1, 0); + + root.calculateLayout(undefined, undefined, Yoga.DIRECTION_LTR); + + let dirtied = 0; + root_child0.setDirtiedFunc(function() { + dirtied++; + }); + + console.assert(0 === dirtied, "0 === dirtied"); + + // dirtied func must NOT be called for descendants. + // NOTE: nodes without a measure function cannot be marked dirty manually, + // but nodes with a measure function can not have children. + // Update the width to dirty the node instead. + root.setWidth(110); + console.assert(0 === dirtied, "0 === dirtied"); + + // dirtied func MUST be called in case of explicit dirtying. + root_child0.markDirty(); + console.assert(1 === dirtied, "1 === dirtied"); + + if (typeof root !== "undefined") + root.freeRecursive(); + + typeof gc !== "undefined" && gc(); + console.assert(0 === Yoga.getInstanceCount(), "0 === Yoga.getInstanceCount() (" + Yoga.getInstanceCount() + ")"); +}); + +it("dirtied_reset", function() { + var root = Yoga.Node.create(); + root.setAlignItems(Yoga.ALIGN_FLEX_START); + root.setWidth(100); + root.setHeight(100); + root.setMeasureFunc(function() {}); + + root.calculateLayout(undefined, undefined, Yoga.DIRECTION_LTR); + + let dirtied = 0; + root.setDirtiedFunc(function() { + dirtied++; + }); + + console.assert(0 === dirtied, "0 === dirtied"); + + // dirtied func MUST be called in case of explicit dirtying. + root.markDirty(); + console.assert(1 === dirtied, "1 === dirtied"); + + // recalculate so the root is no longer dirty + root.calculateLayout(undefined, undefined, Yoga.DIRECTION_LTR); + + root.reset(); + root.setAlignItems(Yoga.ALIGN_FLEX_START); + root.setWidth(100); + root.setHeight(100); + root.setMeasureFunc(function() {}); + + root.markDirty(); + + // dirtied func must NOT be called after reset. + root.markDirty(); + console.assert(1 === dirtied, "1 === dirtied"); + + if (typeof root !== "undefined") + root.freeRecursive(); + + typeof gc !== "undefined" && gc(); + console.assert(0 === Yoga.getInstanceCount(), "0 === Yoga.getInstanceCount() (" + Yoga.getInstanceCount() + ")"); +});