C++ Cleanup 10/N: YGNodeCalculateLayout (#1352)

Summary:
X-link: https://github.com/facebook/react-native/pull/39195

Pull Request resolved: https://github.com/facebook/yoga/pull/1352

## This diff

This splits out all of the logic under `YGNodeCalculateLayout` to a couple of different files, does some mechanical renaming, and starts to split up the implementation a tiny bit. After this, core layout functions are all C++ convention and namespaced.

Each new file is marked as a move for the sake of blame history. It means Phabricator has a very inaccurate count of lines removed though.

## This stack

The organization of the C++ internals of Yoga are in need of attention.
1. Some of the C++ internals are namespaced, but others not.
2. Some of the namespaces include `detail`, but are meant to be used outside of the translation unit (FB Clang Tidy rules warn on any usage of these)
2. Most of the files are in a flat hierarchy, except for event tracing in its own folder
3. Some files and functions begin with YG, others don’t
4. Some functions are uppercase, others are not
5. Almost all of the interesting logic is in Yoga.cpp, and the file is too large to reason about
6. There are multiple grab bag files where folks put random functions they need in (Utils, BitUtils, Yoga-Internal.h)
7. There is no clear indication from file structure or type naming what is private vs not
8. Handles like `YGNodeRef` and `YGConfigRef` can be used to access internals just by importing headers

This stack does some much needed spring cleaning:
1. All non-public headers and C++ implementation details are in separate folders from the root level `yoga`. This will give us room to split up logic and add more files without too large a flat hierarchy
3. All private C++ internals are under the `facebook::yoga` namespace. Details namespaces are only ever used within the same header, as they are intended
4. Utils files are split
5. Most C++ internals drop the YG prefix
6. Most C++ internal function names are all lower camel case
7. We start to split up Yoga.cpp
8. Every header beginning with YG or at the top-level directory is public and C only, with the exception of Yoga-Internal.h which has non-public functions for bindings
9. It is not possible to use private APIs without static casting handles to internal classes

This will give us more leeway to continue splitting monolithic files, and consistent guidelines for style in new files as well.

These changes should not be breaking to any project using only public Yoga headers. This includes every usage of Yoga in fbsource except for RN Fabric which is currently tied to internals. This refactor should make that boundary clearer.

Reviewed By: rshest

Differential Revision: D48770478

fbshipit-source-id: 2a74b86441c3352de03ae193c98fc3a3573047ed
This commit is contained in:
Nick Gerleman
2023-09-05 05:24:54 -07:00
committed by Facebook GitHub Bot
parent 7d8b9176fd
commit 65ae809d5d
15 changed files with 3543 additions and 3292 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -108,15 +108,17 @@ WIN_EXPORT void YGNodePrint(YGNodeRef node, YGPrintOptions options);
WIN_EXPORT bool YGFloatIsUndefined(float value);
// TODO: This should not be part of the public API. Remove after removing
// ComponentKit usage of it.
WIN_EXPORT bool YGNodeCanUseCachedMeasurement(
YGMeasureMode widthMode,
float width,
float availableWidth,
YGMeasureMode heightMode,
float height,
float availableHeight,
YGMeasureMode lastWidthMode,
float lastWidth,
float lastAvailableWidth,
YGMeasureMode lastHeightMode,
float lastHeight,
float lastAvailableHeight,
float lastComputedWidth,
float lastComputedHeight,
float marginRow,

29
yoga/algorithm/Align.h Normal file
View File

@@ -0,0 +1,29 @@
/*
* 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.
*/
#pragma once
#include <yoga/Yoga.h>
#include <yoga/algorithm/FlexDirection.h>
#include <yoga/node/Node.h>
namespace facebook::yoga {
inline YGAlign resolveChildAlignment(
const yoga::Node* node,
const yoga::Node* child) {
const YGAlign align = child->getStyle().alignSelf() == YGAlignAuto
? node->getStyle().alignItems()
: child->getStyle().alignSelf();
if (align == YGAlignBaseline && isColumn(node->getStyle().flexDirection())) {
return YGAlignFlexStart;
}
return align;
}
} // namespace facebook::yoga

View File

@@ -0,0 +1,65 @@
/*
* 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.
*/
#include <yoga/Yoga.h>
#include <yoga/algorithm/Align.h>
#include <yoga/algorithm/Baseline.h>
#include <yoga/debug/AssertFatal.h>
#include <yoga/event/event.h>
namespace facebook::yoga {
float calculateBaseline(const yoga::Node* node, void* layoutContext) {
if (node->hasBaselineFunc()) {
Event::publish<Event::NodeBaselineStart>(node);
const float baseline = node->baseline(
node->getLayout().measuredDimensions[YGDimensionWidth],
node->getLayout().measuredDimensions[YGDimensionHeight],
layoutContext);
Event::publish<Event::NodeBaselineEnd>(node);
yoga::assertFatalWithNode(
node,
!std::isnan(baseline),
"Expect custom baseline function to not return NaN");
return baseline;
}
yoga::Node* baselineChild = nullptr;
const uint32_t childCount = YGNodeGetChildCount(node);
for (uint32_t i = 0; i < childCount; i++) {
auto child = node->getChild(i);
if (child->getLineIndex() > 0) {
break;
}
if (child->getStyle().positionType() == YGPositionTypeAbsolute) {
continue;
}
if (resolveChildAlignment(node, child) == YGAlignBaseline ||
child->isReferenceBaseline()) {
baselineChild = child;
break;
}
if (baselineChild == nullptr) {
baselineChild = child;
}
}
if (baselineChild == nullptr) {
return node->getLayout().measuredDimensions[YGDimensionHeight];
}
const float baseline = calculateBaseline(baselineChild, layoutContext);
return baseline + baselineChild->getLayout().position[YGEdgeTop];
}
} // namespace facebook::yoga

18
yoga/algorithm/Baseline.h Normal file
View File

@@ -0,0 +1,18 @@
/*
* 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.
*/
#pragma once
#include <yoga/Yoga.h>
#include <yoga/node/Node.h>
namespace facebook::yoga {
// Calculate baseline represented as an offset from the top edge of the node.
float calculateBaseline(const yoga::Node* node, void* layoutContext);
} // namespace facebook::yoga

123
yoga/algorithm/Cache.cpp Normal file
View File

@@ -0,0 +1,123 @@
/*
* 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.
*/
#include <yoga/Yoga.h>
#include <yoga/algorithm/Cache.h>
#include <yoga/algorithm/PixelGrid.h>
#include <yoga/numeric/Comparison.h>
namespace facebook::yoga {
static inline bool sizeIsExactAndMatchesOldMeasuredSize(
YGMeasureMode sizeMode,
float size,
float lastComputedSize) {
return sizeMode == YGMeasureModeExactly &&
yoga::inexactEquals(size, lastComputedSize);
}
static inline bool oldSizeIsUnspecifiedAndStillFits(
YGMeasureMode sizeMode,
float size,
YGMeasureMode lastSizeMode,
float lastComputedSize) {
return sizeMode == YGMeasureModeAtMost &&
lastSizeMode == YGMeasureModeUndefined &&
(size >= lastComputedSize || yoga::inexactEquals(size, lastComputedSize));
}
static inline bool newMeasureSizeIsStricterAndStillValid(
YGMeasureMode sizeMode,
float size,
YGMeasureMode lastSizeMode,
float lastSize,
float lastComputedSize) {
return lastSizeMode == YGMeasureModeAtMost &&
sizeMode == YGMeasureModeAtMost && !std::isnan(lastSize) &&
!std::isnan(size) && !std::isnan(lastComputedSize) && lastSize > size &&
(lastComputedSize <= size || yoga::inexactEquals(size, lastComputedSize));
}
bool canUseCachedMeasurement(
const YGMeasureMode widthMode,
const float availableWidth,
const YGMeasureMode heightMode,
const float availableHeight,
const YGMeasureMode lastWidthMode,
const float lastAvailableWidth,
const YGMeasureMode lastHeightMode,
const float lastAvailableHeight,
const float lastComputedWidth,
const float lastComputedHeight,
const float marginRow,
const float marginColumn,
const yoga::Config* const config) {
if ((!std::isnan(lastComputedHeight) && lastComputedHeight < 0) ||
(!std::isnan(lastComputedWidth) && lastComputedWidth < 0)) {
return false;
}
const float pointScaleFactor = config->getPointScaleFactor();
bool useRoundedComparison = config != nullptr && pointScaleFactor != 0;
const float effectiveWidth = useRoundedComparison
? roundValueToPixelGrid(availableWidth, pointScaleFactor, false, false)
: availableWidth;
const float effectiveHeight = useRoundedComparison
? roundValueToPixelGrid(availableHeight, pointScaleFactor, false, false)
: availableHeight;
const float effectiveLastWidth = useRoundedComparison
? roundValueToPixelGrid(
lastAvailableWidth, pointScaleFactor, false, false)
: lastAvailableWidth;
const float effectiveLastHeight = useRoundedComparison
? roundValueToPixelGrid(
lastAvailableHeight, pointScaleFactor, false, false)
: lastAvailableHeight;
const bool hasSameWidthSpec = lastWidthMode == widthMode &&
yoga::inexactEquals(effectiveLastWidth, effectiveWidth);
const bool hasSameHeightSpec = lastHeightMode == heightMode &&
yoga::inexactEquals(effectiveLastHeight, effectiveHeight);
const bool widthIsCompatible =
hasSameWidthSpec ||
sizeIsExactAndMatchesOldMeasuredSize(
widthMode, availableWidth - marginRow, lastComputedWidth) ||
oldSizeIsUnspecifiedAndStillFits(
widthMode,
availableWidth - marginRow,
lastWidthMode,
lastComputedWidth) ||
newMeasureSizeIsStricterAndStillValid(
widthMode,
availableWidth - marginRow,
lastWidthMode,
lastAvailableWidth,
lastComputedWidth);
const bool heightIsCompatible =
hasSameHeightSpec ||
sizeIsExactAndMatchesOldMeasuredSize(
heightMode, availableHeight - marginColumn, lastComputedHeight) ||
oldSizeIsUnspecifiedAndStillFits(
heightMode,
availableHeight - marginColumn,
lastHeightMode,
lastComputedHeight) ||
newMeasureSizeIsStricterAndStillValid(
heightMode,
availableHeight - marginColumn,
lastHeightMode,
lastAvailableHeight,
lastComputedHeight);
return widthIsCompatible && heightIsCompatible;
}
} // namespace facebook::yoga

30
yoga/algorithm/Cache.h Normal file
View File

@@ -0,0 +1,30 @@
/*
* 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.
*/
#pragma once
#include <yoga/Yoga.h>
#include <yoga/config/Config.h>
namespace facebook::yoga {
bool canUseCachedMeasurement(
YGMeasureMode widthMode,
float availableWidth,
YGMeasureMode heightMode,
float availableHeight,
YGMeasureMode lastWidthMode,
float lastAvailableWidth,
YGMeasureMode lastHeightMode,
float lastAvailableHeight,
float lastComputedWidth,
float lastComputedHeight,
float marginRow,
float marginColumn,
const yoga::Config* config);
} // namespace facebook::yoga

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,22 @@
/*
* 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.
*/
#pragma once
#include <yoga/Yoga.h>
#include <yoga/node/Node.h>
namespace facebook::yoga {
void calculateLayout(
yoga::Node* const node,
const float ownerWidth,
const float ownerHeight,
const YGDirection ownerDirection,
void* layoutContext);
} // namespace facebook::yoga

View File

@@ -0,0 +1,135 @@
/*
* 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.
*/
#include <yoga/Yoga.h>
#include <yoga/algorithm/PixelGrid.h>
#include <yoga/numeric/Comparison.h>
namespace facebook::yoga {
float roundValueToPixelGrid(
const double value,
const double pointScaleFactor,
const bool forceCeil,
const bool forceFloor) {
double scaledValue = value * pointScaleFactor;
// We want to calculate `fractial` such that `floor(scaledValue) = scaledValue
// - fractial`.
double fractial = fmod(scaledValue, 1.0);
if (fractial < 0) {
// This branch is for handling negative numbers for `value`.
//
// Regarding `floor` and `ceil`. Note that for a number x, `floor(x) <= x <=
// ceil(x)` even for negative numbers. Here are a couple of examples:
// - x = 2.2: floor( 2.2) = 2, ceil( 2.2) = 3
// - x = -2.2: floor(-2.2) = -3, ceil(-2.2) = -2
//
// Regarding `fmodf`. For fractional negative numbers, `fmodf` returns a
// negative number. For example, `fmodf(-2.2) = -0.2`. However, we want
// `fractial` to be the number such that subtracting it from `value` will
// give us `floor(value)`. In the case of negative numbers, adding 1 to
// `fmodf(value)` gives us this. Let's continue the example from above:
// - fractial = fmodf(-2.2) = -0.2
// - Add 1 to the fraction: fractial2 = fractial + 1 = -0.2 + 1 = 0.8
// - Finding the `floor`: -2.2 - fractial2 = -2.2 - 0.8 = -3
++fractial;
}
if (yoga::inexactEquals(fractial, 0)) {
// First we check if the value is already rounded
scaledValue = scaledValue - fractial;
} else if (yoga::inexactEquals(fractial, 1.0)) {
scaledValue = scaledValue - fractial + 1.0;
} else if (forceCeil) {
// Next we check if we need to use forced rounding
scaledValue = scaledValue - fractial + 1.0;
} else if (forceFloor) {
scaledValue = scaledValue - fractial;
} else {
// Finally we just round the value
scaledValue = scaledValue - fractial +
(!std::isnan(fractial) &&
(fractial > 0.5 || yoga::inexactEquals(fractial, 0.5))
? 1.0
: 0.0);
}
return (std::isnan(scaledValue) || std::isnan(pointScaleFactor))
? YGUndefined
: (float) (scaledValue / pointScaleFactor);
}
void roundLayoutResultsToPixelGrid(
yoga::Node* const node,
const double pointScaleFactor,
const double absoluteLeft,
const double absoluteTop) {
if (pointScaleFactor == 0.0f) {
return;
}
const double nodeLeft = node->getLayout().position[YGEdgeLeft];
const double nodeTop = node->getLayout().position[YGEdgeTop];
const double nodeWidth = node->getLayout().dimensions[YGDimensionWidth];
const double nodeHeight = node->getLayout().dimensions[YGDimensionHeight];
const double absoluteNodeLeft = absoluteLeft + nodeLeft;
const double absoluteNodeTop = absoluteTop + nodeTop;
const double absoluteNodeRight = absoluteNodeLeft + nodeWidth;
const double absoluteNodeBottom = absoluteNodeTop + nodeHeight;
// If a node has a custom measure function we never want to round down its
// size as this could lead to unwanted text truncation.
const bool textRounding = node->getNodeType() == YGNodeTypeText;
node->setLayoutPosition(
roundValueToPixelGrid(nodeLeft, pointScaleFactor, false, textRounding),
YGEdgeLeft);
node->setLayoutPosition(
roundValueToPixelGrid(nodeTop, pointScaleFactor, false, textRounding),
YGEdgeTop);
// We multiply dimension by scale factor and if the result is close to the
// whole number, we don't have any fraction To verify if the result is close
// to whole number we want to check both floor and ceil numbers
const bool hasFractionalWidth =
!yoga::inexactEquals(fmod(nodeWidth * pointScaleFactor, 1.0), 0) &&
!yoga::inexactEquals(fmod(nodeWidth * pointScaleFactor, 1.0), 1.0);
const bool hasFractionalHeight =
!yoga::inexactEquals(fmod(nodeHeight * pointScaleFactor, 1.0), 0) &&
!yoga::inexactEquals(fmod(nodeHeight * pointScaleFactor, 1.0), 1.0);
node->setLayoutDimension(
roundValueToPixelGrid(
absoluteNodeRight,
pointScaleFactor,
(textRounding && hasFractionalWidth),
(textRounding && !hasFractionalWidth)) -
roundValueToPixelGrid(
absoluteNodeLeft, pointScaleFactor, false, textRounding),
YGDimensionWidth);
node->setLayoutDimension(
roundValueToPixelGrid(
absoluteNodeBottom,
pointScaleFactor,
(textRounding && hasFractionalHeight),
(textRounding && !hasFractionalHeight)) -
roundValueToPixelGrid(
absoluteNodeTop, pointScaleFactor, false, textRounding),
YGDimensionHeight);
const uint32_t childCount = YGNodeGetChildCount(node);
for (uint32_t i = 0; i < childCount; i++) {
roundLayoutResultsToPixelGrid(
node->getChild(i), pointScaleFactor, absoluteNodeLeft, absoluteNodeTop);
}
}
} // namespace facebook::yoga

View File

@@ -0,0 +1,30 @@
/*
* 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.
*/
#pragma once
#include <yoga/Yoga.h>
#include <yoga/node/Node.h>
namespace facebook::yoga {
// Round a point value to the nearest physical pixel based on DPI
// (pointScaleFactor)
float roundValueToPixelGrid(
const double value,
const double pointScaleFactor,
const bool forceCeil,
const bool forceFloor);
// Round the layout results of a node and its subtree to the pixel grid.
void roundLayoutResultsToPixelGrid(
yoga::Node* const node,
const double pointScaleFactor,
const double absoluteLeft,
const double absoluteTop);
} // namespace facebook::yoga

View File

@@ -33,12 +33,13 @@ void assertFatal(const bool condition, const char* message) {
}
void assertFatalWithNode(
const YGNodeRef node,
const YGNodeConstRef node,
const bool condition,
const char* message) {
if (!condition) {
yoga::log(
static_cast<yoga::Node*>(node),
// TODO: Break log callbacks and make them const correct
static_cast<yoga::Node*>(const_cast<YGNodeRef>(node)),
YGLogLevelFatal,
nullptr,
"%s\n",
@@ -48,7 +49,7 @@ void assertFatalWithNode(
}
void assertFatalWithConfig(
const YGConfigRef config,
YGConfigRef config,
const bool condition,
const char* message) {
if (!condition) {

View File

@@ -16,7 +16,10 @@ namespace facebook::yoga {
[[noreturn]] void fatalWithMessage(const char* message);
void assertFatal(bool condition, const char* message);
void assertFatalWithNode(YGNodeRef node, bool condition, const char* message);
void assertFatalWithNode(
YGNodeConstRef node,
bool condition,
const char* message);
void assertFatalWithConfig(
YGConfigRef config,
bool condition,

View File

@@ -222,10 +222,11 @@ YGSize Node::measure(
: measure_.noContext(this, width, widthMode, height, heightMode);
}
float Node::baseline(float width, float height, void* layoutContext) {
float Node::baseline(float width, float height, void* layoutContext) const {
return flags_.baselineUsesContext
? baseline_.withContext(this, width, height, layoutContext)
: baseline_.noContext(this, width, height);
? baseline_.withContext(
const_cast<Node*>(this), width, height, layoutContext)
: baseline_.noContext(const_cast<Node*>(this), width, height);
}
// Setters

View File

@@ -121,7 +121,7 @@ public:
return baseline_.noContext != nullptr;
}
float baseline(float width, float height, void* layoutContext);
float baseline(float width, float height, void* layoutContext) const;
bool hasErrata(YGErrata errata) const { return config_->hasErrata(errata); }
@@ -169,7 +169,9 @@ public:
}
}
Node* getChild(uint32_t index) const { return children_.at(index); }
Node* getChild(size_t index) const { return children_.at(index); }
size_t getChildCount() const { return children_.size(); }
Config* getConfig() const { return config_; }