2025-02-15 09:57:36 -03:00
|
|
|
/*
|
|
|
|
|
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
|
|
|
|
|
*
|
|
|
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
|
|
|
*/
|
|
|
|
|
|
2025-03-19 16:50:56 -03:00
|
|
|
#include <AK/JsonArray.h>
|
|
|
|
|
#include <AK/JsonObject.h>
|
2025-02-21 18:20:53 -03:00
|
|
|
#include <AK/JsonValue.h>
|
2026-06-03 09:07:55 -03:00
|
|
|
#include <AK/ScopeGuard.h>
|
2025-02-21 18:20:53 -03:00
|
|
|
#include <LibDevTools/Actors/InspectorActor.h>
|
2025-02-15 09:57:36 -03:00
|
|
|
#include <LibDevTools/Actors/PageStyleActor.h>
|
2026-06-04 12:59:14 -03:00
|
|
|
#include <LibDevTools/Actors/StyleRuleActor.h>
|
2026-06-03 09:07:55 -03:00
|
|
|
#include <LibDevTools/Actors/StyleSheetsActor.h>
|
2025-02-21 18:20:53 -03:00
|
|
|
#include <LibDevTools/Actors/TabActor.h>
|
|
|
|
|
#include <LibDevTools/Actors/WalkerActor.h>
|
|
|
|
|
#include <LibDevTools/DevToolsDelegate.h>
|
|
|
|
|
#include <LibDevTools/DevToolsServer.h>
|
2026-06-03 09:07:55 -03:00
|
|
|
#include <LibWeb/CSS/StyleSheetIdentifier.h>
|
2025-02-15 09:57:36 -03:00
|
|
|
|
|
|
|
|
namespace DevTools {
|
|
|
|
|
|
2025-07-02 13:27:26 -03:00
|
|
|
static void received_layout(JsonObject& response, JsonValue const& node_box_sizing)
|
2025-03-11 10:47:41 -03:00
|
|
|
{
|
2025-07-02 13:27:26 -03:00
|
|
|
if (!node_box_sizing.is_object())
|
|
|
|
|
return;
|
|
|
|
|
|
2025-03-11 10:47:41 -03:00
|
|
|
response.set("autoMargins"sv, JsonObject {});
|
|
|
|
|
|
2025-03-19 13:46:36 -03:00
|
|
|
auto pixel_value = [&](auto key) {
|
2025-07-02 13:27:26 -03:00
|
|
|
return node_box_sizing.as_object().get_double_with_precision_loss(key).value_or(0);
|
2025-03-11 10:47:41 -03:00
|
|
|
};
|
2025-03-19 13:46:36 -03:00
|
|
|
auto set_pixel_value = [&](auto key) {
|
|
|
|
|
response.set(key, MUST(String::formatted("{}px", pixel_value(key))));
|
2025-03-11 10:47:41 -03:00
|
|
|
};
|
2025-03-19 13:46:36 -03:00
|
|
|
auto set_computed_value = [&](auto key) {
|
2025-09-26 11:16:49 -03:00
|
|
|
response.set(key, node_box_sizing.as_object().get(key).value_or(String {}));
|
2025-03-11 10:47:41 -03:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// FIXME: This response should also contain "top", "right", "bottom", and "left", but our box model metrics in
|
|
|
|
|
// WebContent do not provide this information.
|
|
|
|
|
|
2025-03-19 13:46:36 -03:00
|
|
|
set_computed_value("width"sv);
|
|
|
|
|
set_computed_value("height"sv);
|
|
|
|
|
|
|
|
|
|
set_pixel_value("border-top-width"sv);
|
|
|
|
|
set_pixel_value("border-right-width"sv);
|
|
|
|
|
set_pixel_value("border-bottom-width"sv);
|
|
|
|
|
set_pixel_value("border-left-width"sv);
|
|
|
|
|
|
|
|
|
|
set_pixel_value("margin-top"sv);
|
|
|
|
|
set_pixel_value("margin-right"sv);
|
|
|
|
|
set_pixel_value("margin-bottom"sv);
|
|
|
|
|
set_pixel_value("margin-left"sv);
|
|
|
|
|
|
|
|
|
|
set_pixel_value("padding-top"sv);
|
|
|
|
|
set_pixel_value("padding-right"sv);
|
|
|
|
|
set_pixel_value("padding-bottom"sv);
|
|
|
|
|
set_pixel_value("padding-left"sv);
|
|
|
|
|
|
|
|
|
|
set_computed_value("box-sizing"sv);
|
|
|
|
|
set_computed_value("display"sv);
|
|
|
|
|
set_computed_value("float"sv);
|
|
|
|
|
set_computed_value("line-height"sv);
|
|
|
|
|
set_computed_value("position"sv);
|
|
|
|
|
set_computed_value("z-index"sv);
|
2025-03-11 10:47:41 -03:00
|
|
|
}
|
|
|
|
|
|
2025-07-02 13:27:26 -03:00
|
|
|
static void received_computed_style(JsonObject& response, JsonValue const& computed_style)
|
2025-03-11 10:47:41 -03:00
|
|
|
{
|
|
|
|
|
JsonObject computed;
|
|
|
|
|
|
2025-07-02 13:27:26 -03:00
|
|
|
if (computed_style.is_object()) {
|
|
|
|
|
computed_style.as_object().for_each_member([&](String const& name, JsonValue const& value) {
|
|
|
|
|
JsonObject property;
|
|
|
|
|
property.set("matched"sv, true);
|
|
|
|
|
property.set("value"sv, value);
|
|
|
|
|
computed.set(name, move(property));
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-03-11 10:47:41 -03:00
|
|
|
|
|
|
|
|
response.set("computed"sv, move(computed));
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-02 13:27:26 -03:00
|
|
|
static void received_fonts(JsonObject& response, JsonValue const& fonts)
|
2025-03-11 17:01:34 -03:00
|
|
|
{
|
|
|
|
|
JsonArray font_faces;
|
|
|
|
|
|
2025-07-02 13:27:26 -03:00
|
|
|
if (fonts.is_array()) {
|
|
|
|
|
fonts.as_array().for_each([&](JsonValue const& font) {
|
|
|
|
|
if (!font.is_object())
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
auto name = font.as_object().get_string("name"sv).value_or({});
|
|
|
|
|
auto weight = font.as_object().get_integer<i64>("weight"sv).value_or(0);
|
|
|
|
|
|
|
|
|
|
JsonObject font_face;
|
|
|
|
|
font_face.set("CSSFamilyName"sv, name);
|
|
|
|
|
font_face.set("CSSGeneric"sv, JsonValue {});
|
|
|
|
|
font_face.set("format"sv, ""sv);
|
|
|
|
|
font_face.set("localName"sv, ""sv);
|
|
|
|
|
font_face.set("metadata"sv, ""sv);
|
|
|
|
|
font_face.set("name"sv, name);
|
|
|
|
|
font_face.set("srcIndex"sv, -1);
|
|
|
|
|
font_face.set("style"sv, ""sv);
|
|
|
|
|
font_face.set("URI"sv, ""sv);
|
|
|
|
|
font_face.set("variationAxes"sv, JsonArray {});
|
|
|
|
|
font_face.set("variationInstances"sv, JsonArray {});
|
|
|
|
|
font_face.set("weight"sv, weight);
|
|
|
|
|
|
|
|
|
|
font_faces.must_append(move(font_face));
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-03-11 17:01:34 -03:00
|
|
|
|
|
|
|
|
response.set("fontFaces"sv, move(font_faces));
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-03 09:07:55 -03:00
|
|
|
static Optional<Web::CSS::StyleSheetIdentifier> style_sheet_identifier_from_json(JsonObject const& object)
|
|
|
|
|
{
|
|
|
|
|
auto type_string = object.get_string("type"sv);
|
|
|
|
|
if (!type_string.has_value())
|
|
|
|
|
return {};
|
|
|
|
|
|
|
|
|
|
auto type = Web::CSS::style_sheet_identifier_type_from_string(*type_string);
|
|
|
|
|
if (!type.has_value())
|
|
|
|
|
return {};
|
|
|
|
|
|
|
|
|
|
auto dom_element_unique_id = object.get_integer<Web::UniqueNodeID::Type>("domElementUniqueId"sv);
|
|
|
|
|
auto rule_count = object.get_integer<size_t>("ruleCount"sv).value_or(0);
|
|
|
|
|
Optional<String> url;
|
|
|
|
|
if (auto url_value = object.get_string("url"sv); url_value.has_value())
|
|
|
|
|
url = *url_value;
|
|
|
|
|
|
|
|
|
|
return Web::CSS::StyleSheetIdentifier {
|
|
|
|
|
.type = *type,
|
|
|
|
|
.dom_element_unique_id = dom_element_unique_id.map([](auto value) -> Web::UniqueNodeID { return value; }),
|
|
|
|
|
.url = move(url),
|
|
|
|
|
.rule_count = rule_count,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void link_rule_to_style_sheet_resource(WeakPtr<InspectorActor> const& inspector, JsonObject& rule)
|
|
|
|
|
{
|
|
|
|
|
auto style_sheet = rule.get_object("styleSheet"sv);
|
|
|
|
|
if (!style_sheet.has_value())
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
ScopeGuard remove_internal_style_sheet_data = [&] {
|
|
|
|
|
rule.remove("styleSheet"sv);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
auto identifier = style_sheet_identifier_from_json(*style_sheet);
|
|
|
|
|
if (!identifier.has_value())
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
auto style_sheets_actor = InspectorActor::style_sheets_for(inspector);
|
|
|
|
|
if (!style_sheets_actor)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (auto resource_id = style_sheets_actor->resource_id_for(*identifier); resource_id.has_value())
|
|
|
|
|
rule.set("parentStyleSheet"sv, resource_id.release_value());
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-04 12:59:14 -03:00
|
|
|
void PageStyleActor::received_applied_style_rules(JsonObject& response, JsonValue const& applied_style_rules)
|
|
|
|
|
{
|
|
|
|
|
JsonArray entries;
|
|
|
|
|
clear_style_rule_actors();
|
|
|
|
|
|
|
|
|
|
if (applied_style_rules.is_array()) {
|
|
|
|
|
applied_style_rules.as_array().for_each([&](JsonValue const& value) {
|
|
|
|
|
if (!value.is_object())
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
auto entry = value.as_object();
|
|
|
|
|
auto rule = entry.get_object("rule"sv);
|
|
|
|
|
if (!rule.has_value())
|
|
|
|
|
return;
|
|
|
|
|
|
2026-06-03 09:07:55 -03:00
|
|
|
link_rule_to_style_sheet_resource(m_inspector, *rule);
|
|
|
|
|
|
2026-06-04 12:59:14 -03:00
|
|
|
auto& style_rule_actor = devtools().register_actor<StyleRuleActor>(*rule);
|
|
|
|
|
m_style_rule_actors.append(style_rule_actor.name());
|
|
|
|
|
entry.set("rule"sv, style_rule_actor.serialize_rule());
|
|
|
|
|
|
|
|
|
|
if (auto inherited_node_id = entry.get_integer<Web::UniqueNodeID::Type>("inheritedNodeId"sv); inherited_node_id.has_value()) {
|
|
|
|
|
JsonValue inherited { JsonValue {} };
|
|
|
|
|
if (auto walker = InspectorActor::walker_for(m_inspector)) {
|
|
|
|
|
if (auto inherited_node_actor = walker->node_actor_name_for(Web::UniqueNodeID { *inherited_node_id }); inherited_node_actor.has_value())
|
|
|
|
|
inherited = *inherited_node_actor;
|
|
|
|
|
}
|
|
|
|
|
entry.set("inherited"sv, move(inherited));
|
|
|
|
|
entry.remove("inheritedNodeId"sv);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
entries.must_append(move(entry));
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
response.set("entries"sv, move(entries));
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-21 18:20:53 -03:00
|
|
|
NonnullRefPtr<PageStyleActor> PageStyleActor::create(DevToolsServer& devtools, String name, WeakPtr<InspectorActor> inspector)
|
2025-02-15 09:57:36 -03:00
|
|
|
{
|
2025-02-21 18:20:53 -03:00
|
|
|
return adopt_ref(*new PageStyleActor(devtools, move(name), move(inspector)));
|
2025-02-15 09:57:36 -03:00
|
|
|
}
|
|
|
|
|
|
2025-02-21 18:20:53 -03:00
|
|
|
PageStyleActor::PageStyleActor(DevToolsServer& devtools, String name, WeakPtr<InspectorActor> inspector)
|
2025-02-15 09:57:36 -03:00
|
|
|
: Actor(devtools, move(name))
|
2025-02-21 18:20:53 -03:00
|
|
|
, m_inspector(move(inspector))
|
2025-02-15 09:57:36 -03:00
|
|
|
{
|
2025-03-19 16:50:56 -03:00
|
|
|
if (auto tab = InspectorActor::tab_for(m_inspector)) {
|
|
|
|
|
devtools.delegate().listen_for_dom_properties(tab->description(),
|
2026-02-25 05:44:32 -03:00
|
|
|
weak_callback(*this, [](auto& self, WebView::DOMNodeProperties const& properties) {
|
|
|
|
|
self.received_dom_node_properties(properties);
|
|
|
|
|
}));
|
2025-03-19 16:50:56 -03:00
|
|
|
}
|
2025-02-15 09:57:36 -03:00
|
|
|
}
|
|
|
|
|
|
2025-03-19 16:50:56 -03:00
|
|
|
PageStyleActor::~PageStyleActor()
|
|
|
|
|
{
|
2026-06-04 12:59:14 -03:00
|
|
|
clear_style_rule_actors();
|
|
|
|
|
|
2025-03-19 16:50:56 -03:00
|
|
|
if (auto tab = InspectorActor::tab_for(m_inspector))
|
|
|
|
|
devtools().delegate().stop_listening_for_dom_properties(tab->description());
|
|
|
|
|
}
|
2025-02-15 09:57:36 -03:00
|
|
|
|
2026-06-04 12:59:14 -03:00
|
|
|
void PageStyleActor::clear_style_rule_actors()
|
|
|
|
|
{
|
|
|
|
|
for (auto const& actor : m_style_rule_actors)
|
|
|
|
|
devtools().unregister_actor(actor);
|
|
|
|
|
m_style_rule_actors.clear();
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-12 09:01:23 -03:00
|
|
|
void PageStyleActor::handle_message(Message const& message)
|
2025-02-15 09:57:36 -03:00
|
|
|
{
|
2025-02-21 18:20:53 -03:00
|
|
|
JsonObject response;
|
|
|
|
|
|
2025-03-11 17:01:34 -03:00
|
|
|
if (message.type == "getAllUsedFontFaces"sv) {
|
|
|
|
|
response.set("fontFaces"sv, JsonArray {});
|
|
|
|
|
send_response(message, move(response));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-12 09:01:23 -03:00
|
|
|
if (message.type == "getApplied"sv) {
|
2026-06-04 12:59:14 -03:00
|
|
|
inspect_dom_node(message, WebView::DOMNodeProperties::Type::AppliedStyleRules);
|
2025-02-21 18:20:53 -03:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-12 09:01:23 -03:00
|
|
|
if (message.type == "getComputed"sv) {
|
2025-03-19 16:50:56 -03:00
|
|
|
inspect_dom_node(message, WebView::DOMNodeProperties::Type::ComputedStyle);
|
2025-02-21 18:20:53 -03:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-12 09:01:23 -03:00
|
|
|
if (message.type == "getLayout"sv) {
|
2025-03-19 16:50:56 -03:00
|
|
|
inspect_dom_node(message, WebView::DOMNodeProperties::Type::Layout);
|
2025-02-21 18:20:53 -03:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-11 17:01:34 -03:00
|
|
|
if (message.type == "getUsedFontFaces"sv) {
|
2025-03-19 16:50:56 -03:00
|
|
|
inspect_dom_node(message, WebView::DOMNodeProperties::Type::UsedFonts);
|
2025-03-11 17:01:34 -03:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-12 09:01:23 -03:00
|
|
|
if (message.type == "isPositionEditable") {
|
2025-02-21 18:20:53 -03:00
|
|
|
response.set("value"sv, false);
|
LibDevTools: Re-implement how we handle ordered responses
We must reply to requests received from the client in the order they are
received. The wrench in this requirement is handling requests that must
be performed asynchronously, such as fetching the serialized DOM tree
from the WebContent process.
We currently handle this with a "block token". Async request handlers
hold a token that blocks any subsequent responses from being sent. When
that token is removed (i.e. the async request now has a response to be
sent), the async response is then sent followed by the blocked responses
in-order.
This strategy had a limitation that we could not handle an actor trying
to take 2 block tokens, meaning only one async request could be handled
at a time. This has been fine so far, but an upcoming feature (style
sheet sources) will break this limitation. The client will request N
sources at a time, which would try to take N block tokens.
The new strategy is to assign all requests an ID, and store a list of
request IDs that are awaiting a response. When the server wants to send
a reply, we match the ID of the replied-to message to this list of IDs.
If it is not the first in this list, then we are blocked waiting for an
earlier reply, and just store the response. When the earlier request(s)
receive their response, we can then send out all blocked replies (up to
the next request that has not yet received a response).
2025-03-12 13:26:58 -03:00
|
|
|
send_response(message, move(response));
|
2025-02-21 18:20:53 -03:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-12 09:01:23 -03:00
|
|
|
send_unrecognized_packet_type_error(message);
|
2025-02-15 09:57:36 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
JsonValue PageStyleActor::serialize_style() const
|
|
|
|
|
{
|
|
|
|
|
JsonObject traits;
|
|
|
|
|
traits.set("fontStyleLevel4"sv, true);
|
|
|
|
|
traits.set("fontWeightLevel4"sv, true);
|
|
|
|
|
traits.set("fontStretchLevel4"sv, true);
|
|
|
|
|
traits.set("fontVariations"sv, true);
|
|
|
|
|
|
|
|
|
|
JsonObject style;
|
|
|
|
|
style.set("actor"sv, name());
|
|
|
|
|
style.set("traits"sv, move(traits));
|
|
|
|
|
return style;
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-19 16:50:56 -03:00
|
|
|
void PageStyleActor::inspect_dom_node(Message const& message, WebView::DOMNodeProperties::Type property_type)
|
2025-02-21 18:20:53 -03:00
|
|
|
{
|
2025-03-19 16:50:56 -03:00
|
|
|
auto node = get_required_parameter<String>(message, "node"sv);
|
|
|
|
|
if (!node.has_value())
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
auto dom_node = WalkerActor::dom_node_for(InspectorActor::walker_for(m_inspector), *node);
|
2025-03-11 08:55:05 -03:00
|
|
|
if (!dom_node.has_value()) {
|
2025-03-19 16:50:56 -03:00
|
|
|
send_unknown_actor_error(message, *node);
|
2025-02-21 18:20:53 -03:00
|
|
|
return;
|
2025-03-11 08:55:05 -03:00
|
|
|
}
|
2025-02-21 18:20:53 -03:00
|
|
|
|
2026-06-04 12:59:14 -03:00
|
|
|
JsonObject options;
|
|
|
|
|
if (property_type == WebView::DOMNodeProperties::Type::AppliedStyleRules) {
|
|
|
|
|
if (auto inherited = message.data.get_bool("inherited"sv); inherited.has_value())
|
|
|
|
|
options.set("inherited"sv, *inherited);
|
|
|
|
|
if (auto matched_selectors = message.data.get_bool("matchedSelectors"sv); matched_selectors.has_value())
|
|
|
|
|
options.set("matchedSelectors"sv, *matched_selectors);
|
|
|
|
|
if (auto skip_pseudo = message.data.get_bool("skipPseudo"sv); skip_pseudo.has_value())
|
|
|
|
|
options.set("skipPseudo"sv, *skip_pseudo);
|
|
|
|
|
if (auto filter = message.data.get_string("filter"sv); filter.has_value())
|
|
|
|
|
options.set("filter"sv, *filter);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
devtools().delegate().inspect_dom_node(dom_node->tab->description(), property_type, dom_node->identifier.id, dom_node->identifier.pseudo_element, move(options));
|
2025-03-19 16:50:56 -03:00
|
|
|
m_pending_inspect_requests.append({ .id = message.id });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void PageStyleActor::received_dom_node_properties(WebView::DOMNodeProperties const& properties)
|
|
|
|
|
{
|
|
|
|
|
if (m_pending_inspect_requests.is_empty())
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
JsonObject response;
|
|
|
|
|
|
|
|
|
|
switch (properties.type) {
|
2026-06-04 12:59:14 -03:00
|
|
|
case WebView::DOMNodeProperties::Type::AppliedStyleRules:
|
|
|
|
|
received_applied_style_rules(response, properties.properties);
|
|
|
|
|
break;
|
2025-03-19 16:50:56 -03:00
|
|
|
case WebView::DOMNodeProperties::Type::ComputedStyle:
|
2025-07-02 13:27:26 -03:00
|
|
|
received_computed_style(response, properties.properties);
|
2025-03-19 16:50:56 -03:00
|
|
|
break;
|
|
|
|
|
case WebView::DOMNodeProperties::Type::Layout:
|
2025-07-02 13:27:26 -03:00
|
|
|
received_layout(response, properties.properties);
|
2025-03-19 16:50:56 -03:00
|
|
|
break;
|
|
|
|
|
case WebView::DOMNodeProperties::Type::UsedFonts:
|
2025-07-02 13:27:26 -03:00
|
|
|
received_fonts(response, properties.properties);
|
2025-03-19 16:50:56 -03:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto message = m_pending_inspect_requests.take_first();
|
|
|
|
|
send_response(message, move(response));
|
2025-02-21 18:20:53 -03:00
|
|
|
}
|
|
|
|
|
|
2025-02-15 09:57:36 -03:00
|
|
|
}
|