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::collections::hash_map::Entry;
10use std::iter::once;
11
12use base::id::PipelineId;
13use devtools_traits::DevtoolScriptControlMsg::{GetLayout, GetSelectors};
14use devtools_traits::{ComputedNodeLayout, DevtoolScriptControlMsg};
15use ipc_channel::ipc::{self, IpcSender};
16use serde::Serialize;
17use serde_json::{self, Map, Value};
18
19use crate::StreamId;
20use crate::actor::{Actor, ActorError, ActorRegistry};
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)]
49#[serde(rename_all = "kebab-case")]
50struct GetLayoutReply {
51    from: String,
52
53    display: String,
54    position: String,
55    z_index: String,
56    box_sizing: String,
57
58    // Would be nice to use a proper struct, blocked by
59    // https://github.com/serde-rs/serde/issues/43
60    auto_margins: serde_json::value::Value,
61    margin_top: String,
62    margin_right: String,
63    margin_bottom: String,
64    margin_left: String,
65
66    border_top_width: String,
67    border_right_width: String,
68    border_bottom_width: String,
69    border_left_width: String,
70
71    padding_top: String,
72    padding_right: String,
73    padding_bottom: String,
74    padding_left: String,
75
76    width: f32,
77    height: f32,
78}
79
80#[derive(Serialize)]
81pub struct IsPositionEditableReply {
82    pub from: String,
83    pub value: bool,
84}
85
86#[derive(Serialize)]
87pub struct PageStyleMsg {
88    pub actor: String,
89    pub traits: HashMap<String, bool>,
90}
91
92pub struct PageStyleActor {
93    pub name: String,
94    pub script_chan: IpcSender<DevtoolScriptControlMsg>,
95    pub pipeline: PipelineId,
96}
97
98impl Actor for PageStyleActor {
99    fn name(&self) -> String {
100        self.name.clone()
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    fn get_applied(
134        &self,
135        request: ClientRequest,
136        msg: &Map<String, Value>,
137        registry: &ActorRegistry,
138    ) -> Result<(), ActorError> {
139        let target = msg
140            .get("node")
141            .ok_or(ActorError::MissingParameter)?
142            .as_str()
143            .ok_or(ActorError::BadParameterType)?;
144        let node = registry.find::<NodeActor>(target);
145        let walker = registry.find::<WalkerActor>(&node.walker);
146        let entries: Vec<_> = find_child(
147            &node.script_chan,
148            node.pipeline,
149            target,
150            registry,
151            &walker.root_node.actor,
152            vec![],
153            |msg| msg.actor == target,
154        )
155        .unwrap_or_default()
156        .into_iter()
157        .flat_map(|node| {
158            let inherited = (node.actor != target).then(|| node.actor.clone());
159            let node_actor = registry.find::<NodeActor>(&node.actor);
160
161            // Get the css selectors that match this node present in the currently active stylesheets.
162            let selectors = (|| {
163                let (selectors_sender, selector_receiver) = ipc::channel().ok()?;
164                walker
165                    .script_chan
166                    .send(GetSelectors(
167                        walker.pipeline,
168                        registry.actor_to_script(node.actor.clone()),
169                        selectors_sender,
170                    ))
171                    .ok()?;
172                selector_receiver.recv().ok()?
173            })()
174            .unwrap_or_default();
175
176            // For each selector (plus an empty one that represents the style attribute)
177            // get all of the rules associated with it.
178
179            once(("".into(), usize::MAX))
180                .chain(selectors)
181                .filter_map(move |selector| {
182                    let rule = match node_actor.style_rules.borrow_mut().entry(selector) {
183                        Entry::Vacant(e) => {
184                            let name = registry.new_name("style-rule");
185                            let actor = StyleRuleActor::new(
186                                name.clone(),
187                                node_actor.name(),
188                                (!e.key().0.is_empty()).then_some(e.key().clone()),
189                            );
190                            let rule = actor.applied(registry)?;
191
192                            registry.register_later(Box::new(actor));
193                            e.insert(name);
194                            rule
195                        },
196                        Entry::Occupied(e) => {
197                            let actor = registry.find::<StyleRuleActor>(e.get());
198                            actor.applied(registry)?
199                        },
200                    };
201                    if inherited.is_some() && rule.declarations.is_empty() {
202                        return None;
203                    }
204
205                    Some(AppliedEntry {
206                        rule,
207                        // TODO: Handle pseudo elements
208                        pseudo_element: None,
209                        is_system: false,
210                        inherited: inherited.clone(),
211                    })
212                })
213        })
214        .collect();
215        let msg = GetAppliedReply {
216            entries,
217            from: self.name(),
218        };
219        request.reply_final(&msg)
220    }
221
222    fn get_computed(
223        &self,
224        request: ClientRequest,
225        msg: &Map<String, Value>,
226        registry: &ActorRegistry,
227    ) -> Result<(), ActorError> {
228        let target = msg
229            .get("node")
230            .ok_or(ActorError::MissingParameter)?
231            .as_str()
232            .ok_or(ActorError::BadParameterType)?;
233        let node_actor = registry.find::<NodeActor>(target);
234        let computed = (|| match node_actor
235            .style_rules
236            .borrow_mut()
237            .entry(("".into(), usize::MAX))
238        {
239            Entry::Vacant(e) => {
240                let name = registry.new_name("style-rule");
241                let actor = StyleRuleActor::new(name.clone(), target.into(), None);
242                let computed = actor.computed(registry)?;
243                registry.register_later(Box::new(actor));
244                e.insert(name);
245                Some(computed)
246            },
247            Entry::Occupied(e) => {
248                let actor = registry.find::<StyleRuleActor>(e.get());
249                Some(actor.computed(registry)?)
250            },
251        })()
252        .unwrap_or_default();
253        let msg = GetComputedReply {
254            computed,
255            from: self.name(),
256        };
257        request.reply_final(&msg)
258    }
259
260    fn get_layout(
261        &self,
262        request: ClientRequest,
263        msg: &Map<String, Value>,
264        registry: &ActorRegistry,
265    ) -> Result<(), ActorError> {
266        let target = msg
267            .get("node")
268            .ok_or(ActorError::MissingParameter)?
269            .as_str()
270            .ok_or(ActorError::BadParameterType)?;
271        let (computed_node_sender, computed_node_receiver) =
272            ipc::channel().map_err(|_| ActorError::Internal)?;
273        self.script_chan
274            .send(GetLayout(
275                self.pipeline,
276                registry.actor_to_script(target.to_owned()),
277                computed_node_sender,
278            ))
279            .unwrap();
280        let ComputedNodeLayout {
281            display,
282            position,
283            z_index,
284            box_sizing,
285            auto_margins,
286            margin_top,
287            margin_right,
288            margin_bottom,
289            margin_left,
290            border_top_width,
291            border_right_width,
292            border_bottom_width,
293            border_left_width,
294            padding_top,
295            padding_right,
296            padding_bottom,
297            padding_left,
298            width,
299            height,
300        } = computed_node_receiver
301            .recv()
302            .map_err(|_| ActorError::Internal)?
303            .ok_or(ActorError::Internal)?;
304        let msg_auto_margins = msg
305            .get("autoMargins")
306            .and_then(Value::as_bool)
307            .unwrap_or(false);
308        let msg = GetLayoutReply {
309            from: self.name(),
310            display,
311            position,
312            z_index,
313            box_sizing,
314            auto_margins: if msg_auto_margins {
315                let mut m = Map::new();
316                let auto = serde_json::value::Value::String("auto".to_owned());
317                if auto_margins.top {
318                    m.insert("top".to_owned(), auto.clone());
319                }
320                if auto_margins.right {
321                    m.insert("right".to_owned(), auto.clone());
322                }
323                if auto_margins.bottom {
324                    m.insert("bottom".to_owned(), auto.clone());
325                }
326                if auto_margins.left {
327                    m.insert("left".to_owned(), auto);
328                }
329                serde_json::value::Value::Object(m)
330            } else {
331                serde_json::value::Value::Null
332            },
333            margin_top,
334            margin_right,
335            margin_bottom,
336            margin_left,
337            border_top_width,
338            border_right_width,
339            border_bottom_width,
340            border_left_width,
341            padding_top,
342            padding_right,
343            padding_bottom,
344            padding_left,
345            width,
346            height,
347        };
348        let msg = serde_json::to_string(&msg).map_err(|_| ActorError::Internal)?;
349        let msg = serde_json::from_str::<Value>(&msg).map_err(|_| ActorError::Internal)?;
350        request.reply_final(&msg)
351    }
352
353    fn is_position_editable(&self, request: ClientRequest) -> Result<(), ActorError> {
354        let msg = IsPositionEditableReply {
355            from: self.name(),
356            value: false,
357        };
358        request.reply_final(&msg)
359    }
360}