srctree

Robin Linden parent 4357ee7d 7639020a
layout: Add a new module for creating layout trees

Right now this only handles the display attribute, but the plan is forthis module to also handle measuring the layout boxes by looking at thesize/padding/margin/border sizes for the elements being displayed.

inlinesplit
filename was Deleted added: 294, removed: 4, total 290
@@ -0,0 +1,19 @@
load("@rules_cc//cc:defs.bzl", "cc_library", "cc_test")
 
cc_library(
name = "layout",
srcs = ["layout.cpp"],
hdrs = ["layout.h"],
visibility = ["//visibility:public"],
deps = ["//style"],
)
 
cc_test(
name = "layout_test",
size = "small",
srcs = ["layout_test.cpp"],
deps = [
":layout",
"//etest",
],
)
 
filename was Deleted added: 294, removed: 4, total 290
@@ -0,0 +1,76 @@
#include "layout/layout.h"
 
#include <algorithm>
#include <optional>
#include <utility>
#include <variant>
 
namespace layout {
namespace {
 
template<class... Ts>
struct Overloaded : Ts... { using Ts::operator()...; };
 
// Not needed as of C++20, but gcc 10 won't work without it.
template<class... Ts>
Overloaded(Ts...) -> Overloaded<Ts...>;
 
bool last_node_was_anonymous(LayoutBox const &box) {
return !box.children.empty() && box.children.back().type == LayoutType::AnonymousBlock;
}
 
std::optional<std::string_view> get_property(
style::StyledNode const &node,
std::string_view property) {
auto it = std::find_if(cbegin(node.properties), cend(node.properties), [=](auto const &p) {
return p.first == property;
});
 
if (it == cend(node.properties)) {
return std::nullopt;
}
 
return it->second;
}
 
std::optional<LayoutBox> create_layout_tree(style::StyledNode const &node) {
return std::visit(Overloaded {
[](dom::Doctype const &) -> std::optional<LayoutBox> { return std::nullopt; },
[&node](dom::Element const &) -> std::optional<LayoutBox> {
auto display = get_property(node, "display");
if (display && *display == "none") {
return std::nullopt;
}
 
LayoutBox box{&node, display == "inline" ? LayoutType::Inline : LayoutType::Block};
 
for (auto const &child : node.children) {
auto child_box = create_layout_tree(child);
if (!child_box) continue;
 
if (child_box->type == LayoutType::Inline) {
if (!last_node_was_anonymous(box)) {
box.children.push_back(LayoutBox{nullptr, LayoutType::AnonymousBlock});
}
 
box.children.back().children.push_back(std::move(*child_box));
} else {
box.children.push_back(std::move(*child_box));
}
}
 
return box;
},
[&node](dom::Text const &) -> std::optional<LayoutBox> {
return LayoutBox{&node, LayoutType::Inline};
},
}, node.node.data);
}
 
} // namespace
 
LayoutBox create_layout(style::StyledNode const &node) {
return *create_layout_tree(node);
}
 
} // namespace layout
 
filename was Deleted added: 294, removed: 4, total 290
@@ -0,0 +1,37 @@
#include "style/styled_node.h"
 
#include <vector>
 
namespace layout {
 
struct Rect {
float x{}, y{}, width{}, height{};
};
 
constexpr bool operator==(Rect const &a, Rect const &b) noexcept {
return a.x == b.x && a.y == b.y && a.width == b.width && a.height == b.height;
}
 
enum class LayoutType {
Inline,
Block,
AnonymousBlock, // Holds groups of sequential inline boxes.
};
 
struct LayoutBox {
style::StyledNode const *node;
LayoutType type;
Rect dimensions;
std::vector<LayoutBox> children;
};
 
inline bool operator==(LayoutBox const &a, LayoutBox const &b) noexcept {
return a.node == b.node
&& a.type == b.type
&& a.dimensions == b.dimensions
&& a.children == b.children;
}
 
LayoutBox create_layout(style::StyledNode const &node);
 
} // namespace layout
 
filename was Deleted added: 294, removed: 4, total 290
@@ -0,0 +1,158 @@
#include "layout/layout.h"
 
#include "etest/etest.h"
 
using namespace std::literals;
using etest::expect;
using etest::require;
using layout::LayoutType;
 
