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;
10
11use devtools_traits::DevtoolScriptControlMsg::{GetLayout, GetSelectors};
12use devtools_traits::{AutoMargins, ComputedNodeLayout, MatchedRule};
13use malloc_size_of_derive::MallocSizeOf;
14use serde::Serialize;
15use serde_json::{self, Map, Value};
16use servo_base::generic_channel::{self};
17
18use crate::StreamId;
19use crate::actor::{Actor, ActorEncode, ActorError, ActorRegistry};
20use crate::actors::inspector::node::NodeActor;
21use crate::actors::inspector::style_rule::{AppliedRule, ComputedDeclaration, StyleRuleActor};
22use crate::actors::inspector::walker::{WalkerActor, find_child};
23use crate::protocol::ClientRequest;
24
25#[derive(Serialize)]
26struct GetAppliedReply {
27    entries: Vec<AppliedEntry>,
28    from: String,
29}
30
31#[derive(Serialize)]
32struct GetComputedReply {
33    computed: HashMap<String, ComputedDeclaration>,
34    from: String,
35}
36
37#[derive(Serialize)]
38#[serde(rename_all = "camelCase")]
39struct AppliedEntry {
40    rule: AppliedRule,
41    pseudo_element: Option<()>,
42    is_system: bool,
43    #[serde(skip_serializing_if = "Option::is_none")]
44    inherited: Option<String>,
45}
46
47#[derive(Serialize)]
48struct DevtoolsAutoMargins {
49    #[serde(skip_serializing_if = "Option::is_none")]
50    top: Option<String>,
51    #[serde(skip_serializing_if = "Option::is_none")]
52    right: Option<String>,
53    #[serde(skip_serializing_if = "Option::is_none")]
54    bottom: Option<String>,
55    #[serde(skip_serializing_if = "Option::is_none")]
56    left: Option<String>,
57}
58
59impl From<AutoMargins> for DevtoolsAutoMargins {
60    fn from(auto_margins: AutoMargins) -> Self {
61        const AUTO: &str = "auto";
62        Self {
63            top: auto_margins.top.then_some(AUTO.into()),
64            right: auto_margins.right.then_some(AUTO.into()),
65            bottom: auto_margins.bottom.then_some(AUTO.into()),
66            left: auto_margins.left.then_some(AUTO.into()),
67        }
68    }
69}
70
71#[derive(Serialize)]
72struct GetLayoutReply {
73    from: String,
74    #[serde(flatten)]
75    layout: ComputedNodeLayout,
76    #[serde(rename = "autoMargins")]
77    auto_margins: DevtoolsAutoMargins,
78}
79
80#[derive(Serialize)]
81pub(crate) struct IsPositionEditableReply {
82    from: String,
83    value: bool,
84}
85
86#[derive(Serialize)]
87pub(crate) struct PageStyleMsg {
88    pub actor: String,
89    pub traits: HashMap<String, bool>,
90}
91
92#[derive(MallocSizeOf)]
93pub(crate) struct PageStyleActor {
94    pub name: String,
95}
96
97impl Actor for PageStyleActor {
98    fn name(&self) -> String {
99        self.name.clone()
100    }
101
102    /// The page style actor can handle the following messages:
103    ///
104    /// - `getApplied`: Returns the applied styles for a node, they represent the explicit css
105    ///   rules set for them, both in the style attribute and in stylesheets.
106    ///
107    /// - `getComputed`: Returns the computed styles for a node, these include all of the supported
108    ///   css properties calculated values.
109    ///
110    /// - `getLayout`: Returns the box layout properties for a node.
111    ///
112    /// - `isPositionEditable`: Informs whether you can change a style property in the inspector.
113    fn handle_message(
114        &self,
115        request: ClientRequest,
116        registry: &ActorRegistry,
117        msg_type: &str,
118        msg: &Map<String, Value>,
119        _id: StreamId,
120    ) -> Result<(), ActorError> {
121        match msg_type {
122            "getApplied" => self.get_applied(request, msg, registry),
123            "getComputed" => self.get_computed(request, msg, registry),
124            "getLayout" => self.get_layout(request, msg, registry),
125            "isPositionEditable" => self.is_position_editable(request),
126            _ => Err(ActorError::UnrecognizedPacketType),
127        }
128    }
129}
130
131impl PageStyleActor {
132    pub fn register(registry: &ActorRegistry) -> String {
133        let name = registry.new_name::<Self>();
134        let actor = Self { name: name.clone() };
135        registry.register::<Self>(actor);
136        name
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);
152        let browsing_context_actor = walker.browsing_context_actor(registry);
153        let entries: Vec<_> = find_child(
154            &node_actor.script_chan,
155            node_actor.pipeline,
156            node_name,
157            registry,
158            &walker.root(registry)?.actor,
159            vec![],
160            |msg| msg.actor == node_name,
161        )
162        .unwrap_or_default()
163        .into_iter()
164        .flat_map(|node| {
165            let inherited = (node.actor != node_name).then(|| node.actor.clone());
166            let node_actor = registry.find::<NodeActor>(&node.actor);
167
168            // Get the css selectors that match this node present in the currently active stylesheets.
169            let selectors = (|| {
170                let (selectors_sender, selector_receiver) = generic_channel::channel()?;
171                browsing_context_actor
172                    .script_chan()
173                    .send(GetSelectors(
174                        browsing_context_actor.pipeline_id(),
175                        registry.actor_to_script(node.actor.clone()),
176                        selectors_sender,
177                    ))
178                    .ok()?;
179                selector_receiver.recv().ok()?
180            })()
181            .unwrap_or_default();
182
183            // For each selector (plus an empty one that represents the style attribute)
184            // get all of the rules associated with it.
185
186            let style_attribute_rule = MatchedRule {
187                selector: "".into(),
188                stylesheet_index: usize::MAX,
189                block_id: 0,
190                ancestor_data: vec![],
191            };
192
193            once(style_attribute_rule)
194                .chain(selectors)
195                .filter_map(move |matched_rule| {
196                    let style_rule_name = node_actor
197                        .style_rules
198                        .borrow_mut()
199                        .entry(matched_rule.clone())
200                        .or_insert_with(|| {
201                            StyleRuleActor::register(
202                                registry,
203                                node_actor.name(),
204                                (matched_rule.stylesheet_index != usize::MAX)
205                                    .then_some(matched_rule.clone()),
206                            )
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(),
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: "".into(),
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(|| StyleRuleActor::register(registry, node_name.into(), None))
258            .clone();
259        let computed = registry
260            .find::<StyleRuleActor>(&style_rule_name)
261            .computed(registry)
262            .unwrap_or_default();
263
264        let msg = GetComputedReply {
265            computed,
266            from: self.name(),
267        };
268        request.reply_final(&msg)
269    }
270
271    fn get_layout(
272        &self,
273        request: ClientRequest,
274        msg: &Map<String, Value>,
275        registry: &ActorRegistry,
276    ) -> Result<(), ActorError> {
277        let node_name = msg
278            .get("node")
279            .ok_or(ActorError::MissingParameter)?
280            .as_str()
281            .ok_or(ActorError::BadParameterType)?;
282        let node_actor = registry.find::<NodeActor>(node_name);
283        let walker = registry.find::<WalkerActor>(&node_actor.walker);
284        let browsing_context_actor = walker.browsing_context_actor(registry);
285        let (tx, rx) = generic_channel::channel().ok_or(ActorError::Internal)?;
286        browsing_context_actor
287            .script_chan()
288            .send(GetLayout(
289                browsing_context_actor.pipeline_id(),
290                registry.actor_to_script(node_name.to_owned()),
291                tx,
292            ))
293            .map_err(|_| ActorError::Internal)?;
294        let (layout, auto_margins) = rx
295            .recv()
296            .map_err(|_| ActorError::Internal)?
297            .ok_or(ActorError::Internal)?;
298        request.reply_final(&GetLayoutReply {
299            from: self.name(),
300            layout,
301            auto_margins: auto_margins.into(),
302        })
303    }
304
305    fn is_position_editable(&self, request: ClientRequest) -> Result<(), ActorError> {
306        let msg = IsPositionEditableReply {
307            from: self.name(),
308            value: false,
309        };
310        request.reply_final(&msg)
311    }
312}
313
314impl ActorEncode<PageStyleMsg> for PageStyleActor {
315    fn encode(&self, _: &ActorRegistry) -> PageStyleMsg {
316        PageStyleMsg {
317            actor: self.name(),
318            traits: HashMap::from([
319                ("fontStretchLevel4".into(), true),
320                ("fontStyleLevel4".into(), true),
321                ("fontVariations".into(), true),
322                ("fontWeightLevel4".into(), true),
323            ]),
324        }
325    }
326}