devtools: Include layer rules in CSS panel using rule tree (#43912)

DevTools was collecting CSS rules by walking stylesheets and matching
selector text. This ignored cascade order and did not correctly handle
rules inside layer blocks.

This change uses computed values (rule tree) to get the actual applied
rules in cascade order. It then maps those rules back to CSSStyleRule
using the declaration block identity, and walks the CSSOM to get
selector text and layer ancestry.

This fills ancestor_data with layer names and lets the inspector show
layered rules correctly.


Testing: 
- Verified using the minimized testcase from the issue

- Verified on https://www.sharyap.com/

- Confirmed that rules inside layer blocks are now shown with correct
order and hierarchy.


Fixes: #43541

Signed-off-by: arabson99 <arabiusman99@gmail.com>
This commit is contained in:
Abubakar Abdulazeez Usman
2026-04-07 12:26:12 +01:00
committed by GitHub
parent 57adfc136f
commit 750fb41bdb
7 changed files with 250 additions and 148 deletions

View File

@@ -9,7 +9,8 @@ use std::collections::HashMap;
use atomic_refcell::AtomicRefCell;
use devtools_traits::{
AttrModification, DevtoolScriptControlMsg, EventListenerInfo, NodeInfo, ShadowRootMode,
AttrModification, DevtoolScriptControlMsg, EventListenerInfo, MatchedRule, NodeInfo,
ShadowRootMode,
};
use malloc_size_of_derive::MallocSizeOf;
use serde::Serialize;
@@ -132,7 +133,7 @@ pub(crate) struct NodeActor {
pub script_chan: GenericSender<DevtoolScriptControlMsg>,
pub pipeline: PipelineId,
pub walker: String,
pub style_rules: AtomicRefCell<HashMap<(String, usize), String>>,
pub style_rules: AtomicRefCell<HashMap<MatchedRule, String>>,
}
impl Actor for NodeActor {

View File

@@ -6,11 +6,10 @@
//! properties applied, including the attributes and layout of each element.
use std::collections::HashMap;
use std::collections::hash_map::Entry;
use std::iter::once;
use devtools_traits::DevtoolScriptControlMsg::{GetLayout, GetSelectors};
use devtools_traits::{AutoMargins, ComputedNodeLayout};
use devtools_traits::{AutoMargins, ComputedNodeLayout, MatchedRule};
use malloc_size_of_derive::MallocSizeOf;
use serde::Serialize;
use serde_json::{self, Map, Value};
@@ -177,28 +176,32 @@ impl PageStyleActor {
// For each selector (plus an empty one that represents the style attribute)
// get all of the rules associated with it.
once(("".into(), usize::MAX))
.chain(selectors)
.filter_map(move |selector| {
let rule = match node_actor.style_rules.borrow_mut().entry(selector) {
Entry::Vacant(e) => {
let name = registry.new_name::<StyleRuleActor>();
let actor = StyleRuleActor::new(
name.clone(),
node_actor.name(),
(!e.key().0.is_empty()).then_some(e.key().clone()),
);
let rule = actor.applied(registry)?;
let style_attribute_rule = MatchedRule {
selector: "".into(),
stylesheet_index: usize::MAX,
block_id: 0,
ancestor_data: vec![],
};
registry.register(actor);
e.insert(name);
rule
},
Entry::Occupied(e) => {
let actor = registry.find::<StyleRuleActor>(e.get());
actor.applied(registry)?
},
};
once(style_attribute_rule)
.chain(selectors)
.filter_map(move |matched_rule| {
let style_rule_name = node_actor
.style_rules
.borrow_mut()
.entry(matched_rule.clone())
.or_insert_with(|| {
StyleRuleActor::register(
registry,
node_actor.name(),
(matched_rule.stylesheet_index != usize::MAX)
.then_some(matched_rule.clone()),
)
})
.clone();
let actor = registry.find::<StyleRuleActor>(&style_rule_name);
let rule = actor.applied(registry)?;
if inherited.is_some() && rule.declarations.is_empty() {
return None;
}
@@ -232,27 +235,27 @@ impl PageStyleActor {
.as_str()
.ok_or(ActorError::BadParameterType)?;
let node_actor = registry.find::<NodeActor>(node_name);
let computed = (|| match node_actor
.style_rules
.borrow_mut()
.entry(("".into(), usize::MAX))
{
Entry::Vacant(e) => {
let name = registry.new_name::<StyleRuleActor>();
let actor = StyleRuleActor::new(name.clone(), node_name.into(), None);
let computed = actor.computed(registry)?;
registry.register(actor);
e.insert(name);
Some(computed)
},
Entry::Occupied(e) => {
let actor = registry.find::<StyleRuleActor>(e.get());
Some(actor.computed(registry)?)
},
})()
.unwrap_or_default();
let style_attribute_rule = devtools_traits::MatchedRule {
selector: "".into(),
stylesheet_index: usize::MAX,
block_id: 0,
ancestor_data: vec![],
};
let computed = {
let style_rule_name = node_actor
.style_rules
.borrow_mut()
.entry(style_attribute_rule)
.or_insert_with(|| StyleRuleActor::register(registry, node_name.into(), None))
.clone();
let actor = registry.find::<StyleRuleActor>(&style_rule_name);
actor.computed(registry)
};
let msg = GetComputedReply {
computed,
computed: computed.unwrap_or_default(),
from: self.name(),
};
request.reply_final(&msg)

View File

@@ -11,6 +11,7 @@ use std::collections::HashMap;
use devtools_traits::DevtoolScriptControlMsg::{
GetAttributeStyle, GetComputedStyle, GetDocumentElement, GetStylesheetStyle, ModifyRule,
};
use devtools_traits::{AncestorData, MatchedRule};
use malloc_size_of_derive::MallocSizeOf;
use serde::Serialize;
use serde_json::{Map, Value};
@@ -28,7 +29,7 @@ const ELEMENT_STYLE_TYPE: u32 = 100;
#[serde(rename_all = "camelCase")]
pub(crate) struct AppliedRule {
actor: String,
ancestor_data: Vec<()>,
ancestor_data: Vec<AncestorData>,
authored_text: String,
css_text: String,
pub declarations: Vec<AppliedDeclaration>,
@@ -83,7 +84,7 @@ pub(crate) struct StyleRuleActorMsg {
pub(crate) struct StyleRuleActor {
name: String,
node_name: String,
selector: Option<(String, usize)>,
selector: Option<MatchedRule>,
}
impl Actor for StyleRuleActor {
@@ -142,12 +143,19 @@ impl Actor for StyleRuleActor {
}
impl StyleRuleActor {
pub fn new(name: String, node: String, selector: Option<(String, usize)>) -> Self {
Self {
name,
pub fn register(
registry: &ActorRegistry,
node: String,
selector: Option<MatchedRule>,
) -> String {
let name = registry.new_name::<Self>();
let actor = Self {
name: name.clone(),
node_name: node,
selector,
}
};
registry.register::<Self>(actor);
name
}
pub fn applied(&self, registry: &ActorRegistry) -> Option<AppliedRule> {
@@ -169,16 +177,12 @@ impl StyleRuleActor {
// not, this represents the style attribute.
let (style_sender, style_receiver) = generic_channel::channel()?;
let req = match &self.selector {
Some(selector) => {
let (selector, stylesheet) = selector.clone();
GetStylesheetStyle(
browsing_context_actor.pipeline_id(),
registry.actor_to_script(self.node_name.clone()),
selector,
stylesheet,
style_sender,
)
},
Some(matched_rule) => GetStylesheetStyle(
browsing_context_actor.pipeline_id(),
registry.actor_to_script(self.node_name.clone()),
matched_rule.clone(),
style_sender,
),
None => GetAttributeStyle(
browsing_context_actor.pipeline_id(),
registry.actor_to_script(self.node_name.clone()),
@@ -190,7 +194,11 @@ impl StyleRuleActor {
Some(AppliedRule {
actor: self.name(),
ancestor_data: vec![], // TODO: Fill with hierarchy
ancestor_data: self
.selector
.as_ref()
.map(|r| r.ancestor_data.clone())
.unwrap_or_default(),
authored_text: "".into(),
css_text: "".into(), // TODO: Specify the css text
declarations: style
@@ -210,7 +218,7 @@ impl StyleRuleActor {
})
.collect(),
href: node.base_uri,
selectors: self.selector.iter().map(|(s, _)| s).cloned().collect(),
selectors: self.selector.iter().map(|r| r.selector.clone()).collect(),
selectors_specificity: self.selector.iter().map(|_| 1).collect(),
type_: ELEMENT_STYLE_TYPE,
traits: StyleRuleActorTraits {

View File

@@ -7,8 +7,9 @@ use std::collections::HashMap;
use std::str;
use devtools_traits::{
AttrModification, AutoMargins, ComputedNodeLayout, CssDatabaseProperty, EventListenerInfo,
NodeInfo, NodeStyle, RuleModification, TimelineMarker, TimelineMarkerType,
AncestorData, AttrModification, AutoMargins, ComputedNodeLayout, CssDatabaseProperty,
EventListenerInfo, MatchedRule, NodeInfo, NodeStyle, RuleModification, TimelineMarker,
TimelineMarkerType,
};
use js::context::JSContext;
use markup5ever::{LocalName, ns};
@@ -20,6 +21,8 @@ use servo_config::pref;
use style::attr::AttrValue;
use crate::document_collection::DocumentCollection;
use crate::dom::bindings::codegen::Bindings::CSSGroupingRuleBinding::CSSGroupingRuleMethods;
use crate::dom::bindings::codegen::Bindings::CSSLayerBlockRuleBinding::CSSLayerBlockRuleMethods;
use crate::dom::bindings::codegen::Bindings::CSSRuleListBinding::CSSRuleListMethods;
use crate::dom::bindings::codegen::Bindings::CSSStyleDeclarationBinding::CSSStyleDeclarationMethods;
use crate::dom::bindings::codegen::Bindings::CSSStyleRuleBinding::CSSStyleRuleMethods;
@@ -39,7 +42,7 @@ use crate::dom::css::cssstylerule::CSSStyleRule;
use crate::dom::document::AnimationFrameCallback;
use crate::dom::element::Element;
use crate::dom::node::{Node, NodeTraits, ShadowIncluding};
use crate::dom::types::{EventTarget, HTMLElement};
use crate::dom::types::{CSSGroupingRule, CSSLayerBlockRule, EventTarget, HTMLElement};
use crate::realms::enter_realm;
use crate::script_runtime::CanGc;
@@ -313,53 +316,75 @@ pub(crate) fn handle_get_attribute_style(
reply.send(Some(msg)).unwrap();
}
#[cfg_attr(crown, expect(crown::unrooted_must_root))]
#[allow(clippy::too_many_arguments)]
pub(crate) fn handle_get_stylesheet_style(
fn build_rule_map(
cx: &mut JSContext,
state: &DevtoolsState,
documents: &DocumentCollection,
pipeline: PipelineId,
node_id: &str,
selector: String,
stylesheet: usize,
reply: GenericSender<Option<Vec<NodeStyle>>>,
list: &crate::dom::css::cssrulelist::CSSRuleList,
stylesheet_index: usize,
ancestors: &[AncestorData],
map: &mut HashMap<usize, MatchedRule>,
) {
let msg = (|| {
let node = state.find_node_by_unique_id(pipeline, node_id)?;
let can_gc = CanGc::from_cx(cx);
for i in 0..list.Length() {
let Some(rule) = list.Item(i, can_gc) else {
continue;
};
let document = documents.find_document(pipeline)?;
let _realm = enter_realm(document.window());
let owner = node.stylesheet_list_owner();
if let Some(style_rule) = rule.downcast::<CSSStyleRule>() {
let block_id = style_rule.block_id();
map.entry(block_id).or_insert_with(|| MatchedRule {
selector: style_rule.SelectorText().into(),
stylesheet_index,
block_id,
ancestor_data: ancestors.to_vec(),
});
continue;
}
let stylesheet = owner.stylesheet_at(stylesheet)?;
let list = stylesheet.GetCssRules(CanGc::from_cx(cx)).ok()?;
if let Some(layer_rule) = rule.downcast::<CSSLayerBlockRule>() {
let name = layer_rule.Name().to_string();
let mut next = ancestors.to_vec();
next.push(AncestorData::Layer {
actor_id: None,
value: (!name.is_empty()).then_some(name),
});
let inner = layer_rule.upcast::<CSSGroupingRule>().CssRules(cx);
build_rule_map(cx, &inner, stylesheet_index, &next, map);
continue;
}
let styles = (0..list.Length())
.filter_map(move |i| {
let rule = list.Item(i, CanGc::from_cx(cx))?;
let style = rule.downcast::<CSSStyleRule>()?;
if selector != style.SelectorText() {
return None;
};
Some(style.Style(cx))
})
.flat_map(|style| {
(0..style.Length()).map(move |i| {
let name = style.Item(i);
NodeStyle {
name: name.to_string(),
value: style.GetPropertyValue(name.clone()).to_string(),
priority: style.GetPropertyPriority(name).to_string(),
}
})
})
.collect();
if let Some(group_rule) = rule.downcast::<CSSGroupingRule>() {
let inner = group_rule.CssRules(cx);
build_rule_map(cx, &inner, stylesheet_index, ancestors, map);
}
}
}
Some(styles)
})();
fn find_rule_by_block_id(
cx: &mut JSContext,
list: &crate::dom::css::cssrulelist::CSSRuleList,
target_block_id: usize,
) -> Option<DomRoot<CSSStyleRule>> {
let can_gc = CanGc::from_cx(cx);
for i in 0..list.Length() {
let Some(rule) = list.Item(i, can_gc) else {
continue;
};
reply.send(msg).unwrap();
if let Some(style_rule) = rule.downcast::<CSSStyleRule>() {
if style_rule.block_id() == target_block_id {
return Some(DomRoot::from_ref(style_rule));
}
continue;
}
if let Some(group_rule) = rule.downcast::<CSSGroupingRule>() {
let inner = group_rule.CssRules(cx);
if let Some(found) = find_rule_by_block_id(cx, &inner, target_block_id) {
return Some(found);
}
}
}
None
}
#[cfg_attr(crown, expect(crown::unrooted_must_root))]
@@ -369,33 +394,40 @@ pub(crate) fn handle_get_selectors(
documents: &DocumentCollection,
pipeline: PipelineId,
node_id: &str,
reply: GenericSender<Option<Vec<(String, usize)>>>,
reply: GenericSender<Option<Vec<MatchedRule>>>,
) {
let msg = (|| {
let node = state.find_node_by_unique_id(pipeline, node_id)?;
let elem = node.downcast::<Element>()?;
let document = documents.find_document(pipeline)?;
let _realm = enter_realm(document.window());
let owner = node.stylesheet_list_owner();
let rules = (0..owner.stylesheet_count())
.filter_map(|i| {
let stylesheet = owner.stylesheet_at(i)?;
let list = stylesheet.GetCssRules(CanGc::from_cx(cx)).ok()?;
let elem = node.downcast::<Element>()?;
let mut decl_map = HashMap::new();
for i in 0..owner.stylesheet_count() {
let Some(stylesheet) = owner.stylesheet_at(i) else {
continue;
};
let Ok(list) = stylesheet.GetCssRules(CanGc::from_cx(cx)) else {
continue;
};
build_rule_map(cx, &list, i, &[], &mut decl_map);
}
// TODO(#40600): Figure out how to move the cx into the `filter_map`
let can_gc = CanGc::from_cx(cx);
Some((0..list.Length()).filter_map(move |j| {
let rule = list.Item(j, can_gc)?;
let style = rule.downcast::<CSSStyleRule>()?;
let selector = style.SelectorText();
elem.Matches(selector.clone()).ok()?.then_some(())?;
Some((selector.into(), i))
}))
})
.flatten()
.collect();
let mut rules = Vec::new();
let computed = elem.style()?;
if let Some(rule_node) = computed.rules.as_ref() {
for rn in rule_node.self_and_ancestors() {
if let Some(source) = rn.style_source() {
let ptr = source.get().raw_ptr().as_ptr() as usize;
if let Some(matched) = decl_map.get(&ptr) {
rules.push(matched.clone());
}
}
}
}
Some(rules)
})();
@@ -403,6 +435,46 @@ pub(crate) fn handle_get_selectors(
reply.send(msg).unwrap();
}
#[cfg_attr(crown, expect(crown::unrooted_must_root))]
#[allow(clippy::too_many_arguments)]
pub(crate) fn handle_get_stylesheet_style(
cx: &mut JSContext,
state: &DevtoolsState,
documents: &DocumentCollection,
pipeline: PipelineId,
node_id: &str,
matched_rule: MatchedRule,
reply: GenericSender<Option<Vec<NodeStyle>>>,
) {
let msg = (|| {
let node = state.find_node_by_unique_id(pipeline, node_id)?;
let document = documents.find_document(pipeline)?;
let _realm = enter_realm(document.window());
let owner = node.stylesheet_list_owner();
let stylesheet = owner.stylesheet_at(matched_rule.stylesheet_index)?;
let list = stylesheet.GetCssRules(CanGc::from_cx(cx)).ok()?;
let style_rule = find_rule_by_block_id(cx, &list, matched_rule.block_id)?;
let declaration = style_rule.Style(cx);
Some(
(0..declaration.Length())
.map(|i| {
let name = declaration.Item(i);
NodeStyle {
name: name.to_string(),
value: declaration.GetPropertyValue(name.clone()).to_string(),
priority: declaration.GetPropertyPriority(name).to_string(),
}
})
.collect(),
)
})();
reply.send(msg).unwrap();
}
pub(crate) fn handle_get_computed_style(
state: &DevtoolsState,
pipeline: PipelineId,

View File

@@ -86,6 +86,16 @@ impl CSSStyleRule {
*self.style_rule.borrow_mut() = stylerule;
}
pub(crate) fn block_id(&self) -> usize {
let guard = self.css_grouping_rule.shared_lock().read();
self.style_rule
.borrow()
.read_with(&guard)
.block
.raw_ptr()
.as_ptr() as usize
}
}
impl SpecificCSSRule for CSSStyleRule {

View File

@@ -2131,22 +2131,17 @@ impl ScriptThread {
DevtoolScriptControlMsg::GetAttributeStyle(id, node_id, reply) => {
devtools::handle_get_attribute_style(cx, &self.devtools_state, id, &node_id, reply)
},
DevtoolScriptControlMsg::GetStylesheetStyle(
id,
node_id,
selector,
stylesheet,
reply,
) => devtools::handle_get_stylesheet_style(
cx,
&self.devtools_state,
&documents,
id,
&node_id,
selector,
stylesheet,
reply,
),
DevtoolScriptControlMsg::GetStylesheetStyle(id, node_id, matched_rule, reply) => {
devtools::handle_get_stylesheet_style(
cx,
&self.devtools_state,
&documents,
id,
&node_id,
matched_rule,
reply,
)
},
DevtoolScriptControlMsg::GetSelectors(id, node_id, reply) => {
devtools::handle_get_selectors(
cx,

View File

@@ -271,6 +271,24 @@ pub struct NodeStyle {
pub priority: String,
}
#[derive(Clone, Debug, Deserialize, Serialize, MallocSizeOf, PartialEq, Eq, Hash)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum AncestorData {
Layer {
actor_id: Option<String>,
value: Option<String>,
},
}
#[derive(Clone, Debug, Deserialize, Serialize, MallocSizeOf, PartialEq, Eq, Hash)]
#[serde(rename_all = "camelCase")]
pub struct MatchedRule {
pub selector: String,
pub stylesheet_index: usize,
pub block_id: usize,
pub ancestor_data: Vec<AncestorData>,
}
/// The properties of a DOM node as computed by layout.
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
@@ -323,17 +341,12 @@ pub enum DevtoolScriptControlMsg {
GetStylesheetStyle(
PipelineId,
String,
String,
usize,
MatchedRule,
GenericSender<Option<Vec<NodeStyle>>>,
),
/// Retrieves the CSS selectors for the given node. A selector is comprised of the text
/// of the selector and the id of the stylesheet that contains it.
GetSelectors(
PipelineId,
String,
GenericSender<Option<Vec<(String, usize)>>>,
),
GetSelectors(PipelineId, String, GenericSender<Option<Vec<MatchedRule>>>),
/// Retrieve the computed CSS style properties for the given node.
GetComputedStyle(PipelineId, String, GenericSender<Option<Vec<NodeStyle>>>),
/// Get information about event listeners on a node.