Skip to content

Commit

Permalink
xilem_html: Add a DiffMapIterator that computes a diff between two …
Browse files Browse the repository at this point in the history
…ordered maps (#124)

It is used in the diffing of HTML elements in View::rebuild

Co-authored-by: Richard Dodd (dodj) <[email protected]>
  • Loading branch information
Philipp-M and richard-uk1 authored Aug 10, 2023
1 parent 2ccc427 commit f1a11ac
Show file tree
Hide file tree
Showing 3 changed files with 138 additions and 34 deletions.
129 changes: 129 additions & 0 deletions crates/xilem_html/src/diff.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
use std::{cmp::Ordering, collections::BTreeMap, iter::Peekable};

pub fn diff_tree_maps<'a, K: Ord, V: PartialEq>(
prev: &'a BTreeMap<K, V>,
next: &'a BTreeMap<K, V>,
) -> impl Iterator<Item = Diff<&'a K, &'a V>> + 'a {
DiffMapIterator {
prev: prev.iter().peekable(),
next: next.iter().peekable(),
}
}

/// An iterator that compares two ordered maps (like a `BTreeMap`) and outputs a `Diff` for each added, removed or changed key/value pair)
struct DiffMapIterator<'a, K: 'a, V: 'a, I: Iterator<Item = (&'a K, &'a V)>> {
prev: Peekable<I>,
next: Peekable<I>,
}

impl<'a, K: Ord + 'a, V: PartialEq, I: Iterator<Item = (&'a K, &'a V)>> Iterator
for DiffMapIterator<'a, K, V, I>
{
type Item = Diff<&'a K, &'a V>;
fn next(&mut self) -> Option<Self::Item> {
loop {
match (self.prev.peek(), self.next.peek()) {
(Some(&(prev_k, prev_v)), Some(&(next_k, next_v))) => match prev_k.cmp(next_k) {
Ordering::Less => {
self.prev.next();
return Some(Diff::Remove(prev_k));
}
Ordering::Greater => {
self.next.next();
return Some(Diff::Add(next_k, next_v));
}
Ordering::Equal => {
self.prev.next();
self.next.next();
if prev_v != next_v {
return Some(Diff::Change(next_k, next_v));
}
}
},
(Some(&(prev_k, _)), None) => {
self.prev.next();
return Some(Diff::Remove(prev_k));
}
(None, Some(&(next_k, next_v))) => {
self.next.next();
return Some(Diff::Add(next_k, next_v));
}
(None, None) => return None,
}
}
}
}

pub enum Diff<K, V> {
Add(K, V),
Remove(K),
Change(K, V),
}

