From a2567af8b54f340a01a95d77f5acd7fae4b47d30 Mon Sep 17 00:00:00 2001 From: Aaron Dosser Date: Sat, 19 Jul 2025 12:32:59 -0400 Subject: [PATCH 1/6] Add tests, workflow, minor rewrite --- .github/workflows/tests.yml | 27 + CMakeLists.txt | 19 + README.md | 2 +- examples/child_iteration.cpp | 21 +- examples/print_dimensions.cpp | 16 +- include/yoga-cpp/yoga.hpp | 1059 +++++++++++++-------------------- test/layout.cpp | 150 +++++ 7 files changed, 638 insertions(+), 656 deletions(-) create mode 100644 .github/workflows/tests.yml create mode 100644 test/layout.cpp diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..511371e --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,27 @@ +name: Run C++ Tests + +on: + push: + branches: [ "main" ] + pull_request: # No branch filter means this runs for all PRs + +jobs: + build-and-test: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + # Fetch all history for all tags and branches + fetch-depth: 0 + + - name: Configure CMake + run: cmake -B build -S . -D YOGACPP_BUILD_TESTS=ON + + - name: Build project + run: cmake --build build + + - name: Run tests + working-directory: ./build + run: ctest --output-on-failure diff --git a/CMakeLists.txt b/CMakeLists.txt index 686e972..2850cd3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,6 +23,7 @@ target_link_libraries(yoga_cpp PUBLIC yogacore) target_include_directories(yoga_cpp PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) option(YOGACPP_BUILD_EXAMPLES "Build example program(s)" OFF) +option(YOGACPP_BUILD_TESTS "Build tests" OFF) if (YOGACPP_BUILD_EXAMPLES) add_executable(print_dimensions ${CMAKE_CURRENT_SOURCE_DIR}/examples/print_dimensions.cpp) @@ -31,3 +32,21 @@ if (YOGACPP_BUILD_EXAMPLES) add_executable(child_iteration ${CMAKE_CURRENT_SOURCE_DIR}/examples/child_iteration.cpp) target_link_libraries(child_iteration PRIVATE yoga_cpp) endif() + +if (YOGACPP_BUILD_TESTS) + include(FetchContent) + FetchContent_Declare( + googletest + GIT_REPOSITORY https://github.com/google/googletest.git + GIT_TAG v1.17.0 + ) + # For Windows: Prevent overriding the parent project's compiler/linker settings + set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) + FetchContent_MakeAvailable(googletest) + + add_executable(yoga_cpp_tests test/layout.cpp) + target_link_libraries(yoga_cpp_tests GTest::gtest_main yoga_cpp yogacore) + + include(GoogleTest) + gtest_discover_tests(yoga_cpp_tests) +endif() \ No newline at end of file diff --git a/README.md b/README.md index 02cb837..8a0d3ad 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ The project will download and link Yoga for you, so use yoga-cpp as a drop-in re ## Barebones Example ```c++ -#include +#include #include #include diff --git a/examples/child_iteration.cpp b/examples/child_iteration.cpp index 4d0b12f..01d7518 100644 --- a/examples/child_iteration.cpp +++ b/examples/child_iteration.cpp @@ -1,31 +1,22 @@ -#include -#include #include +#include +#include -// define a layout type that stores a name as a context using Layout = Yoga::Layout; int main() { Layout layout; - auto node = layout.createNode(); - layout.addToRoot(node); + auto root = layout.createNode("parent"); for (int i = 0; i < 10; i++) { - auto child = layout.createNode(); - auto ctx = child.getContext(); - if (ctx) - { - auto& ctxRef = ctx->get(); - ctxRef.append(std::format("child{}", i)); - } - node.insertChild(child); + root.createChild(std::format("child{}", i)); } - for (auto child : node.getChildren()) + for (auto child : root.getChildren()) { - std::cout << child.getContext()->get().c_str() << '\n'; + std::cout << child.getContext().c_str() << '\n'; } return 0; diff --git a/examples/print_dimensions.cpp b/examples/print_dimensions.cpp index e016ff2..f3b68ba 100644 --- a/examples/print_dimensions.cpp +++ b/examples/print_dimensions.cpp @@ -1,20 +1,22 @@ -#include -#include #include +#include +#include -struct Empty {}; -// define a layout type with no context -using Layout = Yoga::Layout; +using Layout = Yoga::Layout; int main() { Layout layout; auto node = layout.createNode(); - layout.addToRoot(node); + + node.setWidthPercent(100.f); + node.setHeightPercent(100.f); node.setWidthPercent(50.f); node.setHeightPercent(50.f); - layout.calculate(100.f, 100.f); + + node.calculateLayout(100.f, 100.f); + const auto dimensions = std::format( "X: {}, Y: {}, W: {}, H: {}", node.getLayoutLeft(), diff --git a/include/yoga-cpp/yoga.hpp b/include/yoga-cpp/yoga.hpp index 0a7a3bc..eb4414b 100644 --- a/include/yoga-cpp/yoga.hpp +++ b/include/yoga-cpp/yoga.hpp @@ -1,191 +1,306 @@ #pragma once -#include #include -#include -#include +#include +#include #include -#include -#include -#include +#include + + #include "yoga/Yoga.h" namespace Yoga { - template - concept DefaultConstructible = std::is_default_constructible_v; - - template - class Config + template + class ChildIterator { public: - Config() : _ygConfig{YGConfigNew()} { setContext(&_context); } - ~Config() + using iterator_category = std::random_access_iterator_tag; + using value_type = Node; + using difference_type = std::ptrdiff_t; + using pointer = Node*; + using reference = Node; + + ChildIterator() : _index{0}, _size{0} {} + + explicit ChildIterator(const Node& parent) : _parent{parent}, _index{0}, _size{parent.getChildCount()} {} + + explicit ChildIterator(const Node& parent, const difference_type index, const size_t size) : + _parent{parent}, _index(index), _size(size) { - if (_ygConfig != nullptr) - YGConfigFree(_ygConfig); } - Config(const Config&) = delete; - Config& operator=(const Config&) = delete; + reference operator*() const + { + assert(_index >= 0 && static_cast(_index) < _size && "Iterator out of bounds"); + return _parent.getChild(_index); + } - Config(Config&& other) noexcept : _ygConfig{other._ygConfig}, _context{std::move(other._context)} + pointer operator->() { - other._ygConfig = nullptr; - if (this->_ygConfig != nullptr) - { - YGConfigSetContext(_ygConfig, &_context); - } + assert(_index >= 0 && static_cast(_index) < _size && "Iterator out of bounds"); + _proxyNode = _parent.getChild(_index); + return &_proxyNode; } - Config& operator=(Config&& other) noexcept + + reference operator[](const difference_type offset) const { - if (this != &other) - { - if (this->_ygConfig != nullptr) - { - YGConfigFree(_ygConfig); - } - _ygConfig = other._ygConfig; - other._ygConfig = nullptr; - _context = std::move(other._context); - if (this->_ygConfig != nullptr) - { - YGConfigSetContext(_ygConfig, &_context); - } - } + const auto newIndex = _index + offset; + assert(newIndex >= 0 && static_cast(newIndex) < _size && "Iterator out of bounds"); + return _parent.getChild(newIndex); + } + + ChildIterator& operator++() + { + ++_index; + return *this; + } + + ChildIterator operator++(int) + { + ChildIterator temp = *this; + ++(*this); + return temp; + } + + ChildIterator& operator--() + { + --_index; + return *this; + } + + ChildIterator operator--(int) + { + ChildIterator temp = *this; + --(*this); + return temp; + } + + ChildIterator& operator+=(const difference_type offset) + { + _index += offset; + return *this; + } + + ChildIterator& operator-=(const difference_type offset) + { + _index -= offset; return *this; } - void setUseWebDefaults(const bool useWebDefaults) { YGConfigSetUseWebDefaults(_ygConfig, useWebDefaults); } - [[nodiscard]] bool getUseWebDefaults() const { return YGConfigGetUseWebDefaults(_ygConfig); } + friend ChildIterator operator+(const ChildIterator& it, difference_type offset) + { + return ChildIterator{it._parent, it._index + offset, it._size}; + } - void setPointScaleFactor(const float pointScaleFactor) + friend ChildIterator operator+(difference_type offset, const ChildIterator& it) { return it + offset; } + + friend ChildIterator operator-(const ChildIterator& it, difference_type offset) { - YGConfigSetPointScaleFactor(_ygConfig, pointScaleFactor); + return ChildIterator{it._parent, it._index - offset, it._size}; } - [[nodiscard]] float getPointScaleFactor() const { return YGConfigGetPointScaleFactor(_ygConfig); } - void setErrata(const YGErrata errata) { YGConfigSetErrata(_ygConfig, errata); } - [[nodiscard]] YGErrata getErrata() const { return YGConfigGetErrata(_ygConfig); } + difference_type operator-(const ChildIterator& other) const { return _index - other._index; } - void setLogger(const YGLogger logger) { YGConfigSetLogger(_ygConfig, logger); } + bool operator==(const ChildIterator& other) const { return _index == other._index; } + bool operator!=(const ChildIterator& other) const { return _index != other._index; } + bool operator<(const ChildIterator& other) const { return _index < other._index; } + bool operator>(const ChildIterator& other) const { return _index > other._index; } + bool operator<=(const ChildIterator& other) const { return _index <= other._index; } + bool operator>=(const ChildIterator& other) const { return _index >= other._index; } - void setContext(Ctx* context) { YGConfigSetContext(_ygConfig, context); } + private: + Node _parent; + difference_type _index; + size_t _size; + Node _proxyNode; + }; + + template + class ChildRange : public std::ranges::view_interface> + { + public: + ChildRange() = default; - std::optional> getContext() noexcept + explicit ChildRange(const Node& parent) : _parent{parent} {} + + ChildIterator begin() const { - auto* ctxPtr = YGConfigGetContext(_ygConfig); - if (ctxPtr == nullptr) + if (!_parent.valid()) { - return std::nullopt; + return ChildIterator{}; } - return std::ref(*static_cast(ctxPtr)); + + return ChildIterator{_parent}; } - std::optional> getContext() const noexcept + ChildIterator end() const { - auto* ctxPtr = YGConfigGetContext(_ygConfig); - if (ctxPtr == nullptr) + if (!_parent.valid()) { - return std::nullopt; + return ChildIterator{}; } - return std::ref(*static_cast(ctxPtr)); - } - [[nodiscard]] YGConfigRef getRef() const noexcept { return _ygConfig; } - [[nodiscard]] YGConfigConstRef getConstRef() const noexcept { return _ygConfig; } + const auto count = _parent.getChildCount(); + + return ChildIterator{_parent, static_cast(count), count}; + } private: - YGConfigRef _ygConfig; - Ctx _context; + Node _parent; }; - /** - * Non-owning representation of a Yoga node. - * Contains most of the Yoga API as methods. - * You should acquire one from a Yoga::Layout. - * @tparam Ctx Context type stored per node - must be default constructible - */ - template - class Node + template + class Layout; + + template + class Node; + + template + class Layout { public: - template - friend class Layout; + using context_type = Ctx; + using node_type = Node; + + Layout() = default; + ~Layout() + { + for (auto& [nodeRef, context] : _nodeContexts) + { + YGNodeFree(nodeRef); + } + _nodeContexts.clear(); + } - template - friend class OwningNode; + // Pin layout in memory since it's a manager. + Layout(const Layout&) = delete; + Layout& operator=(const Layout&) = delete; + Layout(Layout&&) = delete; + Layout& operator=(Layout&&) = delete; - using context_type = Ctx; + template + node_type createNode(Args&&... args) + { + auto ygNode = YGNodeNew(); + _nodeContexts.try_emplace(ygNode, std::make_unique(std::forward(args)...)); + auto node = node_type{this, ygNode}; + node.setContext(_nodeContexts.at(ygNode).get()); + return node; + } + + void destroyNode(node_type& node) + { + if (node.get() == nullptr) + return; + + auto it = _nodeContexts.find(node.get()); + assert(it != _nodeContexts.end() && "Layout does not contain this node"); + if (it != _nodeContexts.end()) + { + YGNodeFree(it->first); + _nodeContexts.erase(it); + node.invalidate(); + } + } - Node() = delete; + private: + std::unordered_map> _nodeContexts; + }; - explicit Node(YGNodeRef ref) noexcept : _ygNode{ref} {} - ~Node() = default; + template + class Node + { + public: + using layout_type = Layout; + using context_type = typename layout_type::context_type; - Node(const Node& other) = default; - Node& operator=(const Node& other) = default; - Node(Node&& other) noexcept = default; - Node& operator=(Node&& other) noexcept = default; + Node() : _layout{nullptr}, _node{nullptr} {} - explicit operator bool() const noexcept { return _ygNode != nullptr; } + /** + * @brief Checks if two Node handles refer to the same underlying Yoga node. + * + * This allows Nodes to be used in GTest assertions like EXPECT_EQ. + * + * @param other The other node handle to compare against. + * @return True if both handles point to the same node, false otherwise. + */ + bool operator==(const Node& other) const noexcept { return get() == other.get(); } /** - * Iterator implementation for node children + * @brief Checks if two Node handles refer to different underlying Yoga nodes. + * @param other The other node handle to compare against. + * @return True if the handles point to different nodes, false otherwise. */ - class ChildIterator + bool operator!=(const Node& other) const noexcept { return !(*this == other); } + + + [[nodiscard]] bool valid() const noexcept { return _layout != nullptr && _node != nullptr; } + + context_type& getContext() noexcept { - public: - ChildIterator() = delete; + assert_valid(); + return *static_cast(YGNodeGetContext(_node)); + } - ChildIterator(YGNodeRef node, const size_t index) noexcept : _ygNode{node}, _index{index} {} + const context_type& getContext() const noexcept + { + assert_valid(); + return *YGNodeGetContext(_node); + } - ChildIterator& operator++() noexcept - { - ++_index; - return *this; - } + void setContext(context_type* ctxPtr) { YGNodeSetContext(_node, ctxPtr); } - ChildIterator operator++(int) noexcept - { - ChildIterator tmp = *this; - ++_index; - return tmp; - } + [[nodiscard]] YGNodeRef get() const noexcept { return _node; } - constexpr bool operator!=(const ChildIterator& other) const noexcept - { - return _ygNode != other._ygNode || _index != other._index; - } - constexpr bool operator==(const ChildIterator& other) const noexcept - { - return _ygNode == other._ygNode && _index == other._index; - } + [[nodiscard]] size_t getChildCount() const noexcept + { + assert_valid(); + return YGNodeGetChildCount(_node); + } - Node operator*() const noexcept { return Node{YGNodeGetChild(_ygNode, _index)}; } + Node getChild(const size_t index) const + { + assert_valid(); + return Node{_layout, YGNodeGetChild(_node, index)}; + } - constexpr std::ptrdiff_t operator-(const ChildIterator& other) const noexcept - { - return static_cast(_index) - static_cast(other._index); - } + ChildRange getChildren() + { + assert_valid(); + return ChildRange{*this}; + } - private: - YGNodeRef _ygNode; - size_t _index; - }; + Node getParent() + { + assert_valid(); + return Node{_layout, YGNodeGetParent(_node)}; + } - class ChildView : public std::ranges::view_interface + void insertChild(const Node& child, const size_t index = 0) { - public: - explicit ChildView(const Node& node) noexcept : _ygNode{node.getRef()} {} + assert_valid(); + child.assert_valid(); + assert(_layout == child._layout && "Nodes must belong to the same layout"); + YGNodeInsertChild(_node, child.get(), index); + } - auto begin() const noexcept { return ChildIterator{_ygNode, 0}; } - auto end() const noexcept { return ChildIterator{_ygNode, YGNodeGetChildCount(_ygNode)}; } + void removeChild(const Node& child) + { + assert_valid(); + child.assert_valid(); + YGNodeRemoveChild(_node, child.get()); + } - private: - YGNodeRef _ygNode; - }; + template + Node createChild(Args&&... args) + { + assert_valid(); + auto child = _layout->createNode(std::forward(args)...); + insertChild(child, getChildCount()); + return child; + } /** * Resets this node to its original state. @@ -196,8 +311,8 @@ namespace Yoga */ void reset() noexcept { - assert(_ygNode != nullptr && "Reset called on invalid node"); - YGNodeReset(_ygNode); + assert_valid(); + YGNodeReset(_node); } /** @@ -209,8 +324,8 @@ namespace Yoga void calculateLayout(const float width, const float height, const YGDirection direction = YGDirectionLTR) noexcept { - assert(_ygNode != nullptr && "Calculate layout called on invalid node"); - YGNodeCalculateLayout(_ygNode, width, height, direction); + assert_valid(); + YGNodeCalculateLayout(_node, width, height, direction); } /** @@ -218,8 +333,8 @@ namespace Yoga */ [[nodiscard]] bool hasNewLayout() const noexcept { - assert(_ygNode && "must be a valid node"); - return YGNodeGetHasNewLayout(_ygNode); + assert_valid(); + return YGNodeGetHasNewLayout(_node); } /** @@ -227,31 +342,22 @@ namespace Yoga */ void setHasNewLayout(const bool hasNewLayout) noexcept { - assert(_ygNode && "must be a valid node"); - YGNodeSetHasNewLayout(_ygNode, hasNewLayout); + assert_valid(); + YGNodeSetHasNewLayout(_node, hasNewLayout); } /** * @return Whether the node has changes that require recalculating its layout */ - [[nodiscard]] bool isDirty() const noexcept { return YGNodeIsDirty(_ygNode); } + [[nodiscard]] bool isDirty() const noexcept { return YGNodeIsDirty(_node); } /** * Marks a node as requiring a layout recalculation */ void markDirty() noexcept { - assert(_ygNode && "must be a valid node"); - YGNodeMarkDirty(_ygNode); - } - - /** - * @return The number of children associated with this node - */ - [[nodiscard]] size_t getChildCount() const noexcept - { - assert(_ygNode && "must be a valid node"); - return YGNodeGetChildCount(_ygNode); + assert_valid(); + YGNodeMarkDirty(_node); } /** @@ -259,8 +365,8 @@ namespace Yoga */ void setNodeType(const YGNodeType nodeType) noexcept { - assert(_ygNode && "must be a valid node"); - YGNodeSetNodeType(_ygNode, nodeType); + assert_valid(); + YGNodeSetNodeType(_node, nodeType); } /** @@ -268,8 +374,8 @@ namespace Yoga */ [[nodiscard]] YGNodeType getNodeType() const noexcept { - assert(_ygNode && "must be a valid node"); - return YGNodeGetNodeType(_ygNode); + assert_valid(); + return YGNodeGetNodeType(_node); } /** @@ -277,8 +383,8 @@ namespace Yoga */ [[nodiscard]] float getLayoutLeft() const noexcept { - assert(_ygNode && "must be a valid node"); - return YGNodeLayoutGetLeft(_ygNode); + assert_valid(); + return YGNodeLayoutGetLeft(_node); } /** @@ -286,8 +392,8 @@ namespace Yoga */ [[nodiscard]] float getLayoutTop() const noexcept { - assert(_ygNode && "must be a valid node"); - return YGNodeLayoutGetTop(_ygNode); + assert_valid(); + return YGNodeLayoutGetTop(_node); } /** @@ -295,8 +401,8 @@ namespace Yoga */ [[nodiscard]] float getLayoutWidth() const noexcept { - assert(_ygNode && "must be a valid node"); - return YGNodeLayoutGetWidth(_ygNode); + assert_valid(); + return YGNodeLayoutGetWidth(_node); } /** @@ -304,8 +410,8 @@ namespace Yoga */ [[nodiscard]] float getLayoutHeight() const noexcept { - assert(_ygNode && "must be a valid node"); - return YGNodeLayoutGetHeight(_ygNode); + assert_valid(); + return YGNodeLayoutGetHeight(_node); } /** @@ -313,8 +419,8 @@ namespace Yoga */ [[nodiscard]] float getLayoutBottom() const noexcept { - assert(_ygNode && "must be a valid node"); - return YGNodeLayoutGetBottom(_ygNode); + assert_valid(); + return YGNodeLayoutGetBottom(_node); } /** @@ -322,8 +428,8 @@ namespace Yoga */ [[nodiscard]] float getLayoutRight() const noexcept { - assert(_ygNode && "must be a valid node"); - return YGNodeLayoutGetRight(_ygNode); + assert_valid(); + return YGNodeLayoutGetRight(_node); } /** @@ -331,8 +437,8 @@ namespace Yoga */ [[nodiscard]] YGDirection getLayoutDirection() const noexcept { - assert(_ygNode && "must be a valid node"); - return YGNodeLayoutGetDirection(_ygNode); + assert_valid(); + return YGNodeLayoutGetDirection(_node); } /** @@ -341,8 +447,8 @@ namespace Yoga */ [[nodiscard]] float getLayoutMargin(const YGEdge edge) const noexcept { - assert(_ygNode && "must be a valid node"); - return YGNodeLayoutGetMargin(_ygNode, edge); + assert_valid(); + return YGNodeLayoutGetMargin(_node, edge); } /** @@ -351,8 +457,8 @@ namespace Yoga */ [[nodiscard]] float getLayoutBorder(const YGEdge edge) const noexcept { - assert(_ygNode && "must be a valid node"); - return YGNodeLayoutGetBorder(_ygNode, edge); + assert_valid(); + return YGNodeLayoutGetBorder(_node, edge); } /** @@ -361,8 +467,8 @@ namespace Yoga */ [[nodiscard]] float getLayoutPadding(const YGEdge edge) const noexcept { - assert(_ygNode && "must be a valid node"); - return YGNodeLayoutGetPadding(_ygNode, edge); + assert_valid(); + return YGNodeLayoutGetPadding(_node, edge); } /** @@ -371,8 +477,8 @@ namespace Yoga */ void copyStyle(const Node& other) noexcept { - assert(_ygNode && "must be a valid node"); - YGNodeCopyStyle(_ygNode, other._ygNode); + assert_valid(); + YGNodeCopyStyle(_node, other._node); } /** @@ -382,8 +488,8 @@ namespace Yoga */ void setDirection(const YGDirection direction) noexcept { - assert(_ygNode && "must be a valid node"); - YGNodeStyleSetDirection(_ygNode, direction); + assert_valid(); + YGNodeStyleSetDirection(_node, direction); } /** @@ -391,8 +497,8 @@ namespace Yoga */ [[nodiscard]] YGDirection getDirection() const noexcept { - assert(_ygNode && "must be a valid node"); - return YGNodeStyleGetDirection(_ygNode); + assert_valid(); + return YGNodeStyleGetDirection(_node); } /** @@ -400,8 +506,8 @@ namespace Yoga */ void setFlexDirection(const YGFlexDirection flexDirection) noexcept { - assert(_ygNode && "must be a valid node"); - YGNodeStyleSetFlexDirection(_ygNode, flexDirection); + assert_valid(); + YGNodeStyleSetFlexDirection(_node, flexDirection); } /** @@ -409,8 +515,8 @@ namespace Yoga */ [[nodiscard]] YGFlexDirection getFlexDirection() const noexcept { - assert(_ygNode && "must be a valid node"); - return YGNodeStyleGetFlexDirection(_ygNode); + assert_valid(); + return YGNodeStyleGetFlexDirection(_node); } /** @@ -422,8 +528,8 @@ namespace Yoga */ void setJustifyContent(const YGJustify justifyContent) noexcept { - assert(_ygNode && "must be a valid node"); - YGNodeStyleSetJustifyContent(_ygNode, justifyContent); + assert_valid(); + YGNodeStyleSetJustifyContent(_node, justifyContent); } /** @@ -435,8 +541,8 @@ namespace Yoga */ [[nodiscard]] YGJustify getJustifyContent() const noexcept { - assert(_ygNode && "must be a valid node"); - return YGNodeStyleGetJustifyContent(_ygNode); + assert_valid(); + return YGNodeStyleGetJustifyContent(_node); } /** @@ -445,8 +551,8 @@ namespace Yoga */ void setAlignContent(const YGAlign alignContent) noexcept { - assert(_ygNode && "must be a valid node"); - YGNodeStyleSetAlignContent(_ygNode, alignContent); + assert_valid(); + YGNodeStyleSetAlignContent(_node, alignContent); } /** @@ -459,8 +565,8 @@ namespace Yoga */ [[nodiscard]] YGAlign getAlignContent() const noexcept { - assert(_ygNode && "must be a valid node"); - return YGNodeStyleGetAlignContent(_ygNode); + assert_valid(); + return YGNodeStyleGetAlignContent(_node); } /** @@ -472,8 +578,8 @@ namespace Yoga */ void setAlignItems(const YGAlign alignItems) noexcept { - assert(_ygNode && "must be a valid node"); - YGNodeStyleSetAlignItems(_ygNode, alignItems); + assert_valid(); + YGNodeStyleSetAlignItems(_node, alignItems); } /** @@ -486,8 +592,8 @@ namespace Yoga */ [[nodiscard]] YGAlign getAlignItems() const noexcept { - assert(_ygNode && "must be a valid node"); - return YGNodeStyleGetAlignItems(_ygNode); + assert_valid(); + return YGNodeStyleGetAlignItems(_node); } /** @@ -495,8 +601,8 @@ namespace Yoga */ void setAlignSelf(const YGAlign alignSelf) noexcept { - assert(_ygNode && "must be a valid node"); - YGNodeStyleSetAlignSelf(_ygNode, alignSelf); + assert_valid(); + YGNodeStyleSetAlignSelf(_node, alignSelf); } /** @@ -504,8 +610,8 @@ namespace Yoga */ [[nodiscard]] YGAlign getAlignSelf() const noexcept { - assert(_ygNode && "must be a valid node"); - return YGNodeStyleGetAlignSelf(_ygNode); + assert_valid(); + return YGNodeStyleGetAlignSelf(_node); } /** @@ -513,8 +619,8 @@ namespace Yoga */ void setPositionType(const YGPositionType positionType) noexcept { - assert(_ygNode && "must be a valid node"); - YGNodeStyleSetPositionType(_ygNode, positionType); + assert_valid(); + YGNodeStyleSetPositionType(_node, positionType); } /** @@ -522,8 +628,8 @@ namespace Yoga */ [[nodiscard]] YGPositionType getPositionType() const noexcept { - assert(_ygNode && "must be a valid node"); - return YGNodeStyleGetPositionType(_ygNode); + assert_valid(); + return YGNodeStyleGetPositionType(_node); } /** @@ -531,8 +637,8 @@ namespace Yoga */ void setFlexWrap(const YGWrap flexWrap) noexcept { - assert(_ygNode && "must be a valid node"); - YGNodeStyleSetFlexWrap(_ygNode, flexWrap); + assert_valid(); + YGNodeStyleSetFlexWrap(_node, flexWrap); } /** @@ -540,8 +646,8 @@ namespace Yoga */ [[nodiscard]] YGWrap getFlexWrap() const noexcept { - assert(_ygNode && "must be a valid node"); - return YGNodeStyleGetFlexWrap(_ygNode); + assert_valid(); + return YGNodeStyleGetFlexWrap(_node); } /** @@ -549,8 +655,8 @@ namespace Yoga */ void setOverflow(const YGOverflow overflow) noexcept { - assert(_ygNode && "must be a valid node"); - YGNodeStyleSetOverflow(_ygNode, overflow); + assert_valid(); + YGNodeStyleSetOverflow(_node, overflow); } /** @@ -558,8 +664,8 @@ namespace Yoga */ [[nodiscard]] YGOverflow getOverflow() const noexcept { - assert(_ygNode && "must be a valid node"); - return YGNodeStyleGetOverflow(_ygNode); + assert_valid(); + return YGNodeStyleGetOverflow(_node); } /** @@ -567,8 +673,8 @@ namespace Yoga */ void setDisplay(const YGDisplay display) noexcept { - assert(_ygNode && "must be a valid node"); - YGNodeStyleSetDisplay(_ygNode, display); + assert_valid(); + YGNodeStyleSetDisplay(_node, display); } /** @@ -576,8 +682,8 @@ namespace Yoga */ [[nodiscard]] YGDisplay getDisplay() const noexcept { - assert(_ygNode && "must be a valid node"); - return YGNodeStyleGetDisplay(_ygNode); + assert_valid(); + return YGNodeStyleGetDisplay(_node); } /** @@ -585,8 +691,8 @@ namespace Yoga */ void setFlex(const float flex) noexcept { - assert(_ygNode && "must be a valid node"); - YGNodeStyleSetFlex(_ygNode, flex); + assert_valid(); + YGNodeStyleSetFlex(_node, flex); } /** @@ -594,8 +700,8 @@ namespace Yoga */ [[nodiscard]] float getFlex() const noexcept { - assert(_ygNode && "must be a valid node"); - return YGNodeStyleGetFlex(_ygNode); + assert_valid(); + return YGNodeStyleGetFlex(_node); } /** @@ -603,8 +709,8 @@ namespace Yoga */ void setFlexGrow(const float flexGrow) noexcept { - assert(_ygNode && "must be a valid node"); - YGNodeStyleSetFlexGrow(_ygNode, flexGrow); + assert_valid(); + YGNodeStyleSetFlexGrow(_node, flexGrow); } /** @@ -612,8 +718,8 @@ namespace Yoga */ [[nodiscard]] float getFlexGrow() const noexcept { - assert(_ygNode && "must be a valid node"); - return YGNodeStyleGetFlexGrow(_ygNode); + assert_valid(); + return YGNodeStyleGetFlexGrow(_node); } /** @@ -621,8 +727,8 @@ namespace Yoga */ void setFlexShrink(const float flexShrink) noexcept { - assert(_ygNode && "must be a valid node"); - YGNodeStyleSetFlexShrink(_ygNode, flexShrink); + assert_valid(); + YGNodeStyleSetFlexShrink(_node, flexShrink); } /** @@ -630,8 +736,8 @@ namespace Yoga */ [[nodiscard]] float getFlexShrink() const noexcept { - assert(_ygNode && "must be a valid node"); - return YGNodeStyleGetFlexShrink(_ygNode); + assert_valid(); + return YGNodeStyleGetFlexShrink(_node); } /** @@ -639,8 +745,8 @@ namespace Yoga */ void setFlexBasis(const float flexBasis) noexcept { - assert(_ygNode && "must be a valid node"); - YGNodeStyleSetFlexBasis(_ygNode, flexBasis); + assert_valid(); + YGNodeStyleSetFlexBasis(_node, flexBasis); } /** @@ -648,8 +754,8 @@ namespace Yoga */ void setFlexBasisPercent(const float flexBasisPercent) noexcept { - assert(_ygNode && "must be a valid node"); - YGNodeStyleSetFlexBasisPercent(_ygNode, flexBasisPercent); + assert_valid(); + YGNodeStyleSetFlexBasisPercent(_node, flexBasisPercent); } /** @@ -657,8 +763,8 @@ namespace Yoga */ void setFlexBasisAuto() noexcept { - assert(_ygNode && "must be a valid node"); - YGNodeStyleSetFlexBasisAuto(_ygNode); + assert_valid(); + YGNodeStyleSetFlexBasisAuto(_node); } /** @@ -666,8 +772,8 @@ namespace Yoga */ [[nodiscard]] YGValue getFlexBasis() const noexcept { - assert(_ygNode && "must be a valid node"); - return YGNodeStyleGetFlexBasis(_ygNode); + assert_valid(); + return YGNodeStyleGetFlexBasis(_node); } /** @@ -680,8 +786,8 @@ namespace Yoga */ void setPosition(const YGEdge edge, const float position) noexcept { - assert(_ygNode && "must be a valid node"); - YGNodeStyleSetPosition(_ygNode, edge, position); + assert_valid(); + YGNodeStyleSetPosition(_node, edge, position); } /** @@ -694,8 +800,8 @@ namespace Yoga */ void setPositionPercent(const YGEdge edge, const float positionPercent) noexcept { - assert(_ygNode && "must be a valid node"); - YGNodeStyleSetPositionPercent(_ygNode, edge, positionPercent); + assert_valid(); + YGNodeStyleSetPositionPercent(_node, edge, positionPercent); } /** @@ -705,8 +811,8 @@ namespace Yoga */ void setPositionAuto(const YGEdge edge) noexcept { - assert(_ygNode && "must be a valid node"); - YGNodeStyleSetPositionAuto(_ygNode, edge); + assert_valid(); + YGNodeStyleSetPositionAuto(_node, edge); } /** @@ -718,8 +824,8 @@ namespace Yoga */ [[nodiscard]] YGValue getPosition(const YGEdge edge) const noexcept { - assert(_ygNode && "must be a valid node"); - return YGNodeStyleGetPosition(_ygNode, edge); + assert_valid(); + return YGNodeStyleGetPosition(_node, edge); } /** @@ -732,8 +838,8 @@ namespace Yoga */ void setMargin(const YGEdge edge, const float margin) noexcept { - assert(_ygNode && "must be a valid node"); - YGNodeStyleSetMargin(_ygNode, edge, margin); + assert_valid(); + YGNodeStyleSetMargin(_node, edge, margin); } /** @@ -743,8 +849,8 @@ namespace Yoga */ void setMarginPercent(const YGEdge edge, const float marginPercent) noexcept { - assert(_ygNode && "must be a valid node"); - YGNodeStyleSetMarginPercent(_ygNode, edge, marginPercent); + assert_valid(); + YGNodeStyleSetMarginPercent(_node, edge, marginPercent); } /** @@ -757,8 +863,8 @@ namespace Yoga */ void setMarginAuto(const YGEdge edge) noexcept { - assert(_ygNode && "must be a valid node"); - YGNodeStyleSetMarginAuto(_ygNode, edge); + assert_valid(); + YGNodeStyleSetMarginAuto(_node, edge); } /** @@ -769,8 +875,8 @@ namespace Yoga */ [[nodiscard]] YGValue getMargin(const YGEdge edge) const noexcept { - assert(_ygNode && "must be a valid node"); - return YGNodeStyleGetMargin(_ygNode, edge); + assert_valid(); + return YGNodeStyleGetMargin(_node, edge); } /** @@ -784,8 +890,8 @@ namespace Yoga */ void setPadding(const YGEdge edge, const float padding) noexcept { - assert(_ygNode && "must be a valid node"); - YGNodeStyleSetPadding(_ygNode, edge, padding); + assert_valid(); + YGNodeStyleSetPadding(_node, edge, padding); } /** @@ -798,8 +904,8 @@ namespace Yoga */ void setPaddingPercent(const YGEdge edge, const float paddingPercent) noexcept { - assert(_ygNode && "must be a valid node"); - YGNodeStyleSetPaddingPercent(_ygNode, edge, paddingPercent); + assert_valid(); + YGNodeStyleSetPaddingPercent(_node, edge, paddingPercent); } /** @@ -809,8 +915,8 @@ namespace Yoga */ [[nodiscard]] YGValue getPadding(const YGEdge edge) const noexcept { - assert(_ygNode && "must be a valid node"); - return YGNodeStyleGetPadding(_ygNode, edge); + assert_valid(); + return YGNodeStyleGetPadding(_node, edge); } /** @@ -821,8 +927,8 @@ namespace Yoga */ void setBorder(const YGEdge edge, const float border) noexcept { - assert(_ygNode && "must be a valid node"); - YGNodeStyleSetBorder(_ygNode, edge, border); + assert_valid(); + YGNodeStyleSetBorder(_node, edge, border); } /** @@ -833,8 +939,8 @@ namespace Yoga */ [[nodiscard]] float getBorder(const YGEdge edge) const noexcept { - assert(_ygNode && "must be a valid node"); - return YGNodeStyleGetBorder(_ygNode, edge); + assert_valid(); + return YGNodeStyleGetBorder(_node, edge); } /** @@ -844,8 +950,8 @@ namespace Yoga */ void setGap(const YGGutter gutter, const float length) noexcept { - assert(_ygNode && "must be a valid node"); - YGNodeStyleSetGap(_ygNode, gutter, length); + assert_valid(); + YGNodeStyleSetGap(_node, gutter, length); } /** @@ -855,8 +961,8 @@ namespace Yoga */ void setGapPercent(const YGGutter gutter, const float lengthPercent) noexcept { - assert(_ygNode && "must be a valid node"); - YGNodeStyleSetGapPercent(_ygNode, gutter, lengthPercent); + assert_valid(); + YGNodeStyleSetGapPercent(_node, gutter, lengthPercent); } /** @@ -866,8 +972,8 @@ namespace Yoga */ [[nodiscard]] float getGap(const YGGutter gutter) const noexcept { - assert(_ygNode && "must be a valid node"); - return YGNodeStyleGetGap(_ygNode, gutter); + assert_valid(); + return YGNodeStyleGetGap(_node, gutter); } /** @@ -880,8 +986,8 @@ namespace Yoga */ void setBoxSizing(const YGBoxSizing boxSizing) noexcept { - assert(_ygNode && "must be a valid node"); - YGNodeStyleSetBoxSizing(_ygNode, boxSizing); + assert_valid(); + YGNodeStyleSetBoxSizing(_node, boxSizing); } /** @@ -895,8 +1001,8 @@ namespace Yoga */ [[nodiscard]] YGBoxSizing getBoxSizing() const noexcept { - assert(_ygNode && "must be a valid node"); - return YGNodeStyleGetBoxSizing(_ygNode); + assert_valid(); + return YGNodeStyleGetBoxSizing(_node); } /** @@ -908,8 +1014,8 @@ namespace Yoga */ void setWidth(const float width) noexcept { - assert(_ygNode && "must be a valid node"); - YGNodeStyleSetWidth(_ygNode, width); + assert_valid(); + YGNodeStyleSetWidth(_node, width); } /** @@ -923,8 +1029,8 @@ namespace Yoga */ void setWidthPercent(const float widthPercent) noexcept { - assert(_ygNode && "must be a valid node"); - YGNodeStyleSetWidthPercent(_ygNode, widthPercent); + assert_valid(); + YGNodeStyleSetWidthPercent(_node, widthPercent); } /** @@ -937,8 +1043,8 @@ namespace Yoga */ void setWidthAuto() noexcept { - assert(_ygNode && "must be a valid node"); - YGNodeStyleSetWidthAuto(_ygNode); + assert_valid(); + YGNodeStyleSetWidthAuto(_node); } /** @@ -951,8 +1057,8 @@ namespace Yoga */ [[nodiscard]] YGValue getWidth() const noexcept { - assert(_ygNode && "must be a valid node"); - return YGNodeStyleGetWidth(_ygNode); + assert_valid(); + return YGNodeStyleGetWidth(_node); } /** @@ -964,8 +1070,8 @@ namespace Yoga */ void setHeight(const float height) noexcept { - assert(_ygNode && "must be a valid node"); - YGNodeStyleSetHeight(_ygNode, height); + assert_valid(); + YGNodeStyleSetHeight(_node, height); } /** @@ -978,8 +1084,8 @@ namespace Yoga */ void setHeightPercent(const float heightPercent) noexcept { - assert(_ygNode && "must be a valid node"); - YGNodeStyleSetHeightPercent(_ygNode, heightPercent); + assert_valid(); + YGNodeStyleSetHeightPercent(_node, heightPercent); } /** @@ -990,8 +1096,8 @@ namespace Yoga */ void setHeightAuto() noexcept { - assert(_ygNode && "must be a valid node"); - YGNodeStyleSetHeightAuto(_ygNode); + assert_valid(); + YGNodeStyleSetHeightAuto(_node); } /** @@ -1004,8 +1110,8 @@ namespace Yoga */ [[nodiscard]] YGValue getHeight() const noexcept { - assert(_ygNode && "must be a valid node"); - return YGNodeStyleGetHeight(_ygNode); + assert_valid(); + return YGNodeStyleGetHeight(_node); } /** @@ -1018,8 +1124,8 @@ namespace Yoga */ void setMinWidth(const float minWidth) noexcept { - assert(_ygNode && "must be a valid node"); - YGNodeStyleSetMinWidth(_ygNode, minWidth); + assert_valid(); + YGNodeStyleSetMinWidth(_node, minWidth); } /** @@ -1034,8 +1140,8 @@ namespace Yoga */ void setMinWidthPercent(const float minWidthPercent) noexcept { - assert(_ygNode && "must be a valid node"); - YGNodeStyleSetMinWidthPercent(_ygNode, minWidthPercent); + assert_valid(); + YGNodeStyleSetMinWidthPercent(_node, minWidthPercent); } /** @@ -1048,8 +1154,8 @@ namespace Yoga */ [[nodiscard]] YGValue getMinWidth() const noexcept { - assert(_ygNode && "must be a valid node"); - return YGNodeStyleGetMinWidth(_ygNode); + assert_valid(); + return YGNodeStyleGetMinWidth(_node); } /** @@ -1062,8 +1168,8 @@ namespace Yoga */ void setMinHeight(const float minHeight) noexcept { - assert(_ygNode && "must be a valid node"); - YGNodeStyleSetMinHeight(_ygNode, minHeight); + assert_valid(); + YGNodeStyleSetMinHeight(_node, minHeight); } /** @@ -1075,8 +1181,8 @@ namespace Yoga */ void setMinHeightPercent(const float minHeightPercent) noexcept { - assert(_ygNode && "must be a valid node"); - YGNodeStyleSetMinHeightPercent(_ygNode, minHeightPercent); + assert_valid(); + YGNodeStyleSetMinHeightPercent(_node, minHeightPercent); } /** @@ -1089,8 +1195,8 @@ namespace Yoga */ [[nodiscard]] YGValue getMinHeight() const noexcept { - assert(_ygNode && "must be a valid node"); - return YGNodeStyleGetMinHeight(_ygNode); + assert_valid(); + return YGNodeStyleGetMinHeight(_node); } /** @@ -1104,8 +1210,8 @@ namespace Yoga */ void setMaxWidth(const float maxWidth) noexcept { - assert(_ygNode && "must be a valid node"); - YGNodeStyleSetMaxWidth(_ygNode, maxWidth); + assert_valid(); + YGNodeStyleSetMaxWidth(_node, maxWidth); } /** @@ -1118,8 +1224,8 @@ namespace Yoga */ void setMaxWidthPercent(const float maxWidthPercent) noexcept { - assert(_ygNode && "must be a valid node"); - YGNodeStyleSetMaxWidthPercent(_ygNode, maxWidthPercent); + assert_valid(); + YGNodeStyleSetMaxWidthPercent(_node, maxWidthPercent); } /** @@ -1131,8 +1237,8 @@ namespace Yoga */ [[nodiscard]] YGValue getMaxWidth() const noexcept { - assert(_ygNode && "must be a valid node"); - return YGNodeStyleGetMaxWidth(_ygNode); + assert_valid(); + return YGNodeStyleGetMaxWidth(_node); } /** @@ -1145,8 +1251,8 @@ namespace Yoga */ void setMaxHeight(const float maxHeight) noexcept { - assert(_ygNode && "must be a valid node"); - YGNodeStyleSetMaxHeight(_ygNode, maxHeight); + assert_valid(); + YGNodeStyleSetMaxHeight(_node, maxHeight); } /** @@ -1160,8 +1266,8 @@ namespace Yoga */ void setMaxHeightPercent(const float maxHeightPercent) noexcept { - assert(_ygNode && "must be a valid node"); - YGNodeStyleSetMaxHeightPercent(_ygNode, maxHeightPercent); + assert_valid(); + YGNodeStyleSetMaxHeightPercent(_node, maxHeightPercent); } /** @@ -1174,8 +1280,8 @@ namespace Yoga */ [[nodiscard]] YGValue getMaxHeight() const noexcept { - assert(_ygNode && "must be a valid node"); - return YGNodeStyleGetMaxHeight(_ygNode); + assert_valid(); + return YGNodeStyleGetMaxHeight(_node); } /** @@ -1189,8 +1295,8 @@ namespace Yoga */ void setAspectRatio(const float aspectRatio) noexcept { - assert(_ygNode && "must be a valid node"); - YGNodeStyleSetAspectRatio(_ygNode, aspectRatio); + assert_valid(); + YGNodeStyleSetAspectRatio(_node, aspectRatio); } /** @@ -1202,341 +1308,28 @@ namespace Yoga */ [[nodiscard]] float getAspectRatio() const noexcept { - assert(_ygNode && "must be a valid node"); - return YGNodeStyleGetAspectRatio(_ygNode); - } - - /** - * Retrieves the current context associated with this node. - * - * The context provides relevant state information or parameters. - * - * @return The current context of this node. - */ - [[nodiscard]] std::optional> getContext() noexcept - { - assert(_ygNode && "must be a valid node"); - auto* ctxPtr = YGNodeGetContext(_ygNode); - if (ctxPtr == nullptr) - { - return std::nullopt; - } - return std::optional>{std::ref(*static_cast(ctxPtr))}; - } - - [[nodiscard]] std::optional> getContext() const noexcept - { - assert(_ygNode && "must be a valid node"); - auto* ctxPtr = YGNodeGetContext(_ygNode); - if (ctxPtr == nullptr) - { - return std::nullopt; - } - return std::optional>{std::cref(*static_cast(ctxPtr))}; - } - - /** - * Retrieves a view of child nodes associated with this node. - * - * @return A view containing the child nodes of this node. - */ - [[nodiscard]] ChildView getChildren() const noexcept - { - assert(_ygNode && "must be a valid node"); - return ChildView{*this}; - } - - /** - * Retrieves the child node at the specified index, or a null (invalid) reference. - * - * @param index The position of the child node to retrieve. - * @return The child node at the specified index. Can be a null reference. - */ - [[nodiscard]] Node getChild(const size_t index) const noexcept - { - assert(_ygNode && "must be a valid node"); - return Node{YGNodeGetChild(_ygNode, index)}; - } - - /** - * Inserts a child node into the current node. - * - * The new child will be added to the list of children for this node. - * - * @param child The child node to insert. - */ - void insertChild(const Node& child) noexcept - { - assert(_ygNode && "must be a valid node"); - if (child._ygNode == nullptr) - return; - YGNodeInsertChild(_ygNode, child._ygNode, getChildCount()); - } - - /** - * Inserts a child node into the current node. - * - * The new child will be added to the list of children for this node. - * - * @param child The child node to insert. - * @param index Which index to insert the child at - */ - void insertChild(const Node& child, const size_t index) noexcept - { - assert(_ygNode && "must be a valid node"); - if (child._ygNode == nullptr) - return; - YGNodeInsertChild(_ygNode, child._ygNode, index); - } - - /** - * Removes a child node from this node. - * - * The child node specified will be detached from the current node. - * If the specified node is not a child of this node, no action is taken. - * - * @param child The child node to be removed. - */ - void removeChild(const Node& child) noexcept - { - assert(_ygNode && "must be a valid node"); - if (child._ygNode == nullptr) - return; - YGNodeRemoveChild(_ygNode, child._ygNode); - } - - /** - * Retrieves the parent node of this object, or a null (invalid) node if there is no parent. - * - * @return The parent node of this object or a null reference - */ - Node getParent() const noexcept - { - assert(_ygNode && "must be a valid node"); - return Node{YGNodeGetParent(_ygNode)}; - } - - /** - * @return A raw YGNodeRef, which is an opaque pointer. - */ - [[nodiscard]] YGNodeRef getRef() const noexcept - { - assert(_ygNode && "must be a valid node"); - return _ygNode; - } - - /** - * @return A raw YGConstNodeRef, which is an opaque const pointer. - */ - [[nodiscard]] YGNodeConstRef getConstRef() const noexcept - { - assert(_ygNode && "must be a valid node"); - return _ygNode; - } - - template - void setConfig(Config& config) noexcept - { - assert(_ygNode && "must be a valid node"); - YGNodeSetConfig(_ygNode, config.getRef()); - } - - protected: - YGNodeRef _ygNode; - - void setContext(Ctx* context) { YGNodeSetContext(_ygNode, context); } - }; - - /** - * An owning handle to a Yoga node. You probably want Yoga::Layout! - * - * Inherits functionality from Yoga::Node. - * - * @tparam Ctx Context type stored per node - must be default constructible - */ - template - class OwningNode final : public Node - { - - using base = Node; - - public: - OwningNode() : base{YGNodeNew()} { this->setContext(&_context); } - - template - explicit OwningNode(const Config& config) : base{YGNodeNewWithConfig(config.getRef())} - { - this->setContext(&_context); - } - - OwningNode(const OwningNode& other) = delete; - OwningNode& operator=(const OwningNode& other) = delete; - - OwningNode(OwningNode&& other) noexcept : base{other._ygNode}, _context{std::move(other._context)} - { - other._ygNode = nullptr; - if (this->_ygNode != nullptr) - { - this->setContext(&_context); - } - } - - OwningNode& operator=(OwningNode&& other) noexcept - { - if (this != &other) - { - if (this->_ygNode != nullptr) - { - YGNodeFree(this->_ygNode); - } - this->_ygNode = other._ygNode; - other._ygNode = nullptr; - _context = std::move(other._context); - if (this->_ygNode != nullptr) - { - this->setContext(&_context); - } - } - return *this; - } - - ~OwningNode() - { - if (this->_ygNode != nullptr) - { - YGNodeFree(this->_ygNode); - } + assert_valid(); + return YGNodeStyleGetAspectRatio(_node); } private: - Ctx _context; - }; - - /** - * Layout manager for Yoga nodes with automatic lifetime management. - * - * Manages both the underlying YGNode instances and their associated contexts. - * - * @tparam Ctx Context type stored per node - must be default constructible - */ - template - class Layout - { - public: - /** - * Creates a new layout with a root node sized to 100% width/height - */ - Layout() : _root{nullptr} - { - _config.setPointScaleFactor(1.f); - _root = Node{_nodes.emplace_back(_config).getRef()}; - _root.setWidthPercent(100.f); - _root.setHeightPercent(100.f); - } - - Layout(const Layout& other) = delete; - Layout& operator=(const Layout& other) = delete; - - Layout(Layout&&) = default; - Layout& operator=(Layout&&) = default; - - /** - * Creates a new node managed by this layout. - * - * The node will be automatically cleaned up when the layout is destroyed. - * - * @return Non-owning Node handle for the created node - */ - Node createNode() - { - auto& node = _nodes.emplace_back(_config); - return Node{node.getRef()}; - } - - /** - * Removes a node and all of its children from this layout. - * - * Also removes it from its parent and cleans up associated context. - * - * @param node The node to remove - */ - void removeNode(const Node& node) - { - auto nodeRef = node.getRef(); - - assert(_nodes.front().getRef() != nodeRef && "Cannot remove the root"); - - if (auto parent = node.getParent(); parent) - { - parent.removeChild(node); - } - - for (auto child : node.getChildren()) - { - removeNode(child); - } - - std::erase_if(_nodes, [nodeRef](const OwningNode& owned) { return owned.getRef() == nodeRef; }); - } - - /** - * Add an existing node to the layout root. - * - * A node tree will not be part of the layout unless - * it is parented to the root. - * @param node The node to add - */ - void addToRoot(const Node& node) { _root.insertChild(node); } - - Node getRoot() { return Node{_root.getRef()}; } - - [[nodiscard]] Config& getConfig() { return _config; } - [[nodiscard]] const Config& getConfig() const { return _config; } + friend class Layout; - /** - * Calculates the entire layout tree starting from this node. - * @param width Available width the layout can take up - * @param height Available height the layout can take up - * @param direction Reading direction (left-to-right by default) - */ - void calculate(const float width, const float height, const YGDirection direction = YGDirectionLTR) + Node(layout_type* layout, const YGNodeRef node) : _layout{layout}, _node{node} { - _root.calculateLayout(width, height, direction); + assert(layout != nullptr && "Layout must not be null"); } - /** - * Walks the layout tree, invoking a visitor for each node - * in pre-order (parent, then children). - * @tparam Callable Any callable that accepts a const Node& - * @param visitor The callable to invoke for each node. - */ - template&> Callable> - void walkTree(Callable&& visitor) const + // Allows Layout to invalidate a handle after destruction. + void invalidate() { - walkNode(getRoot(), visitor); + _layout = nullptr; + _node = nullptr; } - private: - Config _config; - std::vector> _nodes; - Node _root; - - /** - * Walks the layout tree, invoking a visitor for each node - * in pre-order (parent, then children). - * @tparam Callable Any callable that accepts a const Node& - * @param node The node to start walking from. - * @param visitor The callable to invoke for each node. - */ - template&> Callable> - void walkNode(Node& node, Callable& visitor) const - { - visitor(node); - for (auto child : node.getChildren()) - { - walkNode(child, visitor); - } - } + void assert_valid() const { assert(valid() && "Node handle is invalid"); } + layout_type* _layout; + YGNodeRef _node; }; } // namespace Yoga diff --git a/test/layout.cpp b/test/layout.cpp new file mode 100644 index 0000000..76e08b5 --- /dev/null +++ b/test/layout.cpp @@ -0,0 +1,150 @@ +#include +#include +#include +#include + +#include "yoga-cpp/yoga.hpp" + +// A simple context struct for testing purposes. +struct TestContext { + int id = 0; + std::string name; + + TestContext() = default; + TestContext(int id, std::string name) : id(id), name(std::move(name)) {} +}; + + +// Define types for convenience +using TestLayout = Yoga::Layout; +using TestNode = Yoga::Node; + +// Test Suite for Layout and Node Lifetime Management +class LayoutLifetimeTest : public ::testing::Test { +protected: + TestLayout layout; +}; + +TEST_F(LayoutLifetimeTest, NodeCreationAndDestruction) { + ASSERT_NO_THROW({ + TestNode node = layout.createNode(); + EXPECT_TRUE(node.valid()); + + layout.destroyNode(node); + EXPECT_FALSE(node.valid()); + }); +} + +TEST_F(LayoutLifetimeTest, ContextCreationAndAccess) { + TestNode node = layout.createNode(42, "MyNode"); + + ASSERT_TRUE(node.valid()); + + // Check if the context was constructed correctly + TestContext& context = node.getContext(); + EXPECT_EQ(context.id, 42); + EXPECT_EQ(context.name, "MyNode"); + + // Modify the context and check if it persists + context.id = 100; + EXPECT_EQ(node.getContext().id, 100); +} + +// Test Suite for Child Management API +class NodeChildManagementTest : public ::testing::Test { +protected: + TestLayout layout; + TestNode parent; + + void SetUp() override { + parent = layout.createNode(1, "Parent"); + } +}; + +TEST_F(NodeChildManagementTest, InsertAndRemoveChild) { + TestNode child = layout.createNode(2, "Child"); + + EXPECT_EQ(parent.getChildCount(), 0); + + parent.insertChild(child, 0); + EXPECT_EQ(parent.getChildCount(), 1); + EXPECT_EQ(parent.getChild(0), child); + + parent.removeChild(child); + EXPECT_EQ(parent.getChildCount(), 0); + + // The child node should still be valid, just detached + EXPECT_TRUE(child.valid()); +} + +TEST_F(NodeChildManagementTest, CreateChildConvenience) { + EXPECT_EQ(parent.getChildCount(), 0); + + TestNode newChild = parent.createChild(10, "CreatedChild"); + + EXPECT_EQ(parent.getChildCount(), 1); + EXPECT_TRUE(newChild.valid()); + EXPECT_EQ(newChild.getContext().id, 10); + EXPECT_EQ(newChild.getContext().name, "CreatedChild"); + EXPECT_EQ(parent.getChild(0), newChild); +} + +TEST_F(NodeChildManagementTest, ChildIteration) { + std::vector children; + children.push_back(parent.createChild(10, "Child1")); + children.push_back(parent.createChild(20, "Child2")); + children.push_back(parent.createChild(30, "Child3")); + + EXPECT_EQ(parent.getChildCount(), 3); + + int iteratedCount = 0; + size_t childIndex = 0; + for (const auto& iteratedChild : parent.getChildren()) { + EXPECT_TRUE(iteratedChild.valid()); + EXPECT_EQ(iteratedChild, children[childIndex]); + iteratedCount++; + childIndex++; + } + + EXPECT_EQ(iteratedCount, 3); +} + +TEST_F(NodeChildManagementTest, GetParent) { + TestNode child = parent.createChild(); + + ASSERT_TRUE(child.getParent().valid()); + EXPECT_EQ(child.getParent(), parent); +} + +class LayoutCalculationTest : public ::testing::Test { +protected: + TestLayout layout; +}; + +TEST_F(LayoutCalculationTest, SimpleFlexLayout) { + TestNode root = layout.createNode(); + root.setFlexDirection(YGFlexDirectionRow); + root.setWidth(500.f); + root.setHeight(100.f); + + TestNode child1 = layout.createNode(); + child1.setFlexGrow(1.f); + + TestNode child2 = layout.createNode(); + child2.setFlexGrow(1.f); + + root.insertChild(child1, 0); + root.insertChild(child2, 1); + + root.calculateLayout(500.f, 100.f); + + EXPECT_FLOAT_EQ(child1.getLayoutLeft(), 0); + EXPECT_FLOAT_EQ(child1.getLayoutTop(), 0); + EXPECT_FLOAT_EQ(child1.getLayoutWidth(), 250.f); + EXPECT_FLOAT_EQ(child1.getLayoutHeight(), 100.f); + + EXPECT_FLOAT_EQ(child2.getLayoutLeft(), 250.f); + EXPECT_FLOAT_EQ(child2.getLayoutTop(), 0); + EXPECT_FLOAT_EQ(child2.getLayoutWidth(), 250.f); + EXPECT_FLOAT_EQ(child2.getLayoutHeight(), 100.f); +} From be1918893ad5fda9b024fcf97707e54bba010ec7 Mon Sep 17 00:00:00 2001 From: Aaron Dosser Date: Sat, 19 Jul 2025 12:51:22 -0400 Subject: [PATCH 2/6] Fix main branch name (should switch to 'main' at some point) --- .github/workflows/tests.yml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 511371e..21d9da4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -2,8 +2,8 @@ name: Run C++ Tests on: push: - branches: [ "main" ] - pull_request: # No branch filter means this runs for all PRs + branches: [ "master" ] + pull_request: jobs: build-and-test: @@ -12,12 +12,9 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v4 - with: - # Fetch all history for all tags and branches - fetch-depth: 0 - name: Configure CMake - run: cmake -B build -S . -D YOGACPP_BUILD_TESTS=ON + run: cmake -B build -S . -DYOGACPP_BUILD_TESTS=ON - name: Build project run: cmake --build build From 7a02d139e2b72d6a3e2bbdd5461f5fe832ca8094 Mon Sep 17 00:00:00 2001 From: Aaron Dosser Date: Sat, 19 Jul 2025 12:55:33 -0400 Subject: [PATCH 3/6] Re-enable RTTI for testing --- CMakeLists.txt | 10 ++++++++-- test/layout.cpp | 9 +-------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2850cd3..1365283 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -40,13 +40,19 @@ if (YOGACPP_BUILD_TESTS) GIT_REPOSITORY https://github.com/google/googletest.git GIT_TAG v1.17.0 ) - # For Windows: Prevent overriding the parent project's compiler/linker settings set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) FetchContent_MakeAvailable(googletest) add_executable(yoga_cpp_tests test/layout.cpp) + + if(NOT MSVC) + target_compile_options(gtest PRIVATE "-frtti") + target_compile_options(gtest_main PRIVATE "-frtti") + target_compile_options(yoga_cpp_tests PRIVATE "-frtti") + endif() + target_link_libraries(yoga_cpp_tests GTest::gtest_main yoga_cpp yogacore) include(GoogleTest) gtest_discover_tests(yoga_cpp_tests) -endif() \ No newline at end of file +endif() diff --git a/test/layout.cpp b/test/layout.cpp index 76e08b5..cfce953 100644 --- a/test/layout.cpp +++ b/test/layout.cpp @@ -5,7 +5,6 @@ #include "yoga-cpp/yoga.hpp" -// A simple context struct for testing purposes. struct TestContext { int id = 0; std::string name; @@ -15,12 +14,10 @@ struct TestContext { }; -// Define types for convenience using TestLayout = Yoga::Layout; using TestNode = Yoga::Node; -// Test Suite for Layout and Node Lifetime Management -class LayoutLifetimeTest : public ::testing::Test { +class LayoutLifetimeTest : public testing::Test { protected: TestLayout layout; }; @@ -40,17 +37,14 @@ TEST_F(LayoutLifetimeTest, ContextCreationAndAccess) { ASSERT_TRUE(node.valid()); - // Check if the context was constructed correctly TestContext& context = node.getContext(); EXPECT_EQ(context.id, 42); EXPECT_EQ(context.name, "MyNode"); - // Modify the context and check if it persists context.id = 100; EXPECT_EQ(node.getContext().id, 100); } -// Test Suite for Child Management API class NodeChildManagementTest : public ::testing::Test { protected: TestLayout layout; @@ -73,7 +67,6 @@ TEST_F(NodeChildManagementTest, InsertAndRemoveChild) { parent.removeChild(child); EXPECT_EQ(parent.getChildCount(), 0); - // The child node should still be valid, just detached EXPECT_TRUE(child.valid()); } From 154bc794ffc14f9eeed00695ab75abfbee0a2053 Mon Sep 17 00:00:00 2001 From: Aaron Dosser Date: Sat, 19 Jul 2025 13:00:48 -0400 Subject: [PATCH 4/6] Test build fixes --- CMakeLists.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1365283..e5a2841 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,7 +11,8 @@ FetchContent_Declare(yoga GIT_REPOSITORY https://github.com/facebook/yoga.git GIT_TAG ${YOGA_VERSION} ) - +set(YOGA_BUILD_TESTS OFF CACHE BOOL "" FORCE) +set(YOGA_BUILD_BENCHMARKS OFF CACHE BOOL "" FORCE) FetchContent_MakeAvailable(yoga) add_library(yoga_cpp STATIC @@ -34,6 +35,8 @@ if (YOGACPP_BUILD_EXAMPLES) endif() if (YOGACPP_BUILD_TESTS) + enable_testing() + include(FetchContent) FetchContent_Declare( googletest From 2e46fd70c9fd1fe1f7c3412fcd42fb8871961332 Mon Sep 17 00:00:00 2001 From: Aaron Dosser Date: Sat, 19 Jul 2025 13:04:44 -0400 Subject: [PATCH 5/6] Try to get yoga to stop building its tests --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e5a2841..e0d72ec 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,10 +11,10 @@ FetchContent_Declare(yoga GIT_REPOSITORY https://github.com/facebook/yoga.git GIT_TAG ${YOGA_VERSION} ) -set(YOGA_BUILD_TESTS OFF CACHE BOOL "" FORCE) -set(YOGA_BUILD_BENCHMARKS OFF CACHE BOOL "" FORCE) FetchContent_MakeAvailable(yoga) +set_target_properties(yogatests PROPERTIES EXCLUDE_FROM_ALL ON) + add_library(yoga_cpp STATIC ${CMAKE_CURRENT_SOURCE_DIR}/include/yoga-cpp/yoga.hpp ${CMAKE_CURRENT_SOURCE_DIR}/src/yoga.cpp From 3fa1814b3a50fd766884ae74c5109dab61ea13ea Mon Sep 17 00:00:00 2001 From: Aaron Dosser Date: Sat, 19 Jul 2025 13:45:34 -0400 Subject: [PATCH 6/6] Update README for v2 --- CMakeLists.txt | 2 +- README.md | 97 ++++++++++++++++++++------------------------------ 2 files changed, 39 insertions(+), 60 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e0d72ec..6c715e2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.15) set(YOGA_VERSION v3.2.1) -set(YOGACPP_VERSION 1.0.0) +set(YOGACPP_VERSION 2.0.0) project(yoga_cpp VERSION ${YOGACPP_VERSION} LANGUAGES CXX) set(CMAKE_CXX_STANDARD 20) diff --git a/README.md b/README.md index 8a0d3ad..7fb25d8 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Yoga C++ (yoga-cpp) -#### version 1.0.0 (Yoga version 3.2.1) +#### version 2.0.0 (Yoga version 3.2.1) ## Background Yoga is a layout engine by Meta (née Facebook) which is written @@ -8,9 +8,6 @@ in C++ but exposes only a C API. This project aims to provide stable user-facing C++ bindings which wrap the C API in a safe and modern interface. -It also includes the optional and more opinionated `Yoga::Layout` class, -which provides a simple way to manage and traverse a Yoga layout tree. - ## Features - Inline docs @@ -33,7 +30,7 @@ include(FetchContent) FetchContent_Declare(yoga_cpp GIT_REPOSITORY https://github.com/detri/yoga-cpp.git - GIT_TAG v1.0.0 + GIT_TAG v2.0.0 ) FetchContent_MakeAvailable(yoga_cpp) @@ -43,8 +40,6 @@ target_link_libraries(MyTarget PRIVATE yoga_cpp) The project will download and link Yoga for you, so use yoga-cpp as a drop-in replacement and not an add-on. - - ## Barebones Example ```c++ #include @@ -58,12 +53,14 @@ using Layout = Yoga::Layout; int main() { Layout layout; - auto node = layout.createNode(); - layout.addToRoot(node); + auto root = layout.createNode(); + root.setWidthPercent(100.f); + root.setHeightPercent(100.f); + auto node = root.createChild(); node.setWidthPercent(50.f); node.setHeightPercent(50.f); - layout.calculate(100.f, 100.f); + root.calculateLayout(100.f, 100.f); auto dimensions = std::format( "X: {}, Y: {}, W: {}, H: {}", node.getLayoutLeft(), @@ -77,35 +74,37 @@ int main() ``` ## Layout\ -**Layout\** is a templated class that manages a layout and its contexts for you. - -The lifetimes of the nodes in a layout are tied to the layout object's lifetime. - -It has one requirement, which is that the context must be default-constructible. +The main feature is the `Layout` template class. +Yoga is just a tree of nodes with no concept of ownership or external storage. -The internals consist of: +This class provides context storage for any constructible type and acts as a factory +that ties Yoga layout nodes and user data together. -1. A Yoga::Config which is an owning wrapper around YGConfig. -2. Flat node storage to keep layout elements alive (you work with copyable references to the tree). This is just `std::vector>`. -3. A reference (`Node`) to the root node used for calculating the layout. +In other words, it acts as the source of truth for Yoga node and context lifetimes. +```c++ +Layout layout; // Creates a layout with "empty" context +``` -The API is primarily used via the `createNode()` method, which gives you a non-owning reference to a brand new wrapped Yoga node, with which you can do as you please. -You can also access the root node with `getRoot()` i.e. to set a background color that may exist in a custom context. +In the spirit of being a manager class, Layouts are not copyable or moveable. +If you need to model ownership of a Layout, construct and use it as a smart pointer. +You can create a node and context together like this: ```c++ -struct StyleCtx -{ - Color bgColor; -} -Layout layout; -layout.getRoot().getContext().bgColor = Color::Black; +Layout layout; +auto node = layout.createNode("I am a context argument"); ``` -Nodes and their contexts can be deleted by using `removeNode(Node node)` on a node reference. -If you track references (i.e. storing Node\ in a map to "name" them), be careful to get rid of any invalid references. -You can compare nodes with the underlying pointer using the `Node::getRef()` method. - -The layout can be calculated based on available viewport space using `calculate(width, height, direction = YGDirectionLTR)`. +Node handles are aware of their owning layouts for ergonomics: +```c++ +Layout layout; +auto node = layout.createNode(); + +// Adding a child through the handle like this: +auto child = node.createChild(); +// is equivalent to this: +auto child = layout.createNode(); +node.insertChild(child, node.getChildCount()); +``` ### Traversing a Layout Tree @@ -117,40 +116,20 @@ for (auto child : node.getChildren()) { child.getContext().doStuff(); } ``` -You can also traverse the entire tree starting from the root: -```c++ -std::vector> allNodes; -layout.walkTree([&](const auto& node) { - allNodes.emplace_back(node); -}); -``` +This allows for straightforward recursion. You can store the resulting references in order and reversely iterate over it if you need to walk the tree in reverse implicit Z-order, such as for hit-testing. ### Important: Node Lifetime -A `Node` is a non-owning reference to a node that is managed by a `Layout`. The node's memory is freed when the `Layout` object is destroyed or when you explicitly call `layout.removeNode(node)`. +A `Node` is a non-owning reference to a node that is managed by a `Layout`. The node's memory is freed when the `Layout` object is destroyed or when you explicitly call `layout.destroyNode(node)`. -Be careful to discard any `Node` objects you have stored elsewhere after removing them from the layout, as they will become invalid. You can check for validity by using the `if (node)` pattern. +Be careful to discard any `Node` objects you have stored elsewhere after removing them from the layout, as they will become invalid. You can check for validity of a specific reference by using `node.valid()`. +Note that in the case of removal, only the reference used to remove the node will become invalid. The rest will become dangling. ## Context Once you have a node from a layout, its context can be accessed with `getContext()`. -Note that this returns an optional wrapped reference to your context, i.e. `std::optional>`. +The previous version of this library returned an optional wrapped reference. As of v2.0.0, contexts +are tightly coupled with Nodes within a Layout, so they are no longer optional. ```c++ -auto ctxWrapper = node.getContext() // std::optional> -if (ctxWrapper) { - auto& ctx = ctxWrapper->get(); // DoStuffCtx& (also available as const) - ctx.doStuff(); -} +MyLayoutCtx& ctx = node.getContext(); ``` - -## Usage without Layout -`yoga-cpp` can be used as plain Yoga bindings by directly constructing -OwningNodes yourself and hooking them together. The method names mostly follow -the C API's naming conventions, in pascalCase and without prefixes, -except `Layout` methods which property names with styles are prefixed by `getLayout` or `setLayout` -instead of just `get` or `set`. - -```c++ -using MyNode = OwningNode<>; // node type with empty/monostate ctx -auto node = MyNode{}; // fresh, blank node that will be destroyed when the scope exits -``` \ No newline at end of file