Skip to main content

blitz_dom/
mutator.rs

1use std::collections::HashSet;
2use std::mem;
3use std::ops::{Deref, DerefMut};
4
5use crate::document::make_device;
6use crate::layout::damage::ALL_DAMAGE;
7use crate::net::{ImageHandler, ResourceHandler, StylesheetHandler};
8use crate::node::{CanvasData, NodeFlags, SpecialElementData};
9use crate::util::ImageType;
10use crate::{
11    Attribute, BaseDocument, Document, ElementData, Node, NodeData, QualName, local_name, qual_name,
12};
13use blitz_traits::shell::Viewport;
14use style::Atom;
15use style::invalidation::element::restyle_hints::RestyleHint;
16use style::stylesheets::OriginSet;
17
18macro_rules! tag_and_attr {
19    ($tag:tt, $attr:tt) => {
20        (&local_name!($tag), &local_name!($attr))
21    };
22}
23
24#[derive(Debug, Clone)]
25pub enum AppendTextErr {
26    /// The node is not a text node
27    NotTextNode,
28}
29
30/// Operations that happen almost immediately, but are deferred within a
31/// function for borrow-checker reasons.
32enum SpecialOp {
33    LoadImage(usize),
34    LoadStylesheet(usize),
35    UnloadStylesheet(usize),
36    LoadCustomPaintSource(usize),
37    ProcessButtonInput(usize),
38    UnloadSubDocument(usize),
39    #[cfg(feature = "custom-widget")]
40    UnloadCustomWidget(usize),
41}
42
43pub struct DocumentMutator<'doc> {
44    /// Document is public as an escape hatch, but users of this API should ideally avoid using it
45    /// and prefer exposing additional functionality in DocumentMutator.
46    pub doc: &'doc mut BaseDocument,
47
48    eager_op_queue: Vec<SpecialOp>,
49
50    // Tracked nodes for deferred processing when mutations have completed
51    title_node: Option<usize>,
52    style_nodes: HashSet<usize>,
53    form_nodes: HashSet<usize>,
54
55    /// Whether an element/attribute that affect animation status has been seen
56    recompute_is_animating: bool,
57
58    /// The (latest) node which has been mounted in and had autofocus=true, if any
59    #[cfg(feature = "autofocus")]
60    node_to_autofocus: Option<usize>,
61}
62
63impl Drop for DocumentMutator<'_> {
64    fn drop(&mut self) {
65        self.flush(); // Defined at bottom of file
66    }
67}
68
69impl DocumentMutator<'_> {
70    pub fn new<'doc>(doc: &'doc mut BaseDocument) -> DocumentMutator<'doc> {
71        DocumentMutator {
72            doc,
73            eager_op_queue: Vec::new(),
74            title_node: None,
75            style_nodes: HashSet::new(),
76            form_nodes: HashSet::new(),
77            recompute_is_animating: false,
78            #[cfg(feature = "autofocus")]
79            node_to_autofocus: None,
80        }
81    }
82
83    // Query methods
84
85    pub fn node_has_parent(&self, node_id: usize) -> bool {
86        self.doc.nodes[node_id].parent.is_some()
87    }
88
89    pub fn previous_sibling_id(&self, node_id: usize) -> Option<usize> {
90        self.doc.nodes[node_id].backward(1).map(|node| node.id)
91    }
92
93    pub fn next_sibling_id(&self, node_id: usize) -> Option<usize> {
94        self.doc.nodes[node_id].forward(1).map(|node| node.id)
95    }
96
97    pub fn parent_id(&self, node_id: usize) -> Option<usize> {
98        self.doc.nodes[node_id].parent
99    }
100
101    pub fn last_child_id(&self, node_id: usize) -> Option<usize> {
102        self.doc.nodes[node_id].children.last().copied()
103    }
104
105    pub fn child_ids(&self, node_id: usize) -> Vec<usize> {
106        self.doc.nodes[node_id].children.clone()
107    }
108
109    pub fn element_name(&self, node_id: usize) -> Option<&QualName> {
110        self.doc.nodes[node_id].element_data().map(|el| &el.name)
111    }
112
113    pub fn node_at_path(&self, start_node_id: usize, path: &[u8]) -> usize {
114        let mut current = &self.doc.nodes[start_node_id];
115        for i in path {
116            let new_id = current.children[*i as usize];
117            current = &self.doc.nodes[new_id];
118        }
119        current.id
120    }
121
122    // Node creation methods
123
124    pub fn create_comment_node(&mut self) -> usize {
125        self.doc.create_node(NodeData::Comment)
126    }
127
128    pub fn create_text_node(&mut self, text: &str) -> usize {
129        self.doc.create_text_node(text)
130    }
131
132    pub fn create_element(&mut self, name: QualName, attrs: Vec<Attribute>) -> usize {
133        let mut data = ElementData::new(name, attrs);
134        data.flush_style_attribute(self.doc.guard(), &self.doc.url.url_extra_data());
135
136        let id = self.doc.create_node(NodeData::Element(data));
137        let node = self.doc.get_node_mut(id).unwrap();
138
139        // Initialise style data
140        *node.stylo_element_data.ensure_init_mut() = style::data::ElementData {
141            damage: ALL_DAMAGE,
142            ..Default::default()
143        };
144
145        id
146    }
147
148    pub fn deep_clone_node(&mut self, node_id: usize) -> usize {
149        self.doc.deep_clone_node(node_id)
150    }
151
152    // Node mutation methods
153
154    pub fn set_node_text(&mut self, node_id: usize, value: &str) {
155        let node = &mut self.doc.nodes[node_id];
156
157        let text = match node.data {
158            NodeData::Text(ref mut text) => text,
159            // TODO: otherwise this is basically element.textContent which is a bit different - need to parse as html
160            _ => return,
161        };
162
163        let changed = text.content != value;
164        if changed {
165            text.content.clear();
166            text.content.push_str(value);
167            node.insert_damage(ALL_DAMAGE);
168            // Mark ancestors dirty so the style traversal visits this subtree.
169            // Without this, the traversal may skip nodes with pending damage.
170            node.mark_ancestors_dirty();
171            let parent_id = node.parent;
172
173            // Also insert damage on the parent element, since text content changes
174            // affect the parent's layout (text may wrap differently, change size, etc.)
175            if let Some(parent_id) = parent_id {
176                let parent = &mut self.doc.nodes[parent_id];
177                parent.insert_damage(ALL_DAMAGE);
178            }
179
180            self.maybe_record_node(parent_id);
181        }
182    }
183
184    pub fn append_text_to_node(&mut self, node_id: usize, text: &str) -> Result<(), AppendTextErr> {
185        let node = &mut self.doc.nodes[node_id];
186        node.insert_damage(ALL_DAMAGE);
187        node.mark_ancestors_dirty();
188        match node.text_data_mut() {
189            Some(data) => {
190                data.content += text;
191                Ok(())
192            }
193            None => Err(AppendTextErr::NotTextNode),
194        }
195    }
196
197    pub fn add_attrs_if_missing(&mut self, node_id: usize, attrs: Vec<Attribute>) {
198        let node = &mut self.doc.nodes[node_id];
199        node.insert_damage(ALL_DAMAGE);
200        let element_data = node.element_data_mut().expect("Not an element");
201
202        let existing_names = element_data
203            .attrs
204            .iter()
205            .map(|e| e.name.clone())
206            .collect::<HashSet<_>>();
207
208        for attr in attrs
209            .into_iter()
210            .filter(|attr| !existing_names.contains(&attr.name))
211        {
212            self.set_attribute(node_id, attr.name, &attr.value);
213        }
214    }
215
216    pub fn set_attribute(&mut self, node_id: usize, name: QualName, value: &str) {
217        let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
218        if node_is_in_document {
219            self.doc.snapshot_node(node_id);
220
221            let node = &mut self.doc.nodes[node_id];
222            if let Some(mut data) = node.stylo_element_data.get_mut() {
223                data.hint |= RestyleHint::restyle_subtree();
224                data.damage.insert(ALL_DAMAGE);
225            }
226
227            // TODO: make this fine grained / conditional based on ElementSelectorFlags
228            let parent = node.parent;
229            if let Some(parent_id) = parent {
230                let parent = &mut self.doc.nodes[parent_id];
231                if let Some(mut data) = parent.stylo_element_data.get_mut() {
232                    data.hint |= RestyleHint::restyle_subtree();
233                }
234            }
235
236            // Mark ancestors dirty so the style traversal visits this subtree.
237            // Without this, the traversal may skip nodes with pending RestyleHint/damage
238            // because it uses dirty_descendants flags to determine which subtrees to visit.
239            self.doc.nodes[node_id].mark_ancestors_dirty();
240        }
241
242        let node = &mut self.doc.nodes[node_id];
243
244        let NodeData::Element(ref mut element) = node.data else {
245            return;
246        };
247
248        // If element is a CustomWidget, then Ccall attribute_changed on it
249        #[cfg(feature = "custom-widget")]
250        if let SpecialElementData::CustomWidget(widget_data) = &mut element.special_data {
251            let old_value = element.attrs.get(&name).as_ref().map(|attr| &*attr.value);
252            widget_data
253                .widget
254                .attribute_changed(&name.local, old_value, Some(value));
255        }
256
257        element.attrs.set(name.clone(), value);
258
259        let tag = &element.name.local;
260        let attr = &name.local;
261
262        if *attr == local_name!("id") {
263            element.id = Some(Atom::from(value))
264        }
265
266        if *attr == local_name!("value") {
267            if let Some(input_data) = element.text_input_data_mut() {
268                // Update text input value
269                input_data.set_text(
270                    &mut self.doc.font_ctx.lock().unwrap(),
271                    &mut self.doc.layout_ctx,
272                    value,
273                );
274            }
275            return;
276        }
277
278        if *attr == local_name!("style") {
279            element.flush_style_attribute(&self.doc.guard, &self.doc.url.url_extra_data());
280            node.mark_style_attr_updated();
281            return;
282        }
283
284        if *attr == local_name!("disabled") && element.can_be_disabled() {
285            node.disable();
286            return;
287        }
288
289        // If node if not in the document, then don't apply any special behaviours
290        // and simply set the attribute value
291        if !node.flags.is_in_document() {
292            return;
293        }
294
295        if (tag, attr) == tag_and_attr!("input", "checked") {
296            set_input_checked_state(element, value.to_string());
297        } else if (tag, attr) == tag_and_attr!("img", "src") {
298            self.load_image(node_id);
299        } else if (tag, attr) == tag_and_attr!("canvas", "src") {
300            self.load_custom_paint_src(node_id);
301        } else if (tag, attr) == tag_and_attr!("link", "href") {
302            self.load_linked_stylesheet(node_id);
303        }
304    }
305
306    pub fn clear_attribute(&mut self, node_id: usize, name: QualName) {
307        let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
308        if node_is_in_document {
309            self.doc.snapshot_node(node_id);
310
311            let node = &mut self.doc.nodes[node_id];
312
313            if let Some(mut data) = node.stylo_element_data.get_mut() {
314                data.hint |= RestyleHint::restyle_subtree();
315                data.damage.insert(ALL_DAMAGE);
316            }
317
318            // Mark ancestors dirty so the style traversal visits this subtree.
319            // Without this, the traversal may skip nodes with pending RestyleHint/damage.
320            node.mark_ancestors_dirty();
321        }
322
323        let node = &mut self.doc.nodes[node_id];
324
325        let Some(element) = node.element_data_mut() else {
326            return;
327        };
328
329        let removed_attr = element.attrs.remove(&name);
330        let had_attr = removed_attr.is_some();
331        if !had_attr {
332            return;
333        }
334
335        // If element is a CustomWidget, then call attribute_changed on it
336        #[cfg(feature = "custom-widget")]
337        if let SpecialElementData::CustomWidget(widget_data) = &mut element.special_data {
338            let old_value = removed_attr.as_ref().map(|attr| &*attr.value);
339            widget_data
340                .widget
341                .attribute_changed(&name.local, old_value, None);
342        }
343
344        if name.local == local_name!("id") {
345            element.id = None;
346        }
347
348        // Update text input value
349        if name.local == local_name!("value") {
350            if let Some(input_data) = element.text_input_data_mut() {
351                input_data.set_text(
352                    &mut self.doc.font_ctx.lock().unwrap(),
353                    &mut self.doc.layout_ctx,
354                    "",
355                );
356            }
357        }
358
359        let tag = &element.name.local;
360        let attr = &name.local;
361
362        if *attr == local_name!("disabled") && element.can_be_disabled() {
363            node.enable();
364            return;
365        }
366
367        if *attr == local_name!("style") {
368            element.flush_style_attribute(&self.doc.guard, &self.doc.url.url_extra_data());
369            node.mark_style_attr_updated();
370        } else if (tag, attr) == tag_and_attr!("canvas", "src") {
371            self.recompute_is_animating = true;
372        } else if (tag, attr) == tag_and_attr!("link", "href") {
373            self.unload_stylesheet(node_id);
374        }
375    }
376
377    pub fn set_style_property(&mut self, node_id: usize, name: &str, value: &str) {
378        self.doc.set_style_property(node_id, name, value)
379    }
380
381    pub fn remove_style_property(&mut self, node_id: usize, name: &str) {
382        self.doc.remove_style_property(node_id, name)
383    }
384
385    pub fn set_sub_document(&mut self, node_id: usize, sub_document: Box<dyn Document>) {
386        self.doc.set_sub_document(node_id, sub_document)
387    }
388
389    pub fn remove_sub_document(&mut self, node_id: usize) {
390        self.doc.remove_sub_document(node_id)
391    }
392
393    #[cfg(feature = "custom-widget")]
394    pub fn set_custom_widget(&mut self, node_id: usize, widget: Box<dyn crate::Widget>) {
395        self.doc.set_custom_widget(node_id, widget)
396    }
397
398    #[cfg(feature = "custom-widget")]
399    pub fn remove_custom_widget(&mut self, node_id: usize) {
400        self.doc.remove_custom_widget(node_id)
401    }
402
403    /// Remove the node from it's parent but don't drop it
404    pub fn remove_node(&mut self, node_id: usize) {
405        let node = &mut self.doc.nodes[node_id];
406
407        // Update child_idx values
408        if let Some(parent_id) = node.parent.take() {
409            let parent = &mut self.doc.nodes[parent_id];
410            parent.insert_damage(ALL_DAMAGE);
411            // Mark ancestors dirty so the style traversal visits this subtree.
412            parent.mark_ancestors_dirty();
413            parent.children.retain(|id| *id != node_id);
414            self.maybe_record_node(parent_id);
415        }
416
417        self.process_removed_subtree(node_id);
418    }
419
420    pub fn remove_and_drop_node(&mut self, node_id: usize) -> Option<Node> {
421        self.process_removed_subtree(node_id);
422
423        let node = self.doc.drop_node_ignoring_parent(node_id);
424
425        // Update child_idx values
426        if let Some(parent_id) = node.as_ref().and_then(|node| node.parent) {
427            let parent = &mut self.doc.nodes[parent_id];
428            parent.insert_damage(ALL_DAMAGE);
429            let parent_is_in_doc = parent.flags.is_in_document();
430
431            // TODO: make this fine grained / conditional based on ElementSelectorFlags
432            if parent_is_in_doc {
433                if let Some(mut data) = parent.stylo_element_data.get_mut() {
434                    data.hint |= RestyleHint::restyle_subtree();
435                }
436                // Mark ancestors dirty so the style traversal visits this subtree.
437                parent.mark_ancestors_dirty();
438            }
439
440            parent.children.retain(|id| *id != node_id);
441            self.maybe_record_node(parent_id);
442        }
443
444        node
445    }
446
447    pub fn remove_and_drop_all_children(&mut self, node_id: usize) {
448        let parent = &mut self.doc.nodes[node_id];
449        let parent_is_in_doc = parent.flags.is_in_document();
450
451        // TODO: make this fine grained / conditional based on ElementSelectorFlags
452        if parent_is_in_doc {
453            if let Some(mut data) = parent.stylo_element_data.get_mut() {
454                data.hint |= RestyleHint::restyle_subtree();
455            }
456            // Mark ancestors dirty so the style traversal visits this subtree.
457            parent.mark_ancestors_dirty();
458        }
459
460        let children = mem::take(&mut parent.children);
461        for child_id in children {
462            self.process_removed_subtree(child_id);
463            let _ = self.doc.drop_node_ignoring_parent(child_id);
464        }
465        self.maybe_record_node(node_id);
466    }
467
468    // Tree mutation methods
469    pub fn remove_node_if_unparented(&mut self, node_id: usize) {
470        if let Some(node) = self.doc.get_node(node_id) {
471            if node.parent.is_none() {
472                self.remove_and_drop_node(node_id);
473            }
474        }
475    }
476
477    /// Remove all of the children from old_parent_id and append them to new_parent_id
478    pub fn append_children(&mut self, parent_id: usize, child_ids: &[usize]) {
479        self.add_children_to_parent(parent_id, child_ids, &|parent, child_ids| {
480            parent.children.extend_from_slice(child_ids);
481        });
482    }
483
484    pub fn insert_nodes_before(&mut self, anchor_node_id: usize, new_node_ids: &[usize]) {
485        let parent_id = self.doc.nodes[anchor_node_id].parent.unwrap();
486        self.add_children_to_parent(parent_id, new_node_ids, &|parent, child_ids| {
487            let node_child_idx = parent.index_of_child(anchor_node_id).unwrap();
488            parent
489                .children
490                .splice(node_child_idx..node_child_idx, child_ids.iter().copied());
491        });
492    }
493
494    fn add_children_to_parent(
495        &mut self,
496        parent_id: usize,
497        child_ids: &[usize],
498        insert_children_fn: &dyn Fn(&mut Node, &[usize]),
499    ) {
500        let new_parent = &mut self.doc.nodes[parent_id];
501        new_parent.insert_damage(ALL_DAMAGE);
502        let new_parent_is_in_doc = new_parent.flags.is_in_document();
503
504        // TODO: make this fine grained / conditional based on ElementSelectorFlags
505        if new_parent_is_in_doc {
506            if let Some(mut data) = new_parent.stylo_element_data.get_mut() {
507                data.hint |= RestyleHint::restyle_subtree();
508            }
509            // Mark ancestors dirty so the style traversal visits this subtree.
510            new_parent.mark_ancestors_dirty();
511        }
512
513        insert_children_fn(new_parent, child_ids);
514
515        for child_id in child_ids.iter().copied() {
516            let child = &mut self.doc.nodes[child_id];
517            let old_parent_id = child.parent.replace(parent_id);
518
519            let child_was_in_doc = child.flags.is_in_document();
520            if new_parent_is_in_doc != child_was_in_doc {
521                self.process_added_subtree(child_id);
522            }
523
524            if let Some(old_parent_id) = old_parent_id {
525                let old_parent = &mut self.doc.nodes[old_parent_id];
526                old_parent.insert_damage(ALL_DAMAGE);
527
528                // TODO: make this fine grained / conditional based on ElementSelectorFlags
529                if child_was_in_doc {
530                    if let Some(mut data) = old_parent.stylo_element_data.get_mut() {
531                        data.hint |= RestyleHint::restyle_subtree();
532                    }
533                    // Mark ancestors dirty so the style traversal visits this subtree.
534                    old_parent.mark_ancestors_dirty();
535                }
536
537                old_parent.children.retain(|id| *id != child_id);
538                self.maybe_record_node(old_parent_id);
539            }
540        }
541
542        self.maybe_record_node(parent_id);
543    }
544
545    // Tree mutation methods (that defer to other methods)
546    pub fn insert_nodes_after(&mut self, anchor_node_id: usize, new_node_ids: &[usize]) {
547        match self.next_sibling_id(anchor_node_id) {
548            Some(id) => self.insert_nodes_before(id, new_node_ids),
549            None => {
550                let parent_id = self.parent_id(anchor_node_id).unwrap();
551                self.append_children(parent_id, new_node_ids)
552            }
553        }
554    }
555
556    pub fn reparent_children(&mut self, old_parent_id: usize, new_parent_id: usize) {
557        let child_ids = std::mem::take(&mut self.doc.nodes[old_parent_id].children);
558        self.maybe_record_node(old_parent_id);
559        self.append_children(new_parent_id, &child_ids);
560    }
561
562    pub fn replace_node_with(&mut self, anchor_node_id: usize, new_node_ids: &[usize]) {
563        self.insert_nodes_before(anchor_node_id, new_node_ids);
564        self.remove_node(anchor_node_id);
565    }
566}
567
568impl<'doc> DocumentMutator<'doc> {
569    pub fn flush(&mut self) {
570        if self.recompute_is_animating {
571            self.doc.has_canvas = self.doc.compute_has_canvas();
572        }
573
574        if let Some(id) = self.title_node {
575            let title = self.doc.nodes[id].text_content();
576            self.doc.shell_provider.set_window_title(title);
577        }
578
579        // Add/Update inline stylesheets (<style> elements)
580        for id in self.style_nodes.drain() {
581            self.doc.process_style_element(id);
582        }
583
584        for id in self.form_nodes.drain() {
585            self.doc.reset_form_owner(id);
586        }
587
588        #[cfg(feature = "autofocus")]
589        if let Some(node_id) = self.node_to_autofocus.take() {
590            if self.doc.get_node(node_id).is_some() {
591                self.doc.set_focus_to(node_id);
592            }
593        }
594    }
595
596    pub fn set_inner_html(&mut self, node_id: usize, html: &str) {
597        self.remove_and_drop_all_children(node_id);
598        self.doc
599            .html_parser_provider
600            .clone()
601            .parse_inner_html(self, node_id, html);
602    }
603
604    fn flush_eager_ops(&mut self) {
605        let mut ops = mem::take(&mut self.eager_op_queue);
606        for op in ops.drain(0..) {
607            match op {
608                SpecialOp::LoadImage(node_id) => self.load_image(node_id),
609                SpecialOp::LoadStylesheet(node_id) => self.load_linked_stylesheet(node_id),
610                SpecialOp::UnloadStylesheet(node_id) => self.unload_stylesheet(node_id),
611                SpecialOp::LoadCustomPaintSource(node_id) => self.load_custom_paint_src(node_id),
612                SpecialOp::ProcessButtonInput(node_id) => self.process_button_input(node_id),
613                SpecialOp::UnloadSubDocument(node_id) => self.remove_sub_document(node_id),
614                #[cfg(feature = "custom-widget")]
615                SpecialOp::UnloadCustomWidget(node_id) => self.remove_custom_widget(node_id),
616            }
617        }
618
619        // Queue is empty, but put Vec back anyway so allocation can be reused.
620        self.eager_op_queue = ops;
621    }
622
623    fn process_added_subtree(&mut self, node_id: usize) {
624        self.doc.iter_subtree_mut(node_id, |node_id, doc| {
625            let node = &mut doc.nodes[node_id];
626            node.flags.set(NodeFlags::IS_IN_DOCUMENT, true);
627            node.insert_damage(ALL_DAMAGE);
628
629            // If the node has an "id" attribute, store it in the ID map.
630            if let Some(id_attr) = node.attr(local_name!("id")) {
631                doc.nodes_to_id.insert(id_attr.to_string(), node_id);
632            }
633
634            let NodeData::Element(ref mut element) = node.data else {
635                return;
636            };
637
638            // Custom post-processing by element tag name
639            let tag = element.name.local.as_ref();
640            match tag {
641                "title" => self.title_node = Some(node_id),
642                "link" => self.eager_op_queue.push(SpecialOp::LoadStylesheet(node_id)),
643                "img" => self.eager_op_queue.push(SpecialOp::LoadImage(node_id)),
644                "canvas" => self
645                    .eager_op_queue
646                    .push(SpecialOp::LoadCustomPaintSource(node_id)),
647                "style" => {
648                    self.style_nodes.insert(node_id);
649                }
650                "button" | "fieldset" | "input" | "select" | "textarea" | "object" | "output" => {
651                    self.eager_op_queue
652                        .push(SpecialOp::ProcessButtonInput(node_id));
653                    self.form_nodes.insert(node_id);
654                }
655                _ => {}
656            }
657
658            #[cfg(feature = "autofocus")]
659            if node.is_focussable() {
660                if let NodeData::Element(ref element) = node.data {
661                    if let Some(value) = element.attr(local_name!("autofocus")) {
662                        if value == "true" {
663                            self.node_to_autofocus = Some(node_id);
664                        }
665                    }
666                }
667            }
668        });
669
670        self.flush_eager_ops();
671    }
672
673    fn process_removed_subtree(&mut self, node_id: usize) {
674        self.doc.iter_subtree_mut(node_id, |node_id, doc| {
675            let node = &mut doc.nodes[node_id];
676            node.flags.set(NodeFlags::IS_IN_DOCUMENT, false);
677
678            // Clear hover state if this node was being hovered.
679            // This prevents stale hover_node_id references.
680            if doc.hover_node_id == Some(node_id) {
681                doc.hover_node_id = None;
682                doc.hover_node_is_text = false;
683            }
684
685            // Clear active state if this node was active
686            // This prevents stale active_node_id references.
687            if doc.active_node_id == Some(node_id) {
688                doc.active_node_id = None;
689            }
690
691            // Remove any snapshot for this node to prevent stale snapshot references
692            // during style invalidation.
693            if node.has_snapshot {
694                let opaque_id = style::dom::TNode::opaque(&&*node);
695                doc.snapshots.remove(&opaque_id);
696                node.has_snapshot = false;
697            }
698
699            // If the node has an "id" attribute remove it from the ID map.
700            if let Some(id_attr) = node.attr(local_name!("id")) {
701                doc.nodes_to_id.remove(id_attr);
702            }
703
704            let NodeData::Element(ref mut element) = node.data else {
705                return;
706            };
707
708            match &element.special_data {
709                SpecialElementData::SubDocument(_) => {
710                    self.eager_op_queue
711                        .push(SpecialOp::UnloadSubDocument(node_id));
712                }
713                #[cfg(feature = "custom-widget")]
714                SpecialElementData::CustomWidget(_) => {
715                    self.eager_op_queue
716                        .push(SpecialOp::UnloadCustomWidget(node_id));
717                }
718                SpecialElementData::Stylesheet(_) => self
719                    .eager_op_queue
720                    .push(SpecialOp::UnloadStylesheet(node_id)),
721                SpecialElementData::Image(_) => {}
722                SpecialElementData::Canvas(_) => {
723                    self.recompute_is_animating = true;
724                }
725                SpecialElementData::TableRoot(_) => {}
726                SpecialElementData::TextInput(_) => {}
727                SpecialElementData::CheckboxInput(_) => {}
728                #[cfg(feature = "file-input")]
729                SpecialElementData::FileInput(_) => {}
730                SpecialElementData::None => {}
731            }
732        });
733
734        self.flush_eager_ops();
735    }
736
737    fn maybe_record_node(&mut self, node_id: impl Into<Option<usize>>) {
738        let Some(node_id) = node_id.into() else {
739            return;
740        };
741
742        let Some(tag_name) = self.doc.nodes[node_id]
743            .data
744            .downcast_element()
745            .map(|elem| &elem.name.local)
746        else {
747            return;
748        };
749
750        match tag_name.as_ref() {
751            "title" => self.title_node = Some(node_id),
752            "style" => {
753                self.style_nodes.insert(node_id);
754            }
755            _ => {}
756        }
757    }
758
759    fn load_linked_stylesheet(&mut self, target_id: usize) {
760        let node = &self.doc.nodes[target_id];
761
762        let mut is_in_head = false;
763        let mut parent_id = node.parent;
764        while let Some(id) = parent_id
765            && !is_in_head
766        {
767            let parent = &self.doc.nodes[id];
768            is_in_head |= parent.data.is_element_with_tag_name(&local_name!("head"));
769            parent_id = parent.parent;
770        }
771
772        let rel_attr = node.attr(local_name!("rel"));
773        let href_attr = node.attr(local_name!("href"));
774
775        let (Some(rels), Some(href)) = (rel_attr, href_attr) else {
776            return;
777        };
778        if !rels.split_ascii_whitespace().any(|rel| rel == "stylesheet") {
779            return;
780        }
781
782        let url = self.doc.resolve_url(href);
783        let handler = ResourceHandler::new(
784            self.doc.tx.clone(),
785            self.doc.id(),
786            Some(node.id),
787            self.doc.shell_provider.clone(),
788            StylesheetHandler {
789                source_url: url.clone(),
790                guard: self.doc.guard.clone(),
791                net_provider: self.doc.net_provider.clone(),
792                abort_signal: self.doc.abort_signal.clone(),
793            },
794        );
795
796        if is_in_head {
797            self.doc
798                .pending_critical_resources
799                .insert(handler.request_id());
800        }
801
802        self.doc.net_provider.fetch(
803            self.doc.id(),
804            self.doc.build_request(url),
805            Box::new(handler),
806        );
807    }
808
809    fn unload_stylesheet(&mut self, node_id: usize) {
810        let node = &mut self.doc.nodes[node_id];
811        let Some(element) = node.element_data_mut() else {
812            unreachable!();
813        };
814        let SpecialElementData::Stylesheet(stylesheet) = element.special_data.take() else {
815            unreachable!();
816        };
817
818        let guard = self.doc.guard.read();
819        self.doc.stylist.remove_stylesheet(stylesheet, &guard);
820        self.doc
821            .stylist
822            .force_stylesheet_origins_dirty(OriginSet::all());
823
824        self.doc.nodes_to_stylesheet.remove(&node_id);
825    }
826
827    fn load_image(&mut self, target_id: usize) {
828        let node = &self.doc.nodes[target_id];
829        if let Some(raw_src) = node.attr(local_name!("src")) {
830            if !raw_src.is_empty() {
831                let src = self.doc.resolve_url(raw_src);
832                let src_string = src.as_str();
833
834                // Check cache first
835                if let Some(cached_image) = self.doc.image_cache.get(src_string) {
836                    #[cfg(feature = "tracing")]
837                    tracing::info!("Loading image {src_string} from cache");
838                    let node = &mut self.doc.nodes[target_id];
839                    node.element_data_mut().unwrap().special_data =
840                        SpecialElementData::Image(Box::new(cached_image.clone()));
841                    node.cache.clear();
842                    node.insert_damage(ALL_DAMAGE);
843                    return;
844                }
845
846                // Check if there's already a pending request for this URL
847                if let Some(waiting_list) = self.doc.pending_images.get_mut(src_string) {
848                    #[cfg(feature = "tracing")]
849                    tracing::info!("Image {src_string} already pending, queueing node {target_id}");
850                    waiting_list.push((target_id, ImageType::Image));
851                    return;
852                }
853
854                // Start fetch and track as pending
855                #[cfg(feature = "tracing")]
856                tracing::info!("Fetching image {src_string}");
857                self.doc
858                    .pending_images
859                    .insert(src_string.to_string(), vec![(target_id, ImageType::Image)]);
860
861                self.doc.net_provider.fetch(
862                    self.doc.id(),
863                    self.doc.build_request(src),
864                    ResourceHandler::boxed(
865                        self.doc.tx.clone(),
866                        self.doc.id(),
867                        None, // Don't pass node_id, we'll handle it via pending_images
868                        self.doc.shell_provider.clone(),
869                        ImageHandler::new(ImageType::Image),
870                    ),
871                );
872            }
873        }
874    }
875
876    fn load_custom_paint_src(&mut self, target_id: usize) {
877        let node = &mut self.doc.nodes[target_id];
878        if let Some(raw_src) = node.attr(local_name!("src")) {
879            if let Ok(custom_paint_source_id) = raw_src.parse::<u64>() {
880                self.recompute_is_animating = true;
881                let canvas_data = SpecialElementData::Canvas(CanvasData {
882                    custom_paint_source_id,
883                });
884                node.element_data_mut().unwrap().special_data = canvas_data;
885            }
886        }
887    }
888
889    fn process_button_input(&mut self, target_id: usize) {
890        let node = &self.doc.nodes[target_id];
891        let Some(data) = node.element_data() else {
892            return;
893        };
894
895        let tagname = data.name.local.as_ref();
896        let type_attr = data.attr(local_name!("type"));
897        let value = data.attr(local_name!("value"));
898
899        // Add content of "value" attribute as a text node child if:
900        //   - Tag name is
901        if let ("input", Some("button" | "submit" | "reset"), Some(value)) =
902            (tagname, type_attr, value)
903        {
904            let value = value.to_string();
905            let id = self.create_text_node(&value);
906            self.append_children(target_id, &[id]);
907            return;
908        }
909        #[cfg(feature = "file-input")]
910        if let ("input", Some("file")) = (tagname, type_attr) {
911            let button_id = self.create_element(
912                qual_name!("button", html),
913                vec![
914                    Attribute {
915                        name: qual_name!("type", html),
916                        value: "button".to_string(),
917                    },
918                    Attribute {
919                        name: qual_name!("tabindex", html),
920                        value: "-1".to_string(),
921                    },
922                ],
923            );
924            let label_id = self.create_element(qual_name!("label", html), vec![]);
925            let text_id = self.create_text_node("No File Selected");
926            let button_text_id = self.create_text_node("Browse");
927            self.append_children(target_id, &[button_id, label_id]);
928            self.append_children(label_id, &[text_id]);
929            self.append_children(button_id, &[button_text_id]);
930        }
931    }
932}
933
934/// Set 'checked' state on an input based on given attributevalue
935fn set_input_checked_state(element: &mut ElementData, value: String) {
936    let Ok(checked) = value.parse() else {
937        return;
938    };
939    match element.special_data {
940        SpecialElementData::CheckboxInput(ref mut checked_mut) => *checked_mut = checked,
941        // If we have just constructed the element, set the node attribute,
942        // and NodeSpecificData will be created from that later
943        // this simulates the checked attribute being set in html,
944        // and the element's checked property being set from that
945        SpecialElementData::None => element.attrs.push(Attribute {
946            name: qual_name!("checked", html),
947            value: checked.to_string(),
948        }),
949        _ => {}
950    }
951}
952
953/// Type that allows mutable access to the viewport
954/// And syncs it back to stylist on drop.
955pub struct ViewportMut<'doc> {
956    doc: &'doc mut BaseDocument,
957    initial_viewport: Viewport,
958}
959impl ViewportMut<'_> {
960    pub fn new(doc: &mut BaseDocument) -> ViewportMut<'_> {
961        let initial_viewport = doc.viewport.clone();
962        ViewportMut {
963            doc,
964            initial_viewport,
965        }
966    }
967}
968impl Deref for ViewportMut<'_> {
969    type Target = Viewport;
970
971    fn deref(&self) -> &Self::Target {
972        &self.doc.viewport
973    }
974}
975impl DerefMut for ViewportMut<'_> {
976    fn deref_mut(&mut self) -> &mut Self::Target {
977        &mut self.doc.viewport
978    }
979}
980impl Drop for ViewportMut<'_> {
981    fn drop(&mut self) {
982        if self.doc.viewport == self.initial_viewport {
983            return;
984        }
985
986        self.doc.set_stylist_device(make_device(
987            &self.doc.viewport,
988            self.doc.media_type.clone(),
989            self.doc.font_ctx.clone(),
990        ));
991        self.doc.scroll_viewport_by(0.0, 0.0); // Clamp scroll offset
992
993        let scale_has_changed =
994            self.doc.viewport().scale_f64() != self.initial_viewport.scale_f64();
995        if scale_has_changed {
996            self.doc.invalidate_inline_contexts();
997            self.doc.shell_provider.request_redraw();
998        }
999    }
1000}
1001
1002#[cfg(test)]
1003mod test {
1004    use style::media_queries::MediaType;
1005    use style_dom::ElementState;
1006
1007    use crate::{Attribute, BaseDocument, DocumentConfig, ElementData, NodeData, qual_name};
1008
1009    #[test]
1010    fn media_type_defaults_to_screen() {
1011        let mut document = BaseDocument::new(DocumentConfig::default());
1012        assert_eq!(*document.media_type(), MediaType::screen());
1013        assert_eq!(document.stylist_device().media_type(), MediaType::screen());
1014    }
1015
1016    #[test]
1017    fn media_type_honors_config() {
1018        let mut document = BaseDocument::new(DocumentConfig {
1019            media_type: Some(MediaType::print()),
1020            ..Default::default()
1021        });
1022        assert_eq!(*document.media_type(), MediaType::print());
1023        assert_eq!(document.stylist_device().media_type(), MediaType::print());
1024    }
1025
1026    #[test]
1027    fn set_media_type_updates_stylist_device() {
1028        let mut document = BaseDocument::new(DocumentConfig::default());
1029        assert_eq!(document.stylist_device().media_type(), MediaType::screen());
1030
1031        document.set_media_type(MediaType::print());
1032        assert_eq!(*document.media_type(), MediaType::print());
1033        assert_eq!(document.stylist_device().media_type(), MediaType::print());
1034    }
1035
1036    #[test]
1037    fn mutator_remove_disabled() {
1038        let mut document = BaseDocument::new(DocumentConfig::default());
1039        let id = document.create_node(NodeData::Element(ElementData::new(
1040            qual_name!("button"),
1041            vec![Attribute {
1042                name: qual_name!("disabled"),
1043                value: "".into(),
1044            }],
1045        )));
1046
1047        let node = document.get_node(id).unwrap();
1048        assert!(
1049            node.element_state.contains(ElementState::DISABLED),
1050            "form node is disabled"
1051        );
1052        assert!(
1053            !node.element_state.contains(ElementState::ENABLED),
1054            "form node is not enabled yet"
1055        );
1056
1057        let mut mutator = document.mutate();
1058        mutator.clear_attribute(id, qual_name!("disabled"));
1059        drop(mutator);
1060
1061        let node = document.get_node(id).unwrap();
1062        assert!(
1063            !node.element_state.contains(ElementState::DISABLED),
1064            "form node is no longer disabled"
1065        );
1066        assert!(
1067            node.element_state.contains(ElementState::ENABLED),
1068            "form node is enabled"
1069        );
1070    }
1071
1072    #[test]
1073    fn mutator_set_disabled() {
1074        let mut document = BaseDocument::new(DocumentConfig::default());
1075        let id = document.create_node(NodeData::Element(ElementData::new(
1076            qual_name!("button"),
1077            vec![],
1078        )));
1079
1080        let node = document.get_node(id).unwrap();
1081        assert!(
1082            !node.element_state.contains(ElementState::DISABLED),
1083            "form node is not disabled"
1084        );
1085        assert!(
1086            node.element_state.contains(ElementState::ENABLED),
1087            "form node is enabled"
1088        );
1089
1090        let mut mutator = document.mutate();
1091        mutator.set_attribute(id, qual_name!("disabled"), "");
1092        drop(mutator);
1093
1094        let node = document.get_node(id).unwrap();
1095
1096        assert!(
1097            node.element_state.contains(ElementState::DISABLED),
1098            "form node is disabled"
1099        );
1100        assert!(
1101            !node.element_state.contains(ElementState::ENABLED),
1102            "form node is no longer enabled enabled"
1103        );
1104    }
1105
1106    #[test]
1107    fn mutator_set_disabled_invalid_node() {
1108        let mut document = BaseDocument::new(DocumentConfig::default());
1109        let id = document.create_node(NodeData::Element(ElementData::new(qual_name!("a"), vec![])));
1110
1111        let node = document.get_node(id).unwrap();
1112        assert!(
1113            !node.element_state.contains(ElementState::DISABLED),
1114            "form node is not disabled"
1115        );
1116        assert!(
1117            !node.element_state.contains(ElementState::ENABLED),
1118            "form node is enabled"
1119        );
1120
1121        let mut mutator = document.mutate();
1122        mutator.set_attribute(id, qual_name!("disabled"), "");
1123        drop(mutator);
1124
1125        let node = document.get_node(id).unwrap();
1126        assert!(
1127            !node.element_state.contains(ElementState::DISABLED),
1128            "form node is not disabled"
1129        );
1130        assert!(
1131            !node.element_state.contains(ElementState::ENABLED),
1132            "form node is enabled"
1133        );
1134    }
1135}