diff options
author | eri <eri@inventati.org> | 2024-08-25 11:30:23 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-08-25 09:30:23 +0000 |
commit | 6357998ede902de7fb75354283f4fabbc141c28c (patch) | |
tree | 75df42a51ede24d86562e00ae60d8c43a686282b /components | |
parent | 67e2bb0ee6039e98f361e33617c0401a52963daf (diff) | |
download | servo-6357998ede902de7fb75354283f4fabbc141c28c.tar.gz servo-6357998ede902de7fb75354283f4fabbc141c28c.zip |
DevTools: Inspect node styles (#33025)
* feat: retrieve applied styles
Signed-off-by: eri <eri@inventati.org>
* feat: preliminary style showing
Signed-off-by: eri <eri@inventati.org>
* chore: some style tests
Signed-off-by: eri <eri@inventati.org>
* feat: edit style rules
Signed-off-by: eri <eri@inventati.org>
* feat: css database
Signed-off-by: eri <eri@inventati.org>
* feat: computed styles
Signed-off-by: eri <eri@inventati.org>
* feat: inherited styles
Signed-off-by: eri <eri@inventati.org>
* feat: get stylesheet styles
Signed-off-by: eri <eri@inventati.org>
* feat: all styles in inspector
Signed-off-by: eri <eri@inventati.org>
* feat: multiple stylesheets
Signed-off-by: eri <eri@inventati.org>
* refactor: clean up
Signed-off-by: eri <eri@inventati.org>
* Some minor cleanup
Signed-off-by: Martin Robinson <mrobinson@igalia.com>
---------
Signed-off-by: eri <eri@inventati.org>
Signed-off-by: Martin Robinson <mrobinson@igalia.com>
Co-authored-by: Martin Robinson <mrobinson@igalia.com>
Diffstat (limited to 'components')
-rw-r--r-- | components/devtools/actors/browsing_context.rs | 12 | ||||
-rw-r--r-- | components/devtools/actors/inspector.rs | 1 | ||||
-rw-r--r-- | components/devtools/actors/inspector/css_properties.rs | 30 | ||||
-rw-r--r-- | components/devtools/actors/inspector/node.rs | 13 | ||||
-rw-r--r-- | components/devtools/actors/inspector/page_style.rs | 378 | ||||
-rw-r--r-- | components/devtools/actors/inspector/style_rule.rs | 252 | ||||
-rw-r--r-- | components/devtools/actors/inspector/walker.rs | 17 | ||||
-rw-r--r-- | components/devtools/actors/watcher.rs | 2 | ||||
-rw-r--r-- | components/script/devtools.rs | 211 | ||||
-rw-r--r-- | components/script/dom/cssstyledeclaration.rs | 2 | ||||
-rw-r--r-- | components/script/script_thread.rs | 24 | ||||
-rw-r--r-- | components/shared/devtools/lib.rs | 52 |
12 files changed, 802 insertions, 192 deletions
diff --git a/components/devtools/actors/browsing_context.rs b/components/devtools/actors/browsing_context.rs index 4a667d48556..9a9173adb7b 100644 --- a/components/devtools/actors/browsing_context.rs +++ b/components/devtools/actors/browsing_context.rs @@ -12,9 +12,9 @@ use std::net::TcpStream; use std::time::{SystemTime, UNIX_EPOCH}; use base::id::{BrowsingContextId, PipelineId}; -use devtools_traits::DevtoolScriptControlMsg::{self, WantsLiveNotifications}; +use devtools_traits::DevtoolScriptControlMsg::{self, GetCssDatabase, WantsLiveNotifications}; use devtools_traits::{ConsoleLog, DevtoolsPageInfo, NavigationState, PageError}; -use ipc_channel::ipc::IpcSender; +use ipc_channel::ipc::{self, IpcSender}; use serde::Serialize; use serde_json::{Map, Value}; @@ -229,7 +229,13 @@ impl BrowsingContextActor { let accessibility = AccessibilityActor::new(actors.new_name("accessibility")); - let css_properties = CssPropertiesActor::new(actors.new_name("css-properties")); + let properties = (|| { + let (properties_sender, properties_receiver) = ipc::channel().ok()?; + script_sender.send(GetCssDatabase(properties_sender)).ok()?; + properties_receiver.recv().ok() + })() + .unwrap_or_default(); + let css_properties = CssPropertiesActor::new(actors.new_name("css-properties"), properties); let inspector = InspectorActor { name: actors.new_name("inspector"), diff --git a/components/devtools/actors/inspector.rs b/components/devtools/actors/inspector.rs index db3cde89bec..dfc13e3433d 100644 --- a/components/devtools/actors/inspector.rs +++ b/components/devtools/actors/inspector.rs @@ -29,6 +29,7 @@ pub mod highlighter; pub mod layout; pub mod node; pub mod page_style; +pub mod style_rule; pub mod walker; #[derive(Serialize)] diff --git a/components/devtools/actors/inspector/css_properties.rs b/components/devtools/actors/inspector/css_properties.rs index 0b411292189..cb7a939f224 100644 --- a/components/devtools/actors/inspector/css_properties.rs +++ b/components/devtools/actors/inspector/css_properties.rs @@ -8,6 +8,7 @@ use std::collections::HashMap; use std::net::TcpStream; +use devtools_traits::CssDatabaseProperty; use serde::Serialize; use serde_json::{Map, Value}; @@ -17,21 +18,13 @@ use crate::StreamId; pub struct CssPropertiesActor { name: String, + properties: HashMap<String, CssDatabaseProperty>, } #[derive(Serialize)] -#[serde(rename_all = "camelCase")] -struct CssDatabaseProperty { - is_inherited: bool, - values: Vec<&'static str>, - supports: Vec<&'static str>, - subproperties: Vec<&'static str>, -} - -#[derive(Serialize)] -struct GetCssDatabaseReply { - properties: HashMap<&'static str, CssDatabaseProperty>, +struct GetCssDatabaseReply<'a> { from: String, + properties: &'a HashMap<String, CssDatabaseProperty>, } impl Actor for CssPropertiesActor { @@ -55,16 +48,7 @@ impl Actor for CssPropertiesActor { "getCSSDatabase" => { let _ = stream.write_json_packet(&GetCssDatabaseReply { from: self.name(), - // TODO: Fill this programatically with other properties - properties: HashMap::from([( - "color", - CssDatabaseProperty { - is_inherited: true, - values: vec!["color"], - supports: vec!["color"], - subproperties: vec!["color"], - }, - )]), + properties: &self.properties, }); ActorMessageStatus::Processed @@ -75,7 +59,7 @@ impl Actor for CssPropertiesActor { } impl CssPropertiesActor { - pub fn new(name: String) -> Self { - Self { name } + pub fn new(name: String, properties: HashMap<String, CssDatabaseProperty>) -> Self { + Self { name, properties } } } diff --git a/components/devtools/actors/inspector/node.rs b/components/devtools/actors/inspector/node.rs index 650b0c6bf54..a203f43b839 100644 --- a/components/devtools/actors/inspector/node.rs +++ b/components/devtools/actors/inspector/node.rs @@ -5,6 +5,7 @@ //! This actor represents one DOM node. It is created by the Walker actor when it is traversing the //! document tree. +use std::cell::RefCell; use std::collections::HashMap; use std::net::TcpStream; @@ -76,9 +77,10 @@ pub struct NodeActorMsg { pub struct NodeActor { name: String, - script_chan: IpcSender<DevtoolScriptControlMsg>, - pipeline: PipelineId, + pub script_chan: IpcSender<DevtoolScriptControlMsg>, + pub pipeline: PipelineId, pub walker: String, + pub style_rules: RefCell<HashMap<(String, usize), String>>, } impl Actor for NodeActor { @@ -102,7 +104,6 @@ impl Actor for NodeActor { ) -> Result<ActorMessageStatus, ()> { Ok(match msg_type { "modifyAttributes" => { - let target = msg.get("to").ok_or(())?.as_str().ok_or(())?; let mods = msg.get("modifications").ok_or(())?.as_array().ok_or(())?; let modifications: Vec<_> = mods .iter() @@ -117,7 +118,7 @@ impl Actor for NodeActor { self.script_chan .send(ModifyAttribute( self.pipeline, - registry.actor_to_script(target.to_owned()), + registry.actor_to_script(self.name()), modifications, )) .map_err(|_| ())?; @@ -176,13 +177,15 @@ impl NodeInfoToProtocol for NodeInfo { ) -> NodeActorMsg { let actor = if !actors.script_actor_registered(self.unique_id.clone()) { let name = actors.new_name("node"); + actors.register_script_actor(self.unique_id, name.clone()); + let node_actor = NodeActor { name: name.clone(), script_chan: script_chan.clone(), pipeline, walker: walker.clone(), + style_rules: RefCell::new(HashMap::new()), }; - actors.register_script_actor(self.unique_id, name.clone()); actors.register_later(Box::new(node_actor)); name } else { diff --git a/components/devtools/actors/inspector/page_style.rs b/components/devtools/actors/inspector/page_style.rs index dc69df13584..481c55c3e3e 100644 --- a/components/devtools/actors/inspector/page_style.rs +++ b/components/devtools/actors/inspector/page_style.rs @@ -5,67 +5,45 @@ //! The page style actor is responsible of informing the DevTools client of the different style //! properties applied, including the attributes and layout of each element. +use std::collections::hash_map::Entry; use std::collections::HashMap; +use std::iter::once; use std::net::TcpStream; use base::id::PipelineId; -use devtools_traits::DevtoolScriptControlMsg::GetLayout; +use devtools_traits::DevtoolScriptControlMsg::{GetLayout, GetSelectors}; use devtools_traits::{ComputedNodeLayout, DevtoolScriptControlMsg}; use ipc_channel::ipc::{self, IpcSender}; use serde::Serialize; use serde_json::{self, Map, Value}; use crate::actor::{Actor, ActorMessageStatus, ActorRegistry}; +use crate::actors::inspector::node::NodeActor; +use crate::actors::inspector::style_rule::{AppliedRule, ComputedDeclaration, StyleRuleActor}; +use crate::actors::inspector::walker::{find_child, WalkerActor}; use crate::protocol::JsonPacketStream; use crate::StreamId; #[derive(Serialize)] struct GetAppliedReply { entries: Vec<AppliedEntry>, - rules: Vec<AppliedRule>, - sheets: Vec<AppliedSheet>, from: String, } #[derive(Serialize)] struct GetComputedReply { - computed: Vec<u32>, //XXX all css props + computed: HashMap<String, ComputedDeclaration>, from: String, } #[derive(Serialize)] #[serde(rename_all = "camelCase")] struct AppliedEntry { - rule: String, - pseudo_element: Value, + rule: AppliedRule, + pseudo_element: Option<()>, is_system: bool, - matched_selectors: Vec<String>, -} - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -struct AppliedRule { - actor: String, - #[serde(rename = "type")] - type_: String, - href: String, - css_text: String, - line: u32, - column: u32, - parent_style_sheet: String, -} - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -struct AppliedSheet { - actor: String, - href: String, - node_href: String, - disabled: bool, - title: String, - system: bool, - style_sheet_index: isize, - rule_count: usize, + #[serde(skip_serializing_if = "Option::is_none")] + inherited: Option<String>, } #[derive(Serialize)] @@ -125,13 +103,15 @@ impl Actor for PageStyleActor { /// The page style actor can handle the following messages: /// - /// - `getApplied`: Returns the applied styles for a node, placeholder + /// - `getApplied`: Returns the applied styles for a node, they represent the explicit css + /// rules set for them, both in the style attribute and in stylesheets. /// - /// - `getComputed`: Returns the computed styles for a node, placeholder + /// - `getComputed`: Returns the computed styles for a node, these include all of the supported + /// css properties calculated values. /// - /// - `getLayout`: Returns the box layout properties for a node, placeholder + /// - `getLayout`: Returns the box layout properties for a node. /// - /// - `isPositionEditable`: Informs whether you can change a style property in the inspector + /// - `isPositionEditable`: Informs whether you can change a style property in the inspector. fn handle_message( &self, registry: &ActorRegistry, @@ -141,123 +121,231 @@ impl Actor for PageStyleActor { _id: StreamId, ) -> Result<ActorMessageStatus, ()> { Ok(match msg_type { - "getApplied" => { - // TODO: Query script for relevant applied styles to node (msg.node) - let msg = GetAppliedReply { - entries: vec![], - rules: vec![], - sheets: vec![], - from: self.name(), - }; - let _ = stream.write_json_packet(&msg); - ActorMessageStatus::Processed - }, + "getApplied" => self.get_applied(msg, registry, stream)?, + "getComputed" => self.get_computed(msg, registry, stream)?, + "getLayout" => self.get_layout(msg, registry, stream)?, + "isPositionEditable" => self.is_position_editable(stream), + _ => ActorMessageStatus::Ignored, + }) + } +} - "getComputed" => { - // TODO: Query script for relevant computed styles on node (msg.node) - let msg = GetComputedReply { - computed: vec![], - from: self.name(), - }; - let _ = stream.write_json_packet(&msg); - ActorMessageStatus::Processed - }, +impl PageStyleActor { + fn get_applied( + &self, + msg: &Map<String, Value>, + registry: &ActorRegistry, + stream: &mut TcpStream, + ) -> Result<ActorMessageStatus, ()> { + let target = msg.get("node").ok_or(())?.as_str().ok_or(())?; + let node = registry.find::<NodeActor>(&target); + let walker = registry.find::<WalkerActor>(&node.walker); + let entries: Vec<_> = find_child( + &node.script_chan, + node.pipeline, + &target, + registry, + &walker.root_node.actor, + vec![], + |msg| msg.actor == target, + ) + .unwrap_or_default() + .into_iter() + .filter_map(|node| { + let inherited = (node.actor != target).then(|| node.actor.clone()); + let node_actor = registry.find::<NodeActor>(&node.actor); - "getLayout" => { - // TODO: Query script for box layout properties of node (msg.node) - let target = msg.get("node").ok_or(())?.as_str().ok_or(())?; - let (tx, rx) = ipc::channel().map_err(|_| ())?; - self.script_chan - .send(GetLayout( - self.pipeline, - registry.actor_to_script(target.to_owned()), - tx, + // Get the css selectors that match this node present in the currently active stylesheets. + let selectors = (|| { + let (selectors_sender, selector_receiver) = ipc::channel().ok()?; + walker + .script_chan + .send(GetSelectors( + walker.pipeline, + registry.actor_to_script(node.actor.clone()), + selectors_sender, )) - .unwrap(); - let ComputedNodeLayout { - display, - position, - z_index, - box_sizing, - auto_margins, - margin_top, - margin_right, - margin_bottom, - margin_left, - border_top_width, - border_right_width, - border_bottom_width, - border_left_width, - padding_top, - padding_right, - padding_bottom, - padding_left, - width, - height, - } = rx.recv().map_err(|_| ())?.ok_or(())?; + .ok()?; + selector_receiver.recv().ok()? + })() + .unwrap_or_default(); - let msg_auto_margins = msg - .get("autoMargins") - .and_then(Value::as_bool) - .unwrap_or(false); + // For each selector (plus an empty one that represents the style attribute) + // get all of the rules associated with it. + let entries = + 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("style-rule"); + let actor = StyleRuleActor::new( + name.clone(), + node_actor.name(), + (e.key().0 != "").then_some(e.key().clone()), + ); + let rule = actor.applied(registry)?; - // https://searchfox.org/mozilla-central/source/devtools/server/actors/page-style.js - let msg = GetLayoutReply { - from: self.name(), - display, - position, - z_index, - box_sizing, - auto_margins: if msg_auto_margins { - let mut m = Map::new(); - let auto = serde_json::value::Value::String("auto".to_owned()); - if auto_margins.top { - m.insert("top".to_owned(), auto.clone()); - } - if auto_margins.right { - m.insert("right".to_owned(), auto.clone()); - } - if auto_margins.bottom { - m.insert("bottom".to_owned(), auto.clone()); + registry.register_later(Box::new(actor)); + e.insert(name); + rule + }, + Entry::Occupied(e) => { + let actor = registry.find::<StyleRuleActor>(e.get()); + actor.applied(registry)? + }, + }; + if inherited.is_some() && rule.declarations.is_empty() { + return None; } - if auto_margins.left { - m.insert("left".to_owned(), auto); - } - serde_json::value::Value::Object(m) - } else { - serde_json::value::Value::Null - }, - margin_top, - margin_right, - margin_bottom, - margin_left, - border_top_width, - border_right_width, - border_bottom_width, - border_left_width, - padding_top, - padding_right, - padding_bottom, - padding_left, - width, - height, - }; - let msg = serde_json::to_string(&msg).map_err(|_| ())?; - let msg = serde_json::from_str::<Value>(&msg).map_err(|_| ())?; - let _ = stream.write_json_packet(&msg); - ActorMessageStatus::Processed + + Some(AppliedEntry { + rule, + // TODO: Handle pseudo elements + pseudo_element: None, + is_system: false, + inherited: inherited.clone(), + }) + }); + Some(entries) + }) + .flatten() + .collect(); + let msg = GetAppliedReply { + entries, + from: self.name(), + }; + let _ = stream.write_json_packet(&msg); + Ok(ActorMessageStatus::Processed) + } + + fn get_computed( + &self, + msg: &Map<String, Value>, + registry: &ActorRegistry, + stream: &mut TcpStream, + ) -> Result<ActorMessageStatus, ()> { + let target = msg.get("node").ok_or(())?.as_str().ok_or(())?; + let node_actor = registry.find::<NodeActor>(&target); + let computed = (|| match node_actor + .style_rules + .borrow_mut() + .entry(("".into(), usize::MAX)) + { + Entry::Vacant(e) => { + let name = registry.new_name("style-rule"); + let actor = StyleRuleActor::new(name.clone(), target.into(), None); + let computed = actor.computed(registry)?; + registry.register_later(Box::new(actor)); + e.insert(name); + Some(computed) }, + Entry::Occupied(e) => { + let actor = registry.find::<StyleRuleActor>(e.get()); + Some(actor.computed(registry)?) + }, + })() + .unwrap_or_default(); + let msg = GetComputedReply { + computed, + from: self.name(), + }; + let _ = stream.write_json_packet(&msg); + Ok(ActorMessageStatus::Processed) + } - "isPositionEditable" => { - let msg = IsPositionEditableReply { - from: self.name(), - value: false, - }; - let _ = stream.write_json_packet(&msg); - ActorMessageStatus::Processed + fn get_layout( + &self, + msg: &Map<String, Value>, + registry: &ActorRegistry, + stream: &mut TcpStream, + ) -> Result<ActorMessageStatus, ()> { + let target = msg.get("node").ok_or(())?.as_str().ok_or(())?; + let (computed_node_sender, computed_node_receiver) = ipc::channel().map_err(|_| ())?; + self.script_chan + .send(GetLayout( + self.pipeline, + registry.actor_to_script(target.to_owned()), + computed_node_sender, + )) + .unwrap(); + let ComputedNodeLayout { + display, + position, + z_index, + box_sizing, + auto_margins, + margin_top, + margin_right, + margin_bottom, + margin_left, + border_top_width, + border_right_width, + border_bottom_width, + border_left_width, + padding_top, + padding_right, + padding_bottom, + padding_left, + width, + height, + } = computed_node_receiver.recv().map_err(|_| ())?.ok_or(())?; + let msg_auto_margins = msg + .get("autoMargins") + .and_then(Value::as_bool) + .unwrap_or(false); + let msg = GetLayoutReply { + from: self.name(), + display, + position, + z_index, + box_sizing, + auto_margins: if msg_auto_margins { + let mut m = Map::new(); + let auto = serde_json::value::Value::String("auto".to_owned()); + if auto_margins.top { + m.insert("top".to_owned(), auto.clone()); + } + if auto_margins.right { + m.insert("right".to_owned(), auto.clone()); + } + if auto_margins.bottom { + m.insert("bottom".to_owned(), auto.clone()); + } + if auto_margins.left { + m.insert("left".to_owned(), auto); + } + serde_json::value::Value::Object(m) + } else { + serde_json::value::Value::Null }, + margin_top, + margin_right, + margin_bottom, + margin_left, + border_top_width, + border_right_width, + border_bottom_width, + border_left_width, + padding_top, + padding_right, + padding_bottom, + padding_left, + width, + height, + }; + let msg = serde_json::to_string(&msg).map_err(|_| ())?; + let msg = serde_json::from_str::<Value>(&msg).map_err(|_| ())?; + let _ = stream.write_json_packet(&msg); + Ok(ActorMessageStatus::Processed) + } - _ => ActorMessageStatus::Ignored, - }) + fn is_position_editable(&self, stream: &mut TcpStream) -> ActorMessageStatus { + let msg = IsPositionEditableReply { + from: self.name(), + value: false, + }; + let _ = stream.write_json_packet(&msg); + ActorMessageStatus::Processed } } diff --git a/components/devtools/actors/inspector/style_rule.rs b/components/devtools/actors/inspector/style_rule.rs new file mode 100644 index 00000000000..0be0d017fc6 --- /dev/null +++ b/components/devtools/actors/inspector/style_rule.rs @@ -0,0 +1,252 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Liberally derived from <https://searchfox.org/mozilla-central/source/devtools/server/actors/thread-configuration.js> +//! This actor represents one css rule group from a node, allowing the inspector to view it and change it. +//! A group is either the html style attribute or one selector from one stylesheet. + +use std::collections::HashMap; +use std::net::TcpStream; + +use devtools_traits::DevtoolScriptControlMsg::{ + GetAttributeStyle, GetComputedStyle, GetDocumentElement, GetStylesheetStyle, ModifyRule, +}; +use ipc_channel::ipc; +use serde::Serialize; +use serde_json::{Map, Value}; + +use crate::actor::{Actor, ActorMessageStatus, ActorRegistry}; +use crate::actors::inspector::node::NodeActor; +use crate::actors::inspector::walker::WalkerActor; +use crate::protocol::JsonPacketStream; +use crate::StreamId; + +const ELEMENT_STYLE_TYPE: u32 = 100; + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AppliedRule { + actor: String, + ancestor_data: Vec<()>, + authored_text: String, + css_text: String, + pub declarations: Vec<AppliedDeclaration>, + href: String, + #[serde(skip_serializing_if = "Vec::is_empty")] + selectors: Vec<String>, + #[serde(skip_serializing_if = "Vec::is_empty")] + selectors_specificity: Vec<u32>, + #[serde(rename = "type")] + type_: u32, + traits: StyleRuleActorTraits, +} + +#[derive(Serialize)] +pub struct IsUsed { + pub used: bool, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AppliedDeclaration { + colon_offsets: Vec<i32>, + is_name_valid: bool, + is_used: IsUsed, + is_valid: bool, + name: String, + offsets: Vec<i32>, + priority: String, + terminator: String, + value: String, +} + +#[derive(Serialize)] +pub struct ComputedDeclaration { + matched: bool, + value: String, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct StyleRuleActorTraits { + pub can_set_rule_text: bool, +} + +#[derive(Serialize)] +pub struct StyleRuleActorMsg { + from: String, + rule: Option<AppliedRule>, +} + +pub struct StyleRuleActor { + name: String, + node: String, + selector: Option<(String, usize)>, +} + +impl Actor for StyleRuleActor { + fn name(&self) -> String { + self.name.clone() + } + + /// The style rule configuration actor can handle the following messages: + /// + /// - `setRuleText`: Applies a set of modifications to the css rules that this actor manages. + /// There is also `modifyProperties`, which has a slightly different API to do the same, but + /// this is preferred. Which one the devtools client sends is decided by the `traits` defined + /// when returning the list of rules. + fn handle_message( + &self, + registry: &ActorRegistry, + msg_type: &str, + msg: &Map<String, Value>, + stream: &mut TcpStream, + _id: StreamId, + ) -> Result<ActorMessageStatus, ()> { + Ok(match msg_type { + "setRuleText" => { + // Parse the modifications sent from the client + let mods = msg.get("modifications").ok_or(())?.as_array().ok_or(())?; + let modifications: Vec<_> = mods + .iter() + .filter_map(|json_mod| { + serde_json::from_str(&serde_json::to_string(json_mod).ok()?).ok() + }) + .collect(); + + // Query the rule modification + let node = registry.find::<NodeActor>(&self.node); + let walker = registry.find::<WalkerActor>(&node.walker); + walker + .script_chan + .send(ModifyRule( + walker.pipeline, + registry.actor_to_script(self.node.clone()), + modifications, + )) + .map_err(|_| ())?; + + let _ = stream.write_json_packet(&self.encodable(registry)); + ActorMessageStatus::Processed + }, + _ => ActorMessageStatus::Ignored, + }) + } +} + +impl StyleRuleActor { + pub fn new(name: String, node: String, selector: Option<(String, usize)>) -> Self { + Self { + name, + node, + selector, + } + } + + pub fn applied(&self, registry: &ActorRegistry) -> Option<AppliedRule> { + let node = registry.find::<NodeActor>(&self.node); + let walker = registry.find::<WalkerActor>(&node.walker); + + let (document_sender, document_receiver) = ipc::channel().ok()?; + walker + .script_chan + .send(GetDocumentElement(walker.pipeline, document_sender)) + .ok()?; + let node = document_receiver.recv().ok()??; + + // Gets the style definitions. If there is a selector, query the relevant stylesheet, if + // not, this represents the style attribute. + let (style_sender, style_receiver) = ipc::channel().ok()?; + let req = match &self.selector { + Some(selector) => { + let (selector, stylesheet) = selector.clone(); + GetStylesheetStyle( + walker.pipeline, + registry.actor_to_script(self.node.clone()), + selector, + stylesheet, + style_sender, + ) + }, + None => GetAttributeStyle( + walker.pipeline, + registry.actor_to_script(self.node.clone()), + style_sender, + ), + }; + walker.script_chan.send(req).ok()?; + let style = style_receiver.recv().ok()??; + + Some(AppliedRule { + actor: self.name(), + ancestor_data: vec![], // TODO: Fill with hierarchy + authored_text: "".into(), + css_text: "".into(), // TODO: Specify the css text + declarations: style + .into_iter() + .filter_map(|decl| { + Some(AppliedDeclaration { + colon_offsets: vec![], + is_name_valid: true, + is_used: IsUsed { used: true }, + is_valid: true, + name: decl.name, + offsets: vec![], // TODO: Get the source of the declaration + priority: decl.priority, + terminator: "".into(), + value: decl.value, + }) + }) + .collect(), + href: node.base_uri.clone(), + selectors: self.selector.iter().map(|(s, _)| s).cloned().collect(), + selectors_specificity: self.selector.iter().map(|_| 1).collect(), + type_: ELEMENT_STYLE_TYPE, + traits: StyleRuleActorTraits { + can_set_rule_text: true, + }, + }) + } + + pub fn computed( + &self, + registry: &ActorRegistry, + ) -> Option<HashMap<String, ComputedDeclaration>> { + let node = registry.find::<NodeActor>(&self.node); + let walker = registry.find::<WalkerActor>(&node.walker); + + let (style_sender, style_receiver) = ipc::channel().ok()?; + walker + .script_chan + .send(GetComputedStyle( + walker.pipeline, + registry.actor_to_script(self.node.clone()), + style_sender, + )) + .ok()?; + let style = style_receiver.recv().ok()??; + + Some( + style + .into_iter() + .map(|s| { + ( + s.name, + ComputedDeclaration { + matched: true, + value: s.value, + }, + ) + }) + .collect(), + ) + } + + pub fn encodable(&self, registry: &ActorRegistry) -> StyleRuleActorMsg { + StyleRuleActorMsg { + from: self.name(), + rule: self.applied(registry), + } + } +} diff --git a/components/devtools/actors/inspector/walker.rs b/components/devtools/actors/inspector/walker.rs index 72ddd01209c..9b3447df7bf 100644 --- a/components/devtools/actors/inspector/walker.rs +++ b/components/devtools/actors/inspector/walker.rs @@ -9,7 +9,7 @@ use std::net::TcpStream; use base::id::PipelineId; use devtools_traits::DevtoolScriptControlMsg::{GetChildren, GetDocumentElement}; -use devtools_traits::{DevtoolScriptControlMsg, Modification}; +use devtools_traits::{AttrModification, DevtoolScriptControlMsg}; use ipc_channel::ipc::{self, IpcSender}; use serde::Serialize; use serde_json::{self, Map, Value}; @@ -31,7 +31,7 @@ pub struct WalkerActor { pub script_chan: IpcSender<DevtoolScriptControlMsg>, pub pipeline: PipelineId, pub root_node: NodeActorMsg, - pub mutations: RefCell<Vec<(Modification, String)>>, + pub mutations: RefCell<Vec<(AttrModification, String)>>, } #[derive(Serialize)] @@ -235,9 +235,9 @@ impl Actor for WalkerActor { self.pipeline, &self.name, registry, - selector, node, vec![], + |msg| msg.display_name == selector, ) .map_err(|_| ())?; hierarchy.reverse(); @@ -273,7 +273,7 @@ impl WalkerActor { &self, stream: &mut TcpStream, target: &str, - modifications: &[Modification], + modifications: &[AttrModification], ) { { let mut mutations = self.mutations.borrow_mut(); @@ -288,14 +288,15 @@ impl WalkerActor { /// Recursively searches for a child with the specified selector /// If it is found, returns a list with the child and all of its ancestors. -fn find_child( +/// TODO: Investigate how to cache this to some extent. +pub fn find_child( script_chan: &IpcSender<DevtoolScriptControlMsg>, pipeline: PipelineId, name: &str, registry: &ActorRegistry, - selector: &str, node: &str, mut hierarchy: Vec<NodeActorMsg>, + compare_fn: impl Fn(&NodeActorMsg) -> bool + Clone, ) -> Result<Vec<NodeActorMsg>, Vec<NodeActorMsg>> { let (tx, rx) = ipc::channel().unwrap(); script_chan @@ -309,7 +310,7 @@ fn find_child( for child in children { let msg = child.encode(registry, true, script_chan.clone(), pipeline, name.into()); - if msg.display_name == selector { + if compare_fn(&msg) { hierarchy.push(msg); return Ok(hierarchy); }; @@ -323,9 +324,9 @@ fn find_child( pipeline, name, registry, - selector, &msg.actor, hierarchy, + compare_fn.clone(), ) { Ok(mut hierarchy) => { hierarchy.push(msg); diff --git a/components/devtools/actors/watcher.rs b/components/devtools/actors/watcher.rs index 55358067b5b..c74a82d421e 100644 --- a/components/devtools/actors/watcher.rs +++ b/components/devtools/actors/watcher.rs @@ -59,7 +59,7 @@ impl SessionContext { // working propperly supported_resources: HashMap::from([ ("console-message", true), - ("css-change", false), + ("css-change", true), ("css-message", false), ("css-registered-properties", false), ("document-event", false), diff --git a/components/script/devtools.rs b/components/script/devtools.rs index 7b031da2886..dacb98683c9 100644 --- a/components/script/devtools.rs +++ b/components/script/devtools.rs @@ -2,34 +2,42 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +use std::collections::HashMap; use std::rc::Rc; use std::str; use base::id::PipelineId; use devtools_traits::{ - AutoMargins, ComputedNodeLayout, EvaluateJSReply, Modification, NodeInfo, TimelineMarker, - TimelineMarkerType, + AttrModification, AutoMargins, ComputedNodeLayout, CssDatabaseProperty, EvaluateJSReply, + NodeInfo, NodeStyle, RuleModification, TimelineMarker, TimelineMarkerType, }; use ipc_channel::ipc::IpcSender; use js::jsval::UndefinedValue; use js::rust::ToString; use uuid::Uuid; +use crate::dom::bindings::codegen::Bindings::CSSRuleListBinding::CSSRuleListMethods; use crate::dom::bindings::codegen::Bindings::CSSStyleDeclarationBinding::CSSStyleDeclarationMethods; +use crate::dom::bindings::codegen::Bindings::CSSStyleRuleBinding::CSSStyleRuleMethods; +use crate::dom::bindings::codegen::Bindings::CSSStyleSheetBinding::CSSStyleSheetMethods; use crate::dom::bindings::codegen::Bindings::DOMRectBinding::DOMRectMethods; use crate::dom::bindings::codegen::Bindings::DocumentBinding::DocumentMethods; use crate::dom::bindings::codegen::Bindings::ElementBinding::ElementMethods; +use crate::dom::bindings::codegen::Bindings::HTMLElementBinding::HTMLElementMethods; use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeConstants; use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods; use crate::dom::bindings::conversions::{jsstring_to_str, ConversionResult, FromJSValConvertible}; use crate::dom::bindings::inheritance::Castable; use crate::dom::bindings::root::DomRoot; use crate::dom::bindings::str::DOMString; +use crate::dom::cssstyledeclaration::ENABLED_LONGHAND_PROPERTIES; +use crate::dom::cssstylerule::CSSStyleRule; use crate::dom::document::AnimationFrameCallback; use crate::dom::element::Element; use crate::dom::globalscope::GlobalScope; use crate::dom::htmlscriptelement::SourceCode; -use crate::dom::node::{window_from_node, Node, ShadowIncluding}; +use crate::dom::node::{stylesheets_owner_from_node, window_from_node, Node, ShadowIncluding}; +use crate::dom::types::HTMLElement; use crate::realms::enter_realm; use crate::script_module::ScriptFetchOptions; use crate::script_thread::Documents; @@ -169,6 +177,151 @@ pub fn handle_get_children( }; } +pub fn handle_get_attribute_style( + documents: &Documents, + pipeline: PipelineId, + node_id: String, + reply: IpcSender<Option<Vec<NodeStyle>>>, +) { + let node = match find_node_by_unique_id(documents, pipeline, &node_id) { + None => return reply.send(None).unwrap(), + Some(found_node) => found_node, + }; + + let elem = node + .downcast::<HTMLElement>() + .expect("This should be an HTMLElement"); + let style = elem.Style(); + + let msg = (0..style.Length()) + .map(|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(); + + reply.send(Some(msg)).unwrap(); +} + +#[allow(crown::unrooted_must_root)] +pub fn handle_get_stylesheet_style( + documents: &Documents, + pipeline: PipelineId, + node_id: String, + selector: String, + stylesheet: usize, + reply: IpcSender<Option<Vec<NodeStyle>>>, +) { + let msg = (|| { + let node = find_node_by_unique_id(documents, pipeline, &node_id)?; + + let document = documents.find_document(pipeline)?; + let _realm = enter_realm(document.window()); + let owner = stylesheets_owner_from_node(&*node); + + let stylesheet = owner.stylesheet_at(stylesheet)?; + let list = stylesheet.GetCssRules().ok()?; + + let styles = (0..list.Length()) + .filter_map(move |i| { + let rule = list.Item(i)?; + let style = rule.downcast::<CSSStyleRule>()?; + if *selector != *style.SelectorText() { + return None; + }; + Some(style.Style()) + }) + .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(), + } + }) + }) + .flatten() + .collect(); + + Some(styles) + })(); + + reply.send(msg).unwrap(); +} + +#[allow(crown::unrooted_must_root)] +pub fn handle_get_selectors( + documents: &Documents, + pipeline: PipelineId, + node_id: String, + reply: IpcSender<Option<Vec<(String, usize)>>>, +) { + let msg = (|| { + let node = find_node_by_unique_id(documents, pipeline, &node_id)?; + + let document = documents.find_document(pipeline)?; + let _realm = enter_realm(document.window()); + let owner = stylesheets_owner_from_node(&*node); + + let rules = (0..owner.stylesheet_count()) + .filter_map(|i| { + let stylesheet = owner.stylesheet_at(i)?; + let list = stylesheet.GetCssRules().ok()?; + let elem = node.downcast::<Element>()?; + + Some((0..list.Length()).filter_map(move |j| { + let rule = list.Item(j)?; + let style = rule.downcast::<CSSStyleRule>()?; + let selector = style.SelectorText(); + let _ = elem.Matches(selector.clone()).ok()?.then_some(())?; + Some((selector.into(), i)) + })) + }) + .flatten() + .collect(); + + Some(rules) + })(); + + reply.send(msg).unwrap(); +} + +pub fn handle_get_computed_style( + documents: &Documents, + pipeline: PipelineId, + node_id: String, + reply: IpcSender<Option<Vec<NodeStyle>>>, +) { + let node = match find_node_by_unique_id(documents, pipeline, &node_id) { + None => return reply.send(None).unwrap(), + Some(found_node) => found_node, + }; + + let window = window_from_node(&*node); + let elem = node + .downcast::<Element>() + .expect("This should be an element"); + let computed_style = window.GetComputedStyle(elem, None); + + let msg = (0..computed_style.Length()) + .map(|i| { + let name = computed_style.Item(i); + NodeStyle { + name: name.to_string(), + value: computed_style.GetPropertyValue(name.clone()).to_string(), + priority: computed_style.GetPropertyPriority(name).to_string(), + } + }) + .collect(); + + reply.send(Some(msg)).unwrap(); +} + pub fn handle_get_layout( documents: &Documents, pipeline: PipelineId, @@ -233,7 +386,7 @@ pub fn handle_modify_attribute( documents: &Documents, pipeline: PipelineId, node_id: String, - modifications: Vec<Modification>, + modifications: Vec<AttrModification>, ) { let Some(document) = documents.find_document(pipeline) else { return warn!("document for pipeline id {} is not found", &pipeline); @@ -267,6 +420,38 @@ pub fn handle_modify_attribute( } } +pub fn handle_modify_rule( + documents: &Documents, + pipeline: PipelineId, + node_id: String, + modifications: Vec<RuleModification>, +) { + let Some(document) = documents.find_document(pipeline) else { + return warn!("Document for pipeline id {} is not found", &pipeline); + }; + let _realm = enter_realm(document.window()); + + let Some(node) = find_node_by_unique_id(documents, pipeline, &node_id) else { + return warn!( + "Node id {} for pipeline id {} is not found", + &node_id, &pipeline + ); + }; + + let elem = node + .downcast::<HTMLElement>() + .expect("This should be an HTMLElement"); + let style = elem.Style(); + + for modification in modifications { + let _ = style.SetProperty( + modification.name.into(), + modification.value.into(), + modification.priority.into(), + ); + } +} + pub fn handle_wants_live_notifications(global: &GlobalScope, send_notifications: bool) { global.set_devtools_wants_updates(send_notifications); } @@ -304,3 +489,21 @@ pub fn handle_reload(documents: &Documents, id: PipelineId) { win.Location().reload_without_origin_check(); } } + +pub fn handle_get_css_database(reply: IpcSender<HashMap<String, CssDatabaseProperty>>) { + let database: HashMap<_, _> = ENABLED_LONGHAND_PROPERTIES + .iter() + .map(|l| { + ( + l.name().into(), + CssDatabaseProperty { + is_inherited: l.inherited(), + values: vec![], // TODO: Get allowed values for each property + supports: vec![], + subproperties: vec![l.name().into()], + }, + ) + }) + .collect(); + let _ = reply.send(database); +} diff --git a/components/script/dom/cssstyledeclaration.rs b/components/script/dom/cssstyledeclaration.rs index 5777e0fe74b..b6efaa7f6c3 100644 --- a/components/script/dom/cssstyledeclaration.rs +++ b/components/script/dom/cssstyledeclaration.rs @@ -344,7 +344,7 @@ impl CSSStyleDeclaration { } } -static ENABLED_LONGHAND_PROPERTIES: LazyLock<Vec<LonghandId>> = LazyLock::new(|| { +pub static ENABLED_LONGHAND_PROPERTIES: LazyLock<Vec<LonghandId>> = LazyLock::new(|| { // The 'all' shorthand contains all the enabled longhands with 2 exceptions: // 'direction' and 'unicode-bidi', so these must be added afterward. let mut enabled_longhands: Vec<LonghandId> = ShorthandId::All.longhands().collect(); diff --git a/components/script/script_thread.rs b/components/script/script_thread.rs index 5940ccf7abb..1f1687f422a 100644 --- a/components/script/script_thread.rs +++ b/components/script/script_thread.rs @@ -2531,12 +2531,33 @@ impl ScriptThread { DevtoolScriptControlMsg::GetChildren(id, node_id, reply) => { devtools::handle_get_children(&documents, id, node_id, reply) }, + DevtoolScriptControlMsg::GetAttributeStyle(id, node_id, reply) => { + devtools::handle_get_attribute_style(&documents, id, node_id, reply) + }, + DevtoolScriptControlMsg::GetStylesheetStyle( + id, + node_id, + selector, + stylesheet, + reply, + ) => devtools::handle_get_stylesheet_style( + &documents, id, node_id, selector, stylesheet, reply, + ), + DevtoolScriptControlMsg::GetSelectors(id, node_id, reply) => { + devtools::handle_get_selectors(&documents, id, node_id, reply) + }, + DevtoolScriptControlMsg::GetComputedStyle(id, node_id, reply) => { + devtools::handle_get_computed_style(&documents, id, node_id, reply) + }, DevtoolScriptControlMsg::GetLayout(id, node_id, reply) => { devtools::handle_get_layout(&documents, id, node_id, reply) }, DevtoolScriptControlMsg::ModifyAttribute(id, node_id, modifications) => { devtools::handle_modify_attribute(&documents, id, node_id, modifications) }, + DevtoolScriptControlMsg::ModifyRule(id, node_id, modifications) => { + devtools::handle_modify_rule(&documents, id, node_id, modifications) + }, DevtoolScriptControlMsg::WantsLiveNotifications(id, to_send) => match documents .find_window(id) { @@ -2553,6 +2574,9 @@ impl ScriptThread { devtools::handle_request_animation_frame(&documents, id, name) }, DevtoolScriptControlMsg::Reload(id) => devtools::handle_reload(&documents, id), + DevtoolScriptControlMsg::GetCssDatabase(reply) => { + devtools::handle_get_css_database(reply) + }, } } diff --git a/components/shared/devtools/lib.rs b/components/shared/devtools/lib.rs index bf6ebed2466..a7dfc0a0262 100644 --- a/components/shared/devtools/lib.rs +++ b/components/shared/devtools/lib.rs @@ -10,6 +10,7 @@ #![crate_type = "rlib"] #![deny(unsafe_code)] +use std::collections::HashMap; use std::net::TcpStream; use std::time::{Duration, SystemTime}; @@ -152,6 +153,14 @@ pub enum TimelineMarkerType { DOMEvent, } +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct NodeStyle { + pub name: String, + pub value: String, + pub priority: String, +} + /// The properties of a DOM node as computed by layout. #[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] @@ -201,10 +210,27 @@ pub enum DevtoolScriptControlMsg { GetDocumentElement(PipelineId, IpcSender<Option<NodeInfo>>), /// Retrieve the details of the child nodes of the given node in the given pipeline. GetChildren(PipelineId, String, IpcSender<Option<Vec<NodeInfo>>>), + /// Retrieve the CSS style properties defined in the attribute tag for the given node. + GetAttributeStyle(PipelineId, String, IpcSender<Option<Vec<NodeStyle>>>), + /// Retrieve the CSS style properties defined in an stylesheet for the given selector. + GetStylesheetStyle( + PipelineId, + String, + String, + usize, + IpcSender<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, IpcSender<Option<Vec<(String, usize)>>>), + /// Retrieve the computed CSS style properties for the given node. + GetComputedStyle(PipelineId, String, IpcSender<Option<Vec<NodeStyle>>>), /// Retrieve the computed layout properties of the given node in the given pipeline. GetLayout(PipelineId, String, IpcSender<Option<ComputedNodeLayout>>), /// Update a given node's attributes with a list of modifications. - ModifyAttribute(PipelineId, String, Vec<Modification>), + ModifyAttribute(PipelineId, String, Vec<AttrModification>), + /// Update a given node's style rules with a list of modifications. + ModifyRule(PipelineId, String, Vec<RuleModification>), /// Request live console messages for a given pipeline (true if desired, false otherwise). WantsLiveNotifications(PipelineId, bool), /// Request live notifications for a given set of timeline events for a given pipeline. @@ -220,16 +246,29 @@ pub enum DevtoolScriptControlMsg { RequestAnimationFrame(PipelineId, String), /// Direct the given pipeline to reload the current page. Reload(PipelineId), + /// Gets the list of all allowed CSS rules and possible values. + GetCssDatabase(IpcSender<HashMap<String, CssDatabaseProperty>>), } #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] -pub struct Modification { +pub struct AttrModification { pub attribute_name: String, pub new_value: Option<String>, } #[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RuleModification { + #[serde(rename = "type")] + pub type_: String, + pub index: u32, + pub name: String, + pub value: String, + pub priority: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] pub enum LogLevel { Log, Debug, @@ -364,3 +403,12 @@ impl PreciseTime { #[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, MallocSizeOf, PartialEq, Serialize)] pub struct WorkerId(pub Uuid); + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CssDatabaseProperty { + pub is_inherited: bool, + pub values: Vec<String>, + pub supports: Vec<String>, + pub subproperties: Vec<String>, +} |