Skip to main content

devtools/actors/inspector/
page_style.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5//! The page style actor is responsible of informing the DevTools client of the different style
6//! properties applied, including the attributes and layout of each element.
7
8use std::collections::HashMap;
9use std::iter::once;
10use std::sync::Arc;
11
12use devtools_traits::DevtoolScriptControlMsg::{GetLayout, GetSelectors};
13use devtools_traits::{AutoMargins, ComputedNodeLayout, MatchedRule};
14use malloc_size_of_derive::MallocSizeOf;
15use serde::Serialize;
16use serde_json::{self, Map, Value};
17use servo_base::generic_channel::{self};
18
19use crate::StreamId;
20use crate::actor::{Actor, ActorEncode, ActorError, ActorRegistry, new_actor_name};
21use crate::actors::inspector::node::NodeActor;
22use crate::actors::inspector::style_rule::{AppliedRule, ComputedDeclaration, StyleRuleActor};
23use crate::actors::inspector::walker::{WalkerActor, find_child};
24use crate::protocol::ClientRequest;
25
26#[derive(Serialize)]
27struct GetAppliedReply {
28    entries: Vec<AppliedEntry>,
29    from: String,
30}
31
32#[derive(Serialize)]
33struct GetComputedReply {
34    computed: HashMap<String, ComputedDeclaration>,
35    from: String,
36}
37
38#[derive(Serialize)]
39#[serde(rename_all = "camelCase")]
40struct AppliedEntry {
41    rule: AppliedRule,
42    pseudo_element: Option<()>,
43    is_system: bool,
44    #[serde(skip_serializing_if = "Option::is_none")]
45    inherited: Option<String>,
46}
47
48#[derive(Serialize)]
49struct DevtoolsAutoMargins {
50    #[serde(skip_serializing_if = "Option::is_none")]
51    top: Option<String>,
52    #[serde(skip_serializing_if = "Option::is_none")]
53    right: Option<String>,
54    #[serde(skip_serializing_if = "Option::is_none")]
55    bottom: Option<String>,
56    #[serde(skip_serializing_if = "Option::is_none")]
57    left: Option<String>,
58}
59
60impl From<AutoMargins> for DevtoolsAutoMargins {
61    fn from(auto_margins: AutoMargins) -> Self {
62        const AUTO: &str = "auto";
63        Self {
64            top: auto_margins.top.then_some(AUTO.into()),
65            right: auto_margins.right.then_some(AUTO.into()),
66            bottom: auto_margins.bottom.then_some(AUTO.into()),
67            left: auto_margins.left.then_some(AUTO.into()),
68        }
69    }
70}
71
72#[derive(Serialize)]
73struct GetLayoutReply {
74    from: String,
75    #[serde(flatten)]
76    layout: ComputedNodeLayout,
77    #[serde(rename = "autoMargins")]
78    auto_margins: DevtoolsAutoMargins,
79}
80
81#[derive(Serialize)]
82pub(crate) struct IsPositionEditableReply {
83    from: String,
84    value: bool,
85}
86
87#[derive(Serialize)]
88pub(crate) struct PageStyleMsg {
89    pub actor: String,
90    pub traits: HashMap<String, bool>,
91}
92
93#[derive(MallocSizeOf)]
94pub(crate) struct PageStyleActor {
95    pub name: String,
96}
97
98impl Actor for PageStyleActor {
99    fn name(&self) -> &str {
100        &self.name
101    }
102
103    /// The page style actor can handle the following messages:
104    ///
105    /// - `getApplied`: Returns the applied styles for a node, they represent the explicit css
106    ///   rules set for them, both in the style attribute and in stylesheets.
107    ///
108    /// - `getComputed`: Returns the computed styles for a node, these include all of the supported
109    ///   css properties calculated values.
110    ///
111    /// - `getLayout`: Returns the box layout properties for a node.
112    ///
113    /// - `isPositionEditable`: Informs whether you can change a style property in the inspector.
114    fn handle_message(
115        &self,
116        request: ClientRequest,
117        registry: &ActorRegistry,
118        msg_type: &str,
119        msg: &Map<String, Value>,
120        _id: StreamId,
121    ) -> Result<(), ActorError> {
122        match msg_type {
123            "getApplied" => self.get_applied(request, msg, registry),
124            "getComputed" => self.get_computed(request, msg, registry),
125            "getLayout" => self.get_layout(request, msg, registry),
126            "isPositionEditable" => self.is_position_editable(request),
127            _ => Err(ActorError::UnrecognizedPacketType),
128        }
129    }
130}
131
132impl PageStyleActor {
133    pub fn register(registry: &ActorRegistry) -> Arc<Self> {
134        let name = new_actor_name::<Self>();
135        let actor = Self { name };
136        registry.register::<Self>(actor)
137    }
138
139    fn get_applied(
140        &self,
141        request: ClientRequest,
142        msg: &Map<String, Value>,
143        registry: &ActorRegistry,
144    ) -> Result<(), ActorError> {
145        let node_name = msg
146            .get("node")
147            .ok_or(ActorError::MissingParameter)?
148            .as_str()
149            .ok_or(ActorError::BadParameterType)?;
150        let node_actor = registry.find::<NodeActor>(node_name);
151        let walker = registry.find::<WalkerActor>(&node_actor.walker_name);
152        let browsing_context_actor = walker.browsing_context_actor(registry);
153        let entries: Vec<_> = find_child(
154            &node_actor.walker_name,
155            registry,
156            &walker.root(registry)?.actor,
157            vec![],
158            |msg| msg.actor == node_name,
159        )
160        .unwrap_or_default()
161        .into_iter()
162        .flat_map(|node| {
163            let inherited = (node.actor != node_name).then(|| node.actor.clone());
164            let node_actor = registry.find::<NodeActor>(&node.actor);
165
166            // Get the css selectors that match this node present in the currently active stylesheets.
167            let selectors = (|| {
168                let (selectors_sender, selector_receiver) = generic_channel::channel()?;
169                browsing_context_actor
170                    .script_chan()
171                    .send(GetSelectors(
172                        browsing_context_actor.pipeline_id(),
173                        registry.actor_to_script(node.actor.clone()),
174                        selectors_sender,
175                    ))
176                    .ok()?;
177                selector_receiver.recv().ok()?
178            })()
179            .unwrap_or_default();
180
181            // For each selector (plus an empty one that represents the style attribute)
182            // get all of the rules associated with it.
183
184            let style_attribute_rule = MatchedRule {
185                selector: String::new(),
186                stylesheet_index: usize::MAX,
187                block_id: 0,
188                ancestor_data: vec![],
189            };
190
191            once(style_attribute_rule)
192                .chain(selectors)
193                .filter_map(move |matched_rule| {
194                    let style_rule_name = node_actor
195                        .style_rules
196                        .borrow_mut()
197                        .entry(matched_rule.clone())
198                        .or_insert_with(|| {
199                            StyleRuleActor::register(
200                                registry,
201                                node_actor.name().into(),
202                                (matched_rule.stylesheet_index != usize::MAX)
203                                    .then_some(matched_rule.clone()),
204                            )
205                            .name()
206                            .into()
207                        })
208                        .clone();
209
210                    let rule = registry
211                        .find::<StyleRuleActor>(&style_rule_name)
212                        .applied(registry)?;
213                    if inherited.is_some() && rule.declarations.is_empty() {
214                        return None;
215                    }
216
217                    Some(AppliedEntry {
218                        rule,
219                        // TODO: Handle pseudo elements
220                        pseudo_element: None,
221                        is_system: false,
222                        inherited: inherited.clone(),
223                    })
224                })
225        })
226        .collect();
227        let msg = GetAppliedReply {
228            entries,
229            from: self.name().into(),
230        };
231        request.reply_final(&msg)
232    }
233
234    fn get_computed(
235        &self,
236        request: ClientRequest,
237        msg: &Map<String, Value>,
238        registry: &ActorRegistry,
239    ) -> Result<(), ActorError> {
240        let node_name = msg
241            .get("node")
242            .ok_or(ActorError::MissingParameter)?
243            .as_str()
244            .ok_or(ActorError::BadParameterType)?;
245        let node_actor = registry.find::<NodeActor>(node_name);
246        let style_attribute_rule = devtools_traits::MatchedRule {
247            selector: String::new(),
248            stylesheet_index: usize::MAX,
249            block_id: 0,
250            ancestor_data: vec![],
251        };
252
253        let style_rule_name = node_actor
254            .style_rules
255            .borrow_mut()
256            .entry(style_attribute_rule)
257            .or_insert_with(|| {
258                StyleRuleActor::register(registry, node_name.into(), None)
259                    .name()
260                    .into()
261            })
262            .clone();
263        let computed = registry
264            .find::<StyleRuleActor>(&style_rule_name)
265            .computed(registry)
266            .unwrap_or_default();
267
268        let msg = GetComputedReply {
269            computed,
270            from: self.name().into(),
271        };
272        request.reply_final(&msg)
273    }
274
275    fn get_layout(
276        &self,
277        request: ClientRequest,
278        msg: &Map<String, Value>,
279        registry: &ActorRegistry,
280    ) -> Result<(), ActorError> {
281        let node_name = msg
282            .get("node")
283            .ok_or(ActorError::MissingParameter)?
284            .as_str()
285            .ok_or(ActorError::BadParameterType)?;
286        let node_actor = registry.find::<NodeActor>(node_name);
287        let walker = registry.find::<WalkerActor>(&node_actor.walker_name);
288        let browsing_context_actor = walker.browsing_context_actor(registry);
289        let (tx, rx) = generic_channel::channel().ok_or(ActorError::Internal)?;
290        browsing_context_actor
291            .script_chan()
292            .send(GetLayout(
293                browsing_context_actor.pipeline_id(),
294                registry.actor_to_script(node_name.to_owned()),
295                tx,
296            ))
297            .map_err(|_| ActorError::Internal)?;
298        let (layout, auto_margins) = rx
299            .recv()
300            .map_err(|_| ActorError::Internal)?
301            .ok_or(ActorError::Internal)?;
302        request.reply_final(&GetLayoutReply {
303            from: self.name().into(),
304            layout,
305            auto_margins: auto_margins.into(),
306        })
307    }
308
309    fn is_position_editable(&self, request: ClientRequest) -> Result<(), ActorError> {
310        let msg = IsPositionEditableReply {
311            from: self.name().into(),
312            value: false,
313        };
314        request.reply_final(&msg)
315    }
316}
317
318impl ActorEncode<PageStyleMsg> for PageStyleActor {
319    fn encode(&self, _: &ActorRegistry) -> PageStyleMsg {
320        PageStyleMsg {
321            actor: self.name().into(),
322            traits: HashMap::from([
323                ("fontStretchLevel4".into(), true),
324                ("fontStyleLevel4".into(), true),
325                ("fontVariations".into(), true),
326                ("fontWeightLevel4".into(), true),
327            ]),
328        }
329    }
330}