#[cfg(test)]
mod tests {
use super::*;

macro_rules! tree_map {
(@single $($x:tt)*) => (());
(@count $($rest:expr),*) => (<[()]>::len(&[$(tree_map!(@single $rest)),*]));

($($key:expr => $value:expr,)+) => { tree_map!($($key => $value),+) };
($($key:expr => $value:expr),*) => {{
let mut _map = ::std::collections::BTreeMap::new();
$(
let _ = _map.insert($key, $value);
)*
_map
}};
}

#[test]
fn maps_are_equal() {
let map = tree_map!("an-entry" => 1, "another-entry" => 42);
let map_same = tree_map!("another-entry" => 42, "an-entry" => 1);
assert!(diff_tree_maps(&map, &map_same).next().is_none());
}

#[test]
fn new_map_has_additions() {
let map = tree_map!("an-entry" => 1);
let map_new = tree_map!("an-entry" => 1, "another-entry" => 42);
let mut diff = diff_tree_maps(&map, &map_new);
assert!(matches!(
diff.next(),
Some(Diff::Add(&"another-entry", &42))
));
assert!(diff.next().is_none());
}

#[test]
fn new_map_has_removal() {
let map = tree_map!("an-entry" => 1, "another-entry" => 42);
let map_new = tree_map!("an-entry" => 1);
let mut diff = diff_tree_maps(&map, &map_new);
assert!(matches!(diff.next(), Some(Diff::Remove(&"another-entry"))));
assert!(diff.next().is_none());
}

#[test]
fn new_map_has_removal_and_addition() {
let map = tree_map!("an-entry" => 1, "another-entry" => 42);
let map_new = tree_map!("an-entry" => 1, "other-entry" => 2);
let mut diff = diff_tree_maps(&map, &map_new);
assert!(matches!(diff.next(), Some(Diff::Remove(&"another-entry"))));
assert!(matches!(diff.next(), Some(Diff::Add(&"other-entry", &2))));
assert!(diff.next().is_none());
}

#[test]
fn new_map_changed() {
let map = tree_map!("an-entry" => 1, "another-entry" => 42);
let map_new = tree_map!("an-entry" => 2, "other-entry" => 2);
let mut diff = diff_tree_maps(&map, &map_new);
assert!(matches!(diff.next(), Some(Diff::Change(&"an-entry", 2))));
assert!(matches!(diff.next(), Some(Diff::Remove(&"another-entry"))));
assert!(matches!(diff.next(), Some(Diff::Add(&"other-entry", &2))));
assert!(diff.next().is_none());
}
}
42 changes: 8 additions & 34 deletions crates/xilem_html/src/element/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
//! `use xilem_html::elements as el` or similar to the top of your file.
use crate::{
context::{ChangeFlags, Cx},
diff::{diff_tree_maps, Diff},
view::{DomElement, Pod, View, ViewMarker, ViewSequence},
};

use std::{borrow::Cow, cmp::Ordering, collections::BTreeMap, fmt};
use std::{borrow::Cow, collections::BTreeMap, fmt};
use wasm_bindgen::{JsCast, UnwrapThrowExt};
use xilem_core::{Id, MessageResult, VecSplice};

Expand Down Expand Up @@ -180,45 +181,18 @@ where
let element = element.as_element_ref();

// update attributes
// TODO can I use VecSplice for this?
let mut prev_attrs = prev.attributes.iter().peekable();
let mut self_attrs = self.attributes.iter().peekable();
while let (Some((prev_name, prev_value)), Some((self_name, self_value))) =
(prev_attrs.peek(), self_attrs.peek())
{
match prev_name.cmp(self_name) {
Ordering::Less => {
// attribute from prev is disappeared
remove_attribute(element, prev_name);
for itm in diff_tree_maps(&prev.attributes, &self.attributes) {
match itm {
Diff::Add(name, value) | Diff::Change(name, value) => {
set_attribute(element, name, value);
changed |= ChangeFlags::OTHER_CHANGE;
prev_attrs.next();
}
Ordering::Greater => {
// new attribute has appeared
set_attribute(element, self_name, self_value);
Diff::Remove(name) => {
remove_attribute(element, name);
changed |= ChangeFlags::OTHER_CHANGE;
self_attrs.next();
}
Ordering::Equal => {
// attribute may has changed
if prev_value != self_value {
set_attribute(element, self_name, self_value);
changed |= ChangeFlags::OTHER_CHANGE;
}
prev_attrs.next();
self_attrs.next();
}
}
}
// Only max 1 of these loops will run
for (name, _) in prev_attrs {
remove_attribute(element, name);
changed |= ChangeFlags::OTHER_CHANGE;
}
for (name, value) in self_attrs {
set_attribute(element, name, value);
changed |= ChangeFlags::OTHER_CHANGE;
}

// update children
let mut splice = VecSplice::new(&mut state.child_elements, &mut state.scratch);
Expand Down
1 change: 1 addition & 0 deletions crates/xilem_html/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use wasm_bindgen::JsCast;
mod app;
mod class;
mod context;
mod diff;
mod element;
mod event;
mod one_of;
Expand Down

0 comments on commit f1a11ac

Please sign in to comment.