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    /// Zero the layout of a node and everything under it.
767    fn clear_layout_of_subtree(doc: &mut BaseDocument, node_id: NodeId) {
768        let mut stack = vec![node_id];
769        while let Some(id) = stack.pop() {
770            let Some(node) = doc.nodes.get_mut(id) else {
771                continue;
772            };
773            // The accessors panic on node kinds that have none, so ask the data
774            // first rather than every node in the subtree: a text node has no
775            // layout of its own and a removal walk hits plenty of them.
776            if node.data.downcast_element().is_some() {
777                *node.unrounded_layout_mut() = taffy::Layout::with_order(0);
778                *node.final_layout_mut() = taffy::Layout::with_order(0);
779                node.cache_mut().clear();
780            }
781            stack.extend(node.children.iter().copied());
782        }
783    }
784
785    /// Remove the node from its parent but don't drop it.
786    pub fn remove_node(&mut self, node_id: NodeId) {
787        let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
788        // Process the subtree *before* severing the parent link so that
789        // interaction state referencing removed nodes can retarget to the
790        // nearest surviving ancestor.
791        self.process_removed_subtree(node_id);
792
793        // A detached node keeps its box otherwise, and a box is all layout and
794        // paint need: the application's boot splash was removed by its
795        // framework the moment the workspace was ready, kept a 1318x880 layout
796        // for the rest of the session, and painted its own background over
797        // whichever panel it landed on. The page looked blank. The node is
798        // deliberately not dropped, so JS wrappers stay valid, but nothing
799        // outside the document should occupy space in it.
800        Self::clear_layout_of_subtree(self.doc, node_id);
801
802        let node = &mut self.doc.nodes[node_id];
803
804        // Update child_idx values
805        if let Some(parent_id) = node.parent.take() {
806            self.mutations_occurred |= node_is_in_document;
807            let parent = &mut self.doc.nodes[parent_id];
808            parent.insert_damage(ALL_DAMAGE);
809            // Mark ancestors dirty so the style traversal visits this subtree.
810            parent.mark_ancestors_dirty();
811            parent.children.retain(|id| *id != node_id);
812            self.maybe_record_node(parent_id);
813        }
814    }
815
816    pub fn remove_and_drop_node(&mut self, node_id: NodeId) -> Option<Node> {
817        self.remove_and_drop_node_with(node_id, &mut |_| {})
818    }
819
820    /// Like [`Self::remove_and_drop_node`], but calls `on_drop` with the id of
821    /// every dropped node (the node itself and all of its descendants).
822    pub fn remove_and_drop_node_with(
823        &mut self,
824        node_id: NodeId,
825        on_drop: &mut dyn FnMut(NodeId),
826    ) -> Option<Node> {
827        let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
828        self.process_removed_subtree(node_id);
829
830        let node = self.doc.drop_node_ignoring_parent_with(node_id, on_drop);
831        self.mutations_occurred |= node_is_in_document;
832
833        // Update child_idx values
834        if let Some(parent_id) = node.as_ref().and_then(|node| node.parent) {
835            let parent = &mut self.doc.nodes[parent_id];
836            parent.insert_damage(ALL_DAMAGE);
837            let parent_is_in_doc = parent.flags.is_in_document();
838
839            // TODO: make this fine grained / conditional based on ElementSelectorFlags
840            if parent_is_in_doc {
841                if let Some(mut data) = parent
842                    .stylo_element_data_opt_mut()
843                    .and_then(|s| s.get_mut())
844                {
845                    data.hint |= RestyleHint::restyle_subtree();
846                }
847                // Mark ancestors dirty so the style traversal visits this subtree.
848                parent.mark_ancestors_dirty();
849            }
850
851            parent.children.retain(|id| *id != node_id);
852            self.maybe_record_node(parent_id);
853        }
854
855        node
856    }
857
858    pub fn remove_and_drop_all_children(&mut self, node_id: NodeId) {
859        let parent = &mut self.doc.nodes[node_id];
860        let parent_is_in_doc = parent.flags.is_in_document();
861
862        // TODO: make this fine grained / conditional based on ElementSelectorFlags
863        if parent_is_in_doc {
864            if let Some(mut data) = parent
865                .stylo_element_data_opt_mut()
866                .and_then(|s| s.get_mut())
867            {
868                data.hint |= RestyleHint::restyle_subtree();
869            }
870            // Mark ancestors dirty so the style traversal visits this subtree.
871            parent.mark_ancestors_dirty();
872        }
873
874        let children = mem::take(&mut parent.children);
875        self.mutations_occurred |= parent_is_in_doc && !children.is_empty();
876        for child_id in children {
877            self.process_removed_subtree(child_id);
878            let _ = self.doc.drop_node_ignoring_parent(child_id);
879        }
880        self.maybe_record_node(node_id);
881    }
882
883    // Tree mutation methods
884    pub fn remove_node_if_unparented(&mut self, node_id: NodeId) {
885        self.remove_node_if_unparented_with(node_id, &mut |_| {});
886    }
887
888    /// Like [`Self::remove_node_if_unparented`], but calls `on_drop` with the id of
889    /// every dropped node (the node itself and all of its descendants).
890    pub fn remove_node_if_unparented_with(
891        &mut self,
892        node_id: NodeId,
893        on_drop: &mut dyn FnMut(NodeId),
894    ) {
895        if let Some(node) = self.doc.get_node(node_id) {
896            if node.parent.is_none() {
897                self.remove_and_drop_node_with(node_id, on_drop);
898            }
899        }
900    }
901
902    /// Remove all of the children from old_parent_id and append them to new_parent_id
903    pub fn append_children(&mut self, parent_id: NodeId, child_ids: &[NodeId]) {
904        self.add_children_to_parent(parent_id, child_ids, &|parent, child_ids| {
905            parent.children.extend_from_slice(child_ids);
906        });
907    }
908
909    pub fn insert_nodes_before(&mut self, anchor_node_id: NodeId, new_node_ids: &[NodeId]) {
910        let parent_id = self.doc.nodes[anchor_node_id].parent.unwrap();
911        self.add_children_to_parent(parent_id, new_node_ids, &|parent, child_ids| {
912            let node_child_idx = parent.index_of_child(anchor_node_id).unwrap();
913            parent
914                .children
915                .splice(node_child_idx..node_child_idx, child_ids.iter().copied());
916        });
917    }
918
919    fn add_children_to_parent(
920        &mut self,
921        parent_id: NodeId,
922        child_ids: &[NodeId],
923        insert_children_fn: &dyn Fn(&mut Node, &[NodeId]),
924    ) {
925        let new_parent_is_in_document = self.doc.nodes[parent_id].flags.is_in_document();
926        self.mutations_occurred |= new_parent_is_in_document && !child_ids.is_empty();
927        // Detach the children from their old parents *before* inserting them into
928        // the new parent (matching DOM `insertBefore` semantics). If a child is
929        // being moved within the same parent then detaching it after insertion
930        // would remove both the old and the newly-inserted entries from the
931        // parent's child list, and anchor indices would be computed against a
932        // child list that still contains the moved nodes.
933        for child_id in child_ids.iter().copied() {
934            let child = &mut self.doc.nodes[child_id];
935            let child_was_in_doc = child.flags.is_in_document();
936            self.mutations_occurred |= child_was_in_doc;
937            let Some(old_parent_id) = child.parent.take() else {
938                continue;
939            };
940
941            let old_parent = &mut self.doc.nodes[old_parent_id];
942            old_parent.insert_damage(ALL_DAMAGE);
943
944            // TODO: make this fine grained / conditional based on ElementSelectorFlags
945            if child_was_in_doc {
946                if let Some(mut data) = old_parent
947                    .stylo_element_data_opt_mut()
948                    .and_then(|s| s.get_mut())
949                {
950                    data.hint |= RestyleHint::restyle_subtree();
951                }
952                // Mark ancestors dirty so the style traversal visits this subtree.
953                old_parent.mark_ancestors_dirty();
954            }
955
956            old_parent.children.retain(|id| *id != child_id);
957            self.maybe_record_node(old_parent_id);
958        }
959
960        let new_parent = &mut self.doc.nodes[parent_id];
961        new_parent.insert_damage(ALL_DAMAGE);
962
963        // TODO: make this fine grained / conditional based on ElementSelectorFlags
964        if new_parent_is_in_document {
965            if let Some(mut data) = new_parent
966                .stylo_element_data_opt_mut()
967                .and_then(|s| s.get_mut())
968            {
969                data.hint |= RestyleHint::restyle_subtree();
970            }
971            // Mark ancestors dirty so the style traversal visits this subtree.
972            new_parent.mark_ancestors_dirty();
973        }
974
975        insert_children_fn(new_parent, child_ids);
976
977        for child_id in child_ids.iter().copied() {
978            let child = &mut self.doc.nodes[child_id];
979            let child_was_in_doc = child.flags.is_in_document();
980            child.parent = Some(parent_id);
981
982            if new_parent_is_in_document && !child_was_in_doc {
983                self.process_added_subtree(child_id);
984            } else if !new_parent_is_in_document && child_was_in_doc {
985                self.process_removed_subtree(child_id);
986            }
987        }
988
989        self.maybe_record_node(parent_id);
990    }
991
992    // Tree mutation methods (that defer to other methods)
993    pub fn insert_nodes_after(&mut self, anchor_node_id: NodeId, new_node_ids: &[NodeId]) {
994        match self.next_sibling_id(anchor_node_id) {
995            Some(id) => self.insert_nodes_before(id, new_node_ids),
996            None => {
997                let parent_id = self.parent_id(anchor_node_id).unwrap();
998                self.append_children(parent_id, new_node_ids)
999            }
1000        }
1001    }
1002
1003    pub fn reparent_children(&mut self, old_parent_id: NodeId, new_parent_id: NodeId) {
1004        let child_ids = std::mem::take(&mut self.doc.nodes[old_parent_id].children);
1005        self.maybe_record_node(old_parent_id);
1006        self.append_children(new_parent_id, &child_ids);
1007    }
1008
1009    pub fn replace_node_with(&mut self, anchor_node_id: NodeId, new_node_ids: &[NodeId]) {
1010        self.insert_nodes_before(anchor_node_id, new_node_ids);
1011        self.remove_node(anchor_node_id);
1012    }
1013}
1014
1015impl<'doc> DocumentMutator<'doc> {
1016    pub fn flush(&mut self) {
1017        if self.recompute_is_animating {
1018            self.doc.has_canvas = self.doc.compute_has_canvas();
1019        }
1020
1021        if let Some(id) = self.title_node {
1022            let title = self.doc.nodes[id].text_content();
1023            self.doc.shell_provider.set_window_title(title);
1024        }
1025
1026        // Add/Update inline stylesheets (<style> elements)
1027        for id in self.style_nodes.drain() {
1028            self.doc.process_style_element(id);
1029        }
1030
1031        for id in self.form_nodes.drain() {
1032            self.doc.reset_form_owner(id);
1033        }
1034
1035        #[cfg(feature = "autofocus")]
1036        if let Some(node_id) = self.node_to_autofocus.take() {
1037            if self.doc.get_node(node_id).is_some() {
1038                self.doc.set_focus_to(node_id);
1039            }
1040        }
1041
1042        #[cfg(feature = "shadow-dom")]
1043        self.dispatch_custom_element_attr_changes();
1044    }
1045
1046    /// Dispatch all deferred custom-element `attribute_changed` callbacks.
1047    #[cfg(feature = "shadow-dom")]
1048    fn dispatch_custom_element_attr_changes(&mut self) {
1049        if self.custom_element_attr_changes.is_empty() {
1050            return;
1051        }
1052        let changes = mem::take(&mut self.custom_element_attr_changes);
1053        for (node_id, name, old_value, new_value) in changes {
1054            // Skip if the registered definition observes a restricted set that
1055            // excludes this attribute. Manually-attached controllers (no
1056            // definition) observe all attributes.
1057            let tag = self
1058                .doc
1059                .get_node(node_id)
1060                .and_then(|node| node.element_data())
1061                .map(|el| el.name.local.clone());
1062            let observed = tag
1063                .as_ref()
1064                .and_then(|tag| self.doc.custom_element_registry.get(tag))
1065                .map(|def| def.observes(&name.local))
1066                .unwrap_or(true);
1067            if !observed {
1068                continue;
1069            }
1070
1071            let Some(shadow_root_id) = self
1072                .doc
1073                .get_node(node_id)
1074                .and_then(|node| node.shadow_root_id())
1075            else {
1076                continue;
1077            };
1078            let Some(mut controller) = self.take_controller(node_id) else {
1079                continue;
1080            };
1081            {
1082                let mut ctx = crate::node::CustomElementCtx {
1083                    mutator: self,
1084                    host_id: node_id,
1085                    shadow_root_id,
1086                };
1087                controller.attribute_changed(
1088                    &mut ctx,
1089                    &name.local,
1090                    old_value.as_deref(),
1091                    new_value.as_deref(),
1092                );
1093            }
1094            self.restore_controller(node_id, controller, false);
1095        }
1096    }
1097
1098    pub fn set_inner_html(&mut self, node_id: NodeId, html: &str) {
1099        self.remove_and_drop_all_children(node_id);
1100        self.doc
1101            .html_parser_provider
1102            .clone()
1103            .parse_inner_html(self, node_id, html);
1104    }
1105
1106    fn flush_eager_ops(&mut self) {
1107        let mut ops = mem::take(&mut self.eager_op_queue);
1108        for op in ops.drain(0..) {
1109            match op {
1110                SpecialOp::LoadImage(node_id) => self.load_image(node_id),
1111                SpecialOp::LoadIframe(node_id) => self.load_iframe(node_id),
1112                SpecialOp::LoadStylesheet(node_id) => self.load_linked_stylesheet(node_id),
1113                SpecialOp::UnloadStylesheet(node_id) => self.unload_stylesheet(node_id),
1114                SpecialOp::LoadCustomPaintSource(node_id) => self.load_custom_paint_src(node_id),
1115                SpecialOp::ProcessButtonInput(node_id) => self.process_button_input(node_id),
1116                SpecialOp::UnloadSubDocument(node_id) => self.remove_sub_document(node_id),
1117                #[cfg(feature = "custom-widget")]
1118                SpecialOp::UnloadCustomWidget(node_id) => self.remove_custom_widget(node_id),
1119                #[cfg(feature = "shadow-dom")]
1120                SpecialOp::UpgradeCustomElement(node_id) => self.upgrade_custom_element(node_id),
1121                #[cfg(feature = "shadow-dom")]
1122                SpecialOp::DisconnectCustomElement(node_id) => {
1123                    self.disconnect_custom_element(node_id)
1124                }
1125            }
1126        }
1127
1128        // Queue is empty, but put Vec back anyway so allocation can be reused.
1129        self.eager_op_queue = ops;
1130    }
1131
1132    fn process_added_subtree(&mut self, node_id: NodeId) {
1133        self.doc.iter_subtree_mut(node_id, |node_id, doc| {
1134            let node = &mut doc.nodes[node_id];
1135            node.flags.set(NodeFlags::IS_IN_DOCUMENT, true);
1136            node.insert_damage(ALL_DAMAGE);
1137
1138            // If the node has an "id" attribute, store it in the ID map.
1139            if let Some(id_attr) = node.attr(local_name!("id")).map(ToString::to_string) {
1140                doc.add_to_id_map(&id_attr, node_id);
1141            }
1142
1143            let node = &mut doc.nodes[node_id];
1144            let NodeData::Element(ref mut element) = node.data else {
1145                return;
1146            };
1147
1148            // Custom post-processing by element tag name
1149            let tag = element.name.local.as_ref();
1150            match tag {
1151                "title" if element.name.ns == ns!(html) => self.title_node = Some(node_id),
1152                "link" => self.eager_op_queue.push(SpecialOp::LoadStylesheet(node_id)),
1153                "img" => self.eager_op_queue.push(SpecialOp::LoadImage(node_id)),
1154                "iframe" => self.eager_op_queue.push(SpecialOp::LoadIframe(node_id)),
1155                "canvas" => self
1156                    .eager_op_queue
1157                    .push(SpecialOp::LoadCustomPaintSource(node_id)),
1158                "style" => {
1159                    self.style_nodes.insert(node_id);
1160                }
1161                "button" | "fieldset" | "input" | "select" | "textarea" | "object" | "output" => {
1162                    self.eager_op_queue
1163                        .push(SpecialOp::ProcessButtonInput(node_id));
1164                    self.form_nodes.insert(node_id);
1165                }
1166                _ => {}
1167            }
1168
1169            // If the element's tag name matches a registered custom element
1170            // definition (and it hasn't already been upgraded), queue it for
1171            // upgrade.
1172            #[cfg(feature = "shadow-dom")]
1173            {
1174                let needs_upgrade = doc.custom_element_registry.contains(&element.name.local)
1175                    && element.custom_element_data().is_none();
1176                if needs_upgrade {
1177                    self.eager_op_queue
1178                        .push(SpecialOp::UpgradeCustomElement(node_id));
1179                }
1180            }
1181
1182            // `autofocus` is a boolean attribute: present is true, whatever
1183            // the value, and absent is the only false. Requiring the literal
1184            // string "true" meant the one spelling almost nothing uses, since
1185            // markup writes `<input autofocus>` and the parser stores that as
1186            // the empty string. Every framework agrees: Solid's boolean
1187            // attribute setter is `setAttribute(name, "")`.
1188            //
1189            // So a field marked autofocus in markup never took focus, and
1190            // blitz-script papered over its own path by writing "true" from
1191            // the property setter, which left the parsed path broken.
1192            #[cfg(feature = "autofocus")]
1193            if node.is_focussable() {
1194                if let NodeData::Element(ref element) = node.data {
1195                    if element.attr(local_name!("autofocus")).is_some() {
1196                        self.node_to_autofocus = Some(node_id);
1197                    }
1198                }
1199            }
1200        });
1201
1202        self.flush_eager_ops();
1203    }
1204
1205    fn process_removed_subtree(&mut self, node_id: NodeId) {
1206        self.doc.iter_subtree_mut(node_id, |node_id, doc| {
1207            doc.nodes[node_id]
1208                .flags
1209                .set(NodeFlags::IS_IN_DOCUMENT, false);
1210
1211            // Clear any interaction state that references this node, running
1212            // the usual teardown steps (unhover/unactive the surviving
1213            // ancestor chain, IME disable on blur of a focused input).
1214            doc.clear_interaction_state_for_removed_node(node_id);
1215
1216            let node = &mut doc.nodes[node_id];
1217
1218            // Same for focus and for the node the last press landed on.
1219            //
1220            // These two were missed, and they are the two most likely to point
1221            // at a node that is being removed: dismissing a panel is a click on
1222            // a control *inside* it, so that control is both the focused node
1223            // and the mousedown node at the moment its subtree goes away.
1224            //
1225            // A stale id here is not inert. The next click calls `set_focus_to`,
1226            // which blurs the old node by indexing it, and indexing a dropped
1227            // id panics inside the event handler. The window then stops
1228            // responding to clicks until something forces a full rebuild.
1229            //
1230            // The upstream fix carried a second failure mode, the blur landing
1231            // on whatever node had taken the recycled slot. That one cannot
1232            // happen here: `NodeId` is versioned, so a dropped id resolves to
1233            // nothing rather than aliasing its successor.
1234            if doc.focus_node_id == Some(node_id) {
1235                doc.focus_node_id = None;
1236            }
1237            if doc.mousedown_node_id == Some(node_id) {
1238                doc.mousedown_node_id = None;
1239            }
1240
1241            // Clear the text selection if one of its endpoints references this node.
1242            // This prevents stale selection endpoint references.
1243            if doc.text_selection.anchor.node_or_parent == Some(node_id)
1244                || doc.text_selection.focus.node_or_parent == Some(node_id)
1245            {
1246                doc.text_selection.clear();
1247            }
1248
1249            // Remove any snapshot for this node to prevent stale snapshot references
1250            // during style invalidation.
1251            if node.has_snapshot() {
1252                let opaque_id = style::dom::TNode::opaque(&&*node);
1253                doc.snapshots.remove(&opaque_id);
1254                node.set_has_snapshot(false);
1255            }
1256
1257            // If the node has an "id" attribute remove it from the ID map.
1258            if let Some(id_attr) = node.attr(local_name!("id")).map(ToString::to_string) {
1259                doc.remove_from_id_map(&id_attr, node_id);
1260            }
1261
1262            let node = &mut doc.nodes[node_id];
1263            let NodeData::Element(ref mut element) = node.data else {
1264                return;
1265            };
1266
1267            match &element.special_data {
1268                SpecialElementData::SubDocument(_) => {
1269                    self.eager_op_queue
1270                        .push(SpecialOp::UnloadSubDocument(node_id));
1271                }
1272                #[cfg(feature = "custom-widget")]
1273                SpecialElementData::CustomWidget(_) => {
1274                    self.eager_op_queue
1275                        .push(SpecialOp::UnloadCustomWidget(node_id));
1276                }
1277                #[cfg(feature = "shadow-dom")]
1278                SpecialElementData::CustomElement(_) => {
1279                    self.eager_op_queue
1280                        .push(SpecialOp::DisconnectCustomElement(node_id));
1281                }
1282                SpecialElementData::Stylesheet(_) => self
1283                    .eager_op_queue
1284                    .push(SpecialOp::UnloadStylesheet(node_id)),
1285                SpecialElementData::Image(_) => {}
1286                SpecialElementData::Canvas(_) => {
1287                    self.recompute_is_animating = true;
1288                }
1289                SpecialElementData::TableRoot(_) => {}
1290                SpecialElementData::TextInput(_) => {}
1291                SpecialElementData::CheckboxInput(_) => {}
1292                #[cfg(feature = "file-input")]
1293                SpecialElementData::FileInput(_) => {}
1294                SpecialElementData::None => {}
1295            }
1296        });
1297
1298        self.flush_eager_ops();
1299    }
1300
1301    fn maybe_record_node(&mut self, node_id: impl Into<Option<NodeId>>) {
1302        let Some(node_id) = node_id.into() else {
1303            return;
1304        };
1305
1306        let Some(element) = self.doc.nodes[node_id].data.downcast_element() else {
1307            return;
1308        };
1309
1310        match element.name.local.as_ref() {
1311            "title" if element.name.ns == ns!(html) => self.title_node = Some(node_id),
1312            "style" => {
1313                self.style_nodes.insert(node_id);
1314            }
1315            _ => {}
1316        }
1317    }
1318
1319    fn load_linked_stylesheet(&mut self, target_id: NodeId) {
1320        let node = &self.doc.nodes[target_id];
1321
1322        let mut is_in_head = false;
1323        let mut parent_id = node.parent;
1324        while let Some(id) = parent_id
1325            && !is_in_head
1326        {
1327            let parent = &self.doc.nodes[id];
1328            is_in_head |= parent.data.is_element_with_tag_name(&local_name!("head"));
1329            parent_id = parent.parent;
1330        }
1331
1332        let rel_attr = node.attr(local_name!("rel"));
1333        let href_attr = node.attr(local_name!("href"));
1334
1335        let (Some(rels), Some(href)) = (rel_attr, href_attr) else {
1336            return;
1337        };
1338        if !rels.split_ascii_whitespace().any(|rel| rel == "stylesheet") {
1339            return;
1340        }
1341
1342        let url = self.doc.resolve_url(href);
1343        let handler = ResourceHandler::new(
1344            self.doc.tx.clone(),
1345            self.doc.id(),
1346            Some(node.id),
1347            self.doc.shell_provider.clone(),
1348            StylesheetHandler {
1349                source_url: url.clone(),
1350                guard: self.doc.guard.clone(),
1351                net_provider: self.doc.net_provider.clone(),
1352                abort_signal: self.doc.abort_signal.clone(),
1353            },
1354        );
1355
1356        if is_in_head && !self.doc.net_provider.is_noop() {
1357            self.doc
1358                .pending_critical_resources
1359                .insert(handler.request_id());
1360        }
1361
1362        self.doc.net_provider.fetch(
1363            self.doc.id(),
1364            self.doc.build_request(url),
1365            Box::new(handler),
1366        );
1367    }
1368
1369    fn unload_stylesheet(&mut self, node_id: NodeId) {
1370        let node = &mut self.doc.nodes[node_id];
1371        let Some(element) = node.element_data_mut() else {
1372            unreachable!();
1373        };
1374        let SpecialElementData::Stylesheet(stylesheet) = element.special_data.take() else {
1375            unreachable!();
1376        };
1377
1378        let guard = self.doc.guard.read();
1379        self.doc.stylist.remove_stylesheet(stylesheet, &guard);
1380        self.doc
1381            .stylist
1382            .force_stylesheet_origins_dirty(OriginSet::all());
1383
1384        self.doc.nodes_to_stylesheet.remove(&node_id);
1385    }
1386
1387    fn load_image(&mut self, target_id: NodeId) {
1388        let node = &self.doc.nodes[target_id];
1389        if let Some(raw_src) = node.attr(local_name!("src")) {
1390            if !raw_src.is_empty() {
1391                let src = self.doc.resolve_url(raw_src);
1392                let src_string = src.as_str();
1393
1394                // Check cache first
1395                if let Some(cached_image) = self.doc.image_cache.get(src_string) {
1396                    #[cfg(feature = "tracing")]
1397                    tracing::info!("Loading image {src_string} from cache");
1398                    let node = &mut self.doc.nodes[target_id];
1399                    node.element_data_mut().unwrap().special_data =
1400                        SpecialElementData::Image(Box::new(cached_image.clone()));
1401                    node.cache_mut().clear();
1402                    node.insert_damage(ALL_DAMAGE);
1403                    return;
1404                }
1405
1406                // Check if there's already a pending request for this URL
1407                if let Some(waiting_list) = self.doc.pending_images.get_mut(src_string) {
1408                    #[cfg(feature = "tracing")]
1409                    tracing::info!("Image {src_string} already pending, queueing node {target_id}");
1410                    waiting_list.push((target_id, ImageType::Image));
1411                    return;
1412                }
1413
1414                // Start fetch and track as pending
1415                #[cfg(feature = "tracing")]
1416                tracing::info!("Fetching image {src_string}");
1417                self.doc
1418                    .pending_images
1419                    .insert(src_string.to_string(), vec![(target_id, ImageType::Image)]);
1420
1421                self.doc.net_provider.fetch(
1422                    self.doc.id(),
1423                    self.doc.build_request(src),
1424                    ResourceHandler::boxed(
1425                        self.doc.tx.clone(),
1426                        self.doc.id(),
1427                        None, // Don't pass node_id, we'll handle it via pending_images
1428                        self.doc.shell_provider.clone(),
1429                        ImageHandler::new(ImageType::Image),
1430                    ),
1431                );
1432            }
1433        }
1434    }
1435
1436    fn load_iframe(&mut self, target_id: NodeId) {
1437        if self.doc.subdocument_depth >= crate::iframe::MAX_SUBDOCUMENT_DEPTH {
1438            #[cfg(feature = "tracing")]
1439            tracing::warn!(
1440                "Not loading iframe: max sub-document nesting depth ({}) reached",
1441                crate::iframe::MAX_SUBDOCUMENT_DEPTH
1442            );
1443            return;
1444        }
1445
1446        let node = &self.doc.nodes[target_id];
1447        let Some(element) = node.element_data() else {
1448            return;
1449        };
1450
1451        // `srcdoc` takes precedence over `src`
1452        if let Some(srcdoc) = element.attr(local_name!("srcdoc")) {
1453            let srcdoc = srcdoc.to_string();
1454            self.doc.load_iframe_srcdoc(target_id, &srcdoc);
1455            return;
1456        }
1457
1458        let Some(raw_src) = element.attr(local_name!("src")) else {
1459            return;
1460        };
1461        if raw_src.is_empty() {
1462            return;
1463        }
1464        let Some(url) = self.doc.url.resolve_relative(raw_src) else {
1465            #[cfg(feature = "tracing")]
1466            tracing::warn!("Not loading iframe: could not resolve url {raw_src}");
1467            return;
1468        };
1469        self.doc.start_iframe_load(target_id, url);
1470    }
1471
1472    fn load_custom_paint_src(&mut self, target_id: NodeId) {
1473        let node = &mut self.doc.nodes[target_id];
1474        if let Some(raw_src) = node.attr(local_name!("src")) {
1475            if let Ok(custom_paint_source_id) = raw_src.parse::<u64>() {
1476                self.recompute_is_animating = true;
1477                let canvas_data = SpecialElementData::Canvas(CanvasData {
1478                    custom_paint_source_id,
1479                });
1480                node.element_data_mut().unwrap().special_data = canvas_data;
1481            }
1482        }
1483    }
1484
1485    fn process_button_input(&mut self, target_id: NodeId) {
1486        let node = &self.doc.nodes[target_id];
1487        let Some(data) = node.element_data() else {
1488            return;
1489        };
1490
1491        let tagname = data.name.local.as_ref();
1492        let type_attr = data.attr(local_name!("type"));
1493        let value = data.attr(local_name!("value"));
1494
1495        // Add content of "value" attribute as a text node child if:
1496        //   - Tag name is
1497        if let ("input", Some("button" | "submit" | "reset"), Some(value)) =
1498            (tagname, type_attr, value)
1499        {
1500            let value = value.to_string();
1501            let id = self.create_text_node(&value);
1502            self.append_children(target_id, &[id]);
1503            return;
1504        }
1505        #[cfg(feature = "file-input")]
1506        if let ("input", Some("file")) = (tagname, type_attr) {
1507            let button_id = self.create_element(
1508                qual_name!("button", html),
1509                vec![
1510                    Attribute {
1511                        name: qual_name!("type", html),
1512                        value: "button".into(),
1513                    },
1514                    Attribute {
1515                        name: qual_name!("tabindex", html),
1516                        value: "-1".into(),
1517                    },
1518                ],
1519            );
1520            let label_id = self.create_element(qual_name!("label", html), vec![]);
1521            let text_id = self.create_text_node("No File Selected");
1522            let button_text_id = self.create_text_node("Browse");
1523            self.append_children(target_id, &[button_id, label_id]);
1524            self.append_children(label_id, &[text_id]);
1525            self.append_children(button_id, &[button_text_id]);
1526        }
1527    }
1528}
1529
1530/// Set 'checked' state on an input based on given attributevalue
1531fn set_input_checked_state(element: &mut ElementData, value: String) {
1532    let Ok(checked) = value.parse() else {
1533        return;
1534    };
1535    match element.special_data {
1536        SpecialElementData::CheckboxInput(ref mut checked_mut) => *checked_mut = checked,
1537        // If we have just constructed the element, set the node attribute,
1538        // and NodeSpecificData will be created from that later
1539        // this simulates the checked attribute being set in html,
1540        // and the element's checked property being set from that
1541        SpecialElementData::None => element.attrs.push(Attribute {
1542            name: qual_name!("checked", html),
1543            value: checked.to_string().into(),
1544        }),
1545        _ => {}
1546    }
1547}
1548
1549/// Type that allows mutable access to the viewport
1550/// And syncs it back to stylist on drop.
1551pub struct ViewportMut<'doc> {
1552    doc: &'doc mut BaseDocument,
1553    initial_viewport: Viewport,
1554}
1555impl ViewportMut<'_> {
1556    pub fn new(doc: &mut BaseDocument) -> ViewportMut<'_> {
1557        let initial_viewport = doc.viewport.clone();
1558        ViewportMut {
1559            doc,
1560            initial_viewport,
1561        }
1562    }
1563}
1564impl Deref for ViewportMut<'_> {
1565    type Target = Viewport;
1566
1567    fn deref(&self) -> &Self::Target {
1568        &self.doc.viewport
1569    }
1570}
1571impl DerefMut for ViewportMut<'_> {
1572    fn deref_mut(&mut self) -> &mut Self::Target {
1573        &mut self.doc.viewport
1574    }
1575}
1576impl Drop for ViewportMut<'_> {
1577    fn drop(&mut self) {
1578        if self.doc.viewport == self.initial_viewport {
1579            return;
1580        }
1581
1582        self.doc.set_stylist_device(make_device(
1583            &self.doc.viewport,
1584            self.doc.media_type.clone(),
1585            self.doc.font_ctx.clone(),
1586        ));
1587        self.doc.scroll_viewport_by(0.0, 0.0); // Clamp scroll offset
1588
1589        let scale_has_changed =
1590            self.doc.viewport().scale_f64() != self.initial_viewport.scale_f64();
1591        if scale_has_changed {
1592            self.doc.invalidate_inline_contexts();
1593            self.doc.shell_provider.request_redraw();
1594        }
1595    }
1596}
1597
1598#[cfg(test)]
1599mod test {
1600    use style::media_queries::MediaType;
1601    use style_dom::ElementState;
1602
1603    use std::sync::{
1604        Arc,
1605        atomic::{AtomicUsize, Ordering},
1606    };
1607
1608    use blitz_traits::shell::{ColorScheme, ShellProvider, Viewport};
1609
1610    use crate::{
1611        Attribute, BaseDocument, DocumentConfig, ElementData, NodeData, NodeId, qual_name,
1612    };
1613
1614    #[test]
1615    fn media_type_defaults_to_screen() {
1616        let mut document = BaseDocument::new(DocumentConfig::default());
1617        assert_eq!(*document.media_type(), MediaType::screen());
1618        assert_eq!(document.stylist_device().media_type(), MediaType::screen());
1619    }
1620
1621    #[test]
1622    fn media_type_honors_config() {
1623        let mut document = BaseDocument::new(DocumentConfig {
1624            media_type: Some(MediaType::print()),
1625            ..Default::default()
1626        });
1627        assert_eq!(*document.media_type(), MediaType::print());
1628        assert_eq!(document.stylist_device().media_type(), MediaType::print());
1629    }
1630
1631    #[test]
1632    fn set_media_type_updates_stylist_device() {
1633        let mut document = BaseDocument::new(DocumentConfig::default());
1634        assert_eq!(document.stylist_device().media_type(), MediaType::screen());
1635
1636        document.set_media_type(MediaType::print());
1637        assert_eq!(*document.media_type(), MediaType::print());
1638        assert_eq!(document.stylist_device().media_type(), MediaType::print());
1639    }
1640
1641    #[test]
1642    fn removing_a_node_forgets_it_as_focused_and_pressed() {
1643        // Dismissing a panel is a click on a control inside it, so at that
1644        // moment the control is both the focused node and the mousedown node,
1645        // and then its subtree goes away. Removal used to clear hover, active
1646        // and the selection endpoints but leave these two, and the next click
1647        // indexed a dropped id and panicked inside the event handler.
1648        let mut document = BaseDocument::new(DocumentConfig::default());
1649        let button = document.create_node(NodeData::Element(Box::new(ElementData::new(
1650            qual_name!("button"),
1651            Vec::new(),
1652        ))));
1653        let root = document.root_node().id;
1654
1655        let mut mutator = document.mutate();
1656        mutator.append_children(root, &[button]);
1657        drop(mutator);
1658
1659        document.set_focus_to(button);
1660        document.set_mousedown_node_id(Some(button));
1661        assert_eq!(document.get_focussed_node_id(), Some(button));
1662        assert_eq!(document.mousedown_node_id, Some(button));
1663
1664        let mut mutator = document.mutate();
1665        mutator.remove_node(button);
1666        drop(mutator);
1667
1668        assert_eq!(
1669            document.get_focussed_node_id(),
1670            None,
1671            "a removed node must not stay focused"
1672        );
1673        assert_eq!(
1674            document.mousedown_node_id, None,
1675            "a removed node must not stay the pressed node"
1676        );
1677    }
1678
1679    #[test]
1680    fn mutator_remove_disabled() {
1681        let mut document = BaseDocument::new(DocumentConfig::default());
1682        let id = document.create_node(NodeData::Element(Box::new(ElementData::new(
1683            qual_name!("button"),
1684            vec![Attribute {
1685                name: qual_name!("disabled"),
1686                value: "".into(),
1687            }],
1688        ))));
1689
1690        let node = document.get_node(id).unwrap();
1691        assert!(
1692            node.element_state().contains(ElementState::DISABLED),
1693            "form node is disabled"
1694        );
1695        assert!(
1696            !node.element_state().contains(ElementState::ENABLED),
1697            "form node is not enabled yet"
1698        );
1699
1700        let mut mutator = document.mutate();
1701        mutator.clear_attribute(id, qual_name!("disabled"));
1702        drop(mutator);
1703
1704        let node = document.get_node(id).unwrap();
1705        assert!(
1706            !node.element_state().contains(ElementState::DISABLED),
1707            "form node is no longer disabled"
1708        );
1709        assert!(
1710            node.element_state().contains(ElementState::ENABLED),
1711            "form node is enabled"
1712        );
1713    }
1714
1715    #[test]
1716    fn mutator_set_disabled() {
1717        let mut document = BaseDocument::new(DocumentConfig::default());
1718        let id = document.create_node(NodeData::Element(Box::new(ElementData::new(
1719            qual_name!("button"),
1720            vec![],
1721        ))));
1722
1723        let node = document.get_node(id).unwrap();
1724        assert!(
1725            !node.element_state().contains(ElementState::DISABLED),
1726            "form node is not disabled"
1727        );
1728        assert!(
1729            node.element_state().contains(ElementState::ENABLED),
1730            "form node is enabled"
1731        );
1732
1733        let mut mutator = document.mutate();
1734        mutator.set_attribute(id, qual_name!("disabled"), "");
1735        drop(mutator);
1736
1737        let node = document.get_node(id).unwrap();
1738
1739        assert!(
1740            node.element_state().contains(ElementState::DISABLED),
1741            "form node is disabled"
1742        );
1743        assert!(
1744            !node.element_state().contains(ElementState::ENABLED),
1745            "form node is no longer enabled enabled"
1746        );
1747    }
1748
1749    #[test]
1750    fn mutator_set_disabled_invalid_node() {
1751        let mut document = BaseDocument::new(DocumentConfig::default());
1752        let id = document.create_node(NodeData::Element(Box::new(ElementData::new(
1753            qual_name!("a"),
1754            vec![],
1755        ))));
1756
1757        let node = document.get_node(id).unwrap();
1758        assert!(
1759            !node.element_state().contains(ElementState::DISABLED),
1760            "form node is not disabled"
1761        );
1762        assert!(
1763            !node.element_state().contains(ElementState::ENABLED),
1764            "form node is enabled"
1765        );
1766
1767        let mut mutator = document.mutate();
1768        mutator.set_attribute(id, qual_name!("disabled"), "");
1769        drop(mutator);
1770
1771        let node = document.get_node(id).unwrap();
1772        assert!(
1773            !node.element_state().contains(ElementState::DISABLED),
1774            "form node is not disabled"
1775        );
1776        assert!(
1777            !node.element_state().contains(ElementState::ENABLED),
1778            "form node is enabled"
1779        );
1780    }
1781
1782    #[test]
1783    fn mutator_id_attribute_updates_id_map() {
1784        let mut document = BaseDocument::new(DocumentConfig::default());
1785        let root_id = document.root_node().id;
1786
1787        let node_id = {
1788            let mut mutator = document.mutate();
1789            let node_id = mutator.create_element(
1790                qual_name!("div"),
1791                vec![Attribute {
1792                    name: qual_name!("id"),
1793                    value: "old".into(),
1794                }],
1795            );
1796            mutator.append_children(root_id, &[node_id]);
1797            node_id
1798        };
1799        assert_eq!(document.get_element_by_id("old"), Some(node_id));
1800
1801        {
1802            let mut mutator = document.mutate();
1803            mutator.set_attribute(node_id, qual_name!("id"), "new");
1804        }
1805        assert_eq!(document.get_element_by_id("new"), Some(node_id));
1806        assert_eq!(document.get_element_by_id("old"), None);
1807
1808        {
1809            let mut mutator = document.mutate();
1810            mutator.clear_attribute(node_id, qual_name!("id"));
1811        }
1812        assert_eq!(document.get_element_by_id("new"), None);
1813    }
1814
1815    #[test]
1816    fn get_element_by_id_duplicate_ids_first_in_tree_order_wins() {
1817        let mut document = BaseDocument::new(DocumentConfig::default());
1818        let root_id = document.root_node().id;
1819
1820        let (first_id, second_id) = {
1821            let mut mutator = document.mutate();
1822            let first_id = mutator.create_element(qual_name!("div"), vec![]);
1823            let second_id = mutator.create_element(qual_name!("div"), vec![]);
1824            mutator.append_children(root_id, &[first_id, second_id]);
1825            // Assign the id to the later node first so that insertion order
1826            // differs from tree order
1827            mutator.set_attribute(second_id, qual_name!("id"), "dup");
1828            mutator.set_attribute(first_id, qual_name!("id"), "dup");
1829            (first_id, second_id)
1830        };
1831        assert_eq!(document.get_element_by_id("dup"), Some(first_id));
1832
1833        {
1834            let mut mutator = document.mutate();
1835            mutator.remove_node(first_id);
1836        }
1837        assert_eq!(document.get_element_by_id("dup"), Some(second_id));
1838    }
1839
1840    #[derive(Default)]
1841    struct RedrawShell {
1842        redraw_requests: AtomicUsize,
1843    }
1844
1845    impl ShellProvider for RedrawShell {
1846        fn request_redraw(&self) {
1847            self.redraw_requests.fetch_add(1, Ordering::Relaxed);
1848        }
1849    }
1850
1851    #[test]
1852    fn mutator_requests_redraw_only_after_mutation() {
1853        let shell = Arc::new(RedrawShell::default());
1854        let mut document = BaseDocument::new(DocumentConfig {
1855            shell_provider: Some(shell.clone()),
1856            ..Default::default()
1857        });
1858        let root_id = document.root_node().id;
1859
1860        {
1861            let mut mutator = document.mutate();
1862            let parent_id = mutator.create_element(qual_name!("div"), vec![]);
1863            let child_id = mutator.create_element(qual_name!("span"), vec![]);
1864            mutator.append_children(parent_id, &[child_id]);
1865            mutator.remove_and_drop_all_children(parent_id);
1866            mutator.set_attribute(parent_id, qual_name!("id"), "detached");
1867        }
1868        assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 0);
1869
1870        {
1871            let mutator = document.mutate();
1872            assert_eq!(mutator.child_ids(root_id).len(), 0);
1873        }
1874
1875        {
1876            let mut mutator = document.mutate();
1877            let node_id = mutator.create_element(qual_name!("div"), vec![]);
1878            mutator.append_children(root_id, &[node_id]);
1879            mutator.set_attribute(node_id, qual_name!("id"), "in-document");
1880        }
1881        assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 1);
1882
1883        {
1884            let mut mutator = document.mutate();
1885            let parent_id = mutator.create_element(qual_name!("div"), vec![]);
1886            let child_id = mutator.create_element(qual_name!("span"), vec![]);
1887            mutator.append_children(root_id, &[parent_id]);
1888            mutator.append_children(parent_id, &[child_id]);
1889            mutator.remove_and_drop_all_children(parent_id);
1890        }
1891        assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 2);
1892
1893        {
1894            let mut mutator = document.mutate();
1895            let parent_id = mutator.create_element(qual_name!("div"), vec![]);
1896            let child_id = mutator.create_element(qual_name!("span"), vec![]);
1897            let detached_target_id = mutator.create_element(qual_name!("div"), vec![]);
1898            mutator.append_children(root_id, &[parent_id]);
1899            mutator.append_children(parent_id, &[child_id]);
1900            assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 2);
1901            mutator.append_children(detached_target_id, &[child_id]);
1902        }
1903        assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 3);
1904    }
1905
1906    #[test]
1907    fn moving_subtree_out_of_document_clears_in_document_flag() {
1908        let shell = Arc::new(RedrawShell::default());
1909        let mut document = BaseDocument::new(DocumentConfig {
1910            shell_provider: Some(shell.clone()),
1911            ..Default::default()
1912        });
1913        let root_id = document.root_node().id;
1914        let (child_id, grandchild_id, detached_parent_id) = {
1915            let mut mutator = document.mutate();
1916            let in_document_parent_id = mutator.create_element(qual_name!("div"), vec![]);
1917            let child_id = mutator.create_element(qual_name!("div"), vec![]);
1918            let grandchild_id = mutator.create_element(qual_name!("span"), vec![]);
1919            let detached_parent_id = mutator.create_element(qual_name!("section"), vec![]);
1920            mutator.append_children(root_id, &[in_document_parent_id]);
1921            mutator.append_children(in_document_parent_id, &[child_id]);
1922            mutator.append_children(child_id, &[grandchild_id]);
1923            (child_id, grandchild_id, detached_parent_id)
1924        };
1925        assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 1);
1926        assert!(document.get_node(child_id).unwrap().flags.is_in_document());
1927        assert!(
1928            document
1929                .get_node(grandchild_id)
1930                .unwrap()
1931                .flags
1932                .is_in_document()
1933        );
1934
1935        {
1936            let mut mutator = document.mutate();
1937            mutator.append_children(detached_parent_id, &[child_id]);
1938        }
1939        assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 2);
1940        assert!(!document.get_node(child_id).unwrap().flags.is_in_document());
1941        assert!(
1942            !document
1943                .get_node(grandchild_id)
1944                .unwrap()
1945                .flags
1946                .is_in_document()
1947        );
1948
1949        {
1950            let mut mutator = document.mutate();
1951            mutator.set_attribute(child_id, qual_name!("id"), "detached");
1952        }
1953        assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 2);
1954
1955        {
1956            let mut mutator = document.mutate();
1957            mutator.append_children(root_id, &[child_id]);
1958        }
1959        assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 3);
1960        assert!(document.get_node(child_id).unwrap().flags.is_in_document());
1961        assert!(
1962            document
1963                .get_node(grandchild_id)
1964                .unwrap()
1965                .flags
1966                .is_in_document()
1967        );
1968    }
1969
1970    /// A `calc()` does not reach taffy as a value. `stylo_taffy` hands it over
1971    /// as a raw pointer into the node's `ComputedValues`, and layout
1972    /// dereferences that pointer on every resolve, so the cached taffy style
1973    /// must never outlive the arc it was built from.
1974    ///
1975    /// A restyle that lands no relayout damage still replaces those computed
1976    /// values. Colour is the cheapest example and it is the real one: a slow
1977    /// command's response restyled the project header two seconds after boot,
1978    /// the header's absolutely positioned chip carries
1979    /// `max-width: calc(100% - 24px)`, and 0.6.x experimental died there in
1980    /// three different ways depending on what had taken the freed allocation.
1981    #[test]
1982    fn a_paint_only_restyle_refreshes_the_calc_the_taffy_style_points_at() {
1983        use style::servo_arc::Arc as ServoArc;
1984
1985        let mut document = BaseDocument::new(DocumentConfig {
1986            viewport: Some(Viewport::new(800, 600, 1.0, ColorScheme::Light)),
1987            ..Default::default()
1988        });
1989        let root_id = document.root_node().id;
1990
1991        let (header_id, chip_id) = {
1992            let mut mutator = document.mutate();
1993            let header_id = mutator.create_element(qual_name!("div"), vec![]);
1994            let chip_id = mutator.create_element(qual_name!("span"), vec![]);
1995            mutator.set_style_property(header_id, "position", "relative");
1996            mutator.set_style_property(header_id, "width", "800px");
1997            mutator.set_style_property(header_id, "height", "60px");
1998            mutator.set_style_property(chip_id, "position", "absolute");
1999            mutator.set_style_property(chip_id, "max-width", "calc(100% - 24px)");
2000            mutator.set_style_property(chip_id, "color", "rgb(1, 2, 3)");
2001            mutator.append_children(header_id, &[chip_id]);
2002            mutator.append_children(root_id, &[header_id]);
2003            (header_id, chip_id)
2004        };
2005
2006        document.resolve(0.0);
2007
2008        // Restyled through inheritance, not directly: the chip's own mutation
2009        // damage would force a rebuild and hide the hazard. Recolouring the
2010        // parent recomputes the child's values — a new arc — while the child's
2011        // own damage stays repaint-only, which is exactly the gap the gate left
2012        // open.
2013        {
2014            let mut mutator = document.mutate();
2015            mutator.set_style_property(header_id, "color", "rgb(4, 5, 6)");
2016        }
2017        document.resolve(0.0);
2018
2019        let node = document.get_node(chip_id).unwrap();
2020        let stylo_data = node.stylo_element_data_opt().and_then(|data| data.get());
2021        let primary = stylo_data
2022            .as_ref()
2023            .and_then(|data| data.styles.get_primary())
2024            .expect("the chip is styled");
2025        let source = node
2026            .style_source_opt()
2027            .expect("a styled node records the computed values its taffy style was built from");
2028
2029        assert!(
2030            ServoArc::ptr_eq(primary, source),
2031            "the cached taffy style still points into computed values that a restyle replaced, \
2032             so every calc() in it is a dangling pointer",
2033        );
2034    }
2035
2036    #[test]
2037    fn style_property_updates_nested_layout() {
2038        let mut document = BaseDocument::new(DocumentConfig {
2039            viewport: Some(Viewport::new(800, 600, 1.0, ColorScheme::Light)),
2040            ..Default::default()
2041        });
2042        let root_id = document.root_node().id;
2043
2044        let mover_id = {
2045            let mut mutator = document.mutate();
2046            let parent_id = mutator.create_element(qual_name!("div"), vec![]);
2047            let mover_id = mutator.create_element(qual_name!("div"), vec![]);
2048            mutator.set_style_property(parent_id, "position", "relative");
2049            mutator.set_style_property(parent_id, "width", "800px");
2050            mutator.set_style_property(parent_id, "height", "600px");
2051            mutator.set_style_property(mover_id, "position", "absolute");
2052            mutator.set_style_property(mover_id, "left", "0px");
2053            mutator.set_style_property(mover_id, "top", "0px");
2054            mutator.append_children(parent_id, &[mover_id]);
2055            mutator.append_children(root_id, &[parent_id]);
2056            mover_id
2057        };
2058
2059        document.resolve(0.0);
2060        assert_eq!(
2061            document
2062                .get_node(mover_id)
2063                .unwrap()
2064                .final_layout()
2065                .location
2066                .x,
2067            0.0
2068        );
2069
2070        {
2071            let mut mutator = document.mutate();
2072            mutator.set_style_property(mover_id, "left", "120px");
2073        }
2074
2075        document.resolve(0.0);
2076        assert_eq!(
2077            document
2078                .get_node(mover_id)
2079                .unwrap()
2080                .final_layout()
2081                .location
2082                .x,
2083            120.0
2084        );
2085    }
2086
2087    /// `<html><body><div>text<!--comment--></div></body></html>`, laid out
2088    /// once, returning the text and comment ids.
2089    fn doc_with_a_comment() -> (BaseDocument, NodeId, NodeId, NodeId) {
2090        let mut doc = BaseDocument::new(DocumentConfig {
2091            viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
2092            ..Default::default()
2093        });
2094        let root_id = doc.root_node().id;
2095
2096        let mut mutr = doc.mutate();
2097        let html = mutr.create_element(qual_name!("html"), vec![]);
2098        let body = mutr.create_element(qual_name!("body"), vec![]);
2099        let container = mutr.create_element(qual_name!("div"), vec![]);
2100        let text = mutr.create_text_node("text");
2101        let comment = mutr.create_comment_node("comment");
2102        mutr.append_children(container, &[text, comment]);
2103        mutr.append_children(body, &[container]);
2104        mutr.append_children(html, &[body]);
2105        mutr.append_children(root_id, &[html]);
2106        drop(mutr);
2107
2108        doc.resolve(0.0);
2109        (doc, container, text, comment)
2110    }
2111
2112    /// A comment is CharacterData: `comment.data = "x"` has to land somewhere.
2113    /// Before this arm existed it fell through and vanished, so a getter that
2114    /// returned the contents would have disagreed with every write.
2115    #[test]
2116    fn setting_a_comments_data_writes_the_contents() {
2117        let (mut doc, _container, _text, comment) = doc_with_a_comment();
2118
2119        doc.mutate().set_node_text(comment, "rewritten");
2120
2121        let NodeData::Comment { contents } = &doc.get_node(comment).unwrap().data else {
2122            panic!("expected a comment node");
2123        };
2124        assert_eq!(contents, "rewritten");
2125    }
2126
2127    /// A comment generates no box, so writing its data must not schedule a
2128    /// relayout. Without this the obvious implementation (copy the Text arm)
2129    /// costs a full resolve per write, and nothing observable would say so.
2130    ///
2131    /// The text-node write at the end is the control: it proves the assertion
2132    /// above is capable of failing.
2133    #[test]
2134    fn setting_a_comments_data_does_not_dirty_layout() {
2135        let (mut doc, container, text, comment) = doc_with_a_comment();
2136
2137        let container_damage_before = doc.get_node(container).unwrap().damage();
2138        let comment_damage_before = doc.get_node(comment).unwrap().damage();
2139
2140        doc.mutate().set_node_text(comment, "rewritten");
2141
2142        assert_eq!(
2143            doc.get_node(comment).unwrap().damage(),
2144            comment_damage_before,
2145            "writing a comment's data damaged the comment"
2146        );
2147        assert_eq!(
2148            doc.get_node(container).unwrap().damage(),
2149            container_damage_before,
2150            "writing a comment's data damaged its parent, scheduling a relayout \
2151             for a change that cannot affect a pixel"
2152        );
2153
2154        doc.mutate().set_node_text(text, "rewritten");
2155        assert_ne!(
2156            doc.get_node(container).unwrap().damage(),
2157            container_damage_before,
2158            "a text write should damage the parent, so the assertions above can fail"
2159        );
2160    }
2161
2162    /// Writing the same contents back is not a change, and must stay as inert
2163    /// as a write of different contents.
2164    #[test]
2165    fn rewriting_a_comment_with_its_own_contents_is_inert() {
2166        let (mut doc, container, _text, comment) = doc_with_a_comment();
2167        let container_damage_before = doc.get_node(container).unwrap().damage();
2168
2169        doc.mutate().set_node_text(comment, "comment");
2170
2171        let NodeData::Comment { contents } = &doc.get_node(comment).unwrap().data else {
2172            panic!("expected a comment node");
2173        };
2174        assert_eq!(contents, "comment");
2175        assert_eq!(
2176            doc.get_node(container).unwrap().damage(),
2177            container_damage_before
2178        );
2179    }
2180}