use apace_traits::node_id::NodeId;
use markup5ever::LocalName;
use std::collections::HashSet;
use std::mem;
use std::ops::{Deref, DerefMut, Range};
use crate::layout::damage::ALL_DAMAGE;
use crate::net::{ImageHandler, ResourceHandler, StylesheetHandler};
use crate::node::{CanvasData, NodeFlags, SpecialElementData};
use crate::stylo_device::DeviceChanges;
use crate::util::ImageType;
use crate::{
Attribute, BaseDocument, Document, ElementData, Node, NodeData, QualName, local_name, qual_name,
};
use apace_traits::shell::Viewport;
use selectors::matching::ElementSelectorFlags;
use style::Atom;
use style::invalidation::element::restyle_hints::RestyleHint;
use style::stylesheets::OriginSet;
use thin_vec::ThinVec;
macro_rules! tag_and_attr {
($tag:tt, $attr:tt) => {
(&local_name!($tag), &local_name!($attr))
};
}
#[derive(Debug, Clone)]
pub enum AppendTextErr {
NotTextNode,
}
enum SpecialOp {
LoadImage(NodeId),
LoadIframe(NodeId),
LoadStylesheet(NodeId),
UnloadStylesheet(NodeId),
LoadCustomPaintSource(NodeId),
ProcessButtonInput(NodeId),
UnloadSubDocument(NodeId),
#[cfg(feature = "custom-widget")]
UnloadCustomWidget(NodeId),
}
pub struct DocumentMutator<'doc> {
pub doc: &'doc mut BaseDocument,
eager_op_queue: Vec<SpecialOp>,
title_node: Option<NodeId>,
style_nodes: HashSet<NodeId>,
form_nodes: HashSet<NodeId>,
recompute_is_animating: bool,
mutations_occurred: bool,
previous_focus_path: Option<Vec<NodeId>>,
#[cfg(feature = "autofocus")]
node_to_autofocus: Option<NodeId>,
}
impl Drop for DocumentMutator<'_> {
fn drop(&mut self) {
self.flush(); if self.mutations_occurred {
self.doc.shell_provider.request_redraw();
}
}
}
impl DocumentMutator<'_> {
pub fn new<'doc>(doc: &'doc mut BaseDocument) -> DocumentMutator<'doc> {
DocumentMutator {
doc,
eager_op_queue: Vec::new(),
title_node: None,
style_nodes: HashSet::new(),
form_nodes: HashSet::new(),
recompute_is_animating: false,
mutations_occurred: false,
previous_focus_path: None,
#[cfg(feature = "autofocus")]
node_to_autofocus: None,
}
}
pub fn set_input_checked(&mut self, node_id: NodeId, value: bool) -> bool {
self.doc.set_input_checked(node_id, value)
}
pub fn set_input_indeterminate(&mut self, node_id: NodeId, value: bool) -> bool {
let Some(previous) = self
.doc
.nodes
.get(node_id)
.and_then(|node| node.element_data())
.and_then(ElementData::checkbox_input_indeterminate)
else {
return false;
};
if previous == value {
return true;
}
self.mutations_occurred = true;
self.doc
.snapshot_node_and(node_id, style_dom::ElementState::INDETERMINATE, |node| {
node.element_data_mut()
.unwrap()
.set_checkbox_input_indeterminate(value);
node.mark_ancestors_dirty();
node.insert_damage(ALL_DAMAGE);
});
true
}
pub fn node_has_parent(&self, node_id: NodeId) -> bool {
self.doc.nodes[node_id].parent.is_some()
}
pub fn previous_sibling_id(&self, node_id: NodeId) -> Option<NodeId> {
self.doc.nodes[node_id].backward(1).map(|node| node.id)
}
pub fn next_sibling_id(&self, node_id: NodeId) -> Option<NodeId> {
self.doc.nodes[node_id].forward(1).map(|node| node.id)
}
pub fn parent_id(&self, node_id: NodeId) -> Option<NodeId> {
self.doc.nodes[node_id].parent
}
pub fn last_child_id(&self, node_id: NodeId) -> Option<NodeId> {
self.doc.nodes[node_id].children.last().copied()
}
pub fn child_ids(&self, node_id: NodeId) -> ThinVec<NodeId> {
self.doc.nodes[node_id].children.clone()
}
pub fn element_name(&self, node_id: NodeId) -> Option<&QualName> {
self.doc.nodes[node_id].element_data().map(|el| &el.name)
}
pub fn node_at_path(&self, start_node_id: NodeId, path: &[u8]) -> NodeId {
let mut current = &self.doc.nodes[start_node_id];
for i in path {
let new_id = current.children[*i as usize];
current = &self.doc.nodes[new_id];
}
current.id
}
pub fn create_comment_node(&mut self, contents: &str) -> NodeId {
self.doc.create_node(NodeData::Comment {
contents: contents.to_string(),
})
}
pub fn create_text_node(&mut self, text: &str) -> NodeId {
self.doc.create_text_node(text)
}
pub fn create_element(&mut self, name: QualName, attrs: Vec<Attribute>) -> NodeId {
let mut data = ElementData::new(name, attrs);
data.flush_style_attribute(self.doc.guard(), &self.doc.url.url_extra_data());
let id = self.doc.create_node(NodeData::Element(Box::new(data)));
let node = self.doc.get_node_mut(id).unwrap();
*node.stylo_element_data_mut().ensure_init_mut() = style::data::ElementData {
damage: ALL_DAMAGE,
..Default::default()
};
id
}
pub fn deep_clone_node(&mut self, node_id: NodeId) -> NodeId {
self.doc.deep_clone_node(node_id)
}
pub fn template_contents(&mut self, template_id: NodeId) -> NodeId {
if let Some(contents_id) = self.try_template_contents(template_id) {
return contents_id;
}
let name = QualName::new(
None,
markup5ever::ns!(html),
markup5ever::LocalName::from("#document-fragment"),
);
let contents_id = self.create_element(name, Vec::new());
if let Some(element) = self
.doc
.get_node_mut(template_id)
.and_then(|node| node.element_data_mut())
{
element.template_contents = Some(contents_id);
}
contents_id
}
pub fn try_template_contents(&self, template_id: NodeId) -> Option<NodeId> {
self.doc
.get_node(template_id)
.and_then(|node| node.element_data())
.and_then(|element| element.template_contents)
}
pub fn set_node_text(&mut self, node_id: NodeId, value: &str) {
let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
let node = &mut self.doc.nodes[node_id];
let text = match node.data {
NodeData::Text(ref mut text) => text,
_ => return,
};
let changed = text.content != value;
if changed {
self.mutations_occurred |= node_is_in_document;
text.content.clear();
text.content.push_str(value);
node.insert_damage(ALL_DAMAGE);
node.mark_ancestors_dirty();
let parent_id = node.parent;
if let Some(parent_id) = parent_id {
let parent = &mut self.doc.nodes[parent_id];
parent.insert_damage(ALL_DAMAGE);
}
self.maybe_record_node(parent_id);
if let Some(parent_id) = parent_id {
self.doc.sync_placeholder_state(parent_id);
}
}
}
pub fn append_text_to_node(
&mut self,
node_id: NodeId,
text: &str,
) -> Result<(), AppendTextErr> {
let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
let node = &mut self.doc.nodes[node_id];
node.insert_damage(ALL_DAMAGE);
node.mark_ancestors_dirty();
let parent_id = node.parent;
match node.text_data_mut() {
Some(data) => {
data.content += text;
self.mutations_occurred |= node_is_in_document;
if let Some(parent_id) = parent_id {
self.doc.nodes[parent_id].insert_damage(ALL_DAMAGE);
}
Ok(())
}
None => Err(AppendTextErr::NotTextNode),
}
}
pub fn add_attrs_if_missing(&mut self, node_id: NodeId, attrs: Vec<Attribute>) {
let node = &mut self.doc.nodes[node_id];
node.insert_damage(ALL_DAMAGE);
let element_data = node.element_data_mut().expect("Not an element");
let existing_names = element_data
.attrs
.iter()
.map(|e| e.name.clone())
.collect::<HashSet<_>>();
for attr in attrs
.into_iter()
.filter(|attr| !existing_names.contains(&attr.name))
{
self.set_attribute(node_id, attr.name, &attr.value);
}
}
pub fn set_attribute(&mut self, node_id: NodeId, name: QualName, value: &str) {
let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
if node_is_in_document {
self.doc.snapshot_node_for_attribute(node_id, &name.local);
self.restyle_for_attribute_change(node_id, &name.local);
}
if name.local == local_name!("id") && node_is_in_document {
if let Some(old_id) = self.doc.nodes[node_id]
.element_data()
.map(|element| element.id.clone())
{
if let Some(old_id) = old_id {
self.doc.remove_from_id_map(&old_id, node_id);
}
self.doc.add_to_id_map(value, node_id);
}
}
let node = &mut self.doc.nodes[node_id];
let NodeData::Element(ref mut element) = node.data else {
return;
};
self.mutations_occurred |= node_is_in_document;
#[cfg(feature = "custom-widget")]
if let SpecialElementData::CustomWidget(widget_data) = &mut element.special_data {
let old_value = element.attrs.get(&name).as_ref().map(|attr| &*attr.value);
widget_data
.widget
.attribute_changed(&name.local, old_value, Some(value));
}
element.attrs.set(name.clone(), value);
if name.local == local_name!("type") && element.name.local == local_name!("input") {
if value != "checkbox" {
element
.element_state
.remove(style_dom::ElementState::INDETERMINATE);
}
if let Some(input) = element.text_input_data_mut() {
input.set_password(value == "password");
input.refresh_display_config(
&mut self.doc.font_ctx.lock().unwrap(),
&mut self.doc.layout_ctx,
);
}
}
if name.local == local_name!("tabindex")
|| name.local == local_name!("href")
|| name.local == local_name!("disabled")
{
element.flush_is_focussable();
}
if name.local == local_name!("href") {
element.flush_link_state();
}
if name.local == local_name!("open") {
element.flush_open_state();
}
let tag = &element.name.local;
let attr = &name.local;
if *attr == local_name!("id") {
element.id = Some(Atom::from(value))
}
if *attr == local_name!("class") {
element.classes = crate::node::parse_classes(value);
}
if *attr == local_name!("value") {
if let Some(input_data) = element.text_input_data_mut() {
input_data.set_text(
&mut self.doc.font_ctx.lock().unwrap(),
&mut self.doc.layout_ctx,
value,
);
}
self.doc.sync_placeholder_state(node_id);
return;
}
if *attr == local_name!("placeholder") {
if let Some(input) = element.text_input_data_mut() {
input.set_placeholder(Some(value));
input.refresh_display(
&mut self.doc.font_ctx.lock().unwrap(),
&mut self.doc.layout_ctx,
);
}
self.doc.sync_placeholder_state(node_id);
return;
}
if *attr == local_name!("style") {
element.flush_style_attribute(&self.doc.guard, &self.doc.url.url_extra_data());
node.set_restyle_hint(RestyleHint::RESTYLE_STYLE_ATTRIBUTE);
return;
}
if *attr == local_name!("disabled") && element.can_be_disabled() {
node.disable();
return;
}
if !node.flags.is_in_document() {
self.doc.sync_placeholder_state(node_id);
if matches!(name.local.as_ref(), "name" | "form" | "type") {
self.doc.reconcile_radio(node_id);
}
return;
}
#[cfg(feature = "custom-widget")]
if element.custom_widget_data().is_some() {
return;
}
if (tag, attr) == tag_and_attr!("input", "checked") {
set_input_checked_state(element, value.to_string());
} else if (tag, attr) == tag_and_attr!("img", "src") {
node.element_data_mut().unwrap().special_data =
SpecialElementData::Image(Box::new(crate::node::ImageData::None));
self.load_image(node_id);
} else if (tag, attr) == tag_and_attr!("canvas", "src") {
self.load_custom_paint_src(node_id);
} else if (tag, attr) == tag_and_attr!("link", "href")
|| (tag, attr) == tag_and_attr!("link", "rel")
{
self.load_linked_stylesheet(node_id);
} else if (tag, attr) == tag_and_attr!("iframe", "src")
|| (tag, attr) == tag_and_attr!("iframe", "srcdoc")
{
self.load_iframe(node_id);
}
if matches!(name.local.as_ref(), "name" | "form" | "type" | "checked") {
self.doc.reconcile_radio(node_id);
}
self.doc.sync_placeholder_state(node_id);
}
pub fn clear_attribute(&mut self, node_id: NodeId, name: QualName) {
let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
if node_is_in_document {
self.doc.snapshot_node_for_attribute(node_id, &name.local);
self.restyle_for_attribute_change(node_id, &name.local);
}
if name.local == local_name!("id") && node_is_in_document {
if let Some(old_id) = self.doc.nodes[node_id]
.element_data()
.and_then(|element| element.id.clone())
{
self.doc.remove_from_id_map(&old_id, node_id);
}
}
let node = &mut self.doc.nodes[node_id];
let Some(element) = node.element_data_mut() else {
return;
};
let removed_attr = element.attrs.remove(&name);
let had_attr = removed_attr.is_some();
if !had_attr {
return;
}
self.mutations_occurred |= node_is_in_document;
#[cfg(feature = "custom-widget")]
if let SpecialElementData::CustomWidget(widget_data) = &mut element.special_data {
let old_value = removed_attr.as_ref().map(|attr| &*attr.value);
widget_data
.widget
.attribute_changed(&name.local, old_value, None);
}
if name.local == local_name!("id") {
element.id = None;
}
if name.local == local_name!("class") {
element.classes.clear();
}
if name.local == local_name!("tabindex")
|| name.local == local_name!("href")
|| name.local == local_name!("disabled")
{
element.flush_is_focussable();
}
if name.local == local_name!("href") {
element.flush_link_state();
}
if name.local == local_name!("open") {
element.flush_open_state();
}
if name.local == local_name!("type") && element.name.local == local_name!("input") {
element
.element_state
.remove(style_dom::ElementState::INDETERMINATE);
if let Some(input) = element.text_input_data_mut() {
input.set_password(false);
}
}
if name.local == local_name!("value") {
if let Some(input_data) = element.text_input_data_mut() {
input_data.set_text(
&mut self.doc.font_ctx.lock().unwrap(),
&mut self.doc.layout_ctx,
"",
);
}
}
if name.local == local_name!("placeholder") {
if let Some(input) = element.text_input_data_mut() {
input.set_placeholder(None);
input.refresh_display(
&mut self.doc.font_ctx.lock().unwrap(),
&mut self.doc.layout_ctx,
);
}
self.doc.sync_placeholder_state(node_id);
return;
}
let tag = &element.name.local;
let attr = &name.local;
if *attr == local_name!("disabled") && element.can_be_disabled() {
node.enable();
return;
}
if *attr == local_name!("style") {
element.flush_style_attribute(&self.doc.guard, &self.doc.url.url_extra_data());
node.set_restyle_hint(RestyleHint::RESTYLE_STYLE_ATTRIBUTE);
} else if (tag, attr) == tag_and_attr!("input", "checked") {
element.set_checkbox_input_checked(false);
} else if (tag, attr) == tag_and_attr!("img", "src") {
element.special_data =
SpecialElementData::Image(Box::new(crate::node::ImageData::None));
node.clear_layout_cache();
node.insert_damage(ALL_DAMAGE);
self.load_image(node_id);
} else if (tag, attr) == tag_and_attr!("canvas", "src") {
self.recompute_is_animating = true;
} else if (tag, attr) == tag_and_attr!("link", "href") {
self.unload_stylesheet(node_id);
} else if (tag, attr) == tag_and_attr!("iframe", "srcdoc") && node_is_in_document {
self.load_iframe(node_id);
}
if matches!(name.local.as_ref(), "name" | "form" | "type") {
self.doc.reconcile_radio(node_id);
}
self.doc.sync_placeholder_state(node_id);
}
pub fn set_style_property(&mut self, node_id: NodeId, name: &str, value: &str) {
let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
self.doc.set_style_property(node_id, name, value);
self.mutations_occurred |= node_is_in_document;
}
pub fn remove_style_property(&mut self, node_id: NodeId, name: &str) {
let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
self.doc.remove_style_property(node_id, name);
self.mutations_occurred |= node_is_in_document;
}
pub fn set_sub_document(&mut self, node_id: NodeId, sub_document: Box<dyn Document>) {
let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
self.doc.set_sub_document(node_id, sub_document);
self.mutations_occurred |= node_is_in_document;
}
pub fn remove_sub_document(&mut self, node_id: NodeId) {
let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
self.doc.remove_sub_document(node_id);
self.mutations_occurred |= node_is_in_document;
}
#[cfg(feature = "custom-widget")]
pub fn set_custom_widget(&mut self, node_id: NodeId, widget: Box<dyn crate::Widget>) {
let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
self.doc.set_custom_widget(node_id, widget);
self.mutations_occurred |= node_is_in_document;
}
#[cfg(feature = "custom-widget")]
pub fn remove_custom_widget(&mut self, node_id: NodeId) {
let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
self.doc.remove_custom_widget(node_id);
self.mutations_occurred |= node_is_in_document;
}
pub fn remove_node(&mut self, node_id: NodeId) {
let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
self.process_removed_subtree(node_id);
let node = &mut self.doc.nodes[node_id];
if let Some(parent_id) = node.parent.take() {
self.mutations_occurred |= node_is_in_document;
let parent = &mut self.doc.nodes[parent_id];
parent.insert_damage(ALL_DAMAGE);
let index = parent.index_of_child(node_id).unwrap_or(0);
parent.children.retain(|id| *id != node_id);
if node_is_in_document {
self.restyle_for_child_list_change(parent_id, index..index);
}
self.maybe_record_node(parent_id);
}
}
pub fn remove_and_drop_node(&mut self, node_id: NodeId) -> Option<Node> {
self.remove_and_drop_node_with(node_id, &mut |_| {})
}
pub fn remove_and_drop_node_with(
&mut self,
node_id: NodeId,
on_drop: &mut dyn FnMut(NodeId),
) -> Option<Node> {
let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
self.process_removed_subtree(node_id);
let node = self.doc.drop_node_ignoring_parent_with(node_id, on_drop);
self.mutations_occurred |= node_is_in_document;
if let Some(parent_id) = node.as_ref().and_then(|node| node.parent) {
let parent = &mut self.doc.nodes[parent_id];
parent.insert_damage(ALL_DAMAGE);
let parent_is_in_doc = parent.flags.is_in_document();
let index = parent.index_of_child(node_id).unwrap_or(0);
parent.children.retain(|id| *id != node_id);
if parent_is_in_doc {
self.restyle_for_child_list_change(parent_id, index..index);
}
self.maybe_record_node(parent_id);
}
node
}
pub fn remove_and_drop_all_children(&mut self, node_id: NodeId) {
let parent = &mut self.doc.nodes[node_id];
let parent_is_in_doc = parent.flags.is_in_document();
if !parent.children.is_empty() {
parent.insert_damage(ALL_DAMAGE);
}
let children = mem::take(&mut parent.children);
self.mutations_occurred |= parent_is_in_doc && !children.is_empty();
if parent_is_in_doc {
self.restyle_for_child_list_change(node_id, 0..0);
}
for child_id in children {
self.process_removed_subtree(child_id);
let _ = self.doc.drop_node_ignoring_parent(child_id);
}
self.maybe_record_node(node_id);
}
fn restyle_for_attribute_change(&mut self, node_id: NodeId, attr: &LocalName) {
let node = &mut self.doc.nodes[node_id];
let is_style = *attr == local_name!("style");
let is_svg = node
.element_data()
.is_some_and(|element| element.name.ns == markup5ever::ns!(svg));
let read_by_layout = is_svg
|| matches!(
*attr,
local_name!("type")
| local_name!("value")
| local_name!("checked")
| local_name!("width")
| local_name!("height")
| local_name!("colspan")
| local_name!("rowspan")
| local_name!("rows")
| local_name!("cols")
| local_name!("start")
| local_name!("reversed")
| local_name!("src")
| local_name!("srcdoc")
| local_name!("span")
);
#[cfg(feature = "custom-widget")]
let read_by_layout = read_by_layout
|| node.element_data().is_some_and(|element| {
matches!(element.special_data, SpecialElementData::CustomWidget(_))
});
if let Some(mut data) = node.try_stylo_element_data_mut().and_then(|s| s.get_mut()) {
data.hint |= if is_style {
RestyleHint::RESTYLE_STYLE_ATTRIBUTE
} else {
RestyleHint::RESTYLE_SELF
};
if read_by_layout {
data.damage.insert(ALL_DAMAGE);
}
}
if read_by_layout {
node.mark_damaged();
}
node.mark_ancestors_dirty();
}
fn restyle_for_child_list_change(&mut self, parent_id: NodeId, changed: Range<usize>) {
let parent = &self.doc.nodes[parent_id];
let flags = parent.selector_flags().get();
let mut hint = RestyleHint::empty();
if flags.intersects(
ElementSelectorFlags::HAS_EMPTY_SELECTOR
| ElementSelectorFlags::ANCHORS_RELATIVE_SELECTOR
| ElementSelectorFlags::ANCHORS_RELATIVE_SELECTOR_NON_SUBJECT,
) {
hint |= RestyleHint::RESTYLE_SELF;
}
let front_change = flags.contains(ElementSelectorFlags::HAS_SLOW_SELECTOR_LATER_SIBLINGS)
&& !parent.children[..changed.start]
.iter()
.any(|id| self.doc.nodes[*id].is_element());
if front_change
|| flags.intersects(
ElementSelectorFlags::HAS_SLOW_SELECTOR
| ElementSelectorFlags::HAS_SLOW_SELECTOR_NTH_OF
| ElementSelectorFlags::MAY_HAVE_TREE_COUNTING_FUNCTION,
)
{
hint |= RestyleHint::RESTYLE_DESCENDANTS;
} else {
let children = mem::take(&mut self.doc.nodes[parent_id].children);
let later = if flags.contains(ElementSelectorFlags::HAS_SLOW_SELECTOR_LATER_SIBLINGS) {
changed.end..children.len()
} else {
0..0
};
let mut edge = [None, None];
if flags.contains(ElementSelectorFlags::HAS_EDGE_CHILD_SELECTOR) {
let is_element = |id: &NodeId| self.doc.nodes[*id].is_element();
if !children[..changed.start].iter().any(is_element) {
edge[0] = children[changed.end..].iter().copied().find(is_element);
}
if !children[changed.end..].iter().any(is_element) {
edge[1] = children[..changed.start]
.iter()
.rev()
.copied()
.find(is_element);
}
}
for id in children[later].iter().chain(edge.iter().flatten()) {
self.doc.nodes[*id].set_restyle_hint(RestyleHint::restyle_subtree());
}
self.doc.nodes[parent_id].children = children;
}
let parent = &mut self.doc.nodes[parent_id];
if !hint.is_empty() {
if let Some(mut data) = parent
.try_stylo_element_data_mut()
.and_then(|s| s.get_mut())
{
data.hint |= hint;
}
}
parent.set_dirty_descendants();
parent.mark_ancestors_dirty();
}
pub fn remove_node_if_unparented(&mut self, node_id: NodeId) {
self.remove_node_if_unparented_with(node_id, &mut |_| {});
}
pub fn remove_node_if_unparented_with(
&mut self,
node_id: NodeId,
on_drop: &mut dyn FnMut(NodeId),
) {
if let Some(node) = self.doc.get_node(node_id) {
if node.parent.is_none() {
self.remove_and_drop_node_with(node_id, on_drop);
}
}
}
pub fn append_children(&mut self, parent_id: NodeId, child_ids: &[NodeId]) {
self.add_children_to_parent(parent_id, child_ids, &|parent, child_ids| {
let index = parent.children.len();
parent.children.extend_from_slice(child_ids);
index
});
}
pub fn insert_nodes_before(&mut self, anchor_node_id: NodeId, new_node_ids: &[NodeId]) {
let parent_id = self.doc.nodes[anchor_node_id].parent.unwrap();
self.add_children_to_parent(parent_id, new_node_ids, &|parent, child_ids| {
let node_child_idx = parent.index_of_child(anchor_node_id).unwrap();
parent
.children
.splice(node_child_idx..node_child_idx, child_ids.iter().copied());
node_child_idx
});
}
fn add_children_to_parent(
&mut self,
parent_id: NodeId,
child_ids: &[NodeId],
insert_children_fn: &dyn Fn(&mut Node, &[NodeId]) -> usize,
) {
if self.previous_focus_path.is_none()
&& self.doc.focus_node_id.is_some()
&& child_ids.iter().any(|id| {
self.doc.nodes[*id]
.parent
.is_some_and(|old| old != parent_id)
})
{
let path = self
.doc
.maybe_node_element_ancestors(self.doc.focus_node_id);
if path.iter().any(|id| child_ids.contains(id)) {
self.previous_focus_path = Some(path);
}
}
let new_parent_is_in_document = self.doc.nodes[parent_id].flags.is_in_document();
self.mutations_occurred |= new_parent_is_in_document && !child_ids.is_empty();
for child_id in child_ids.iter().copied() {
let child = &self.doc.nodes[child_id];
if child.parent.is_some()
|| child.layout_parent.get().is_some()
|| child.oof_containing_block.get().is_some()
{
self.doc.retire_moved_layout_references(child_id);
}
let child = &mut self.doc.nodes[child_id];
let child_was_in_doc = child.flags.is_in_document();
self.mutations_occurred |= child_was_in_doc;
let Some(old_parent_id) = child.parent.take() else {
continue;
};
let old_parent = &mut self.doc.nodes[old_parent_id];
old_parent.insert_damage(ALL_DAMAGE);
let index = old_parent.index_of_child(child_id).unwrap_or(0);
old_parent.children.retain(|id| *id != child_id);
if child_was_in_doc {
self.restyle_for_child_list_change(old_parent_id, index..index);
}
self.maybe_record_node(old_parent_id);
}
let new_parent = &mut self.doc.nodes[parent_id];
new_parent.insert_damage(ALL_DAMAGE);
let index = insert_children_fn(new_parent, child_ids);
if new_parent_is_in_document {
self.restyle_for_child_list_change(parent_id, index..index + child_ids.len());
}
for child_id in child_ids.iter().copied() {
let child = &mut self.doc.nodes[child_id];
let child_was_in_doc = child.flags.is_in_document();
child.parent = Some(parent_id);
if new_parent_is_in_document && child.primary_styles().is_some() {
child.set_restyle_hint(RestyleHint::restyle_subtree());
}
if new_parent_is_in_document && !child_was_in_doc {
self.process_added_subtree(child_id);
} else if !new_parent_is_in_document && child_was_in_doc {
self.process_removed_subtree(child_id);
}
}
self.doc.reconcile_radio_subtrees(child_ids);
self.maybe_record_node(parent_id);
}
pub fn insert_nodes_after(&mut self, anchor_node_id: NodeId, new_node_ids: &[NodeId]) {
match self.next_sibling_id(anchor_node_id) {
Some(id) => self.insert_nodes_before(id, new_node_ids),
None => {
let parent_id = self.parent_id(anchor_node_id).unwrap();
self.append_children(parent_id, new_node_ids)
}
}
}
pub fn reparent_children(&mut self, old_parent_id: NodeId, new_parent_id: NodeId) {
let child_ids = std::mem::take(&mut self.doc.nodes[old_parent_id].children);
self.maybe_record_node(old_parent_id);
self.append_children(new_parent_id, &child_ids);
}
pub fn replace_node_with(&mut self, anchor_node_id: NodeId, new_node_ids: &[NodeId]) {
self.insert_nodes_before(anchor_node_id, new_node_ids);
self.remove_node(anchor_node_id);
}
fn detach_all(&mut self, node_ids: &[NodeId]) {
for node_id in node_ids {
if self.node_has_parent(*node_id) {
self.remove_node(*node_id);
}
}
}
fn viable_sibling(
&self,
anchor_id: NodeId,
offset: isize,
excluded: &[NodeId],
) -> Option<NodeId> {
let node = self.doc.get_node(anchor_id)?;
let parent = self.doc.get_node(node.parent?)?;
let mut index = parent.index_of_child(anchor_id)?;
loop {
index = index.checked_add_signed(offset)?;
let sibling_id = *parent.children.get(index)?;
if !excluded.contains(&sibling_id) {
return Some(sibling_id);
}
}
}
pub fn prepend_nodes(&mut self, parent_id: NodeId, node_ids: &[NodeId]) {
self.detach_all(node_ids);
match self.child_ids(parent_id).first().copied() {
Some(first_child_id) => self.insert_nodes_before(first_child_id, node_ids),
None => self.append_children(parent_id, node_ids),
}
}
pub fn replace_children(&mut self, parent_id: NodeId, node_ids: &[NodeId]) {
self.detach_all(node_ids);
for child_id in self.child_ids(parent_id) {
self.remove_node(child_id);
}
self.append_children(parent_id, node_ids);
}
pub fn before_node(&mut self, anchor_id: NodeId, node_ids: &[NodeId]) {
let Some(parent_id) = self.parent_id(anchor_id) else {
return;
};
let viable_prev = self.viable_sibling(anchor_id, -1, node_ids);
self.detach_all(node_ids);
match viable_prev {
Some(prev_id) => self.insert_nodes_after(prev_id, node_ids),
None => match self.child_ids(parent_id).first().copied() {
Some(first_child_id) => self.insert_nodes_before(first_child_id, node_ids),
None => self.append_children(parent_id, node_ids),
},
}
}
pub fn after_node(&mut self, anchor_id: NodeId, node_ids: &[NodeId]) {
let Some(parent_id) = self.parent_id(anchor_id) else {
return;
};
let viable_next = self.viable_sibling(anchor_id, 1, node_ids);
self.detach_all(node_ids);
match viable_next {
Some(next_id) => self.insert_nodes_before(next_id, node_ids),
None => self.append_children(parent_id, node_ids),
}
}
pub fn replace_with_nodes(&mut self, anchor_id: NodeId, node_ids: &[NodeId]) {
let Some(parent_id) = self.parent_id(anchor_id) else {
return;
};
let viable_next = self.viable_sibling(anchor_id, 1, node_ids);
self.detach_all(node_ids);
if self.node_has_parent(anchor_id) {
self.replace_node_with(anchor_id, node_ids);
} else {
match viable_next {
Some(next_id) => self.insert_nodes_before(next_id, node_ids),
None => self.append_children(parent_id, node_ids),
}
}
}
}
impl<'doc> DocumentMutator<'doc> {
pub fn flush(&mut self) {
if let Some(old) = self.previous_focus_path.take() {
let new = self
.doc
.maybe_node_element_ancestors(self.doc.focus_node_id);
self.doc.update_focus_within_paths(old, new);
}
if self.recompute_is_animating {
self.doc.has_canvas = self.doc.compute_has_canvas();
}
if let Some(id) = self.title_node {
let title = self.doc.nodes[id].text_content();
self.doc.shell_provider.set_window_title(title);
}
for id in self.style_nodes.drain() {
self.doc.process_style_element(id);
}
for id in self.form_nodes.drain() {
self.doc.reset_form_owner(id);
self.doc.sync_placeholder_state(id);
}
#[cfg(feature = "autofocus")]
if let Some(node_id) = self.node_to_autofocus.take() {
if self.doc.get_node(node_id).is_some() {
self.doc.set_focus_to(node_id);
}
}
}
pub fn set_inner_html(&mut self, node_id: NodeId, html: &str) {
self.remove_and_drop_all_children(node_id);
self.doc
.html_parser_provider
.clone()
.parse_inner_html(self, node_id, html);
}
fn flush_eager_ops(&mut self) {
let mut ops = mem::take(&mut self.eager_op_queue);
for op in ops.drain(0..) {
match op {
SpecialOp::LoadImage(node_id) => self.load_image(node_id),
SpecialOp::LoadIframe(node_id) => self.load_iframe(node_id),
SpecialOp::LoadStylesheet(node_id) => self.load_linked_stylesheet(node_id),
SpecialOp::UnloadStylesheet(node_id) => self.unload_stylesheet(node_id),
SpecialOp::LoadCustomPaintSource(node_id) => self.load_custom_paint_src(node_id),
SpecialOp::ProcessButtonInput(node_id) => self.process_button_input(node_id),
SpecialOp::UnloadSubDocument(node_id) => self.remove_sub_document(node_id),
#[cfg(feature = "custom-widget")]
SpecialOp::UnloadCustomWidget(node_id) => self.remove_custom_widget(node_id),
}
}
self.eager_op_queue = ops;
}
fn process_added_subtree(&mut self, node_id: NodeId) {
self.doc.iter_subtree_mut(node_id, |node_id, doc| {
let node = &mut doc.nodes[node_id];
node.flags.set(NodeFlags::IS_IN_DOCUMENT, true);
node.insert_damage(ALL_DAMAGE);
if let Some(id_attr) = node.attr(local_name!("id")).map(ToString::to_string) {
doc.add_to_id_map(&id_attr, node_id);
}
let node = &mut doc.nodes[node_id];
let NodeData::Element(ref mut element) = node.data else {
return;
};
if element
.background_images
.iter()
.chain(&element.mask_images)
.flatten()
.any(|image| image.status == crate::node::Status::Error)
{
doc.pending_style_image_nodes.push(node_id);
}
#[cfg(feature = "custom-widget")]
let has_custom_widget = element.custom_widget_data().is_some();
#[cfg(not(feature = "custom-widget"))]
let has_custom_widget = false;
let tag = element.name.local.as_ref();
match tag {
"title" => self.title_node = Some(node_id),
"style" => {
self.style_nodes.insert(node_id);
}
_ if has_custom_widget => {}
"link" => self.eager_op_queue.push(SpecialOp::LoadStylesheet(node_id)),
"img" => self.eager_op_queue.push(SpecialOp::LoadImage(node_id)),
"iframe" => self.eager_op_queue.push(SpecialOp::LoadIframe(node_id)),
"canvas" => self
.eager_op_queue
.push(SpecialOp::LoadCustomPaintSource(node_id)),
"button" | "fieldset" | "input" | "select" | "textarea" | "object" | "output" => {
self.eager_op_queue
.push(SpecialOp::ProcessButtonInput(node_id));
self.form_nodes.insert(node_id);
}
_ => {}
}
#[cfg(feature = "autofocus")]
if node.is_focussable() {
if let NodeData::Element(ref element) = node.data {
if let Some(value) = element.attr(local_name!("autofocus")) {
if value != "false" {
self.node_to_autofocus = Some(node_id);
}
}
}
}
});
self.flush_eager_ops();
}
fn process_removed_subtree(&mut self, node_id: NodeId) {
self.doc.iter_subtree_mut(node_id, |node_id, doc| {
doc.retire_top_layer(node_id);
doc.nodes[node_id]
.flags
.set(NodeFlags::IS_IN_DOCUMENT, false);
doc.clear_interaction_state_for_removed_node(node_id);
let node = &mut doc.nodes[node_id];
if doc.text_selection.anchor.node_or_parent == Some(node_id)
|| doc.text_selection.focus.node_or_parent == Some(node_id)
{
doc.text_selection.clear();
}
if node.has_snapshot() {
let opaque_id = style::dom::TNode::opaque(&&*node);
doc.snapshots.remove(&opaque_id);
node.set_has_snapshot(false);
}
if let Some(id_attr) = node.attr(local_name!("id")).map(ToString::to_string) {
doc.remove_from_id_map(&id_attr, node_id);
}
let node = &mut doc.nodes[node_id];
let NodeData::Element(ref mut element) = node.data else {
return;
};
match &element.special_data {
SpecialElementData::SubDocument(_) => {
self.eager_op_queue
.push(SpecialOp::UnloadSubDocument(node_id));
}
#[cfg(feature = "custom-widget")]
SpecialElementData::CustomWidget(_) => {
self.eager_op_queue
.push(SpecialOp::UnloadCustomWidget(node_id));
}
SpecialElementData::Stylesheet(_) => self
.eager_op_queue
.push(SpecialOp::UnloadStylesheet(node_id)),
SpecialElementData::Image(_) => {}
SpecialElementData::Canvas(_) => {
self.recompute_is_animating = true;
}
SpecialElementData::TableRoot(_) => {}
SpecialElementData::TextInput(_) => {}
SpecialElementData::CheckboxInput(_) => {}
#[cfg(feature = "file-input")]
SpecialElementData::FileInput(_) => {}
SpecialElementData::None => {}
}
});
self.flush_eager_ops();
}
fn maybe_record_node(&mut self, node_id: impl Into<Option<NodeId>>) {
let Some(node_id) = node_id.into() else {
return;
};
let Some(tag_name) = self.doc.nodes[node_id]
.data
.downcast_element()
.map(|elem| &elem.name.local)
else {
return;
};
match tag_name.as_ref() {
"title" => self.title_node = Some(node_id),
"style" => {
self.style_nodes.insert(node_id);
}
_ => {}
}
}
fn load_linked_stylesheet(&mut self, target_id: NodeId) {
let node = &self.doc.nodes[target_id];
let mut is_in_head = false;
let mut parent_id = node.parent;
while let Some(id) = parent_id
&& !is_in_head
{
let parent = &self.doc.nodes[id];
is_in_head |= parent.data.is_element_with_tag_name(&local_name!("head"));
parent_id = parent.parent;
}
let rel_attr = node.attr(local_name!("rel"));
let href_attr = node.attr(local_name!("href"));
let (Some(rels), Some(href)) = (rel_attr, href_attr) else {
return;
};
if !rels.split_ascii_whitespace().any(|rel| rel == "stylesheet") {
return;
}
let Some(url) = self.doc.url.resolve_relative(href) else {
return;
};
let Some(handler) = ResourceHandler::new(
self.doc.tx.clone(),
self.doc.id(),
Some(node.id),
self.doc.shell_provider.clone(),
StylesheetHandler {
guard: self.doc.guard.clone(),
net_provider: self.doc.net_provider.clone(),
abort_signal: self.doc.abort_signal.clone(),
},
) else {
return;
};
if is_in_head && !self.doc.net_provider.is_noop() {
self.doc
.pending_critical_resources
.insert(handler.request_id());
}
self.doc.net_provider.fetch(
self.doc.id(),
self.doc.build_request(url),
Box::new(handler),
);
}
fn unload_stylesheet(&mut self, node_id: NodeId) {
let node = &mut self.doc.nodes[node_id];
let Some(element) = node.element_data_mut() else {
unreachable!();
};
let SpecialElementData::Stylesheet(stylesheet) = element.special_data.take() else {
unreachable!();
};
let guard = self.doc.guard.read();
self.doc.stylist.remove_stylesheet(stylesheet, &guard);
self.doc
.stylist
.force_stylesheet_origins_dirty(OriginSet::all());
self.doc.nodes_to_stylesheet.remove(&node_id);
self.doc.stylesheet_generation += 1;
}
fn load_image(&mut self, target_id: NodeId) {
let Some(request) = self.doc.image_observations.begin(target_id) else {
self.doc.resource_counters.rejected =
self.doc.resource_counters.rejected.saturating_add(1);
return;
};
self.doc.cancel_replaced_image_consumers(target_id);
if self.doc.net_provider.is_noop() {
self.doc.image_observations.complete(
target_id,
request,
crate::ImageLoadStatus::Failed,
);
return;
}
let node = &self.doc.nodes[target_id];
if let Some(raw_src) = node.attr(local_name!("src")) {
if !raw_src.is_empty() {
let Some(src) = self.doc.url.resolve_relative(raw_src) else {
self.doc.resource_counters.failures =
self.doc.resource_counters.failures.saturating_add(1);
self.doc.image_observations.complete(
target_id,
request,
crate::ImageLoadStatus::Failed,
);
return;
};
let src_string = src.as_str();
if let Some(cached_image) = self.doc.image_cache.get(src_string) {
#[cfg(feature = "tracing")]
tracing::info!("Loading image {src_string} from cache");
let node = &mut self.doc.nodes[target_id];
node.element_data_mut().unwrap().special_data =
SpecialElementData::Image(Box::new(cached_image.clone()));
node.clear_layout_cache();
node.insert_damage(ALL_DAMAGE);
self.doc.image_observations.complete(
target_id,
request,
crate::ImageLoadStatus::Loaded,
);
return;
}
if let Some(waiting_list) = self.doc.pending_images.get_mut(src_string) {
#[cfg(feature = "tracing")]
tracing::info!("Image {src_string} already pending, queueing node {target_id}");
if waiting_list.waiters.len() < self.doc.resource_limits.image_waiters {
waiting_list
.waiters
.push((target_id, ImageType::Image, Some(request)));
} else {
self.doc.resource_counters.rejected =
self.doc.resource_counters.rejected.saturating_add(1);
self.doc.image_observations.complete(
target_id,
request,
crate::ImageLoadStatus::Failed,
);
}
return;
}
#[cfg(feature = "tracing")]
tracing::info!("Fetching image {src_string}");
if self.doc.pending_images.len() >= self.doc.resource_limits.pending_images
|| self.doc.resource_limits.image_waiters == 0
{
self.doc.resource_counters.rejected =
self.doc.resource_counters.rejected.saturating_add(1);
self.doc.image_observations.complete(
target_id,
request,
crate::ImageLoadStatus::Failed,
);
return;
}
let Some(handler) = ResourceHandler::new(
self.doc.tx.clone(),
self.doc.id(),
None,
self.doc.shell_provider.clone(),
ImageHandler::with_limits(ImageType::Image, self.doc.resource_limits),
) else {
self.doc.image_observations.complete(
target_id,
request,
crate::ImageLoadStatus::Failed,
);
return;
};
self.doc.pending_images.insert(
src_string.to_string(),
crate::resources::PendingImage {
request_id: handler.request_id(),
waiters: vec![(target_id, ImageType::Image, Some(request))],
},
);
self.doc.net_provider.fetch(
self.doc.id(),
self.doc.build_request(src),
Box::new(handler),
);
return;
}
}
self.doc
.image_observations
.complete(target_id, request, crate::ImageLoadStatus::Failed);
}
fn load_iframe(&mut self, target_id: NodeId) {
if self.doc.subdocument_depth >= crate::iframe::MAX_SUBDOCUMENT_DEPTH {
#[cfg(feature = "tracing")]
tracing::warn!(
"Not loading iframe: max sub-document nesting depth ({}) reached",
crate::iframe::MAX_SUBDOCUMENT_DEPTH
);
return;
}
let node = &self.doc.nodes[target_id];
let Some(element) = node.element_data() else {
return;
};
if let Some(srcdoc) = element.attr(local_name!("srcdoc")) {
let srcdoc = srcdoc.to_string();
self.doc.load_iframe_srcdoc(target_id, &srcdoc);
return;
}
let Some(raw_src) = element.attr(local_name!("src")) else {
return;
};
if raw_src.is_empty() {
return;
}
let Some(url) = self.doc.url.resolve_relative(raw_src) else {
#[cfg(feature = "tracing")]
tracing::warn!("Not loading iframe: could not resolve url {raw_src}");
return;
};
self.doc.start_iframe_load(target_id, url);
}
fn load_custom_paint_src(&mut self, target_id: NodeId) {
let node = &mut self.doc.nodes[target_id];
if let Some(raw_src) = node.attr(local_name!("src")) {
if let Ok(custom_paint_source_id) = raw_src.parse::<u64>() {
self.recompute_is_animating = true;
let canvas_data = SpecialElementData::Canvas(CanvasData {
custom_paint_source_id,
});
node.element_data_mut().unwrap().special_data = canvas_data;
}
}
}
fn process_button_input(&mut self, target_id: NodeId) {
let node = &self.doc.nodes[target_id];
let Some(data) = node.element_data() else {
return;
};
let tagname = data.name.local.as_ref();
let type_attr = data.attr(local_name!("type"));
let value = data.attr(local_name!("value"));
if let ("input", Some("button" | "submit" | "reset"), Some(value)) =
(tagname, type_attr, value)
{
let value = value.to_string();
let id = self.create_text_node(&value);
self.append_children(target_id, &[id]);
return;
}
#[cfg(feature = "file-input")]
if let ("input", Some("file")) = (tagname, type_attr) {
let button_id = self.create_element(
qual_name!("button", html),
vec![
Attribute {
name: qual_name!("type", html),
value: "button".to_string(),
},
Attribute {
name: qual_name!("tabindex", html),
value: "-1".to_string(),
},
],
);
let label_id = self.create_element(qual_name!("label", html), vec![]);
let text_id = self.create_text_node("No File Selected");
let button_text_id = self.create_text_node("Browse");
self.append_children(target_id, &[button_id, label_id]);
self.append_children(label_id, &[text_id]);
self.append_children(button_id, &[button_text_id]);
}
}
}
fn set_input_checked_state(element: &mut ElementData, value: String) {
let Ok(checked) = value.parse() else {
return;
};
match element.special_data {
SpecialElementData::CheckboxInput(_) => element.set_checkbox_input_checked(checked),
SpecialElementData::None => element.attrs.push(Attribute {
name: qual_name!("checked", html),
value: checked.to_string(),
}),
_ => {}
}
}
pub struct ViewportMut<'doc> {
doc: &'doc mut BaseDocument,
initial_viewport: Viewport,
}
impl ViewportMut<'_> {
pub fn new(doc: &mut BaseDocument) -> ViewportMut<'_> {
let initial_viewport = doc.viewport.clone();
ViewportMut {
doc,
initial_viewport,
}
}
}
impl Deref for ViewportMut<'_> {
type Target = Viewport;
fn deref(&self) -> &Self::Target {
&self.doc.viewport
}
}
impl DerefMut for ViewportMut<'_> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.doc.viewport
}
}
impl Drop for ViewportMut<'_> {
fn drop(&mut self) {
if self.doc.viewport == self.initial_viewport {
return;
}
let changes = DeviceChanges::from_viewports(&self.initial_viewport, &self.doc.viewport);
self.doc.queue_device_changes(changes);
}
}
#[cfg(test)]
mod test {
use style::media_queries::MediaType;
use style::servo_arc::Arc as ServoArc;
use style_dom::ElementState;
use std::sync::{
Arc,
atomic::{AtomicUsize, Ordering},
};
use apace_traits::shell::{ColorScheme, ShellProvider, Viewport};
use super::{DocumentMutator, NodeId};
use crate::{Attribute, BaseDocument, DocumentConfig, ElementData, NodeData, qual_name};
#[cfg(feature = "custom-widget")]
#[test]
fn custom_widget_replays_initial_attributes() {
use std::sync::Mutex;
type AttributeChange = (String, Option<String>, Option<String>);
struct RecordingWidget(Arc<Mutex<Vec<AttributeChange>>>);
impl crate::Widget for RecordingWidget {
fn attribute_changed(&mut self, name: &str, old: Option<&str>, new: Option<&str>) {
self.0.lock().unwrap().push((
name.to_owned(),
old.map(str::to_owned),
new.map(str::to_owned),
));
}
}
for via_mutator in [false, true] {
for initially_empty in [false, true] {
let mut document = BaseDocument::new(DocumentConfig::default());
let attrs = if initially_empty {
vec![]
} else {
vec![
Attribute {
name: qual_name!("title"),
value: "initial".into(),
},
Attribute {
name: qual_name!("lang"),
value: "".into(),
},
]
};
let id = document.create_node(NodeData::Element(Box::new(ElementData::new(
qual_name!("div"),
attrs,
))));
let calls = Arc::new(Mutex::new(Vec::new()));
let widget = Box::new(RecordingWidget(Arc::clone(&calls)));
if via_mutator {
document.mutate().set_custom_widget(id, widget);
} else {
document.set_custom_widget(id, widget);
}
let mut expected = if initially_empty {
vec![]
} else {
vec![
("title".into(), None, Some("initial".into())),
("lang".into(), None, Some("".into())),
]
};
assert_eq!(*calls.lock().unwrap(), expected);
let mut mutator = document.mutate();
mutator.set_attribute(id, qual_name!("title"), "updated");
mutator.clear_attribute(id, qual_name!("title"));
expected.extend([
(
"title".into(),
(!initially_empty).then(|| "initial".into()),
Some("updated".into()),
),
("title".into(), Some("updated".into()), None),
]);
assert_eq!(*calls.lock().unwrap(), expected);
}
}
}
#[test]
fn media_type_defaults_to_screen() {
let mut document = BaseDocument::new(DocumentConfig::default());
assert_eq!(*document.media_type(), MediaType::screen());
assert_eq!(document.stylist_device().media_type(), MediaType::screen());
}
#[test]
fn media_type_honors_config() {
let mut document = BaseDocument::new(DocumentConfig {
media_type: Some(MediaType::print()),
..Default::default()
});
assert_eq!(*document.media_type(), MediaType::print());
assert_eq!(document.stylist_device().media_type(), MediaType::print());
}
#[test]
fn set_media_type_updates_stylist_device() {
let mut document = BaseDocument::new(DocumentConfig::default());
assert_eq!(document.stylist_device().media_type(), MediaType::screen());
document.set_media_type(MediaType::print());
assert_eq!(*document.media_type(), MediaType::print());
assert_eq!(document.stylist_device().media_type(), MediaType::print());
}
#[test]
fn mutator_remove_disabled() {
let mut document = BaseDocument::new(DocumentConfig::default());
let id = document.create_node(NodeData::Element(Box::new(ElementData::new(
qual_name!("button"),
vec![Attribute {
name: qual_name!("disabled"),
value: "".into(),
}],
))));
let node = document.get_node(id).unwrap();
assert!(
node.element_state().contains(ElementState::DISABLED),
"form node is disabled"
);
assert!(
!node.element_state().contains(ElementState::ENABLED),
"form node is not enabled yet"
);
let mut mutator = document.mutate();
mutator.clear_attribute(id, qual_name!("disabled"));
drop(mutator);
let node = document.get_node(id).unwrap();
assert!(
!node.element_state().contains(ElementState::DISABLED),
"form node is no longer disabled"
);
assert!(
node.element_state().contains(ElementState::ENABLED),
"form node is enabled"
);
}
#[test]
fn mutator_set_disabled() {
let mut document = BaseDocument::new(DocumentConfig::default());
let id = document.create_node(NodeData::Element(Box::new(ElementData::new(
qual_name!("button"),
vec![],
))));
let node = document.get_node(id).unwrap();
assert!(
!node.element_state().contains(ElementState::DISABLED),
"form node is not disabled"
);
assert!(
node.element_state().contains(ElementState::ENABLED),
"form node is enabled"
);
let mut mutator = document.mutate();
mutator.set_attribute(id, qual_name!("disabled"), "");
drop(mutator);
let node = document.get_node(id).unwrap();
assert!(
node.element_state().contains(ElementState::DISABLED),
"form node is disabled"
);
assert!(
!node.element_state().contains(ElementState::ENABLED),
"form node is no longer enabled enabled"
);
}
#[test]
fn mutator_set_disabled_invalid_node() {
let mut document = BaseDocument::new(DocumentConfig::default());
let id = document.create_node(NodeData::Element(Box::new(ElementData::new(
qual_name!("a"),
vec![],
))));
let node = document.get_node(id).unwrap();
assert!(
!node.element_state().contains(ElementState::DISABLED),
"form node is not disabled"
);
assert!(
!node.element_state().contains(ElementState::ENABLED),
"form node is enabled"
);
let mut mutator = document.mutate();
mutator.set_attribute(id, qual_name!("disabled"), "");
drop(mutator);
let node = document.get_node(id).unwrap();
assert!(
!node.element_state().contains(ElementState::DISABLED),
"form node is not disabled"
);
assert!(
!node.element_state().contains(ElementState::ENABLED),
"form node is enabled"
);
}
#[test]
fn mutator_id_attribute_updates_id_map() {
let mut document = BaseDocument::new(DocumentConfig::default());
let root_id = document.root_node().id;
let node_id = {
let mut mutator = document.mutate();
let node_id = mutator.create_element(
qual_name!("div"),
vec![Attribute {
name: qual_name!("id"),
value: "old".into(),
}],
);
mutator.append_children(root_id, &[node_id]);
node_id
};
assert_eq!(document.get_element_by_id("old"), Some(node_id));
{
let mut mutator = document.mutate();
mutator.set_attribute(node_id, qual_name!("id"), "new");
}
assert_eq!(document.get_element_by_id("new"), Some(node_id));
assert_eq!(document.get_element_by_id("old"), None);
{
let mut mutator = document.mutate();
mutator.clear_attribute(node_id, qual_name!("id"));
}
assert_eq!(document.get_element_by_id("new"), None);
}
#[test]
fn get_element_by_id_duplicate_ids_first_in_tree_order_wins() {
let mut document = BaseDocument::new(DocumentConfig::default());
let root_id = document.root_node().id;
let (first_id, second_id) = {
let mut mutator = document.mutate();
let first_id = mutator.create_element(qual_name!("div"), vec![]);
let second_id = mutator.create_element(qual_name!("div"), vec![]);
mutator.append_children(root_id, &[first_id, second_id]);
mutator.set_attribute(second_id, qual_name!("id"), "dup");
mutator.set_attribute(first_id, qual_name!("id"), "dup");
(first_id, second_id)
};
assert_eq!(document.get_element_by_id("dup"), Some(first_id));
{
let mut mutator = document.mutate();
mutator.remove_node(first_id);
}
assert_eq!(document.get_element_by_id("dup"), Some(second_id));
}
#[derive(Default)]
struct RedrawShell {
redraw_requests: AtomicUsize,
}
impl ShellProvider for RedrawShell {
fn request_redraw(&self) {
self.redraw_requests.fetch_add(1, Ordering::Relaxed);
}
}
#[test]
fn mutator_requests_redraw_only_after_mutation() {
let shell = Arc::new(RedrawShell::default());
let mut document = BaseDocument::new(DocumentConfig {
shell_provider: Some(shell.clone()),
..Default::default()
});
let root_id = document.root_node().id;
{
let mut mutator = document.mutate();
let parent_id = mutator.create_element(qual_name!("div"), vec![]);
let child_id = mutator.create_element(qual_name!("span"), vec![]);
mutator.append_children(parent_id, &[child_id]);
mutator.remove_and_drop_all_children(parent_id);
mutator.set_attribute(parent_id, qual_name!("id"), "detached");
}
assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 0);
{
let mutator = document.mutate();
assert_eq!(mutator.child_ids(root_id).len(), 0);
}
{
let mut mutator = document.mutate();
let node_id = mutator.create_element(qual_name!("div"), vec![]);
mutator.append_children(root_id, &[node_id]);
mutator.set_attribute(node_id, qual_name!("id"), "in-document");
}
assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 1);
{
let mut mutator = document.mutate();
let parent_id = mutator.create_element(qual_name!("div"), vec![]);
let child_id = mutator.create_element(qual_name!("span"), vec![]);
mutator.append_children(root_id, &[parent_id]);
mutator.append_children(parent_id, &[child_id]);
mutator.remove_and_drop_all_children(parent_id);
}
assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 2);
{
let mut mutator = document.mutate();
let parent_id = mutator.create_element(qual_name!("div"), vec![]);
let child_id = mutator.create_element(qual_name!("span"), vec![]);
let detached_target_id = mutator.create_element(qual_name!("div"), vec![]);
mutator.append_children(root_id, &[parent_id]);
mutator.append_children(parent_id, &[child_id]);
assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 2);
mutator.append_children(detached_target_id, &[child_id]);
}
assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 3);
}
#[test]
fn moving_subtree_out_of_document_clears_in_document_flag() {
let shell = Arc::new(RedrawShell::default());
let mut document = BaseDocument::new(DocumentConfig {
shell_provider: Some(shell.clone()),
..Default::default()
});
let root_id = document.root_node().id;
let (child_id, grandchild_id, detached_parent_id) = {
let mut mutator = document.mutate();
let in_document_parent_id = mutator.create_element(qual_name!("div"), vec![]);
let child_id = mutator.create_element(qual_name!("div"), vec![]);
let grandchild_id = mutator.create_element(qual_name!("span"), vec![]);
let detached_parent_id = mutator.create_element(qual_name!("section"), vec![]);
mutator.append_children(root_id, &[in_document_parent_id]);
mutator.append_children(in_document_parent_id, &[child_id]);
mutator.append_children(child_id, &[grandchild_id]);
(child_id, grandchild_id, detached_parent_id)
};
assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 1);
assert!(document.get_node(child_id).unwrap().flags.is_in_document());
assert!(
document
.get_node(grandchild_id)
.unwrap()
.flags
.is_in_document()
);
{
let mut mutator = document.mutate();
mutator.append_children(detached_parent_id, &[child_id]);
}
assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 2);
assert!(!document.get_node(child_id).unwrap().flags.is_in_document());
assert!(
!document
.get_node(grandchild_id)
.unwrap()
.flags
.is_in_document()
);
{
let mut mutator = document.mutate();
mutator.set_attribute(child_id, qual_name!("id"), "detached");
}
assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 2);
{
let mut mutator = document.mutate();
mutator.append_children(root_id, &[child_id]);
}
assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 3);
assert!(document.get_node(child_id).unwrap().flags.is_in_document());
assert!(
document
.get_node(grandchild_id)
.unwrap()
.flags
.is_in_document()
);
}
#[test]
fn style_property_updates_nested_layout() {
let mut document = BaseDocument::new(DocumentConfig {
viewport: Some(Viewport::new(800, 600, 1.0, ColorScheme::Light)),
..Default::default()
});
let root_id = document.root_node().id;
let mover_id = {
let mut mutator = document.mutate();
let parent_id = mutator.create_element(qual_name!("div"), vec![]);
let mover_id = mutator.create_element(qual_name!("div"), vec![]);
mutator.set_style_property(parent_id, "position", "relative");
mutator.set_style_property(parent_id, "width", "800px");
mutator.set_style_property(parent_id, "height", "600px");
mutator.set_style_property(mover_id, "position", "absolute");
mutator.set_style_property(mover_id, "left", "0px");
mutator.set_style_property(mover_id, "top", "0px");
mutator.append_children(parent_id, &[mover_id]);
mutator.append_children(root_id, &[parent_id]);
mover_id
};
document.resolve(0.0);
assert_eq!(
document
.get_node(mover_id)
.unwrap()
.final_layout()
.location
.x,
0.0
);
{
let mut mutator = document.mutate();
mutator.set_style_property(mover_id, "left", "120px");
}
document.resolve(0.0);
assert_eq!(
document
.get_node(mover_id)
.unwrap()
.final_layout()
.location
.x,
120.0
);
}
#[test]
fn hit_testing_before_the_first_resolve() {
let mut document = BaseDocument::new(DocumentConfig {
viewport: Some(Viewport::new(800, 600, 1.0, ColorScheme::Light)),
..Default::default()
});
let root_id = document.root_node().id;
{
let mut mutator = document.mutate();
let html = mutator.create_element(qual_name!("html", html), vec![]);
mutator.append_children(root_id, &[html]);
}
assert!(document.hit(10.0, 10.0).is_none());
}
#[test]
fn style_property_mutation_copies_blocks_in_rule_tree() {
let mut document = BaseDocument::new(DocumentConfig {
viewport: Some(Viewport::new(800, 600, 1.0, ColorScheme::Light)),
..Default::default()
});
let root_id = document.root_node().id;
let div_id = {
let mut mutator = document.mutate();
let div_id = mutator.create_element(qual_name!("div"), vec![]);
mutator.set_style_property(div_id, "width", "100px");
mutator.set_style_property(div_id, "height", "50px");
mutator.append_children(root_id, &[div_id]);
div_id
};
let style_block = |document: &BaseDocument| {
document
.get_node(div_id)
.unwrap()
.element_data()
.unwrap()
.style_attribute
.clone()
.unwrap()
};
let before_resolve = style_block(&document);
{
let mut mutator = document.mutate();
mutator.set_style_property(div_id, "height", "60px");
}
assert!(ServoArc::ptr_eq(&before_resolve, &style_block(&document)));
document.resolve(0.0);
let in_rule_tree = style_block(&document);
assert!(
in_rule_tree
.read_with(&document.guard.read())
.immutable
.load(Ordering::Relaxed)
);
{
let mut mutator = document.mutate();
mutator.set_style_property(div_id, "width", "200px");
}
let after_set = style_block(&document);
assert!(!ServoArc::ptr_eq(&in_rule_tree, &after_set));
{
let guard = document.guard.read();
let old = in_rule_tree.read_with(&guard);
let new = after_set.read_with(&guard);
assert_eq!(old.declarations().len(), 2);
assert_eq!(new.declarations().len(), 2);
assert_ne!(old, new);
}
document.resolve(0.0);
assert_eq!(
document.get_node(div_id).unwrap().final_layout().size.width,
200.0
);
let in_rule_tree = style_block(&document);
{
let mut mutator = document.mutate();
mutator.remove_style_property(div_id, "height");
}
let after_remove = style_block(&document);
assert!(!ServoArc::ptr_eq(&in_rule_tree, &after_remove));
{
let guard = document.guard.read();
assert_eq!(in_rule_tree.read_with(&guard).declarations().len(), 2);
assert_eq!(after_remove.read_with(&guard).declarations().len(), 1);
}
}
fn qual_name(name: &str) -> crate::QualName {
crate::QualName::new(None, crate::ns!(), crate::LocalName::from(name))
}
fn attr(name: &str, value: &str) -> Attribute {
Attribute {
name: qual_name(name),
value: value.to_string(),
}
}
fn div(m: &mut DocumentMutator<'_>, attrs: &[(&str, &str)]) -> NodeId {
let attrs = attrs.iter().map(|(k, v)| attr(k, v)).collect();
m.create_element(qual_name!("div"), attrs)
}
type Snapshot = Vec<(String, [f32; 4], Vec<String>)>;
fn snapshot(doc: &BaseDocument) -> Snapshot {
let mut out = Vec::new();
let mut stack = vec![(doc.root_node().id, "")];
while let Some((id, pseudo)) = stack.pop() {
let node = doc.get_node(id).unwrap();
if let Some(el) = node.element_data() {
let l = node.final_layout();
let rect = [l.location.x, l.location.y, l.size.width, l.size.height];
let style = crate::resolved_style_property_names()
.iter()
.map(|name| doc.resolved_style_value(id, name))
.collect();
out.push((format!("{}{pseudo}", el.name.local), rect, style));
}
stack.extend(node.after().map(|id| (id, "::after")));
stack.extend(node.children.iter().rev().map(|id| (*id, "")));
stack.extend(node.before().map(|id| (id, "::before")));
}
out
}
fn assert_matches_fresh(incremental: &Snapshot, fresh: &Snapshot) {
let boxes = |s: &Snapshot| -> Vec<(String, [f32; 4])> {
s.iter().map(|(n, r, _)| (n.clone(), *r)).collect()
};
assert_eq!(
boxes(incremental),
boxes(fresh),
"incremental layout differs from a fresh layout of the mutated document"
);
for (i, ((name, _, inc), (_, _, fre))) in incremental.iter().zip(fresh).enumerate() {
let differing: Vec<String> = crate::resolved_style_property_names()
.iter()
.zip(inc.iter().zip(fre))
.filter(|(_, (a, b))| a != b)
.map(|(property, (a, b))| format!("{property}: {a:?} vs {b:?}"))
.collect();
assert!(
differing.is_empty(),
"resolved style of {name} (element {i}) differs from a fresh layout of the mutated document: {}",
differing.join(", ")
);
}
}
fn check_incremental_restyle(
css: &str,
build: impl Fn(&mut DocumentMutator<'_>) -> Vec<NodeId>,
mutate: impl Fn(&mut DocumentMutator<'_>, &[NodeId]),
) {
check_incremental_restyle_steps(css, build, &[&mutate]);
}
type Step<'a> = &'a dyn Fn(&mut DocumentMutator<'_>, &[NodeId]);
fn check_incremental_restyle_steps(
css: &str,
build: impl Fn(&mut DocumentMutator<'_>) -> Vec<NodeId>,
steps: &[Step<'_>],
) {
let new_document = || {
let mut doc = BaseDocument::new(DocumentConfig {
viewport: Some(Viewport::new(800, 600, 1.0, ColorScheme::Light)),
..Default::default()
});
doc.add_user_agent_stylesheet(crate::UserAgentStylesheet::parse(&format!(
"html, div {{ display: block }} {css}"
)));
let root_id = doc.root_node().id;
let ids = {
let mut m = doc.mutate();
let html = m.create_element(qual_name!("html"), vec![]);
m.append_children(root_id, &[html]);
build(&mut m)
};
(doc, ids)
};
let (mut incremental, ids) = new_document();
incremental.resolve(0.0);
let before = snapshot(&incremental);
for mutate in steps {
mutate(&mut incremental.mutate(), &ids);
incremental.resolve(0.0);
}
let after = snapshot(&incremental);
let (mut fresh, ids) = new_document();
for mutate in steps {
mutate(&mut fresh.mutate(), &ids);
}
fresh.resolve(0.0);
assert_matches_fresh(&after, &snapshot(&fresh));
assert_ne!(
before, after,
"the mutation should change the layout or styles"
);
}
fn parent_with_children(m: &mut DocumentMutator<'_>, count: usize) -> Vec<NodeId> {
let root_id = m.doc.root_element().id;
let parent = div(m, &[]);
let children: Vec<NodeId> = (0..count).map(|_| div(m, &[])).collect();
m.append_children(parent, &children);
m.append_children(root_id, &[parent]);
std::iter::once(parent).chain(children).collect()
}
fn hidden_parent_with_children(m: &mut DocumentMutator<'_>, count: usize) -> Vec<NodeId> {
let ids = parent_with_children(m, count);
m.set_attribute(ids[0], qual_name!("class"), "hidden");
ids
}
const HIDDEN_CSS: &str = "div { height: 10px; width: 100px } .hidden { display: none }";
fn unhide(m: &mut DocumentMutator<'_>, ids: &[NodeId]) {
m.clear_attribute(ids[0], qual_name!("class"));
}
#[test]
fn attribute_change_in_hidden_subtree_then_unhide() {
check_incremental_restyle_steps(
&format!("{HIDDEN_CSS} [data-a] ~ div {{ width: 200px; color: red }}"),
|m| hidden_parent_with_children(m, 3),
&[
&|m, ids| m.set_attribute(ids[1], qual_name("data-a"), "1"),
&unhide,
],
);
}
#[test]
fn class_change_in_hidden_subtree_then_unhide() {
check_incremental_restyle_steps(
&format!("{HIDDEN_CSS} .x {{ width: 200px }} .x div {{ color: red }}"),
|m| {
let ids = hidden_parent_with_children(m, 1);
let grandchild = div(m, &[]);
m.append_children(ids[1], &[grandchild]);
ids
},
&[
&|m, ids| m.set_attribute(ids[1], qual_name!("class"), "x"),
&unhide,
],
);
}
#[test]
fn style_change_in_hidden_subtree_then_unhide() {
check_incremental_restyle_steps(
&format!("{HIDDEN_CSS} .half {{ width: 50% }}"),
|m| {
let ids = hidden_parent_with_children(m, 1);
let grandchild = div(m, &[("class", "half")]);
m.append_children(ids[1], &[grandchild]);
ids
},
&[
&|m, ids| m.set_attribute(ids[1], qual_name!("style"), "width: 400px"),
&unhide,
],
);
}
#[test]
fn insert_in_hidden_subtree_then_unhide() {
check_incremental_restyle_steps(
&format!("{HIDDEN_CSS} div:last-child {{ height: 30px }} div:empty {{ width: 20px }}"),
|m| hidden_parent_with_children(m, 1),
&[
&|m, ids| {
let child = div(m, &[]);
m.append_children(ids[0], &[child]);
},
&unhide,
],
);
}
#[test]
fn remove_in_hidden_subtree_then_unhide() {
check_incremental_restyle_steps(
&format!("{HIDDEN_CSS} div:first-child {{ height: 30px }}"),
|m| hidden_parent_with_children(m, 2),
&[
&|m, ids| {
m.remove_and_drop_node(ids[1]);
},
&unhide,
],
);
}
#[test]
fn pseudo_rule_matched_in_hidden_subtree_then_unhide() {
check_incremental_restyle_steps(
&format!(
"{HIDDEN_CSS} .x > div::before {{ content: 'a'; display: block; height: 20px }}"
),
|m| {
let ids = hidden_parent_with_children(m, 1);
let grandchild = div(m, &[]);
m.append_children(ids[1], &[grandchild]);
ids
},
&[
&|m, ids| m.set_attribute(ids[1], qual_name!("class"), "x"),
&unhide,
],
);
}
#[test]
fn unhide_and_attribute_change_in_one_batch() {
check_incremental_restyle(
&format!("{HIDDEN_CSS} [data-a] ~ div {{ width: 200px; color: red }}"),
|m| hidden_parent_with_children(m, 3),
|m, ids| {
unhide(m, ids);
m.set_attribute(ids[1], qual_name("data-a"), "1");
},
);
}
#[test]
fn hide_and_attribute_change_then_unhide() {
check_incremental_restyle_steps(
&format!("{HIDDEN_CSS} [data-a] ~ div {{ width: 200px; color: red }}"),
|m| parent_with_children(m, 3),
&[
&|m, ids| {
m.set_attribute(ids[0], qual_name!("class"), "hidden");
m.set_attribute(ids[1], qual_name("data-a"), "1");
},
&unhide,
],
);
}
#[test]
fn hide_then_mutate_then_unhide() {
check_incremental_restyle_steps(
&format!("{HIDDEN_CSS} .x {{ color: red }} div:empty {{ width: 20px }}"),
|m| parent_with_children(m, 2),
&[
&|m, ids| m.set_attribute(ids[0], qual_name!("class"), "hidden"),
&|m, ids| {
m.set_attribute(ids[1], qual_name!("class"), "x");
m.remove_and_drop_node(ids[2]);
},
&unhide,
],
);
}
#[test]
fn class_change_restyles_descendants() {
check_incremental_restyle(
"div { height: 10px; width: 100px } .wide div { width: 300px }",
|m| parent_with_children(m, 1),
|m, ids| m.set_attribute(ids[0], qual_name!("class"), "wide"),
);
}
#[test]
fn class_removal_restyles_descendants() {
check_incremental_restyle(
"div { height: 10px; width: 100px } .wide div { width: 300px }",
|m| {
let ids = parent_with_children(m, 1);
m.set_attribute(ids[0], qual_name!("class"), "wide");
ids
},
|m, ids| m.clear_attribute(ids[0], qual_name!("class")),
);
}
#[test]
fn id_change_restyles_children() {
check_incremental_restyle(
"div { height: 10px; width: 100px } #x > div { height: 50px }",
|m| parent_with_children(m, 2),
|m, ids| m.set_attribute(ids[0], qual_name!("id"), "x"),
);
}
#[test]
fn attribute_change_restyles_later_siblings() {
check_incremental_restyle(
"div { height: 10px; width: 50px } [data-a] ~ div { width: 200px }",
|m| parent_with_children(m, 3),
|m, ids| m.set_attribute(ids[1], qual_name("data-a"), "1"),
);
}
#[test]
fn style_attribute_change_restyles_inheriting_descendants() {
check_incremental_restyle(
"div { height: 10px } .em { width: 2em }",
|m| {
let root_id = m.doc.root_element().id;
let parent = div(m, &[]);
let child = div(m, &[("class", "em")]);
m.append_children(parent, &[child]);
m.append_children(root_id, &[parent]);
vec![parent, child]
},
|m, ids| m.set_attribute(ids[0], qual_name!("style"), "font-size: 40px"),
);
}
#[test]
fn style_width_change_relayouts_descendants() {
check_incremental_restyle(
"div { height: 10px } .half { width: 50% }",
|m| {
let root_id = m.doc.root_element().id;
let parent = div(m, &[("style", "width: 400px")]);
let child = div(m, &[("class", "half")]);
m.append_children(parent, &[child]);
m.append_children(root_id, &[parent]);
vec![parent, child]
},
|m, ids| m.set_style_property(ids[0], "width", "200px"),
);
}
#[test]
fn appending_a_child_restyles_empty_parent() {
check_incremental_restyle(
"div { height: 10px; width: 100px } div:empty { height: 30px }",
|m| parent_with_children(m, 0),
|m, ids| {
let child = div(m, &[]);
m.append_children(ids[0], &[child]);
},
);
}
#[test]
fn removing_the_last_child_restyles_empty_parent() {
check_incremental_restyle(
"div { height: 10px; width: 100px } div:empty { height: 30px }",
|m| parent_with_children(m, 1),
|m, ids| {
m.remove_and_drop_node(ids[1]);
},
);
}
#[test]
fn inserting_a_child_restyles_nth_child_siblings() {
check_incremental_restyle(
"div { height: 10px; width: 50px } div:nth-child(2n) { width: 200px }",
|m| parent_with_children(m, 3),
|m, ids| {
let child = div(m, &[]);
m.insert_nodes_before(ids[1], &[child]);
},
);
}
#[test]
fn removing_a_child_restyles_nth_child_siblings() {
check_incremental_restyle(
"div { height: 10px; width: 50px } div:nth-child(2n) { width: 200px }",
|m| parent_with_children(m, 3),
|m, ids| {
m.remove_and_drop_node(ids[1]);
},
);
}
#[test]
fn appending_a_child_restyles_previous_last_child() {
check_incremental_restyle(
"div { height: 10px; width: 50px } div:last-child { width: 200px }",
|m| parent_with_children(m, 2),
|m, ids| {
let child = div(m, &[]);
m.append_children(ids[0], &[child]);
},
);
}
#[test]
fn removing_the_first_child_restyles_new_first_child() {
check_incremental_restyle(
"div { height: 10px; width: 50px } div:first-child { width: 200px }",
|m| parent_with_children(m, 2),
|m, ids| {
m.remove_and_drop_node(ids[1]);
},
);
}
#[test]
fn inserting_a_sibling_restyles_later_siblings() {
check_incremental_restyle(
"div { height: 10px; width: 50px } .a ~ div { width: 200px }",
|m| parent_with_children(m, 2),
|m, ids| {
let child = div(m, &[("class", "a")]);
m.insert_nodes_before(ids[1], &[child]);
},
);
}
#[test]
fn style_change_on_inline_element_rebuilds_inline_layout() {
check_incremental_restyle(
"div { width: 300px; font-size: 20px }",
|m| {
let root_id = m.doc.root_element().id;
let block = div(m, &[]);
let before = m.create_text_node("Text is available under the ");
let link = m.create_element(qual_name!("a"), vec![]);
let link_text =
m.create_text_node("Creative Commons Attribution-ShareAlike 4.0 License");
let after = m.create_text_node("; additional terms may apply.");
m.append_children(link, &[link_text]);
m.append_children(block, &[before, link, after]);
m.append_children(root_id, &[block]);
vec![block, link]
},
|m, ids| m.set_attribute(ids[1], qual_name!("style"), "font-size: 40px"),
);
}
#[test]
fn moving_a_child_restyles_both_parents() {
check_incremental_restyle(
"div { height: 10px; width: 50px } div:empty { height: 30px } div:last-child { width: 200px }",
|m| {
let root_id = m.doc.root_element().id;
let a = div(m, &[]);
let b = div(m, &[]);
let child = div(m, &[]);
let other = div(m, &[]);
m.append_children(a, &[child, other]);
m.append_children(root_id, &[a, b]);
vec![a, b, child]
},
|m, ids| m.append_children(ids[1], &[ids[2]]),
);
}
#[test]
fn class_change_recolors_descendants() {
check_incremental_restyle(
"div { height: 10px; width: 100px } .x div { color: red }",
|m| parent_with_children(m, 1),
|m, ids| m.set_attribute(ids[0], qual_name!("class"), "x"),
);
}
const PSEUDO_CSS: &str = "div { height: 10px; width: 100px } \
.x > div::before { content: 'a'; display: block; height: 20px }";
#[test]
fn class_change_creates_before_pseudo_on_children() {
check_incremental_restyle(
PSEUDO_CSS,
|m| parent_with_children(m, 1),
|m, ids| m.set_attribute(ids[0], qual_name!("class"), "x"),
);
}
#[test]
fn class_removal_removes_before_pseudo_on_children() {
check_incremental_restyle(
PSEUDO_CSS,
|m| {
let ids = parent_with_children(m, 1);
m.set_attribute(ids[0], qual_name!("class"), "x");
ids
},
|m, ids| m.clear_attribute(ids[0], qual_name!("class")),
);
}
#[test]
fn attribute_change_creates_after_pseudo_on_later_siblings() {
check_incremental_restyle(
"div { height: 10px; width: 100px } \
[data-a] ~ div::after { content: 'a'; display: block; height: 20px }",
|m| parent_with_children(m, 2),
|m, ids| m.set_attribute(ids[1], qual_name("data-a"), ""),
);
}
#[test]
fn class_change_recolors_before_pseudo_on_children() {
check_incremental_restyle(
"div { height: 10px; width: 100px } \
div::before { content: 'a'; display: block; height: 20px } \
.x > div::before { color: red }",
|m| parent_with_children(m, 1),
|m, ids| m.set_attribute(ids[0], qual_name!("class"), "x"),
);
}
#[test]
fn attribute_change_recolors_after_pseudo_on_later_siblings() {
check_incremental_restyle(
"div { height: 10px; width: 100px } \
div::after { content: 'a'; display: block; height: 20px } \
[data-a] ~ div::after { color: red }",
|m| parent_with_children(m, 2),
|m, ids| m.set_attribute(ids[1], qual_name("data-a"), ""),
);
}
fn check_incremental_restyle_with_focus(
css: &str,
focus: usize,
build: impl Fn(&mut DocumentMutator<'_>) -> Vec<NodeId>,
mutate: impl Fn(&mut DocumentMutator<'_>, &[NodeId]),
) {
let new_document = || {
let mut doc = BaseDocument::new(DocumentConfig {
viewport: Some(Viewport::new(800, 600, 1.0, ColorScheme::Light)),
..Default::default()
});
doc.add_user_agent_stylesheet(crate::UserAgentStylesheet::parse(&format!(
"html, div {{ display: block }} {css}"
)));
let root_id = doc.root_node().id;
let ids = {
let mut m = doc.mutate();
let html = m.create_element(qual_name!("html"), vec![]);
m.append_children(root_id, &[html]);
build(&mut m)
};
doc.set_focus_to(ids[focus]);
(doc, ids)
};
let (mut incremental, ids) = new_document();
incremental.resolve(0.0);
let before = snapshot(&incremental);
mutate(&mut incremental.mutate(), &ids);
incremental.resolve(0.0);
let after = snapshot(&incremental);
let (mut fresh, ids) = new_document();
mutate(&mut fresh.mutate(), &ids);
fresh.resolve(0.0);
assert_matches_fresh(&after, &snapshot(&fresh));
assert_ne!(
before, after,
"the mutation should change the layout or styles"
);
}
fn focus_tree(m: &mut DocumentMutator<'_>) -> Vec<NodeId> {
let root_id = m.doc.root_element().id;
let container = div(m, &[]);
let sibling = div(m, &[]);
let holder = div(m, &[]);
let input = m.create_element(qual_name!("input"), vec![]);
let other = div(m, &[]);
m.append_children(holder, &[input]);
m.append_children(container, &[sibling, holder]);
m.append_children(root_id, &[container, other]);
vec![container, sibling, holder, input, other]
}
#[test]
fn removing_the_focused_subtree_restyles_focus_within_ancestors() {
check_incremental_restyle_with_focus(
"div { height: 10px; width: 100px } div:focus-within { height: 30px }",
3,
focus_tree,
|m, ids| {
m.remove_and_drop_node(ids[0]);
},
);
}
#[test]
fn moving_the_focused_element_restyles_focus_within_ancestors() {
check_incremental_restyle_with_focus(
"div { height: 10px; width: 100px } div:focus-within { height: 30px }",
3,
focus_tree,
|m, ids| m.append_children(ids[4], &[ids[3]]),
);
}
fn parent_with_text_and_children(m: &mut DocumentMutator<'_>, count: usize) -> Vec<NodeId> {
let root_id = m.doc.root_element().id;
let parent = div(m, &[]);
let leading = m.create_text_node(" ");
let children: Vec<NodeId> = (0..count).map(|_| div(m, &[])).collect();
let trailing = m.create_text_node(" ");
m.append_children(parent, &[leading]);
m.append_children(parent, &children);
m.append_children(parent, &[trailing]);
m.append_children(root_id, &[parent]);
std::iter::once(parent).chain(children).collect()
}
#[test]
fn inserting_before_the_first_element_child_restyles_it_across_text() {
check_incremental_restyle(
"div > div:first-child { width: 200px }",
|m| parent_with_text_and_children(m, 2),
|m, ids| {
let new = div(m, &[]);
m.insert_nodes_before(ids[1], &[new]);
},
);
}
#[test]
fn inserting_after_the_last_element_child_restyles_it_across_text() {
check_incremental_restyle(
"div > div:last-child { width: 200px }",
|m| parent_with_text_and_children(m, 2),
|m, ids| {
let new = div(m, &[]);
m.insert_nodes_after(ids[2], &[new]);
},
);
}
#[test]
fn removing_the_first_element_child_restyles_the_next_across_text() {
check_incremental_restyle(
"div > div:first-child { width: 200px }",
|m| parent_with_text_and_children(m, 2),
|m, ids| m.remove_node(ids[1]),
);
}
#[test]
fn removing_the_last_element_child_restyles_the_previous_across_text() {
check_incremental_restyle(
"div > div:last-child { width: 200px }",
|m| parent_with_text_and_children(m, 2),
|m, ids| m.remove_node(ids[2]),
);
}
#[test]
fn removing_the_first_child_restyles_nth_child_siblings() {
check_incremental_restyle(
"div > div:nth-child(2) { width: 200px }",
|m| parent_with_children(m, 3),
|m, ids| m.remove_node(ids[1]),
);
}
#[test]
fn removing_children_front_to_back_restyles_nth_child_siblings() {
check_incremental_restyle(
"div > div:nth-child(2) { width: 200px } div + div { color: red }",
|m| parent_with_text_and_children(m, 4),
|m, ids| {
m.remove_node(ids[1]);
m.remove_node(ids[2]);
},
);
}
#[test]
fn inserting_at_the_front_restyles_adjacent_sibling_rules() {
check_incremental_restyle(
"div + div { color: red } div > div:nth-child(2) { width: 200px }",
|m| parent_with_text_and_children(m, 3),
|m, ids| {
let new = div(m, &[("class", "a")]);
m.insert_nodes_before(ids[1], &[new]);
},
);
}
}