use crate::dom::Dom;
use crate::node_id::NodeId;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum InteractionKind {
Hover,
Focus,
}
#[derive(Debug, Clone)]
pub enum Mutation {
AttributeChanged {
id: NodeId,
name: String,
old: Option<String>,
new: Option<String>,
},
ClassChanged {
id: NodeId,
added: Vec<String>,
removed: Vec<String>,
},
ChildListChanged {
parent: NodeId,
added: Vec<NodeId>,
removed: Vec<NodeId>,
},
CharacterDataChanged {
id: NodeId,
old: String,
new: String,
},
InteractionChanged {
prev: Option<NodeId>,
next: Option<NodeId>,
kind: InteractionKind,
},
SelectionChanged {
prev: Option<crate::Selection>,
next: Option<crate::Selection>,
},
PreDetach {
detached_root: NodeId,
focused: Option<NodeId>,
hovered: Option<NodeId>,
},
}
pub trait MutationObserver<Ext>: 'static {
fn observe(&mut self, dom: &mut Dom<Ext>, record: &Mutation);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ObserverId(pub(crate) u32);
pub(crate) struct ObserverStore<Ext> {
next_id: u32,
entries: Vec<(ObserverId, Box<dyn MutationObserver<Ext>>)>,
}
impl<Ext> Default for ObserverStore<Ext> {
fn default() -> Self {
Self {
next_id: 0,
entries: Vec::new(),
}
}
}
impl<Ext> std::fmt::Debug for ObserverStore<Ext> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ObserverStore")
.field("count", &self.entries.len())
.field("next_id", &self.next_id)
.finish()
}
}
impl<Ext> ObserverStore<Ext> {
pub(crate) fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
impl<Ext: 'static> Dom<Ext> {
pub fn add_mutation_observer(
&mut self,
observer: Box<dyn MutationObserver<Ext>>,
) -> ObserverId {
let id = ObserverId(self.observers.next_id);
self.observers.next_id += 1;
self.observers.entries.push((id, observer));
id
}
pub fn remove_mutation_observer(&mut self, id: ObserverId) -> bool {
let before = self.observers.entries.len();
self.observers.entries.retain(|(oid, _)| *oid != id);
self.observers.entries.len() < before
}
pub fn observer_count(&self) -> usize {
self.observers.entries.len()
}
pub(crate) fn fire_mutation(&mut self, record: Mutation) {
if self.is_observing {
panic!(
"rdom-core: mutation attempted inside MutationObserver callback: {:?}. \
Observers must not mutate the tree during `observe()`. \
Schedule the mutation for after dispatch returns.",
record
);
}
if self.observers.is_empty() {
return;
}
let mut taken = std::mem::take(&mut self.observers.entries);
self.is_observing = true;
for (_, obs) in &mut taken {
obs.observe(self, &record);
}
self.is_observing = false;
if self.observers.entries.is_empty() {
self.observers.entries = taken;
} else {
taken.extend(std::mem::take(&mut self.observers.entries));
self.observers.entries = taken;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Dom;
use std::cell::RefCell;
use std::rc::Rc;
struct Collector {
records: Rc<RefCell<Vec<Mutation>>>,
}
impl MutationObserver<()> for Collector {
fn observe(&mut self, _dom: &mut Dom<()>, record: &Mutation) {
self.records.borrow_mut().push(record.clone());
}
}
fn install_collector(dom: &mut Dom<()>) -> (ObserverId, Rc<RefCell<Vec<Mutation>>>) {
let records = Rc::new(RefCell::new(Vec::new()));
let obs = Box::new(Collector {
records: records.clone(),
});
let id = dom.add_mutation_observer(obs);
(id, records)
}
#[test]
fn add_and_remove_observer() {
let mut dom: Dom = Dom::new();
let (id, _) = install_collector(&mut dom);
assert_eq!(dom.observer_count(), 1);
assert!(dom.remove_mutation_observer(id));
assert_eq!(dom.observer_count(), 0);
assert!(!dom.remove_mutation_observer(id));
}
#[test]
fn no_observers_means_no_fires() {
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
let _ = dom.set_attribute(el, "id", "x");
assert_eq!(dom.observer_count(), 0);
}
#[test]
fn attribute_changed_fires_with_old_new() {
let mut dom: Dom = Dom::new();
let (_, records) = install_collector(&mut dom);
let el = dom.create_element("div");
dom.set_attribute(el, "role", "banner").unwrap();
dom.set_attribute(el, "role", "navigation").unwrap();
dom.remove_attribute(el, "role").unwrap();
let recs = records.borrow();
let matches: Vec<_> = recs
.iter()
.filter_map(|r| match r {
Mutation::AttributeChanged { old, new, .. } => Some((old.clone(), new.clone())),
_ => None,
})
.collect();
assert_eq!(matches.len(), 3);
assert_eq!(matches[0], (None, Some("banner".into())));
assert_eq!(
matches[1],
(Some("banner".into()), Some("navigation".into()))
);
assert_eq!(matches[2], (Some("navigation".into()), None));
}
#[test]
fn class_changed_fires_add_remove_toggle_replace() {
let mut dom: Dom = Dom::new();
let (_, records) = install_collector(&mut dom);
let el = dom.create_element("div");
dom.add_class(el, "active").unwrap();
dom.remove_class(el, "active").unwrap();
dom.toggle_class(el, "on").unwrap(); dom.toggle_class(el, "on").unwrap(); dom.add_class(el, "old").unwrap();
dom.replace_class(el, "old", "new").unwrap();
let cls_recs: Vec<_> = records
.borrow()
.iter()
.filter_map(|r| match r {
Mutation::ClassChanged { added, removed, .. } => {
Some((added.clone(), removed.clone()))
}
_ => None,
})
.collect();
assert_eq!(cls_recs.len(), 6);
assert_eq!(cls_recs[0], (vec!["active".to_string()], vec![]));
assert_eq!(cls_recs[1], (vec![], vec!["active".to_string()]));
assert_eq!(cls_recs[2], (vec!["on".to_string()], vec![]));
assert_eq!(cls_recs[3], (vec![], vec!["on".to_string()]));
assert_eq!(cls_recs[4], (vec!["old".to_string()], vec![]));
assert_eq!(
cls_recs[5],
(vec!["new".to_string()], vec!["old".to_string()])
);
}
#[test]
fn child_list_changed_on_append() {
let mut dom: Dom = Dom::new();
let (_, records) = install_collector(&mut dom);
let parent = dom.create_element("div");
let child = dom.create_element("span");
dom.append_child(parent, child).unwrap();
let tree_recs: Vec<_> = records
.borrow()
.iter()
.filter_map(|r| match r {
Mutation::ChildListChanged {
parent,
added,
removed,
} => Some((*parent, added.clone(), removed.clone())),
_ => None,
})
.collect();
assert_eq!(tree_recs.len(), 1);
assert_eq!(tree_recs[0].0, parent);
assert_eq!(tree_recs[0].1, vec![child]);
assert_eq!(tree_recs[0].2, Vec::<NodeId>::new());
}
#[test]
fn child_list_changed_on_remove() {
let mut dom: Dom = Dom::new();
let parent = dom.create_element("div");
let child = dom.create_element("span");
dom.append_child(parent, child).unwrap();
let (_, records) = install_collector(&mut dom);
dom.remove_child(parent, child).unwrap();
let rec = records
.borrow()
.iter()
.find(|r| matches!(r, Mutation::ChildListChanged { .. }))
.cloned()
.unwrap();
match rec {
Mutation::ChildListChanged { added, removed, .. } => {
assert!(added.is_empty());
assert_eq!(removed, vec![child]);
}
_ => unreachable!(),
}
}
#[test]
fn interaction_changed_fires_on_set_hovered() {
let mut dom: Dom = Dom::new();
let (_, records) = install_collector(&mut dom);
let el = dom.create_element("div");
dom.set_hovered(Some(el));
dom.set_hovered(None);
let interactions: Vec<_> = records
.borrow()
.iter()
.filter_map(|r| match r {
Mutation::InteractionChanged { prev, next, kind } => Some((*prev, *next, *kind)),
_ => None,
})
.collect();
assert_eq!(interactions.len(), 2);
assert_eq!(interactions[0], (None, Some(el), InteractionKind::Hover));
assert_eq!(interactions[1], (Some(el), None, InteractionKind::Hover));
}
#[test]
fn interaction_changed_fires_on_set_focused() {
let mut dom: Dom = Dom::new();
let (_, records) = install_collector(&mut dom);
let a = dom.create_element("a");
let b = dom.create_element("b");
dom.set_focused(Some(a));
dom.set_focused(Some(b));
let interactions: Vec<_> = records
.borrow()
.iter()
.filter_map(|r| match r {
Mutation::InteractionChanged {
kind: InteractionKind::Focus,
prev,
next,
} => Some((*prev, *next)),
_ => None,
})
.collect();
assert_eq!(interactions.len(), 2);
assert_eq!(interactions[0], (None, Some(a)));
assert_eq!(interactions[1], (Some(a), Some(b)));
}
#[test]
fn set_hovered_to_same_does_not_fire() {
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
dom.set_hovered(Some(el));
let (_, records) = install_collector(&mut dom);
dom.set_hovered(Some(el));
assert!(
records
.borrow()
.iter()
.all(|r| !matches!(r, Mutation::InteractionChanged { .. }))
);
}
#[test]
fn re_entrant_mutation_panics() {
struct EvilObserver {
target: NodeId,
}
impl MutationObserver<()> for EvilObserver {
fn observe(&mut self, dom: &mut Dom<()>, _record: &Mutation) {
let _ = dom.set_attribute(self.target, "evil", "1");
}
}
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
dom.add_mutation_observer(Box::new(EvilObserver { target: el }));
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _ = dom.set_attribute(el, "x", "1");
}));
assert!(result.is_err(), "expected panic on re-entrant mutation");
}
#[test]
fn multiple_observers_all_fire() {
let mut dom: Dom = Dom::new();
let (_, r1) = install_collector(&mut dom);
let (_, r2) = install_collector(&mut dom);
let el = dom.create_element("div");
dom.set_attribute(el, "id", "x").unwrap();
assert!(!r1.borrow().is_empty());
assert!(!r2.borrow().is_empty());
}
#[test]
fn unregistered_observer_does_not_fire() {
let mut dom: Dom = Dom::new();
let (id, records) = install_collector(&mut dom);
dom.remove_mutation_observer(id);
let el = dom.create_element("div");
dom.set_attribute(el, "id", "x").unwrap();
assert!(records.borrow().is_empty());
}
#[test]
fn toggle_attribute_fires_twice() {
let mut dom: Dom = Dom::new();
let (_, records) = install_collector(&mut dom);
let el = dom.create_element("input");
dom.toggle_attribute(el, "disabled").unwrap(); dom.toggle_attribute(el, "disabled").unwrap();
let attr_count = records
.borrow()
.iter()
.filter(|r| matches!(r, Mutation::AttributeChanged { .. }))
.count();
assert_eq!(attr_count, 2);
}
}