LibWeb: Add RefCountedTreeNode
This is preparation for moving the Paintable tree from GC allocation to ordinary ref-counting. Paintables need tree ownership semantics that match TreeNode, but their lifetime is about to stop being controlled by the JS heap. Add a small ref-counted tree helper and unit tests for ownership, sibling links, removal, destruction, and preorder traversal. No Paintable code uses it yet; the behavior change happens in the next commit.
This commit is contained in:
parent
da9abd9a4c
commit
8bc3f5950f
3 changed files with 487 additions and 0 deletions
298
Libraries/LibWeb/RefCountedTreeNode.h
Normal file
298
Libraries/LibWeb/RefCountedTreeNode.h
Normal file
|
|
@ -0,0 +1,298 @@
|
|||
/*
|
||||
* Copyright (c) 2026, the Ladybird developers.
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Assertions.h>
|
||||
#include <AK/IterationDecision.h>
|
||||
#include <AK/RefPtr.h>
|
||||
#include <AK/TypeCasts.h>
|
||||
#include <AK/WeakPtr.h>
|
||||
#include <LibWeb/Export.h>
|
||||
#include <LibWeb/TraversalDecision.h>
|
||||
|
||||
namespace Web {
|
||||
|
||||
template<typename T, typename Callback>
|
||||
TraversalDecision traverse_ref_counted_preorder(RefPtr<T> root, Callback callback)
|
||||
{
|
||||
auto current = root;
|
||||
while (current) {
|
||||
TraversalDecision decision = callback(*current);
|
||||
if (decision == TraversalDecision::Break)
|
||||
return TraversalDecision::Break;
|
||||
|
||||
if (decision != TraversalDecision::SkipChildrenAndContinue) {
|
||||
if (auto first_child = current->first_child()) {
|
||||
current = move(first_child);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (current == root)
|
||||
break;
|
||||
|
||||
if (auto next_sibling = current->next_sibling()) {
|
||||
current = move(next_sibling);
|
||||
continue;
|
||||
}
|
||||
|
||||
while (current != root && !current->next_sibling())
|
||||
current = current->parent();
|
||||
if (current == root)
|
||||
break;
|
||||
|
||||
current = current->next_sibling();
|
||||
}
|
||||
return TraversalDecision::Continue;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
class WEB_API RefCountedTreeNode {
|
||||
public:
|
||||
RefPtr<T> parent() { return m_parent.strong_ref(); }
|
||||
RefPtr<T const> parent() const { return m_parent.strong_ref(); }
|
||||
|
||||
bool has_children() const { return m_first_child; }
|
||||
|
||||
RefPtr<T> first_child()
|
||||
{
|
||||
return m_first_child;
|
||||
}
|
||||
RefPtr<T const> first_child() const
|
||||
{
|
||||
return const_cast<RefCountedTreeNode&>(*this).first_child();
|
||||
}
|
||||
|
||||
RefPtr<T> last_child()
|
||||
{
|
||||
return m_last_child.strong_ref();
|
||||
}
|
||||
RefPtr<T const> last_child() const
|
||||
{
|
||||
return const_cast<RefCountedTreeNode&>(*this).last_child();
|
||||
}
|
||||
|
||||
RefPtr<T> next_sibling()
|
||||
{
|
||||
return m_next_sibling;
|
||||
}
|
||||
RefPtr<T const> next_sibling() const
|
||||
{
|
||||
return const_cast<RefCountedTreeNode&>(*this).next_sibling();
|
||||
}
|
||||
|
||||
RefPtr<T> previous_sibling()
|
||||
{
|
||||
return m_previous_sibling.strong_ref();
|
||||
}
|
||||
RefPtr<T const> previous_sibling() const
|
||||
{
|
||||
return const_cast<RefCountedTreeNode&>(*this).previous_sibling();
|
||||
}
|
||||
|
||||
void append_child(NonnullRefPtr<T> node)
|
||||
{
|
||||
VERIFY(!node->parent());
|
||||
VERIFY(!node->next_sibling());
|
||||
VERIFY(!node->previous_sibling());
|
||||
|
||||
auto previous_last_child = last_child();
|
||||
node->m_previous_sibling = previous_last_child;
|
||||
node->m_parent = static_cast<T&>(*this);
|
||||
m_last_child = node;
|
||||
|
||||
if (previous_last_child)
|
||||
previous_last_child->m_next_sibling = move(node);
|
||||
else
|
||||
m_first_child = move(node);
|
||||
}
|
||||
|
||||
void remove_child(T& node)
|
||||
{
|
||||
RefPtr<T> self = static_cast<T&>(*this);
|
||||
RefPtr<T> child_to_remove = node;
|
||||
VERIFY(node.parent() == self);
|
||||
|
||||
auto previous_sibling = node.previous_sibling();
|
||||
auto next_sibling = node.next_sibling();
|
||||
if (previous_sibling) {
|
||||
VERIFY(previous_sibling->m_next_sibling == child_to_remove);
|
||||
previous_sibling->m_next_sibling = next_sibling;
|
||||
} else {
|
||||
VERIFY(m_first_child == child_to_remove);
|
||||
m_first_child = next_sibling;
|
||||
}
|
||||
|
||||
if (next_sibling) {
|
||||
VERIFY(next_sibling->previous_sibling() == child_to_remove);
|
||||
next_sibling->m_previous_sibling = previous_sibling;
|
||||
} else {
|
||||
VERIFY(last_child() == child_to_remove);
|
||||
m_last_child = previous_sibling;
|
||||
}
|
||||
|
||||
node.m_next_sibling.clear();
|
||||
node.m_previous_sibling.clear();
|
||||
node.m_parent.clear();
|
||||
}
|
||||
|
||||
void remove()
|
||||
{
|
||||
auto parent = this->parent();
|
||||
VERIFY(parent);
|
||||
parent->remove_child(static_cast<T&>(*this));
|
||||
}
|
||||
|
||||
template<typename Callback>
|
||||
TraversalDecision for_each_in_inclusive_subtree(Callback callback) const
|
||||
{
|
||||
return traverse_ref_counted_preorder(RefPtr<T const> { static_cast<T const&>(*this) }, callback);
|
||||
}
|
||||
|
||||
template<typename Callback>
|
||||
TraversalDecision for_each_in_inclusive_subtree(Callback callback)
|
||||
{
|
||||
return traverse_ref_counted_preorder(RefPtr<T> { static_cast<T&>(*this) }, callback);
|
||||
}
|
||||
|
||||
template<typename U, typename Callback>
|
||||
TraversalDecision for_each_in_inclusive_subtree_of_type(Callback callback)
|
||||
{
|
||||
return for_each_in_inclusive_subtree([callback = move(callback)](T& node) {
|
||||
if (auto* node_of_type = as_if<U>(node))
|
||||
return callback(*node_of_type);
|
||||
return TraversalDecision::Continue;
|
||||
});
|
||||
}
|
||||
|
||||
template<typename U, typename Callback>
|
||||
TraversalDecision for_each_in_inclusive_subtree_of_type(Callback callback) const
|
||||
{
|
||||
return for_each_in_inclusive_subtree([callback = move(callback)](T const& node) {
|
||||
if (auto const* node_of_type = as_if<U>(node))
|
||||
return callback(*node_of_type);
|
||||
return TraversalDecision::Continue;
|
||||
});
|
||||
}
|
||||
|
||||
template<typename Callback>
|
||||
TraversalDecision for_each_in_subtree(Callback callback) const
|
||||
{
|
||||
for (auto child = first_child(); child; child = child->next_sibling()) {
|
||||
if (child->for_each_in_inclusive_subtree(callback) == TraversalDecision::Break)
|
||||
return TraversalDecision::Break;
|
||||
}
|
||||
return TraversalDecision::Continue;
|
||||
}
|
||||
|
||||
template<typename Callback>
|
||||
TraversalDecision for_each_in_subtree(Callback callback)
|
||||
{
|
||||
for (auto child = first_child(); child; child = child->next_sibling()) {
|
||||
if (child->for_each_in_inclusive_subtree(callback) == TraversalDecision::Break)
|
||||
return TraversalDecision::Break;
|
||||
}
|
||||
return TraversalDecision::Continue;
|
||||
}
|
||||
|
||||
template<typename U, typename Callback>
|
||||
TraversalDecision for_each_in_subtree_of_type(Callback callback)
|
||||
{
|
||||
for (auto child = first_child(); child; child = child->next_sibling()) {
|
||||
if (child->template for_each_in_inclusive_subtree_of_type<U>(callback) == TraversalDecision::Break)
|
||||
return TraversalDecision::Break;
|
||||
}
|
||||
return TraversalDecision::Continue;
|
||||
}
|
||||
|
||||
template<typename U, typename Callback>
|
||||
TraversalDecision for_each_in_subtree_of_type(Callback callback) const
|
||||
{
|
||||
for (auto child = first_child(); child; child = child->next_sibling()) {
|
||||
if (child->template for_each_in_inclusive_subtree_of_type<U>(callback) == TraversalDecision::Break)
|
||||
return TraversalDecision::Break;
|
||||
}
|
||||
return TraversalDecision::Continue;
|
||||
}
|
||||
|
||||
template<typename Callback>
|
||||
void for_each_child(Callback callback) const
|
||||
{
|
||||
return const_cast<RefCountedTreeNode&>(*this).for_each_child(move(callback));
|
||||
}
|
||||
|
||||
template<typename Callback>
|
||||
void for_each_child(Callback callback)
|
||||
{
|
||||
for (auto node = first_child(); node; node = node->next_sibling()) {
|
||||
if (callback(*node) == IterationDecision::Break)
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
template<typename U, typename Callback>
|
||||
void for_each_child_of_type(Callback callback)
|
||||
{
|
||||
for (auto node = first_child(); node; node = node->next_sibling()) {
|
||||
auto* node_of_type = as_if<U>(*node);
|
||||
if (!node_of_type)
|
||||
continue;
|
||||
if (callback(*node_of_type) == IterationDecision::Break)
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
template<typename U, typename Callback>
|
||||
void for_each_child_of_type(Callback callback) const
|
||||
{
|
||||
return const_cast<RefCountedTreeNode&>(*this).template for_each_child_of_type<U>(move(callback));
|
||||
}
|
||||
|
||||
template<typename U>
|
||||
RefPtr<U const> first_ancestor_of_type() const
|
||||
{
|
||||
return const_cast<RefCountedTreeNode&>(*this).template first_ancestor_of_type<U>();
|
||||
}
|
||||
|
||||
template<typename U>
|
||||
RefPtr<U> first_ancestor_of_type()
|
||||
{
|
||||
for (auto ancestor = parent(); ancestor; ancestor = ancestor->parent()) {
|
||||
if (auto* ancestor_of_type = as_if<U>(*ancestor))
|
||||
return *ancestor_of_type;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
~RefCountedTreeNode()
|
||||
{
|
||||
if (auto parent = this->parent())
|
||||
parent->remove_child(static_cast<T&>(*this));
|
||||
|
||||
while (m_first_child) {
|
||||
auto child = m_first_child;
|
||||
m_first_child = child->m_next_sibling;
|
||||
if (m_first_child)
|
||||
m_first_child->m_previous_sibling.clear();
|
||||
child->m_next_sibling.clear();
|
||||
child->m_previous_sibling.clear();
|
||||
child->m_parent.clear();
|
||||
}
|
||||
m_last_child.clear();
|
||||
}
|
||||
|
||||
protected:
|
||||
RefCountedTreeNode() = default;
|
||||
|
||||
private:
|
||||
WeakPtr<T> m_parent;
|
||||
RefPtr<T> m_first_child;
|
||||
WeakPtr<T> m_last_child;
|
||||
RefPtr<T> m_next_sibling;
|
||||
WeakPtr<T> m_previous_sibling;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ set(TEST_SOURCES
|
|||
TestMicrosyntax.cpp
|
||||
TestMimeSniff.cpp
|
||||
TestNumbers.cpp
|
||||
TestRefCountedTreeNode.cpp
|
||||
TestSourceHighlighter.cpp
|
||||
TestStrings.cpp
|
||||
)
|
||||
|
|
|
|||
188
Tests/LibWeb/TestRefCountedTreeNode.cpp
Normal file
188
Tests/LibWeb/TestRefCountedTreeNode.cpp
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
/*
|
||||
* Copyright (c) 2026, the Ladybird developers.
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/RefCounted.h>
|
||||
#include <AK/Vector.h>
|
||||
#include <AK/WeakPtr.h>
|
||||
#include <AK/Weakable.h>
|
||||
#include <LibTest/TestCase.h>
|
||||
#include <LibWeb/RefCountedTreeNode.h>
|
||||
|
||||
class TestNode
|
||||
: public RefCounted<TestNode>
|
||||
, public Weakable<TestNode>
|
||||
, public Web::RefCountedTreeNode<TestNode> {
|
||||
public:
|
||||
static NonnullRefPtr<TestNode> create(int value)
|
||||
{
|
||||
return adopt_ref(*new TestNode(value));
|
||||
}
|
||||
|
||||
~TestNode() = default;
|
||||
|
||||
int value() const { return m_value; }
|
||||
|
||||
private:
|
||||
explicit TestNode(int value)
|
||||
: m_value(value)
|
||||
{
|
||||
}
|
||||
|
||||
int m_value { 0 };
|
||||
};
|
||||
|
||||
TEST_CASE(sibling_links_are_updated_when_appending)
|
||||
{
|
||||
auto root = TestNode::create(0);
|
||||
auto first = TestNode::create(1);
|
||||
auto second = TestNode::create(2);
|
||||
auto third = TestNode::create(3);
|
||||
|
||||
root->append_child(first);
|
||||
root->append_child(second);
|
||||
root->append_child(third);
|
||||
|
||||
EXPECT_EQ(root->first_child().ptr(), first.ptr());
|
||||
EXPECT_EQ(root->last_child().ptr(), third.ptr());
|
||||
|
||||
EXPECT_EQ(first->previous_sibling().ptr(), nullptr);
|
||||
EXPECT_EQ(first->next_sibling().ptr(), second.ptr());
|
||||
EXPECT_EQ(second->previous_sibling().ptr(), first.ptr());
|
||||
EXPECT_EQ(second->next_sibling().ptr(), third.ptr());
|
||||
EXPECT_EQ(third->previous_sibling().ptr(), second.ptr());
|
||||
EXPECT_EQ(third->next_sibling().ptr(), nullptr);
|
||||
}
|
||||
|
||||
TEST_CASE(sibling_links_are_updated_when_removing)
|
||||
{
|
||||
auto root = TestNode::create(0);
|
||||
auto first = TestNode::create(1);
|
||||
auto second = TestNode::create(2);
|
||||
auto third = TestNode::create(3);
|
||||
|
||||
root->append_child(first);
|
||||
root->append_child(second);
|
||||
root->append_child(third);
|
||||
|
||||
root->remove_child(*second);
|
||||
EXPECT_EQ(first->next_sibling().ptr(), third.ptr());
|
||||
EXPECT_EQ(third->previous_sibling().ptr(), first.ptr());
|
||||
EXPECT_EQ(second->parent().ptr(), nullptr);
|
||||
EXPECT_EQ(second->previous_sibling().ptr(), nullptr);
|
||||
EXPECT_EQ(second->next_sibling().ptr(), nullptr);
|
||||
|
||||
root->remove_child(*first);
|
||||
EXPECT_EQ(root->first_child().ptr(), third.ptr());
|
||||
EXPECT_EQ(third->previous_sibling().ptr(), nullptr);
|
||||
|
||||
root->remove_child(*third);
|
||||
EXPECT_EQ(root->first_child().ptr(), nullptr);
|
||||
EXPECT_EQ(root->last_child().ptr(), nullptr);
|
||||
EXPECT_EQ(third->parent().ptr(), nullptr);
|
||||
}
|
||||
|
||||
TEST_CASE(tree_owns_children_through_forward_links)
|
||||
{
|
||||
WeakPtr<TestNode> first;
|
||||
WeakPtr<TestNode> second;
|
||||
WeakPtr<TestNode> third;
|
||||
|
||||
{
|
||||
auto root = TestNode::create(0);
|
||||
|
||||
auto first_child = TestNode::create(1);
|
||||
first = first_child;
|
||||
root->append_child(move(first_child));
|
||||
|
||||
auto second_child = TestNode::create(2);
|
||||
second = second_child;
|
||||
root->append_child(move(second_child));
|
||||
|
||||
auto third_child = TestNode::create(3);
|
||||
third = third_child;
|
||||
root->append_child(move(third_child));
|
||||
|
||||
EXPECT(first);
|
||||
EXPECT(second);
|
||||
EXPECT(third);
|
||||
}
|
||||
|
||||
EXPECT(first.is_null());
|
||||
EXPECT(second.is_null());
|
||||
EXPECT(third.is_null());
|
||||
}
|
||||
|
||||
TEST_CASE(removing_child_releases_tree_ownership)
|
||||
{
|
||||
auto root = TestNode::create(0);
|
||||
WeakPtr<TestNode> child_weak;
|
||||
|
||||
{
|
||||
auto child = TestNode::create(1);
|
||||
child_weak = child;
|
||||
root->append_child(move(child));
|
||||
}
|
||||
|
||||
EXPECT(child_weak);
|
||||
|
||||
{
|
||||
auto child = root->first_child();
|
||||
root->remove_child(*child);
|
||||
EXPECT_EQ(root->first_child().ptr(), nullptr);
|
||||
EXPECT(child_weak);
|
||||
}
|
||||
|
||||
EXPECT(child_weak.is_null());
|
||||
}
|
||||
|
||||
TEST_CASE(parent_destruction_detaches_children_and_siblings)
|
||||
{
|
||||
auto first = TestNode::create(1);
|
||||
auto second = TestNode::create(2);
|
||||
auto third = TestNode::create(3);
|
||||
|
||||
{
|
||||
auto root = TestNode::create(0);
|
||||
root->append_child(first);
|
||||
root->append_child(second);
|
||||
root->append_child(third);
|
||||
}
|
||||
|
||||
EXPECT_EQ(first->parent().ptr(), nullptr);
|
||||
EXPECT_EQ(first->next_sibling().ptr(), nullptr);
|
||||
EXPECT_EQ(second->parent().ptr(), nullptr);
|
||||
EXPECT_EQ(second->previous_sibling().ptr(), nullptr);
|
||||
EXPECT_EQ(second->next_sibling().ptr(), nullptr);
|
||||
EXPECT_EQ(third->parent().ptr(), nullptr);
|
||||
EXPECT_EQ(third->previous_sibling().ptr(), nullptr);
|
||||
}
|
||||
|
||||
TEST_CASE(preorder_traversal_uses_sibling_links)
|
||||
{
|
||||
auto root = TestNode::create(0);
|
||||
auto first = TestNode::create(1);
|
||||
auto second = TestNode::create(2);
|
||||
auto third = TestNode::create(3);
|
||||
auto second_child = TestNode::create(4);
|
||||
|
||||
root->append_child(first);
|
||||
root->append_child(second);
|
||||
root->append_child(third);
|
||||
second->append_child(second_child);
|
||||
|
||||
Vector<int> values;
|
||||
root->for_each_in_inclusive_subtree([&](TestNode& node) {
|
||||
values.append(node.value());
|
||||
return Web::TraversalDecision::Continue;
|
||||
});
|
||||
|
||||
EXPECT_EQ(values.size(), 5u);
|
||||
EXPECT_EQ(values[0], 0);
|
||||
EXPECT_EQ(values[1], 1);
|
||||
EXPECT_EQ(values[2], 2);
|
||||
EXPECT_EQ(values[3], 4);
|
||||
EXPECT_EQ(values[4], 3);
|
||||
}
|
||||
Loading…
Reference in a new issue