1use std::cell::{Ref, RefCell, RefMut};
6use std::collections::HashMap;
7use std::str;
8
9use devtools_traits::{
10 AncestorData, AttrModification, AutoMargins, ComputedNodeLayout, CssDatabaseProperty,
11 EventListenerInfo, MatchedRule, NodeInfo, NodeStyle, RuleModification, TimelineMarker,
12 TimelineMarkerType,
13};
14use js::context::JSContext;
15use markup5ever::{LocalName, ns};
16use rustc_hash::FxHashMap;
17use script_bindings::root::Dom;
18use servo_base::generic_channel::GenericSender;
19use servo_base::id::PipelineId;
20use servo_config::pref;
21use style::attr::AttrValue;
22
23use crate::document_collection::DocumentCollection;
24use crate::dom::bindings::codegen::Bindings::CSSGroupingRuleBinding::CSSGroupingRuleMethods;
25use crate::dom::bindings::codegen::Bindings::CSSLayerBlockRuleBinding::CSSLayerBlockRuleMethods;
26use crate::dom::bindings::codegen::Bindings::CSSRuleListBinding::CSSRuleListMethods;
27use crate::dom::bindings::codegen::Bindings::CSSStyleDeclarationBinding::CSSStyleDeclarationMethods;
28use crate::dom::bindings::codegen::Bindings::CSSStyleRuleBinding::CSSStyleRuleMethods;
29use crate::dom::bindings::codegen::Bindings::CSSStyleSheetBinding::CSSStyleSheetMethods;
30use crate::dom::bindings::codegen::Bindings::DOMRectBinding::DOMRectMethods;
31use crate::dom::bindings::codegen::Bindings::DocumentBinding::DocumentMethods;
32use crate::dom::bindings::codegen::Bindings::ElementBinding::ElementMethods;
33use crate::dom::bindings::codegen::Bindings::HTMLElementBinding::HTMLElementMethods;
34use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeConstants;
35use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
36use crate::dom::bindings::inheritance::Castable;
37use crate::dom::bindings::root::DomRoot;
38use crate::dom::bindings::str::DOMString;
39use crate::dom::bindings::trace::NoTrace;
40use crate::dom::css::cssstyledeclaration::ENABLED_LONGHAND_PROPERTIES;
41use crate::dom::css::cssstylerule::CSSStyleRule;
42use crate::dom::document::AnimationFrameCallback;
43use crate::dom::element::Element;
44use crate::dom::node::{Node, NodeTraits, ShadowIncluding};
45use crate::dom::types::{CSSGroupingRule, CSSLayerBlockRule, EventTarget, HTMLElement};
46use crate::realms::enter_realm;
47use crate::script_runtime::CanGc;
48
49#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
50#[derive(JSTraceable)]
51pub(crate) struct PerPipelineState {
52 #[no_trace]
53 pipeline: PipelineId,
54
55 known_nodes: FxHashMap<String, Dom<Node>>,
57}
58
59#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
60#[derive(JSTraceable, Default)]
61pub(crate) struct DevtoolsState {
62 per_pipeline_state: RefCell<FxHashMap<NoTrace<PipelineId>, PerPipelineState>>,
63}
64
65impl PerPipelineState {
66 fn register_node(&mut self, node: &Node) {
67 let unique_id = node.unique_id(self.pipeline);
68 self.known_nodes
69 .entry(unique_id)
70 .or_insert_with(|| Dom::from_ref(node));
71 }
72}
73
74impl DevtoolsState {
75 pub(crate) fn notify_pipeline_created(&self, pipeline: PipelineId) {
76 self.per_pipeline_state.borrow_mut().insert(
77 NoTrace(pipeline),
78 PerPipelineState {
79 pipeline,
80 known_nodes: Default::default(),
81 },
82 );
83 }
84 pub(crate) fn notify_pipeline_exited(&self, pipeline: PipelineId) {
85 self.per_pipeline_state
86 .borrow_mut()
87 .remove(&NoTrace(pipeline));
88 }
89
90 fn pipeline_state_for(&self, pipeline: PipelineId) -> Option<Ref<'_, PerPipelineState>> {
91 Ref::filter_map(self.per_pipeline_state.borrow(), |state| {
92 state.get(&NoTrace(pipeline))
93 })
94 .ok()
95 }
96
97 fn mut_pipeline_state_for(&self, pipeline: PipelineId) -> Option<RefMut<'_, PerPipelineState>> {
98 RefMut::filter_map(self.per_pipeline_state.borrow_mut(), |state| {
99 state.get_mut(&NoTrace(pipeline))
100 })
101 .ok()
102 }
103
104 pub(crate) fn wants_updates_for_node(&self, pipeline: PipelineId, node: &Node) -> bool {
105 let Some(unique_id) = node.unique_id_if_already_present() else {
106 return false;
109 };
110 self.pipeline_state_for(pipeline)
111 .is_some_and(|pipeline_state| pipeline_state.known_nodes.contains_key(&unique_id))
112 }
113
114 fn find_node_by_unique_id(&self, pipeline: PipelineId, node_id: &str) -> Option<DomRoot<Node>> {
115 self.pipeline_state_for(pipeline)?
116 .known_nodes
117 .get(node_id)
118 .map(|node: &Dom<Node>| node.as_rooted())
119 }
120}
121
122pub(crate) fn handle_set_timeline_markers(
123 documents: &DocumentCollection,
124 pipeline: PipelineId,
125 marker_types: Vec<TimelineMarkerType>,
126 reply: GenericSender<Option<TimelineMarker>>,
127) {
128 match documents.find_window(pipeline) {
129 None => reply.send(None).unwrap(),
130 Some(window) => window.set_devtools_timeline_markers(marker_types, reply),
131 }
132}
133
134pub(crate) fn handle_drop_timeline_markers(
135 documents: &DocumentCollection,
136 pipeline: PipelineId,
137 marker_types: Vec<TimelineMarkerType>,
138) {
139 if let Some(window) = documents.find_window(pipeline) {
140 window.drop_devtools_timeline_markers(marker_types);
141 }
142}
143
144pub(crate) fn handle_request_animation_frame(
145 documents: &DocumentCollection,
146 id: PipelineId,
147 actor_name: String,
148) {
149 if let Some(doc) = documents.find_document(id) {
150 doc.request_animation_frame(AnimationFrameCallback::DevtoolsFramerateTick { actor_name });
151 }
152}
153
154pub(crate) fn handle_get_css_database(reply: GenericSender<HashMap<String, CssDatabaseProperty>>) {
155 let database: HashMap<_, _> = ENABLED_LONGHAND_PROPERTIES
156 .iter()
157 .map(|l| {
158 (
159 l.name().into(),
160 CssDatabaseProperty {
161 is_inherited: l.inherited(),
162 values: vec![], supports: vec![],
164 subproperties: vec![l.name().into()],
165 },
166 )
167 })
168 .collect();
169 let _ = reply.send(database);
170}
171
172pub(crate) fn handle_get_event_listener_info(
173 state: &DevtoolsState,
174 pipeline: PipelineId,
175 node_id: &str,
176 reply: GenericSender<Vec<EventListenerInfo>>,
177) {
178 let Some(node) = state.find_node_by_unique_id(pipeline, node_id) else {
179 reply.send(vec![]).unwrap();
180 return;
181 };
182
183 let event_listeners = node
184 .upcast::<EventTarget>()
185 .summarize_event_listeners_for_devtools();
186 reply.send(event_listeners).unwrap();
187}
188
189pub(crate) fn handle_get_root_node(
190 cx: &mut JSContext,
191 state: &DevtoolsState,
192 documents: &DocumentCollection,
193 pipeline: PipelineId,
194 reply: GenericSender<Option<NodeInfo>>,
195) {
196 let info = documents
197 .find_document(pipeline)
198 .map(DomRoot::upcast::<Node>)
199 .inspect(|node| {
200 state
201 .mut_pipeline_state_for(pipeline)
202 .unwrap()
203 .register_node(node)
204 })
205 .map(|document| document.upcast::<Node>().summarize(CanGc::from_cx(cx)));
206 reply.send(info).unwrap();
207}
208
209pub(crate) fn handle_get_document_element(
210 cx: &mut JSContext,
211 state: &DevtoolsState,
212 documents: &DocumentCollection,
213 pipeline: PipelineId,
214 reply: GenericSender<Option<NodeInfo>>,
215) {
216 let info = documents
217 .find_document(pipeline)
218 .and_then(|document| document.GetDocumentElement())
219 .inspect(|element| {
220 state
221 .mut_pipeline_state_for(pipeline)
222 .unwrap()
223 .register_node(element.upcast())
224 })
225 .map(|element| element.upcast::<Node>().summarize(CanGc::from_cx(cx)));
226 reply.send(info).unwrap();
227}
228
229pub(crate) fn handle_get_children(
230 cx: &mut JSContext,
231 state: &DevtoolsState,
232 pipeline: PipelineId,
233 node_id: &str,
234 reply: GenericSender<Option<Vec<NodeInfo>>>,
235) {
236 let Some(parent) = state.find_node_by_unique_id(pipeline, node_id) else {
237 reply.send(None).unwrap();
238 return;
239 };
240 let is_whitespace = |node: &NodeInfo| {
241 node.node_type == NodeConstants::TEXT_NODE &&
242 node.node_value.as_ref().is_none_or(|v| v.trim().is_empty())
243 };
244 let mut pipeline_state = state.mut_pipeline_state_for(pipeline).unwrap();
245
246 let inline: Vec<_> = parent
247 .children()
248 .map(|child| {
249 let window = child.owner_window();
250 let Some(elem) = child.downcast::<Element>() else {
251 return false;
252 };
253 let computed_style = window.GetComputedStyle(elem, None);
254 let display = computed_style.Display();
255 display == "inline"
256 })
257 .collect();
258
259 let mut children = vec![];
260 if let Some(shadow_root) = parent.downcast::<Element>().and_then(Element::shadow_root) {
261 if !shadow_root.is_user_agent_widget() || pref!(inspector_show_servo_internal_shadow_roots)
262 {
263 children.push(shadow_root.upcast::<Node>().summarize(CanGc::from_cx(cx)));
264 }
265 }
266 let children_iter = parent.children().enumerate().filter_map(|(i, child)| {
267 let prev_inline = i > 0 && inline[i - 1];
270 let next_inline = i < inline.len() - 1 && inline[i + 1];
271 let is_inline_level = prev_inline && next_inline;
272
273 let info = child.summarize(CanGc::from_cx(cx));
274 if is_whitespace(&info) && !is_inline_level {
275 return None;
276 }
277 pipeline_state.register_node(&child);
278
279 Some(info)
280 });
281 children.extend(children_iter);
282
283 reply.send(Some(children)).unwrap();
284}
285
286pub(crate) fn handle_get_attribute_style(
287 cx: &mut JSContext,
288 state: &DevtoolsState,
289 pipeline: PipelineId,
290 node_id: &str,
291 reply: GenericSender<Option<Vec<NodeStyle>>>,
292) {
293 let node = match state.find_node_by_unique_id(pipeline, node_id) {
294 None => return reply.send(None).unwrap(),
295 Some(found_node) => found_node,
296 };
297
298 let Some(elem) = node.downcast::<HTMLElement>() else {
299 reply.send(None).unwrap();
301 return;
302 };
303 let style = elem.Style(CanGc::from_cx(cx));
304
305 let msg = (0..style.Length())
306 .map(|i| {
307 let name = style.Item(i);
308 NodeStyle {
309 name: name.to_string(),
310 value: style.GetPropertyValue(name.clone()).to_string(),
311 priority: style.GetPropertyPriority(name).to_string(),
312 }
313 })
314 .collect();
315
316 reply.send(Some(msg)).unwrap();
317}
318
319fn build_rule_map(
320 cx: &mut JSContext,
321 list: &crate::dom::css::cssrulelist::CSSRuleList,
322 stylesheet_index: usize,
323 ancestors: &[AncestorData],
324 map: &mut HashMap<usize, MatchedRule>,
325) {
326 let can_gc = CanGc::from_cx(cx);
327 for i in 0..list.Length() {
328 let Some(rule) = list.Item(i, can_gc) else {
329 continue;
330 };
331
332 if let Some(style_rule) = rule.downcast::<CSSStyleRule>() {
333 let block_id = style_rule.block_id();
334 map.entry(block_id).or_insert_with(|| MatchedRule {
335 selector: style_rule.SelectorText().into(),
336 stylesheet_index,
337 block_id,
338 ancestor_data: ancestors.to_vec(),
339 });
340 continue;
341 }
342
343 if let Some(layer_rule) = rule.downcast::<CSSLayerBlockRule>() {
344 let name = layer_rule.Name().to_string();
345 let mut next = ancestors.to_vec();
346 next.push(AncestorData::Layer {
347 actor_id: None,
348 value: (!name.is_empty()).then_some(name),
349 });
350 let inner = layer_rule.upcast::<CSSGroupingRule>().CssRules(cx);
351 build_rule_map(cx, &inner, stylesheet_index, &next, map);
352 continue;
353 }
354
355 if let Some(group_rule) = rule.downcast::<CSSGroupingRule>() {
356 let inner = group_rule.CssRules(cx);
357 build_rule_map(cx, &inner, stylesheet_index, ancestors, map);
358 }
359 }
360}
361
362fn find_rule_by_block_id(
363 cx: &mut JSContext,
364 list: &crate::dom::css::cssrulelist::CSSRuleList,
365 target_block_id: usize,
366) -> Option<DomRoot<CSSStyleRule>> {
367 let can_gc = CanGc::from_cx(cx);
368 for i in 0..list.Length() {
369 let Some(rule) = list.Item(i, can_gc) else {
370 continue;
371 };
372
373 if let Some(style_rule) = rule.downcast::<CSSStyleRule>() {
374 if style_rule.block_id() == target_block_id {
375 return Some(DomRoot::from_ref(style_rule));
376 }
377 continue;
378 }
379
380 if let Some(group_rule) = rule.downcast::<CSSGroupingRule>() {
381 let inner = group_rule.CssRules(cx);
382 if let Some(found) = find_rule_by_block_id(cx, &inner, target_block_id) {
383 return Some(found);
384 }
385 }
386 }
387 None
388}
389
390#[cfg_attr(crown, expect(crown::unrooted_must_root))]
391pub(crate) fn handle_get_selectors(
392 cx: &mut JSContext,
393 state: &DevtoolsState,
394 documents: &DocumentCollection,
395 pipeline: PipelineId,
396 node_id: &str,
397 reply: GenericSender<Option<Vec<MatchedRule>>>,
398) {
399 let msg = (|| {
400 let node = state.find_node_by_unique_id(pipeline, node_id)?;
401 let elem = node.downcast::<Element>()?;
402 let document = documents.find_document(pipeline)?;
403 let _realm = enter_realm(document.window());
404 let owner = node.stylesheet_list_owner();
405
406 let mut decl_map = HashMap::new();
407 for i in 0..owner.stylesheet_count() {
408 let Some(stylesheet) = owner.stylesheet_at(i) else {
409 continue;
410 };
411 let Ok(list) = stylesheet.GetCssRules(CanGc::from_cx(cx)) else {
412 continue;
413 };
414 build_rule_map(cx, &list, i, &[], &mut decl_map);
415 }
416
417 let mut rules = Vec::new();
418 let computed = elem.style()?;
419
420 if let Some(rule_node) = computed.rules.as_ref() {
421 for rn in rule_node.self_and_ancestors() {
422 if let Some(source) = rn.style_source() {
423 let ptr = source.get().raw_ptr().as_ptr() as usize;
424
425 if let Some(matched) = decl_map.get(&ptr) {
426 rules.push(matched.clone());
427 }
428 }
429 }
430 }
431
432 Some(rules)
433 })();
434
435 reply.send(msg).unwrap();
436}
437
438#[cfg_attr(crown, expect(crown::unrooted_must_root))]
439#[allow(clippy::too_many_arguments)]
440pub(crate) fn handle_get_stylesheet_style(
441 cx: &mut JSContext,
442 state: &DevtoolsState,
443 documents: &DocumentCollection,
444 pipeline: PipelineId,
445 node_id: &str,
446 matched_rule: MatchedRule,
447 reply: GenericSender<Option<Vec<NodeStyle>>>,
448) {
449 let msg = (|| {
450 let node = state.find_node_by_unique_id(pipeline, node_id)?;
451 let document = documents.find_document(pipeline)?;
452 let _realm = enter_realm(document.window());
453 let owner = node.stylesheet_list_owner();
454
455 let stylesheet = owner.stylesheet_at(matched_rule.stylesheet_index)?;
456 let list = stylesheet.GetCssRules(CanGc::from_cx(cx)).ok()?;
457
458 let style_rule = find_rule_by_block_id(cx, &list, matched_rule.block_id)?;
459 let declaration = style_rule.Style(cx);
460
461 Some(
462 (0..declaration.Length())
463 .map(|i| {
464 let name = declaration.Item(i);
465 NodeStyle {
466 name: name.to_string(),
467 value: declaration.GetPropertyValue(name.clone()).to_string(),
468 priority: declaration.GetPropertyPriority(name).to_string(),
469 }
470 })
471 .collect(),
472 )
473 })();
474
475 reply.send(msg).unwrap();
476}
477
478pub(crate) fn handle_get_computed_style(
479 state: &DevtoolsState,
480 pipeline: PipelineId,
481 node_id: &str,
482 reply: GenericSender<Option<Vec<NodeStyle>>>,
483) {
484 let node = match state.find_node_by_unique_id(pipeline, node_id) {
485 None => return reply.send(None).unwrap(),
486 Some(found_node) => found_node,
487 };
488
489 let window = node.owner_window();
490 let elem = node
491 .downcast::<Element>()
492 .expect("This should be an element");
493 let computed_style = window.GetComputedStyle(elem, None);
494
495 let msg = (0..computed_style.Length())
496 .map(|i| {
497 let name = computed_style.Item(i);
498 NodeStyle {
499 name: name.to_string(),
500 value: computed_style.GetPropertyValue(name.clone()).to_string(),
501 priority: computed_style.GetPropertyPriority(name).to_string(),
502 }
503 })
504 .collect();
505
506 reply.send(Some(msg)).unwrap();
507}
508
509pub(crate) fn handle_get_layout(
510 cx: &mut JSContext,
511 state: &DevtoolsState,
512 pipeline: PipelineId,
513 node_id: &str,
514 reply: GenericSender<Option<(ComputedNodeLayout, AutoMargins)>>,
515) {
516 let node = match state.find_node_by_unique_id(pipeline, node_id) {
517 None => return reply.send(None).unwrap(),
518 Some(found_node) => found_node,
519 };
520 let auto_margins = determine_auto_margins(&node);
521
522 let elem = node
523 .downcast::<Element>()
524 .expect("should be getting layout of element");
525 let rect = elem.GetBoundingClientRect(CanGc::from_cx(cx));
526 let width = rect.Width() as f32;
527 let height = rect.Height() as f32;
528
529 let window = node.owner_window();
530 let computed_style = window.GetComputedStyle(elem, None);
531 let computed_layout = ComputedNodeLayout {
532 display: computed_style.Display().into(),
533 position: computed_style.Position().into(),
534 z_index: computed_style.ZIndex().into(),
535 box_sizing: computed_style.BoxSizing().into(),
536 margin_top: computed_style.MarginTop().into(),
537 margin_right: computed_style.MarginRight().into(),
538 margin_bottom: computed_style.MarginBottom().into(),
539 margin_left: computed_style.MarginLeft().into(),
540 border_top_width: computed_style.BorderTopWidth().into(),
541 border_right_width: computed_style.BorderRightWidth().into(),
542 border_bottom_width: computed_style.BorderBottomWidth().into(),
543 border_left_width: computed_style.BorderLeftWidth().into(),
544 padding_top: computed_style.PaddingTop().into(),
545 padding_right: computed_style.PaddingRight().into(),
546 padding_bottom: computed_style.PaddingBottom().into(),
547 padding_left: computed_style.PaddingLeft().into(),
548 width,
549 height,
550 };
551
552 reply.send(Some((computed_layout, auto_margins))).unwrap();
553}
554
555pub(crate) fn handle_get_xpath(
556 state: &DevtoolsState,
557 pipeline: PipelineId,
558 node_id: &str,
559 reply: GenericSender<String>,
560) {
561 let Some(node) = state.find_node_by_unique_id(pipeline, node_id) else {
562 return reply.send(Default::default()).unwrap();
563 };
564
565 let selector = node
566 .inclusive_ancestors(ShadowIncluding::Yes)
567 .filter_map(|ancestor| {
568 let Some(element) = ancestor.downcast::<Element>() else {
569 return None;
571 };
572
573 let mut result = "/".to_owned();
574 if *element.namespace() != ns!(html) {
575 result.push_str(element.namespace());
576 result.push(':');
577 }
578
579 result.push_str(element.local_name());
580
581 let would_node_also_match_selector = |sibling: &Node| {
582 let Some(sibling) = sibling.downcast::<Element>() else {
583 return false;
584 };
585 sibling.namespace() == element.namespace() &&
586 sibling.local_name() == element.local_name()
587 };
588
589 let matching_elements_before = ancestor
590 .preceding_siblings()
591 .filter(|node| would_node_also_match_selector(node))
592 .count();
593 let matching_elements_after = ancestor
594 .following_siblings()
595 .filter(|node| would_node_also_match_selector(node))
596 .count();
597
598 if matching_elements_before + matching_elements_after != 0 {
599 result.push_str(&format!("[{}]", matching_elements_before + 1));
601 }
602
603 Some(result)
604 })
605 .collect::<Vec<_>>()
606 .into_iter()
607 .rev()
608 .collect::<Vec<_>>()
609 .join("");
610
611 reply.send(selector).unwrap();
612}
613
614pub(crate) fn handle_modify_attribute(
615 cx: &mut JSContext,
616 state: &DevtoolsState,
617 documents: &DocumentCollection,
618 pipeline: PipelineId,
619 node_id: &str,
620 modifications: Vec<AttrModification>,
621) {
622 let Some(document) = documents.find_document(pipeline) else {
623 return warn!("document for pipeline id {} is not found", &pipeline);
624 };
625 let _realm = enter_realm(document.window());
626
627 let node = match state.find_node_by_unique_id(pipeline, node_id) {
628 None => {
629 return warn!(
630 "node id {} for pipeline id {} is not found",
631 &node_id, &pipeline
632 );
633 },
634 Some(found_node) => found_node,
635 };
636
637 let elem = node
638 .downcast::<Element>()
639 .expect("should be getting layout of element");
640
641 for modification in modifications {
642 match modification.new_value {
643 Some(string) => {
644 elem.set_attribute(
645 &LocalName::from(modification.attribute_name),
646 AttrValue::String(string),
647 CanGc::from_cx(cx),
648 );
649 },
650 None => elem.RemoveAttribute(
651 DOMString::from(modification.attribute_name),
652 CanGc::from_cx(cx),
653 ),
654 }
655 }
656}
657
658pub(crate) fn handle_modify_rule(
659 cx: &mut JSContext,
660 state: &DevtoolsState,
661 documents: &DocumentCollection,
662 pipeline: PipelineId,
663 node_id: &str,
664 modifications: Vec<RuleModification>,
665) {
666 let Some(document) = documents.find_document(pipeline) else {
667 return warn!("Document for pipeline id {} is not found", &pipeline);
668 };
669 let _realm = enter_realm(document.window());
670
671 let Some(node) = state.find_node_by_unique_id(pipeline, node_id) else {
672 return warn!(
673 "Node id {} for pipeline id {} is not found",
674 &node_id, &pipeline
675 );
676 };
677
678 let elem = node
679 .downcast::<HTMLElement>()
680 .expect("This should be an HTMLElement");
681 let style = elem.Style(CanGc::from_cx(cx));
682
683 for modification in modifications {
684 let _ = style.SetProperty(
685 cx,
686 modification.name.into(),
687 modification.value.into(),
688 modification.priority.into(),
689 );
690 }
691}
692
693pub(crate) fn handle_highlight_dom_node(
694 state: &DevtoolsState,
695 documents: &DocumentCollection,
696 id: PipelineId,
697 node_id: Option<&str>,
698) {
699 let node = node_id.and_then(|node_id| {
700 let node = state.find_node_by_unique_id(id, node_id);
701 if node.is_none() {
702 log::warn!("Node id {node_id} for pipeline id {id} is not found",);
703 }
704 node
705 });
706
707 if let Some(window) = documents.find_window(id) {
708 window.Document().highlight_dom_node(node.as_deref());
709 }
710}
711
712fn determine_auto_margins(node: &Node) -> AutoMargins {
713 let Some(style) = node.style() else {
714 return AutoMargins::default();
715 };
716 let margin = style.get_margin();
717 AutoMargins {
718 top: margin.margin_top.is_auto(),
719 right: margin.margin_right.is_auto(),
720 bottom: margin.margin_bottom.is_auto(),
721 left: margin.margin_left.is_auto(),
722 }
723}