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 devtools_traits::DevtoolScriptControlMsg::{GetLayout, GetSelectors};
13use devtools_traits::{AutoMargins, ComputedNodeLayout};
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};
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) -> 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_actor = registry.find::<NodeActor>(target);
145        let walker = registry.find::<WalkerActor>(&node_actor.walker);
146        let browsing_context_actor = walker.browsing_context_actor(registry);
147        let entries: Vec<_> = find_child(
148            &node_actor.script_chan,
149            node_actor.pipeline,
150            target,
151            registry,
152            &walker.root(registry)?.actor,
153            vec![],
154            |msg| msg.actor == target,
155        )
156        .unwrap_or_default()
157        .into_iter()
158        .flat_map(|node| {
159            let inherited = (node.actor != target).then(|| node.actor.clone());
160            let node_actor = registry.find::<NodeActor>(&node.actor);
161
162            // Get the css selectors that match this node present in the currently active stylesheets.
163            let selectors = (|| {
164                let (selectors_sender, selector_receiver) = generic_channel::channel()?;
165                browsing_context_actor
166                    .script_chan()
167                    .send(GetSelectors(
168                        browsing_context_actor.pipeline_id(),
169                        registry.actor_to_script(node.actor.clone()),
170                        selectors_sender,
171                    ))
172                    .ok()?;
173                selector_receiver.recv().ok()?
174            })()
175            .unwrap_or_default();
176
177            // For each selector (plus an empty one that represents the style attribute)
178            // get all of the rules associated with it.
179
180            once(("".into(), usize::MAX))
181                .chain(selectors)
182                .filter_map(move |selector| {
183                    let rule = match node_actor.style_rules.borrow_mut().entry(selector) {
184                        Entry::Vacant(e) => {
185                            let name = registry.new_name::<StyleRuleActor>();
186                            let actor = StyleRuleActor::new(
187                                name.clone(),
188                                node_actor.name(),
189                                (!e.key().0.is_empty()).then_some(e.key().clone()),
190                            );
191                            let rule = actor.applied(registry)?;
192
193                            registry.register(actor);
194                            e.insert(name);
195                            rule
196                        },
197                        Entry::Occupied(e) => {
198                            let actor = registry.find::<StyleRuleActor>(e.get());
199                            actor.applied(registry)?
200                        },
201                    };
202                    if inherited.is_some() && rule.declarations.is_empty() {
203                        return None;
204                    }
205
206                    Some(AppliedEntry {
207                        rule,
208                        // TODO: Handle pseudo elements
209                        pseudo_element: None,
210                        is_system: false,
211                        inherited: inherited.clone(),
212                    })
213                })
214        })
215        .collect();
216        let msg = GetAppliedReply {
217            entries,
218            from: self.name(),
219        };
220        request.reply_final(&msg)
221    }
222
223    fn get_computed(
224        &self,
225        request: ClientRequest,
226        msg: &Map<String, Value>,
227        registry: &ActorRegistry,
228    ) -> Result<(), ActorError> {
229        let target = msg
230            .get("node")
231            .ok_or(ActorError::MissingParameter)?
232            .as_str()
233            .ok_or(ActorError::BadParameterType)?;
234        let node_actor = registry.find::<NodeActor>(target);
235        let computed = (|| match node_actor
236            .style_rules
237            .borrow_mut()
238            .entry(("".into(), usize::MAX))
239        {
240            Entry::Vacant(e) => {
241                let name = registry.new_name::<StyleRuleActor>();
242                let actor = StyleRuleActor::new(name.clone(), target.into(), None);
243                let computed = actor.computed(registry)?;
244                registry.register(actor);
245                e.insert(name);
246                Some(computed)
247            },
248            Entry::Occupied(e) => {
249                let actor = registry.find::<StyleRuleActor>(e.get());
250                Some(actor.computed(registry)?)
251            },
252        })()
253        .unwrap_or_default();
254        let msg = GetComputedReply {
255            computed,
256            from: self.name(),
257        };
258        request.reply_final(&msg)
259    }
260
261    fn get_layout(
262        &self,
263        request: ClientRequest,
264        msg: &Map<String, Value>,
265        registry: &ActorRegistry,
266    ) -> Result<(), ActorError> {
267        let target = msg
268            .get("node")
269            .ok_or(ActorError::MissingParameter)?
270            .as_str()
271            .ok_or(ActorError::BadParameterType)?;
272        let node_actor = registry.find::<NodeActor>(target);
273        let walker = registry.find::<WalkerActor>(&node_actor.walker);
274        let browsing_context_actor = walker.browsing_context_actor(registry);
275        let (tx, rx) = generic_channel::channel().ok_or(ActorError::Internal)?;
276        browsing_context_actor
277            .script_chan()
278            .send(GetLayout(
279                browsing_context_actor.pipeline_id(),
280                registry.actor_to_script(target.to_owned()),
281                tx,
282            ))
283            .map_err(|_| ActorError::Internal)?;
284        let (layout, auto_margins) = rx
285            .recv()
286            .map_err(|_| ActorError::Internal)?
287            .ok_or(ActorError::Internal)?;
288        request.reply_final(&GetLayoutReply {
289            from: self.name(),
290            layout,
291            auto_margins: auto_margins.into(),
292        })
293    }
294
295    fn is_position_editable(&self, request: ClientRequest) -> Result<(), ActorError> {
296        let msg = IsPositionEditableReply {
297            from: self.name(),
298            value: false,
299        };
300        request.reply_final(&msg)
301    }
302}
303
304impl ActorEncode<PageStyleMsg> for PageStyleActor {
305    fn encode(&self, _: &ActorRegistry) -> PageStyleMsg {
306        PageStyleMsg {
307            actor: self.name(),
308            traits: HashMap::from([
309                ("fontStretchLevel4".into(), true),
310                ("fontStyleLevel4".into(), true),
311                ("fontVariations".into(), true),
312                ("fontWeightLevel4".into(), true),
313            ]),
314        }
315    }
316}