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);
type ObserverSlot<Ext> = (ObserverId, Option<Box<dyn MutationObserver<Ext>>>);
pub(crate) struct ObserverStore<Ext> {
next_id: u32,
entries: Vec<ObserverSlot<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, Some(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 ids: Vec<ObserverId> = self.observers.entries.iter().map(|(id, _)| *id).collect();
self.is_observing = true;
for id in ids {
let Some(pos) = self
.observers
.entries
.iter()
.position(|(oid, _)| *oid == id)
else {
continue; };
let Some(mut obs) = self.observers.entries[pos].1.take() else {
continue;
};
let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
obs.observe(self, &record);
}));
if let Some(pos) = self
.observers
.entries
.iter()
.position(|(oid, _)| *oid == id)
{
self.observers.entries[pos].1 = Some(obs);
}
if let Err(payload) = outcome {
self.is_observing = false;
std::panic::resume_unwind(payload);
}
}
self.is_observing = false;
}
}
#[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 panicking_observer_leaves_dom_usable_and_observers_installed() {
struct Bomb;
impl MutationObserver<()> for Bomb {
fn observe(&mut self, _dom: &mut Dom<()>, _record: &Mutation) {
panic!("observer bomb");
}
}
let mut dom: Dom = Dom::new();
let root = dom.root();
let (_, before) = install_collector(&mut dom);
let bomb_id = dom.add_mutation_observer(Box::new(Bomb));
let (_, after) = install_collector(&mut dom);
assert_eq!(dom.observer_count(), 3);
let el = dom.create_element("div");
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
dom.append_child(root, el).unwrap();
}));
assert!(result.is_err(), "the bomb must actually fire");
assert_eq!(dom.observer_count(), 3);
assert!(dom.remove_mutation_observer(bomb_id));
let n_before = before.borrow().len();
let n_after = after.borrow().len();
dom.set_attribute(el, "id", "x").unwrap();
assert_eq!(before.borrow().len(), n_before + 1);
assert_eq!(after.borrow().len(), n_after + 1);
}
#[test]
fn observer_can_remove_itself_during_callback() {
struct SelfRemover {
me: Rc<std::cell::Cell<Option<ObserverId>>>,
seen: Rc<std::cell::Cell<u32>>,
removed: Rc<std::cell::Cell<Option<bool>>>,
}
impl MutationObserver<()> for SelfRemover {
fn observe(&mut self, dom: &mut Dom<()>, _record: &Mutation) {
self.seen.set(self.seen.get() + 1);
let id = self.me.get().expect("id stored before first mutation");
self.removed.set(Some(dom.remove_mutation_observer(id)));
}
}
let mut dom: Dom = Dom::new();
let (_, other) = install_collector(&mut dom);
let me = Rc::new(std::cell::Cell::new(None));
let seen = Rc::new(std::cell::Cell::new(0));
let removed = Rc::new(std::cell::Cell::new(None));
let id = dom.add_mutation_observer(Box::new(SelfRemover {
me: me.clone(),
seen: seen.clone(),
removed: removed.clone(),
}));
me.set(Some(id));
assert_eq!(dom.observer_count(), 2);
let el = dom.create_element("div");
dom.set_attribute(el, "id", "a").unwrap();
assert_eq!(seen.get(), 1);
assert_eq!(
removed.get(),
Some(true),
"removal inside observe() succeeds"
);
assert_eq!(dom.observer_count(), 1);
let n = other.borrow().len();
dom.set_attribute(el, "id", "b").unwrap();
assert_eq!(seen.get(), 1, "the removed observer gets nothing more");
assert_eq!(
other.borrow().len(),
n + 1,
"the other observer still fires"
);
}
#[test]
fn observer_added_during_callback_sees_only_later_records() {
struct Installer {
installed: Rc<std::cell::Cell<bool>>,
records: Rc<RefCell<Vec<Mutation>>>,
}
impl MutationObserver<()> for Installer {
fn observe(&mut self, dom: &mut Dom<()>, _record: &Mutation) {
if !self.installed.replace(true) {
dom.add_mutation_observer(Box::new(Collector {
records: self.records.clone(),
}));
}
}
}
let mut dom: Dom = Dom::new();
let late = Rc::new(RefCell::new(Vec::new()));
dom.add_mutation_observer(Box::new(Installer {
installed: Rc::new(std::cell::Cell::new(false)),
records: late.clone(),
}));
let el = dom.create_element("div");
dom.set_attribute(el, "id", "first").unwrap();
assert_eq!(dom.observer_count(), 2);
assert!(late.borrow().is_empty(), "not the record that installed it");
dom.set_attribute(el, "id", "second").unwrap();
assert_eq!(late.borrow().len(), 1);
}
#[test]
fn observer_can_remove_a_later_observer_before_it_fires() {
struct Remover {
victim: Rc<std::cell::Cell<Option<ObserverId>>>,
}
impl MutationObserver<()> for Remover {
fn observe(&mut self, dom: &mut Dom<()>, _record: &Mutation) {
if let Some(v) = self.victim.take() {
assert!(dom.remove_mutation_observer(v));
}
}
}
let mut dom: Dom = Dom::new();
let victim = Rc::new(std::cell::Cell::new(None));
dom.add_mutation_observer(Box::new(Remover {
victim: victim.clone(),
}));
let (victim_id, victim_records) = install_collector(&mut dom);
victim.set(Some(victim_id));
let el = dom.create_element("div");
dom.set_attribute(el, "id", "x").unwrap();
assert!(
victim_records.borrow().is_empty(),
"removed before its turn"
);
assert_eq!(dom.observer_count(), 1);
}
#[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);
}
}