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