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