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