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