Skip to main content

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