Yoga is an embeddable and performant flexbox layout engine with bindings for multiple languages.
This repository provides a backport of Yoga
from C++20
to C++11
, enabling broader compatibility with older compilers and
outdated environments.
By maintaining the core functionality of Yoga while relying on C++11
features,
this project aims to make the library accessible to a wider range of developers
and systems, especially those that do not support the latest compilers and C++
standards.
In our imperfect world, the number of such systems is greater than it seems, and such a useful library as Yoga should be distributed as widely as possible.
Tip
Current state: based on 51e6095 commit (Mar 24, 2025)
Note
Tested on GCC 4.9.2
(Released: October 30, 2014)
Important
The main implementation targets C++11
, but to build the unit tests
(which rely on GoogleTest) needed
C++17
support.
Notable changes description (compared to mainstream Yoga (C++20))
-
yogacore
(Yoga C++ library) now forced to build with C++11 standard. See /yoga/CMakeLists.txt -
Nested namespaces described explicitly:
// C++20 namespace facebook::yoga { ... } // namespace facebook::yoga // C++11 namespace facebook { namespace yoga { ... } // namespace yoga } // namespace facebook
-
Binary literals replaced by hex:
// C++20 static constexpr uint16_t kHandleTypeMask = 0b0000'0000'0000'0111; static constexpr uint16_t kHandleIndexedMask = 0b0000'0000'0000'1000; static constexpr uint16_t kHandleValueMask = 0b1111'1111'1111'0000; // C++11 static constexpr uint16_t kHandleTypeMask = 0x0007; // 0b0000'0000'0000'0111; static constexpr uint16_t kHandleIndexedMask = 0x0008; // 0b0000'0000'0000'1000; static constexpr uint16_t kHandleValueMask = 0xFFF0; // 0b1111'1111'1111'0000;
-
Instead of std::bit_cast(), std::bit_width(), std::make_unique(), std::equal() used C++11-compatible analogues. See /yoga/compat/
-
Added additional curly braces in places where arrays initialized:
// C++20 std::array<uint32_t, BufferSize> buffer_{}; // C++11 std::array<uint32_t, BufferSize> buffer_{{}};
-
Removed
noexcept
specifier. Have no idea, why we cant have it here in C++11:// C++20 Node& operator=(Node&&) noexcept = default; // C++11 Node& operator=(Node&&) = default;
-
constexpr
functions-
Changed implementation of some
constexpr
functions/methods to be compatible with C++11 (for example: in C++11 we cannot useswitch
statement):// C++20 constexpr FloatOptional resolve(float referenceLength) { switch (unit_) { case Unit::Point: return value_; case Unit::Percent: return FloatOptional{value_.unwrap() * referenceLength * 0.01f}; default: return FloatOptional{}; } } // C++11 constexpr FloatOptional resolve(float referenceLength) { return (unit_ == Unit::Point) ? value_ : (unit_ == Unit::Percent) ? FloatOptional{value_.unwrap() * referenceLength * 0.01f} : FloatOptional{}; // default }
-
constexpr void
methods changed intoinline void
, since it not allowed in C++11:// C++20 constexpr void setType(Type handleType) { repr_ &= (~kHandleTypeMask); repr_ |= static_cast<uint8_t>(handleType); } // C++11 inline void setType(Type handleType) { repr_ &= (~kHandleTypeMask); repr_ |= static_cast<uint8_t>(handleType); }
-
Some
constexpr
functions with variables declaration in body (its not allowed in C++11) changed intoinline
, since I'm too lazy to replace them byconstexpr
implementation, compatible with C++11 :D// C++20 static constexpr uint16_t packInlineInteger(float value) { uint16_t isNegative = value < 0 ? 1 : 0; return static_cast<uint16_t>( (isNegative << 11) | (static_cast<int32_t>(value) * (isNegative != 0u ? -1 : 1))); } // C++11 static inline uint16_t packInlineInteger(float value) { uint16_t isNegative = value < 0 ? 1 : 0; return static_cast<uint16_t>( (isNegative << 11) | (static_cast<int32_t>(value) * (isNegative != 0u ? -1 : 1))); }
-
-
Concepts
-
Concepts was replaced by
static_assert()
s inside functions:// C++20 template <typename EnumT> concept HasOrdinality = (ordinalCount<EnumT>() > 0); template <HasOrdinality EnumT> constexpr int32_t bitCount() { return std::bit_width( static_cast< std::underlying_type_t<EnumT> >(ordinalCount<EnumT>() - 1) ); } // C++11 template <typename EnumT> constexpr bool HasOrdinality() { return ordinalCount<EnumT>() > 0; } template <typename EnumT> constexpr int32_t bitCount() { static_assert(HasOrdinality<EnumT>() == true, "EnumT not has ordinality"); return compat::bit_width( static_cast< typename std::underlying_type<EnumT>::type >(ordinalCount<EnumT>() - 1) ); }
// C++20 constexpr bool isUndefined(std::floating_point auto value) { return value != value; } // C++11 template <typename T> constexpr bool isUndefined(T value) { static_assert(std::is_floating_point<T>::value == true, "T must be floating-point"); return value != value; }
-
Some concepts too hard to properly implement in C++11, so single usage of std::input_iterator was simply commented :D
// C++20 static_assert(std::input_iterator<LayoutableChildren<T>::Iterator>); // C++11 //static_assert(std::input_iterator<LayoutableChildren<T>::Iterator>); // <-- FIXME: TOO HARD TO EXPRESS IT IN C++11
-
-
Operators
-
Added
operator !=
for a few structures, when it called somewhere in code. In C++20 compiler generates it implicitly, if we provideoperator ==
, but in C++11 we must write it explicitly:// C++20 constexpr bool operator==(const StyleLength& rhs) const { return value_ == rhs.value_ && unit_ == rhs.unit_; } // C++11 constexpr bool operator==(const StyleLength& rhs) const { return value_ == rhs.value_ && unit_ == rhs.unit_; } // Added explicit declaration and implementation constexpr bool operator!=(const StyleLength& rhs) const { return !(*this == rhs); }
-
default
comparison operators replaced by explicit implementation:// C++20 bool operator==(const Iterator& other) const = default; bool operator!=(const Iterator& other) const = default; // C++11 bool operator==(const Iterator& other) const { return this->e == other.e; } bool operator!=(const Iterator& other) const { return this->e != other.e; }
-
-
Designated initializers replaced by simple initialization:
// C++20 return FlexLine{ .itemsInFlow = std::move(itemsInFlow), .sizeConsumed = sizeConsumed, .numberOfAutoMargins = numberOfAutoMargins, .layout = FlexLineRunningLayout{ totalFlexGrowFactors, totalFlexShrinkScaledFactors, }}; // C++11 // Added constructor FlexLine( std::vector<yoga::Node*>&& itemsInFlow_, float sizeConsumed_, size_t numberOfAutoMargins_, const FlexLineRunningLayout& layout_ ) : itemsInFlow( std::move(itemsInFlow_) ) , sizeConsumed(sizeConsumed_) , numberOfAutoMargins(numberOfAutoMargins_) , layout(layout_) {} // Constructor call return FlexLine{ std::move(itemsInFlow), // itemsInFlow sizeConsumed, // sizeConsumed numberOfAutoMargins, // numberOfAutoMargins FlexLineRunningLayout{ // layout totalFlexGrowFactors, totalFlexShrinkScaledFactors, }};
// C++20 return { .width = maxOrDefined(0.0f, size.width), .height = maxOrDefined(0.0f, size.height)}; // C++11 return { maxOrDefined(0.0f, size.width), // width maxOrDefined(0.0f, size.height) // height };
-
auto
in templates replaced by explicit types:// C++20 template <auto LayoutMember> float getResolvedLayoutProperty(const YGNodeConstRef nodeRef, const Edge edge) // C++11 template <float (LayoutResults::*LayoutMember)(PhysicalEdge) const> float getResolvedLayoutProperty(const YGNodeConstRef nodeRef, const Edge edge)
// C++20 template <auto GetterT, auto SetterT, typename IdxT, typename ValueT> void updateStyle(YGNodeRef node, IdxT idx, ValueT value) // C++11 template < typename IdxT, typename ValueT, ValueT (Style::*GetterT)(IdxT) const, void (Style::*SetterT)(IdxT, ValueT) > void updateStyle(YGNodeRef node, IdxT idx, ValueT value) // --------------------------------------------------------------------- // C++20 updateStyle<&Style::margin, &Style::setMargin>(...) // C++11 updateStyle<Edge, Style::Length, &Style::margin, &Style::setMargin>(...)
-
Member bit-fields changes
// C++20 Direction direction_ : bitCount<Direction>() = Direction::Inherit; bool hadOverflow_ : 1 = false;
In C++11 we have the next restrictions:
- We cannot initialize bit-fields in-place, only in constructor.
- We cannot use scoped enums with restricted bit count.
That's why in C++11 we have the next boilerplate:
// C++11 // Member bit-fields initialization in constructor LayoutResults() : direction_{yoga::to_underlying(Direction::Inherit)} // Raw value initialized from scoped enum value , hadOverflow_{false} {} ... Direction direction() const { return static_cast<Direction>(direction_); // Added conversion: raw value --> scoped enum } void setDirection(Direction direction) { direction_ = yoga::to_underlying(direction); // Added conversion: scoped enum value --> raw value } ... // Raw type instead scoped enum as bit-field typename std::underlying_type<Direction>::type direction_ : bitCount<Direction>(); // Default: Direction::Inherit bool hadOverflow_ : 1; // Default: false
Detailed description - why we use as bit-field value type 'raw type' instead of 'scoped enum type'
In the next cases:
Direction direction_ : bitCount<Direction>(); // Default: Direction::Inherit
old compilers (like
gcc 4.9.2
) produces the next warning:'facebook::yoga::LayoutResults::direction_' is too small to hold all values of 'enum class facebook::yoga::Direction'
Types info:
enum class Direction : uint8_t { Inherit = YGDirectionInherit, // 0 LTR = YGDirectionLTR, // 1 RTL = YGDirectionRTL, // 2 }; template <> constexpr int32_t ordinalCount<Direction>() { return 3; }
So
bitCount<Direction>()
returns2
, because we have 3 enumerators andbit_width(3 - 1)
=bit_width(2)
= 2 bits.However, an
enum class Direction : uint8_t
promises it can hold any value representable byuint8_t
, i.e.0..255
. By sayingDirection direction_ : 2;
, we're telling the compiler you only have 2 bits of storage - but the full range ofuint8_t
(0..255) obviously won't fit in 2 bits. That discrepancy triggers the "too small to hold all values" warning/error.Possible solutions to fix it:
-
Using enum's
underlying_type
(raw type) instead:typename std::undrlying_type<Direction>::type direction_ : bitCount<Direction>();
This satisfies the compiler that
direction_
is just a 2-bit integer. But we control how it maps to/from theenum class Direction
. -
Stop forcing
Direction
to have an 8-bit underlying type, by declaring:enum class Direction { ... };
the compiler might still give
int
storage by default - but at least we're no longer guaranteeing it's an 8-bit type with a 0..255 range. Some compilers could store it in fewer bits internally, though that's not guaranteed. Even then, a bit-field of typeDirection
can still trigger warnings unless the compiler sees that the enum range fits exactly in the specified bits. -
Use a normal member, not a bit-field:
Direction direction_ = Direction::Inherit;
But without a bit-field we lose the space optimization, even the warning goes away.
Choosed solution - we use scoped enum's
underlying_type
(raw type) as bit-field value type.
To build the main library and run unit tests provided a wrapper:
./unit_tests <Debug|Release>
While not required, this script will use ninja if it is installed for faster builds.
Yoga is additionally part of the vcpkg collection of ports maintained by Microsoft and community contributors. If the version is out of date, please create an issue or pull request on the vcpkg repository.
Many of Yoga's tests are automatically generated, using HTML fixtures describing node structure. These are rendered in Chrome to generate an expected layout result for the tree. New fixtures can be added to gentest/fixtures
.
<div id="my_test" style="width: 100px; height: 100px; align-items: center;">
<div style="width: 50px; height: 50px;"></div>
</div>
To generate new tests from added fixtures:
- Ensure you have yarn classic installed.
- Run
yarn install
to install dependencies for the test generator. - Run
yarn gentest
in theyoga
directory.
Yoga provides a VSCode "launch.json" configuration which allows debugging unit tests. Simply add your breakpoints, and run "Debug C++ Unit tests (lldb)" (or "Debug C++ Unit tests (vsdbg)" on Windows).