Skip to main content

apace_dom/
mutator.rs

1use apace_traits::node_id::NodeId;
2use std::collections::HashSet;
3use std::mem;
4use std::ops::{Deref, DerefMut};
5
6use crate::layout::damage::ALL_DAMAGE;
7use crate::net::{ImageHandler, ResourceHandler, StylesheetHandler};
8use crate::node::{CanvasData, NodeFlags, SpecialElementData};
9use crate::stylo_device::DeviceChanges;
10use crate::util::ImageType;
11use crate::{
12    Attribute, BaseDocument, Document, ElementData, Node, NodeData, QualName, local_name, qual_name,
13};
14use apace_traits::shell::Viewport;
15use style::Atom;
16use style::invalidation::element::restyle_hints::RestyleHint;
17use style::stylesheets::OriginSet;
18use thin_vec::ThinVec;
19
20macro_rules! tag_and_attr {
21    ($tag:tt, $attr:tt) => {
22        (&local_name!($tag), &local_name!($attr))
23    };
24}
25
26#[derive(Debug, Clone)]
27pub enum AppendTextErr {
28    /// The node is not a text node
29    NotTextNode,
30}
31
32/// Operations that happen almost immediately, but are deferred within a
33/// function for borrow-checker reasons.
34enum SpecialOp {
35    LoadImage(NodeId),
36    LoadIframe(NodeId),
37    LoadStylesheet(NodeId),
38    UnloadStylesheet(NodeId),
39    LoadCustomPaintSource(NodeId),
40    ProcessButtonInput(NodeId),
41    UnloadSubDocument(NodeId),
42    #[cfg(feature = "custom-widget")]
43    UnloadCustomWidget(NodeId),
44}
45
46pub struct DocumentMutator<'doc> {
47    /// Document is public as an escape hatch, but users of this API should ideally avoid using it
48    /// and prefer exposing additional functionality in DocumentMutator.
49    pub doc: &'doc mut BaseDocument,
50
51    eager_op_queue: Vec<SpecialOp>,
52
53    // Tracked nodes for deferred processing when mutations have completed
54    title_node: Option<NodeId>,
55    style_nodes: HashSet<NodeId>,
56    form_nodes: HashSet<NodeId>,
57
58    /// Whether an element/attribute that affect animation status has been seen
59    recompute_is_animating: bool,
60
61    /// Whether any mutation that affects rendered output has been performed
62    mutations_occurred: bool,
63    // Captured lazily only for a cross-parent move of focused ancestry.
64    previous_focus_path: Option<Vec<NodeId>>,
65
66    /// The (latest) node which has been mounted in and had autofocus=true, if any
67    #[cfg(feature = "autofocus")]
68    node_to_autofocus: Option<NodeId>,
69}
70
71impl Drop for DocumentMutator<'_> {
72    fn drop(&mut self) {
73        self.flush(); // Defined at bottom of file
74        if self.mutations_occurred {
75            self.doc.shell_provider.request_redraw();
76        }
77    }
78}
79
80impl DocumentMutator<'_> {
81    pub fn new<'doc>(doc: &'doc mut BaseDocument) -> DocumentMutator<'doc> {
82        DocumentMutator {
83            doc,
84            eager_op_queue: Vec::new(),
85            title_node: None,
86            style_nodes: HashSet::new(),
87            form_nodes: HashSet::new(),
88            recompute_is_animating: false,
89            mutations_occurred: false,
90            previous_focus_path: None,
91            #[cfg(feature = "autofocus")]
92            node_to_autofocus: None,
93        }
94    }
95
96    // Query methods
97
98    pub fn set_input_checked(&mut self, node_id: NodeId, value: bool) -> bool {
99        self.doc.set_input_checked(node_id, value)
100    }
101
102    /// Set the live checkbox property independently of its checked value. This
103    /// also works before attachment; unsupported/retired nodes are rejected.
104    pub fn set_input_indeterminate(&mut self, node_id: NodeId, value: bool) -> bool {
105        let Some(previous) = self
106            .doc
107            .nodes
108            .get(node_id)
109            .and_then(|node| node.element_data())
110            .and_then(ElementData::checkbox_input_indeterminate)
111        else {
112            return false;
113        };
114        if previous == value {
115            return true;
116        }
117        self.mutations_occurred = true;
118        self.doc
119            .snapshot_node_and(node_id, style_dom::ElementState::INDETERMINATE, |node| {
120                node.element_data_mut()
121                    .unwrap()
122                    .set_checkbox_input_indeterminate(value);
123                node.mark_ancestors_dirty();
124                node.insert_damage(ALL_DAMAGE);
125            });
126        true
127    }
128
129    pub fn node_has_parent(&self, node_id: NodeId) -> bool {
130        self.doc.nodes[node_id].parent.is_some()
131    }
132
133    pub fn previous_sibling_id(&self, node_id: NodeId) -> Option<NodeId> {
134        self.doc.nodes[node_id].backward(1).map(|node| node.id)
135    }
136
137    pub fn next_sibling_id(&self, node_id: NodeId) -> Option<NodeId> {
138        self.doc.nodes[node_id].forward(1).map(|node| node.id)
139    }
140
141    pub fn parent_id(&self, node_id: NodeId) -> Option<NodeId> {
142        self.doc.nodes[node_id].parent
143    }
144
145    pub fn last_child_id(&self, node_id: NodeId) -> Option<NodeId> {
146        self.doc.nodes[node_id].children.last().copied()
147    }
148
149    pub fn child_ids(&self, node_id: NodeId) -> ThinVec<NodeId> {
150        self.doc.nodes[node_id].children.clone()
151    }
152
153    pub fn element_name(&self, node_id: NodeId) -> Option<&QualName> {
154        self.doc.nodes[node_id].element_data().map(|el| &el.name)
155    }
156
157    pub fn node_at_path(&self, start_node_id: NodeId, path: &[u8]) -> NodeId {
158        let mut current = &self.doc.nodes[start_node_id];
159        for i in path {
160            let new_id = current.children[*i as usize];
161            current = &self.doc.nodes[new_id];
162        }
163        current.id
164    }
165
166    // Node creation methods
167
168    pub fn create_comment_node(&mut self, contents: &str) -> NodeId {
169        self.doc.create_node(NodeData::Comment {
170            contents: contents.to_string(),
171        })
172    }
173
174    pub fn create_text_node(&mut self, text: &str) -> NodeId {
175        self.doc.create_text_node(text)
176    }
177
178    pub fn create_element(&mut self, name: QualName, attrs: Vec<Attribute>) -> NodeId {
179        let mut data = ElementData::new(name, attrs);
180        data.flush_style_attribute(self.doc.guard(), &self.doc.url.url_extra_data());
181
182        let id = self.doc.create_node(NodeData::Element(Box::new(data)));
183        let node = self.doc.get_node_mut(id).unwrap();
184
185        // Initialise style data
186        *node.stylo_element_data_mut().ensure_init_mut() = style::data::ElementData {
187            damage: ALL_DAMAGE,
188            ..Default::default()
189        };
190
191        id
192    }
193
194    pub fn deep_clone_node(&mut self, node_id: NodeId) -> NodeId {
195        self.doc.deep_clone_node(node_id)
196    }
197
198    /// Create (or return the existing) "template contents" fragment node for a
199    /// `<template>` element. The contents node is detached from the tree (so it
200    /// is inert: never styled or rendered) and is exposed to JavaScript as the
201    /// template's `content` DocumentFragment.
202    pub fn template_contents(&mut self, template_id: NodeId) -> NodeId {
203        if let Some(contents_id) = self.try_template_contents(template_id) {
204            return contents_id;
205        }
206        let name = QualName::new(
207            None,
208            markup5ever::ns!(html),
209            markup5ever::LocalName::from("#document-fragment"),
210        );
211        let contents_id = self.create_element(name, Vec::new());
212        if let Some(element) = self
213            .doc
214            .get_node_mut(template_id)
215            .and_then(|node| node.element_data_mut())
216        {
217            element.template_contents = Some(contents_id);
218        }
219        contents_id
220    }
221
222    /// The "template contents" fragment node of a `<template>` element (if any)
223    pub fn try_template_contents(&self, template_id: NodeId) -> Option<NodeId> {
224        self.doc
225            .get_node(template_id)
226            .and_then(|node| node.element_data())
227            .and_then(|element| element.template_contents)
228    }
229
230    // Node mutation methods
231
232    pub fn set_node_text(&mut self, node_id: NodeId, value: &str) {
233        let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
234        let node = &mut self.doc.nodes[node_id];
235
236        let text = match node.data {
237            NodeData::Text(ref mut text) => text,
238            // TODO: otherwise this is basically element.textContent which is a bit different - need to parse as html
239            _ => return,
240        };
241
242        let changed = text.content != value;
243        if changed {
244            self.mutations_occurred |= node_is_in_document;
245            text.content.clear();
246            text.content.push_str(value);
247            node.insert_damage(ALL_DAMAGE);
248            // Mark ancestors dirty so the style traversal visits this subtree.
249            // Without this, the traversal may skip nodes with pending damage.
250            node.mark_ancestors_dirty();
251            let parent_id = node.parent;
252
253            // Also insert damage on the parent element, since text content changes
254            // affect the parent's layout (text may wrap differently, change size, etc.)
255            if let Some(parent_id) = parent_id {
256                let parent = &mut self.doc.nodes[parent_id];
257                parent.insert_damage(ALL_DAMAGE);
258            }
259
260            self.maybe_record_node(parent_id);
261            if let Some(parent_id) = parent_id {
262                self.doc.sync_placeholder_state(parent_id);
263            }
264        }
265    }
266
267    pub fn append_text_to_node(
268        &mut self,
269        node_id: NodeId,
270        text: &str,
271    ) -> Result<(), AppendTextErr> {
272        let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
273        let node = &mut self.doc.nodes[node_id];
274        node.insert_damage(ALL_DAMAGE);
275        node.mark_ancestors_dirty();
276        match node.text_data_mut() {
277            Some(data) => {
278                data.content += text;
279                self.mutations_occurred |= node_is_in_document;
280                Ok(())
281            }
282            None => Err(AppendTextErr::NotTextNode),
283        }
284    }
285
286    pub fn add_attrs_if_missing(&mut self, node_id: NodeId, attrs: Vec<Attribute>) {
287        let node = &mut self.doc.nodes[node_id];
288        node.insert_damage(ALL_DAMAGE);
289        let element_data = node.element_data_mut().expect("Not an element");
290
291        let existing_names = element_data
292            .attrs
293            .iter()
294            .map(|e| e.name.clone())
295            .collect::<HashSet<_>>();
296
297        for attr in attrs
298            .into_iter()
299            .filter(|attr| !existing_names.contains(&attr.name))
300        {
301            self.set_attribute(node_id, attr.name, &attr.value);
302        }
303    }
304
305    pub fn set_attribute(&mut self, node_id: NodeId, name: QualName, value: &str) {
306        // Inline declarations still invalidate selectors and inherited styles,
307        // but their layout damage comes from the computed-style difference.
308        // A top/height edit must not reconstruct all descendant text contexts.
309        // A hint edit changes its presentation layout only; matching selectors
310        // can still change the real editor's styles through ordinary damage.
311        let computed_damage_only_attribute = name.local == local_name!("style")
312            || name.local == local_name!("placeholder")
313                && self.doc.nodes[node_id]
314                    .element_data()
315                    .is_some_and(|e| e.text_input_data().is_some());
316        #[cfg(feature = "custom-widget")]
317        let computed_damage_only_attribute = computed_damage_only_attribute
318            && !self.doc.nodes[node_id]
319                .element_data()
320                .is_some_and(|element| {
321                    matches!(element.special_data, SpecialElementData::CustomWidget(_))
322                });
323        let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
324        if node_is_in_document {
325            self.doc.snapshot_node(node_id);
326
327            let node = &mut self.doc.nodes[node_id];
328            if let Some(mut data) = node.try_stylo_element_data_mut().and_then(|s| s.get_mut()) {
329                data.hint |= RestyleHint::restyle_subtree();
330                if !computed_damage_only_attribute {
331                    data.damage.insert(ALL_DAMAGE);
332                }
333            }
334            node.mark_damaged();
335
336            // TODO: make this fine grained / conditional based on ElementSelectorFlags
337            let parent = node.parent;
338            if let Some(parent_id) = parent {
339                let parent = &mut self.doc.nodes[parent_id];
340                if let Some(mut data) = parent
341                    .try_stylo_element_data_mut()
342                    .and_then(|s| s.get_mut())
343                {
344                    data.hint |= RestyleHint::restyle_subtree();
345                }
346            }
347
348            // Mark ancestors dirty so the style traversal visits this subtree.
349            // Without this, the traversal may skip nodes with pending RestyleHint/damage
350            // because it uses dirty_descendants flags to determine which subtrees to visit.
351            self.doc.nodes[node_id].mark_ancestors_dirty();
352        }
353
354        if name.local == local_name!("id") && node_is_in_document {
355            if let Some(old_id) = self.doc.nodes[node_id]
356                .element_data()
357                .map(|element| element.id.clone())
358            {
359                if let Some(old_id) = old_id {
360                    self.doc.remove_from_id_map(&old_id, node_id);
361                }
362                self.doc.add_to_id_map(value, node_id);
363            }
364        }
365
366        let node = &mut self.doc.nodes[node_id];
367
368        let NodeData::Element(ref mut element) = node.data else {
369            return;
370        };
371
372        self.mutations_occurred |= node_is_in_document;
373        // If element is a CustomWidget, then Ccall attribute_changed on it
374        #[cfg(feature = "custom-widget")]
375        if let SpecialElementData::CustomWidget(widget_data) = &mut element.special_data {
376            let old_value = element.attrs.get(&name).as_ref().map(|attr| &*attr.value);
377            widget_data
378                .widget
379                .attribute_changed(&name.local, old_value, Some(value));
380        }
381
382        element.attrs.set(name.clone(), value);
383        if name.local == local_name!("type") && element.name.local == local_name!("input") {
384            if value != "checkbox" {
385                element
386                    .element_state
387                    .remove(style_dom::ElementState::INDETERMINATE);
388            }
389            if let Some(input) = element.text_input_data_mut() {
390                input.set_password(value == "password");
391                input.refresh_display_config(
392                    &mut self.doc.font_ctx.lock().unwrap(),
393                    &mut self.doc.layout_ctx,
394                );
395            }
396        }
397
398        // Focusability is cached on the element and comes from these
399        // attributes, so it has to follow a change to one of them: a widget
400        // that hands the focus around its own children - a menu, a grid -
401        // sets their tabindex after creating them.
402        if name.local == local_name!("tabindex")
403            || name.local == local_name!("href")
404            || name.local == local_name!("disabled")
405        {
406            element.flush_is_focussable();
407        }
408
409        if name.local == local_name!("href") {
410            element.flush_link_state();
411        }
412
413        let tag = &element.name.local;
414        let attr = &name.local;
415
416        if *attr == local_name!("id") {
417            element.id = Some(Atom::from(value))
418        }
419
420        if *attr == local_name!("value") {
421            if let Some(input_data) = element.text_input_data_mut() {
422                // Update text input value
423                input_data.set_text(
424                    &mut self.doc.font_ctx.lock().unwrap(),
425                    &mut self.doc.layout_ctx,
426                    value,
427                );
428            }
429            self.doc.sync_placeholder_state(node_id);
430            return;
431        }
432
433        if *attr == local_name!("placeholder") {
434            if let Some(input) = element.text_input_data_mut() {
435                input.set_placeholder(Some(value));
436                input.refresh_display(
437                    &mut self.doc.font_ctx.lock().unwrap(),
438                    &mut self.doc.layout_ctx,
439                );
440            }
441            self.doc.sync_placeholder_state(node_id);
442            return;
443        }
444
445        if *attr == local_name!("style") {
446            element.flush_style_attribute(&self.doc.guard, &self.doc.url.url_extra_data());
447            node.set_restyle_hint(RestyleHint::RESTYLE_STYLE_ATTRIBUTE);
448            return;
449        }
450
451        if *attr == local_name!("disabled") && element.can_be_disabled() {
452            node.disable();
453            return;
454        }
455
456        // If node if not in the document, then don't apply any special behaviours
457        // and simply set the attribute value
458        if !node.flags.is_in_document() {
459            self.doc.sync_placeholder_state(node_id);
460            if matches!(name.local.as_ref(), "name" | "form" | "type") {
461                self.doc.reconcile_radio(node_id);
462            }
463            return;
464        }
465
466        // Custom widgets replace the element's built-in tag-specific behaviour
467        #[cfg(feature = "custom-widget")]
468        if element.custom_widget_data().is_some() {
469            return;
470        }
471
472        if (tag, attr) == tag_and_attr!("input", "checked") {
473            set_input_checked_state(element, value.to_string());
474        } else if (tag, attr) == tag_and_attr!("img", "src") {
475            // A request for a new src must not keep the previous image alive.
476            node.element_data_mut().unwrap().special_data =
477                SpecialElementData::Image(Box::new(crate::node::ImageData::None));
478            self.load_image(node_id);
479        } else if (tag, attr) == tag_and_attr!("canvas", "src") {
480            self.load_custom_paint_src(node_id);
481        } else if (tag, attr) == tag_and_attr!("link", "href")
482            || (tag, attr) == tag_and_attr!("link", "rel")
483        {
484            self.load_linked_stylesheet(node_id);
485        } else if (tag, attr) == tag_and_attr!("iframe", "src")
486            || (tag, attr) == tag_and_attr!("iframe", "srcdoc")
487        {
488            self.load_iframe(node_id);
489        }
490        if matches!(name.local.as_ref(), "name" | "form" | "type" | "checked") {
491            self.doc.reconcile_radio(node_id);
492        }
493        self.doc.sync_placeholder_state(node_id);
494    }
495
496    pub fn clear_attribute(&mut self, node_id: NodeId, name: QualName) {
497        let computed_damage_only_attribute = name.local == local_name!("style")
498            || name.local == local_name!("placeholder")
499                && self.doc.nodes[node_id]
500                    .element_data()
501                    .is_some_and(|e| e.text_input_data().is_some());
502        #[cfg(feature = "custom-widget")]
503        let computed_damage_only_attribute = computed_damage_only_attribute
504            && !self.doc.nodes[node_id]
505                .element_data()
506                .is_some_and(|element| {
507                    matches!(element.special_data, SpecialElementData::CustomWidget(_))
508                });
509        let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
510        if node_is_in_document {
511            self.doc.snapshot_node(node_id);
512
513            let node = &mut self.doc.nodes[node_id];
514
515            if let Some(mut data) = node.try_stylo_element_data_mut().and_then(|s| s.get_mut()) {
516                data.hint |= RestyleHint::restyle_subtree();
517                if !computed_damage_only_attribute {
518                    data.damage.insert(ALL_DAMAGE);
519                }
520            }
521            node.mark_damaged();
522
523            // Mark ancestors dirty so the style traversal visits this subtree.
524            // Without this, the traversal may skip nodes with pending RestyleHint/damage.
525            node.mark_ancestors_dirty();
526        }
527
528        if name.local == local_name!("id") && node_is_in_document {
529            if let Some(old_id) = self.doc.nodes[node_id]
530                .element_data()
531                .and_then(|element| element.id.clone())
532            {
533                self.doc.remove_from_id_map(&old_id, node_id);
534            }
535        }
536
537        let node = &mut self.doc.nodes[node_id];
538
539        let Some(element) = node.element_data_mut() else {
540            return;
541        };
542
543        let removed_attr = element.attrs.remove(&name);
544        let had_attr = removed_attr.is_some();
545        if !had_attr {
546            return;
547        }
548        self.mutations_occurred |= node_is_in_document;
549
550        // If element is a CustomWidget, then call attribute_changed on it
551        #[cfg(feature = "custom-widget")]
552        if let SpecialElementData::CustomWidget(widget_data) = &mut element.special_data {
553            let old_value = removed_attr.as_ref().map(|attr| &*attr.value);
554            widget_data
555                .widget
556                .attribute_changed(&name.local, old_value, None);
557        }
558
559        if name.local == local_name!("id") {
560            element.id = None;
561        }
562
563        // As in `set_attribute`: taking one of these away can make the element
564        // unfocusable again.
565        if name.local == local_name!("tabindex")
566            || name.local == local_name!("href")
567            || name.local == local_name!("disabled")
568        {
569            element.flush_is_focussable();
570        }
571
572        if name.local == local_name!("href") {
573            element.flush_link_state();
574        }
575
576        // Update text input value
577        if name.local == local_name!("type") && element.name.local == local_name!("input") {
578            element
579                .element_state
580                .remove(style_dom::ElementState::INDETERMINATE);
581            if let Some(input) = element.text_input_data_mut() {
582                input.set_password(false);
583            }
584        }
585        if name.local == local_name!("value") {
586            if let Some(input_data) = element.text_input_data_mut() {
587                input_data.set_text(
588                    &mut self.doc.font_ctx.lock().unwrap(),
589                    &mut self.doc.layout_ctx,
590                    "",
591                );
592            }
593        }
594
595        if name.local == local_name!("placeholder") {
596            if let Some(input) = element.text_input_data_mut() {
597                input.set_placeholder(None);
598                input.refresh_display(
599                    &mut self.doc.font_ctx.lock().unwrap(),
600                    &mut self.doc.layout_ctx,
601                );
602            }
603            self.doc.sync_placeholder_state(node_id);
604            return;
605        }
606
607        let tag = &element.name.local;
608        let attr = &name.local;
609
610        if *attr == local_name!("disabled") && element.can_be_disabled() {
611            node.enable();
612            return;
613        }
614
615        if *attr == local_name!("style") {
616            element.flush_style_attribute(&self.doc.guard, &self.doc.url.url_extra_data());
617            node.set_restyle_hint(RestyleHint::RESTYLE_STYLE_ATTRIBUTE);
618        } else if (tag, attr) == tag_and_attr!("input", "checked") {
619            element.set_checkbox_input_checked(false);
620        } else if (tag, attr) == tag_and_attr!("img", "src") {
621            element.special_data =
622                SpecialElementData::Image(Box::new(crate::node::ImageData::None));
623            node.clear_layout_cache();
624            node.insert_damage(ALL_DAMAGE);
625            self.load_image(node_id);
626        } else if (tag, attr) == tag_and_attr!("canvas", "src") {
627            self.recompute_is_animating = true;
628        } else if (tag, attr) == tag_and_attr!("link", "href") {
629            self.unload_stylesheet(node_id);
630        } else if (tag, attr) == tag_and_attr!("iframe", "srcdoc") && node_is_in_document {
631            // Fall back to loading from the `src` attribute (if any)
632            self.load_iframe(node_id);
633        }
634        if matches!(name.local.as_ref(), "name" | "form" | "type") {
635            self.doc.reconcile_radio(node_id);
636        }
637        self.doc.sync_placeholder_state(node_id);
638    }
639
640    pub fn set_style_property(&mut self, node_id: NodeId, name: &str, value: &str) {
641        let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
642        self.doc.set_style_property(node_id, name, value);
643        self.mutations_occurred |= node_is_in_document;
644    }
645
646    pub fn remove_style_property(&mut self, node_id: NodeId, name: &str) {
647        let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
648        self.doc.remove_style_property(node_id, name);
649        self.mutations_occurred |= node_is_in_document;
650    }
651
652    pub fn set_sub_document(&mut self, node_id: NodeId, sub_document: Box<dyn Document>) {
653        let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
654        self.doc.set_sub_document(node_id, sub_document);
655        self.mutations_occurred |= node_is_in_document;
656    }
657
658    pub fn remove_sub_document(&mut self, node_id: NodeId) {
659        let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
660        self.doc.remove_sub_document(node_id);
661        self.mutations_occurred |= node_is_in_document;
662    }
663
664    #[cfg(feature = "custom-widget")]
665    pub fn set_custom_widget(&mut self, node_id: NodeId, widget: Box<dyn crate::Widget>) {
666        let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
667        self.doc.set_custom_widget(node_id, widget);
668        self.mutations_occurred |= node_is_in_document;
669    }
670
671    #[cfg(feature = "custom-widget")]
672    pub fn remove_custom_widget(&mut self, node_id: NodeId) {
673        let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
674        self.doc.remove_custom_widget(node_id);
675        self.mutations_occurred |= node_is_in_document;
676    }
677
678    /// Remove the node from it's parent but don't drop it
679    pub fn remove_node(&mut self, node_id: NodeId) {
680        let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
681        // Process the subtree *before* severing the parent link so that
682        // interaction state referencing removed nodes can retarget to the
683        // nearest surviving ancestor.
684        self.process_removed_subtree(node_id);
685
686        let node = &mut self.doc.nodes[node_id];
687
688        // Update child_idx values
689        if let Some(parent_id) = node.parent.take() {
690            self.mutations_occurred |= node_is_in_document;
691            let parent = &mut self.doc.nodes[parent_id];
692            parent.insert_damage(ALL_DAMAGE);
693            // Mark ancestors dirty so the style traversal visits this subtree.
694            parent.mark_ancestors_dirty();
695            parent.children.retain(|id| *id != node_id);
696            self.maybe_record_node(parent_id);
697        }
698    }
699
700    pub fn remove_and_drop_node(&mut self, node_id: NodeId) -> Option<Node> {
701        self.remove_and_drop_node_with(node_id, &mut |_| {})
702    }
703
704    /// Like [`Self::remove_and_drop_node`], but calls `on_drop` with the id of
705    /// every dropped node (the node itself and all of its descendants).
706    pub fn remove_and_drop_node_with(
707        &mut self,
708        node_id: NodeId,
709        on_drop: &mut dyn FnMut(NodeId),
710    ) -> Option<Node> {
711        let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
712        self.process_removed_subtree(node_id);
713
714        let node = self.doc.drop_node_ignoring_parent_with(node_id, on_drop);
715        self.mutations_occurred |= node_is_in_document;
716
717        // Update child_idx values
718        if let Some(parent_id) = node.as_ref().and_then(|node| node.parent) {
719            let parent = &mut self.doc.nodes[parent_id];
720            parent.insert_damage(ALL_DAMAGE);
721            let parent_is_in_doc = parent.flags.is_in_document();
722
723            // TODO: make this fine grained / conditional based on ElementSelectorFlags
724            if parent_is_in_doc {
725                if let Some(mut data) = parent
726                    .try_stylo_element_data_mut()
727                    .and_then(|s| s.get_mut())
728                {
729                    data.hint |= RestyleHint::restyle_subtree();
730                }
731                // Mark ancestors dirty so the style traversal visits this subtree.
732                parent.mark_ancestors_dirty();
733            }
734
735            parent.children.retain(|id| *id != node_id);
736            self.maybe_record_node(parent_id);
737        }
738
739        node
740    }
741
742    pub fn remove_and_drop_all_children(&mut self, node_id: NodeId) {
743        let parent = &mut self.doc.nodes[node_id];
744        let parent_is_in_doc = parent.flags.is_in_document();
745        if !parent.children.is_empty() {
746            // Restyling identical declarations does not invalidate the paint
747            // tree. Structural removal must also retire cached inline boxes
748            // and contributions to ancestor stacking contexts.
749            parent.insert_damage(ALL_DAMAGE);
750        }
751
752        // TODO: make this fine grained / conditional based on ElementSelectorFlags
753        if parent_is_in_doc {
754            if let Some(mut data) = parent
755                .try_stylo_element_data_mut()
756                .and_then(|s| s.get_mut())
757            {
758                data.hint |= RestyleHint::restyle_subtree();
759            }
760            // Mark ancestors dirty so the style traversal visits this subtree.
761            parent.mark_ancestors_dirty();
762        }
763
764        let children = mem::take(&mut parent.children);
765        self.mutations_occurred |= parent_is_in_doc && !children.is_empty();
766        for child_id in children {
767            self.process_removed_subtree(child_id);
768            let _ = self.doc.drop_node_ignoring_parent(child_id);
769        }
770        self.maybe_record_node(node_id);
771    }
772
773    // Tree mutation methods
774    pub fn remove_node_if_unparented(&mut self, node_id: NodeId) {
775        self.remove_node_if_unparented_with(node_id, &mut |_| {});
776    }
777
778    /// Like [`Self::remove_node_if_unparented`], but calls `on_drop` with the id of
779    /// every dropped node (the node itself and all of its descendants).
780    pub fn remove_node_if_unparented_with(
781        &mut self,
782        node_id: NodeId,
783        on_drop: &mut dyn FnMut(NodeId),
784    ) {
785        if let Some(node) = self.doc.get_node(node_id) {
786            if node.parent.is_none() {
787                self.remove_and_drop_node_with(node_id, on_drop);
788            }
789        }
790    }
791
792    /// Remove all of the children from old_parent_id and append them to new_parent_id
793    pub fn append_children(&mut self, parent_id: NodeId, child_ids: &[NodeId]) {
794        self.add_children_to_parent(parent_id, child_ids, &|parent, child_ids| {
795            parent.children.extend_from_slice(child_ids);
796        });
797    }
798
799    pub fn insert_nodes_before(&mut self, anchor_node_id: NodeId, new_node_ids: &[NodeId]) {
800        let parent_id = self.doc.nodes[anchor_node_id].parent.unwrap();
801        self.add_children_to_parent(parent_id, new_node_ids, &|parent, child_ids| {
802            let node_child_idx = parent.index_of_child(anchor_node_id).unwrap();
803            parent
804                .children
805                .splice(node_child_idx..node_child_idx, child_ids.iter().copied());
806        });
807    }
808
809    fn add_children_to_parent(
810        &mut self,
811        parent_id: NodeId,
812        child_ids: &[NodeId],
813        insert_children_fn: &dyn Fn(&mut Node, &[NodeId]),
814    ) {
815        if self.previous_focus_path.is_none()
816            && self.doc.focus_node_id.is_some()
817            && child_ids.iter().any(|id| {
818                self.doc.nodes[*id]
819                    .parent
820                    .is_some_and(|old| old != parent_id)
821            })
822        {
823            let path = self
824                .doc
825                .maybe_node_element_ancestors(self.doc.focus_node_id);
826            if path.iter().any(|id| child_ids.contains(id)) {
827                self.previous_focus_path = Some(path);
828            }
829        }
830        let new_parent_is_in_document = self.doc.nodes[parent_id].flags.is_in_document();
831        self.mutations_occurred |= new_parent_is_in_document && !child_ids.is_empty();
832        // Detach the children from their old parents *before* inserting them into
833        // the new parent (matching DOM `insertBefore` semantics). If a child is
834        // being moved within the same parent then detaching it after insertion
835        // would remove both the old and the newly-inserted entries from the
836        // parent's child list, and anchor indices would be computed against a
837        // child list that still contains the moved nodes.
838        for child_id in child_ids.iter().copied() {
839            let child = &self.doc.nodes[child_id];
840            if child.parent.is_some()
841                || child.layout_parent.get().is_some()
842                || child.oof_containing_block.get().is_some()
843            {
844                self.doc.retire_moved_layout_references(child_id);
845            }
846            let child = &mut self.doc.nodes[child_id];
847            let child_was_in_doc = child.flags.is_in_document();
848            self.mutations_occurred |= child_was_in_doc;
849            let Some(old_parent_id) = child.parent.take() else {
850                continue;
851            };
852
853            let old_parent = &mut self.doc.nodes[old_parent_id];
854            old_parent.insert_damage(ALL_DAMAGE);
855
856            // TODO: make this fine grained / conditional based on ElementSelectorFlags
857            if child_was_in_doc {
858                if let Some(mut data) = old_parent
859                    .try_stylo_element_data_mut()
860                    .and_then(|s| s.get_mut())
861                {
862                    data.hint |= RestyleHint::restyle_subtree();
863                }
864                // Mark ancestors dirty so the style traversal visits this subtree.
865                old_parent.mark_ancestors_dirty();
866            }
867
868            old_parent.children.retain(|id| *id != child_id);
869            self.maybe_record_node(old_parent_id);
870        }
871
872        let new_parent = &mut self.doc.nodes[parent_id];
873        new_parent.insert_damage(ALL_DAMAGE);
874
875        // TODO: make this fine grained / conditional based on ElementSelectorFlags
876        if new_parent_is_in_document {
877            if let Some(mut data) = new_parent
878                .try_stylo_element_data_mut()
879                .and_then(|s| s.get_mut())
880            {
881                data.hint |= RestyleHint::restyle_subtree();
882            }
883            // Mark ancestors dirty so the style traversal visits this subtree.
884            new_parent.mark_ancestors_dirty();
885        }
886
887        insert_children_fn(new_parent, child_ids);
888
889        for child_id in child_ids.iter().copied() {
890            let child = &mut self.doc.nodes[child_id];
891            let child_was_in_doc = child.flags.is_in_document();
892            child.parent = Some(parent_id);
893
894            if new_parent_is_in_document && !child_was_in_doc {
895                self.process_added_subtree(child_id);
896            } else if !new_parent_is_in_document && child_was_in_doc {
897                self.process_removed_subtree(child_id);
898            }
899        }
900
901        self.doc.reconcile_radio_subtrees(child_ids);
902
903        self.maybe_record_node(parent_id);
904    }
905
906    // Tree mutation methods (that defer to other methods)
907    pub fn insert_nodes_after(&mut self, anchor_node_id: NodeId, new_node_ids: &[NodeId]) {
908        match self.next_sibling_id(anchor_node_id) {
909            Some(id) => self.insert_nodes_before(id, new_node_ids),
910            None => {
911                let parent_id = self.parent_id(anchor_node_id).unwrap();
912                self.append_children(parent_id, new_node_ids)
913            }
914        }
915    }
916
917    pub fn reparent_children(&mut self, old_parent_id: NodeId, new_parent_id: NodeId) {
918        let child_ids = std::mem::take(&mut self.doc.nodes[old_parent_id].children);
919        self.maybe_record_node(old_parent_id);
920        self.append_children(new_parent_id, &child_ids);
921    }
922
923    pub fn replace_node_with(&mut self, anchor_node_id: NodeId, new_node_ids: &[NodeId]) {
924        self.insert_nodes_before(anchor_node_id, new_node_ids);
925        self.remove_node(anchor_node_id);
926    }
927
928    // === ParentNode / ChildNode mixin mutation methods ===
929    //
930    // These implement the DOM spec's mutation semantics for the ParentNode and
931    // ChildNode mixins (`append`/`prepend`/`replaceChildren` and
932    // `before`/`after`/`replaceWith`). Callers pass lists of node ids with
933    // string arguments already converted to (detached) text nodes and
934    // DocumentFragment arguments already expanded into their children (per the
935    // DOM spec's "insert a node" steps).
936
937    /// Detach (rather than drop) any already-parented nodes, so that references
938    /// to them (and their descendants) remain valid
939    fn detach_all(&mut self, node_ids: &[NodeId]) {
940        for node_id in node_ids {
941            if self.node_has_parent(*node_id) {
942                self.remove_node(*node_id);
943            }
944        }
945    }
946
947    /// The nearest sibling of `anchor_id` (in the direction given by `offset`:
948    /// +1 = following, -1 = preceding) which is not in `excluded` — the spec's
949    /// "viable next/previous sibling" for ChildNode mutation methods
950    fn viable_sibling(
951        &self,
952        anchor_id: NodeId,
953        offset: isize,
954        excluded: &[NodeId],
955    ) -> Option<NodeId> {
956        let node = self.doc.get_node(anchor_id)?;
957        let parent = self.doc.get_node(node.parent?)?;
958        let mut index = parent.index_of_child(anchor_id)?;
959        loop {
960            index = index.checked_add_signed(offset)?;
961            let sibling_id = *parent.children.get(index)?;
962            if !excluded.contains(&sibling_id) {
963                return Some(sibling_id);
964            }
965        }
966    }
967
968    /// ParentNode's `prepend()`: insert the nodes at the start of `parent_id`'s
969    /// children (ParentNode's `append()` is [`append_children`](Self::append_children))
970    pub fn prepend_nodes(&mut self, parent_id: NodeId, node_ids: &[NodeId]) {
971        self.detach_all(node_ids);
972        match self.child_ids(parent_id).first().copied() {
973            Some(first_child_id) => self.insert_nodes_before(first_child_id, node_ids),
974            None => self.append_children(parent_id, node_ids),
975        }
976    }
977
978    /// ParentNode's `replaceChildren()`: replace all of `parent_id`'s children
979    /// with the given nodes. The existing children are detached rather than
980    /// dropped, so that references to them remain valid.
981    pub fn replace_children(&mut self, parent_id: NodeId, node_ids: &[NodeId]) {
982        self.detach_all(node_ids);
983        for child_id in self.child_ids(parent_id) {
984            self.remove_node(child_id);
985        }
986        self.append_children(parent_id, node_ids);
987    }
988
989    /// ChildNode's `before()`: insert the nodes before `anchor_id`. The
990    /// insertion point is after the nearest preceding sibling which isn't
991    /// itself being inserted (or at the start of the parent if there is none).
992    /// Does nothing if the anchor has no parent.
993    pub fn before_node(&mut self, anchor_id: NodeId, node_ids: &[NodeId]) {
994        let Some(parent_id) = self.parent_id(anchor_id) else {
995            return;
996        };
997        let viable_prev = self.viable_sibling(anchor_id, -1, node_ids);
998        self.detach_all(node_ids);
999        match viable_prev {
1000            Some(prev_id) => self.insert_nodes_after(prev_id, node_ids),
1001            None => match self.child_ids(parent_id).first().copied() {
1002                Some(first_child_id) => self.insert_nodes_before(first_child_id, node_ids),
1003                None => self.append_children(parent_id, node_ids),
1004            },
1005        }
1006    }
1007
1008    /// ChildNode's `after()`: insert the nodes after `anchor_id`. The insertion
1009    /// point is before the nearest following sibling which isn't itself being
1010    /// inserted (or at the end of the parent if there is none). Does nothing if
1011    /// the anchor has no parent.
1012    pub fn after_node(&mut self, anchor_id: NodeId, node_ids: &[NodeId]) {
1013        let Some(parent_id) = self.parent_id(anchor_id) else {
1014            return;
1015        };
1016        let viable_next = self.viable_sibling(anchor_id, 1, node_ids);
1017        self.detach_all(node_ids);
1018        match viable_next {
1019            Some(next_id) => self.insert_nodes_before(next_id, node_ids),
1020            None => self.append_children(parent_id, node_ids),
1021        }
1022    }
1023
1024    /// ChildNode's `replaceWith()`: replace `anchor_id` with the nodes in its
1025    /// parent's child list. If the anchor is itself one of the inserted nodes,
1026    /// they are inserted at its old position (the spec's "viable next sibling"
1027    /// handling). The anchor is detached rather than dropped, so references to
1028    /// it remain valid. Does nothing if the anchor has no parent.
1029    pub fn replace_with_nodes(&mut self, anchor_id: NodeId, node_ids: &[NodeId]) {
1030        let Some(parent_id) = self.parent_id(anchor_id) else {
1031            return;
1032        };
1033        let viable_next = self.viable_sibling(anchor_id, 1, node_ids);
1034        self.detach_all(node_ids);
1035        if self.node_has_parent(anchor_id) {
1036            self.replace_node_with(anchor_id, node_ids);
1037        } else {
1038            match viable_next {
1039                Some(next_id) => self.insert_nodes_before(next_id, node_ids),
1040                None => self.append_children(parent_id, node_ids),
1041            }
1042        }
1043    }
1044}
1045
1046impl<'doc> DocumentMutator<'doc> {
1047    pub fn flush(&mut self) {
1048        if let Some(old) = self.previous_focus_path.take() {
1049            let new = self
1050                .doc
1051                .maybe_node_element_ancestors(self.doc.focus_node_id);
1052            self.doc.update_focus_within_paths(old, new);
1053        }
1054        if self.recompute_is_animating {
1055            self.doc.has_canvas = self.doc.compute_has_canvas();
1056        }
1057
1058        if let Some(id) = self.title_node {
1059            let title = self.doc.nodes[id].text_content();
1060            self.doc.shell_provider.set_window_title(title);
1061        }
1062
1063        // Add/Update inline stylesheets (<style> elements)
1064        for id in self.style_nodes.drain() {
1065            self.doc.process_style_element(id);
1066        }
1067
1068        for id in self.form_nodes.drain() {
1069            self.doc.reset_form_owner(id);
1070            self.doc.sync_placeholder_state(id);
1071        }
1072
1073        #[cfg(feature = "autofocus")]
1074        if let Some(node_id) = self.node_to_autofocus.take() {
1075            if self.doc.get_node(node_id).is_some() {
1076                self.doc.set_focus_to(node_id);
1077            }
1078        }
1079    }
1080
1081    pub fn set_inner_html(&mut self, node_id: NodeId, html: &str) {
1082        self.remove_and_drop_all_children(node_id);
1083        self.doc
1084            .html_parser_provider
1085            .clone()
1086            .parse_inner_html(self, node_id, html);
1087    }
1088
1089    fn flush_eager_ops(&mut self) {
1090        let mut ops = mem::take(&mut self.eager_op_queue);
1091        for op in ops.drain(0..) {
1092            match op {
1093                SpecialOp::LoadImage(node_id) => self.load_image(node_id),
1094                SpecialOp::LoadIframe(node_id) => self.load_iframe(node_id),
1095                SpecialOp::LoadStylesheet(node_id) => self.load_linked_stylesheet(node_id),
1096                SpecialOp::UnloadStylesheet(node_id) => self.unload_stylesheet(node_id),
1097                SpecialOp::LoadCustomPaintSource(node_id) => self.load_custom_paint_src(node_id),
1098                SpecialOp::ProcessButtonInput(node_id) => self.process_button_input(node_id),
1099                SpecialOp::UnloadSubDocument(node_id) => self.remove_sub_document(node_id),
1100                #[cfg(feature = "custom-widget")]
1101                SpecialOp::UnloadCustomWidget(node_id) => self.remove_custom_widget(node_id),
1102            }
1103        }
1104
1105        // Queue is empty, but put Vec back anyway so allocation can be reused.
1106        self.eager_op_queue = ops;
1107    }
1108
1109    fn process_added_subtree(&mut self, node_id: NodeId) {
1110        self.doc.iter_subtree_mut(node_id, |node_id, doc| {
1111            let node = &mut doc.nodes[node_id];
1112            node.flags.set(NodeFlags::IS_IN_DOCUMENT, true);
1113            node.insert_damage(ALL_DAMAGE);
1114
1115            // If the node has an "id" attribute, store it in the ID map.
1116            if let Some(id_attr) = node.attr(local_name!("id")).map(ToString::to_string) {
1117                doc.add_to_id_map(&id_attr, node_id);
1118            }
1119
1120            let node = &mut doc.nodes[node_id];
1121            let NodeData::Element(ref mut element) = node.data else {
1122                return;
1123            };
1124
1125            // Resume may keep identical computed style, so the style diff does
1126            // not schedule image layers again. Revisit cancelled layer loads.
1127            if element
1128                .background_images
1129                .iter()
1130                .chain(&element.mask_images)
1131                .flatten()
1132                .any(|image| image.status == crate::node::Status::Error)
1133            {
1134                doc.pending_style_image_nodes.push(node_id);
1135            }
1136
1137            // Custom widgets replace the element's built-in tag-specific behaviour
1138            #[cfg(feature = "custom-widget")]
1139            let has_custom_widget = element.custom_widget_data().is_some();
1140            #[cfg(not(feature = "custom-widget"))]
1141            let has_custom_widget = false;
1142
1143            // Custom post-processing by element tag name
1144            let tag = element.name.local.as_ref();
1145            match tag {
1146                "title" => self.title_node = Some(node_id),
1147                "style" => {
1148                    self.style_nodes.insert(node_id);
1149                }
1150                _ if has_custom_widget => {}
1151                "link" => self.eager_op_queue.push(SpecialOp::LoadStylesheet(node_id)),
1152                "img" => self.eager_op_queue.push(SpecialOp::LoadImage(node_id)),
1153                "iframe" => self.eager_op_queue.push(SpecialOp::LoadIframe(node_id)),
1154                "canvas" => self
1155                    .eager_op_queue
1156                    .push(SpecialOp::LoadCustomPaintSource(node_id)),
1157                "button" | "fieldset" | "input" | "select" | "textarea" | "object" | "output" => {
1158                    self.eager_op_queue
1159                        .push(SpecialOp::ProcessButtonInput(node_id));
1160                    self.form_nodes.insert(node_id);
1161                }
1162                _ => {}
1163            }
1164
1165            #[cfg(feature = "autofocus")]
1166            if node.is_focussable() {
1167                if let NodeData::Element(ref element) = node.data {
1168                    // `autofocus` is a boolean attribute, so presence alone enables it
1169                    // (`<input autofocus>`). "false" is still ignored, because Dioxus
1170                    // writes `autofocus: false` as the string "false".
1171                    if let Some(value) = element.attr(local_name!("autofocus")) {
1172                        if value != "false" {
1173                            self.node_to_autofocus = Some(node_id);
1174                        }
1175                    }
1176                }
1177            }
1178        });
1179
1180        self.flush_eager_ops();
1181    }
1182
1183    fn process_removed_subtree(&mut self, node_id: NodeId) {
1184        self.doc.iter_subtree_mut(node_id, |node_id, doc| {
1185            doc.retire_top_layer(node_id);
1186            doc.nodes[node_id]
1187                .flags
1188                .set(NodeFlags::IS_IN_DOCUMENT, false);
1189
1190            // Clear any interaction state that references this node, running
1191            // the usual teardown steps (unhover/unactive the surviving
1192            // ancestor chain, IME disable on blur of a focused input).
1193            doc.clear_interaction_state_for_removed_node(node_id);
1194
1195            let node = &mut doc.nodes[node_id];
1196
1197            // Clear the text selection if one of its endpoints references this node.
1198            // This prevents stale selection endpoint references.
1199            if doc.text_selection.anchor.node_or_parent == Some(node_id)
1200                || doc.text_selection.focus.node_or_parent == Some(node_id)
1201            {
1202                doc.text_selection.clear();
1203            }
1204
1205            // Remove any snapshot for this node to prevent stale snapshot references
1206            // during style invalidation.
1207            if node.has_snapshot() {
1208                let opaque_id = style::dom::TNode::opaque(&&*node);
1209                doc.snapshots.remove(&opaque_id);
1210                node.set_has_snapshot(false);
1211            }
1212
1213            // If the node has an "id" attribute remove it from the ID map.
1214            if let Some(id_attr) = node.attr(local_name!("id")).map(ToString::to_string) {
1215                doc.remove_from_id_map(&id_attr, node_id);
1216            }
1217
1218            let node = &mut doc.nodes[node_id];
1219            let NodeData::Element(ref mut element) = node.data else {
1220                return;
1221            };
1222
1223            match &element.special_data {
1224                SpecialElementData::SubDocument(_) => {
1225                    self.eager_op_queue
1226                        .push(SpecialOp::UnloadSubDocument(node_id));
1227                }
1228                #[cfg(feature = "custom-widget")]
1229                SpecialElementData::CustomWidget(_) => {
1230                    self.eager_op_queue
1231                        .push(SpecialOp::UnloadCustomWidget(node_id));
1232                }
1233                SpecialElementData::Stylesheet(_) => self
1234                    .eager_op_queue
1235                    .push(SpecialOp::UnloadStylesheet(node_id)),
1236                SpecialElementData::Image(_) => {}
1237                SpecialElementData::Canvas(_) => {
1238                    self.recompute_is_animating = true;
1239                }
1240                SpecialElementData::TableRoot(_) => {}
1241                SpecialElementData::TextInput(_) => {}
1242                SpecialElementData::CheckboxInput(_) => {}
1243                #[cfg(feature = "file-input")]
1244                SpecialElementData::FileInput(_) => {}
1245                SpecialElementData::None => {}
1246            }
1247        });
1248
1249        self.flush_eager_ops();
1250    }
1251
1252    fn maybe_record_node(&mut self, node_id: impl Into<Option<NodeId>>) {
1253        let Some(node_id) = node_id.into() else {
1254            return;
1255        };
1256
1257        let Some(tag_name) = self.doc.nodes[node_id]
1258            .data
1259            .downcast_element()
1260            .map(|elem| &elem.name.local)
1261        else {
1262            return;
1263        };
1264
1265        match tag_name.as_ref() {
1266            "title" => self.title_node = Some(node_id),
1267            "style" => {
1268                self.style_nodes.insert(node_id);
1269            }
1270            _ => {}
1271        }
1272    }
1273
1274    fn load_linked_stylesheet(&mut self, target_id: NodeId) {
1275        let node = &self.doc.nodes[target_id];
1276
1277        let mut is_in_head = false;
1278        let mut parent_id = node.parent;
1279        while let Some(id) = parent_id
1280            && !is_in_head
1281        {
1282            let parent = &self.doc.nodes[id];
1283            is_in_head |= parent.data.is_element_with_tag_name(&local_name!("head"));
1284            parent_id = parent.parent;
1285        }
1286
1287        let rel_attr = node.attr(local_name!("rel"));
1288        let href_attr = node.attr(local_name!("href"));
1289
1290        let (Some(rels), Some(href)) = (rel_attr, href_attr) else {
1291            return;
1292        };
1293        if !rels.split_ascii_whitespace().any(|rel| rel == "stylesheet") {
1294            return;
1295        }
1296
1297        let url = self.doc.resolve_url(href);
1298        let Some(handler) = ResourceHandler::new(
1299            self.doc.tx.clone(),
1300            self.doc.id(),
1301            Some(node.id),
1302            self.doc.shell_provider.clone(),
1303            StylesheetHandler {
1304                guard: self.doc.guard.clone(),
1305                net_provider: self.doc.net_provider.clone(),
1306                abort_signal: self.doc.abort_signal.clone(),
1307            },
1308        ) else {
1309            return;
1310        };
1311
1312        if is_in_head && !self.doc.net_provider.is_noop() {
1313            self.doc
1314                .pending_critical_resources
1315                .insert(handler.request_id());
1316        }
1317
1318        self.doc.net_provider.fetch(
1319            self.doc.id(),
1320            self.doc.build_request(url),
1321            Box::new(handler),
1322        );
1323    }
1324
1325    fn unload_stylesheet(&mut self, node_id: NodeId) {
1326        let node = &mut self.doc.nodes[node_id];
1327        let Some(element) = node.element_data_mut() else {
1328            unreachable!();
1329        };
1330        let SpecialElementData::Stylesheet(stylesheet) = element.special_data.take() else {
1331            unreachable!();
1332        };
1333
1334        let guard = self.doc.guard.read();
1335        self.doc.stylist.remove_stylesheet(stylesheet, &guard);
1336        self.doc
1337            .stylist
1338            .force_stylesheet_origins_dirty(OriginSet::all());
1339
1340        self.doc.nodes_to_stylesheet.remove(&node_id);
1341        self.doc.stylesheet_generation += 1;
1342    }
1343
1344    fn load_image(&mut self, target_id: NodeId) {
1345        let Some(request) = self.doc.image_observations.begin(target_id) else {
1346            self.doc.resource_counters.rejected =
1347                self.doc.resource_counters.rejected.saturating_add(1);
1348            return;
1349        };
1350        // Retire obsolete consumers before a same-URL replacement can join its fetch.
1351        self.doc.cancel_replaced_image_consumers(target_id);
1352        if self.doc.net_provider.is_noop() {
1353            self.doc.image_observations.complete(
1354                target_id,
1355                request,
1356                crate::ImageLoadStatus::Failed,
1357            );
1358            return;
1359        }
1360        let node = &self.doc.nodes[target_id];
1361        if let Some(raw_src) = node.attr(local_name!("src")) {
1362            if !raw_src.is_empty() {
1363                let Some(src) = self.doc.url.resolve_relative(raw_src) else {
1364                    self.doc.resource_counters.failures =
1365                        self.doc.resource_counters.failures.saturating_add(1);
1366                    self.doc.image_observations.complete(
1367                        target_id,
1368                        request,
1369                        crate::ImageLoadStatus::Failed,
1370                    );
1371                    return;
1372                };
1373                let src_string = src.as_str();
1374
1375                // Check cache first
1376                if let Some(cached_image) = self.doc.image_cache.get(src_string) {
1377                    #[cfg(feature = "tracing")]
1378                    tracing::info!("Loading image {src_string} from cache");
1379                    let node = &mut self.doc.nodes[target_id];
1380                    node.element_data_mut().unwrap().special_data =
1381                        SpecialElementData::Image(Box::new(cached_image.clone()));
1382                    node.clear_layout_cache();
1383                    node.insert_damage(ALL_DAMAGE);
1384                    self.doc.image_observations.complete(
1385                        target_id,
1386                        request,
1387                        crate::ImageLoadStatus::Loaded,
1388                    );
1389                    return;
1390                }
1391
1392                // Check if there's already a pending request for this URL
1393                if let Some(waiting_list) = self.doc.pending_images.get_mut(src_string) {
1394                    #[cfg(feature = "tracing")]
1395                    tracing::info!("Image {src_string} already pending, queueing node {target_id}");
1396                    if waiting_list.waiters.len() < self.doc.resource_limits.image_waiters {
1397                        waiting_list
1398                            .waiters
1399                            .push((target_id, ImageType::Image, Some(request)));
1400                    } else {
1401                        self.doc.resource_counters.rejected =
1402                            self.doc.resource_counters.rejected.saturating_add(1);
1403                        self.doc.image_observations.complete(
1404                            target_id,
1405                            request,
1406                            crate::ImageLoadStatus::Failed,
1407                        );
1408                    }
1409                    return;
1410                }
1411
1412                // Start fetch and track as pending
1413                #[cfg(feature = "tracing")]
1414                tracing::info!("Fetching image {src_string}");
1415                if self.doc.pending_images.len() >= self.doc.resource_limits.pending_images
1416                    || self.doc.resource_limits.image_waiters == 0
1417                {
1418                    self.doc.resource_counters.rejected =
1419                        self.doc.resource_counters.rejected.saturating_add(1);
1420                    self.doc.image_observations.complete(
1421                        target_id,
1422                        request,
1423                        crate::ImageLoadStatus::Failed,
1424                    );
1425                    return;
1426                }
1427                let Some(handler) = ResourceHandler::new(
1428                    self.doc.tx.clone(),
1429                    self.doc.id(),
1430                    None,
1431                    self.doc.shell_provider.clone(),
1432                    ImageHandler::with_limits(ImageType::Image, self.doc.resource_limits),
1433                ) else {
1434                    self.doc.image_observations.complete(
1435                        target_id,
1436                        request,
1437                        crate::ImageLoadStatus::Failed,
1438                    );
1439                    return;
1440                };
1441                self.doc.pending_images.insert(
1442                    src_string.to_string(),
1443                    crate::resources::PendingImage {
1444                        request_id: handler.request_id(),
1445                        waiters: vec![(target_id, ImageType::Image, Some(request))],
1446                    },
1447                );
1448
1449                self.doc.net_provider.fetch(
1450                    self.doc.id(),
1451                    self.doc.build_request(src),
1452                    Box::new(handler),
1453                );
1454                return;
1455            }
1456        }
1457        self.doc
1458            .image_observations
1459            .complete(target_id, request, crate::ImageLoadStatus::Failed);
1460    }
1461
1462    fn load_iframe(&mut self, target_id: NodeId) {
1463        if self.doc.subdocument_depth >= crate::iframe::MAX_SUBDOCUMENT_DEPTH {
1464            #[cfg(feature = "tracing")]
1465            tracing::warn!(
1466                "Not loading iframe: max sub-document nesting depth ({}) reached",
1467                crate::iframe::MAX_SUBDOCUMENT_DEPTH
1468            );
1469            return;
1470        }
1471
1472        let node = &self.doc.nodes[target_id];
1473        let Some(element) = node.element_data() else {
1474            return;
1475        };
1476
1477        // `srcdoc` takes precedence over `src`
1478        if let Some(srcdoc) = element.attr(local_name!("srcdoc")) {
1479            let srcdoc = srcdoc.to_string();
1480            self.doc.load_iframe_srcdoc(target_id, &srcdoc);
1481            return;
1482        }
1483
1484        let Some(raw_src) = element.attr(local_name!("src")) else {
1485            return;
1486        };
1487        if raw_src.is_empty() {
1488            return;
1489        }
1490        let Some(url) = self.doc.url.resolve_relative(raw_src) else {
1491            #[cfg(feature = "tracing")]
1492            tracing::warn!("Not loading iframe: could not resolve url {raw_src}");
1493            return;
1494        };
1495        self.doc.start_iframe_load(target_id, url);
1496    }
1497
1498    fn load_custom_paint_src(&mut self, target_id: NodeId) {
1499        let node = &mut self.doc.nodes[target_id];
1500        if let Some(raw_src) = node.attr(local_name!("src")) {
1501            if let Ok(custom_paint_source_id) = raw_src.parse::<u64>() {
1502                self.recompute_is_animating = true;
1503                let canvas_data = SpecialElementData::Canvas(CanvasData {
1504                    custom_paint_source_id,
1505                });
1506                node.element_data_mut().unwrap().special_data = canvas_data;
1507            }
1508        }
1509    }
1510
1511    fn process_button_input(&mut self, target_id: NodeId) {
1512        let node = &self.doc.nodes[target_id];
1513        let Some(data) = node.element_data() else {
1514            return;
1515        };
1516
1517        let tagname = data.name.local.as_ref();
1518        let type_attr = data.attr(local_name!("type"));
1519        let value = data.attr(local_name!("value"));
1520
1521        // Add content of "value" attribute as a text node child if:
1522        //   - Tag name is
1523        if let ("input", Some("button" | "submit" | "reset"), Some(value)) =
1524            (tagname, type_attr, value)
1525        {
1526            let value = value.to_string();
1527            let id = self.create_text_node(&value);
1528            self.append_children(target_id, &[id]);
1529            return;
1530        }
1531        #[cfg(feature = "file-input")]
1532        if let ("input", Some("file")) = (tagname, type_attr) {
1533            let button_id = self.create_element(
1534                qual_name!("button", html),
1535                vec![
1536                    Attribute {
1537                        name: qual_name!("type", html),
1538                        value: "button".to_string(),
1539                    },
1540                    Attribute {
1541                        name: qual_name!("tabindex", html),
1542                        value: "-1".to_string(),
1543                    },
1544                ],
1545            );
1546            let label_id = self.create_element(qual_name!("label", html), vec![]);
1547            let text_id = self.create_text_node("No File Selected");
1548            let button_text_id = self.create_text_node("Browse");
1549            self.append_children(target_id, &[button_id, label_id]);
1550            self.append_children(label_id, &[text_id]);
1551            self.append_children(button_id, &[button_text_id]);
1552        }
1553    }
1554}
1555
1556/// Set 'checked' state on an input based on given attributevalue
1557fn set_input_checked_state(element: &mut ElementData, value: String) {
1558    let Ok(checked) = value.parse() else {
1559        return;
1560    };
1561    match element.special_data {
1562        SpecialElementData::CheckboxInput(_) => element.set_checkbox_input_checked(checked),
1563        // If we have just constructed the element, set the node attribute,
1564        // and NodeSpecificData will be created from that later
1565        // this simulates the checked attribute being set in html,
1566        // and the element's checked property being set from that
1567        SpecialElementData::None => element.attrs.push(Attribute {
1568            name: qual_name!("checked", html),
1569            value: checked.to_string(),
1570        }),
1571        _ => {}
1572    }
1573}
1574
1575/// Type that allows mutable access to the viewport
1576/// And syncs it back to stylist on drop.
1577pub struct ViewportMut<'doc> {
1578    doc: &'doc mut BaseDocument,
1579    initial_viewport: Viewport,
1580}
1581impl ViewportMut<'_> {
1582    pub fn new(doc: &mut BaseDocument) -> ViewportMut<'_> {
1583        let initial_viewport = doc.viewport.clone();
1584        ViewportMut {
1585            doc,
1586            initial_viewport,
1587        }
1588    }
1589}
1590impl Deref for ViewportMut<'_> {
1591    type Target = Viewport;
1592
1593    fn deref(&self) -> &Self::Target {
1594        &self.doc.viewport
1595    }
1596}
1597impl DerefMut for ViewportMut<'_> {
1598    fn deref_mut(&mut self) -> &mut Self::Target {
1599        &mut self.doc.viewport
1600    }
1601}
1602impl Drop for ViewportMut<'_> {
1603    fn drop(&mut self) {
1604        if self.doc.viewport == self.initial_viewport {
1605            return;
1606        }
1607
1608        let changes = DeviceChanges::from_viewports(&self.initial_viewport, &self.doc.viewport);
1609        self.doc.queue_device_changes(changes);
1610    }
1611}
1612
1613#[cfg(test)]
1614mod test {
1615    use style::media_queries::MediaType;
1616    use style::servo_arc::Arc as ServoArc;
1617    use style_dom::ElementState;
1618
1619    use std::sync::{
1620        Arc,
1621        atomic::{AtomicUsize, Ordering},
1622    };
1623
1624    use apace_traits::shell::{ColorScheme, ShellProvider, Viewport};
1625
1626    use crate::{Attribute, BaseDocument, DocumentConfig, ElementData, NodeData, qual_name};
1627
1628    #[test]
1629    fn media_type_defaults_to_screen() {
1630        let mut document = BaseDocument::new(DocumentConfig::default());
1631        assert_eq!(*document.media_type(), MediaType::screen());
1632        assert_eq!(document.stylist_device().media_type(), MediaType::screen());
1633    }
1634
1635    #[test]
1636    fn media_type_honors_config() {
1637        let mut document = BaseDocument::new(DocumentConfig {
1638            media_type: Some(MediaType::print()),
1639            ..Default::default()
1640        });
1641        assert_eq!(*document.media_type(), MediaType::print());
1642        assert_eq!(document.stylist_device().media_type(), MediaType::print());
1643    }
1644
1645    #[test]
1646    fn set_media_type_updates_stylist_device() {
1647        let mut document = BaseDocument::new(DocumentConfig::default());
1648        assert_eq!(document.stylist_device().media_type(), MediaType::screen());
1649
1650        document.set_media_type(MediaType::print());
1651        assert_eq!(*document.media_type(), MediaType::print());
1652        assert_eq!(document.stylist_device().media_type(), MediaType::print());
1653    }
1654
1655    #[test]
1656    fn mutator_remove_disabled() {
1657        let mut document = BaseDocument::new(DocumentConfig::default());
1658        let id = document.create_node(NodeData::Element(Box::new(ElementData::new(
1659            qual_name!("button"),
1660            vec![Attribute {
1661                name: qual_name!("disabled"),
1662                value: "".into(),
1663            }],
1664        ))));
1665
1666        let node = document.get_node(id).unwrap();
1667        assert!(
1668            node.element_state().contains(ElementState::DISABLED),
1669            "form node is disabled"
1670        );
1671        assert!(
1672            !node.element_state().contains(ElementState::ENABLED),
1673            "form node is not enabled yet"
1674        );
1675
1676        let mut mutator = document.mutate();
1677        mutator.clear_attribute(id, qual_name!("disabled"));
1678        drop(mutator);
1679
1680        let node = document.get_node(id).unwrap();
1681        assert!(
1682            !node.element_state().contains(ElementState::DISABLED),
1683            "form node is no longer disabled"
1684        );
1685        assert!(
1686            node.element_state().contains(ElementState::ENABLED),
1687            "form node is enabled"
1688        );
1689    }
1690
1691    #[test]
1692    fn mutator_set_disabled() {
1693        let mut document = BaseDocument::new(DocumentConfig::default());
1694        let id = document.create_node(NodeData::Element(Box::new(ElementData::new(
1695            qual_name!("button"),
1696            vec![],
1697        ))));
1698
1699        let node = document.get_node(id).unwrap();
1700        assert!(
1701            !node.element_state().contains(ElementState::DISABLED),
1702            "form node is not disabled"
1703        );
1704        assert!(
1705            node.element_state().contains(ElementState::ENABLED),
1706            "form node is enabled"
1707        );
1708
1709        let mut mutator = document.mutate();
1710        mutator.set_attribute(id, qual_name!("disabled"), "");
1711        drop(mutator);
1712
1713        let node = document.get_node(id).unwrap();
1714
1715        assert!(
1716            node.element_state().contains(ElementState::DISABLED),
1717            "form node is disabled"
1718        );
1719        assert!(
1720            !node.element_state().contains(ElementState::ENABLED),
1721            "form node is no longer enabled enabled"
1722        );
1723    }
1724
1725    #[test]
1726    fn mutator_set_disabled_invalid_node() {
1727        let mut document = BaseDocument::new(DocumentConfig::default());
1728        let id = document.create_node(NodeData::Element(Box::new(ElementData::new(
1729            qual_name!("a"),
1730            vec![],
1731        ))));
1732
1733        let node = document.get_node(id).unwrap();
1734        assert!(
1735            !node.element_state().contains(ElementState::DISABLED),
1736            "form node is not disabled"
1737        );
1738        assert!(
1739            !node.element_state().contains(ElementState::ENABLED),
1740            "form node is enabled"
1741        );
1742
1743        let mut mutator = document.mutate();
1744        mutator.set_attribute(id, qual_name!("disabled"), "");
1745        drop(mutator);
1746
1747        let node = document.get_node(id).unwrap();
1748        assert!(
1749            !node.element_state().contains(ElementState::DISABLED),
1750            "form node is not disabled"
1751        );
1752        assert!(
1753            !node.element_state().contains(ElementState::ENABLED),
1754            "form node is enabled"
1755        );
1756    }
1757
1758    #[test]
1759    fn mutator_id_attribute_updates_id_map() {
1760        let mut document = BaseDocument::new(DocumentConfig::default());
1761        let root_id = document.root_node().id;
1762
1763        let node_id = {
1764            let mut mutator = document.mutate();
1765            let node_id = mutator.create_element(
1766                qual_name!("div"),
1767                vec![Attribute {
1768                    name: qual_name!("id"),
1769                    value: "old".into(),
1770                }],
1771            );
1772            mutator.append_children(root_id, &[node_id]);
1773            node_id
1774        };
1775        assert_eq!(document.get_element_by_id("old"), Some(node_id));
1776
1777        {
1778            let mut mutator = document.mutate();
1779            mutator.set_attribute(node_id, qual_name!("id"), "new");
1780        }
1781        assert_eq!(document.get_element_by_id("new"), Some(node_id));
1782        assert_eq!(document.get_element_by_id("old"), None);
1783
1784        {
1785            let mut mutator = document.mutate();
1786            mutator.clear_attribute(node_id, qual_name!("id"));
1787        }
1788        assert_eq!(document.get_element_by_id("new"), None);
1789    }
1790
1791    #[test]
1792    fn get_element_by_id_duplicate_ids_first_in_tree_order_wins() {
1793        let mut document = BaseDocument::new(DocumentConfig::default());
1794        let root_id = document.root_node().id;
1795
1796        let (first_id, second_id) = {
1797            let mut mutator = document.mutate();
1798            let first_id = mutator.create_element(qual_name!("div"), vec![]);
1799            let second_id = mutator.create_element(qual_name!("div"), vec![]);
1800            mutator.append_children(root_id, &[first_id, second_id]);
1801            // Assign the id to the later node first so that insertion order
1802            // differs from tree order
1803            mutator.set_attribute(second_id, qual_name!("id"), "dup");
1804            mutator.set_attribute(first_id, qual_name!("id"), "dup");
1805            (first_id, second_id)
1806        };
1807        assert_eq!(document.get_element_by_id("dup"), Some(first_id));
1808
1809        {
1810            let mut mutator = document.mutate();
1811            mutator.remove_node(first_id);
1812        }
1813        assert_eq!(document.get_element_by_id("dup"), Some(second_id));
1814    }
1815
1816    #[derive(Default)]
1817    struct RedrawShell {
1818        redraw_requests: AtomicUsize,
1819    }
1820
1821    impl ShellProvider for RedrawShell {
1822        fn request_redraw(&self) {
1823            self.redraw_requests.fetch_add(1, Ordering::Relaxed);
1824        }
1825    }
1826
1827    #[test]
1828    fn mutator_requests_redraw_only_after_mutation() {
1829        let shell = Arc::new(RedrawShell::default());
1830        let mut document = BaseDocument::new(DocumentConfig {
1831            shell_provider: Some(shell.clone()),
1832            ..Default::default()
1833        });
1834        let root_id = document.root_node().id;
1835
1836        {
1837            let mut mutator = document.mutate();
1838            let parent_id = mutator.create_element(qual_name!("div"), vec![]);
1839            let child_id = mutator.create_element(qual_name!("span"), vec![]);
1840            mutator.append_children(parent_id, &[child_id]);
1841            mutator.remove_and_drop_all_children(parent_id);
1842            mutator.set_attribute(parent_id, qual_name!("id"), "detached");
1843        }
1844        assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 0);
1845
1846        {
1847            let mutator = document.mutate();
1848            assert_eq!(mutator.child_ids(root_id).len(), 0);
1849        }
1850
1851        {
1852            let mut mutator = document.mutate();
1853            let node_id = mutator.create_element(qual_name!("div"), vec![]);
1854            mutator.append_children(root_id, &[node_id]);
1855            mutator.set_attribute(node_id, qual_name!("id"), "in-document");
1856        }
1857        assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 1);
1858
1859        {
1860            let mut mutator = document.mutate();
1861            let parent_id = mutator.create_element(qual_name!("div"), vec![]);
1862            let child_id = mutator.create_element(qual_name!("span"), vec![]);
1863            mutator.append_children(root_id, &[parent_id]);
1864            mutator.append_children(parent_id, &[child_id]);
1865            mutator.remove_and_drop_all_children(parent_id);
1866        }
1867        assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 2);
1868
1869        {
1870            let mut mutator = document.mutate();
1871            let parent_id = mutator.create_element(qual_name!("div"), vec![]);
1872            let child_id = mutator.create_element(qual_name!("span"), vec![]);
1873            let detached_target_id = mutator.create_element(qual_name!("div"), vec![]);
1874            mutator.append_children(root_id, &[parent_id]);
1875            mutator.append_children(parent_id, &[child_id]);
1876            assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 2);
1877            mutator.append_children(detached_target_id, &[child_id]);
1878        }
1879        assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 3);
1880    }
1881
1882    #[test]
1883    fn moving_subtree_out_of_document_clears_in_document_flag() {
1884        let shell = Arc::new(RedrawShell::default());
1885        let mut document = BaseDocument::new(DocumentConfig {
1886            shell_provider: Some(shell.clone()),
1887            ..Default::default()
1888        });
1889        let root_id = document.root_node().id;
1890        let (child_id, grandchild_id, detached_parent_id) = {
1891            let mut mutator = document.mutate();
1892            let in_document_parent_id = mutator.create_element(qual_name!("div"), vec![]);
1893            let child_id = mutator.create_element(qual_name!("div"), vec![]);
1894            let grandchild_id = mutator.create_element(qual_name!("span"), vec![]);
1895            let detached_parent_id = mutator.create_element(qual_name!("section"), vec![]);
1896            mutator.append_children(root_id, &[in_document_parent_id]);
1897            mutator.append_children(in_document_parent_id, &[child_id]);
1898            mutator.append_children(child_id, &[grandchild_id]);
1899            (child_id, grandchild_id, detached_parent_id)
1900        };
1901        assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 1);
1902        assert!(document.get_node(child_id).unwrap().flags.is_in_document());
1903        assert!(
1904            document
1905                .get_node(grandchild_id)
1906                .unwrap()
1907                .flags
1908                .is_in_document()
1909        );
1910
1911        {
1912            let mut mutator = document.mutate();
1913            mutator.append_children(detached_parent_id, &[child_id]);
1914        }
1915        assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 2);
1916        assert!(!document.get_node(child_id).unwrap().flags.is_in_document());
1917        assert!(
1918            !document
1919                .get_node(grandchild_id)
1920                .unwrap()
1921                .flags
1922                .is_in_document()
1923        );
1924
1925        {
1926            let mut mutator = document.mutate();
1927            mutator.set_attribute(child_id, qual_name!("id"), "detached");
1928        }
1929        assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 2);
1930
1931        {
1932            let mut mutator = document.mutate();
1933            mutator.append_children(root_id, &[child_id]);
1934        }
1935        assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 3);
1936        assert!(document.get_node(child_id).unwrap().flags.is_in_document());
1937        assert!(
1938            document
1939                .get_node(grandchild_id)
1940                .unwrap()
1941                .flags
1942                .is_in_document()
1943        );
1944    }
1945
1946    #[test]
1947    fn style_property_updates_nested_layout() {
1948        let mut document = BaseDocument::new(DocumentConfig {
1949            viewport: Some(Viewport::new(800, 600, 1.0, ColorScheme::Light)),
1950            ..Default::default()
1951        });
1952        let root_id = document.root_node().id;
1953
1954        let mover_id = {
1955            let mut mutator = document.mutate();
1956            let parent_id = mutator.create_element(qual_name!("div"), vec![]);
1957            let mover_id = mutator.create_element(qual_name!("div"), vec![]);
1958            mutator.set_style_property(parent_id, "position", "relative");
1959            mutator.set_style_property(parent_id, "width", "800px");
1960            mutator.set_style_property(parent_id, "height", "600px");
1961            mutator.set_style_property(mover_id, "position", "absolute");
1962            mutator.set_style_property(mover_id, "left", "0px");
1963            mutator.set_style_property(mover_id, "top", "0px");
1964            mutator.append_children(parent_id, &[mover_id]);
1965            mutator.append_children(root_id, &[parent_id]);
1966            mover_id
1967        };
1968
1969        document.resolve(0.0);
1970        assert_eq!(
1971            document
1972                .get_node(mover_id)
1973                .unwrap()
1974                .final_layout()
1975                .location
1976                .x,
1977            0.0
1978        );
1979
1980        {
1981            let mut mutator = document.mutate();
1982            mutator.set_style_property(mover_id, "left", "120px");
1983        }
1984
1985        document.resolve(0.0);
1986        assert_eq!(
1987            document
1988                .get_node(mover_id)
1989                .unwrap()
1990                .final_layout()
1991                .location
1992                .x,
1993            120.0
1994        );
1995    }
1996
1997    #[test]
1998    fn style_property_mutation_copies_blocks_in_rule_tree() {
1999        let mut document = BaseDocument::new(DocumentConfig {
2000            viewport: Some(Viewport::new(800, 600, 1.0, ColorScheme::Light)),
2001            ..Default::default()
2002        });
2003        let root_id = document.root_node().id;
2004
2005        let div_id = {
2006            let mut mutator = document.mutate();
2007            let div_id = mutator.create_element(qual_name!("div"), vec![]);
2008            mutator.set_style_property(div_id, "width", "100px");
2009            mutator.set_style_property(div_id, "height", "50px");
2010            mutator.append_children(root_id, &[div_id]);
2011            div_id
2012        };
2013
2014        let style_block = |document: &BaseDocument| {
2015            document
2016                .get_node(div_id)
2017                .unwrap()
2018                .element_data()
2019                .unwrap()
2020                .style_attribute
2021                .clone()
2022                .unwrap()
2023        };
2024
2025        let before_resolve = style_block(&document);
2026        {
2027            let mut mutator = document.mutate();
2028            mutator.set_style_property(div_id, "height", "60px");
2029        }
2030        // Not yet in the rule tree: mutated in place.
2031        assert!(ServoArc::ptr_eq(&before_resolve, &style_block(&document)));
2032
2033        document.resolve(0.0);
2034        let in_rule_tree = style_block(&document);
2035        assert!(
2036            in_rule_tree
2037                .read_with(&document.guard.read())
2038                .immutable
2039                .load(Ordering::Relaxed)
2040        );
2041
2042        {
2043            let mut mutator = document.mutate();
2044            mutator.set_style_property(div_id, "width", "200px");
2045        }
2046        let after_set = style_block(&document);
2047        assert!(!ServoArc::ptr_eq(&in_rule_tree, &after_set));
2048        {
2049            let guard = document.guard.read();
2050            let old = in_rule_tree.read_with(&guard);
2051            let new = after_set.read_with(&guard);
2052            assert_eq!(old.declarations().len(), 2);
2053            assert_eq!(new.declarations().len(), 2);
2054            assert_ne!(old, new);
2055        }
2056
2057        document.resolve(0.0);
2058        assert_eq!(
2059            document.get_node(div_id).unwrap().final_layout().size.width,
2060            200.0
2061        );
2062
2063        let in_rule_tree = style_block(&document);
2064        {
2065            let mut mutator = document.mutate();
2066            mutator.remove_style_property(div_id, "height");
2067        }
2068        let after_remove = style_block(&document);
2069        assert!(!ServoArc::ptr_eq(&in_rule_tree, &after_remove));
2070        {
2071            let guard = document.guard.read();
2072            assert_eq!(in_rule_tree.read_with(&guard).declarations().len(), 2);
2073            assert_eq!(after_remove.read_with(&guard).declarations().len(), 1);
2074        }
2075    }
2076}