Skip to main content

blitz_dom/
mutator.rs

1use blitz_traits::node_id::NodeId;
2use std::collections::HashSet;
3use std::mem;
4use std::ops::{Deref, DerefMut};
5
6use crate::document::make_device;
7use crate::layout::damage::ALL_DAMAGE;
8use crate::net::{ImageHandler, ResourceHandler, StylesheetHandler};
9use crate::node::{CanvasData, NodeFlags, SpecialElementData};
10use crate::util::ImageType;
11use crate::{
12    Attribute, BaseDocument, Document, ElementData, Node, NodeData, QualName, local_name, qual_name,
13};
14use blitz_traits::shell::Viewport;
15use markup5ever::ns;
16use selectors::matching::ElementSelectorFlags;
17use style::Atom;
18use style::invalidation::element::restyle_hints::RestyleHint;
19use style::stylesheets::OriginSet;
20use thin_vec::ThinVec;
21
22macro_rules! tag_and_attr {
23    ($tag:tt, $attr:tt) => {
24        (&local_name!($tag), &local_name!($attr))
25    };
26}
27
28#[derive(Debug, Clone)]
29pub enum AppendTextErr {
30    /// The node is not a text node
31    NotTextNode,
32}
33
34/// Operations that happen almost immediately, but are deferred within a
35/// function for borrow-checker reasons.
36enum SpecialOp {
37    LoadImage(NodeId),
38    LoadIframe(NodeId),
39    LoadStylesheet(NodeId),
40    UnloadStylesheet(NodeId),
41    LoadCustomPaintSource(NodeId),
42    ProcessButtonInput(NodeId),
43    UnloadSubDocument(NodeId),
44    #[cfg(feature = "custom-widget")]
45    UnloadCustomWidget(NodeId),
46    #[cfg(feature = "shadow-dom")]
47    UpgradeCustomElement(NodeId),
48    #[cfg(feature = "shadow-dom")]
49    DisconnectCustomElement(NodeId),
50}
51
52pub struct DocumentMutator<'doc> {
53    /// Document is public as an escape hatch, but users of this API should ideally avoid using it
54    /// and prefer exposing additional functionality in DocumentMutator.
55    pub doc: &'doc mut BaseDocument,
56
57    eager_op_queue: Vec<SpecialOp>,
58
59    // Tracked nodes for deferred processing when mutations have completed
60    title_node: Option<NodeId>,
61    style_nodes: HashSet<NodeId>,
62    form_nodes: HashSet<NodeId>,
63
64    /// Whether an element/attribute that affect animation status has been seen
65    recompute_is_animating: bool,
66
67    /// Whether any mutation that affects rendered output has been performed
68    mutations_occurred: bool,
69
70    /// Deferred custom-element attribute-change notifications: (host_id, attr
71    /// name, old value, new value). Drained and dispatched on flush.
72    #[cfg(feature = "shadow-dom")]
73    custom_element_attr_changes: Vec<(NodeId, QualName, Option<String>, Option<String>)>,
74
75    /// The (latest) node which has been mounted in and had autofocus=true, if any
76    #[cfg(feature = "autofocus")]
77    node_to_autofocus: Option<NodeId>,
78}
79
80impl Drop for DocumentMutator<'_> {
81    fn drop(&mut self) {
82        self.flush(); // Defined at bottom of file
83        if self.mutations_occurred {
84            self.doc.shell_provider.request_redraw();
85        }
86    }
87}
88
89impl DocumentMutator<'_> {
90    pub fn new<'doc>(doc: &'doc mut BaseDocument) -> DocumentMutator<'doc> {
91        DocumentMutator {
92            doc,
93            eager_op_queue: Vec::new(),
94            title_node: None,
95            style_nodes: HashSet::new(),
96            form_nodes: HashSet::new(),
97            recompute_is_animating: false,
98            mutations_occurred: false,
99            #[cfg(feature = "shadow-dom")]
100            custom_element_attr_changes: Vec::new(),
101            #[cfg(feature = "autofocus")]
102            node_to_autofocus: None,
103        }
104    }
105
106    // Query methods
107
108    pub fn node_has_parent(&self, node_id: NodeId) -> bool {
109        self.doc.nodes[node_id].parent.is_some()
110    }
111
112    pub fn previous_sibling_id(&self, node_id: NodeId) -> Option<NodeId> {
113        self.doc.nodes[node_id].backward(1).map(|node| node.id)
114    }
115
116    pub fn next_sibling_id(&self, node_id: NodeId) -> Option<NodeId> {
117        self.doc.nodes[node_id].forward(1).map(|node| node.id)
118    }
119
120    pub fn parent_id(&self, node_id: NodeId) -> Option<NodeId> {
121        self.doc.nodes[node_id].parent
122    }
123
124    pub fn last_child_id(&self, node_id: NodeId) -> Option<NodeId> {
125        self.doc.nodes[node_id].children.last().copied()
126    }
127
128    pub fn child_ids(&self, node_id: NodeId) -> ThinVec<NodeId> {
129        self.doc.nodes[node_id].children.clone()
130    }
131
132    pub fn element_name(&self, node_id: NodeId) -> Option<&QualName> {
133        self.doc.nodes[node_id].element_data().map(|el| &el.name)
134    }
135
136    pub fn node_at_path(&self, start_node_id: NodeId, path: &[u8]) -> NodeId {
137        let mut current = &self.doc.nodes[start_node_id];
138        for i in path {
139            let new_id = current.children[*i as usize];
140            current = &self.doc.nodes[new_id];
141        }
142        current.id
143    }
144
145    // Node creation methods
146
147    pub fn create_comment_node(&mut self, contents: &str) -> NodeId {
148        self.doc.create_node(NodeData::Comment {
149            contents: contents.to_string(),
150        })
151    }
152
153    pub fn create_text_node(&mut self, text: &str) -> NodeId {
154        self.doc.create_text_node(text)
155    }
156
157    /// A `DocumentFragment`: a parentless container that never appears in a box
158    /// tree and whose children are what gets inserted when it is.
159    ///
160    /// jQuery builds one during initialisation, so without this the library
161    /// throws before it defines `jQuery` and every page depending on it loses
162    /// its scripting. Eight sites in a hundred-site corpus failed that way, and
163    /// they reported it as `jQuery is not defined` — the missing method itself
164    /// says only `not a callable function`, which names nothing.
165    pub fn create_document_fragment(&mut self) -> NodeId {
166        self.doc.create_node(NodeData::DocumentFragment)
167    }
168
169    /// A detached HTML document: `html > head > title`, and `body`.
170    ///
171    /// This backs `document.implementation.createHTMLDocument`, which jQuery
172    /// calls during initialisation to decide whether it can parse markup
173    /// through a second document. Reading `createHTMLDocument` off an absent
174    /// `implementation` threw before jQuery had assigned itself to `window`,
175    /// so every site depending on it reported `jQuery is not defined` -- a
176    /// global that was never missing.
177    ///
178    /// The node lives in this document's arena but is attached to nothing, so
179    /// it never lays out or paints. That is what makes it cheap: no second
180    /// `BaseDocument`, no second style engine, no second arena.
181    pub fn create_html_document(&mut self, title: &str) -> NodeId {
182        let doc_id = self.doc.create_node(NodeData::Document(Box::default()));
183
184        let html = self.create_element(html_tag("html"), Vec::new());
185        let head = self.create_element(html_tag("head"), Vec::new());
186        let title_el = self.create_element(html_tag("title"), Vec::new());
187        let body = self.create_element(html_tag("body"), Vec::new());
188
189        // An empty title argument still gets an element, because the DOM has
190        // one either way; it simply has no text child.
191        if !title.is_empty() {
192            let text = self.create_text_node(title);
193            self.append_children(title_el, &[text]);
194        }
195
196        self.append_children(head, &[title_el]);
197        self.append_children(html, &[head, body]);
198        self.append_children(doc_id, &[html]);
199
200        doc_id
201    }
202
203    pub fn create_element(&mut self, name: QualName, attrs: Vec<Attribute>) -> NodeId {
204        let mut data = ElementData::new(name, attrs);
205        data.flush_style_attribute(self.doc.guard(), &self.doc.url.url_extra_data());
206
207        let id = self.doc.create_node(NodeData::Element(Box::new(data)));
208        let node = self.doc.get_node_mut(id).unwrap();
209
210        // Initialise style data
211        *node.stylo_element_data_mut().ensure_init_mut() = style::data::ElementData {
212            damage: ALL_DAMAGE,
213            ..Default::default()
214        };
215
216        id
217    }
218
219    pub fn deep_clone_node(&mut self, node_id: NodeId) -> NodeId {
220        self.doc.deep_clone_node(node_id)
221    }
222
223    // Node mutation methods
224
225    pub fn set_node_text(&mut self, node_id: NodeId, value: &str) {
226        let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
227        let node = &mut self.doc.nodes[node_id];
228
229        // A comment is CharacterData too: `comment.data = "x"` and
230        // `comment.nodeValue = "x"` both land here, and until this arm existed
231        // they fell through to the `_ => return` below and vanished. The
232        // contents were already on the node and simply never written.
233        //
234        // Deliberately not the Text arm's damage handling. A comment generates
235        // no layout box, so `insert_damage(ALL_DAMAGE)` and
236        // `mark_ancestors_dirty` would schedule a relayout for a change that
237        // cannot affect a pixel, once per write. Nothing rendered depends on
238        // this string, so setting it is the whole operation.
239        if let NodeData::Comment { ref mut contents } = node.data {
240            if contents != value {
241                contents.clear();
242                contents.push_str(value);
243            }
244            return;
245        }
246
247        let text = match node.data {
248            NodeData::Text(ref mut text) => text,
249            // TODO: otherwise this is basically element.textContent which is a bit different - need to parse as html
250            _ => return,
251        };
252
253        let changed = text.content != value;
254        if changed {
255            self.mutations_occurred |= node_is_in_document;
256            text.content.clear();
257            text.content.push_str(value);
258            node.insert_damage(ALL_DAMAGE);
259            // Mark ancestors dirty so the style traversal visits this subtree.
260            // Without this, the traversal may skip nodes with pending damage.
261            node.mark_ancestors_dirty();
262            let parent_id = node.parent;
263
264            // Also insert damage on the parent element, since text content changes
265            // affect the parent's layout (text may wrap differently, change size, etc.)
266            if let Some(parent_id) = parent_id {
267                let parent = &mut self.doc.nodes[parent_id];
268                parent.insert_damage(ALL_DAMAGE);
269            }
270
271            self.maybe_record_node(parent_id);
272        }
273    }
274
275    pub fn append_text_to_node(
276        &mut self,
277        node_id: NodeId,
278        text: &str,
279    ) -> Result<(), AppendTextErr> {
280        let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
281        let node = &mut self.doc.nodes[node_id];
282        node.insert_damage(ALL_DAMAGE);
283        node.mark_ancestors_dirty();
284        match node.text_data_mut() {
285            Some(data) => {
286                data.content += text;
287                self.mutations_occurred |= node_is_in_document;
288                Ok(())
289            }
290            None => Err(AppendTextErr::NotTextNode),
291        }
292    }
293
294    pub fn add_attrs_if_missing(&mut self, node_id: NodeId, attrs: Vec<Attribute>) {
295        let node = &mut self.doc.nodes[node_id];
296        node.insert_damage(ALL_DAMAGE);
297        let element_data = node.element_data_mut().expect("Not an element");
298
299        let existing_names = element_data
300            .attrs
301            .iter()
302            .map(|e| e.name.clone())
303            .collect::<HashSet<_>>();
304
305        for attr in attrs
306            .into_iter()
307            .filter(|attr| !existing_names.contains(&attr.name))
308        {
309            self.set_attribute(node_id, attr.name, &attr.value);
310        }
311    }
312
313    pub fn set_attribute(&mut self, node_id: NodeId, name: QualName, value: &str) {
314        let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
315        if node_is_in_document {
316            self.doc.snapshot_node(node_id);
317
318            // Damage is asserted only where an attribute can change what the
319            // element renders without changing a computed value.
320            //
321            // For everything else Stylo calls `compute_layout_damage` with the
322            // old and new values during the restyle the hint above asks for,
323            // and that answer is the accurate one. Asserting `ALL_DAMAGE`
324            // first can only OR it back up to everything, which is what made a
325            // colour-only class toggle reconstruct a box: 842us against 295us,
326            // and four nodes recomputed where the correct answer is none.
327            //
328            // The exceptions are real. `<use href>` names a sprite symbol and
329            // no computed value moves when it changes, so the cached SVG has to
330            // be rebuilt by damage or not at all
331            // (`setting_a_use_href_later_rebuilds_the_cached_svg`). Replaced
332            // elements are the same story for `src`, `width` and `height`.
333            let renders_from_attributes = self.doc.nodes[node_id]
334                .data
335                .downcast_element()
336                .is_some_and(|el| {
337                    el.name.ns == ns!(svg)
338                        || crate::layout::replaced::is_replaced_element(&el.name.local)
339                });
340
341            let node = &mut self.doc.nodes[node_id];
342            if let Some(mut data) = node.stylo_element_data_opt_mut().and_then(|s| s.get_mut()) {
343                data.hint |= RestyleHint::restyle_subtree();
344                if renders_from_attributes {
345                    data.damage.insert(ALL_DAMAGE);
346                }
347            }
348
349            // The parent is restyled only when a selector says it depends on
350            // its children.
351            //
352            // It used to be restyled unconditionally, which meant a class
353            // toggle on one row restyled every sibling of that row: on a
354            // 40-row list, a colour-only change cost 773us of style against
355            // 15us for a frame that changed nothing, while layout recomputed
356            // four nodes. Style was half of the whole resolve, for one
357            // element's colour.
358            //
359            // The flags say exactly when the wide hint is needed, because
360            // `apply_selector_flags` deposits them on the parent while matching:
361            // `:empty` and `:only-child` on the parent, `:nth-child` and the
362            // sibling combinators on the siblings, `:has()` through the
363            // relative-selector directions. A parent carrying none of them has
364            // no rule whose match can change because a child's attribute did.
365            let parent = node.parent;
366            if let Some(parent_id) = parent {
367                let parent = &self.doc.nodes[parent_id];
368                let flags = parent.selector_flags().get();
369                let child_dependent = ElementSelectorFlags::HAS_SLOW_SELECTOR
370                    | ElementSelectorFlags::HAS_SLOW_SELECTOR_LATER_SIBLINGS
371                    | ElementSelectorFlags::HAS_EDGE_CHILD_SELECTOR
372                    | ElementSelectorFlags::HAS_EMPTY_SELECTOR
373                    | ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR
374                    | ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_SIBLING
375                    | ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR_SIBLING;
376
377                if flags.intersects(child_dependent) {
378                    let parent = &mut self.doc.nodes[parent_id];
379                    if let Some(mut data) = parent
380                        .stylo_element_data_opt_mut()
381                        .and_then(|s| s.get_mut())
382                    {
383                        data.hint |= RestyleHint::restyle_subtree();
384                    }
385                }
386            }
387
388            // Mark ancestors dirty so the style traversal visits this subtree.
389            // Without this, the traversal may skip nodes with pending RestyleHint/damage
390            // because it uses dirty_descendants flags to determine which subtrees to visit.
391            self.doc.nodes[node_id].mark_ancestors_dirty();
392        }
393
394        if name.local == local_name!("id") && node_is_in_document {
395            if let Some(old_id) = self.doc.nodes[node_id]
396                .element_data()
397                .map(|element| element.id.clone())
398            {
399                if let Some(old_id) = old_id {
400                    self.doc.remove_from_id_map(&old_id, node_id);
401                }
402                self.doc.add_to_id_map(value, node_id);
403            }
404        }
405
406        let node = &mut self.doc.nodes[node_id];
407
408        let NodeData::Element(ref mut element) = node.data else {
409            return;
410        };
411
412        self.mutations_occurred |= node_is_in_document;
413        // If element is a CustomWidget, then Ccall attribute_changed on it
414        #[cfg(feature = "custom-widget")]
415        if let SpecialElementData::CustomWidget(widget_data) = &mut element.special_data {
416            let old_value = element.attrs.get(&name).as_ref().map(|attr| &*attr.value);
417            widget_data
418                .widget
419                .attribute_changed(&name.local, old_value, Some(value));
420        }
421
422        // If element is a CustomElement, defer an attribute_changed notification
423        // (it needs mutable document access, so it can't run inline here).
424        #[cfg(feature = "shadow-dom")]
425        if element.custom_element_data().is_some() {
426            let old_value = element
427                .attrs
428                .get(&name)
429                .as_ref()
430                .map(|attr| attr.value.to_string());
431            self.custom_element_attr_changes.push((
432                node_id,
433                name.clone(),
434                old_value,
435                Some(value.to_string()),
436            ));
437        }
438
439        element.attrs.set(name.clone(), value);
440
441        // Focusability is cached on the element and comes from these
442        // attributes, so it has to follow a change to one of them: a widget
443        // that hands the focus around its own children - a menu, a grid -
444        // sets their tabindex after creating them.
445        if name.local == local_name!("tabindex")
446            || name.local == local_name!("href")
447            || name.local == local_name!("disabled")
448        {
449            element.flush_is_focussable();
450        }
451
452        let tag = &element.name.local;
453        let attr = &name.local;
454
455        if *attr == local_name!("id") {
456            element.id = Some(Atom::from(value))
457        }
458
459        if *attr == local_name!("value") {
460            if let Some(input_data) = element.text_input_data_mut() {
461                // Update text input value
462                input_data.set_text(
463                    &mut self.doc.font_ctx.lock().unwrap(),
464                    &mut self.doc.layout_ctx,
465                    value,
466                );
467            }
468            return;
469        }
470
471        if *attr == local_name!("style") {
472            element.flush_style_attribute(&self.doc.guard, &self.doc.url.url_extra_data());
473            node.mark_style_attr_updated();
474            return;
475        }
476
477        if *attr == local_name!("disabled") && element.can_be_disabled() {
478            node.disable();
479            return;
480        }
481
482        // If node if not in the document, then don't apply any special behaviours
483        // and simply set the attribute value
484        if !node.flags.is_in_document() {
485            return;
486        }
487
488        if (tag, attr) == tag_and_attr!("input", "checked") {
489            set_input_checked_state(element, value.to_string());
490        } else if (tag, attr) == tag_and_attr!("img", "src") {
491            self.load_image(node_id);
492        } else if (tag, attr) == tag_and_attr!("canvas", "src") {
493            self.load_custom_paint_src(node_id);
494        } else if (tag, attr) == tag_and_attr!("link", "href") {
495            self.load_linked_stylesheet(node_id);
496        } else if (tag, attr) == tag_and_attr!("iframe", "src")
497            || (tag, attr) == tag_and_attr!("iframe", "srcdoc")
498        {
499            self.load_iframe(node_id);
500        }
501    }
502
503    pub fn clear_attribute(&mut self, node_id: NodeId, name: QualName) {
504        let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
505        if node_is_in_document {
506            self.doc.snapshot_node(node_id);
507
508            let node = &mut self.doc.nodes[node_id];
509
510            if let Some(mut data) = node.stylo_element_data_opt_mut().and_then(|s| s.get_mut()) {
511                data.hint |= RestyleHint::restyle_subtree();
512                data.damage.insert(ALL_DAMAGE);
513            }
514
515            // Mark ancestors dirty so the style traversal visits this subtree.
516            // Without this, the traversal may skip nodes with pending RestyleHint/damage.
517            node.mark_ancestors_dirty();
518        }
519
520        if name.local == local_name!("id") && node_is_in_document {
521            if let Some(old_id) = self.doc.nodes[node_id]
522                .element_data()
523                .and_then(|element| element.id.clone())
524            {
525                self.doc.remove_from_id_map(&old_id, node_id);
526            }
527        }
528
529        let node = &mut self.doc.nodes[node_id];
530
531        let Some(element) = node.element_data_mut() else {
532            return;
533        };
534
535        let removed_attr = element.attrs.remove(&name);
536        let had_attr = removed_attr.is_some();
537        if !had_attr {
538            return;
539        }
540        self.mutations_occurred |= node_is_in_document;
541
542        // If element is a CustomWidget, then call attribute_changed on it
543        #[cfg(feature = "custom-widget")]
544        if let SpecialElementData::CustomWidget(widget_data) = &mut element.special_data {
545            let old_value = removed_attr.as_ref().map(|attr| &*attr.value);
546            widget_data
547                .widget
548                .attribute_changed(&name.local, old_value, None);
549        }
550
551        // If element is a CustomElement, defer an attribute_changed notification.
552        #[cfg(feature = "shadow-dom")]
553        if element.custom_element_data().is_some() {
554            let old_value = removed_attr.as_ref().map(|attr| attr.value.to_string());
555            self.custom_element_attr_changes
556                .push((node_id, name.clone(), 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        // Update text input value
573        if name.local == local_name!("value") {
574            if let Some(input_data) = element.text_input_data_mut() {
575                input_data.set_text(
576                    &mut self.doc.font_ctx.lock().unwrap(),
577                    &mut self.doc.layout_ctx,
578                    "",
579                );
580            }
581        }
582
583        let tag = &element.name.local;
584        let attr = &name.local;
585
586        if *attr == local_name!("disabled") && element.can_be_disabled() {
587            node.enable();
588            return;
589        }
590
591        if *attr == local_name!("style") {
592            element.flush_style_attribute(&self.doc.guard, &self.doc.url.url_extra_data());
593            node.mark_style_attr_updated();
594        } else if (tag, attr) == tag_and_attr!("canvas", "src") {
595            self.recompute_is_animating = true;
596        } else if (tag, attr) == tag_and_attr!("link", "href") {
597            self.unload_stylesheet(node_id);
598        } else if (tag, attr) == tag_and_attr!("iframe", "srcdoc") && node_is_in_document {
599            // Fall back to loading from the `src` attribute (if any)
600            self.load_iframe(node_id);
601        }
602    }
603
604    pub fn set_style_property(&mut self, node_id: NodeId, name: &str, value: &str) {
605        let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
606        self.doc.set_style_property(node_id, name, value);
607        self.mutations_occurred |= node_is_in_document;
608    }
609
610    pub fn remove_style_property(&mut self, node_id: NodeId, name: &str) {
611        let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
612        self.doc.remove_style_property(node_id, name);
613        self.mutations_occurred |= node_is_in_document;
614    }
615
616    pub fn set_sub_document(&mut self, node_id: NodeId, sub_document: Box<dyn Document>) {
617        let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
618        self.doc.set_sub_document(node_id, sub_document);
619        self.mutations_occurred |= node_is_in_document;
620    }
621
622    pub fn remove_sub_document(&mut self, node_id: NodeId) {
623        let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
624        self.doc.remove_sub_document(node_id);
625        self.mutations_occurred |= node_is_in_document;
626    }
627
628    #[cfg(feature = "custom-widget")]
629    pub fn set_custom_widget(&mut self, node_id: NodeId, widget: Box<dyn crate::Widget>) {
630        let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
631        self.doc.set_custom_widget(node_id, widget);
632        self.mutations_occurred |= node_is_in_document;
633    }
634
635    #[cfg(feature = "custom-widget")]
636    pub fn remove_custom_widget(&mut self, node_id: NodeId) {
637        let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
638        self.doc.remove_custom_widget(node_id);
639        self.mutations_occurred |= node_is_in_document;
640    }
641
642    /// Attach a shadow root to the given host element, returning the shadow
643    /// root's node id.
644    #[cfg(feature = "shadow-dom")]
645    pub fn attach_shadow(&mut self, host_id: NodeId, mode: crate::node::ShadowRootMode) -> NodeId {
646        self.doc.attach_shadow(host_id, mode)
647    }
648
649    /// Attach a custom element controller to the given node and run its
650    /// `connected` lifecycle callback (attaching a shadow root if needed).
651    #[cfg(feature = "shadow-dom")]
652    pub fn set_custom_element(
653        &mut self,
654        node_id: NodeId,
655        controller: Box<dyn crate::node::CustomElement>,
656    ) {
657        self.doc.set_custom_element(node_id, controller);
658        self.upgrade_custom_element(node_id);
659    }
660
661    /// Remove the custom element controller from the given node, running its
662    /// `disconnected` callback first.
663    #[cfg(feature = "shadow-dom")]
664    pub fn remove_custom_element(&mut self, node_id: NodeId) {
665        self.disconnect_custom_element(node_id);
666        let _ = self.doc.take_custom_element(node_id);
667    }
668
669    /// Upgrade an element into a custom element: instantiate a controller from
670    /// the registry (if the node does not already have one), attach a shadow
671    /// root, and run the `connected` lifecycle callback. No-op if the element is
672    /// already upgraded or has no matching definition / controller.
673    #[cfg(feature = "shadow-dom")]
674    pub(crate) fn upgrade_custom_element(&mut self, node_id: NodeId) {
675        use crate::node::{CustomElementData, ShadowRootMode, SpecialElementData};
676
677        let Some(node) = self.doc.get_node(node_id) else {
678            return;
679        };
680        let Some(element) = node.element_data() else {
681            return;
682        };
683
684        // Determine whether a controller is already attached, and if not, look
685        // up a matching registry definition to instantiate one.
686        let already_has_controller =
687            matches!(element.special_data, SpecialElementData::CustomElement(_));
688
689        let mode = if already_has_controller {
690            // Already attached (e.g. via set_custom_element). Default mode.
691            ShadowRootMode::Open
692        } else {
693            let tag = element.name.local.clone();
694            let Some(definition) = self.doc.custom_element_registry.get(&tag) else {
695                return;
696            };
697            let mode = definition.mode;
698            let controller = (definition.factory)();
699            self.doc.nodes[node_id]
700                .element_data_mut()
701                .unwrap()
702                .special_data =
703                SpecialElementData::CustomElement(CustomElementData::new(controller));
704            self.doc.custom_element_nodes.insert(node_id);
705            mode
706        };
707
708        // Bail out if already upgraded.
709        let is_upgraded = self.doc.nodes[node_id]
710            .element_data()
711            .and_then(|el| el.custom_element_data())
712            .map(|data| data.upgraded)
713            .unwrap_or(true);
714        if is_upgraded {
715            return;
716        }
717
718        // Ensure a shadow root is attached.
719        let shadow_root_id = self.doc.attach_shadow(node_id, mode);
720
721        // Take the controller out so we can pass `&mut self` (the mutator) to it.
722        let Some(mut controller) = self.take_controller(node_id) else {
723            return;
724        };
725
726        {
727            let mut ctx = crate::node::CustomElementCtx {
728                mutator: self,
729                host_id: node_id,
730                shadow_root_id,
731            };
732            controller.connected(&mut ctx);
733        }
734
735        self.restore_controller(node_id, controller, true);
736    }
737
738    /// Run the `disconnected` callback for a custom element node.
739    #[cfg(feature = "shadow-dom")]
740    pub(crate) fn disconnect_custom_element(&mut self, node_id: NodeId) {
741        let Some(shadow_root_id) = self
742            .doc
743            .get_node(node_id)
744            .and_then(|node| node.shadow_root_id())
745        else {
746            // No shadow root: still run disconnected if a controller exists.
747            if let Some(mut controller) = self.take_controller(node_id) {
748                // Use the host id as a stand-in shadow root id; controllers
749                // should guard against missing shadow trees.
750                {
751                    let mut ctx = crate::node::CustomElementCtx {
752                        mutator: self,
753                        host_id: node_id,
754                        shadow_root_id: node_id,
755                    };
756                    controller.disconnected(&mut ctx);
757                }
758                self.restore_controller(node_id, controller, false);
759            }
760            return;
761        };
762
763        if let Some(mut controller) = self.take_controller(node_id) {
764            {
765                let mut ctx = crate::node::CustomElementCtx {
766                    mutator: self,
767                    host_id: node_id,
768                    shadow_root_id,
769                };
770                controller.disconnected(&mut ctx);
771            }
772            self.restore_controller(node_id, controller, false);
773        }
774    }
775
776    /// Take the custom element controller out of a node, leaving the
777    /// `CustomElementData` in place (with `controller == None`).
778    #[cfg(feature = "shadow-dom")]
779    fn take_controller(&mut self, node_id: NodeId) -> Option<Box<dyn crate::node::CustomElement>> {
780        self.doc
781            .nodes
782            .get_mut(node_id)?
783            .element_data_mut()?
784            .custom_element_data_mut()?
785            .controller
786            .take()
787    }
788
789    /// Put a controller back into a node's `CustomElementData`, optionally
790    /// marking it as upgraded.
791    #[cfg(feature = "shadow-dom")]
792    fn restore_controller(
793        &mut self,
794        node_id: NodeId,
795        controller: Box<dyn crate::node::CustomElement>,
796        upgraded: bool,
797    ) {
798        if let Some(data) = self
799            .doc
800            .nodes
801            .get_mut(node_id)
802            .and_then(|node| node.element_data_mut())
803            .and_then(|el| el.custom_element_data_mut())
804        {
805            data.controller = Some(controller);
806            if upgraded {
807                data.upgraded = true;
808            }
809        }
810    }
811
812    /// Sever the cached box-tree edge before a DOM child is detached or freed.
813    ///
814    /// The layout tree is not always the DOM tree: inline content can sit
815    /// below an anonymous block, and fixed content can be hoisted. Hidden
816    /// subtrees deliberately retain their layout caches, so damage on the DOM
817    /// parent alone cannot make a stale cached child safe before rounding and
818    /// painting traverse it. Invalidating the actual layout parent at mutation
819    /// time prevents either pass from indexing a SlotMap key that was freed.
820    fn invalidate_layout_parent_edge(&mut self, node_id: NodeId) {
821        let Some(layout_parent_id) = self
822            .doc
823            .nodes
824            .get(node_id)
825            .and_then(|node| node.layout_parent.get())
826        else {
827            return;
828        };
829        if let Some(layout_parent) = self.doc.nodes.get_mut(layout_parent_id) {
830            layout_parent.layout_children.get_mut().take();
831            layout_parent.paint_children.get_mut().take();
832            layout_parent.insert_damage(ALL_DAMAGE);
833        }
834        if let Some(node) = self.doc.nodes.get(node_id) {
835            node.layout_parent.set(None);
836        }
837    }
838
839    /// Zero the layout of a node and everything under it.
840    fn clear_layout_of_subtree(doc: &mut BaseDocument, node_id: NodeId) {
841        let mut stack = vec![node_id];
842        while let Some(id) = stack.pop() {
843            let Some(node) = doc.nodes.get_mut(id) else {
844                continue;
845            };
846            // The accessors panic on node kinds that have none, so ask the data
847            // first rather than every node in the subtree: a text node has no
848            // layout of its own and a removal walk hits plenty of them.
849            if node.data.downcast_element().is_some() {
850                *node.unrounded_layout_mut() = taffy::Layout::with_order(0);
851                *node.final_layout_mut() = taffy::Layout::with_order(0);
852                node.cache_mut().clear();
853            }
854            stack.extend(node.children.iter().copied());
855        }
856    }
857
858    /// Remove the node from its parent but don't drop it.
859    pub fn remove_node(&mut self, node_id: NodeId) {
860        let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
861        // Process the subtree *before* severing the parent link so that
862        // interaction state referencing removed nodes can retarget to the
863        // nearest surviving ancestor.
864        self.process_removed_subtree(node_id);
865
866        // A detached node keeps its box otherwise, and a box is all layout and
867        // paint need: the application's boot splash was removed by its
868        // framework the moment the workspace was ready, kept a 1318x880 layout
869        // for the rest of the session, and painted its own background over
870        // whichever panel it landed on. The page looked blank. The node is
871        // deliberately not dropped, so JS wrappers stay valid, but nothing
872        // outside the document should occupy space in it.
873        Self::clear_layout_of_subtree(self.doc, node_id);
874        self.invalidate_layout_parent_edge(node_id);
875
876        let node = &mut self.doc.nodes[node_id];
877
878        // Update child_idx values
879        if let Some(parent_id) = node.parent.take() {
880            self.mutations_occurred |= node_is_in_document;
881            let parent = &mut self.doc.nodes[parent_id];
882            parent.insert_damage(ALL_DAMAGE);
883            // Mark ancestors dirty so the style traversal visits this subtree.
884            parent.mark_ancestors_dirty();
885            parent.children.retain(|id| *id != node_id);
886            self.maybe_record_node(parent_id);
887        }
888    }
889
890    pub fn remove_and_drop_node(&mut self, node_id: NodeId) -> Option<Node> {
891        self.remove_and_drop_node_with(node_id, &mut |_| {})
892    }
893
894    /// Like [`Self::remove_and_drop_node`], but calls `on_drop` with the id of
895    /// every dropped node (the node itself and all of its descendants).
896    pub fn remove_and_drop_node_with(
897        &mut self,
898        node_id: NodeId,
899        on_drop: &mut dyn FnMut(NodeId),
900    ) -> Option<Node> {
901        let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
902        self.process_removed_subtree(node_id);
903        self.invalidate_layout_parent_edge(node_id);
904
905        let node = self.doc.drop_node_ignoring_parent_with(node_id, on_drop);
906        self.mutations_occurred |= node_is_in_document;
907
908        // Update child_idx values
909        if let Some(parent_id) = node.as_ref().and_then(|node| node.parent) {
910            let parent = &mut self.doc.nodes[parent_id];
911            parent.insert_damage(ALL_DAMAGE);
912            let parent_is_in_doc = parent.flags.is_in_document();
913
914            // TODO: make this fine grained / conditional based on ElementSelectorFlags
915            if parent_is_in_doc {
916                if let Some(mut data) = parent
917                    .stylo_element_data_opt_mut()
918                    .and_then(|s| s.get_mut())
919                {
920                    data.hint |= RestyleHint::restyle_subtree();
921                }
922                // Mark ancestors dirty so the style traversal visits this subtree.
923                parent.mark_ancestors_dirty();
924            }
925
926            parent.children.retain(|id| *id != node_id);
927            self.maybe_record_node(parent_id);
928        }
929
930        node
931    }
932
933    pub fn remove_and_drop_all_children(&mut self, node_id: NodeId) {
934        let parent = &mut self.doc.nodes[node_id];
935        let parent_is_in_doc = parent.flags.is_in_document();
936
937        // TODO: make this fine grained / conditional based on ElementSelectorFlags
938        if parent_is_in_doc {
939            if let Some(mut data) = parent
940                .stylo_element_data_opt_mut()
941                .and_then(|s| s.get_mut())
942            {
943                data.hint |= RestyleHint::restyle_subtree();
944            }
945            // Mark ancestors dirty so the style traversal visits this subtree.
946            parent.mark_ancestors_dirty();
947        }
948
949        let children = mem::take(&mut parent.children);
950        self.mutations_occurred |= parent_is_in_doc && !children.is_empty();
951        for child_id in children {
952            self.process_removed_subtree(child_id);
953            self.invalidate_layout_parent_edge(child_id);
954            let _ = self.doc.drop_node_ignoring_parent(child_id);
955        }
956        self.maybe_record_node(node_id);
957    }
958
959    // Tree mutation methods
960    pub fn remove_node_if_unparented(&mut self, node_id: NodeId) {
961        self.remove_node_if_unparented_with(node_id, &mut |_| {});
962    }
963
964    /// Like [`Self::remove_node_if_unparented`], but calls `on_drop` with the id of
965    /// every dropped node (the node itself and all of its descendants).
966    pub fn remove_node_if_unparented_with(
967        &mut self,
968        node_id: NodeId,
969        on_drop: &mut dyn FnMut(NodeId),
970    ) {
971        if let Some(node) = self.doc.get_node(node_id) {
972            if node.parent.is_none() {
973                self.remove_and_drop_node_with(node_id, on_drop);
974            }
975        }
976    }
977
978    /// Remove all of the children from old_parent_id and append them to new_parent_id
979    pub fn append_children(&mut self, parent_id: NodeId, child_ids: &[NodeId]) {
980        self.add_children_to_parent(parent_id, child_ids, &|parent, child_ids| {
981            parent.children.extend_from_slice(child_ids);
982        });
983    }
984
985    pub fn insert_nodes_before(&mut self, anchor_node_id: NodeId, new_node_ids: &[NodeId]) {
986        let parent_id = self.doc.nodes[anchor_node_id].parent.unwrap();
987        self.add_children_to_parent(parent_id, new_node_ids, &|parent, child_ids| {
988            let node_child_idx = parent.index_of_child(anchor_node_id).unwrap();
989            parent
990                .children
991                .splice(node_child_idx..node_child_idx, child_ids.iter().copied());
992        });
993    }
994
995    fn add_children_to_parent(
996        &mut self,
997        parent_id: NodeId,
998        child_ids: &[NodeId],
999        insert_children_fn: &dyn Fn(&mut Node, &[NodeId]),
1000    ) {
1001        // A fragment inserts its children, not itself. Flattened here rather
1002        // than in each caller because this is the one place `appendChild` and
1003        // `insertBefore` both pass through, so they cannot disagree about it.
1004        //
1005        // The fragment is emptied as it is expanded, which is what the spec
1006        // requires: after insertion a fragment has no children, and leaving
1007        // them behind would give every child two parents.
1008        let child_ids: Vec<NodeId> = if child_ids
1009            .iter()
1010            .any(|id| matches!(self.doc.nodes[*id].data, NodeData::DocumentFragment))
1011        {
1012            let mut flattened = Vec::with_capacity(child_ids.len());
1013            for id in child_ids.iter().copied() {
1014                if matches!(self.doc.nodes[id].data, NodeData::DocumentFragment) {
1015                    let moved = std::mem::take(&mut self.doc.nodes[id].children);
1016                    flattened.extend(moved);
1017                } else {
1018                    flattened.push(id);
1019                }
1020            }
1021            flattened
1022        } else {
1023            child_ids.to_vec()
1024        };
1025        let child_ids = child_ids.as_slice();
1026
1027        let new_parent_is_in_document = self.doc.nodes[parent_id].flags.is_in_document();
1028        self.mutations_occurred |= new_parent_is_in_document && !child_ids.is_empty();
1029        // Detach the children from their old parents *before* inserting them into
1030        // the new parent (matching DOM `insertBefore` semantics). If a child is
1031        // being moved within the same parent then detaching it after insertion
1032        // would remove both the old and the newly-inserted entries from the
1033        // parent's child list, and anchor indices would be computed against a
1034        // child list that still contains the moved nodes.
1035        for child_id in child_ids.iter().copied() {
1036            self.invalidate_layout_parent_edge(child_id);
1037            let child = &mut self.doc.nodes[child_id];
1038            let child_was_in_doc = child.flags.is_in_document();
1039            self.mutations_occurred |= child_was_in_doc;
1040            let Some(old_parent_id) = child.parent.take() else {
1041                continue;
1042            };
1043
1044            let old_parent = &mut self.doc.nodes[old_parent_id];
1045            old_parent.insert_damage(ALL_DAMAGE);
1046
1047            // TODO: make this fine grained / conditional based on ElementSelectorFlags
1048            if child_was_in_doc {
1049                if let Some(mut data) = old_parent
1050                    .stylo_element_data_opt_mut()
1051                    .and_then(|s| s.get_mut())
1052                {
1053                    data.hint |= RestyleHint::restyle_subtree();
1054                }
1055                // Mark ancestors dirty so the style traversal visits this subtree.
1056                old_parent.mark_ancestors_dirty();
1057            }
1058
1059            old_parent.children.retain(|id| *id != child_id);
1060            self.maybe_record_node(old_parent_id);
1061        }
1062
1063        let new_parent = &mut self.doc.nodes[parent_id];
1064        new_parent.insert_damage(ALL_DAMAGE);
1065
1066        // TODO: make this fine grained / conditional based on ElementSelectorFlags
1067        if new_parent_is_in_document {
1068            if let Some(mut data) = new_parent
1069                .stylo_element_data_opt_mut()
1070                .and_then(|s| s.get_mut())
1071            {
1072                data.hint |= RestyleHint::restyle_subtree();
1073            }
1074            // Mark ancestors dirty so the style traversal visits this subtree.
1075            new_parent.mark_ancestors_dirty();
1076        }
1077
1078        insert_children_fn(new_parent, child_ids);
1079
1080        for child_id in child_ids.iter().copied() {
1081            let child = &mut self.doc.nodes[child_id];
1082            let child_was_in_doc = child.flags.is_in_document();
1083            child.parent = Some(parent_id);
1084
1085            if new_parent_is_in_document && !child_was_in_doc {
1086                self.process_added_subtree(child_id);
1087            } else if !new_parent_is_in_document && child_was_in_doc {
1088                self.process_removed_subtree(child_id);
1089            }
1090        }
1091
1092        self.maybe_record_node(parent_id);
1093    }
1094
1095    // Tree mutation methods (that defer to other methods)
1096    pub fn insert_nodes_after(&mut self, anchor_node_id: NodeId, new_node_ids: &[NodeId]) {
1097        match self.next_sibling_id(anchor_node_id) {
1098            Some(id) => self.insert_nodes_before(id, new_node_ids),
1099            None => {
1100                let parent_id = self.parent_id(anchor_node_id).unwrap();
1101                self.append_children(parent_id, new_node_ids)
1102            }
1103        }
1104    }
1105
1106    pub fn reparent_children(&mut self, old_parent_id: NodeId, new_parent_id: NodeId) {
1107        let child_ids = std::mem::take(&mut self.doc.nodes[old_parent_id].children);
1108        self.maybe_record_node(old_parent_id);
1109        self.append_children(new_parent_id, &child_ids);
1110    }
1111
1112    pub fn replace_node_with(&mut self, anchor_node_id: NodeId, new_node_ids: &[NodeId]) {
1113        self.insert_nodes_before(anchor_node_id, new_node_ids);
1114        self.remove_node(anchor_node_id);
1115    }
1116}
1117
1118impl<'doc> DocumentMutator<'doc> {
1119    pub fn flush(&mut self) {
1120        if self.recompute_is_animating {
1121            self.doc.has_canvas = self.doc.compute_has_canvas();
1122        }
1123
1124        if let Some(id) = self.title_node {
1125            let title = self.doc.nodes[id].text_content();
1126            self.doc.shell_provider.set_window_title(title);
1127        }
1128
1129        // Add/Update inline stylesheets (<style> elements)
1130        for id in self.style_nodes.drain() {
1131            self.doc.process_style_element(id);
1132        }
1133
1134        for id in self.form_nodes.drain() {
1135            self.doc.reset_form_owner(id);
1136        }
1137
1138        #[cfg(feature = "autofocus")]
1139        if let Some(node_id) = self.node_to_autofocus.take() {
1140            if self.doc.get_node(node_id).is_some() {
1141                self.doc.set_focus_to(node_id);
1142            }
1143        }
1144
1145        #[cfg(feature = "shadow-dom")]
1146        self.dispatch_custom_element_attr_changes();
1147    }
1148
1149    /// Dispatch all deferred custom-element `attribute_changed` callbacks.
1150    #[cfg(feature = "shadow-dom")]
1151    fn dispatch_custom_element_attr_changes(&mut self) {
1152        if self.custom_element_attr_changes.is_empty() {
1153            return;
1154        }
1155        let changes = mem::take(&mut self.custom_element_attr_changes);
1156        for (node_id, name, old_value, new_value) in changes {
1157            // Skip if the registered definition observes a restricted set that
1158            // excludes this attribute. Manually-attached controllers (no
1159            // definition) observe all attributes.
1160            let tag = self
1161                .doc
1162                .get_node(node_id)
1163                .and_then(|node| node.element_data())
1164                .map(|el| el.name.local.clone());
1165            let observed = tag
1166                .as_ref()
1167                .and_then(|tag| self.doc.custom_element_registry.get(tag))
1168                .map(|def| def.observes(&name.local))
1169                .unwrap_or(true);
1170            if !observed {
1171                continue;
1172            }
1173
1174            let Some(shadow_root_id) = self
1175                .doc
1176                .get_node(node_id)
1177                .and_then(|node| node.shadow_root_id())
1178            else {
1179                continue;
1180            };
1181            let Some(mut controller) = self.take_controller(node_id) else {
1182                continue;
1183            };
1184            {
1185                let mut ctx = crate::node::CustomElementCtx {
1186                    mutator: self,
1187                    host_id: node_id,
1188                    shadow_root_id,
1189                };
1190                controller.attribute_changed(
1191                    &mut ctx,
1192                    &name.local,
1193                    old_value.as_deref(),
1194                    new_value.as_deref(),
1195                );
1196            }
1197            self.restore_controller(node_id, controller, false);
1198        }
1199    }
1200
1201    pub fn set_inner_html(&mut self, node_id: NodeId, html: &str) {
1202        self.remove_and_drop_all_children(node_id);
1203        self.doc
1204            .html_parser_provider
1205            .clone()
1206            .parse_inner_html(self, node_id, html);
1207    }
1208
1209    fn flush_eager_ops(&mut self) {
1210        let mut ops = mem::take(&mut self.eager_op_queue);
1211        for op in ops.drain(0..) {
1212            match op {
1213                SpecialOp::LoadImage(node_id) => self.load_image(node_id),
1214                SpecialOp::LoadIframe(node_id) => self.load_iframe(node_id),
1215                SpecialOp::LoadStylesheet(node_id) => self.load_linked_stylesheet(node_id),
1216                SpecialOp::UnloadStylesheet(node_id) => self.unload_stylesheet(node_id),
1217                SpecialOp::LoadCustomPaintSource(node_id) => self.load_custom_paint_src(node_id),
1218                SpecialOp::ProcessButtonInput(node_id) => self.process_button_input(node_id),
1219                SpecialOp::UnloadSubDocument(node_id) => self.remove_sub_document(node_id),
1220                #[cfg(feature = "custom-widget")]
1221                SpecialOp::UnloadCustomWidget(node_id) => self.remove_custom_widget(node_id),
1222                #[cfg(feature = "shadow-dom")]
1223                SpecialOp::UpgradeCustomElement(node_id) => self.upgrade_custom_element(node_id),
1224                #[cfg(feature = "shadow-dom")]
1225                SpecialOp::DisconnectCustomElement(node_id) => {
1226                    self.disconnect_custom_element(node_id)
1227                }
1228            }
1229        }
1230
1231        // Queue is empty, but put Vec back anyway so allocation can be reused.
1232        self.eager_op_queue = ops;
1233    }
1234
1235    fn process_added_subtree(&mut self, node_id: NodeId) {
1236        self.doc.iter_subtree_mut(node_id, |node_id, doc| {
1237            let node = &mut doc.nodes[node_id];
1238            node.flags.set(NodeFlags::IS_IN_DOCUMENT, true);
1239            node.insert_damage(ALL_DAMAGE);
1240
1241            // If the node has an "id" attribute, store it in the ID map.
1242            if let Some(id_attr) = node.attr(local_name!("id")).map(ToString::to_string) {
1243                doc.add_to_id_map(&id_attr, node_id);
1244            }
1245
1246            let node = &mut doc.nodes[node_id];
1247            let NodeData::Element(ref mut element) = node.data else {
1248                return;
1249            };
1250
1251            // Custom post-processing by element tag name
1252            let tag = element.name.local.as_ref();
1253            match tag {
1254                "title" if element.name.ns == ns!(html) => self.title_node = Some(node_id),
1255                "link" => self.eager_op_queue.push(SpecialOp::LoadStylesheet(node_id)),
1256                "img" => self.eager_op_queue.push(SpecialOp::LoadImage(node_id)),
1257                "iframe" => self.eager_op_queue.push(SpecialOp::LoadIframe(node_id)),
1258                "canvas" => self
1259                    .eager_op_queue
1260                    .push(SpecialOp::LoadCustomPaintSource(node_id)),
1261                "style" => {
1262                    self.style_nodes.insert(node_id);
1263                }
1264                "button" | "fieldset" | "input" | "select" | "textarea" | "object" | "output" => {
1265                    self.eager_op_queue
1266                        .push(SpecialOp::ProcessButtonInput(node_id));
1267                    self.form_nodes.insert(node_id);
1268                }
1269                _ => {}
1270            }
1271
1272            // If the element's tag name matches a registered custom element
1273            // definition (and it hasn't already been upgraded), queue it for
1274            // upgrade.
1275            #[cfg(feature = "shadow-dom")]
1276            {
1277                let needs_upgrade = doc.custom_element_registry.contains(&element.name.local)
1278                    && element.custom_element_data().is_none();
1279                if needs_upgrade {
1280                    self.eager_op_queue
1281                        .push(SpecialOp::UpgradeCustomElement(node_id));
1282                }
1283            }
1284
1285            // `autofocus` is a boolean attribute: present is true, whatever
1286            // the value, and absent is the only false. Requiring the literal
1287            // string "true" meant the one spelling almost nothing uses, since
1288            // markup writes `<input autofocus>` and the parser stores that as
1289            // the empty string. Every framework agrees: Solid's boolean
1290            // attribute setter is `setAttribute(name, "")`.
1291            //
1292            // So a field marked autofocus in markup never took focus, and
1293            // blitz-script papered over its own path by writing "true" from
1294            // the property setter, which left the parsed path broken.
1295            #[cfg(feature = "autofocus")]
1296            if node.is_focussable() {
1297                if let NodeData::Element(ref element) = node.data {
1298                    if element.attr(local_name!("autofocus")).is_some() {
1299                        self.node_to_autofocus = Some(node_id);
1300                    }
1301                }
1302            }
1303        });
1304
1305        self.flush_eager_ops();
1306    }
1307
1308    fn process_removed_subtree(&mut self, node_id: NodeId) {
1309        self.doc.iter_subtree_mut(node_id, |node_id, doc| {
1310            doc.nodes[node_id]
1311                .flags
1312                .set(NodeFlags::IS_IN_DOCUMENT, false);
1313
1314            // Clear any interaction state that references this node, running
1315            // the usual teardown steps (unhover/unactive the surviving
1316            // ancestor chain, IME disable on blur of a focused input).
1317            doc.clear_interaction_state_for_removed_node(node_id);
1318
1319            let node = &mut doc.nodes[node_id];
1320
1321            // Same for focus and for the node the last press landed on.
1322            //
1323            // These two were missed, and they are the two most likely to point
1324            // at a node that is being removed: dismissing a panel is a click on
1325            // a control *inside* it, so that control is both the focused node
1326            // and the mousedown node at the moment its subtree goes away.
1327            //
1328            // A stale id here is not inert. The next click calls `set_focus_to`,
1329            // which blurs the old node by indexing it, and indexing a dropped
1330            // id panics inside the event handler. The window then stops
1331            // responding to clicks until something forces a full rebuild.
1332            //
1333            // The upstream fix carried a second failure mode, the blur landing
1334            // on whatever node had taken the recycled slot. That one cannot
1335            // happen here: `NodeId` is versioned, so a dropped id resolves to
1336            // nothing rather than aliasing its successor.
1337            if doc.focus_node_id == Some(node_id) {
1338                doc.focus_node_id = None;
1339            }
1340            if doc.mousedown_node_id == Some(node_id) {
1341                doc.mousedown_node_id = None;
1342            }
1343
1344            // Clear the text selection if one of its endpoints references this node.
1345            // This prevents stale selection endpoint references.
1346            if doc.text_selection.anchor.node_or_parent == Some(node_id)
1347                || doc.text_selection.focus.node_or_parent == Some(node_id)
1348            {
1349                doc.text_selection.clear();
1350            }
1351
1352            // Remove any snapshot for this node to prevent stale snapshot references
1353            // during style invalidation.
1354            if node.has_snapshot() {
1355                let opaque_id = style::dom::TNode::opaque(&&*node);
1356                doc.snapshots.remove(&opaque_id);
1357                node.set_has_snapshot(false);
1358            }
1359
1360            // If the node has an "id" attribute remove it from the ID map.
1361            if let Some(id_attr) = node.attr(local_name!("id")).map(ToString::to_string) {
1362                doc.remove_from_id_map(&id_attr, node_id);
1363            }
1364
1365            let node = &mut doc.nodes[node_id];
1366            let NodeData::Element(ref mut element) = node.data else {
1367                return;
1368            };
1369
1370            match &element.special_data {
1371                SpecialElementData::SubDocument(_) => {
1372                    self.eager_op_queue
1373                        .push(SpecialOp::UnloadSubDocument(node_id));
1374                }
1375                #[cfg(feature = "custom-widget")]
1376                SpecialElementData::CustomWidget(_) => {
1377                    self.eager_op_queue
1378                        .push(SpecialOp::UnloadCustomWidget(node_id));
1379                }
1380                #[cfg(feature = "shadow-dom")]
1381                SpecialElementData::CustomElement(_) => {
1382                    self.eager_op_queue
1383                        .push(SpecialOp::DisconnectCustomElement(node_id));
1384                }
1385                SpecialElementData::Stylesheet(_) => self
1386                    .eager_op_queue
1387                    .push(SpecialOp::UnloadStylesheet(node_id)),
1388                SpecialElementData::Image(_) => {}
1389                SpecialElementData::Canvas(_) => {
1390                    self.recompute_is_animating = true;
1391                }
1392                SpecialElementData::TableRoot(_) => {}
1393                SpecialElementData::TextInput(_) => {}
1394                SpecialElementData::CheckboxInput(_) => {}
1395                #[cfg(feature = "file-input")]
1396                SpecialElementData::FileInput(_) => {}
1397                SpecialElementData::None => {}
1398            }
1399        });
1400
1401        self.flush_eager_ops();
1402    }
1403
1404    fn maybe_record_node(&mut self, node_id: impl Into<Option<NodeId>>) {
1405        let Some(node_id) = node_id.into() else {
1406            return;
1407        };
1408
1409        let Some(element) = self.doc.nodes[node_id].data.downcast_element() else {
1410            return;
1411        };
1412
1413        match element.name.local.as_ref() {
1414            "title" if element.name.ns == ns!(html) => self.title_node = Some(node_id),
1415            "style" => {
1416                self.style_nodes.insert(node_id);
1417            }
1418            _ => {}
1419        }
1420    }
1421
1422    fn load_linked_stylesheet(&mut self, target_id: NodeId) {
1423        let node = &self.doc.nodes[target_id];
1424
1425        let mut is_in_head = false;
1426        let mut parent_id = node.parent;
1427        while let Some(id) = parent_id
1428            && !is_in_head
1429        {
1430            let parent = &self.doc.nodes[id];
1431            is_in_head |= parent.data.is_element_with_tag_name(&local_name!("head"));
1432            parent_id = parent.parent;
1433        }
1434
1435        let rel_attr = node.attr(local_name!("rel"));
1436        let href_attr = node.attr(local_name!("href"));
1437
1438        let (Some(rels), Some(href)) = (rel_attr, href_attr) else {
1439            return;
1440        };
1441        if !rels.split_ascii_whitespace().any(|rel| rel == "stylesheet") {
1442            return;
1443        }
1444
1445        let url = self.doc.resolve_url(href);
1446        let handler = ResourceHandler::new(
1447            self.doc.tx.clone(),
1448            self.doc.id(),
1449            Some(node.id),
1450            self.doc.shell_provider.clone(),
1451            StylesheetHandler {
1452                source_url: url.clone(),
1453                guard: self.doc.guard.clone(),
1454                net_provider: self.doc.net_provider.clone(),
1455                abort_signal: self.doc.abort_signal.clone(),
1456            },
1457        );
1458
1459        if is_in_head && !self.doc.net_provider.is_noop() {
1460            self.doc
1461                .pending_critical_resources
1462                .insert(handler.request_id());
1463        }
1464
1465        self.doc.net_provider.fetch(
1466            self.doc.id(),
1467            self.doc.build_request(url),
1468            Box::new(handler),
1469        );
1470    }
1471
1472    fn unload_stylesheet(&mut self, node_id: NodeId) {
1473        let node = &mut self.doc.nodes[node_id];
1474        let Some(element) = node.element_data_mut() else {
1475            unreachable!();
1476        };
1477        let SpecialElementData::Stylesheet(stylesheet) = element.special_data.take() else {
1478            unreachable!();
1479        };
1480
1481        let guard = self.doc.guard.read();
1482        self.doc.stylist.remove_stylesheet(stylesheet, &guard);
1483        self.doc
1484            .stylist
1485            .force_stylesheet_origins_dirty(OriginSet::all());
1486
1487        self.doc.nodes_to_stylesheet.remove(&node_id);
1488    }
1489
1490    fn load_image(&mut self, target_id: NodeId) {
1491        let node = &self.doc.nodes[target_id];
1492        if let Some(raw_src) = node.attr(local_name!("src")) {
1493            if !raw_src.is_empty() {
1494                let src = self.doc.resolve_url(raw_src);
1495                let src_string = src.as_str();
1496
1497                // Check cache first
1498                if let Some(cached_image) = self.doc.image_cache.get(src_string) {
1499                    #[cfg(feature = "tracing")]
1500                    tracing::info!("Loading image {src_string} from cache");
1501                    let node = &mut self.doc.nodes[target_id];
1502                    node.element_data_mut().unwrap().special_data =
1503                        SpecialElementData::Image(Box::new(cached_image.clone()));
1504                    node.cache_mut().clear();
1505                    node.insert_damage(ALL_DAMAGE);
1506                    return;
1507                }
1508
1509                // Check if there's already a pending request for this URL
1510                if let Some(waiting_list) = self.doc.pending_images.get_mut(src_string) {
1511                    #[cfg(feature = "tracing")]
1512                    tracing::info!("Image {src_string} already pending, queueing node {target_id}");
1513                    waiting_list.push((target_id, ImageType::Image));
1514                    return;
1515                }
1516
1517                // Start fetch and track as pending
1518                #[cfg(feature = "tracing")]
1519                tracing::info!("Fetching image {src_string}");
1520                self.doc
1521                    .pending_images
1522                    .insert(src_string.to_string(), vec![(target_id, ImageType::Image)]);
1523
1524                self.doc.net_provider.fetch(
1525                    self.doc.id(),
1526                    self.doc.build_request(src),
1527                    ResourceHandler::boxed(
1528                        self.doc.tx.clone(),
1529                        self.doc.id(),
1530                        None, // Don't pass node_id, we'll handle it via pending_images
1531                        self.doc.shell_provider.clone(),
1532                        ImageHandler::new(ImageType::Image),
1533                    ),
1534                );
1535            }
1536        }
1537    }
1538
1539    fn load_iframe(&mut self, target_id: NodeId) {
1540        if self.doc.subdocument_depth >= crate::iframe::MAX_SUBDOCUMENT_DEPTH {
1541            #[cfg(feature = "tracing")]
1542            tracing::warn!(
1543                "Not loading iframe: max sub-document nesting depth ({}) reached",
1544                crate::iframe::MAX_SUBDOCUMENT_DEPTH
1545            );
1546            return;
1547        }
1548
1549        let node = &self.doc.nodes[target_id];
1550        let Some(element) = node.element_data() else {
1551            return;
1552        };
1553
1554        // `srcdoc` takes precedence over `src`
1555        if let Some(srcdoc) = element.attr(local_name!("srcdoc")) {
1556            let srcdoc = srcdoc.to_string();
1557            self.doc.load_iframe_srcdoc(target_id, &srcdoc);
1558            return;
1559        }
1560
1561        let Some(raw_src) = element.attr(local_name!("src")) else {
1562            return;
1563        };
1564        if raw_src.is_empty() {
1565            return;
1566        }
1567        let Some(url) = self.doc.url.resolve_relative(raw_src) else {
1568            #[cfg(feature = "tracing")]
1569            tracing::warn!("Not loading iframe: could not resolve url {raw_src}");
1570            return;
1571        };
1572        self.doc.start_iframe_load(target_id, url);
1573    }
1574
1575    fn load_custom_paint_src(&mut self, target_id: NodeId) {
1576        let node = &mut self.doc.nodes[target_id];
1577        if let Some(raw_src) = node.attr(local_name!("src")) {
1578            if let Ok(custom_paint_source_id) = raw_src.parse::<u64>() {
1579                self.recompute_is_animating = true;
1580                let canvas_data = SpecialElementData::Canvas(CanvasData {
1581                    custom_paint_source_id,
1582                });
1583                node.element_data_mut().unwrap().special_data = canvas_data;
1584            }
1585        }
1586    }
1587
1588    fn process_button_input(&mut self, target_id: NodeId) {
1589        let node = &self.doc.nodes[target_id];
1590        let Some(data) = node.element_data() else {
1591            return;
1592        };
1593
1594        let tagname = data.name.local.as_ref();
1595        let type_attr = data.attr(local_name!("type"));
1596        let value = data.attr(local_name!("value"));
1597
1598        // Add content of "value" attribute as a text node child if:
1599        //   - Tag name is
1600        if let ("input", Some("button" | "submit" | "reset"), Some(value)) =
1601            (tagname, type_attr, value)
1602        {
1603            let value = value.to_string();
1604            let id = self.create_text_node(&value);
1605            self.append_children(target_id, &[id]);
1606            return;
1607        }
1608        #[cfg(feature = "file-input")]
1609        if let ("input", Some("file")) = (tagname, type_attr) {
1610            let button_id = self.create_element(
1611                qual_name!("button", html),
1612                vec![
1613                    Attribute {
1614                        name: qual_name!("type", html),
1615                        value: "button".into(),
1616                    },
1617                    Attribute {
1618                        name: qual_name!("tabindex", html),
1619                        value: "-1".into(),
1620                    },
1621                ],
1622            );
1623            let label_id = self.create_element(qual_name!("label", html), vec![]);
1624            let text_id = self.create_text_node("No File Selected");
1625            let button_text_id = self.create_text_node("Browse");
1626            self.append_children(target_id, &[button_id, label_id]);
1627            self.append_children(label_id, &[text_id]);
1628            self.append_children(button_id, &[button_text_id]);
1629        }
1630    }
1631}
1632
1633/// Set 'checked' state on an input based on given attributevalue
1634fn set_input_checked_state(element: &mut ElementData, value: String) {
1635    let Ok(checked) = value.parse() else {
1636        return;
1637    };
1638    match element.special_data {
1639        SpecialElementData::CheckboxInput(ref mut checked_mut) => *checked_mut = checked,
1640        // If we have just constructed the element, set the node attribute,
1641        // and NodeSpecificData will be created from that later
1642        // this simulates the checked attribute being set in html,
1643        // and the element's checked property being set from that
1644        SpecialElementData::None => element.attrs.push(Attribute {
1645            name: qual_name!("checked", html),
1646            value: checked.to_string().into(),
1647        }),
1648        _ => {}
1649    }
1650}
1651
1652/// Type that allows mutable access to the viewport
1653/// And syncs it back to stylist on drop.
1654pub struct ViewportMut<'doc> {
1655    doc: &'doc mut BaseDocument,
1656    initial_viewport: Viewport,
1657}
1658impl ViewportMut<'_> {
1659    pub fn new(doc: &mut BaseDocument) -> ViewportMut<'_> {
1660        let initial_viewport = doc.viewport.clone();
1661        ViewportMut {
1662            doc,
1663            initial_viewport,
1664        }
1665    }
1666}
1667impl Deref for ViewportMut<'_> {
1668    type Target = Viewport;
1669
1670    fn deref(&self) -> &Self::Target {
1671        &self.doc.viewport
1672    }
1673}
1674impl DerefMut for ViewportMut<'_> {
1675    fn deref_mut(&mut self) -> &mut Self::Target {
1676        &mut self.doc.viewport
1677    }
1678}
1679impl Drop for ViewportMut<'_> {
1680    fn drop(&mut self) {
1681        if self.doc.viewport == self.initial_viewport {
1682            return;
1683        }
1684
1685        self.doc.set_stylist_device(make_device(
1686            &self.doc.viewport,
1687            self.doc.media_type.clone(),
1688            self.doc.font_ctx.clone(),
1689        ));
1690        self.doc.scroll_viewport_by(0.0, 0.0); // Clamp scroll offset
1691
1692        let scale_has_changed =
1693            self.doc.viewport().scale_f64() != self.initial_viewport.scale_f64();
1694        if scale_has_changed {
1695            self.doc.invalidate_inline_contexts();
1696            self.doc.shell_provider.request_redraw();
1697        }
1698    }
1699}
1700
1701/// A qualified name in the HTML namespace.
1702fn html_tag(name: &str) -> markup5ever::QualName {
1703    markup5ever::QualName::new(None, markup5ever::ns!(html), name.into())
1704}
1705
1706#[cfg(test)]
1707mod test {
1708    use style::media_queries::MediaType;
1709    use style_dom::ElementState;
1710
1711    use std::sync::{
1712        Arc,
1713        atomic::{AtomicUsize, Ordering},
1714    };
1715
1716    use blitz_traits::shell::{ColorScheme, ShellProvider, Viewport};
1717
1718    use crate::{
1719        Attribute, BaseDocument, DocumentConfig, ElementData, NodeData, NodeId, qual_name,
1720    };
1721
1722    #[test]
1723    fn media_type_defaults_to_screen() {
1724        let mut document = BaseDocument::new(DocumentConfig::default());
1725        assert_eq!(*document.media_type(), MediaType::screen());
1726        assert_eq!(document.stylist_device().media_type(), MediaType::screen());
1727    }
1728
1729    #[test]
1730    fn media_type_honors_config() {
1731        let mut document = BaseDocument::new(DocumentConfig {
1732            media_type: Some(MediaType::print()),
1733            ..Default::default()
1734        });
1735        assert_eq!(*document.media_type(), MediaType::print());
1736        assert_eq!(document.stylist_device().media_type(), MediaType::print());
1737    }
1738
1739    #[test]
1740    fn set_media_type_updates_stylist_device() {
1741        let mut document = BaseDocument::new(DocumentConfig::default());
1742        assert_eq!(document.stylist_device().media_type(), MediaType::screen());
1743
1744        document.set_media_type(MediaType::print());
1745        assert_eq!(*document.media_type(), MediaType::print());
1746        assert_eq!(document.stylist_device().media_type(), MediaType::print());
1747    }
1748
1749    #[test]
1750    fn removing_a_node_forgets_it_as_focused_and_pressed() {
1751        // Dismissing a panel is a click on a control inside it, so at that
1752        // moment the control is both the focused node and the mousedown node,
1753        // and then its subtree goes away. Removal used to clear hover, active
1754        // and the selection endpoints but leave these two, and the next click
1755        // indexed a dropped id and panicked inside the event handler.
1756        let mut document = BaseDocument::new(DocumentConfig::default());
1757        let button = document.create_node(NodeData::Element(Box::new(ElementData::new(
1758            qual_name!("button"),
1759            Vec::new(),
1760        ))));
1761        let root = document.root_node().id;
1762
1763        let mut mutator = document.mutate();
1764        mutator.append_children(root, &[button]);
1765        drop(mutator);
1766
1767        document.set_focus_to(button);
1768        document.set_mousedown_node_id(Some(button));
1769        assert_eq!(document.get_focussed_node_id(), Some(button));
1770        assert_eq!(document.mousedown_node_id, Some(button));
1771
1772        let mut mutator = document.mutate();
1773        mutator.remove_node(button);
1774        drop(mutator);
1775
1776        assert_eq!(
1777            document.get_focussed_node_id(),
1778            None,
1779            "a removed node must not stay focused"
1780        );
1781        assert_eq!(
1782            document.mousedown_node_id, None,
1783            "a removed node must not stay the pressed node"
1784        );
1785    }
1786
1787    #[test]
1788    fn dropping_a_child_clears_a_hidden_retained_layout_edge() {
1789        let mut document = BaseDocument::new(DocumentConfig {
1790            viewport: Some(Viewport::new(800, 600, 1.0, ColorScheme::Light)),
1791            ..Default::default()
1792        });
1793        let root = document.root_node().id;
1794        let (parent, child) = {
1795            let mut mutator = document.mutate();
1796            let parent = mutator.create_element(qual_name!("div"), vec![]);
1797            let child = mutator.create_element(qual_name!("button"), vec![]);
1798            mutator.set_style_property(parent, "width", "200px");
1799            mutator.set_style_property(parent, "height", "100px");
1800            mutator.append_children(parent, &[child]);
1801            mutator.append_children(root, &[parent]);
1802            (parent, child)
1803        };
1804
1805        document.resolve(0.0);
1806        {
1807            let mut mutator = document.mutate();
1808            mutator.set_style_property(parent, "display", "none");
1809        }
1810        document.resolve(0.0);
1811        assert!(
1812            document.nodes[parent]
1813                .layout_children
1814                .borrow()
1815                .as_ref()
1816                .is_some_and(|children| children.contains(&child)),
1817            "the hidden subtree should retain the layout edge that makes this regression possible"
1818        );
1819
1820        document.mutate().remove_and_drop_node(child);
1821        assert!(document.get_node(child).is_none(), "the child was freed");
1822        assert!(
1823            document.nodes[parent].layout_children.borrow().is_none(),
1824            "the surviving layout parent must not retain the freed key"
1825        );
1826
1827        // This used to panic in Taffy's rounding pass after indexing `child`.
1828        document.resolve(0.0);
1829    }
1830
1831    #[test]
1832    fn mutator_remove_disabled() {
1833        let mut document = BaseDocument::new(DocumentConfig::default());
1834        let id = document.create_node(NodeData::Element(Box::new(ElementData::new(
1835            qual_name!("button"),
1836            vec![Attribute {
1837                name: qual_name!("disabled"),
1838                value: "".into(),
1839            }],
1840        ))));
1841
1842        let node = document.get_node(id).unwrap();
1843        assert!(
1844            node.element_state().contains(ElementState::DISABLED),
1845            "form node is disabled"
1846        );
1847        assert!(
1848            !node.element_state().contains(ElementState::ENABLED),
1849            "form node is not enabled yet"
1850        );
1851
1852        let mut mutator = document.mutate();
1853        mutator.clear_attribute(id, qual_name!("disabled"));
1854        drop(mutator);
1855
1856        let node = document.get_node(id).unwrap();
1857        assert!(
1858            !node.element_state().contains(ElementState::DISABLED),
1859            "form node is no longer disabled"
1860        );
1861        assert!(
1862            node.element_state().contains(ElementState::ENABLED),
1863            "form node is enabled"
1864        );
1865    }
1866
1867    #[test]
1868    fn mutator_set_disabled() {
1869        let mut document = BaseDocument::new(DocumentConfig::default());
1870        let id = document.create_node(NodeData::Element(Box::new(ElementData::new(
1871            qual_name!("button"),
1872            vec![],
1873        ))));
1874
1875        let node = document.get_node(id).unwrap();
1876        assert!(
1877            !node.element_state().contains(ElementState::DISABLED),
1878            "form node is not disabled"
1879        );
1880        assert!(
1881            node.element_state().contains(ElementState::ENABLED),
1882            "form node is enabled"
1883        );
1884
1885        let mut mutator = document.mutate();
1886        mutator.set_attribute(id, qual_name!("disabled"), "");
1887        drop(mutator);
1888
1889        let node = document.get_node(id).unwrap();
1890
1891        assert!(
1892            node.element_state().contains(ElementState::DISABLED),
1893            "form node is disabled"
1894        );
1895        assert!(
1896            !node.element_state().contains(ElementState::ENABLED),
1897            "form node is no longer enabled enabled"
1898        );
1899    }
1900
1901    #[test]
1902    fn mutator_set_disabled_invalid_node() {
1903        let mut document = BaseDocument::new(DocumentConfig::default());
1904        let id = document.create_node(NodeData::Element(Box::new(ElementData::new(
1905            qual_name!("a"),
1906            vec![],
1907        ))));
1908
1909        let node = document.get_node(id).unwrap();
1910        assert!(
1911            !node.element_state().contains(ElementState::DISABLED),
1912            "form node is not disabled"
1913        );
1914        assert!(
1915            !node.element_state().contains(ElementState::ENABLED),
1916            "form node is enabled"
1917        );
1918
1919        let mut mutator = document.mutate();
1920        mutator.set_attribute(id, qual_name!("disabled"), "");
1921        drop(mutator);
1922
1923        let node = document.get_node(id).unwrap();
1924        assert!(
1925            !node.element_state().contains(ElementState::DISABLED),
1926            "form node is not disabled"
1927        );
1928        assert!(
1929            !node.element_state().contains(ElementState::ENABLED),
1930            "form node is enabled"
1931        );
1932    }
1933
1934    #[test]
1935    fn mutator_id_attribute_updates_id_map() {
1936        let mut document = BaseDocument::new(DocumentConfig::default());
1937        let root_id = document.root_node().id;
1938
1939        let node_id = {
1940            let mut mutator = document.mutate();
1941            let node_id = mutator.create_element(
1942                qual_name!("div"),
1943                vec![Attribute {
1944                    name: qual_name!("id"),
1945                    value: "old".into(),
1946                }],
1947            );
1948            mutator.append_children(root_id, &[node_id]);
1949            node_id
1950        };
1951        assert_eq!(document.get_element_by_id("old"), Some(node_id));
1952
1953        {
1954            let mut mutator = document.mutate();
1955            mutator.set_attribute(node_id, qual_name!("id"), "new");
1956        }
1957        assert_eq!(document.get_element_by_id("new"), Some(node_id));
1958        assert_eq!(document.get_element_by_id("old"), None);
1959
1960        {
1961            let mut mutator = document.mutate();
1962            mutator.clear_attribute(node_id, qual_name!("id"));
1963        }
1964        assert_eq!(document.get_element_by_id("new"), None);
1965    }
1966
1967    #[test]
1968    fn get_element_by_id_duplicate_ids_first_in_tree_order_wins() {
1969        let mut document = BaseDocument::new(DocumentConfig::default());
1970        let root_id = document.root_node().id;
1971
1972        let (first_id, second_id) = {
1973            let mut mutator = document.mutate();
1974            let first_id = mutator.create_element(qual_name!("div"), vec![]);
1975            let second_id = mutator.create_element(qual_name!("div"), vec![]);
1976            mutator.append_children(root_id, &[first_id, second_id]);
1977            // Assign the id to the later node first so that insertion order
1978            // differs from tree order
1979            mutator.set_attribute(second_id, qual_name!("id"), "dup");
1980            mutator.set_attribute(first_id, qual_name!("id"), "dup");
1981            (first_id, second_id)
1982        };
1983        assert_eq!(document.get_element_by_id("dup"), Some(first_id));
1984
1985        {
1986            let mut mutator = document.mutate();
1987            mutator.remove_node(first_id);
1988        }
1989        assert_eq!(document.get_element_by_id("dup"), Some(second_id));
1990    }
1991
1992    #[derive(Default)]
1993    struct RedrawShell {
1994        redraw_requests: AtomicUsize,
1995    }
1996
1997    impl ShellProvider for RedrawShell {
1998        fn request_redraw(&self) {
1999            self.redraw_requests.fetch_add(1, Ordering::Relaxed);
2000        }
2001    }
2002
2003    #[test]
2004    fn mutator_requests_redraw_only_after_mutation() {
2005        let shell = Arc::new(RedrawShell::default());
2006        let mut document = BaseDocument::new(DocumentConfig {
2007            shell_provider: Some(shell.clone()),
2008            ..Default::default()
2009        });
2010        let root_id = document.root_node().id;
2011
2012        {
2013            let mut mutator = document.mutate();
2014            let parent_id = mutator.create_element(qual_name!("div"), vec![]);
2015            let child_id = mutator.create_element(qual_name!("span"), vec![]);
2016            mutator.append_children(parent_id, &[child_id]);
2017            mutator.remove_and_drop_all_children(parent_id);
2018            mutator.set_attribute(parent_id, qual_name!("id"), "detached");
2019        }
2020        assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 0);
2021
2022        {
2023            let mutator = document.mutate();
2024            assert_eq!(mutator.child_ids(root_id).len(), 0);
2025        }
2026
2027        {
2028            let mut mutator = document.mutate();
2029            let node_id = mutator.create_element(qual_name!("div"), vec![]);
2030            mutator.append_children(root_id, &[node_id]);
2031            mutator.set_attribute(node_id, qual_name!("id"), "in-document");
2032        }
2033        assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 1);
2034
2035        {
2036            let mut mutator = document.mutate();
2037            let parent_id = mutator.create_element(qual_name!("div"), vec![]);
2038            let child_id = mutator.create_element(qual_name!("span"), vec![]);
2039            mutator.append_children(root_id, &[parent_id]);
2040            mutator.append_children(parent_id, &[child_id]);
2041            mutator.remove_and_drop_all_children(parent_id);
2042        }
2043        assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 2);
2044
2045        {
2046            let mut mutator = document.mutate();
2047            let parent_id = mutator.create_element(qual_name!("div"), vec![]);
2048            let child_id = mutator.create_element(qual_name!("span"), vec![]);
2049            let detached_target_id = mutator.create_element(qual_name!("div"), vec![]);
2050            mutator.append_children(root_id, &[parent_id]);
2051            mutator.append_children(parent_id, &[child_id]);
2052            assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 2);
2053            mutator.append_children(detached_target_id, &[child_id]);
2054        }
2055        assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 3);
2056    }
2057
2058    #[test]
2059    fn moving_subtree_out_of_document_clears_in_document_flag() {
2060        let shell = Arc::new(RedrawShell::default());
2061        let mut document = BaseDocument::new(DocumentConfig {
2062            shell_provider: Some(shell.clone()),
2063            ..Default::default()
2064        });
2065        let root_id = document.root_node().id;
2066        let (child_id, grandchild_id, detached_parent_id) = {
2067            let mut mutator = document.mutate();
2068            let in_document_parent_id = mutator.create_element(qual_name!("div"), vec![]);
2069            let child_id = mutator.create_element(qual_name!("div"), vec![]);
2070            let grandchild_id = mutator.create_element(qual_name!("span"), vec![]);
2071            let detached_parent_id = mutator.create_element(qual_name!("section"), vec![]);
2072            mutator.append_children(root_id, &[in_document_parent_id]);
2073            mutator.append_children(in_document_parent_id, &[child_id]);
2074            mutator.append_children(child_id, &[grandchild_id]);
2075            (child_id, grandchild_id, detached_parent_id)
2076        };
2077        assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 1);
2078        assert!(document.get_node(child_id).unwrap().flags.is_in_document());
2079        assert!(
2080            document
2081                .get_node(grandchild_id)
2082                .unwrap()
2083                .flags
2084                .is_in_document()
2085        );
2086
2087        {
2088            let mut mutator = document.mutate();
2089            mutator.append_children(detached_parent_id, &[child_id]);
2090        }
2091        assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 2);
2092        assert!(!document.get_node(child_id).unwrap().flags.is_in_document());
2093        assert!(
2094            !document
2095                .get_node(grandchild_id)
2096                .unwrap()
2097                .flags
2098                .is_in_document()
2099        );
2100
2101        {
2102            let mut mutator = document.mutate();
2103            mutator.set_attribute(child_id, qual_name!("id"), "detached");
2104        }
2105        assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 2);
2106
2107        {
2108            let mut mutator = document.mutate();
2109            mutator.append_children(root_id, &[child_id]);
2110        }
2111        assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 3);
2112        assert!(document.get_node(child_id).unwrap().flags.is_in_document());
2113        assert!(
2114            document
2115                .get_node(grandchild_id)
2116                .unwrap()
2117                .flags
2118                .is_in_document()
2119        );
2120    }
2121
2122    /// A `calc()` does not reach taffy as a value. `stylo_taffy` hands it over
2123    /// as a raw pointer into the node's `ComputedValues`, and layout
2124    /// dereferences that pointer on every resolve, so the cached taffy style
2125    /// must never outlive the arc it was built from.
2126    ///
2127    /// A restyle that lands no relayout damage still replaces those computed
2128    /// values. Colour is the cheapest example and it is the real one: a slow
2129    /// command's response restyled the project header two seconds after boot,
2130    /// the header's absolutely positioned chip carries
2131    /// `max-width: calc(100% - 24px)`, and 0.6.x experimental died there in
2132    /// three different ways depending on what had taken the freed allocation.
2133    #[test]
2134    fn a_paint_only_restyle_refreshes_the_calc_the_taffy_style_points_at() {
2135        use style::servo_arc::Arc as ServoArc;
2136
2137        let mut document = BaseDocument::new(DocumentConfig {
2138            viewport: Some(Viewport::new(800, 600, 1.0, ColorScheme::Light)),
2139            ..Default::default()
2140        });
2141        let root_id = document.root_node().id;
2142
2143        let (header_id, chip_id) = {
2144            let mut mutator = document.mutate();
2145            let header_id = mutator.create_element(qual_name!("div"), vec![]);
2146            let chip_id = mutator.create_element(qual_name!("span"), vec![]);
2147            mutator.set_style_property(header_id, "position", "relative");
2148            mutator.set_style_property(header_id, "width", "800px");
2149            mutator.set_style_property(header_id, "height", "60px");
2150            mutator.set_style_property(chip_id, "position", "absolute");
2151            mutator.set_style_property(chip_id, "max-width", "calc(100% - 24px)");
2152            mutator.set_style_property(chip_id, "color", "rgb(1, 2, 3)");
2153            mutator.append_children(header_id, &[chip_id]);
2154            mutator.append_children(root_id, &[header_id]);
2155            (header_id, chip_id)
2156        };
2157
2158        document.resolve(0.0);
2159
2160        // Restyled through inheritance, not directly: the chip's own mutation
2161        // damage would force a rebuild and hide the hazard. Recolouring the
2162        // parent recomputes the child's values — a new arc — while the child's
2163        // own damage stays repaint-only, which is exactly the gap the gate left
2164        // open.
2165        {
2166            let mut mutator = document.mutate();
2167            mutator.set_style_property(header_id, "color", "rgb(4, 5, 6)");
2168        }
2169        document.resolve(0.0);
2170
2171        let node = document.get_node(chip_id).unwrap();
2172        let stylo_data = node.stylo_element_data_opt().and_then(|data| data.get());
2173        let primary = stylo_data
2174            .as_ref()
2175            .and_then(|data| data.styles.get_primary())
2176            .expect("the chip is styled");
2177        let source = node
2178            .style_source_opt()
2179            .expect("a styled node records the computed values its taffy style was built from");
2180
2181        assert!(
2182            ServoArc::ptr_eq(primary, source),
2183            "the cached taffy style still points into computed values that a restyle replaced, \
2184             so every calc() in it is a dangling pointer",
2185        );
2186    }
2187
2188    #[test]
2189    fn style_property_updates_nested_layout() {
2190        let mut document = BaseDocument::new(DocumentConfig {
2191            viewport: Some(Viewport::new(800, 600, 1.0, ColorScheme::Light)),
2192            ..Default::default()
2193        });
2194        let root_id = document.root_node().id;
2195
2196        let mover_id = {
2197            let mut mutator = document.mutate();
2198            let parent_id = mutator.create_element(qual_name!("div"), vec![]);
2199            let mover_id = mutator.create_element(qual_name!("div"), vec![]);
2200            mutator.set_style_property(parent_id, "position", "relative");
2201            mutator.set_style_property(parent_id, "width", "800px");
2202            mutator.set_style_property(parent_id, "height", "600px");
2203            mutator.set_style_property(mover_id, "position", "absolute");
2204            mutator.set_style_property(mover_id, "left", "0px");
2205            mutator.set_style_property(mover_id, "top", "0px");
2206            mutator.append_children(parent_id, &[mover_id]);
2207            mutator.append_children(root_id, &[parent_id]);
2208            mover_id
2209        };
2210
2211        document.resolve(0.0);
2212        assert_eq!(
2213            document
2214                .get_node(mover_id)
2215                .unwrap()
2216                .final_layout()
2217                .location
2218                .x,
2219            0.0
2220        );
2221
2222        {
2223            let mut mutator = document.mutate();
2224            mutator.set_style_property(mover_id, "left", "120px");
2225        }
2226
2227        document.resolve(0.0);
2228        assert_eq!(
2229            document
2230                .get_node(mover_id)
2231                .unwrap()
2232                .final_layout()
2233                .location
2234                .x,
2235            120.0
2236        );
2237    }
2238
2239    /// `<html><body><div>text<!--comment--></div></body></html>`, laid out
2240    /// once, returning the text and comment ids.
2241    fn doc_with_a_comment() -> (BaseDocument, NodeId, NodeId, NodeId) {
2242        let mut doc = BaseDocument::new(DocumentConfig {
2243            viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
2244            ..Default::default()
2245        });
2246        let root_id = doc.root_node().id;
2247
2248        let mut mutr = doc.mutate();
2249        let html = mutr.create_element(qual_name!("html"), vec![]);
2250        let body = mutr.create_element(qual_name!("body"), vec![]);
2251        let container = mutr.create_element(qual_name!("div"), vec![]);
2252        let text = mutr.create_text_node("text");
2253        let comment = mutr.create_comment_node("comment");
2254        mutr.append_children(container, &[text, comment]);
2255        mutr.append_children(body, &[container]);
2256        mutr.append_children(html, &[body]);
2257        mutr.append_children(root_id, &[html]);
2258        drop(mutr);
2259
2260        doc.resolve(0.0);
2261        (doc, container, text, comment)
2262    }
2263
2264    /// A comment is CharacterData: `comment.data = "x"` has to land somewhere.
2265    /// Before this arm existed it fell through and vanished, so a getter that
2266    /// returned the contents would have disagreed with every write.
2267    #[test]
2268    fn setting_a_comments_data_writes_the_contents() {
2269        let (mut doc, _container, _text, comment) = doc_with_a_comment();
2270
2271        doc.mutate().set_node_text(comment, "rewritten");
2272
2273        let NodeData::Comment { contents } = &doc.get_node(comment).unwrap().data else {
2274            panic!("expected a comment node");
2275        };
2276        assert_eq!(contents, "rewritten");
2277    }
2278
2279    /// A comment generates no box, so writing its data must not schedule a
2280    /// relayout. Without this the obvious implementation (copy the Text arm)
2281    /// costs a full resolve per write, and nothing observable would say so.
2282    ///
2283    /// The text-node write at the end is the control: it proves the assertion
2284    /// above is capable of failing.
2285    #[test]
2286    fn setting_a_comments_data_does_not_dirty_layout() {
2287        let (mut doc, container, text, comment) = doc_with_a_comment();
2288
2289        let container_damage_before = doc.get_node(container).unwrap().damage();
2290        let comment_damage_before = doc.get_node(comment).unwrap().damage();
2291
2292        doc.mutate().set_node_text(comment, "rewritten");
2293
2294        assert_eq!(
2295            doc.get_node(comment).unwrap().damage(),
2296            comment_damage_before,
2297            "writing a comment's data damaged the comment"
2298        );
2299        assert_eq!(
2300            doc.get_node(container).unwrap().damage(),
2301            container_damage_before,
2302            "writing a comment's data damaged its parent, scheduling a relayout \
2303             for a change that cannot affect a pixel"
2304        );
2305
2306        doc.mutate().set_node_text(text, "rewritten");
2307        assert_ne!(
2308            doc.get_node(container).unwrap().damage(),
2309            container_damage_before,
2310            "a text write should damage the parent, so the assertions above can fail"
2311        );
2312    }
2313
2314    /// Writing the same contents back is not a change, and must stay as inert
2315    /// as a write of different contents.
2316    #[test]
2317    fn rewriting_a_comment_with_its_own_contents_is_inert() {
2318        let (mut doc, container, _text, comment) = doc_with_a_comment();
2319        let container_damage_before = doc.get_node(container).unwrap().damage();
2320
2321        doc.mutate().set_node_text(comment, "comment");
2322
2323        let NodeData::Comment { contents } = &doc.get_node(comment).unwrap().data else {
2324            panic!("expected a comment node");
2325        };
2326        assert_eq!(contents, "comment");
2327        assert_eq!(
2328            doc.get_node(container).unwrap().damage(),
2329            container_damage_before
2330        );
2331    }
2332}