int main() {
etest::test("simple tree", [] {
auto dom_root = dom::create_element_node("html", {}, {
dom::create_element_node("head", {}, {}),
dom::create_element_node("body", {}, {
dom::create_element_node("p", {}, {}),
}),
});
 
auto style_root = style::StyledNode{
.node = dom_root,
.properties = {},
.children = {
{dom_root.children[0], {}, {}},
{dom_root.children[1], {}, {
{dom_root.children[1].children[0], {}, {}},
}},
},
};
 
auto expected_layout = layout::LayoutBox{
.node = &style_root,
.type = LayoutType::Block,
.dimensions = {},
.children = {
{&style_root.children[0], LayoutType::Block, {}, {}},
{&style_root.children[1], LayoutType::Block, {}, {
{&style_root.children[1].children[0], LayoutType::Block, {}, {}},
}},
}
};
 
auto layout_root = layout::create_layout(style_root);
expect(expected_layout == layout_root);
});
 
etest::test("layouting removes display:none nodes", [] {
auto dom_root = dom::create_element_node("html", {}, {
dom::create_element_node("head", {}, {}),
dom::create_element_node("body", {}, {
dom::create_element_node("p", {}, {}),
}),
});
 
auto style_root = style::StyledNode{
.node = dom_root,
.properties = {},
.children = {
{dom_root.children[0], {{"display", "none"}}, {}},
{dom_root.children[1], {}, {
{dom_root.children[1].children[0], {}, {}},
}},
},
};
 
auto expected_layout = layout::LayoutBox{
.node = &style_root,
.type = LayoutType::Block,
.dimensions = {},
.children = {
{&style_root.children[1], LayoutType::Block, {}, {
{&style_root.children[1].children[0], LayoutType::Block, {}, {}},
}},
}
};
 
auto layout_root = layout::create_layout(style_root);
expect(expected_layout == layout_root);
});
 
etest::test("inline nodes get wrapped in anonymous blocks", [] {
auto dom_root = dom::create_element_node("html", {}, {
dom::create_element_node("head", {}, {}),
dom::create_element_node("body", {}, {
dom::create_element_node("p", {}, {}),
}),
});
 
auto style_root = style::StyledNode{
.node = dom_root,
.properties = {},
.children = {
{dom_root.children[0], {{"display", "inline"}}, {}},
{dom_root.children[1], {{"display", "inline"}}, {
{dom_root.children[1].children[0], {}, {}},
}},
},
};
 
// TODO(robinlinden)
// Having block elements inside of inline ones isn't allowed,
// but I haven't looked up how to handle them yet.
auto expected_layout = layout::LayoutBox{
.node = &style_root,
.type = LayoutType::Block,
.dimensions = {},
.children = {
{nullptr, LayoutType::AnonymousBlock, {}, {
{&style_root.children[0], LayoutType::Inline, {}, {}},
{&style_root.children[1], LayoutType::Inline, {}, {
{&style_root.children[1].children[0], LayoutType::Block, {}, {}},
}},
}},
}
};
 
auto layout_root = layout::create_layout(style_root);
expect(expected_layout == layout_root);
});
 
etest::test("text", [] {
auto dom_root = dom::create_element_node("html", {}, {
dom::create_element_node("body", {}, {
dom::create_text_node("hello"),
dom::create_text_node("goodbye"),
}),
});
 
auto style_root = style::StyledNode{
.node = dom_root,
.properties = {},
.children = {
{dom_root.children[0], {}, {
{dom_root.children[0].children[0], {}, {}},
{dom_root.children[0].children[1], {}, {}},
}},
},
};
 
auto expected_layout = layout::LayoutBox{
.node = &style_root,
.type = LayoutType::Block,
.dimensions = {},
.children = {
{&style_root.children[0], LayoutType::Block, {}, {
{nullptr, LayoutType::AnonymousBlock, {}, {
{&style_root.children[0].children[0], LayoutType::Inline, {}, {}},
{&style_root.children[0].children[1], LayoutType::Inline, {}, {}},
}},
}},
}
};
 
auto layout_root = layout::create_layout(style_root);
expect(expected_layout == layout_root);
});
 
return etest::run_all_tests();
}