1use blitz_traits::net::RequestDestination;
2use blitz_traits::node_id::NodeId;
3use rustc_hash::FxHashSet;
4use std::collections::HashSet;
5use std::mem;
6use std::ops::{Deref, DerefMut};
7
8use crate::document::make_device;
9use crate::layout::damage::ALL_DAMAGE;
10use crate::net::{ImageHandler, ResourceHandler, StylesheetHandler};
11use crate::node::{CanvasData, NodeFlags, SpecialElementData};
12use crate::util::ImageType;
13use crate::{
14 Attribute, BaseDocument, Document, ElementData, Node, NodeData, QualName, local_name, qual_name,
15};
16use blitz_traits::shell::Viewport;
17use markup5ever::ns;
18use selectors::matching::ElementSelectorFlags;
19use style::Atom;
20use style::invalidation::element::restyle_hints::RestyleHint;
21use style::stylesheets::OriginSet;
22use thin_vec::ThinVec;
23
24macro_rules! tag_and_attr {
25 ($tag:tt, $attr:tt) => {
26 (&local_name!($tag), &local_name!($attr))
27 };
28}
29
30#[derive(Debug, Clone)]
31pub enum AppendTextErr {
32 NotTextNode,
34}
35
36enum SpecialOp {
39 LoadImage(NodeId),
40 LoadIframe(NodeId),
41 LoadStylesheet(NodeId),
42 UnloadStylesheet(NodeId),
43 LoadCustomPaintSource(NodeId),
44 ProcessButtonInput(NodeId),
45 UnloadSubDocument(NodeId),
46 #[cfg(feature = "custom-widget")]
47 UnloadCustomWidget(NodeId),
48 #[cfg(feature = "shadow-dom")]
49 UpgradeCustomElement(NodeId),
50 #[cfg(feature = "shadow-dom")]
51 DisconnectCustomElement(NodeId),
52}
53
54pub struct DocumentMutator<'doc> {
55 pub doc: &'doc mut BaseDocument,
58
59 eager_op_queue: Vec<SpecialOp>,
60
61 title_node: Option<NodeId>,
63 style_nodes: HashSet<NodeId>,
64 form_nodes: HashSet<NodeId>,
65
66 recompute_is_animating: bool,
68
69 mutations_occurred: bool,
71
72 #[cfg(feature = "shadow-dom")]
75 custom_element_attr_changes: Vec<(NodeId, QualName, Option<String>, Option<String>)>,
76
77 #[cfg(feature = "autofocus")]
79 node_to_autofocus: Option<NodeId>,
80}
81
82impl Drop for DocumentMutator<'_> {
83 fn drop(&mut self) {
84 self.flush(); self.doc.mutator_redraw_pending |= self.mutations_occurred;
86 self.doc.mutator_depth -= 1;
87 if self.doc.mutator_depth == 0 && mem::take(&mut self.doc.mutator_redraw_pending) {
88 self.doc.shell_provider.request_redraw();
89 }
90 }
91}
92
93impl DocumentMutator<'_> {
94 pub fn new<'doc>(doc: &'doc mut BaseDocument) -> DocumentMutator<'doc> {
95 doc.mutator_depth += 1;
96 DocumentMutator {
97 doc,
98 eager_op_queue: Vec::new(),
99 title_node: None,
100 style_nodes: HashSet::new(),
101 form_nodes: HashSet::new(),
102 recompute_is_animating: false,
103 mutations_occurred: false,
104 #[cfg(feature = "shadow-dom")]
105 custom_element_attr_changes: Vec::new(),
106 #[cfg(feature = "autofocus")]
107 node_to_autofocus: None,
108 }
109 }
110
111 fn restyle_parent_for_attribute_change(&mut self, parent_id: Option<NodeId>) {
114 let Some(parent_id) = parent_id else {
115 return;
116 };
117 let flags = self.doc.nodes[parent_id].selector_flags().get();
118 let child_dependent = ElementSelectorFlags::HAS_SLOW_SELECTOR
119 | ElementSelectorFlags::HAS_SLOW_SELECTOR_LATER_SIBLINGS
120 | ElementSelectorFlags::HAS_EDGE_CHILD_SELECTOR
121 | ElementSelectorFlags::HAS_EMPTY_SELECTOR
122 | ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR
123 | ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_SIBLING
124 | ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR_SIBLING;
125 if flags.intersects(child_dependent) {
126 if let Some(mut data) = self.doc.nodes[parent_id]
127 .stylo_element_data_opt_mut()
128 .and_then(|s| s.get_mut())
129 {
130 data.hint |= RestyleHint::restyle_subtree();
131 }
132 }
133 }
134
135 pub fn node_has_parent(&self, node_id: NodeId) -> bool {
138 self.doc.nodes[node_id].parent.is_some()
139 }
140
141 pub fn previous_sibling_id(&self, node_id: NodeId) -> Option<NodeId> {
142 self.doc.nodes[node_id].backward(1).map(|node| node.id)
143 }
144
145 pub fn next_sibling_id(&self, node_id: NodeId) -> Option<NodeId> {
146 self.doc.nodes[node_id].forward(1).map(|node| node.id)
147 }
148
149 pub fn parent_id(&self, node_id: NodeId) -> Option<NodeId> {
150 self.doc.nodes[node_id].parent
151 }
152
153 pub fn last_child_id(&self, node_id: NodeId) -> Option<NodeId> {
154 self.doc.nodes[node_id].children.last().copied()
155 }
156
157 pub fn child_ids(&self, node_id: NodeId) -> ThinVec<NodeId> {
158 self.doc.nodes[node_id].children.clone()
159 }
160
161 pub fn element_name(&self, node_id: NodeId) -> Option<&QualName> {
162 self.doc.nodes[node_id].element_data().map(|el| &el.name)
163 }
164
165 pub fn node_at_path(&self, start_node_id: NodeId, path: &[u8]) -> NodeId {
166 let mut current = &self.doc.nodes[start_node_id];
167 for i in path {
168 let new_id = current.children[*i as usize];
169 current = &self.doc.nodes[new_id];
170 }
171 current.id
172 }
173
174 pub fn create_comment_node(&mut self, contents: &str) -> NodeId {
177 self.doc.create_node(NodeData::Comment {
178 contents: contents.to_string(),
179 })
180 }
181
182 pub fn create_text_node(&mut self, text: &str) -> NodeId {
183 self.doc.create_text_node(text)
184 }
185
186 pub fn create_document_fragment(&mut self) -> NodeId {
195 self.doc.create_node(NodeData::DocumentFragment)
196 }
197
198 fn create_template_contents(&mut self) -> NodeId {
201 let contents = self.create_document_fragment();
202 let owner = self.doc.template_contents_owner();
203 self.doc.nodes[contents].owner_document = Some(owner);
204 contents
205 }
206
207 pub fn create_html_document(&mut self, title: &str) -> NodeId {
220 let doc_id = self.create_document("text/html");
221
222 let html = self.create_element(html_tag("html"), Vec::new());
223 let head = self.create_element(html_tag("head"), Vec::new());
224 let title_el = self.create_element(html_tag("title"), Vec::new());
225 let body = self.create_element(html_tag("body"), Vec::new());
226
227 if !title.is_empty() {
230 let text = self.create_text_node(title);
231 self.append_children(title_el, &[text]);
232 }
233
234 self.append_children(head, &[title_el]);
235 self.append_children(html, &[head, body]);
236 self.append_children(doc_id, &[html]);
237
238 doc_id
239 }
240
241 pub fn create_document(&mut self, content_type: &'static str) -> NodeId {
243 let mut data = crate::node::DocumentData::default();
244 data.content_type = content_type;
245 self.doc.create_node(NodeData::Document(Box::new(data)))
246 }
247
248 pub fn adopt_node(&mut self, node_id: NodeId, document_id: NodeId) {
250 self.doc.adopt_script_subtree(node_id, document_id);
251 }
252
253 pub fn create_element(&mut self, name: QualName, attrs: Vec<Attribute>) -> NodeId {
254 let is_template = name.ns == ns!(html) && name.local == local_name!("template");
255 let mut data = ElementData::new(name, attrs);
256 data.flush_style_attribute(self.doc.guard(), &self.doc.url.url_extra_data());
257 if is_template {
258 data.template_contents = Some(self.create_template_contents());
259 }
260
261 let id = self.doc.create_node(NodeData::Element(Box::new(data)));
262 let node = self.doc.get_node_mut(id).unwrap();
263
264 *node.stylo_element_data_mut().ensure_init_mut() = style::data::ElementData {
266 damage: ALL_DAMAGE,
267 ..Default::default()
268 };
269
270 id
271 }
272
273 pub fn deep_clone_node(&mut self, node_id: NodeId) -> NodeId {
274 self.doc.deep_clone_node(node_id)
275 }
276
277 pub fn template_contents(&mut self, node_id: NodeId) -> NodeId {
279 if let Some(contents) = self.doc.nodes[node_id]
280 .element_data()
281 .and_then(|element| element.template_contents)
282 {
283 return contents;
284 }
285 let contents = self.create_template_contents();
286 self.doc.nodes[node_id]
287 .element_data_mut()
288 .expect("template contents require an element")
289 .template_contents = Some(contents);
290 contents
291 }
292
293 pub fn set_node_text(&mut self, node_id: NodeId, value: &str) {
296 if matches!(
297 self.doc.nodes[node_id].markup.as_deref(),
298 Some(crate::node::MarkupNode::Doctype { .. })
299 ) {
300 return;
301 }
302 if !self.doc.live_ranges.is_empty()
303 && let Some(length) = self
304 .doc
305 .range_character_data(node_id)
306 .map(|text| text.encode_utf16().count())
307 {
308 self.doc
309 .range_replace_data(node_id, 0, length, value.encode_utf16().count());
310 }
311 self.set_node_text_without_range_update(node_id, value);
312 }
313
314 pub fn replace_character_data(
316 &mut self,
317 node_id: NodeId,
318 offset: usize,
319 count: usize,
320 value: &str,
321 ) -> Result<(), &'static str> {
322 let old = self
323 .doc
324 .range_character_data(node_id)
325 .ok_or("InvalidNodeTypeError")?;
326 let length = old.encode_utf16().count();
327 if offset > length {
328 return Err("IndexSizeError");
329 }
330 let count = count.min(length - offset);
331 let mut replacement = crate::range::utf16_slice(old, 0, offset);
332 replacement.push_str(value);
333 replacement.push_str(&crate::range::utf16_slice(old, offset + count, length));
334 self.doc
335 .range_replace_data(node_id, offset, count, value.encode_utf16().count());
336 self.set_node_text_without_range_update(node_id, &replacement);
337 Ok(())
338 }
339
340 pub fn split_text(&mut self, node_id: NodeId, offset: usize) -> Result<NodeId, &'static str> {
341 let node = self.doc.get_node(node_id).ok_or("InvalidNodeTypeError")?;
342 let NodeData::Text(text) = &node.data else {
343 return Err("InvalidNodeTypeError");
344 };
345 let length = text.content.encode_utf16().count();
346 if offset > length {
347 return Err("IndexSizeError");
348 }
349 let before = crate::range::utf16_slice(&text.content, 0, offset);
350 let after = crate::range::utf16_slice(&text.content, offset, length);
351 let position = node
352 .parent
353 .and_then(|parent| Some((parent, self.doc.get_node(parent)?.index_of_child(node_id)?)));
354 let new_id = self.create_text_node(&after);
355 if position.is_some() {
356 self.insert_nodes_after(node_id, &[new_id]);
357 }
358 self.doc.range_split_text(node_id, new_id, offset, position);
359 self.set_node_text_without_range_update(node_id, &before);
360 Ok(new_id)
361 }
362
363 fn set_node_text_without_range_update(&mut self, node_id: NodeId, value: &str) {
364 let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
365 if node_is_in_document && self.doc.is_recording_mutations() {
366 let old_value = match &self.doc.nodes[node_id].data {
367 NodeData::Comment { contents } => Some(contents.clone()),
368 NodeData::Text(text) => Some(text.content.clone()),
369 _ => None,
370 };
371 if let Some(old_value) = old_value.filter(|old| old != value) {
372 self.doc.record_mutation(crate::DomMutation::CharacterData {
373 target: node_id,
374 old_value,
375 });
376 }
377 }
378 let node = &mut self.doc.nodes[node_id];
379
380 if let NodeData::Comment { ref mut contents } = node.data {
391 if contents != value {
392 contents.clear();
393 contents.push_str(value);
394 }
395 return;
396 }
397
398 let text = match node.data {
399 NodeData::Text(ref mut text) => text,
400 _ => return,
402 };
403
404 let changed = text.content != value;
405 if changed {
406 self.mutations_occurred |= node_is_in_document;
407 text.content.clear();
408 text.content.push_str(value);
409 node.insert_damage(ALL_DAMAGE);
410 node.mark_ancestors_dirty();
413 let parent_id = node.parent;
414
415 if let Some(parent_id) = parent_id {
418 let parent = &mut self.doc.nodes[parent_id];
419 parent.insert_damage(ALL_DAMAGE);
420 }
421
422 self.maybe_record_node(parent_id);
423 }
424 }
425
426 pub fn append_text_to_node(
427 &mut self,
428 node_id: NodeId,
429 text: &str,
430 ) -> Result<(), AppendTextErr> {
431 let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
432 let node = &mut self.doc.nodes[node_id];
433 node.insert_damage(ALL_DAMAGE);
434 node.mark_ancestors_dirty();
435 let parent = node.parent;
436 match node.text_data_mut() {
437 Some(data) => {
438 data.content += text;
439 self.mutations_occurred |= node_is_in_document;
440 if let Some(parent) = parent
444 && self.doc.nodes[parent]
445 .data
446 .downcast_element()
447 .is_some_and(|element| element.name.local.as_ref() == "style")
448 {
449 self.style_nodes.insert(parent);
450 }
451 Ok(())
452 }
453 None => Err(AppendTextErr::NotTextNode),
454 }
455 }
456
457 pub fn add_attrs_if_missing(&mut self, node_id: NodeId, attrs: Vec<Attribute>) {
458 let node = &mut self.doc.nodes[node_id];
459 node.insert_damage(ALL_DAMAGE);
460 let element_data = node.element_data_mut().expect("Not an element");
461
462 let existing_names = element_data
463 .attrs
464 .iter()
465 .map(|e| e.name.clone())
466 .collect::<HashSet<_>>();
467
468 for attr in attrs
469 .into_iter()
470 .filter(|attr| !existing_names.contains(&attr.name))
471 {
472 self.set_attribute(node_id, attr.name, &attr.value);
473 }
474 }
475
476 pub fn set_attribute(&mut self, node_id: NodeId, name: QualName, value: &str) {
477 #[cfg(feature = "shadow-dom")]
478 if name.local == local_name!("slot") || name.local == local_name!("name") {
479 self.doc.note_shadow_tree_change(node_id);
480 }
481 self.doc
482 .record_script_custom_element_attribute(node_id, &name, Some(value));
483 self.doc
484 .record_inline_handler_attribute(node_id, &name, Some(value));
485 self.doc.record_details_open(node_id, &name, true);
486 let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
487 if node_is_in_document && self.doc.is_recording_mutations() {
488 if let Some(element) = self.doc.nodes[node_id].element_data() {
492 let old_value = element.attr(name.local.clone()).map(str::to_owned);
493 self.doc.record_mutation(crate::DomMutation::Attributes {
494 target: node_id,
495 name: name.local.to_string(),
496 old_value,
497 });
498 }
499 }
500 if node_is_in_document {
501 self.doc.snapshot_attribute(node_id, &name.local);
502
503 let renders_from_attributes = self.doc.nodes[node_id]
519 .data
520 .downcast_element()
521 .is_some_and(|el| {
522 el.name.ns == ns!(svg)
523 || crate::layout::replaced::is_replaced_element(&el.name.local)
524 });
525
526 let node = &mut self.doc.nodes[node_id];
527 node.set_dirty_descendants();
528 if let Some(mut data) = node.stylo_element_data_opt_mut().and_then(|s| s.get_mut()) {
529 data.hint |= RestyleHint::restyle_subtree();
530 if renders_from_attributes {
531 data.damage.insert(ALL_DAMAGE);
532 }
533 }
534
535 let parent = node.parent;
552 if let Some(parent_id) = parent {
553 let parent = &self.doc.nodes[parent_id];
554 let flags = parent.selector_flags().get();
555 let child_dependent = ElementSelectorFlags::HAS_SLOW_SELECTOR
556 | ElementSelectorFlags::HAS_SLOW_SELECTOR_LATER_SIBLINGS
557 | ElementSelectorFlags::HAS_EDGE_CHILD_SELECTOR
558 | ElementSelectorFlags::HAS_EMPTY_SELECTOR
559 | ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR
560 | ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_SIBLING
561 | ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR_SIBLING;
562
563 if flags.intersects(child_dependent) {
564 let parent = &mut self.doc.nodes[parent_id];
565 if let Some(mut data) = parent
566 .stylo_element_data_opt_mut()
567 .and_then(|s| s.get_mut())
568 {
569 data.hint |= RestyleHint::restyle_subtree();
570 }
571 }
572 }
573
574 self.doc.nodes[node_id].mark_ancestors_dirty();
578 }
579
580 if name.ns == ns!() && name.local == local_name!("id") {
581 if let Some(owner) = self.doc.nodes[node_id].owner_document {
582 let old_id = self.doc.nodes[node_id]
583 .element_data()
584 .and_then(|element| element.id.as_ref())
585 .map(ToString::to_string);
586 if let Some(old_id) = old_id {
587 self.doc
588 .remove_from_document_id_map(owner, &old_id, node_id);
589 }
590 self.doc.add_to_document_id_map(owner, value, node_id);
591 }
592 if node_is_in_document {
593 if let Some(old_id) = self.doc.nodes[node_id]
594 .element_data()
595 .map(|element| element.id.clone())
596 {
597 if let Some(old_id) = old_id {
598 self.doc.remove_from_id_map(&old_id, node_id);
599 }
600 self.doc.add_to_id_map(value, node_id);
601 }
602 }
603 }
604
605 let node = &mut self.doc.nodes[node_id];
606
607 let NodeData::Element(ref mut element) = node.data else {
608 return;
609 };
610
611 self.mutations_occurred |= node_is_in_document;
612 #[cfg(feature = "custom-widget")]
614 if let SpecialElementData::CustomWidget(widget_data) = &mut element.special_data {
615 let old_value = element.attrs.get(&name).as_ref().map(|attr| &*attr.value);
616 widget_data
617 .widget
618 .attribute_changed(&name.local, old_value, Some(value));
619 }
620
621 #[cfg(feature = "shadow-dom")]
624 if element.custom_element_data().is_some() {
625 let old_value = element
626 .attrs
627 .get(&name)
628 .as_ref()
629 .map(|attr| attr.value.to_string());
630 self.custom_element_attr_changes.push((
631 node_id,
632 name.clone(),
633 old_value,
634 Some(value.to_string()),
635 ));
636 }
637
638 element.attrs.set(name.clone(), value);
639 if name.ns != ns!() {
640 return;
641 }
642
643 if name.local == local_name!("tabindex")
648 || name.local == local_name!("href")
649 || name.local == local_name!("disabled")
650 {
651 element.flush_is_focussable();
652 }
653
654 let tag = &element.name.local;
655 let attr = &name.local;
656
657 if *attr == local_name!("id") {
658 element.id = Some(Atom::from(value))
659 }
660
661 if *attr == local_name!("value") {
662 if let Some(input_data) = element.text_input_data_mut() {
663 input_data.set_text(
665 &mut self.doc.font_ctx.lock().unwrap(),
666 &mut self.doc.layout_ctx,
667 value,
668 );
669 }
670 return;
671 }
672
673 if *attr == local_name!("style") {
674 element.flush_style_attribute(&self.doc.guard, &self.doc.url.url_extra_data());
675 node.mark_style_attr_updated();
676 return;
677 }
678
679 if *attr == local_name!("disabled") && element.can_be_disabled() {
680 node.disable();
681 return;
682 }
683
684 if !node.flags.is_in_document() {
687 return;
688 }
689
690 if (tag, attr) == tag_and_attr!("input", "checked") {
691 set_input_checked_state(element, value.to_string());
692 } else if (tag, attr) == tag_and_attr!("img", "src") {
693 self.load_image(node_id);
694 } else if (tag, attr) == tag_and_attr!("canvas", "src") {
695 self.load_custom_paint_src(node_id);
696 } else if (tag, attr) == tag_and_attr!("link", "href") {
697 self.load_linked_stylesheet(node_id);
698 } else if (tag, attr) == tag_and_attr!("iframe", "src")
699 || (tag, attr) == tag_and_attr!("iframe", "srcdoc")
700 {
701 self.load_iframe(node_id);
702 } else if (tag, attr) == tag_and_attr!("option", "selected") {
703 self.set_option_selected_state(node_id, true);
714 }
715 }
716
717 fn set_option_selected_state(&mut self, option_id: NodeId, selected: bool) {
720 let Some(select_id) = self.doc.option_owner_select(option_id) else {
721 return;
722 };
723 let Some(index) = self
724 .doc
725 .select_options(select_id)
726 .iter()
727 .position(|id| *id == option_id)
728 else {
729 return;
730 };
731 let changed = if selected {
732 self.doc.set_select_selected_index(select_id, index)
733 } else {
734 self.doc
735 .get_node_mut(select_id)
736 .and_then(|node| node.data.downcast_element_mut())
737 .and_then(|el| el.select_data_mut())
738 .is_some_and(|data| data.set_selected(index, false))
739 };
740 if changed {
741 self.doc.snapshot_node(option_id);
744 self.doc.snapshot_node(select_id);
745 }
746 }
747
748 pub fn clear_attribute(&mut self, node_id: NodeId, name: QualName) {
749 #[cfg(feature = "shadow-dom")]
750 if name.local == local_name!("slot") || name.local == local_name!("name") {
751 self.doc.note_shadow_tree_change(node_id);
752 }
753 self.doc
754 .record_script_custom_element_attribute(node_id, &name, None);
755 self.doc
756 .record_inline_handler_attribute(node_id, &name, None);
757 self.doc.record_details_open(node_id, &name, false);
758 let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
759 if node_is_in_document && self.doc.is_recording_mutations() {
760 let old_value = self.doc.nodes[node_id]
763 .element_data()
764 .and_then(|element| element.attr(name.local.clone()))
765 .map(str::to_owned);
766 if old_value.is_some() {
767 self.doc.record_mutation(crate::DomMutation::Attributes {
768 target: node_id,
769 name: name.local.to_string(),
770 old_value,
771 });
772 }
773 }
774 if node_is_in_document {
775 let removes_existing_attribute = self.doc.nodes[node_id]
776 .element_data()
777 .is_some_and(|element| element.attrs.iter().any(|attr| attr.name == name));
778 self.doc.snapshot_attribute(node_id, &name.local);
779 let parent_id = self.doc.nodes[node_id].parent;
780 {
781 let node = &mut self.doc.nodes[node_id];
782 node.set_dirty_descendants();
783 if let Some(mut data) = node.stylo_element_data_opt_mut().and_then(|s| s.get_mut())
784 {
785 data.hint |= RestyleHint::restyle_subtree();
786 data.damage.insert(ALL_DAMAGE);
787 }
788 node.mark_ancestors_dirty();
790 }
791 if removes_existing_attribute {
792 self.restyle_parent_for_attribute_change(parent_id);
793 }
794 }
795
796 if name.ns == ns!() && name.local == local_name!("id") {
797 if let Some(old_id) = self.doc.nodes[node_id]
798 .element_data()
799 .and_then(|element| element.id.clone())
800 {
801 if let Some(owner) = self.doc.nodes[node_id].owner_document {
802 self.doc
803 .remove_from_document_id_map(owner, &old_id, node_id);
804 }
805 if node_is_in_document {
806 self.doc.remove_from_id_map(&old_id, node_id);
807 }
808 }
809 }
810
811 let node = &mut self.doc.nodes[node_id];
812
813 let Some(element) = node.element_data_mut() else {
814 return;
815 };
816
817 let removed_attr = element.attrs.remove(&name);
818 let had_attr = removed_attr.is_some();
819 if !had_attr {
820 return;
821 }
822 self.mutations_occurred |= node_is_in_document;
823
824 #[cfg(feature = "custom-widget")]
826 if let SpecialElementData::CustomWidget(widget_data) = &mut element.special_data {
827 let old_value = removed_attr.as_ref().map(|attr| &*attr.value);
828 widget_data
829 .widget
830 .attribute_changed(&name.local, old_value, None);
831 }
832
833 #[cfg(feature = "shadow-dom")]
835 if element.custom_element_data().is_some() {
836 let old_value = removed_attr.as_ref().map(|attr| attr.value.to_string());
837 self.custom_element_attr_changes
838 .push((node_id, name.clone(), old_value, None));
839 }
840
841 if name.ns != ns!() {
842 return;
843 }
844 if name.local == local_name!("id") {
845 element.id = None;
846 }
847
848 if name.local == local_name!("tabindex")
851 || name.local == local_name!("href")
852 || name.local == local_name!("disabled")
853 {
854 element.flush_is_focussable();
855 }
856
857 if name.local == local_name!("value") {
859 if let Some(input_data) = element.text_input_data_mut() {
860 input_data.set_text(
861 &mut self.doc.font_ctx.lock().unwrap(),
862 &mut self.doc.layout_ctx,
863 "",
864 );
865 }
866 }
867
868 let tag = &element.name.local;
869 let attr = &name.local;
870
871 if *attr == local_name!("disabled") && element.can_be_disabled() {
872 node.enable();
873 return;
874 }
875
876 if *attr == local_name!("style") {
877 element.flush_style_attribute(&self.doc.guard, &self.doc.url.url_extra_data());
878 node.mark_style_attr_updated();
879 } else if (tag, attr) == tag_and_attr!("canvas", "src") {
880 self.recompute_is_animating = true;
881 } else if (tag, attr) == tag_and_attr!("link", "href") {
882 self.unload_stylesheet(node_id);
883 } else if (tag, attr) == tag_and_attr!("iframe", "srcdoc") && node_is_in_document {
884 self.load_iframe(node_id);
886 }
887 }
888
889 pub fn set_style_property(&mut self, node_id: NodeId, name: &str, value: &str) {
890 let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
891 self.doc.set_style_property(node_id, name, value);
892 self.mutations_occurred |= node_is_in_document;
893 }
894
895 pub fn remove_style_property(&mut self, node_id: NodeId, name: &str) {
896 let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
897 self.doc.remove_style_property(node_id, name);
898 self.mutations_occurred |= node_is_in_document;
899 }
900
901 pub fn set_sub_document(&mut self, node_id: NodeId, sub_document: Box<dyn Document>) {
902 let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
903 self.doc.set_sub_document(node_id, sub_document);
904 self.mutations_occurred |= node_is_in_document;
905 }
906
907 pub fn remove_sub_document(&mut self, node_id: NodeId) {
908 let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
909 self.doc.remove_sub_document(node_id);
910 self.mutations_occurred |= node_is_in_document;
911 }
912
913 #[cfg(feature = "custom-widget")]
914 pub fn set_custom_widget(&mut self, node_id: NodeId, widget: Box<dyn crate::Widget>) {
915 let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
916 self.doc.set_custom_widget(node_id, widget);
917 self.mutations_occurred |= node_is_in_document;
918 }
919
920 #[cfg(feature = "custom-widget")]
921 pub fn remove_custom_widget(&mut self, node_id: NodeId) {
922 let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
923 self.doc.remove_custom_widget(node_id);
924 self.mutations_occurred |= node_is_in_document;
925 }
926
927 #[cfg(feature = "shadow-dom")]
930 pub fn attach_shadow(&mut self, host_id: NodeId, mode: crate::node::ShadowRootMode) -> NodeId {
931 self.doc.attach_shadow(host_id, mode)
932 }
933
934 #[cfg(feature = "shadow-dom")]
935 pub fn try_attach_declarative_shadow(
936 &mut self,
937 host_id: NodeId,
938 template_id: NodeId,
939 mode: crate::node::ShadowRootMode,
940 clonable: bool,
941 serializable: bool,
942 delegates_focus: bool,
943 ) -> bool {
944 self.doc.try_attach_declarative_shadow(
945 host_id,
946 template_id,
947 mode,
948 clonable,
949 serializable,
950 delegates_focus,
951 )
952 }
953
954 #[cfg(feature = "shadow-dom")]
955 pub fn declarative_shadow_for_template(&self, template_id: NodeId) -> Option<NodeId> {
956 self.doc
957 .declarative_template_shadows
958 .get(&template_id)
959 .copied()
960 }
961
962 #[cfg(feature = "shadow-dom")]
963 pub fn forget_declarative_template(&mut self, template_id: NodeId) {
964 self.doc.declarative_template_shadows.remove(&template_id);
965 }
966
967 #[cfg(feature = "shadow-dom")]
970 pub fn set_custom_element(
971 &mut self,
972 node_id: NodeId,
973 controller: Box<dyn crate::node::CustomElement>,
974 ) {
975 self.doc.set_custom_element(node_id, controller);
976 self.upgrade_custom_element(node_id);
977 }
978
979 #[cfg(feature = "shadow-dom")]
982 pub fn remove_custom_element(&mut self, node_id: NodeId) {
983 self.disconnect_custom_element(node_id);
984 let _ = self.doc.take_custom_element(node_id);
985 }
986
987 #[cfg(feature = "shadow-dom")]
992 pub(crate) fn upgrade_custom_element(&mut self, node_id: NodeId) {
993 use crate::node::{CustomElementData, ShadowRootMode, SpecialElementData};
994
995 let Some(node) = self.doc.get_node(node_id) else {
996 return;
997 };
998 let Some(element) = node.element_data() else {
999 return;
1000 };
1001
1002 let already_has_controller =
1005 matches!(element.special_data, SpecialElementData::CustomElement(_));
1006
1007 let mode = if already_has_controller {
1008 ShadowRootMode::Open
1010 } else {
1011 let tag = element.name.local.clone();
1012 let Some(definition) = self.doc.custom_element_registry.get(&tag) else {
1013 return;
1014 };
1015 let mode = definition.mode;
1016 let controller = (definition.factory)();
1017 self.doc.nodes[node_id]
1018 .element_data_mut()
1019 .unwrap()
1020 .special_data =
1021 SpecialElementData::CustomElement(CustomElementData::new(controller));
1022 self.doc.custom_element_nodes.insert(node_id);
1023 mode
1024 };
1025
1026 let is_upgraded = self.doc.nodes[node_id]
1028 .element_data()
1029 .and_then(|el| el.custom_element_data())
1030 .map(|data| data.upgraded)
1031 .unwrap_or(true);
1032 if is_upgraded {
1033 return;
1034 }
1035
1036 let shadow_root_id = self.doc.attach_shadow(node_id, mode);
1038
1039 let Some(mut controller) = self.take_controller(node_id) else {
1041 return;
1042 };
1043
1044 {
1045 let mut ctx = crate::node::CustomElementCtx {
1046 mutator: self,
1047 host_id: node_id,
1048 shadow_root_id,
1049 };
1050 controller.connected(&mut ctx);
1051 }
1052
1053 self.restore_controller(node_id, controller, true);
1054 }
1055
1056 #[cfg(feature = "shadow-dom")]
1058 pub(crate) fn disconnect_custom_element(&mut self, node_id: NodeId) {
1059 let Some(shadow_root_id) = self
1060 .doc
1061 .get_node(node_id)
1062 .and_then(|node| node.shadow_root_id())
1063 else {
1064 if let Some(mut controller) = self.take_controller(node_id) {
1066 {
1069 let mut ctx = crate::node::CustomElementCtx {
1070 mutator: self,
1071 host_id: node_id,
1072 shadow_root_id: node_id,
1073 };
1074 controller.disconnected(&mut ctx);
1075 }
1076 self.restore_controller(node_id, controller, false);
1077 }
1078 return;
1079 };
1080
1081 if let Some(mut controller) = self.take_controller(node_id) {
1082 {
1083 let mut ctx = crate::node::CustomElementCtx {
1084 mutator: self,
1085 host_id: node_id,
1086 shadow_root_id,
1087 };
1088 controller.disconnected(&mut ctx);
1089 }
1090 self.restore_controller(node_id, controller, false);
1091 }
1092 }
1093
1094 #[cfg(feature = "shadow-dom")]
1097 fn take_controller(&mut self, node_id: NodeId) -> Option<Box<dyn crate::node::CustomElement>> {
1098 self.doc
1099 .nodes
1100 .get_mut(node_id)?
1101 .element_data_mut()?
1102 .custom_element_data_mut()?
1103 .controller
1104 .take()
1105 }
1106
1107 #[cfg(feature = "shadow-dom")]
1110 fn restore_controller(
1111 &mut self,
1112 node_id: NodeId,
1113 controller: Box<dyn crate::node::CustomElement>,
1114 upgraded: bool,
1115 ) {
1116 if let Some(data) = self
1117 .doc
1118 .nodes
1119 .get_mut(node_id)
1120 .and_then(|node| node.element_data_mut())
1121 .and_then(|el| el.custom_element_data_mut())
1122 {
1123 data.controller = Some(controller);
1124 if upgraded {
1125 data.upgraded = true;
1126 }
1127 }
1128 }
1129
1130 fn invalidate_layout_parent_edge(&mut self, node_id: NodeId) {
1139 let Some(layout_parent_id) = self
1140 .doc
1141 .nodes
1142 .get(node_id)
1143 .and_then(|node| node.layout_parent.get())
1144 else {
1145 return;
1146 };
1147 if let Some(layout_parent) = self.doc.nodes.get_mut(layout_parent_id) {
1148 layout_parent.layout_children.get_mut().take();
1149 layout_parent.paint_children.get_mut().take();
1150 layout_parent.insert_damage(ALL_DAMAGE);
1151 }
1152 if let Some(node) = self.doc.nodes.get(node_id) {
1153 node.layout_parent.set(None);
1154 }
1155 }
1156
1157 fn clear_layout_of_subtree(doc: &mut BaseDocument, node_id: NodeId) {
1159 let mut stack = vec![node_id];
1160 while let Some(id) = stack.pop() {
1161 let Some(node) = doc.nodes.get_mut(id) else {
1162 continue;
1163 };
1164 if node.data.downcast_element().is_some() {
1168 *node.unrounded_layout_mut() = taffy::Layout::with_order(0);
1169 *node.final_layout_mut() = taffy::Layout::with_order(0);
1170 node.cache_mut().clear();
1171 }
1172 stack.extend(node.children.iter().copied());
1173 }
1174 }
1175
1176 fn record_removal(&mut self, node_id: NodeId) {
1180 self.doc.range_remove_node(node_id);
1181 if !self.doc.is_recording_mutations() || !self.doc.nodes[node_id].flags.is_in_document() {
1182 return;
1183 }
1184 if let Some((parent_id, previous_sibling, next_sibling)) = self.doc.sibling_context(node_id)
1185 {
1186 self.doc.record_mutation(crate::DomMutation::ChildList {
1187 target: parent_id,
1188 added: Vec::new(),
1189 removed: vec![node_id],
1190 previous_sibling,
1191 next_sibling,
1192 });
1193 }
1194 }
1195
1196 pub fn remove_node(&mut self, node_id: NodeId) {
1197 self.record_removal(node_id);
1198 self.doc.detach_script_custom_element_subtree(node_id);
1199 let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
1200 self.process_removed_subtree(node_id);
1204
1205 Self::clear_layout_of_subtree(self.doc, node_id);
1213 self.invalidate_layout_parent_edge(node_id);
1214
1215 let node = &mut self.doc.nodes[node_id];
1216
1217 if let Some(parent_id) = node.parent.take() {
1219 self.mutations_occurred |= node_is_in_document;
1220 let parent = &mut self.doc.nodes[parent_id];
1221 parent.insert_damage(ALL_DAMAGE);
1222 parent.mark_ancestors_dirty();
1224 parent.children.retain(|id| *id != node_id);
1225 self.maybe_record_node(parent_id);
1226 }
1227 }
1228
1229 pub fn remove_and_drop_node(&mut self, node_id: NodeId) -> Option<Node> {
1230 self.remove_and_drop_node_with(node_id, &mut |_| {})
1231 }
1232
1233 pub fn remove_and_drop_node_with(
1236 &mut self,
1237 node_id: NodeId,
1238 on_drop: &mut dyn FnMut(NodeId),
1239 ) -> Option<Node> {
1240 self.record_removal(node_id);
1241 self.doc.detach_script_custom_element_subtree(node_id);
1242 let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
1243 self.process_removed_subtree(node_id);
1244 self.invalidate_layout_parent_edge(node_id);
1245
1246 let node = self.doc.drop_node_ignoring_parent_with(node_id, on_drop);
1247 self.mutations_occurred |= node_is_in_document;
1248
1249 if let Some(parent_id) = node.as_ref().and_then(|node| node.parent) {
1251 let parent = &mut self.doc.nodes[parent_id];
1252 parent.insert_damage(ALL_DAMAGE);
1253 let parent_is_in_doc = parent.flags.is_in_document();
1254
1255 if parent_is_in_doc {
1257 if let Some(mut data) = parent
1258 .stylo_element_data_opt_mut()
1259 .and_then(|s| s.get_mut())
1260 {
1261 data.hint |= RestyleHint::restyle_subtree();
1262 }
1263 parent.mark_ancestors_dirty();
1265 }
1266
1267 parent.children.retain(|id| *id != node_id);
1268 self.maybe_record_node(parent_id);
1269 }
1270
1271 node
1272 }
1273
1274 pub fn remove_and_drop_all_children(&mut self, node_id: NodeId) {
1275 if !self.doc.live_ranges.is_empty() {
1276 let children = self.doc.nodes[node_id].children.clone();
1277 for child in children.into_iter().rev() {
1278 self.doc.range_remove_node(child);
1279 }
1280 }
1281 if self.doc.is_recording_mutations()
1282 && self.doc.nodes[node_id].flags.is_in_document()
1283 && !self.doc.nodes[node_id].children.is_empty()
1284 {
1285 let removed = self.doc.nodes[node_id].children.to_vec();
1286 self.doc.record_mutation(crate::DomMutation::ChildList {
1287 target: node_id,
1288 added: Vec::new(),
1289 removed,
1290 previous_sibling: None,
1291 next_sibling: None,
1292 });
1293 }
1294 let parent = &mut self.doc.nodes[node_id];
1295 let parent_is_in_doc = parent.flags.is_in_document();
1296
1297 if parent_is_in_doc {
1299 if let Some(mut data) = parent
1300 .stylo_element_data_opt_mut()
1301 .and_then(|s| s.get_mut())
1302 {
1303 data.hint |= RestyleHint::restyle_subtree();
1304 }
1305 parent.mark_ancestors_dirty();
1307 }
1308
1309 let children = mem::take(&mut parent.children);
1310 self.mutations_occurred |= parent_is_in_doc && !children.is_empty();
1311 for child_id in children {
1312 self.doc.detach_script_custom_element_subtree(child_id);
1313 self.process_removed_subtree(child_id);
1314 self.invalidate_layout_parent_edge(child_id);
1315 let _ = self.doc.drop_node_ignoring_parent(child_id);
1316 }
1317 self.maybe_record_node(node_id);
1318 }
1319
1320 pub fn remove_node_if_unparented(&mut self, node_id: NodeId) {
1322 self.remove_node_if_unparented_with(node_id, &mut |_| {});
1323 }
1324
1325 pub fn remove_node_if_unparented_with(
1328 &mut self,
1329 node_id: NodeId,
1330 on_drop: &mut dyn FnMut(NodeId),
1331 ) {
1332 if let Some(node) = self.doc.get_node(node_id) {
1333 if node.parent.is_none() {
1334 self.remove_and_drop_node_with(node_id, on_drop);
1335 }
1336 }
1337 }
1338
1339 pub fn append_children(&mut self, parent_id: NodeId, child_ids: &[NodeId]) {
1341 self.add_children_to_parent(parent_id, child_ids, &|parent, child_ids| {
1342 parent.children.extend_from_slice(child_ids);
1343 });
1344 }
1345
1346 pub fn insert_nodes_before(&mut self, anchor_node_id: NodeId, new_node_ids: &[NodeId]) {
1347 let parent_id = self.doc.nodes[anchor_node_id].parent.unwrap();
1348 self.add_children_to_parent(parent_id, new_node_ids, &|parent, child_ids| {
1349 let node_child_idx = parent.index_of_child(anchor_node_id).unwrap();
1350 parent
1351 .children
1352 .splice(node_child_idx..node_child_idx, child_ids.iter().copied());
1353 });
1354 }
1355
1356 fn add_children_to_parent(
1357 &mut self,
1358 parent_id: NodeId,
1359 child_ids: &[NodeId],
1360 insert_children_fn: &dyn Fn(&mut Node, &[NodeId]),
1361 ) {
1362 let child_ids: Vec<NodeId> = if child_ids
1370 .iter()
1371 .any(|id| matches!(self.doc.nodes[*id].data, NodeData::DocumentFragment))
1372 {
1373 let mut flattened = Vec::with_capacity(child_ids.len());
1374 for id in child_ids.iter().copied() {
1375 if matches!(self.doc.nodes[id].data, NodeData::DocumentFragment) {
1376 if !self.doc.live_ranges.is_empty() {
1377 let children = self.doc.nodes[id].children.clone();
1378 for child in children.into_iter().rev() {
1379 self.doc.range_remove_node(child);
1380 }
1381 }
1382 let moved = std::mem::take(&mut self.doc.nodes[id].children);
1383 flattened.extend(moved);
1384 } else {
1385 flattened.push(id);
1386 }
1387 }
1388 flattened
1389 } else {
1390 child_ids.to_vec()
1391 };
1392 let child_ids = child_ids.as_slice();
1393
1394 let new_parent_is_in_document = self.doc.nodes[parent_id].flags.is_in_document();
1395 self.mutations_occurred |= new_parent_is_in_document && !child_ids.is_empty();
1396 for child_id in child_ids.iter().copied() {
1403 self.record_removal(child_id);
1404 self.doc.detach_script_custom_element_subtree(child_id);
1405 self.invalidate_layout_parent_edge(child_id);
1406 let child = &mut self.doc.nodes[child_id];
1407 let child_was_in_doc = child.flags.is_in_document();
1408 self.mutations_occurred |= child_was_in_doc;
1409 let Some(old_parent_id) = child.parent.take() else {
1410 continue;
1411 };
1412
1413 let old_parent = &mut self.doc.nodes[old_parent_id];
1414 old_parent.insert_damage(ALL_DAMAGE);
1415
1416 if child_was_in_doc {
1418 if let Some(mut data) = old_parent
1419 .stylo_element_data_opt_mut()
1420 .and_then(|s| s.get_mut())
1421 {
1422 data.hint |= RestyleHint::restyle_subtree();
1423 }
1424 old_parent.mark_ancestors_dirty();
1426 }
1427
1428 old_parent.children.retain(|id| *id != child_id);
1429 self.maybe_record_node(old_parent_id);
1430 }
1431
1432 let new_parent = &mut self.doc.nodes[parent_id];
1433 new_parent.insert_damage(ALL_DAMAGE);
1434
1435 if new_parent_is_in_document {
1437 if let Some(mut data) = new_parent
1438 .stylo_element_data_opt_mut()
1439 .and_then(|s| s.get_mut())
1440 {
1441 data.hint |= RestyleHint::restyle_subtree();
1442 }
1443 new_parent.mark_ancestors_dirty();
1445 }
1446
1447 insert_children_fn(new_parent, child_ids);
1448 if !self.doc.live_ranges.is_empty()
1449 && let Some(first) = child_ids.first()
1450 && let Some(index) = self.doc.nodes[parent_id].index_of_child(*first)
1451 {
1452 self.doc
1453 .range_insert_nodes(parent_id, index, child_ids.len());
1454 }
1455
1456 for child_id in child_ids.iter().copied() {
1457 let child = &mut self.doc.nodes[child_id];
1458 let child_was_in_doc = child.flags.is_in_document();
1459 child.parent = Some(parent_id);
1460 self.doc
1461 .attach_script_custom_element_subtree(child_id, parent_id);
1462
1463 if new_parent_is_in_document && !child_was_in_doc {
1464 self.process_added_subtree(child_id);
1465 } else if !new_parent_is_in_document && child_was_in_doc {
1466 self.process_removed_subtree(child_id);
1467 }
1468 }
1469
1470 if new_parent_is_in_document && self.doc.is_recording_mutations() {
1471 if let (Some(first), Some(last)) = (child_ids.first(), child_ids.last()) {
1472 let previous_sibling = self.doc.sibling_context(*first).and_then(|c| c.1);
1473 let next_sibling = self.doc.sibling_context(*last).and_then(|c| c.2);
1474 self.doc.record_mutation(crate::DomMutation::ChildList {
1475 target: parent_id,
1476 added: child_ids.to_vec(),
1477 removed: Vec::new(),
1478 previous_sibling,
1479 next_sibling,
1480 });
1481 }
1482 }
1483
1484 self.maybe_record_node(parent_id);
1485 }
1486
1487 pub fn insert_nodes_after(&mut self, anchor_node_id: NodeId, new_node_ids: &[NodeId]) {
1489 match self.next_sibling_id(anchor_node_id) {
1490 Some(id) => self.insert_nodes_before(id, new_node_ids),
1491 None => {
1492 let parent_id = self.parent_id(anchor_node_id).unwrap();
1493 self.append_children(parent_id, new_node_ids)
1494 }
1495 }
1496 }
1497
1498 pub fn reparent_children(&mut self, old_parent_id: NodeId, new_parent_id: NodeId) {
1499 if self.doc.is_recording_mutations()
1500 && self.doc.nodes[old_parent_id].flags.is_in_document()
1501 && !self.doc.nodes[old_parent_id].children.is_empty()
1502 {
1503 let removed = self.doc.nodes[old_parent_id].children.to_vec();
1504 self.doc.record_mutation(crate::DomMutation::ChildList {
1505 target: old_parent_id,
1506 added: Vec::new(),
1507 removed,
1508 previous_sibling: None,
1509 next_sibling: None,
1510 });
1511 }
1512 if !self.doc.live_ranges.is_empty() {
1513 let children = self.doc.nodes[old_parent_id].children.clone();
1514 for child in children.into_iter().rev() {
1515 self.doc.range_remove_node(child);
1516 }
1517 }
1518 let child_ids = std::mem::take(&mut self.doc.nodes[old_parent_id].children);
1519 self.maybe_record_node(old_parent_id);
1520 self.append_children(new_parent_id, &child_ids);
1521 }
1522
1523 pub fn replace_node_with(&mut self, anchor_node_id: NodeId, new_node_ids: &[NodeId]) {
1524 self.insert_nodes_before(anchor_node_id, new_node_ids);
1525 self.remove_node(anchor_node_id);
1526 }
1527}
1528
1529impl<'doc> DocumentMutator<'doc> {
1530 pub fn flush(&mut self) {
1531 if self.recompute_is_animating {
1532 self.doc.has_canvas = self.doc.compute_has_canvas();
1533 }
1534
1535 if let Some(id) = self.title_node {
1536 let title = self.doc.nodes[id].text_content();
1537 self.doc.shell_provider.set_window_title(title);
1538 }
1539
1540 for id in self.style_nodes.drain() {
1542 self.doc.process_style_element(id);
1543 }
1544
1545 for id in self.form_nodes.drain() {
1546 self.doc.reset_form_owner(id);
1547 }
1548
1549 #[cfg(feature = "autofocus")]
1550 if let Some(node_id) = self.node_to_autofocus.take() {
1551 if self.doc.get_node(node_id).is_some() {
1552 self.doc.set_focus_to(node_id);
1553 }
1554 }
1555
1556 #[cfg(feature = "shadow-dom")]
1557 self.dispatch_custom_element_attr_changes();
1558 }
1559
1560 #[cfg(feature = "shadow-dom")]
1562 fn dispatch_custom_element_attr_changes(&mut self) {
1563 if self.custom_element_attr_changes.is_empty() {
1564 return;
1565 }
1566 let changes = mem::take(&mut self.custom_element_attr_changes);
1567 for (node_id, name, old_value, new_value) in changes {
1568 let tag = self
1572 .doc
1573 .get_node(node_id)
1574 .and_then(|node| node.element_data())
1575 .map(|el| el.name.local.clone());
1576 let observed = tag
1577 .as_ref()
1578 .and_then(|tag| self.doc.custom_element_registry.get(tag))
1579 .map(|def| def.observes(&name.local))
1580 .unwrap_or(true);
1581 if !observed {
1582 continue;
1583 }
1584
1585 let Some(shadow_root_id) = self
1586 .doc
1587 .get_node(node_id)
1588 .and_then(|node| node.shadow_root_id())
1589 else {
1590 continue;
1591 };
1592 let Some(mut controller) = self.take_controller(node_id) else {
1593 continue;
1594 };
1595 {
1596 let mut ctx = crate::node::CustomElementCtx {
1597 mutator: self,
1598 host_id: node_id,
1599 shadow_root_id,
1600 };
1601 controller.attribute_changed(
1602 &mut ctx,
1603 &name.local,
1604 old_value.as_deref(),
1605 new_value.as_deref(),
1606 );
1607 }
1608 self.restore_controller(node_id, controller, false);
1609 }
1610 }
1611
1612 pub fn set_inner_html(&mut self, node_id: NodeId, html: &str) {
1613 self.set_html_replacing(node_id, html, false);
1614 }
1615
1616 pub fn set_html_unsafe(&mut self, node_id: NodeId, html: &str) {
1618 self.set_html_replacing(node_id, html, true);
1619 }
1620
1621 fn set_html_replacing(
1622 &mut self,
1623 node_id: NodeId,
1624 html: &str,
1625 allow_declarative_shadow_roots: bool,
1626 ) {
1627 let target = self.doc.nodes[node_id]
1628 .element_data()
1629 .and_then(|element| element.template_contents)
1630 .unwrap_or(node_id);
1631 self.remove_and_drop_all_children(target);
1632 #[cfg(feature = "shadow-dom")]
1633 if let Some(host_id) = self.doc.nodes[node_id]
1634 .shadow_root_data()
1635 .map(|root| root.host)
1636 {
1637 let name = self.doc.nodes[host_id].element_data().unwrap().name.clone();
1643 let context_id = self.create_element(name, Vec::new());
1644 if allow_declarative_shadow_roots {
1645 self.doc
1646 .html_parser_provider
1647 .clone()
1648 .parse_fragment(self, context_id, html, true);
1649 } else {
1650 self.doc
1651 .html_parser_provider
1652 .clone()
1653 .parse_inner_html(self, context_id, html);
1654 }
1655 let children = self.child_ids(context_id);
1656 self.append_children(node_id, &children);
1657 self.remove_and_drop_node(context_id);
1658 self.doc.note_shadow_tree_change(node_id);
1659 return;
1660 }
1661 if allow_declarative_shadow_roots {
1662 self.doc
1663 .html_parser_provider
1664 .clone()
1665 .parse_fragment(self, node_id, html, true);
1666 #[cfg(feature = "shadow-dom")]
1671 self.promote_direct_declarative_shadows(target);
1672 } else {
1673 self.doc
1674 .html_parser_provider
1675 .clone()
1676 .parse_inner_html(self, node_id, html);
1677 }
1678 }
1679
1680 #[cfg(feature = "shadow-dom")]
1684 fn promote_direct_declarative_shadows(&mut self, target: NodeId) {
1685 let can_host = self
1686 .doc
1687 .nodes
1688 .get(target)
1689 .and_then(|node| node.element_data())
1690 .is_some_and(|element| {
1691 BaseDocument::element_can_be_shadow_host(element) && element.shadow_root.is_none()
1692 });
1693 if !can_host {
1694 return;
1695 }
1696 let children = self.child_ids(target);
1697 for child_id in children {
1698 let info = self.doc.nodes[child_id]
1699 .element_data()
1700 .and_then(Self::declarative_template_flags);
1701 let Some((mode, clonable, serializable, delegates_focus, contents)) = info else {
1702 continue;
1703 };
1704 let root_id = self.doc.attach_shadow(target, mode);
1705 if let Some(root) = self.doc.nodes[root_id].shadow_root_data_mut() {
1706 root.declarative = true;
1707 root.clonable = clonable;
1708 root.serializable = serializable;
1709 root.delegates_focus = delegates_focus;
1710 }
1711 if let Some(contents) = contents {
1712 let inner = self.child_ids(contents);
1713 if !inner.is_empty() {
1714 self.append_children(root_id, &inner);
1715 }
1716 }
1717 self.remove_and_drop_node(child_id);
1718 self.doc.note_shadow_tree_change(target);
1719 break;
1720 }
1721 }
1722
1723 #[cfg(feature = "shadow-dom")]
1724 fn declarative_template_flags(
1725 element: &ElementData,
1726 ) -> Option<(
1727 crate::node::ShadowRootMode,
1728 bool,
1729 bool,
1730 bool,
1731 Option<NodeId>,
1732 )> {
1733 if element.name.ns != ns!(html) || element.name.local != local_name!("template") {
1734 return None;
1735 }
1736 let raw = element.attr(local_name!("shadowrootmode"))?;
1737 let mode = if raw.eq_ignore_ascii_case("open") {
1738 crate::node::ShadowRootMode::Open
1739 } else if raw.eq_ignore_ascii_case("closed") {
1740 crate::node::ShadowRootMode::Closed
1741 } else {
1742 return None;
1743 };
1744 Some((
1745 mode,
1746 element.has_attr(local_name!("shadowrootclonable")),
1747 element.has_attr(local_name!("shadowrootserializable")),
1748 element.has_attr(local_name!("shadowrootdelegatesfocus")),
1749 element.template_contents,
1750 ))
1751 }
1752
1753 fn flush_eager_ops(&mut self) {
1754 let mut ops = mem::take(&mut self.eager_op_queue);
1755 for op in ops.drain(0..) {
1756 match op {
1757 SpecialOp::LoadImage(node_id) => self.load_image(node_id),
1758 SpecialOp::LoadIframe(node_id) => self.load_iframe(node_id),
1759 SpecialOp::LoadStylesheet(node_id) => self.load_linked_stylesheet(node_id),
1760 SpecialOp::UnloadStylesheet(node_id) => self.unload_stylesheet(node_id),
1761 SpecialOp::LoadCustomPaintSource(node_id) => self.load_custom_paint_src(node_id),
1762 SpecialOp::ProcessButtonInput(node_id) => self.process_button_input(node_id),
1763 SpecialOp::UnloadSubDocument(node_id) => self.remove_sub_document(node_id),
1764 #[cfg(feature = "custom-widget")]
1765 SpecialOp::UnloadCustomWidget(node_id) => self.remove_custom_widget(node_id),
1766 #[cfg(feature = "shadow-dom")]
1767 SpecialOp::UpgradeCustomElement(node_id) => self.upgrade_custom_element(node_id),
1768 #[cfg(feature = "shadow-dom")]
1769 SpecialOp::DisconnectCustomElement(node_id) => {
1770 self.disconnect_custom_element(node_id)
1771 }
1772 }
1773 }
1774
1775 self.eager_op_queue = ops;
1777 }
1778
1779 fn process_added_subtree(&mut self, node_id: NodeId) {
1780 self.doc
1781 .iter_shadow_including_subtree_mut(node_id, |node_id, doc| {
1782 let node = &mut doc.nodes[node_id];
1783 node.flags.set(NodeFlags::IS_IN_DOCUMENT, true);
1784 node.insert_damage(ALL_DAMAGE);
1785
1786 if let Some(id_attr) = node.attr(local_name!("id")).map(ToString::to_string) {
1788 doc.add_to_id_map(&id_attr, node_id);
1789 }
1790
1791 let node = &mut doc.nodes[node_id];
1792 let NodeData::Element(ref mut element) = node.data else {
1793 return;
1794 };
1795
1796 let tag = element.name.local.as_ref();
1798 match tag {
1799 "title" if element.name.ns == ns!(html) => self.title_node = Some(node_id),
1800 "link" => self.eager_op_queue.push(SpecialOp::LoadStylesheet(node_id)),
1801 "img" => self.eager_op_queue.push(SpecialOp::LoadImage(node_id)),
1802 "iframe" => self.eager_op_queue.push(SpecialOp::LoadIframe(node_id)),
1803 "canvas" => self
1804 .eager_op_queue
1805 .push(SpecialOp::LoadCustomPaintSource(node_id)),
1806 "style" => {
1807 self.style_nodes.insert(node_id);
1808 }
1809 "button" | "fieldset" | "input" | "select" | "textarea" | "object"
1810 | "output" => {
1811 self.eager_op_queue
1812 .push(SpecialOp::ProcessButtonInput(node_id));
1813 self.form_nodes.insert(node_id);
1814 }
1815 _ => {}
1816 }
1817
1818 #[cfg(feature = "shadow-dom")]
1822 {
1823 let needs_upgrade = doc.custom_element_registry.contains(&element.name.local)
1824 && element.custom_element_data().is_none();
1825 if needs_upgrade {
1826 self.eager_op_queue
1827 .push(SpecialOp::UpgradeCustomElement(node_id));
1828 }
1829 }
1830
1831 #[cfg(feature = "autofocus")]
1842 if node.is_focussable() {
1843 if let NodeData::Element(ref element) = node.data {
1844 if element.attr(local_name!("autofocus")).is_some() {
1845 self.node_to_autofocus = Some(node_id);
1846 }
1847 }
1848 }
1849 });
1850
1851 self.flush_eager_ops();
1852 }
1853
1854 fn process_removed_subtree(&mut self, node_id: NodeId) {
1855 let mut removed = FxHashSet::default();
1856 let mut parents = FxHashSet::default();
1857 self.doc
1858 .iter_shadow_including_subtree_mut(node_id, |node_id, doc| {
1859 removed.insert(node_id);
1860 let node = &doc.nodes[node_id];
1861 parents.extend(
1862 [node.parent, node.layout_parent.get()]
1863 .into_iter()
1864 .flatten(),
1865 );
1866 doc.nodes[node_id]
1867 .flags
1868 .set(NodeFlags::IS_IN_DOCUMENT, false);
1869
1870 doc.clear_interaction_state_for_removed_node(node_id);
1874
1875 let node = &mut doc.nodes[node_id];
1876
1877 if doc.focus_node_id == Some(node_id) {
1894 doc.focus_node_id = None;
1895 }
1896 if doc.mousedown_node_id == Some(node_id) {
1897 doc.mousedown_node_id = None;
1898 }
1899
1900 if doc.text_selection.anchor.node_or_parent == Some(node_id)
1903 || doc.text_selection.focus.node_or_parent == Some(node_id)
1904 {
1905 doc.text_selection.clear();
1906 }
1907
1908 if node.has_snapshot() {
1911 let opaque_id = style::dom::TNode::opaque(&&*node);
1912 doc.snapshots.remove(&opaque_id);
1913 node.set_has_snapshot(false);
1914 }
1915
1916 if let Some(id_attr) = node.attr(local_name!("id")).map(ToString::to_string) {
1918 doc.remove_from_id_map(&id_attr, node_id);
1919 }
1920
1921 let node = &mut doc.nodes[node_id];
1922 let NodeData::Element(ref mut element) = node.data else {
1923 return;
1924 };
1925
1926 match &element.special_data {
1927 SpecialElementData::SubDocument(_) => {
1928 self.eager_op_queue
1929 .push(SpecialOp::UnloadSubDocument(node_id));
1930 }
1931 #[cfg(feature = "custom-widget")]
1932 SpecialElementData::CustomWidget(_) => {
1933 self.eager_op_queue
1934 .push(SpecialOp::UnloadCustomWidget(node_id));
1935 }
1936 #[cfg(feature = "shadow-dom")]
1937 SpecialElementData::CustomElement(_) => {
1938 self.eager_op_queue
1939 .push(SpecialOp::DisconnectCustomElement(node_id));
1940 }
1941 SpecialElementData::Stylesheet(_) => self
1942 .eager_op_queue
1943 .push(SpecialOp::UnloadStylesheet(node_id)),
1944 SpecialElementData::Image(_) => {}
1945 SpecialElementData::Canvas(_) => {
1946 self.recompute_is_animating = true;
1947 }
1948 SpecialElementData::TableRoot(_) => {}
1949 SpecialElementData::TextInput(_) => {}
1950 SpecialElementData::CheckboxInput(_) => {}
1951 SpecialElementData::Select(_) => {}
1952 #[cfg(feature = "file-input")]
1953 SpecialElementData::FileInput(_) => {}
1954 SpecialElementData::None => {}
1955 }
1956 });
1957
1958 self.doc
1962 .invalidate_node_references(&removed, parents, FxHashSet::default());
1963 #[cfg(feature = "autofocus")]
1964 if self
1965 .node_to_autofocus
1966 .is_some_and(|id| removed.contains(&id))
1967 {
1968 self.node_to_autofocus = None;
1969 }
1970
1971 self.flush_eager_ops();
1972 }
1973
1974 fn maybe_record_node(&mut self, node_id: impl Into<Option<NodeId>>) {
1975 let Some(node_id) = node_id.into() else {
1976 return;
1977 };
1978 if self.doc.nodes[node_id].owner_document != Some(self.doc.root_node().id) {
1979 return;
1980 }
1981
1982 #[cfg(feature = "shadow-dom")]
1983 self.doc.note_shadow_tree_change(node_id);
1984
1985 let Some(element) = self.doc.nodes[node_id].data.downcast_element() else {
1986 return;
1987 };
1988
1989 match element.name.local.as_ref() {
1990 "title" if element.name.ns == ns!(html) => self.title_node = Some(node_id),
1991 "style" => {
1992 self.style_nodes.insert(node_id);
1993 }
1994 _ => {}
1995 }
1996 }
1997
1998 fn load_linked_stylesheet(&mut self, target_id: NodeId) {
1999 let node = &self.doc.nodes[target_id];
2000
2001 let mut is_in_head = false;
2002 let mut parent_id = node.parent;
2003 while let Some(id) = parent_id
2004 && !is_in_head
2005 {
2006 let parent = &self.doc.nodes[id];
2007 is_in_head |= parent.data.is_element_with_tag_name(&local_name!("head"));
2008 parent_id = parent.parent;
2009 }
2010
2011 let rel_attr = node.attr(local_name!("rel"));
2012 let href_attr = node.attr(local_name!("href"));
2013
2014 let (Some(rels), Some(href)) = (rel_attr, href_attr) else {
2015 return;
2016 };
2017 if !rels.split_ascii_whitespace().any(|rel| rel == "stylesheet") {
2018 return;
2019 }
2020
2021 let url = self.doc.resolve_url(href);
2022 let handler = ResourceHandler::new(
2023 self.doc.tx.clone(),
2024 self.doc.id(),
2025 Some(node.id),
2026 self.doc.shell_provider.clone(),
2027 StylesheetHandler {
2028 source_url: url.clone(),
2029 guard: self.doc.guard.clone(),
2030 net_provider: self.doc.net_provider.clone(),
2031 abort_signal: self.doc.abort_signal.clone(),
2032 },
2033 );
2034
2035 if is_in_head && !self.doc.net_provider.is_noop() {
2036 self.doc
2037 .pending_critical_resources
2038 .insert(handler.request_id());
2039 }
2040
2041 self.doc.net_provider.fetch(
2042 self.doc.id(),
2043 self.doc
2044 .build_request(url)
2045 .destination(RequestDestination::Style),
2046 Box::new(handler),
2047 );
2048 }
2049
2050 fn unload_stylesheet(&mut self, node_id: NodeId) {
2051 #[cfg(feature = "shadow-dom")]
2052 let shadow_root = self
2053 .doc
2054 .shadow_sheet_roots
2055 .remove(&node_id)
2056 .or_else(|| self.doc.containing_shadow_root(node_id));
2057 let node = &mut self.doc.nodes[node_id];
2058 let Some(element) = node.element_data_mut() else {
2059 return;
2060 };
2061 let SpecialElementData::Stylesheet(stylesheet) = element.special_data.take() else {
2062 return;
2063 };
2064
2065 self.doc.nodes_to_stylesheet.remove(&node_id);
2066 #[cfg(feature = "shadow-dom")]
2067 if let Some(root_id) = shadow_root {
2068 self.doc.invalidate_shadow_styles(root_id);
2069 return;
2070 }
2071
2072 if !self.doc.document_sheet_nodes.remove(&node_id) {
2073 return;
2074 }
2075 let guard = self.doc.guard.read();
2076 self.doc.stylist.remove_stylesheet(stylesheet, &guard);
2077 self.doc
2078 .stylist
2079 .force_stylesheet_origins_dirty(OriginSet::all());
2080 }
2081
2082 fn load_image(&mut self, target_id: NodeId) {
2083 let node = &self.doc.nodes[target_id];
2084 if let Some(raw_src) = node.attr(local_name!("src")) {
2085 if !raw_src.is_empty() {
2086 let src = self.doc.resolve_url(raw_src);
2087 let src_string = src.as_str();
2088
2089 if let Some(cached_image) = self.doc.image_cache.get(src_string) {
2091 #[cfg(feature = "tracing")]
2092 tracing::info!("Loading image {src_string} from cache");
2093 let node = &mut self.doc.nodes[target_id];
2094 node.element_data_mut().unwrap().special_data =
2095 SpecialElementData::Image(Box::new(cached_image.clone()));
2096 node.cache_mut().clear();
2097 node.insert_damage(ALL_DAMAGE);
2098 return;
2099 }
2100
2101 if let Some(waiting_list) = self.doc.pending_images.get_mut(src_string) {
2103 #[cfg(feature = "tracing")]
2104 tracing::info!("Image {src_string} already pending, queueing node {target_id}");
2105 waiting_list.push((target_id, ImageType::Image));
2106 return;
2107 }
2108
2109 #[cfg(feature = "tracing")]
2111 tracing::info!("Fetching image {src_string}");
2112 self.doc
2113 .pending_images
2114 .insert(src_string.to_string(), vec![(target_id, ImageType::Image)]);
2115
2116 self.doc.net_provider.fetch(
2117 self.doc.id(),
2118 self.doc
2119 .build_request(src)
2120 .destination(RequestDestination::Image),
2121 ResourceHandler::boxed(
2122 self.doc.tx.clone(),
2123 self.doc.id(),
2124 None, self.doc.shell_provider.clone(),
2126 ImageHandler::new(ImageType::Image),
2127 ),
2128 );
2129 }
2130 }
2131 }
2132
2133 fn load_iframe(&mut self, target_id: NodeId) {
2134 if self.doc.subdocument_depth >= crate::iframe::MAX_SUBDOCUMENT_DEPTH {
2135 #[cfg(feature = "tracing")]
2136 tracing::warn!(
2137 "Not loading iframe: max sub-document nesting depth ({}) reached",
2138 crate::iframe::MAX_SUBDOCUMENT_DEPTH
2139 );
2140 return;
2141 }
2142
2143 let node = &self.doc.nodes[target_id];
2144 let Some(element) = node.element_data() else {
2145 return;
2146 };
2147
2148 if let Some(srcdoc) = element.attr(local_name!("srcdoc")) {
2150 let srcdoc = srcdoc.to_string();
2151 self.doc.load_iframe_srcdoc(target_id, &srcdoc);
2152 return;
2153 }
2154
2155 let Some(raw_src) = element.attr(local_name!("src")) else {
2156 return;
2157 };
2158 if raw_src.is_empty() {
2159 return;
2160 }
2161 let Some(url) = self.doc.url.resolve_relative(raw_src) else {
2162 #[cfg(feature = "tracing")]
2163 tracing::warn!("Not loading iframe: could not resolve url {raw_src}");
2164 return;
2165 };
2166 self.doc.start_iframe_load(target_id, url);
2167 }
2168
2169 fn load_custom_paint_src(&mut self, target_id: NodeId) {
2170 let node = &mut self.doc.nodes[target_id];
2171 if let Some(raw_src) = node.attr(local_name!("src")) {
2172 if let Ok(custom_paint_source_id) = raw_src.parse::<u64>() {
2173 self.recompute_is_animating = true;
2174 let canvas_data = SpecialElementData::Canvas(CanvasData {
2175 custom_paint_source_id,
2176 });
2177 node.element_data_mut().unwrap().special_data = canvas_data;
2178 }
2179 }
2180 }
2181
2182 fn process_button_input(&mut self, target_id: NodeId) {
2183 let node = &self.doc.nodes[target_id];
2184 let Some(data) = node.element_data() else {
2185 return;
2186 };
2187
2188 let tagname = data.name.local.as_ref();
2189 let type_attr = data.attr(local_name!("type"));
2190 let value = data.attr(local_name!("value"));
2191
2192 if let ("input", Some("button" | "submit" | "reset"), Some(value)) =
2195 (tagname, type_attr, value)
2196 {
2197 let value = value.to_string();
2198 let id = self.create_text_node(&value);
2199 self.append_children(target_id, &[id]);
2200 #[cfg_attr(not(feature = "file-input"), allow(clippy::needless_return))]
2202 return;
2203 }
2204 #[cfg(feature = "file-input")]
2205 if let ("input", Some("file")) = (tagname, type_attr) {
2206 let button_id = self.create_element(
2207 qual_name!("button", html),
2208 vec![
2209 Attribute {
2210 name: qual_name!("type", html),
2211 value: "button".into(),
2212 },
2213 Attribute {
2214 name: qual_name!("tabindex", html),
2215 value: "-1".into(),
2216 },
2217 ],
2218 );
2219 let label_id = self.create_element(qual_name!("label", html), vec![]);
2220 let text_id = self.create_text_node("No File Selected");
2221 let button_text_id = self.create_text_node("Browse");
2222 self.append_children(target_id, &[button_id, label_id]);
2223 self.append_children(label_id, &[text_id]);
2224 self.append_children(button_id, &[button_text_id]);
2225 }
2226 }
2227}
2228
2229fn set_input_checked_state(element: &mut ElementData, value: String) {
2231 let Ok(checked) = value.parse() else {
2232 return;
2233 };
2234 match element.special_data {
2235 SpecialElementData::CheckboxInput(ref mut checked_mut) => *checked_mut = checked,
2236 SpecialElementData::None => element.attrs.push(Attribute {
2241 name: qual_name!("checked", html),
2242 value: checked.to_string().into(),
2243 }),
2244 _ => {}
2245 }
2246}
2247
2248pub struct ViewportMut<'doc> {
2251 doc: &'doc mut BaseDocument,
2252 initial_viewport: Viewport,
2253}
2254impl ViewportMut<'_> {
2255 pub fn new(doc: &mut BaseDocument) -> ViewportMut<'_> {
2256 let initial_viewport = doc.viewport.clone();
2257 ViewportMut {
2258 doc,
2259 initial_viewport,
2260 }
2261 }
2262}
2263impl Deref for ViewportMut<'_> {
2264 type Target = Viewport;
2265
2266 fn deref(&self) -> &Self::Target {
2267 &self.doc.viewport
2268 }
2269}
2270impl DerefMut for ViewportMut<'_> {
2271 fn deref_mut(&mut self) -> &mut Self::Target {
2272 &mut self.doc.viewport
2273 }
2274}
2275impl Drop for ViewportMut<'_> {
2276 fn drop(&mut self) {
2277 if self.doc.viewport == self.initial_viewport {
2278 return;
2279 }
2280
2281 self.doc.set_stylist_device(make_device(
2282 &self.doc.viewport,
2283 self.doc.media_type.clone(),
2284 self.doc.font_ctx.clone(),
2285 ));
2286 self.doc.scroll_viewport_by(0.0, 0.0); let scale_has_changed =
2289 self.doc.viewport().scale_f64() != self.initial_viewport.scale_f64();
2290 if scale_has_changed {
2291 self.doc.invalidate_inline_contexts();
2292 self.doc.shell_provider.request_redraw();
2293 }
2294 }
2295}
2296
2297fn html_tag(name: &str) -> markup5ever::QualName {
2299 markup5ever::QualName::new(None, markup5ever::ns!(html), name.into())
2300}
2301
2302#[cfg(test)]
2303mod test {
2304 use style::media_queries::MediaType;
2305 use style_dom::ElementState;
2306
2307 use std::sync::{
2308 Arc,
2309 atomic::{AtomicUsize, Ordering},
2310 };
2311
2312 use blitz_traits::shell::{ColorScheme, ShellProvider, Viewport};
2313
2314 use crate::{
2315 Attribute, BaseDocument, DocumentConfig, ElementData, NodeData, NodeId, qual_name,
2316 };
2317
2318 #[test]
2319 fn media_type_defaults_to_screen() {
2320 let mut document = BaseDocument::new(DocumentConfig::default());
2321 assert_eq!(*document.media_type(), MediaType::screen());
2322 assert_eq!(document.stylist_device().media_type(), MediaType::screen());
2323 }
2324
2325 #[test]
2326 fn media_type_honors_config() {
2327 let mut document = BaseDocument::new(DocumentConfig {
2328 media_type: Some(MediaType::print()),
2329 ..Default::default()
2330 });
2331 assert_eq!(*document.media_type(), MediaType::print());
2332 assert_eq!(document.stylist_device().media_type(), MediaType::print());
2333 }
2334
2335 #[test]
2336 fn set_media_type_updates_stylist_device() {
2337 let mut document = BaseDocument::new(DocumentConfig::default());
2338 assert_eq!(document.stylist_device().media_type(), MediaType::screen());
2339
2340 document.set_media_type(MediaType::print());
2341 assert_eq!(*document.media_type(), MediaType::print());
2342 assert_eq!(document.stylist_device().media_type(), MediaType::print());
2343 }
2344
2345 #[test]
2346 fn removing_a_node_forgets_it_as_focused_and_pressed() {
2347 let mut document = BaseDocument::new(DocumentConfig::default());
2353 let button = document.create_node(NodeData::Element(Box::new(ElementData::new(
2354 qual_name!("button"),
2355 Vec::new(),
2356 ))));
2357 let root = document.root_node().id;
2358
2359 let mut mutator = document.mutate();
2360 mutator.append_children(root, &[button]);
2361 drop(mutator);
2362
2363 document.set_focus_to(button);
2364 document.set_mousedown_node_id(Some(button));
2365 assert_eq!(document.get_focussed_node_id(), Some(button));
2366 assert_eq!(document.mousedown_node_id, Some(button));
2367
2368 let mut mutator = document.mutate();
2369 mutator.remove_node(button);
2370 drop(mutator);
2371
2372 assert_eq!(
2373 document.get_focussed_node_id(),
2374 None,
2375 "a removed node must not stay focused"
2376 );
2377 assert_eq!(
2378 document.mousedown_node_id, None,
2379 "a removed node must not stay the pressed node"
2380 );
2381 }
2382
2383 #[test]
2384 fn dropping_a_child_clears_a_hidden_retained_layout_edge() {
2385 let mut document = BaseDocument::new(DocumentConfig {
2386 viewport: Some(Viewport::new(800, 600, 1.0, ColorScheme::Light)),
2387 ..Default::default()
2388 });
2389 let root = document.root_node().id;
2390 let (parent, child) = {
2391 let mut mutator = document.mutate();
2392 let parent = mutator.create_element(qual_name!("div"), vec![]);
2393 let child = mutator.create_element(qual_name!("button"), vec![]);
2394 mutator.set_style_property(parent, "width", "200px");
2395 mutator.set_style_property(parent, "height", "100px");
2396 mutator.append_children(parent, &[child]);
2397 mutator.append_children(root, &[parent]);
2398 (parent, child)
2399 };
2400
2401 document.resolve(0.0);
2402 {
2403 let mut mutator = document.mutate();
2404 mutator.set_style_property(parent, "display", "none");
2405 }
2406 document.resolve(0.0);
2407 assert!(
2408 document.nodes[parent]
2409 .layout_children
2410 .borrow()
2411 .as_ref()
2412 .is_some_and(|children| children.contains(&child)),
2413 "the hidden subtree should retain the layout edge that makes this regression possible"
2414 );
2415
2416 document.mutate().remove_and_drop_node(child);
2417 assert!(document.get_node(child).is_none(), "the child was freed");
2418 assert!(
2419 document.nodes[parent].layout_children.borrow().is_none(),
2420 "the surviving layout parent must not retain the freed key"
2421 );
2422
2423 document.resolve(0.0);
2425 }
2426
2427 #[test]
2428 fn mutator_remove_disabled() {
2429 let mut document = BaseDocument::new(DocumentConfig::default());
2430 let id = document.create_node(NodeData::Element(Box::new(ElementData::new(
2431 qual_name!("button"),
2432 vec![Attribute {
2433 name: qual_name!("disabled"),
2434 value: "".into(),
2435 }],
2436 ))));
2437
2438 let node = document.get_node(id).unwrap();
2439 assert!(
2440 node.element_state().contains(ElementState::DISABLED),
2441 "form node is disabled"
2442 );
2443 assert!(
2444 !node.element_state().contains(ElementState::ENABLED),
2445 "form node is not enabled yet"
2446 );
2447
2448 let mut mutator = document.mutate();
2449 mutator.clear_attribute(id, qual_name!("disabled"));
2450 drop(mutator);
2451
2452 let node = document.get_node(id).unwrap();
2453 assert!(
2454 !node.element_state().contains(ElementState::DISABLED),
2455 "form node is no longer disabled"
2456 );
2457 assert!(
2458 node.element_state().contains(ElementState::ENABLED),
2459 "form node is enabled"
2460 );
2461 }
2462
2463 #[test]
2464 fn mutator_set_disabled() {
2465 let mut document = BaseDocument::new(DocumentConfig::default());
2466 let id = document.create_node(NodeData::Element(Box::new(ElementData::new(
2467 qual_name!("button"),
2468 vec![],
2469 ))));
2470
2471 let node = document.get_node(id).unwrap();
2472 assert!(
2473 !node.element_state().contains(ElementState::DISABLED),
2474 "form node is not disabled"
2475 );
2476 assert!(
2477 node.element_state().contains(ElementState::ENABLED),
2478 "form node is enabled"
2479 );
2480
2481 let mut mutator = document.mutate();
2482 mutator.set_attribute(id, qual_name!("disabled"), "");
2483 drop(mutator);
2484
2485 let node = document.get_node(id).unwrap();
2486
2487 assert!(
2488 node.element_state().contains(ElementState::DISABLED),
2489 "form node is disabled"
2490 );
2491 assert!(
2492 !node.element_state().contains(ElementState::ENABLED),
2493 "form node is no longer enabled enabled"
2494 );
2495 }
2496
2497 #[test]
2498 fn mutator_set_disabled_invalid_node() {
2499 let mut document = BaseDocument::new(DocumentConfig::default());
2500 let id = document.create_node(NodeData::Element(Box::new(ElementData::new(
2501 qual_name!("a"),
2502 vec![],
2503 ))));
2504
2505 let node = document.get_node(id).unwrap();
2506 assert!(
2507 !node.element_state().contains(ElementState::DISABLED),
2508 "form node is not disabled"
2509 );
2510 assert!(
2511 !node.element_state().contains(ElementState::ENABLED),
2512 "form node is enabled"
2513 );
2514
2515 let mut mutator = document.mutate();
2516 mutator.set_attribute(id, qual_name!("disabled"), "");
2517 drop(mutator);
2518
2519 let node = document.get_node(id).unwrap();
2520 assert!(
2521 !node.element_state().contains(ElementState::DISABLED),
2522 "form node is not disabled"
2523 );
2524 assert!(
2525 !node.element_state().contains(ElementState::ENABLED),
2526 "form node is enabled"
2527 );
2528 }
2529
2530 #[test]
2531 fn mutator_id_attribute_updates_id_map() {
2532 let mut document = BaseDocument::new(DocumentConfig::default());
2533 let root_id = document.root_node().id;
2534
2535 let node_id = {
2536 let mut mutator = document.mutate();
2537 let node_id = mutator.create_element(
2538 qual_name!("div"),
2539 vec![Attribute {
2540 name: qual_name!("id"),
2541 value: "old".into(),
2542 }],
2543 );
2544 mutator.append_children(root_id, &[node_id]);
2545 node_id
2546 };
2547 assert_eq!(document.get_element_by_id("old"), Some(node_id));
2548
2549 {
2550 let mut mutator = document.mutate();
2551 mutator.set_attribute(node_id, qual_name!("id"), "new");
2552 }
2553 assert_eq!(document.get_element_by_id("new"), Some(node_id));
2554 assert_eq!(document.get_element_by_id("old"), None);
2555
2556 {
2557 let mut mutator = document.mutate();
2558 mutator.clear_attribute(node_id, qual_name!("id"));
2559 }
2560 assert_eq!(document.get_element_by_id("new"), None);
2561 }
2562
2563 #[test]
2564 fn get_element_by_id_duplicate_ids_first_in_tree_order_wins() {
2565 let mut document = BaseDocument::new(DocumentConfig::default());
2566 let root_id = document.root_node().id;
2567
2568 let (first_id, second_id) = {
2569 let mut mutator = document.mutate();
2570 let first_id = mutator.create_element(qual_name!("div"), vec![]);
2571 let second_id = mutator.create_element(qual_name!("div"), vec![]);
2572 mutator.append_children(root_id, &[first_id, second_id]);
2573 mutator.set_attribute(second_id, qual_name!("id"), "dup");
2576 mutator.set_attribute(first_id, qual_name!("id"), "dup");
2577 (first_id, second_id)
2578 };
2579 assert_eq!(document.get_element_by_id("dup"), Some(first_id));
2580
2581 {
2582 let mut mutator = document.mutate();
2583 mutator.remove_node(first_id);
2584 }
2585 assert_eq!(document.get_element_by_id("dup"), Some(second_id));
2586 }
2587
2588 #[derive(Default)]
2589 struct RedrawShell {
2590 redraw_requests: AtomicUsize,
2591 }
2592
2593 impl ShellProvider for RedrawShell {
2594 fn request_redraw(&self) {
2595 self.redraw_requests.fetch_add(1, Ordering::Relaxed);
2596 }
2597 }
2598
2599 #[test]
2600 fn mutator_requests_redraw_only_after_mutation() {
2601 let shell = Arc::new(RedrawShell::default());
2602 let mut document = BaseDocument::new(DocumentConfig {
2603 shell_provider: Some(shell.clone()),
2604 ..Default::default()
2605 });
2606 let root_id = document.root_node().id;
2607
2608 {
2609 let mut mutator = document.mutate();
2610 let parent_id = mutator.create_element(qual_name!("div"), vec![]);
2611 let child_id = mutator.create_element(qual_name!("span"), vec![]);
2612 mutator.append_children(parent_id, &[child_id]);
2613 mutator.remove_and_drop_all_children(parent_id);
2614 mutator.set_attribute(parent_id, qual_name!("id"), "detached");
2615 }
2616 assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 0);
2617
2618 {
2619 let mutator = document.mutate();
2620 assert_eq!(mutator.child_ids(root_id).len(), 0);
2621 }
2622
2623 {
2624 let mut mutator = document.mutate();
2625 let node_id = mutator.create_element(qual_name!("div"), vec![]);
2626 mutator.append_children(root_id, &[node_id]);
2627 mutator.set_attribute(node_id, qual_name!("id"), "in-document");
2628 }
2629 assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 1);
2630
2631 {
2632 let mut mutator = document.mutate();
2633 let parent_id = mutator.create_element(qual_name!("div"), vec![]);
2634 let child_id = mutator.create_element(qual_name!("span"), vec![]);
2635 mutator.append_children(root_id, &[parent_id]);
2636 mutator.append_children(parent_id, &[child_id]);
2637 mutator.remove_and_drop_all_children(parent_id);
2638 }
2639 assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 2);
2640
2641 {
2642 let mut mutator = document.mutate();
2643 let parent_id = mutator.create_element(qual_name!("div"), vec![]);
2644 let child_id = mutator.create_element(qual_name!("span"), vec![]);
2645 let detached_target_id = mutator.create_element(qual_name!("div"), vec![]);
2646 mutator.append_children(root_id, &[parent_id]);
2647 mutator.append_children(parent_id, &[child_id]);
2648 assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 2);
2649 mutator.append_children(detached_target_id, &[child_id]);
2650 }
2651 assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 3);
2652 }
2653
2654 #[test]
2655 fn moving_subtree_out_of_document_clears_in_document_flag() {
2656 let shell = Arc::new(RedrawShell::default());
2657 let mut document = BaseDocument::new(DocumentConfig {
2658 shell_provider: Some(shell.clone()),
2659 ..Default::default()
2660 });
2661 let root_id = document.root_node().id;
2662 let (child_id, grandchild_id, detached_parent_id) = {
2663 let mut mutator = document.mutate();
2664 let in_document_parent_id = mutator.create_element(qual_name!("div"), vec![]);
2665 let child_id = mutator.create_element(qual_name!("div"), vec![]);
2666 let grandchild_id = mutator.create_element(qual_name!("span"), vec![]);
2667 let detached_parent_id = mutator.create_element(qual_name!("section"), vec![]);
2668 mutator.append_children(root_id, &[in_document_parent_id]);
2669 mutator.append_children(in_document_parent_id, &[child_id]);
2670 mutator.append_children(child_id, &[grandchild_id]);
2671 (child_id, grandchild_id, detached_parent_id)
2672 };
2673 assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 1);
2674 assert!(document.get_node(child_id).unwrap().flags.is_in_document());
2675 assert!(
2676 document
2677 .get_node(grandchild_id)
2678 .unwrap()
2679 .flags
2680 .is_in_document()
2681 );
2682
2683 {
2684 let mut mutator = document.mutate();
2685 mutator.append_children(detached_parent_id, &[child_id]);
2686 }
2687 assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 2);
2688 assert!(!document.get_node(child_id).unwrap().flags.is_in_document());
2689 assert!(
2690 !document
2691 .get_node(grandchild_id)
2692 .unwrap()
2693 .flags
2694 .is_in_document()
2695 );
2696
2697 {
2698 let mut mutator = document.mutate();
2699 mutator.set_attribute(child_id, qual_name!("id"), "detached");
2700 }
2701 assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 2);
2702
2703 {
2704 let mut mutator = document.mutate();
2705 mutator.append_children(root_id, &[child_id]);
2706 }
2707 assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 3);
2708 assert!(document.get_node(child_id).unwrap().flags.is_in_document());
2709 assert!(
2710 document
2711 .get_node(grandchild_id)
2712 .unwrap()
2713 .flags
2714 .is_in_document()
2715 );
2716 }
2717
2718 #[test]
2730 fn a_paint_only_restyle_refreshes_the_calc_the_taffy_style_points_at() {
2731 use style::servo_arc::Arc as ServoArc;
2732
2733 let mut document = BaseDocument::new(DocumentConfig {
2734 viewport: Some(Viewport::new(800, 600, 1.0, ColorScheme::Light)),
2735 ..Default::default()
2736 });
2737 let root_id = document.root_node().id;
2738
2739 let (header_id, chip_id) = {
2740 let mut mutator = document.mutate();
2741 let header_id = mutator.create_element(qual_name!("div"), vec![]);
2742 let chip_id = mutator.create_element(qual_name!("span"), vec![]);
2743 mutator.set_style_property(header_id, "position", "relative");
2744 mutator.set_style_property(header_id, "width", "800px");
2745 mutator.set_style_property(header_id, "height", "60px");
2746 mutator.set_style_property(chip_id, "position", "absolute");
2747 mutator.set_style_property(chip_id, "max-width", "calc(100% - 24px)");
2748 mutator.set_style_property(chip_id, "color", "rgb(1, 2, 3)");
2749 mutator.append_children(header_id, &[chip_id]);
2750 mutator.append_children(root_id, &[header_id]);
2751 (header_id, chip_id)
2752 };
2753
2754 document.resolve(0.0);
2755
2756 {
2762 let mut mutator = document.mutate();
2763 mutator.set_style_property(header_id, "color", "rgb(4, 5, 6)");
2764 }
2765 document.resolve(0.0);
2766
2767 let node = document.get_node(chip_id).unwrap();
2768 let stylo_data = node.stylo_element_data_opt().and_then(|data| data.get());
2769 let primary = stylo_data
2770 .as_ref()
2771 .and_then(|data| data.styles.get_primary())
2772 .expect("the chip is styled");
2773 let source = node
2774 .style_source_opt()
2775 .expect("a styled node records the computed values its taffy style was built from");
2776
2777 assert!(
2778 ServoArc::ptr_eq(primary, source),
2779 "the cached taffy style still points into computed values that a restyle replaced, \
2780 so every calc() in it is a dangling pointer",
2781 );
2782 }
2783
2784 #[test]
2785 fn style_property_updates_nested_layout() {
2786 let mut document = BaseDocument::new(DocumentConfig {
2787 viewport: Some(Viewport::new(800, 600, 1.0, ColorScheme::Light)),
2788 ..Default::default()
2789 });
2790 let root_id = document.root_node().id;
2791
2792 let mover_id = {
2793 let mut mutator = document.mutate();
2794 let parent_id = mutator.create_element(qual_name!("div"), vec![]);
2795 let mover_id = mutator.create_element(qual_name!("div"), vec![]);
2796 mutator.set_style_property(parent_id, "position", "relative");
2797 mutator.set_style_property(parent_id, "width", "800px");
2798 mutator.set_style_property(parent_id, "height", "600px");
2799 mutator.set_style_property(mover_id, "position", "absolute");
2800 mutator.set_style_property(mover_id, "left", "0px");
2801 mutator.set_style_property(mover_id, "top", "0px");
2802 mutator.append_children(parent_id, &[mover_id]);
2803 mutator.append_children(root_id, &[parent_id]);
2804 mover_id
2805 };
2806
2807 document.resolve(0.0);
2808 assert_eq!(
2809 document
2810 .get_node(mover_id)
2811 .unwrap()
2812 .final_layout()
2813 .location
2814 .x,
2815 0.0
2816 );
2817
2818 {
2819 let mut mutator = document.mutate();
2820 mutator.set_style_property(mover_id, "left", "120px");
2821 }
2822
2823 document.resolve(0.0);
2824 assert_eq!(
2825 document
2826 .get_node(mover_id)
2827 .unwrap()
2828 .final_layout()
2829 .location
2830 .x,
2831 120.0
2832 );
2833 }
2834
2835 fn doc_with_a_comment() -> (BaseDocument, NodeId, NodeId, NodeId) {
2838 let mut doc = BaseDocument::new(DocumentConfig {
2839 viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
2840 ..Default::default()
2841 });
2842 let root_id = doc.root_node().id;
2843
2844 let mut mutr = doc.mutate();
2845 let html = mutr.create_element(qual_name!("html"), vec![]);
2846 let body = mutr.create_element(qual_name!("body"), vec![]);
2847 let container = mutr.create_element(qual_name!("div"), vec![]);
2848 let text = mutr.create_text_node("text");
2849 let comment = mutr.create_comment_node("comment");
2850 mutr.append_children(container, &[text, comment]);
2851 mutr.append_children(body, &[container]);
2852 mutr.append_children(html, &[body]);
2853 mutr.append_children(root_id, &[html]);
2854 drop(mutr);
2855
2856 doc.resolve(0.0);
2857 (doc, container, text, comment)
2858 }
2859
2860 #[test]
2864 fn setting_a_comments_data_writes_the_contents() {
2865 let (mut doc, _container, _text, comment) = doc_with_a_comment();
2866
2867 doc.mutate().set_node_text(comment, "rewritten");
2868
2869 let NodeData::Comment { contents } = &doc.get_node(comment).unwrap().data else {
2870 panic!("expected a comment node");
2871 };
2872 assert_eq!(contents, "rewritten");
2873 }
2874
2875 #[test]
2882 fn setting_a_comments_data_does_not_dirty_layout() {
2883 let (mut doc, container, text, comment) = doc_with_a_comment();
2884
2885 let container_damage_before = doc.get_node(container).unwrap().damage();
2886 let comment_damage_before = doc.get_node(comment).unwrap().damage();
2887
2888 doc.mutate().set_node_text(comment, "rewritten");
2889
2890 assert_eq!(
2891 doc.get_node(comment).unwrap().damage(),
2892 comment_damage_before,
2893 "writing a comment's data damaged the comment"
2894 );
2895 assert_eq!(
2896 doc.get_node(container).unwrap().damage(),
2897 container_damage_before,
2898 "writing a comment's data damaged its parent, scheduling a relayout \
2899 for a change that cannot affect a pixel"
2900 );
2901
2902 doc.mutate().set_node_text(text, "rewritten");
2903 assert_ne!(
2904 doc.get_node(container).unwrap().damage(),
2905 container_damage_before,
2906 "a text write should damage the parent, so the assertions above can fail"
2907 );
2908 }
2909
2910 #[test]
2913 fn rewriting_a_comment_with_its_own_contents_is_inert() {
2914 let (mut doc, container, _text, comment) = doc_with_a_comment();
2915 let container_damage_before = doc.get_node(container).unwrap().damage();
2916
2917 doc.mutate().set_node_text(comment, "comment");
2918
2919 let NodeData::Comment { contents } = &doc.get_node(comment).unwrap().data else {
2920 panic!("expected a comment node");
2921 };
2922 assert_eq!(contents, "comment");
2923 assert_eq!(
2924 doc.get_node(container).unwrap().damage(),
2925 container_damage_before
2926 );
2927 }
2928}