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