DevTools: Include subframe grids in layout inspection

Firefox's grid inspector builds its subgrid tree from two protocol
signals: isSubgrid on each GridActor form, and
Walker.getParentGridNode() for each subgrid container. Teach the walker
to answer the parent grid lookup so Firefox can attach subgrids below
their parent grid in the Layout panel.
This commit is contained in:
Sam Atkins 2026-05-22 15:13:09 +01:00
parent ad541dca7f
commit fb96cdd7a1
5 changed files with 99 additions and 9 deletions

View file

@ -208,6 +208,26 @@ void WalkerActor::handle_message(Message const& message)
return;
}
if (message.type == "getParentGridNode"sv) {
auto node = get_required_parameter<String>(message, "node"sv);
if (!node.has_value())
return;
auto dom_node = WalkerActor::dom_node_for(*this, *node);
if (!dom_node.has_value()) {
send_unknown_actor_error(message, *node);
return;
}
JsonValue parent_grid_node;
if (auto parent_grid = parent_grid_node_for_node(dom_node->node); parent_grid.has_value())
parent_grid_node = serialize_node(parent_grid.value());
response.set("node"sv, move(parent_grid_node));
send_response(message, move(response));
return;
}
if (message.type == "innerHTML"sv) {
auto node = get_required_parameter<String>(message, "node"sv);
if (!node.has_value())
@ -679,6 +699,24 @@ Optional<JsonObject const&> WalkerActor::next_sibling_for_node(JsonObject const&
return sibling_for_node(*parent.value(), node, Direction::Next);
}
Optional<JsonObject const&> WalkerActor::parent_grid_node_for_node(JsonObject const& node) const
{
auto parent = m_dom_node_to_parent_map.get(&node);
while (parent.has_value() && parent.value()) {
auto display = parent.value()->get_string("display"sv);
if (!display.has_value())
return {};
if (display->contains("grid"sv))
return *parent.value();
if (*display != "contents"sv)
return {};
parent = m_dom_node_to_parent_map.get(parent.value());
}
return {};
}
Optional<JsonObject const&> WalkerActor::remove_node(JsonObject const& node)
{
auto maybe_parent = m_dom_node_to_parent_map.get(&node);

View file

@ -43,6 +43,7 @@ private:
Optional<JsonObject const&> previous_sibling_for_node(JsonObject const& node);
Optional<JsonObject const&> next_sibling_for_node(JsonObject const& node);
Optional<JsonObject const&> parent_grid_node_for_node(JsonObject const& node) const;
Optional<JsonObject const&> remove_node(JsonObject const& node);
void new_dom_node_mutation(WebView::Mutation);

View file

@ -2422,6 +2422,7 @@ void GridFormattingContext::save_grid_layout_data(CSS::GridTrackSizeList&& colum
{
auto data = make<GridLayoutData>();
data->direction = grid_container().computed_values().direction();
data->is_subgrid = is_subgridded_axis(GridDimension::Column) || is_subgridded_axis(GridDimension::Row);
data->writing_mode = grid_container().computed_values().writing_mode();
data->resolved_grid_template_columns = CSS::GridTrackSizeListStyleValue::create(move(columns));
data->resolved_grid_template_rows = CSS::GridTrackSizeListStyleValue::create(move(rows));

View file

@ -45,6 +45,8 @@
#include <LibWeb/HTML/BroadcastChannel.h>
#include <LibWeb/HTML/BrowsingContext.h>
#include <LibWeb/HTML/HTMLInputElement.h>
#include <LibWeb/HTML/Navigable.h>
#include <LibWeb/HTML/NavigableContainer.h>
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
#include <LibWeb/HTML/SelectedFile.h>
#include <LibWeb/HTML/Storage.h>
@ -761,6 +763,33 @@ static Optional<JsonObject> grid_layout_for_node(Web::DOM::Node const& node)
return layout;
}
static void append_grid_layouts_for_node_and_frame_descendants(Web::DOM::Node& root_node, JsonArray& grid_layouts)
{
root_node.for_each_in_inclusive_subtree([&](Web::DOM::Node& node) {
if (auto grid_layout = grid_layout_for_node(node); grid_layout.has_value())
grid_layouts.must_append(grid_layout.release_value());
auto* navigable_container = as_if<Web::HTML::NavigableContainer>(node);
if (!navigable_container)
return Web::TraversalDecision::Continue;
auto content_navigable = navigable_container->content_navigable();
if (!content_navigable)
return Web::TraversalDecision::Continue;
auto content_document = content_navigable->active_document();
if (!content_document)
return Web::TraversalDecision::Continue;
if (!content_document->origin().is_same_origin_domain(navigable_container->document().origin()))
return Web::TraversalDecision::Continue;
content_document->update_layout(Web::DOM::UpdateLayoutReason::InspectGridLayout);
append_grid_layouts_for_node_and_frame_descendants(*content_document, grid_layouts);
return Web::TraversalDecision::Continue;
});
}
void ConnectionFromClient::inspect_grid_layouts(u64 page_id, Web::UniqueNodeID root_node_id)
{
auto page = this->page(page_id);
@ -776,11 +805,7 @@ void ConnectionFromClient::inspect_grid_layouts(u64 page_id, Web::UniqueNodeID r
root_node->document().update_layout(Web::DOM::UpdateLayoutReason::InspectGridLayout);
JsonArray grid_layouts;
root_node->for_each_in_inclusive_subtree([&](Web::DOM::Node& node) {
if (auto grid_layout = grid_layout_for_node(node); grid_layout.has_value())
grid_layouts.must_append(grid_layout.release_value());
return Web::TraversalDecision::Continue;
});
append_grid_layouts_for_node_and_frame_descendants(*root_node, grid_layouts);
async_did_inspect_grid_layouts(page_id, grid_layouts.serialized());
}

View file

@ -57,6 +57,7 @@ static JsonObject make_dom_tree()
JsonObject target = make_node(4, "element"sv, "DIV"sv);
target.set("visible"sv, true);
target.set("scrollable"sv, true);
target.set("display"sv, "grid"sv);
JsonObject target_attributes;
target_attributes.set("id"sv, "target"sv);
@ -76,11 +77,16 @@ static JsonObject make_dom_tree()
before.set("parent-id"sv, 4);
before.set("pseudo-element"sv, to_underlying(Web::CSS::PseudoElement::Before));
JsonObject subgrid = make_node(9, "element"sv, "SECTION"sv);
subgrid.set("visible"sv, true);
subgrid.set("display"sv, "grid"sv);
JsonArray target_children;
target_children.must_append(move(text));
target_children.must_append(move(comment));
target_children.must_append(move(whitespace));
target_children.must_append(move(before));
target_children.must_append(move(subgrid));
target.set("children"sv, move(target_children));
JsonObject sibling = make_node(8, "element"sv, "SPAN"sv);
@ -212,7 +218,7 @@ static JsonObject make_grid_dimension()
return dimension;
}
static JsonObject make_grid_layout(Web::UniqueNodeID container_node_id, StringView area_name)
static JsonObject make_grid_layout(Web::UniqueNodeID container_node_id, StringView area_name, bool is_subgrid = false)
{
JsonObject area;
area.set("columnEnd"sv, 2);
@ -237,7 +243,7 @@ static JsonObject make_grid_layout(Web::UniqueNodeID container_node_id, StringVi
layout.set("containerNodeId"sv, container_node_id.value());
layout.set("direction"sv, "ltr"sv);
layout.set("gridFragments"sv, move(fragments));
layout.set("isSubgrid"sv, false);
layout.set("isSubgrid"sv, is_subgrid);
layout.set("writingMode"sv, "horizontal-tb"sv);
return layout;
}
@ -246,6 +252,7 @@ static JsonArray make_grid_layouts()
{
JsonArray grids;
grids.must_append(make_grid_layout(Web::UniqueNodeID { 4 }, "content"sv));
grids.must_append(make_grid_layout(Web::UniqueNodeID { 9 }, "subgrid"sv, true));
grids.must_append(make_grid_layout(Web::UniqueNodeID { 8 }, "subframe"sv));
return grids;
}
@ -979,6 +986,7 @@ TEST_CASE(inspector_walker_highlighter_layout_and_editing)
EXPECT_EQ(children_response.get_array("nodes"sv)->size(), 1u);
auto div_actor = query_selector(client, walker_actor, root_node_actor, "div"sv);
auto section_actor = query_selector(client, walker_actor, root_node_actor, "section"sv);
auto span_actor = query_selector(client, walker_actor, root_node_actor, "span"sv);
JsonObject previous_sibling;
previous_sibling.set("to"sv, walker_actor);
@ -1082,7 +1090,7 @@ TEST_CASE(inspector_walker_highlighter_layout_and_editing)
auto grids = client.request(move(get_grids)).get_array("grids"sv).release_value();
EXPECT_EQ(session->delegate.inspect_grid_layouts_call_count, 1u);
EXPECT_EQ(session->delegate.last_grid_root_node.value(), 1u);
EXPECT_EQ(grids.size(), 2u);
EXPECT_EQ(grids.size(), 3u);
auto const& content_grid = grids.at(0).as_object();
EXPECT(!content_grid.has("containerNodeId"sv));
@ -1095,7 +1103,24 @@ TEST_CASE(inspector_walker_highlighter_layout_and_editing)
EXPECT_EQ(content_grid_fragment.get_array("areas"sv)->at(0).as_object().get_string("name"sv).value(), "content"sv);
EXPECT_EQ(content_grid_fragment.get_object("cols"sv)->get_array("lines"sv)->at(0).as_object().get_integer<i32>("negativeNumber"sv).value(), -2);
auto const& subframe_grid = grids.at(1).as_object();
auto const& subgrid = grids.at(1).as_object();
EXPECT_EQ(subgrid.get_string("containerNodeActorID"sv).value(), section_actor);
EXPECT_EQ(subgrid.get_bool("isSubgrid"sv).value(), true);
EXPECT_EQ(subgrid.get_array("gridFragments"sv)->at(0).as_object().get_array("areas"sv)->at(0).as_object().get_string("name"sv).value(), "subgrid"sv);
JsonObject get_parent_grid_node;
get_parent_grid_node.set("to"sv, walker_actor);
get_parent_grid_node.set("type"sv, "getParentGridNode"sv);
get_parent_grid_node.set("node"sv, section_actor);
EXPECT_EQ(client.request(move(get_parent_grid_node)).get_object("node"sv)->get_string("actor"sv).value(), div_actor);
JsonObject get_missing_parent_grid_node;
get_missing_parent_grid_node.set("to"sv, walker_actor);
get_missing_parent_grid_node.set("type"sv, "getParentGridNode"sv);
get_missing_parent_grid_node.set("node"sv, div_actor);
EXPECT(client.request(move(get_missing_parent_grid_node)).get("node"sv).value().is_null());
auto const& subframe_grid = grids.at(2).as_object();
EXPECT_EQ(subframe_grid.get_string("containerNodeActorID"sv).value(), span_actor);
EXPECT_EQ(subframe_grid.get_array("gridFragments"sv)->at(0).as_object().get_array("areas"sv)->at(0).as_object().get_string("name"sv).value(), "subframe"sv);