1use blitz_traits::node_id::NodeId;
2use std::collections::HashSet;
3use std::mem;
4use std::ops::{Deref, DerefMut};
5
6use crate::document::make_device;
7use crate::layout::damage::ALL_DAMAGE;
8use crate::net::{ImageHandler, ResourceHandler, StylesheetHandler};
9use crate::node::{CanvasData, NodeFlags, SpecialElementData};
10use crate::util::ImageType;
11use crate::{
12 Attribute, BaseDocument, Document, ElementData, Node, NodeData, QualName, local_name, qual_name,
13};
14use blitz_traits::shell::Viewport;
15use markup5ever::ns;
16use selectors::matching::ElementSelectorFlags;
17use style::Atom;
18use style::invalidation::element::restyle_hints::RestyleHint;
19use style::stylesheets::OriginSet;
20use thin_vec::ThinVec;
21
22macro_rules! tag_and_attr {
23 ($tag:tt, $attr:tt) => {
24 (&local_name!($tag), &local_name!($attr))
25 };
26}
27
28#[derive(Debug, Clone)]
29pub enum AppendTextErr {
30 NotTextNode,
32}
33
34enum SpecialOp {
37 LoadImage(NodeId),
38 LoadIframe(NodeId),
39 LoadStylesheet(NodeId),
40 UnloadStylesheet(NodeId),
41 LoadCustomPaintSource(NodeId),
42 ProcessButtonInput(NodeId),
43 UnloadSubDocument(NodeId),
44 #[cfg(feature = "custom-widget")]
45 UnloadCustomWidget(NodeId),
46 #[cfg(feature = "shadow-dom")]
47 UpgradeCustomElement(NodeId),
48 #[cfg(feature = "shadow-dom")]
49 DisconnectCustomElement(NodeId),
50}
51
52pub struct DocumentMutator<'doc> {
53 pub doc: &'doc mut BaseDocument,
56
57 eager_op_queue: Vec<SpecialOp>,
58
59 title_node: Option<NodeId>,
61 style_nodes: HashSet<NodeId>,
62 form_nodes: HashSet<NodeId>,
63
64 recompute_is_animating: bool,
66
67 mutations_occurred: bool,
69
70 #[cfg(feature = "shadow-dom")]
73 custom_element_attr_changes: Vec<(NodeId, QualName, Option<String>, Option<String>)>,
74
75 #[cfg(feature = "autofocus")]
77 node_to_autofocus: Option<NodeId>,
78}
79
80impl Drop for DocumentMutator<'_> {
81 fn drop(&mut self) {
82 self.flush(); if self.mutations_occurred {
84 self.doc.shell_provider.request_redraw();
85 }
86 }
87}
88
89impl DocumentMutator<'_> {
90 pub fn new<'doc>(doc: &'doc mut BaseDocument) -> DocumentMutator<'doc> {
91 DocumentMutator {
92 doc,
93 eager_op_queue: Vec::new(),
94 title_node: None,
95 style_nodes: HashSet::new(),
96 form_nodes: HashSet::new(),
97 recompute_is_animating: false,
98 mutations_occurred: false,
99 #[cfg(feature = "shadow-dom")]
100 custom_element_attr_changes: Vec::new(),
101 #[cfg(feature = "autofocus")]
102 node_to_autofocus: None,
103 }
104 }
105
106 pub fn node_has_parent(&self, node_id: NodeId) -> bool {
109 self.doc.nodes[node_id].parent.is_some()
110 }
111
112 pub fn previous_sibling_id(&self, node_id: NodeId) -> Option<NodeId> {
113 self.doc.nodes[node_id].backward(1).map(|node| node.id)
114 }
115
116 pub fn next_sibling_id(&self, node_id: NodeId) -> Option<NodeId> {
117 self.doc.nodes[node_id].forward(1).map(|node| node.id)
118 }
119
120 pub fn parent_id(&self, node_id: NodeId) -> Option<NodeId> {
121 self.doc.nodes[node_id].parent
122 }
123
124 pub fn last_child_id(&self, node_id: NodeId) -> Option<NodeId> {
125 self.doc.nodes[node_id].children.last().copied()
126 }
127
128 pub fn child_ids(&self, node_id: NodeId) -> ThinVec<NodeId> {
129 self.doc.nodes[node_id].children.clone()
130 }
131
132 pub fn element_name(&self, node_id: NodeId) -> Option<&QualName> {
133 self.doc.nodes[node_id].element_data().map(|el| &el.name)
134 }
135
136 pub fn node_at_path(&self, start_node_id: NodeId, path: &[u8]) -> NodeId {
137 let mut current = &self.doc.nodes[start_node_id];
138 for i in path {
139 let new_id = current.children[*i as usize];
140 current = &self.doc.nodes[new_id];
141 }
142 current.id
143 }
144
145 pub fn create_comment_node(&mut self, contents: &str) -> NodeId {
148 self.doc.create_node(NodeData::Comment {
149 contents: contents.to_string(),
150 })
151 }
152
153 pub fn create_text_node(&mut self, text: &str) -> NodeId {
154 self.doc.create_text_node(text)
155 }
156
157 pub fn create_document_fragment(&mut self) -> NodeId {
166 self.doc.create_node(NodeData::DocumentFragment)
167 }
168
169 pub fn create_html_document(&mut self, title: &str) -> NodeId {
182 let doc_id = self.doc.create_node(NodeData::Document(Box::default()));
183
184 let html = self.create_element(html_tag("html"), Vec::new());
185 let head = self.create_element(html_tag("head"), Vec::new());
186 let title_el = self.create_element(html_tag("title"), Vec::new());
187 let body = self.create_element(html_tag("body"), Vec::new());
188
189 if !title.is_empty() {
192 let text = self.create_text_node(title);
193 self.append_children(title_el, &[text]);
194 }
195
196 self.append_children(head, &[title_el]);
197 self.append_children(html, &[head, body]);
198 self.append_children(doc_id, &[html]);
199
200 doc_id
201 }
202
203 pub fn create_element(&mut self, name: QualName, attrs: Vec<Attribute>) -> NodeId {
204 let mut data = ElementData::new(name, attrs);
205 data.flush_style_attribute(self.doc.guard(), &self.doc.url.url_extra_data());
206
207 let id = self.doc.create_node(NodeData::Element(Box::new(data)));
208 let node = self.doc.get_node_mut(id).unwrap();
209
210 *node.stylo_element_data_mut().ensure_init_mut() = style::data::ElementData {
212 damage: ALL_DAMAGE,
213 ..Default::default()
214 };
215
216 id
217 }
218
219 pub fn deep_clone_node(&mut self, node_id: NodeId) -> NodeId {
220 self.doc.deep_clone_node(node_id)
221 }
222
223 pub fn set_node_text(&mut self, node_id: NodeId, value: &str) {
226 let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
227 if node_is_in_document && self.doc.is_recording_mutations() {
228 let old_value = match &self.doc.nodes[node_id].data {
229 NodeData::Comment { contents } => Some(contents.clone()),
230 NodeData::Text(text) => Some(text.content.clone()),
231 _ => None,
232 };
233 if let Some(old_value) = old_value.filter(|old| old != value) {
234 self.doc.record_mutation(crate::DomMutation::CharacterData {
235 target: node_id,
236 old_value,
237 });
238 }
239 }
240 let node = &mut self.doc.nodes[node_id];
241
242 if let NodeData::Comment { ref mut contents } = node.data {
253 if contents != value {
254 contents.clear();
255 contents.push_str(value);
256 }
257 return;
258 }
259
260 let text = match node.data {
261 NodeData::Text(ref mut text) => text,
262 _ => return,
264 };
265
266 let changed = text.content != value;
267 if changed {
268 self.mutations_occurred |= node_is_in_document;
269 text.content.clear();
270 text.content.push_str(value);
271 node.insert_damage(ALL_DAMAGE);
272 node.mark_ancestors_dirty();
275 let parent_id = node.parent;
276
277 if let Some(parent_id) = parent_id {
280 let parent = &mut self.doc.nodes[parent_id];
281 parent.insert_damage(ALL_DAMAGE);
282 }
283
284 self.maybe_record_node(parent_id);
285 }
286 }
287
288 pub fn append_text_to_node(
289 &mut self,
290 node_id: NodeId,
291 text: &str,
292 ) -> Result<(), AppendTextErr> {
293 let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
294 let node = &mut self.doc.nodes[node_id];
295 node.insert_damage(ALL_DAMAGE);
296 node.mark_ancestors_dirty();
297 match node.text_data_mut() {
298 Some(data) => {
299 data.content += text;
300 self.mutations_occurred |= node_is_in_document;
301 Ok(())
302 }
303 None => Err(AppendTextErr::NotTextNode),
304 }
305 }
306
307 pub fn add_attrs_if_missing(&mut self, node_id: NodeId, attrs: Vec<Attribute>) {
308 let node = &mut self.doc.nodes[node_id];
309 node.insert_damage(ALL_DAMAGE);
310 let element_data = node.element_data_mut().expect("Not an element");
311
312 let existing_names = element_data
313 .attrs
314 .iter()
315 .map(|e| e.name.clone())
316 .collect::<HashSet<_>>();
317
318 for attr in attrs
319 .into_iter()
320 .filter(|attr| !existing_names.contains(&attr.name))
321 {
322 self.set_attribute(node_id, attr.name, &attr.value);
323 }
324 }
325
326 pub fn set_attribute(&mut self, node_id: NodeId, name: QualName, value: &str) {
327 let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
328 if node_is_in_document && self.doc.is_recording_mutations() {
329 if let Some(element) = self.doc.nodes[node_id].element_data() {
333 let old_value = element.attr(name.local.clone()).map(str::to_owned);
334 self.doc.record_mutation(crate::DomMutation::Attributes {
335 target: node_id,
336 name: name.local.to_string(),
337 old_value,
338 });
339 }
340 }
341 if node_is_in_document {
342 self.doc.snapshot_node(node_id);
343
344 let renders_from_attributes = self.doc.nodes[node_id]
360 .data
361 .downcast_element()
362 .is_some_and(|el| {
363 el.name.ns == ns!(svg)
364 || crate::layout::replaced::is_replaced_element(&el.name.local)
365 });
366
367 let node = &mut self.doc.nodes[node_id];
368 if let Some(mut data) = node.stylo_element_data_opt_mut().and_then(|s| s.get_mut()) {
369 data.hint |= RestyleHint::restyle_subtree();
370 if renders_from_attributes {
371 data.damage.insert(ALL_DAMAGE);
372 }
373 }
374
375 let parent = node.parent;
392 if let Some(parent_id) = parent {
393 let parent = &self.doc.nodes[parent_id];
394 let flags = parent.selector_flags().get();
395 let child_dependent = ElementSelectorFlags::HAS_SLOW_SELECTOR
396 | ElementSelectorFlags::HAS_SLOW_SELECTOR_LATER_SIBLINGS
397 | ElementSelectorFlags::HAS_EDGE_CHILD_SELECTOR
398 | ElementSelectorFlags::HAS_EMPTY_SELECTOR
399 | ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR
400 | ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_SIBLING
401 | ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR_SIBLING;
402
403 if flags.intersects(child_dependent) {
404 let parent = &mut self.doc.nodes[parent_id];
405 if let Some(mut data) = parent
406 .stylo_element_data_opt_mut()
407 .and_then(|s| s.get_mut())
408 {
409 data.hint |= RestyleHint::restyle_subtree();
410 }
411 }
412 }
413
414 self.doc.nodes[node_id].mark_ancestors_dirty();
418 }
419
420 if name.local == local_name!("id") && node_is_in_document {
421 if let Some(old_id) = self.doc.nodes[node_id]
422 .element_data()
423 .map(|element| element.id.clone())
424 {
425 if let Some(old_id) = old_id {
426 self.doc.remove_from_id_map(&old_id, node_id);
427 }
428 self.doc.add_to_id_map(value, node_id);
429 }
430 }
431
432 let node = &mut self.doc.nodes[node_id];
433
434 let NodeData::Element(ref mut element) = node.data else {
435 return;
436 };
437
438 self.mutations_occurred |= node_is_in_document;
439 #[cfg(feature = "custom-widget")]
441 if let SpecialElementData::CustomWidget(widget_data) = &mut element.special_data {
442 let old_value = element.attrs.get(&name).as_ref().map(|attr| &*attr.value);
443 widget_data
444 .widget
445 .attribute_changed(&name.local, old_value, Some(value));
446 }
447
448 #[cfg(feature = "shadow-dom")]
451 if element.custom_element_data().is_some() {
452 let old_value = element
453 .attrs
454 .get(&name)
455 .as_ref()
456 .map(|attr| attr.value.to_string());
457 self.custom_element_attr_changes.push((
458 node_id,
459 name.clone(),
460 old_value,
461 Some(value.to_string()),
462 ));
463 }
464
465 element.attrs.set(name.clone(), value);
466
467 if name.local == local_name!("tabindex")
472 || name.local == local_name!("href")
473 || name.local == local_name!("disabled")
474 {
475 element.flush_is_focussable();
476 }
477
478 let tag = &element.name.local;
479 let attr = &name.local;
480
481 if *attr == local_name!("id") {
482 element.id = Some(Atom::from(value))
483 }
484
485 if *attr == local_name!("value") {
486 if let Some(input_data) = element.text_input_data_mut() {
487 input_data.set_text(
489 &mut self.doc.font_ctx.lock().unwrap(),
490 &mut self.doc.layout_ctx,
491 value,
492 );
493 }
494 return;
495 }
496
497 if *attr == local_name!("style") {
498 element.flush_style_attribute(&self.doc.guard, &self.doc.url.url_extra_data());
499 node.mark_style_attr_updated();
500 return;
501 }
502
503 if *attr == local_name!("disabled") && element.can_be_disabled() {
504 node.disable();
505 return;
506 }
507
508 if !node.flags.is_in_document() {
511 return;
512 }
513
514 if (tag, attr) == tag_and_attr!("input", "checked") {
515 set_input_checked_state(element, value.to_string());
516 } else if (tag, attr) == tag_and_attr!("img", "src") {
517 self.load_image(node_id);
518 } else if (tag, attr) == tag_and_attr!("canvas", "src") {
519 self.load_custom_paint_src(node_id);
520 } else if (tag, attr) == tag_and_attr!("link", "href") {
521 self.load_linked_stylesheet(node_id);
522 } else if (tag, attr) == tag_and_attr!("iframe", "src")
523 || (tag, attr) == tag_and_attr!("iframe", "srcdoc")
524 {
525 self.load_iframe(node_id);
526 } else if (tag, attr) == tag_and_attr!("option", "selected") {
527 self.set_option_selected_state(node_id, true);
538 }
539 }
540
541 fn set_option_selected_state(&mut self, option_id: NodeId, selected: bool) {
544 let Some(select_id) = self.doc.option_owner_select(option_id) else {
545 return;
546 };
547 let Some(index) = self
548 .doc
549 .select_options(select_id)
550 .iter()
551 .position(|id| *id == option_id)
552 else {
553 return;
554 };
555 let changed = if selected {
556 self.doc.set_select_selected_index(select_id, index)
557 } else {
558 self.doc
559 .get_node_mut(select_id)
560 .and_then(|node| node.data.downcast_element_mut())
561 .and_then(|el| el.select_data_mut())
562 .is_some_and(|data| data.set_selected(index, false))
563 };
564 if changed {
565 self.doc.snapshot_node(option_id);
568 self.doc.snapshot_node(select_id);
569 }
570 }
571
572 pub fn clear_attribute(&mut self, node_id: NodeId, name: QualName) {
573 let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
574 if node_is_in_document && self.doc.is_recording_mutations() {
575 let old_value = self.doc.nodes[node_id]
578 .element_data()
579 .and_then(|element| element.attr(name.local.clone()))
580 .map(str::to_owned);
581 if old_value.is_some() {
582 self.doc.record_mutation(crate::DomMutation::Attributes {
583 target: node_id,
584 name: name.local.to_string(),
585 old_value,
586 });
587 }
588 }
589 if node_is_in_document {
590 self.doc.snapshot_node(node_id);
591
592 let node = &mut self.doc.nodes[node_id];
593
594 if let Some(mut data) = node.stylo_element_data_opt_mut().and_then(|s| s.get_mut()) {
595 data.hint |= RestyleHint::restyle_subtree();
596 data.damage.insert(ALL_DAMAGE);
597 }
598
599 node.mark_ancestors_dirty();
602 }
603
604 if name.local == local_name!("id") && node_is_in_document {
605 if let Some(old_id) = self.doc.nodes[node_id]
606 .element_data()
607 .and_then(|element| element.id.clone())
608 {
609 self.doc.remove_from_id_map(&old_id, node_id);
610 }
611 }
612
613 let node = &mut self.doc.nodes[node_id];
614
615 let Some(element) = node.element_data_mut() else {
616 return;
617 };
618
619 let removed_attr = element.attrs.remove(&name);
620 let had_attr = removed_attr.is_some();
621 if !had_attr {
622 return;
623 }
624 self.mutations_occurred |= node_is_in_document;
625
626 #[cfg(feature = "custom-widget")]
628 if let SpecialElementData::CustomWidget(widget_data) = &mut element.special_data {
629 let old_value = removed_attr.as_ref().map(|attr| &*attr.value);
630 widget_data
631 .widget
632 .attribute_changed(&name.local, old_value, None);
633 }
634
635 #[cfg(feature = "shadow-dom")]
637 if element.custom_element_data().is_some() {
638 let old_value = removed_attr.as_ref().map(|attr| attr.value.to_string());
639 self.custom_element_attr_changes
640 .push((node_id, name.clone(), old_value, None));
641 }
642
643 if name.local == local_name!("id") {
644 element.id = None;
645 }
646
647 if name.local == local_name!("tabindex")
650 || name.local == local_name!("href")
651 || name.local == local_name!("disabled")
652 {
653 element.flush_is_focussable();
654 }
655
656 if name.local == local_name!("value") {
658 if let Some(input_data) = element.text_input_data_mut() {
659 input_data.set_text(
660 &mut self.doc.font_ctx.lock().unwrap(),
661 &mut self.doc.layout_ctx,
662 "",
663 );
664 }
665 }
666
667 let tag = &element.name.local;
668 let attr = &name.local;
669
670 if *attr == local_name!("disabled") && element.can_be_disabled() {
671 node.enable();
672 return;
673 }
674
675 if *attr == local_name!("style") {
676 element.flush_style_attribute(&self.doc.guard, &self.doc.url.url_extra_data());
677 node.mark_style_attr_updated();
678 } else if (tag, attr) == tag_and_attr!("canvas", "src") {
679 self.recompute_is_animating = true;
680 } else if (tag, attr) == tag_and_attr!("link", "href") {
681 self.unload_stylesheet(node_id);
682 } else if (tag, attr) == tag_and_attr!("iframe", "srcdoc") && node_is_in_document {
683 self.load_iframe(node_id);
685 }
686 }
687
688 pub fn set_style_property(&mut self, node_id: NodeId, name: &str, value: &str) {
689 let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
690 self.doc.set_style_property(node_id, name, value);
691 self.mutations_occurred |= node_is_in_document;
692 }
693
694 pub fn remove_style_property(&mut self, node_id: NodeId, name: &str) {
695 let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
696 self.doc.remove_style_property(node_id, name);
697 self.mutations_occurred |= node_is_in_document;
698 }
699
700 pub fn set_sub_document(&mut self, node_id: NodeId, sub_document: Box<dyn Document>) {
701 let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
702 self.doc.set_sub_document(node_id, sub_document);
703 self.mutations_occurred |= node_is_in_document;
704 }
705
706 pub fn remove_sub_document(&mut self, node_id: NodeId) {
707 let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
708 self.doc.remove_sub_document(node_id);
709 self.mutations_occurred |= node_is_in_document;
710 }
711
712 #[cfg(feature = "custom-widget")]
713 pub fn set_custom_widget(&mut self, node_id: NodeId, widget: Box<dyn crate::Widget>) {
714 let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
715 self.doc.set_custom_widget(node_id, widget);
716 self.mutations_occurred |= node_is_in_document;
717 }
718
719 #[cfg(feature = "custom-widget")]
720 pub fn remove_custom_widget(&mut self, node_id: NodeId) {
721 let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
722 self.doc.remove_custom_widget(node_id);
723 self.mutations_occurred |= node_is_in_document;
724 }
725
726 #[cfg(feature = "shadow-dom")]
729 pub fn attach_shadow(&mut self, host_id: NodeId, mode: crate::node::ShadowRootMode) -> NodeId {
730 self.doc.attach_shadow(host_id, mode)
731 }
732
733 #[cfg(feature = "shadow-dom")]
736 pub fn set_custom_element(
737 &mut self,
738 node_id: NodeId,
739 controller: Box<dyn crate::node::CustomElement>,
740 ) {
741 self.doc.set_custom_element(node_id, controller);
742 self.upgrade_custom_element(node_id);
743 }
744
745 #[cfg(feature = "shadow-dom")]
748 pub fn remove_custom_element(&mut self, node_id: NodeId) {
749 self.disconnect_custom_element(node_id);
750 let _ = self.doc.take_custom_element(node_id);
751 }
752
753 #[cfg(feature = "shadow-dom")]
758 pub(crate) fn upgrade_custom_element(&mut self, node_id: NodeId) {
759 use crate::node::{CustomElementData, ShadowRootMode, SpecialElementData};
760
761 let Some(node) = self.doc.get_node(node_id) else {
762 return;
763 };
764 let Some(element) = node.element_data() else {
765 return;
766 };
767
768 let already_has_controller =
771 matches!(element.special_data, SpecialElementData::CustomElement(_));
772
773 let mode = if already_has_controller {
774 ShadowRootMode::Open
776 } else {
777 let tag = element.name.local.clone();
778 let Some(definition) = self.doc.custom_element_registry.get(&tag) else {
779 return;
780 };
781 let mode = definition.mode;
782 let controller = (definition.factory)();
783 self.doc.nodes[node_id]
784 .element_data_mut()
785 .unwrap()
786 .special_data =
787 SpecialElementData::CustomElement(CustomElementData::new(controller));
788 self.doc.custom_element_nodes.insert(node_id);
789 mode
790 };
791
792 let is_upgraded = self.doc.nodes[node_id]
794 .element_data()
795 .and_then(|el| el.custom_element_data())
796 .map(|data| data.upgraded)
797 .unwrap_or(true);
798 if is_upgraded {
799 return;
800 }
801
802 let shadow_root_id = self.doc.attach_shadow(node_id, mode);
804
805 let Some(mut controller) = self.take_controller(node_id) else {
807 return;
808 };
809
810 {
811 let mut ctx = crate::node::CustomElementCtx {
812 mutator: self,
813 host_id: node_id,
814 shadow_root_id,
815 };
816 controller.connected(&mut ctx);
817 }
818
819 self.restore_controller(node_id, controller, true);
820 }
821
822 #[cfg(feature = "shadow-dom")]
824 pub(crate) fn disconnect_custom_element(&mut self, node_id: NodeId) {
825 let Some(shadow_root_id) = self
826 .doc
827 .get_node(node_id)
828 .and_then(|node| node.shadow_root_id())
829 else {
830 if let Some(mut controller) = self.take_controller(node_id) {
832 {
835 let mut ctx = crate::node::CustomElementCtx {
836 mutator: self,
837 host_id: node_id,
838 shadow_root_id: node_id,
839 };
840 controller.disconnected(&mut ctx);
841 }
842 self.restore_controller(node_id, controller, false);
843 }
844 return;
845 };
846
847 if let Some(mut controller) = self.take_controller(node_id) {
848 {
849 let mut ctx = crate::node::CustomElementCtx {
850 mutator: self,
851 host_id: node_id,
852 shadow_root_id,
853 };
854 controller.disconnected(&mut ctx);
855 }
856 self.restore_controller(node_id, controller, false);
857 }
858 }
859
860 #[cfg(feature = "shadow-dom")]
863 fn take_controller(&mut self, node_id: NodeId) -> Option<Box<dyn crate::node::CustomElement>> {
864 self.doc
865 .nodes
866 .get_mut(node_id)?
867 .element_data_mut()?
868 .custom_element_data_mut()?
869 .controller
870 .take()
871 }
872
873 #[cfg(feature = "shadow-dom")]
876 fn restore_controller(
877 &mut self,
878 node_id: NodeId,
879 controller: Box<dyn crate::node::CustomElement>,
880 upgraded: bool,
881 ) {
882 if let Some(data) = self
883 .doc
884 .nodes
885 .get_mut(node_id)
886 .and_then(|node| node.element_data_mut())
887 .and_then(|el| el.custom_element_data_mut())
888 {
889 data.controller = Some(controller);
890 if upgraded {
891 data.upgraded = true;
892 }
893 }
894 }
895
896 fn invalidate_layout_parent_edge(&mut self, node_id: NodeId) {
905 let Some(layout_parent_id) = self
906 .doc
907 .nodes
908 .get(node_id)
909 .and_then(|node| node.layout_parent.get())
910 else {
911 return;
912 };
913 if let Some(layout_parent) = self.doc.nodes.get_mut(layout_parent_id) {
914 layout_parent.layout_children.get_mut().take();
915 layout_parent.paint_children.get_mut().take();
916 layout_parent.insert_damage(ALL_DAMAGE);
917 }
918 if let Some(node) = self.doc.nodes.get(node_id) {
919 node.layout_parent.set(None);
920 }
921 }
922
923 fn clear_layout_of_subtree(doc: &mut BaseDocument, node_id: NodeId) {
925 let mut stack = vec![node_id];
926 while let Some(id) = stack.pop() {
927 let Some(node) = doc.nodes.get_mut(id) else {
928 continue;
929 };
930 if node.data.downcast_element().is_some() {
934 *node.unrounded_layout_mut() = taffy::Layout::with_order(0);
935 *node.final_layout_mut() = taffy::Layout::with_order(0);
936 node.cache_mut().clear();
937 }
938 stack.extend(node.children.iter().copied());
939 }
940 }
941
942 fn record_removal(&mut self, node_id: NodeId) {
946 if !self.doc.is_recording_mutations() || !self.doc.nodes[node_id].flags.is_in_document() {
947 return;
948 }
949 if let Some((parent_id, previous_sibling, next_sibling)) = self.doc.sibling_context(node_id)
950 {
951 self.doc.record_mutation(crate::DomMutation::ChildList {
952 target: parent_id,
953 added: Vec::new(),
954 removed: vec![node_id],
955 previous_sibling,
956 next_sibling,
957 });
958 }
959 }
960
961 pub fn remove_node(&mut self, node_id: NodeId) {
962 self.record_removal(node_id);
963 let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
964 self.process_removed_subtree(node_id);
968
969 Self::clear_layout_of_subtree(self.doc, node_id);
977 self.invalidate_layout_parent_edge(node_id);
978
979 let node = &mut self.doc.nodes[node_id];
980
981 if let Some(parent_id) = node.parent.take() {
983 self.mutations_occurred |= node_is_in_document;
984 let parent = &mut self.doc.nodes[parent_id];
985 parent.insert_damage(ALL_DAMAGE);
986 parent.mark_ancestors_dirty();
988 parent.children.retain(|id| *id != node_id);
989 self.maybe_record_node(parent_id);
990 }
991 }
992
993 pub fn remove_and_drop_node(&mut self, node_id: NodeId) -> Option<Node> {
994 self.remove_and_drop_node_with(node_id, &mut |_| {})
995 }
996
997 pub fn remove_and_drop_node_with(
1000 &mut self,
1001 node_id: NodeId,
1002 on_drop: &mut dyn FnMut(NodeId),
1003 ) -> Option<Node> {
1004 self.record_removal(node_id);
1005 let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
1006 self.process_removed_subtree(node_id);
1007 self.invalidate_layout_parent_edge(node_id);
1008
1009 let node = self.doc.drop_node_ignoring_parent_with(node_id, on_drop);
1010 self.mutations_occurred |= node_is_in_document;
1011
1012 if let Some(parent_id) = node.as_ref().and_then(|node| node.parent) {
1014 let parent = &mut self.doc.nodes[parent_id];
1015 parent.insert_damage(ALL_DAMAGE);
1016 let parent_is_in_doc = parent.flags.is_in_document();
1017
1018 if parent_is_in_doc {
1020 if let Some(mut data) = parent
1021 .stylo_element_data_opt_mut()
1022 .and_then(|s| s.get_mut())
1023 {
1024 data.hint |= RestyleHint::restyle_subtree();
1025 }
1026 parent.mark_ancestors_dirty();
1028 }
1029
1030 parent.children.retain(|id| *id != node_id);
1031 self.maybe_record_node(parent_id);
1032 }
1033
1034 node
1035 }
1036
1037 pub fn remove_and_drop_all_children(&mut self, node_id: NodeId) {
1038 if self.doc.is_recording_mutations()
1039 && self.doc.nodes[node_id].flags.is_in_document()
1040 && !self.doc.nodes[node_id].children.is_empty()
1041 {
1042 let removed = self.doc.nodes[node_id].children.to_vec();
1043 self.doc.record_mutation(crate::DomMutation::ChildList {
1044 target: node_id,
1045 added: Vec::new(),
1046 removed,
1047 previous_sibling: None,
1048 next_sibling: None,
1049 });
1050 }
1051 let parent = &mut self.doc.nodes[node_id];
1052 let parent_is_in_doc = parent.flags.is_in_document();
1053
1054 if parent_is_in_doc {
1056 if let Some(mut data) = parent
1057 .stylo_element_data_opt_mut()
1058 .and_then(|s| s.get_mut())
1059 {
1060 data.hint |= RestyleHint::restyle_subtree();
1061 }
1062 parent.mark_ancestors_dirty();
1064 }
1065
1066 let children = mem::take(&mut parent.children);
1067 self.mutations_occurred |= parent_is_in_doc && !children.is_empty();
1068 for child_id in children {
1069 self.process_removed_subtree(child_id);
1070 self.invalidate_layout_parent_edge(child_id);
1071 let _ = self.doc.drop_node_ignoring_parent(child_id);
1072 }
1073 self.maybe_record_node(node_id);
1074 }
1075
1076 pub fn remove_node_if_unparented(&mut self, node_id: NodeId) {
1078 self.remove_node_if_unparented_with(node_id, &mut |_| {});
1079 }
1080
1081 pub fn remove_node_if_unparented_with(
1084 &mut self,
1085 node_id: NodeId,
1086 on_drop: &mut dyn FnMut(NodeId),
1087 ) {
1088 if let Some(node) = self.doc.get_node(node_id) {
1089 if node.parent.is_none() {
1090 self.remove_and_drop_node_with(node_id, on_drop);
1091 }
1092 }
1093 }
1094
1095 pub fn append_children(&mut self, parent_id: NodeId, child_ids: &[NodeId]) {
1097 self.add_children_to_parent(parent_id, child_ids, &|parent, child_ids| {
1098 parent.children.extend_from_slice(child_ids);
1099 });
1100 }
1101
1102 pub fn insert_nodes_before(&mut self, anchor_node_id: NodeId, new_node_ids: &[NodeId]) {
1103 let parent_id = self.doc.nodes[anchor_node_id].parent.unwrap();
1104 self.add_children_to_parent(parent_id, new_node_ids, &|parent, child_ids| {
1105 let node_child_idx = parent.index_of_child(anchor_node_id).unwrap();
1106 parent
1107 .children
1108 .splice(node_child_idx..node_child_idx, child_ids.iter().copied());
1109 });
1110 }
1111
1112 fn add_children_to_parent(
1113 &mut self,
1114 parent_id: NodeId,
1115 child_ids: &[NodeId],
1116 insert_children_fn: &dyn Fn(&mut Node, &[NodeId]),
1117 ) {
1118 let child_ids: Vec<NodeId> = if child_ids
1126 .iter()
1127 .any(|id| matches!(self.doc.nodes[*id].data, NodeData::DocumentFragment))
1128 {
1129 let mut flattened = Vec::with_capacity(child_ids.len());
1130 for id in child_ids.iter().copied() {
1131 if matches!(self.doc.nodes[id].data, NodeData::DocumentFragment) {
1132 let moved = std::mem::take(&mut self.doc.nodes[id].children);
1133 flattened.extend(moved);
1134 } else {
1135 flattened.push(id);
1136 }
1137 }
1138 flattened
1139 } else {
1140 child_ids.to_vec()
1141 };
1142 let child_ids = child_ids.as_slice();
1143
1144 let new_parent_is_in_document = self.doc.nodes[parent_id].flags.is_in_document();
1145 self.mutations_occurred |= new_parent_is_in_document && !child_ids.is_empty();
1146 for child_id in child_ids.iter().copied() {
1153 self.record_removal(child_id);
1154 self.invalidate_layout_parent_edge(child_id);
1155 let child = &mut self.doc.nodes[child_id];
1156 let child_was_in_doc = child.flags.is_in_document();
1157 self.mutations_occurred |= child_was_in_doc;
1158 let Some(old_parent_id) = child.parent.take() else {
1159 continue;
1160 };
1161
1162 let old_parent = &mut self.doc.nodes[old_parent_id];
1163 old_parent.insert_damage(ALL_DAMAGE);
1164
1165 if child_was_in_doc {
1167 if let Some(mut data) = old_parent
1168 .stylo_element_data_opt_mut()
1169 .and_then(|s| s.get_mut())
1170 {
1171 data.hint |= RestyleHint::restyle_subtree();
1172 }
1173 old_parent.mark_ancestors_dirty();
1175 }
1176
1177 old_parent.children.retain(|id| *id != child_id);
1178 self.maybe_record_node(old_parent_id);
1179 }
1180
1181 let new_parent = &mut self.doc.nodes[parent_id];
1182 new_parent.insert_damage(ALL_DAMAGE);
1183
1184 if new_parent_is_in_document {
1186 if let Some(mut data) = new_parent
1187 .stylo_element_data_opt_mut()
1188 .and_then(|s| s.get_mut())
1189 {
1190 data.hint |= RestyleHint::restyle_subtree();
1191 }
1192 new_parent.mark_ancestors_dirty();
1194 }
1195
1196 insert_children_fn(new_parent, child_ids);
1197
1198 for child_id in child_ids.iter().copied() {
1199 let child = &mut self.doc.nodes[child_id];
1200 let child_was_in_doc = child.flags.is_in_document();
1201 child.parent = Some(parent_id);
1202
1203 if new_parent_is_in_document && !child_was_in_doc {
1204 self.process_added_subtree(child_id);
1205 } else if !new_parent_is_in_document && child_was_in_doc {
1206 self.process_removed_subtree(child_id);
1207 }
1208 }
1209
1210 if new_parent_is_in_document && self.doc.is_recording_mutations() {
1211 if let (Some(first), Some(last)) = (child_ids.first(), child_ids.last()) {
1212 let previous_sibling = self.doc.sibling_context(*first).and_then(|c| c.1);
1213 let next_sibling = self.doc.sibling_context(*last).and_then(|c| c.2);
1214 self.doc.record_mutation(crate::DomMutation::ChildList {
1215 target: parent_id,
1216 added: child_ids.to_vec(),
1217 removed: Vec::new(),
1218 previous_sibling,
1219 next_sibling,
1220 });
1221 }
1222 }
1223
1224 self.maybe_record_node(parent_id);
1225 }
1226
1227 pub fn insert_nodes_after(&mut self, anchor_node_id: NodeId, new_node_ids: &[NodeId]) {
1229 match self.next_sibling_id(anchor_node_id) {
1230 Some(id) => self.insert_nodes_before(id, new_node_ids),
1231 None => {
1232 let parent_id = self.parent_id(anchor_node_id).unwrap();
1233 self.append_children(parent_id, new_node_ids)
1234 }
1235 }
1236 }
1237
1238 pub fn reparent_children(&mut self, old_parent_id: NodeId, new_parent_id: NodeId) {
1239 if self.doc.is_recording_mutations()
1240 && self.doc.nodes[old_parent_id].flags.is_in_document()
1241 && !self.doc.nodes[old_parent_id].children.is_empty()
1242 {
1243 let removed = self.doc.nodes[old_parent_id].children.to_vec();
1244 self.doc.record_mutation(crate::DomMutation::ChildList {
1245 target: old_parent_id,
1246 added: Vec::new(),
1247 removed,
1248 previous_sibling: None,
1249 next_sibling: None,
1250 });
1251 }
1252 let child_ids = std::mem::take(&mut self.doc.nodes[old_parent_id].children);
1253 self.maybe_record_node(old_parent_id);
1254 self.append_children(new_parent_id, &child_ids);
1255 }
1256
1257 pub fn replace_node_with(&mut self, anchor_node_id: NodeId, new_node_ids: &[NodeId]) {
1258 self.insert_nodes_before(anchor_node_id, new_node_ids);
1259 self.remove_node(anchor_node_id);
1260 }
1261}
1262
1263impl<'doc> DocumentMutator<'doc> {
1264 pub fn flush(&mut self) {
1265 if self.recompute_is_animating {
1266 self.doc.has_canvas = self.doc.compute_has_canvas();
1267 }
1268
1269 if let Some(id) = self.title_node {
1270 let title = self.doc.nodes[id].text_content();
1271 self.doc.shell_provider.set_window_title(title);
1272 }
1273
1274 for id in self.style_nodes.drain() {
1276 self.doc.process_style_element(id);
1277 }
1278
1279 for id in self.form_nodes.drain() {
1280 self.doc.reset_form_owner(id);
1281 }
1282
1283 #[cfg(feature = "autofocus")]
1284 if let Some(node_id) = self.node_to_autofocus.take() {
1285 if self.doc.get_node(node_id).is_some() {
1286 self.doc.set_focus_to(node_id);
1287 }
1288 }
1289
1290 #[cfg(feature = "shadow-dom")]
1291 self.dispatch_custom_element_attr_changes();
1292 }
1293
1294 #[cfg(feature = "shadow-dom")]
1296 fn dispatch_custom_element_attr_changes(&mut self) {
1297 if self.custom_element_attr_changes.is_empty() {
1298 return;
1299 }
1300 let changes = mem::take(&mut self.custom_element_attr_changes);
1301 for (node_id, name, old_value, new_value) in changes {
1302 let tag = self
1306 .doc
1307 .get_node(node_id)
1308 .and_then(|node| node.element_data())
1309 .map(|el| el.name.local.clone());
1310 let observed = tag
1311 .as_ref()
1312 .and_then(|tag| self.doc.custom_element_registry.get(tag))
1313 .map(|def| def.observes(&name.local))
1314 .unwrap_or(true);
1315 if !observed {
1316 continue;
1317 }
1318
1319 let Some(shadow_root_id) = self
1320 .doc
1321 .get_node(node_id)
1322 .and_then(|node| node.shadow_root_id())
1323 else {
1324 continue;
1325 };
1326 let Some(mut controller) = self.take_controller(node_id) else {
1327 continue;
1328 };
1329 {
1330 let mut ctx = crate::node::CustomElementCtx {
1331 mutator: self,
1332 host_id: node_id,
1333 shadow_root_id,
1334 };
1335 controller.attribute_changed(
1336 &mut ctx,
1337 &name.local,
1338 old_value.as_deref(),
1339 new_value.as_deref(),
1340 );
1341 }
1342 self.restore_controller(node_id, controller, false);
1343 }
1344 }
1345
1346 pub fn set_inner_html(&mut self, node_id: NodeId, html: &str) {
1347 self.remove_and_drop_all_children(node_id);
1348 self.doc
1349 .html_parser_provider
1350 .clone()
1351 .parse_inner_html(self, node_id, html);
1352 }
1353
1354 fn flush_eager_ops(&mut self) {
1355 let mut ops = mem::take(&mut self.eager_op_queue);
1356 for op in ops.drain(0..) {
1357 match op {
1358 SpecialOp::LoadImage(node_id) => self.load_image(node_id),
1359 SpecialOp::LoadIframe(node_id) => self.load_iframe(node_id),
1360 SpecialOp::LoadStylesheet(node_id) => self.load_linked_stylesheet(node_id),
1361 SpecialOp::UnloadStylesheet(node_id) => self.unload_stylesheet(node_id),
1362 SpecialOp::LoadCustomPaintSource(node_id) => self.load_custom_paint_src(node_id),
1363 SpecialOp::ProcessButtonInput(node_id) => self.process_button_input(node_id),
1364 SpecialOp::UnloadSubDocument(node_id) => self.remove_sub_document(node_id),
1365 #[cfg(feature = "custom-widget")]
1366 SpecialOp::UnloadCustomWidget(node_id) => self.remove_custom_widget(node_id),
1367 #[cfg(feature = "shadow-dom")]
1368 SpecialOp::UpgradeCustomElement(node_id) => self.upgrade_custom_element(node_id),
1369 #[cfg(feature = "shadow-dom")]
1370 SpecialOp::DisconnectCustomElement(node_id) => {
1371 self.disconnect_custom_element(node_id)
1372 }
1373 }
1374 }
1375
1376 self.eager_op_queue = ops;
1378 }
1379
1380 fn process_added_subtree(&mut self, node_id: NodeId) {
1381 self.doc.iter_subtree_mut(node_id, |node_id, doc| {
1382 let node = &mut doc.nodes[node_id];
1383 node.flags.set(NodeFlags::IS_IN_DOCUMENT, true);
1384 node.insert_damage(ALL_DAMAGE);
1385
1386 if let Some(id_attr) = node.attr(local_name!("id")).map(ToString::to_string) {
1388 doc.add_to_id_map(&id_attr, node_id);
1389 }
1390
1391 let node = &mut doc.nodes[node_id];
1392 let NodeData::Element(ref mut element) = node.data else {
1393 return;
1394 };
1395
1396 let tag = element.name.local.as_ref();
1398 match tag {
1399 "title" if element.name.ns == ns!(html) => self.title_node = Some(node_id),
1400 "link" => self.eager_op_queue.push(SpecialOp::LoadStylesheet(node_id)),
1401 "img" => self.eager_op_queue.push(SpecialOp::LoadImage(node_id)),
1402 "iframe" => self.eager_op_queue.push(SpecialOp::LoadIframe(node_id)),
1403 "canvas" => self
1404 .eager_op_queue
1405 .push(SpecialOp::LoadCustomPaintSource(node_id)),
1406 "style" => {
1407 self.style_nodes.insert(node_id);
1408 }
1409 "button" | "fieldset" | "input" | "select" | "textarea" | "object" | "output" => {
1410 self.eager_op_queue
1411 .push(SpecialOp::ProcessButtonInput(node_id));
1412 self.form_nodes.insert(node_id);
1413 }
1414 _ => {}
1415 }
1416
1417 #[cfg(feature = "shadow-dom")]
1421 {
1422 let needs_upgrade = doc.custom_element_registry.contains(&element.name.local)
1423 && element.custom_element_data().is_none();
1424 if needs_upgrade {
1425 self.eager_op_queue
1426 .push(SpecialOp::UpgradeCustomElement(node_id));
1427 }
1428 }
1429
1430 #[cfg(feature = "autofocus")]
1441 if node.is_focussable() {
1442 if let NodeData::Element(ref element) = node.data {
1443 if element.attr(local_name!("autofocus")).is_some() {
1444 self.node_to_autofocus = Some(node_id);
1445 }
1446 }
1447 }
1448 });
1449
1450 self.flush_eager_ops();
1451 }
1452
1453 fn process_removed_subtree(&mut self, node_id: NodeId) {
1454 self.doc.iter_subtree_mut(node_id, |node_id, doc| {
1455 doc.nodes[node_id]
1456 .flags
1457 .set(NodeFlags::IS_IN_DOCUMENT, false);
1458
1459 doc.clear_interaction_state_for_removed_node(node_id);
1463
1464 let node = &mut doc.nodes[node_id];
1465
1466 if doc.focus_node_id == Some(node_id) {
1483 doc.focus_node_id = None;
1484 }
1485 if doc.mousedown_node_id == Some(node_id) {
1486 doc.mousedown_node_id = None;
1487 }
1488
1489 if doc.text_selection.anchor.node_or_parent == Some(node_id)
1492 || doc.text_selection.focus.node_or_parent == Some(node_id)
1493 {
1494 doc.text_selection.clear();
1495 }
1496
1497 if node.has_snapshot() {
1500 let opaque_id = style::dom::TNode::opaque(&&*node);
1501 doc.snapshots.remove(&opaque_id);
1502 node.set_has_snapshot(false);
1503 }
1504
1505 if let Some(id_attr) = node.attr(local_name!("id")).map(ToString::to_string) {
1507 doc.remove_from_id_map(&id_attr, node_id);
1508 }
1509
1510 let node = &mut doc.nodes[node_id];
1511 let NodeData::Element(ref mut element) = node.data else {
1512 return;
1513 };
1514
1515 match &element.special_data {
1516 SpecialElementData::SubDocument(_) => {
1517 self.eager_op_queue
1518 .push(SpecialOp::UnloadSubDocument(node_id));
1519 }
1520 #[cfg(feature = "custom-widget")]
1521 SpecialElementData::CustomWidget(_) => {
1522 self.eager_op_queue
1523 .push(SpecialOp::UnloadCustomWidget(node_id));
1524 }
1525 #[cfg(feature = "shadow-dom")]
1526 SpecialElementData::CustomElement(_) => {
1527 self.eager_op_queue
1528 .push(SpecialOp::DisconnectCustomElement(node_id));
1529 }
1530 SpecialElementData::Stylesheet(_) => self
1531 .eager_op_queue
1532 .push(SpecialOp::UnloadStylesheet(node_id)),
1533 SpecialElementData::Image(_) => {}
1534 SpecialElementData::Canvas(_) => {
1535 self.recompute_is_animating = true;
1536 }
1537 SpecialElementData::TableRoot(_) => {}
1538 SpecialElementData::TextInput(_) => {}
1539 SpecialElementData::CheckboxInput(_) => {}
1540 SpecialElementData::Select(_) => {}
1541 #[cfg(feature = "file-input")]
1542 SpecialElementData::FileInput(_) => {}
1543 SpecialElementData::None => {}
1544 }
1545 });
1546
1547 self.flush_eager_ops();
1548 }
1549
1550 fn maybe_record_node(&mut self, node_id: impl Into<Option<NodeId>>) {
1551 let Some(node_id) = node_id.into() else {
1552 return;
1553 };
1554
1555 let Some(element) = self.doc.nodes[node_id].data.downcast_element() else {
1556 return;
1557 };
1558
1559 match element.name.local.as_ref() {
1560 "title" if element.name.ns == ns!(html) => self.title_node = Some(node_id),
1561 "style" => {
1562 self.style_nodes.insert(node_id);
1563 }
1564 _ => {}
1565 }
1566 }
1567
1568 fn load_linked_stylesheet(&mut self, target_id: NodeId) {
1569 let node = &self.doc.nodes[target_id];
1570
1571 let mut is_in_head = false;
1572 let mut parent_id = node.parent;
1573 while let Some(id) = parent_id
1574 && !is_in_head
1575 {
1576 let parent = &self.doc.nodes[id];
1577 is_in_head |= parent.data.is_element_with_tag_name(&local_name!("head"));
1578 parent_id = parent.parent;
1579 }
1580
1581 let rel_attr = node.attr(local_name!("rel"));
1582 let href_attr = node.attr(local_name!("href"));
1583
1584 let (Some(rels), Some(href)) = (rel_attr, href_attr) else {
1585 return;
1586 };
1587 if !rels.split_ascii_whitespace().any(|rel| rel == "stylesheet") {
1588 return;
1589 }
1590
1591 let url = self.doc.resolve_url(href);
1592 let handler = ResourceHandler::new(
1593 self.doc.tx.clone(),
1594 self.doc.id(),
1595 Some(node.id),
1596 self.doc.shell_provider.clone(),
1597 StylesheetHandler {
1598 source_url: url.clone(),
1599 guard: self.doc.guard.clone(),
1600 net_provider: self.doc.net_provider.clone(),
1601 abort_signal: self.doc.abort_signal.clone(),
1602 },
1603 );
1604
1605 if is_in_head && !self.doc.net_provider.is_noop() {
1606 self.doc
1607 .pending_critical_resources
1608 .insert(handler.request_id());
1609 }
1610
1611 self.doc.net_provider.fetch(
1612 self.doc.id(),
1613 self.doc.build_request(url),
1614 Box::new(handler),
1615 );
1616 }
1617
1618 fn unload_stylesheet(&mut self, node_id: NodeId) {
1619 let node = &mut self.doc.nodes[node_id];
1620 let Some(element) = node.element_data_mut() else {
1621 unreachable!();
1622 };
1623 let SpecialElementData::Stylesheet(stylesheet) = element.special_data.take() else {
1624 unreachable!();
1625 };
1626
1627 let guard = self.doc.guard.read();
1628 self.doc.stylist.remove_stylesheet(stylesheet, &guard);
1629 self.doc
1630 .stylist
1631 .force_stylesheet_origins_dirty(OriginSet::all());
1632
1633 self.doc.nodes_to_stylesheet.remove(&node_id);
1634 }
1635
1636 fn load_image(&mut self, target_id: NodeId) {
1637 let node = &self.doc.nodes[target_id];
1638 if let Some(raw_src) = node.attr(local_name!("src")) {
1639 if !raw_src.is_empty() {
1640 let src = self.doc.resolve_url(raw_src);
1641 let src_string = src.as_str();
1642
1643 if let Some(cached_image) = self.doc.image_cache.get(src_string) {
1645 #[cfg(feature = "tracing")]
1646 tracing::info!("Loading image {src_string} from cache");
1647 let node = &mut self.doc.nodes[target_id];
1648 node.element_data_mut().unwrap().special_data =
1649 SpecialElementData::Image(Box::new(cached_image.clone()));
1650 node.cache_mut().clear();
1651 node.insert_damage(ALL_DAMAGE);
1652 return;
1653 }
1654
1655 if let Some(waiting_list) = self.doc.pending_images.get_mut(src_string) {
1657 #[cfg(feature = "tracing")]
1658 tracing::info!("Image {src_string} already pending, queueing node {target_id}");
1659 waiting_list.push((target_id, ImageType::Image));
1660 return;
1661 }
1662
1663 #[cfg(feature = "tracing")]
1665 tracing::info!("Fetching image {src_string}");
1666 self.doc
1667 .pending_images
1668 .insert(src_string.to_string(), vec![(target_id, ImageType::Image)]);
1669
1670 self.doc.net_provider.fetch(
1671 self.doc.id(),
1672 self.doc.build_request(src),
1673 ResourceHandler::boxed(
1674 self.doc.tx.clone(),
1675 self.doc.id(),
1676 None, self.doc.shell_provider.clone(),
1678 ImageHandler::new(ImageType::Image),
1679 ),
1680 );
1681 }
1682 }
1683 }
1684
1685 fn load_iframe(&mut self, target_id: NodeId) {
1686 if self.doc.subdocument_depth >= crate::iframe::MAX_SUBDOCUMENT_DEPTH {
1687 #[cfg(feature = "tracing")]
1688 tracing::warn!(
1689 "Not loading iframe: max sub-document nesting depth ({}) reached",
1690 crate::iframe::MAX_SUBDOCUMENT_DEPTH
1691 );
1692 return;
1693 }
1694
1695 let node = &self.doc.nodes[target_id];
1696 let Some(element) = node.element_data() else {
1697 return;
1698 };
1699
1700 if let Some(srcdoc) = element.attr(local_name!("srcdoc")) {
1702 let srcdoc = srcdoc.to_string();
1703 self.doc.load_iframe_srcdoc(target_id, &srcdoc);
1704 return;
1705 }
1706
1707 let Some(raw_src) = element.attr(local_name!("src")) else {
1708 return;
1709 };
1710 if raw_src.is_empty() {
1711 return;
1712 }
1713 let Some(url) = self.doc.url.resolve_relative(raw_src) else {
1714 #[cfg(feature = "tracing")]
1715 tracing::warn!("Not loading iframe: could not resolve url {raw_src}");
1716 return;
1717 };
1718 self.doc.start_iframe_load(target_id, url);
1719 }
1720
1721 fn load_custom_paint_src(&mut self, target_id: NodeId) {
1722 let node = &mut self.doc.nodes[target_id];
1723 if let Some(raw_src) = node.attr(local_name!("src")) {
1724 if let Ok(custom_paint_source_id) = raw_src.parse::<u64>() {
1725 self.recompute_is_animating = true;
1726 let canvas_data = SpecialElementData::Canvas(CanvasData {
1727 custom_paint_source_id,
1728 });
1729 node.element_data_mut().unwrap().special_data = canvas_data;
1730 }
1731 }
1732 }
1733
1734 fn process_button_input(&mut self, target_id: NodeId) {
1735 let node = &self.doc.nodes[target_id];
1736 let Some(data) = node.element_data() else {
1737 return;
1738 };
1739
1740 let tagname = data.name.local.as_ref();
1741 let type_attr = data.attr(local_name!("type"));
1742 let value = data.attr(local_name!("value"));
1743
1744 if let ("input", Some("button" | "submit" | "reset"), Some(value)) =
1747 (tagname, type_attr, value)
1748 {
1749 let value = value.to_string();
1750 let id = self.create_text_node(&value);
1751 self.append_children(target_id, &[id]);
1752 return;
1753 }
1754 #[cfg(feature = "file-input")]
1755 if let ("input", Some("file")) = (tagname, type_attr) {
1756 let button_id = self.create_element(
1757 qual_name!("button", html),
1758 vec![
1759 Attribute {
1760 name: qual_name!("type", html),
1761 value: "button".into(),
1762 },
1763 Attribute {
1764 name: qual_name!("tabindex", html),
1765 value: "-1".into(),
1766 },
1767 ],
1768 );
1769 let label_id = self.create_element(qual_name!("label", html), vec![]);
1770 let text_id = self.create_text_node("No File Selected");
1771 let button_text_id = self.create_text_node("Browse");
1772 self.append_children(target_id, &[button_id, label_id]);
1773 self.append_children(label_id, &[text_id]);
1774 self.append_children(button_id, &[button_text_id]);
1775 }
1776 }
1777}
1778
1779fn set_input_checked_state(element: &mut ElementData, value: String) {
1781 let Ok(checked) = value.parse() else {
1782 return;
1783 };
1784 match element.special_data {
1785 SpecialElementData::CheckboxInput(ref mut checked_mut) => *checked_mut = checked,
1786 SpecialElementData::None => element.attrs.push(Attribute {
1791 name: qual_name!("checked", html),
1792 value: checked.to_string().into(),
1793 }),
1794 _ => {}
1795 }
1796}
1797
1798pub struct ViewportMut<'doc> {
1801 doc: &'doc mut BaseDocument,
1802 initial_viewport: Viewport,
1803}
1804impl ViewportMut<'_> {
1805 pub fn new(doc: &mut BaseDocument) -> ViewportMut<'_> {
1806 let initial_viewport = doc.viewport.clone();
1807 ViewportMut {
1808 doc,
1809 initial_viewport,
1810 }
1811 }
1812}
1813impl Deref for ViewportMut<'_> {
1814 type Target = Viewport;
1815
1816 fn deref(&self) -> &Self::Target {
1817 &self.doc.viewport
1818 }
1819}
1820impl DerefMut for ViewportMut<'_> {
1821 fn deref_mut(&mut self) -> &mut Self::Target {
1822 &mut self.doc.viewport
1823 }
1824}
1825impl Drop for ViewportMut<'_> {
1826 fn drop(&mut self) {
1827 if self.doc.viewport == self.initial_viewport {
1828 return;
1829 }
1830
1831 self.doc.set_stylist_device(make_device(
1832 &self.doc.viewport,
1833 self.doc.media_type.clone(),
1834 self.doc.font_ctx.clone(),
1835 ));
1836 self.doc.scroll_viewport_by(0.0, 0.0); let scale_has_changed =
1839 self.doc.viewport().scale_f64() != self.initial_viewport.scale_f64();
1840 if scale_has_changed {
1841 self.doc.invalidate_inline_contexts();
1842 self.doc.shell_provider.request_redraw();
1843 }
1844 }
1845}
1846
1847fn html_tag(name: &str) -> markup5ever::QualName {
1849 markup5ever::QualName::new(None, markup5ever::ns!(html), name.into())
1850}
1851
1852#[cfg(test)]
1853mod test {
1854 use style::media_queries::MediaType;
1855 use style_dom::ElementState;
1856
1857 use std::sync::{
1858 Arc,
1859 atomic::{AtomicUsize, Ordering},
1860 };
1861
1862 use blitz_traits::shell::{ColorScheme, ShellProvider, Viewport};
1863
1864 use crate::{
1865 Attribute, BaseDocument, DocumentConfig, ElementData, NodeData, NodeId, qual_name,
1866 };
1867
1868 #[test]
1869 fn media_type_defaults_to_screen() {
1870 let mut document = BaseDocument::new(DocumentConfig::default());
1871 assert_eq!(*document.media_type(), MediaType::screen());
1872 assert_eq!(document.stylist_device().media_type(), MediaType::screen());
1873 }
1874
1875 #[test]
1876 fn media_type_honors_config() {
1877 let mut document = BaseDocument::new(DocumentConfig {
1878 media_type: Some(MediaType::print()),
1879 ..Default::default()
1880 });
1881 assert_eq!(*document.media_type(), MediaType::print());
1882 assert_eq!(document.stylist_device().media_type(), MediaType::print());
1883 }
1884
1885 #[test]
1886 fn set_media_type_updates_stylist_device() {
1887 let mut document = BaseDocument::new(DocumentConfig::default());
1888 assert_eq!(document.stylist_device().media_type(), MediaType::screen());
1889
1890 document.set_media_type(MediaType::print());
1891 assert_eq!(*document.media_type(), MediaType::print());
1892 assert_eq!(document.stylist_device().media_type(), MediaType::print());
1893 }
1894
1895 #[test]
1896 fn removing_a_node_forgets_it_as_focused_and_pressed() {
1897 let mut document = BaseDocument::new(DocumentConfig::default());
1903 let button = document.create_node(NodeData::Element(Box::new(ElementData::new(
1904 qual_name!("button"),
1905 Vec::new(),
1906 ))));
1907 let root = document.root_node().id;
1908
1909 let mut mutator = document.mutate();
1910 mutator.append_children(root, &[button]);
1911 drop(mutator);
1912
1913 document.set_focus_to(button);
1914 document.set_mousedown_node_id(Some(button));
1915 assert_eq!(document.get_focussed_node_id(), Some(button));
1916 assert_eq!(document.mousedown_node_id, Some(button));
1917
1918 let mut mutator = document.mutate();
1919 mutator.remove_node(button);
1920 drop(mutator);
1921
1922 assert_eq!(
1923 document.get_focussed_node_id(),
1924 None,
1925 "a removed node must not stay focused"
1926 );
1927 assert_eq!(
1928 document.mousedown_node_id, None,
1929 "a removed node must not stay the pressed node"
1930 );
1931 }
1932
1933 #[test]
1934 fn dropping_a_child_clears_a_hidden_retained_layout_edge() {
1935 let mut document = BaseDocument::new(DocumentConfig {
1936 viewport: Some(Viewport::new(800, 600, 1.0, ColorScheme::Light)),
1937 ..Default::default()
1938 });
1939 let root = document.root_node().id;
1940 let (parent, child) = {
1941 let mut mutator = document.mutate();
1942 let parent = mutator.create_element(qual_name!("div"), vec![]);
1943 let child = mutator.create_element(qual_name!("button"), vec![]);
1944 mutator.set_style_property(parent, "width", "200px");
1945 mutator.set_style_property(parent, "height", "100px");
1946 mutator.append_children(parent, &[child]);
1947 mutator.append_children(root, &[parent]);
1948 (parent, child)
1949 };
1950
1951 document.resolve(0.0);
1952 {
1953 let mut mutator = document.mutate();
1954 mutator.set_style_property(parent, "display", "none");
1955 }
1956 document.resolve(0.0);
1957 assert!(
1958 document.nodes[parent]
1959 .layout_children
1960 .borrow()
1961 .as_ref()
1962 .is_some_and(|children| children.contains(&child)),
1963 "the hidden subtree should retain the layout edge that makes this regression possible"
1964 );
1965
1966 document.mutate().remove_and_drop_node(child);
1967 assert!(document.get_node(child).is_none(), "the child was freed");
1968 assert!(
1969 document.nodes[parent].layout_children.borrow().is_none(),
1970 "the surviving layout parent must not retain the freed key"
1971 );
1972
1973 document.resolve(0.0);
1975 }
1976
1977 #[test]
1978 fn mutator_remove_disabled() {
1979 let mut document = BaseDocument::new(DocumentConfig::default());
1980 let id = document.create_node(NodeData::Element(Box::new(ElementData::new(
1981 qual_name!("button"),
1982 vec![Attribute {
1983 name: qual_name!("disabled"),
1984 value: "".into(),
1985 }],
1986 ))));
1987
1988 let node = document.get_node(id).unwrap();
1989 assert!(
1990 node.element_state().contains(ElementState::DISABLED),
1991 "form node is disabled"
1992 );
1993 assert!(
1994 !node.element_state().contains(ElementState::ENABLED),
1995 "form node is not enabled yet"
1996 );
1997
1998 let mut mutator = document.mutate();
1999 mutator.clear_attribute(id, qual_name!("disabled"));
2000 drop(mutator);
2001
2002 let node = document.get_node(id).unwrap();
2003 assert!(
2004 !node.element_state().contains(ElementState::DISABLED),
2005 "form node is no longer disabled"
2006 );
2007 assert!(
2008 node.element_state().contains(ElementState::ENABLED),
2009 "form node is enabled"
2010 );
2011 }
2012
2013 #[test]
2014 fn mutator_set_disabled() {
2015 let mut document = BaseDocument::new(DocumentConfig::default());
2016 let id = document.create_node(NodeData::Element(Box::new(ElementData::new(
2017 qual_name!("button"),
2018 vec![],
2019 ))));
2020
2021 let node = document.get_node(id).unwrap();
2022 assert!(
2023 !node.element_state().contains(ElementState::DISABLED),
2024 "form node is not disabled"
2025 );
2026 assert!(
2027 node.element_state().contains(ElementState::ENABLED),
2028 "form node is enabled"
2029 );
2030
2031 let mut mutator = document.mutate();
2032 mutator.set_attribute(id, qual_name!("disabled"), "");
2033 drop(mutator);
2034
2035 let node = document.get_node(id).unwrap();
2036
2037 assert!(
2038 node.element_state().contains(ElementState::DISABLED),
2039 "form node is disabled"
2040 );
2041 assert!(
2042 !node.element_state().contains(ElementState::ENABLED),
2043 "form node is no longer enabled enabled"
2044 );
2045 }
2046
2047 #[test]
2048 fn mutator_set_disabled_invalid_node() {
2049 let mut document = BaseDocument::new(DocumentConfig::default());
2050 let id = document.create_node(NodeData::Element(Box::new(ElementData::new(
2051 qual_name!("a"),
2052 vec![],
2053 ))));
2054
2055 let node = document.get_node(id).unwrap();
2056 assert!(
2057 !node.element_state().contains(ElementState::DISABLED),
2058 "form node is not disabled"
2059 );
2060 assert!(
2061 !node.element_state().contains(ElementState::ENABLED),
2062 "form node is enabled"
2063 );
2064
2065 let mut mutator = document.mutate();
2066 mutator.set_attribute(id, qual_name!("disabled"), "");
2067 drop(mutator);
2068
2069 let node = document.get_node(id).unwrap();
2070 assert!(
2071 !node.element_state().contains(ElementState::DISABLED),
2072 "form node is not disabled"
2073 );
2074 assert!(
2075 !node.element_state().contains(ElementState::ENABLED),
2076 "form node is enabled"
2077 );
2078 }
2079
2080 #[test]
2081 fn mutator_id_attribute_updates_id_map() {
2082 let mut document = BaseDocument::new(DocumentConfig::default());
2083 let root_id = document.root_node().id;
2084
2085 let node_id = {
2086 let mut mutator = document.mutate();
2087 let node_id = mutator.create_element(
2088 qual_name!("div"),
2089 vec![Attribute {
2090 name: qual_name!("id"),
2091 value: "old".into(),
2092 }],
2093 );
2094 mutator.append_children(root_id, &[node_id]);
2095 node_id
2096 };
2097 assert_eq!(document.get_element_by_id("old"), Some(node_id));
2098
2099 {
2100 let mut mutator = document.mutate();
2101 mutator.set_attribute(node_id, qual_name!("id"), "new");
2102 }
2103 assert_eq!(document.get_element_by_id("new"), Some(node_id));
2104 assert_eq!(document.get_element_by_id("old"), None);
2105
2106 {
2107 let mut mutator = document.mutate();
2108 mutator.clear_attribute(node_id, qual_name!("id"));
2109 }
2110 assert_eq!(document.get_element_by_id("new"), None);
2111 }
2112
2113 #[test]
2114 fn get_element_by_id_duplicate_ids_first_in_tree_order_wins() {
2115 let mut document = BaseDocument::new(DocumentConfig::default());
2116 let root_id = document.root_node().id;
2117
2118 let (first_id, second_id) = {
2119 let mut mutator = document.mutate();
2120 let first_id = mutator.create_element(qual_name!("div"), vec![]);
2121 let second_id = mutator.create_element(qual_name!("div"), vec![]);
2122 mutator.append_children(root_id, &[first_id, second_id]);
2123 mutator.set_attribute(second_id, qual_name!("id"), "dup");
2126 mutator.set_attribute(first_id, qual_name!("id"), "dup");
2127 (first_id, second_id)
2128 };
2129 assert_eq!(document.get_element_by_id("dup"), Some(first_id));
2130
2131 {
2132 let mut mutator = document.mutate();
2133 mutator.remove_node(first_id);
2134 }
2135 assert_eq!(document.get_element_by_id("dup"), Some(second_id));
2136 }
2137
2138 #[derive(Default)]
2139 struct RedrawShell {
2140 redraw_requests: AtomicUsize,
2141 }
2142
2143 impl ShellProvider for RedrawShell {
2144 fn request_redraw(&self) {
2145 self.redraw_requests.fetch_add(1, Ordering::Relaxed);
2146 }
2147 }
2148
2149 #[test]
2150 fn mutator_requests_redraw_only_after_mutation() {
2151 let shell = Arc::new(RedrawShell::default());
2152 let mut document = BaseDocument::new(DocumentConfig {
2153 shell_provider: Some(shell.clone()),
2154 ..Default::default()
2155 });
2156 let root_id = document.root_node().id;
2157
2158 {
2159 let mut mutator = document.mutate();
2160 let parent_id = mutator.create_element(qual_name!("div"), vec![]);
2161 let child_id = mutator.create_element(qual_name!("span"), vec![]);
2162 mutator.append_children(parent_id, &[child_id]);
2163 mutator.remove_and_drop_all_children(parent_id);
2164 mutator.set_attribute(parent_id, qual_name!("id"), "detached");
2165 }
2166 assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 0);
2167
2168 {
2169 let mutator = document.mutate();
2170 assert_eq!(mutator.child_ids(root_id).len(), 0);
2171 }
2172
2173 {
2174 let mut mutator = document.mutate();
2175 let node_id = mutator.create_element(qual_name!("div"), vec![]);
2176 mutator.append_children(root_id, &[node_id]);
2177 mutator.set_attribute(node_id, qual_name!("id"), "in-document");
2178 }
2179 assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 1);
2180
2181 {
2182 let mut mutator = document.mutate();
2183 let parent_id = mutator.create_element(qual_name!("div"), vec![]);
2184 let child_id = mutator.create_element(qual_name!("span"), vec![]);
2185 mutator.append_children(root_id, &[parent_id]);
2186 mutator.append_children(parent_id, &[child_id]);
2187 mutator.remove_and_drop_all_children(parent_id);
2188 }
2189 assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 2);
2190
2191 {
2192 let mut mutator = document.mutate();
2193 let parent_id = mutator.create_element(qual_name!("div"), vec![]);
2194 let child_id = mutator.create_element(qual_name!("span"), vec![]);
2195 let detached_target_id = mutator.create_element(qual_name!("div"), vec![]);
2196 mutator.append_children(root_id, &[parent_id]);
2197 mutator.append_children(parent_id, &[child_id]);
2198 assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 2);
2199 mutator.append_children(detached_target_id, &[child_id]);
2200 }
2201 assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 3);
2202 }
2203
2204 #[test]
2205 fn moving_subtree_out_of_document_clears_in_document_flag() {
2206 let shell = Arc::new(RedrawShell::default());
2207 let mut document = BaseDocument::new(DocumentConfig {
2208 shell_provider: Some(shell.clone()),
2209 ..Default::default()
2210 });
2211 let root_id = document.root_node().id;
2212 let (child_id, grandchild_id, detached_parent_id) = {
2213 let mut mutator = document.mutate();
2214 let in_document_parent_id = mutator.create_element(qual_name!("div"), vec![]);
2215 let child_id = mutator.create_element(qual_name!("div"), vec![]);
2216 let grandchild_id = mutator.create_element(qual_name!("span"), vec![]);
2217 let detached_parent_id = mutator.create_element(qual_name!("section"), vec![]);
2218 mutator.append_children(root_id, &[in_document_parent_id]);
2219 mutator.append_children(in_document_parent_id, &[child_id]);
2220 mutator.append_children(child_id, &[grandchild_id]);
2221 (child_id, grandchild_id, detached_parent_id)
2222 };
2223 assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 1);
2224 assert!(document.get_node(child_id).unwrap().flags.is_in_document());
2225 assert!(
2226 document
2227 .get_node(grandchild_id)
2228 .unwrap()
2229 .flags
2230 .is_in_document()
2231 );
2232
2233 {
2234 let mut mutator = document.mutate();
2235 mutator.append_children(detached_parent_id, &[child_id]);
2236 }
2237 assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 2);
2238 assert!(!document.get_node(child_id).unwrap().flags.is_in_document());
2239 assert!(
2240 !document
2241 .get_node(grandchild_id)
2242 .unwrap()
2243 .flags
2244 .is_in_document()
2245 );
2246
2247 {
2248 let mut mutator = document.mutate();
2249 mutator.set_attribute(child_id, qual_name!("id"), "detached");
2250 }
2251 assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 2);
2252
2253 {
2254 let mut mutator = document.mutate();
2255 mutator.append_children(root_id, &[child_id]);
2256 }
2257 assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 3);
2258 assert!(document.get_node(child_id).unwrap().flags.is_in_document());
2259 assert!(
2260 document
2261 .get_node(grandchild_id)
2262 .unwrap()
2263 .flags
2264 .is_in_document()
2265 );
2266 }
2267
2268 #[test]
2280 fn a_paint_only_restyle_refreshes_the_calc_the_taffy_style_points_at() {
2281 use style::servo_arc::Arc as ServoArc;
2282
2283 let mut document = BaseDocument::new(DocumentConfig {
2284 viewport: Some(Viewport::new(800, 600, 1.0, ColorScheme::Light)),
2285 ..Default::default()
2286 });
2287 let root_id = document.root_node().id;
2288
2289 let (header_id, chip_id) = {
2290 let mut mutator = document.mutate();
2291 let header_id = mutator.create_element(qual_name!("div"), vec![]);
2292 let chip_id = mutator.create_element(qual_name!("span"), vec![]);
2293 mutator.set_style_property(header_id, "position", "relative");
2294 mutator.set_style_property(header_id, "width", "800px");
2295 mutator.set_style_property(header_id, "height", "60px");
2296 mutator.set_style_property(chip_id, "position", "absolute");
2297 mutator.set_style_property(chip_id, "max-width", "calc(100% - 24px)");
2298 mutator.set_style_property(chip_id, "color", "rgb(1, 2, 3)");
2299 mutator.append_children(header_id, &[chip_id]);
2300 mutator.append_children(root_id, &[header_id]);
2301 (header_id, chip_id)
2302 };
2303
2304 document.resolve(0.0);
2305
2306 {
2312 let mut mutator = document.mutate();
2313 mutator.set_style_property(header_id, "color", "rgb(4, 5, 6)");
2314 }
2315 document.resolve(0.0);
2316
2317 let node = document.get_node(chip_id).unwrap();
2318 let stylo_data = node.stylo_element_data_opt().and_then(|data| data.get());
2319 let primary = stylo_data
2320 .as_ref()
2321 .and_then(|data| data.styles.get_primary())
2322 .expect("the chip is styled");
2323 let source = node
2324 .style_source_opt()
2325 .expect("a styled node records the computed values its taffy style was built from");
2326
2327 assert!(
2328 ServoArc::ptr_eq(primary, source),
2329 "the cached taffy style still points into computed values that a restyle replaced, \
2330 so every calc() in it is a dangling pointer",
2331 );
2332 }
2333
2334 #[test]
2335 fn style_property_updates_nested_layout() {
2336 let mut document = BaseDocument::new(DocumentConfig {
2337 viewport: Some(Viewport::new(800, 600, 1.0, ColorScheme::Light)),
2338 ..Default::default()
2339 });
2340 let root_id = document.root_node().id;
2341
2342 let mover_id = {
2343 let mut mutator = document.mutate();
2344 let parent_id = mutator.create_element(qual_name!("div"), vec![]);
2345 let mover_id = mutator.create_element(qual_name!("div"), vec![]);
2346 mutator.set_style_property(parent_id, "position", "relative");
2347 mutator.set_style_property(parent_id, "width", "800px");
2348 mutator.set_style_property(parent_id, "height", "600px");
2349 mutator.set_style_property(mover_id, "position", "absolute");
2350 mutator.set_style_property(mover_id, "left", "0px");
2351 mutator.set_style_property(mover_id, "top", "0px");
2352 mutator.append_children(parent_id, &[mover_id]);
2353 mutator.append_children(root_id, &[parent_id]);
2354 mover_id
2355 };
2356
2357 document.resolve(0.0);
2358 assert_eq!(
2359 document
2360 .get_node(mover_id)
2361 .unwrap()
2362 .final_layout()
2363 .location
2364 .x,
2365 0.0
2366 );
2367
2368 {
2369 let mut mutator = document.mutate();
2370 mutator.set_style_property(mover_id, "left", "120px");
2371 }
2372
2373 document.resolve(0.0);
2374 assert_eq!(
2375 document
2376 .get_node(mover_id)
2377 .unwrap()
2378 .final_layout()
2379 .location
2380 .x,
2381 120.0
2382 );
2383 }
2384
2385 fn doc_with_a_comment() -> (BaseDocument, NodeId, NodeId, NodeId) {
2388 let mut doc = BaseDocument::new(DocumentConfig {
2389 viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
2390 ..Default::default()
2391 });
2392 let root_id = doc.root_node().id;
2393
2394 let mut mutr = doc.mutate();
2395 let html = mutr.create_element(qual_name!("html"), vec![]);
2396 let body = mutr.create_element(qual_name!("body"), vec![]);
2397 let container = mutr.create_element(qual_name!("div"), vec![]);
2398 let text = mutr.create_text_node("text");
2399 let comment = mutr.create_comment_node("comment");
2400 mutr.append_children(container, &[text, comment]);
2401 mutr.append_children(body, &[container]);
2402 mutr.append_children(html, &[body]);
2403 mutr.append_children(root_id, &[html]);
2404 drop(mutr);
2405
2406 doc.resolve(0.0);
2407 (doc, container, text, comment)
2408 }
2409
2410 #[test]
2414 fn setting_a_comments_data_writes_the_contents() {
2415 let (mut doc, _container, _text, comment) = doc_with_a_comment();
2416
2417 doc.mutate().set_node_text(comment, "rewritten");
2418
2419 let NodeData::Comment { contents } = &doc.get_node(comment).unwrap().data else {
2420 panic!("expected a comment node");
2421 };
2422 assert_eq!(contents, "rewritten");
2423 }
2424
2425 #[test]
2432 fn setting_a_comments_data_does_not_dirty_layout() {
2433 let (mut doc, container, text, comment) = doc_with_a_comment();
2434
2435 let container_damage_before = doc.get_node(container).unwrap().damage();
2436 let comment_damage_before = doc.get_node(comment).unwrap().damage();
2437
2438 doc.mutate().set_node_text(comment, "rewritten");
2439
2440 assert_eq!(
2441 doc.get_node(comment).unwrap().damage(),
2442 comment_damage_before,
2443 "writing a comment's data damaged the comment"
2444 );
2445 assert_eq!(
2446 doc.get_node(container).unwrap().damage(),
2447 container_damage_before,
2448 "writing a comment's data damaged its parent, scheduling a relayout \
2449 for a change that cannot affect a pixel"
2450 );
2451
2452 doc.mutate().set_node_text(text, "rewritten");
2453 assert_ne!(
2454 doc.get_node(container).unwrap().damage(),
2455 container_damage_before,
2456 "a text write should damage the parent, so the assertions above can fail"
2457 );
2458 }
2459
2460 #[test]
2463 fn rewriting_a_comment_with_its_own_contents_is_inert() {
2464 let (mut doc, container, _text, comment) = doc_with_a_comment();
2465 let container_damage_before = doc.get_node(container).unwrap().damage();
2466
2467 doc.mutate().set_node_text(comment, "comment");
2468
2469 let NodeData::Comment { contents } = &doc.get_node(comment).unwrap().data else {
2470 panic!("expected a comment node");
2471 };
2472 assert_eq!(contents, "comment");
2473 assert_eq!(
2474 doc.get_node(container).unwrap().damage(),
2475 container_damage_before
2476 );
2477 }
2478}