diff --git a/Cargo.toml b/Cargo.toml index 481938c..cce6883 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,6 +61,9 @@ features = [ "Event", "Node", "Window", + "Text", + "DocumentType", + "CharacterData", ] [dev-dependencies] diff --git a/examples/counter/Cargo.toml b/examples/counter/Cargo.toml index 5cb2587..3a0775d 100644 --- a/examples/counter/Cargo.toml +++ b/examples/counter/Cargo.toml @@ -27,6 +27,7 @@ features = [ "MouseEvent", "Node", "Window", + "NodeList", ] [dev-dependencies] diff --git a/examples/game-of-life/Cargo.toml b/examples/game-of-life/Cargo.toml index 1f7a1cc..0168ded 100644 --- a/examples/game-of-life/Cargo.toml +++ b/examples/game-of-life/Cargo.toml @@ -23,6 +23,9 @@ features = [ "HtmlElement", "Node", "Window", + "Event", + "EventTarget", + "NodeList", ] [dev-dependencies] diff --git a/examples/hello-world/Cargo.toml b/examples/hello-world/Cargo.toml index 12666c3..917d260 100644 --- a/examples/hello-world/Cargo.toml +++ b/examples/hello-world/Cargo.toml @@ -21,6 +21,9 @@ features = [ "HtmlElement", "Node", "Window", + "Event", + "EventTarget", + "NodeList", ] [dev-dependencies] diff --git a/examples/input-form/Cargo.toml b/examples/input-form/Cargo.toml index 7c1c3f2..04d9d59 100644 --- a/examples/input-form/Cargo.toml +++ b/examples/input-form/Cargo.toml @@ -26,6 +26,7 @@ features = [ "MouseEvent", "Node", "Window", + "NodeList", ] [dev-dependencies] diff --git a/examples/js-component/Cargo.toml b/examples/js-component/Cargo.toml index 122f3a8..6a2459e 100644 --- a/examples/js-component/Cargo.toml +++ b/examples/js-component/Cargo.toml @@ -23,4 +23,7 @@ features = [ "HtmlElement", "Node", "Window", + "Event", + "EventTarget", + "NodeList", ] diff --git a/examples/moire/Cargo.toml b/examples/moire/Cargo.toml index a609c41..6944c20 100644 --- a/examples/moire/Cargo.toml +++ b/examples/moire/Cargo.toml @@ -31,6 +31,7 @@ features = [ "Node", "Performance", "Window", + "NodeList", ] [dev-dependencies] diff --git a/examples/sierpinski-triangle/Cargo.toml b/examples/sierpinski-triangle/Cargo.toml index ce964ce..ea6af16 100644 --- a/examples/sierpinski-triangle/Cargo.toml +++ b/examples/sierpinski-triangle/Cargo.toml @@ -22,6 +22,9 @@ features = [ "Node", "Performance", "Window", + "Event", + "EventTarget", + "NodeList", ] [dev-dependencies] diff --git a/examples/todomvc/Cargo.toml b/examples/todomvc/Cargo.toml index b21c6a9..a651750 100644 --- a/examples/todomvc/Cargo.toml +++ b/examples/todomvc/Cargo.toml @@ -31,6 +31,7 @@ features = [ "Storage", "Node", "Window", + "NodeList", ] [dev-dependencies] diff --git a/js/change-list-interpreter.js b/js/change-list-interpreter.js deleted file mode 100644 index d840028..0000000 --- a/js/change-list-interpreter.js +++ /dev/null @@ -1,377 +0,0 @@ -const decoder = new TextDecoder(); - -function top(stack) { - return stack[stack.length - 1]; -} - -function string(mem, pointer, length) { - const buf = mem.subarray(pointer, pointer + length); - return decoder.decode(buf); -} - -const OP_TABLE = [ - // 0 - function setText(interpreter, mem8, mem32, i) { - const pointer = mem32[i++]; - const length = mem32[i++]; - const str = string(mem8, pointer, length); - top(interpreter.stack).textContent = str; - return i; - }, - - // 1 - function removeSelfAndNextSiblings(interpreter, mem8, mem32, i) { - const node = interpreter.stack.pop(); - let sibling = node.nextSibling; - while (sibling) { - const temp = sibling.nextSibling; - sibling.remove(); - sibling = temp; - } - node.remove(); - return i; - }, - - // 2 - function replaceWith(interpreter, mem8, mem32, i) { - const newNode = interpreter.stack.pop(); - const oldNode = interpreter.stack.pop(); - oldNode.replaceWith(newNode); - interpreter.stack.push(newNode); - return i; - }, - - // 3 - function setAttribute(interpreter, mem8, mem32, i) { - const nameId = mem32[i++]; - const valueId = mem32[i++]; - const name = interpreter.getCachedString(nameId); - const value = interpreter.getCachedString(valueId); - const node = top(interpreter.stack); - node.setAttribute(name, value); - - // Some attributes are "volatile" and don't work through `setAttribute`. - if (name === "value") { - node.value = value; - } - if (name === "checked") { - node.checked = true; - } - if (name === "selected") { - node.selected = true; - } - - return i; - }, - - // 4 - function removeAttribute(interpreter, mem8, mem32, i) { - const nameId = mem32[i++]; - const name = interpreter.getCachedString(nameId); - const node = top(interpreter.stack); - node.removeAttribute(name); - - // Some attributes are "volatile" and don't work through `removeAttribute`. - if (name === "value") { - node.value = null; - } - if (name === "checked") { - node.checked = false; - } - if (name === "selected") { - node.selected = false; - } - - return i; - }, - - // 5 - function pushReverseChild(interpreter, mem8, mem32, i) { - const n = mem32[i++]; - const parent = top(interpreter.stack); - const children = parent.childNodes; - const child = children[children.length - n - 1]; - interpreter.stack.push(child); - return i; - }, - - // 6 - function popPushChild(interpreter, mem8, mem32, i) { - const n = mem32[i++]; - interpreter.stack.pop(); - const parent = top(interpreter.stack); - const children = parent.childNodes; - const child = children[n]; - interpreter.stack.push(child); - return i; - }, - - // 7 - function pop(interpreter, mem8, mem32, i) { - interpreter.stack.pop(); - return i; - }, - - // 8 - function appendChild(interpreter, mem8, mem32, i) { - const child = interpreter.stack.pop(); - top(interpreter.stack).appendChild(child); - return i; - }, - - // 9 - function createTextNode(interpreter, mem8, mem32, i) { - const pointer = mem32[i++]; - const length = mem32[i++]; - const text = string(mem8, pointer, length); - interpreter.stack.push(document.createTextNode(text)); - return i; - }, - - // 10 - function createElement(interpreter, mem8, mem32, i) { - const tagNameId = mem32[i++]; - const tagName = interpreter.getCachedString(tagNameId); - interpreter.stack.push(document.createElement(tagName)); - return i; - }, - - // 11 - function newEventListener(interpreter, mem8, mem32, i) { - const eventId = mem32[i++]; - const eventType = interpreter.getCachedString(eventId); - const a = mem32[i++]; - const b = mem32[i++]; - const el = top(interpreter.stack); - el.addEventListener(eventType, interpreter.eventHandler); - el[`dodrio-a-${eventType}`] = a; - el[`dodrio-b-${eventType}`] = b; - return i; - }, - - // 12 - function updateEventListener(interpreter, mem8, mem32, i) { - const eventId = mem32[i++]; - const eventType = interpreter.getCachedString(eventId); - const el = top(interpreter.stack); - el[`dodrio-a-${eventType}`] = mem32[i++]; - el[`dodrio-b-${eventType}`] = mem32[i++]; - return i; - }, - - // 13 - function removeEventListener(interpreter, mem8, mem32, i) { - const eventId = mem32[i++]; - const eventType = interpreter.getCachedString(eventId); - const el = top(interpreter.stack); - el.removeEventListener(eventType, interpreter.eventHandler); - return i; - }, - - // 14 - function addCachedString(interpreter, mem8, mem32, i) { - const pointer = mem32[i++]; - const length = mem32[i++]; - const id = mem32[i++]; - const str = string(mem8, pointer, length); - interpreter.addCachedString(str, id); - return i; - }, - - // 15 - function dropCachedString(interpreter, mem8, mem32, i) { - const id = mem32[i++]; - interpreter.dropCachedString(id); - return i; - }, - - // 16 - function createElementNS(interpreter, mem8, mem32, i) { - const tagNameId = mem32[i++]; - const tagName = interpreter.getCachedString(tagNameId); - const nsId = mem32[i++]; - const ns = interpreter.getCachedString(nsId); - interpreter.stack.push(document.createElementNS(ns, tagName)); - return i; - }, - - // 17 - function saveChildrenToTemporaries(interpreter, mem8, mem32, i) { - let temp = mem32[i++]; - const start = mem32[i++]; - const end = mem32[i++]; - const parent = top(interpreter.stack); - const children = parent.childNodes; - for (let i = start; i < end; i++) { - interpreter.temporaries[temp++] = children[i]; - } - return i; - }, - - // 18 - function pushChild(interpreter, mem8, mem32, i) { - const parent = top(interpreter.stack); - const n = mem32[i++]; - const child = parent.childNodes[n]; - interpreter.stack.push(child); - return i; - }, - - // 19 - function pushTemporary(interpreter, mem8, mem32, i) { - const temp = mem32[i++]; - interpreter.stack.push(interpreter.temporaries[temp]); - return i; - }, - - // 20 - function insertBefore(interpreter, mem8, mem32, i) { - const before = interpreter.stack.pop(); - const after = interpreter.stack.pop(); - after.parentNode.insertBefore(before, after); - interpreter.stack.push(before); - return i; - }, - - // 21 - function popPushReverseChild(interpreter, mem8, mem32, i) { - const n = mem32[i++]; - interpreter.stack.pop(); - const parent = top(interpreter.stack); - const children = parent.childNodes; - const child = children[children.length - n - 1]; - interpreter.stack.push(child); - return i; - }, - - // 22 - function removeChild(interpreter, mem8, mem32, i) { - const n = mem32[i++]; - const parent = top(interpreter.stack); - const child = parent.childNodes[n]; - child.remove(); - return i; - }, - - // 23 - function setClass(interpreter, mem8, mem32, i) { - const classId = mem32[i++]; - const className = interpreter.getCachedString(classId); - top(interpreter.stack).className = className; - return i; - }, - - // 24 - function saveTemplate(interpreter, mem8, mem32, i) { - const id = mem32[i++]; - const template = top(interpreter.stack); - interpreter.saveTemplate(id, template.cloneNode(true)); - return i; - }, - - // 25 - function pushTemplate(interpreter, mem8, mem32, i) { - const id = mem32[i++]; - const template = interpreter.getTemplate(id); - interpreter.stack.push(template.cloneNode(true)); - return i; - } -]; - -export class ChangeListInterpreter { - constructor(container) { - this.trampoline = null; - this.container = container; - this.ranges = []; - this.stack = []; - this.strings = new Map(); - this.temporaries = []; - this.templates = new Map(); - } - - unmount() { - this.trampoline.mounted = false; - - // Null out all of our properties just to ensure that if we mistakenly ever - // call a method on this instance again, it will throw. - this.trampoline = null; - this.container = null; - this.ranges = null; - this.stack = null; - this.strings = null; - this.temporaries = null; - this.templates = null; - } - - addChangeListRange(start, len) { - this.ranges.push(start); - this.ranges.push(len); - } - - applyChanges(memory) { - if (this.ranges.length == 0) { - return; - } - - this.stack.push(this.container.firstChild); - const mem8 = new Uint8Array(memory.buffer); - const mem32 = new Uint32Array(memory.buffer); - - for (let i = 0; i < this.ranges.length; i += 2) { - const start = this.ranges[i]; - const len = this.ranges[i + 1]; - this.applyChangeRange(mem8, mem32, start, len); - } - - this.ranges.length = 0; - this.stack.length = 0; - this.temporaries.length = 0; - } - - applyChangeRange(mem8, mem32, start, len) { - const end = (start + len) / 4; - for (let i = start / 4; i < end; ) { - const op = mem32[i++]; - i = OP_TABLE[op](this, mem8, mem32, i); - } - } - - addCachedString(str, id) { - this.strings.set(id, str); - } - - dropCachedString(id) { - this.strings.delete(id); - } - - getCachedString(id) { - return this.strings.get(id); - } - - saveTemplate(id, template) { - this.templates.set(id, template); - } - - getTemplate(id) { - return this.templates.get(id); - } - - initEventsTrampoline(trampoline) { - this.trampoline = trampoline; - trampoline.mounted = true; - this.eventHandler = function(event) { - if (!trampoline.mounted) { - throw new Error("invocation of listener after VDOM has been unmounted"); - } - - // `this` always refers to the element the handler was added to. - // Since we're adding the handler to all elements our content wants - // to listen for events on, this ensures that we always get the right - // values for `a` and `b`. - const type = event.type; - const a = this[`dodrio-a-${type}`]; - const b = this[`dodrio-b-${type}`]; - trampoline(event, a, b); - } - } -} diff --git a/src/change_list/emitter.rs b/src/change_list/emitter.rs deleted file mode 100755 index c8c3794..0000000 --- a/src/change_list/emitter.rs +++ /dev/null @@ -1,292 +0,0 @@ -//! The `InstructionEmitter` is responsible for encoding change list -//! instructions and ensuring that each instruction has the correct number of -//! immediates. It ensures that the resulting change list instruction stream is -//! *syntactically* correct (opcodes have the correct arity of immediate -//! arguments, etc), while `ChangeListBuilder` wraps an emitter and additionally -//! ensures that the resulting change list program is *semantically* correct -//! (doesn't reference cached strings before they've been added to the cache, -//! etc). -//! -//! We encode the instructions directly into a dedicated bump arena. We -//! eventually pass the bump arena's chunks to the interpreter in JS, so it is -//! critical that nothing other than change list instructions are allocated -//! inside this bump, and that the instructions themselves do not contain any -//! padding or uninitialized memory. See the documentation for the the -//! `Bump::each_allocated_chunk` method for details. - -use bumpalo::Bump; - -#[derive(Debug)] -pub(crate) struct InstructionEmitter { - bump: Bump, -} - -impl InstructionEmitter { - /// Construct a new `InstructionEmitter` with its own bump arena. - pub fn new() -> InstructionEmitter { - let bump = Bump::new(); - InstructionEmitter { bump } - } - - /// Invoke the given function with each of the allocated instruction - /// sequences that this emitter has built up. - #[cfg_attr(feature = "xxx-unstable-internal-use-only", allow(dead_code))] - pub fn each_instruction_sequence(&mut self, f: F) - where - F: FnMut(&[u8]), - { - // Note: the safety invariants required for `each_allocated_chunk` are - // maintained by the fact that everything we encode as allocations in - // the bump arena are in the form of `u32`s, and therefore we don't have - // any uninitialized memory padding in the arena. - unsafe { - self.bump.each_allocated_chunk(f); - } - } - - /// Reset to an empty sequence of instructions. - pub fn reset(&mut self) { - self.bump.reset(); - } -} - -macro_rules! define_change_list_instructions { - ( $( - $( #[$attr:meta] )* - $name:ident ( - $($immediate:ident),* - ) = $discriminant:expr, - )* ) => { - impl InstructionEmitter { - $( - $( #[$attr] )* - #[inline] - pub fn $name(&self $(, $immediate: u32)*) { - self.bump.alloc_with(|| [$discriminant $(, $immediate )* ]); - } - )* - } - } -} - -define_change_list_instructions! { - /// Stack: `[... TextNode] -> [... TextNode]` - /// - /// ```text - /// stack.top().textContent = readString(pointer, length) - /// ``` - set_text(pointer, length) = 0, - - /// Stack: `[... Node] -> [...]` - /// - /// ```text - /// node = stack.pop() - /// while (node.nextSibling) { - /// node.nextSibling.remove(); - /// } - /// node.remove() - /// ``` - remove_self_and_next_siblings() = 1, - - /// Stack: `[... Node Node] -> [... Node]` - /// - /// ```text - /// new = stack.pop() - /// old = stack.pop() - /// old.replaceWith(new) - /// stack.push(new) - /// ``` - replace_with() = 2, - - /// Stack: `[... Node] -> [... Node]` - /// - /// ```text - /// stack.top().setAttribute(getCachedString(attribute_key), getCachedString(value_key)) - /// ``` - set_attribute(attribute_key, value_key) = 3, - - /// Stack: `[... Node] -> [... Node]` - /// - /// ```text - /// stack.top().removeAttribute(getCachedString(attribute_key)) - /// ``` - remove_attribute(attribute_key) = 4, - - /// Stack: `[... Node] -> [... Node Node]` - /// - /// ```text - /// parent = stack.top() - /// child = parent.childNodes[parent.childNodes.length - n - 1] - /// stack.push(child) - /// ``` - push_reverse_child(n) = 5, - - /// Stack: `[... Node Node] -> [... Node Node]` - /// - /// ```text - /// stack.pop(); - /// parent = stack.top(); - /// child = parent.childNodes[n] - /// stack.push(child) - /// ``` - pop_push_child(n) = 6, - - /// Stack: `[... T] -> [...]` - /// - /// ```text - /// stack.pop() - /// ``` - pop() = 7, - - /// Stack: `[... Node Node] -> [... Node]` - /// - /// ```text - /// child = stack.pop() - /// stack.top().appendChild(child) - /// ``` - append_child() = 8, - - /// Stack: `[...] -> [... Node]` - /// - /// ```text - /// stack.push(document.createTextNode(readString(pointer, length))) - /// ``` - create_text_node(pointer, length) = 9, - - /// Stack: `[...] -> [... Node]` - /// - /// ```text - /// stack.push(document.createElement(getCachedString(tag_name_key)) - /// ``` - create_element(tag_name_key) = 10, - - /// Stack: `[... Node] -> [... Node]` - /// - /// ```text - /// event = getCachedString(event_key) - /// callback = createProxyToRustCallback(a, b) - /// stack.top().addEventListener(event, callback) - /// ``` - new_event_listener(event_key, a, b) = 11, - - /// Stack: `[... Node] -> [... Node]` - /// - /// ```text - /// event = getCachedString(event_key) - /// new_callback = createProxyToRustCallback(a, b); - /// stack.top().updateEventlistener(new_callback) - /// ``` - update_event_listener(event_key, a, b) = 12, - - /// Stack: `[... Node] -> [... Node]` - /// - /// ```text - /// stack.top().removeEventListener(getCachedString(event_key)); - /// ``` - remove_event_listener(event_key) = 13, - - /// Stack: `[...] -> [...]` - /// - /// ```text - /// addCachedString(readString(pointer, length), key); - /// ``` - add_cached_string(pointer, length, key) = 14, - - /// Stack: `[...] -> [...]` - /// - /// ```text - /// dropCachedString(key); - /// ``` - drop_cached_string(key) = 15, - - /// Stack: `[...] -> [... Node]` - /// - /// ```text - /// tag_name = getCachedString(tag_name_key) - /// namespace = getCachedString(tag_name_key) - /// stack.push(document.createElementNS(tag_name, namespace)) - /// ``` - create_element_ns(tag_name_key, namespace_key) = 16, - - /// Stack: `[...] -> [...]` - /// - /// ```text - /// parent = stack.top() - /// children = parent.childNodes - /// temp = temp_base - /// for i in start .. end: - /// temporaries[temp] = children[i] - /// temp += 1 - /// ``` - save_children_to_temporaries(temp_base, start, end) = 17, - - /// Stack: `[... Node] -> [... Node Node]` - /// - /// ```text - /// parent = stack.top() - /// child = parent.childNodes[n] - /// stack.push(child) - /// ``` - push_child(n) = 18, - - /// Stack: `[...] -> [... Node]` - /// - /// ```text - /// stack.push(temporaries[temp]) - /// ``` - push_temporary(temp) = 19, - - /// Stack: `[... Node Node] -> [... Node]` - /// - /// ```text - /// before = stack.pop() - /// after = stack.pop() - /// after.insertBefore(before) - /// stack.push(before) - /// ``` - insert_before() = 20, - - /// Stack: `[... Node Node] -> [... Node Node]` - /// - /// ```text - /// stack.pop() - /// parent = stack.top() - /// child = parent.childNodes[parent.childNodes.length - n - 1] - /// stack.push(child) - /// ``` - pop_push_reverse_child(n) = 21, - - /// Stack: `[... Node] -> [... Node]` - /// - /// ```text - /// parent = stack.top() - /// child = parent.childNodes[n] - /// child.remove() - /// ``` - remove_child(n) = 22, - - /// Stack: `[... Node] -> [... Node]` - /// - /// ```text - /// class = getCachedString(class) - /// node = stack.top() - /// node.className = class - /// ``` - set_class(class) = 23, - - /// Stack: `[... Node] -> [... Node]` - /// - /// ```text - /// template = stack.top() - /// saveTemplate(id, template) - /// ``` - save_template(id) = 24, - - /// Stack: `[...] -> [... Node]` - /// - /// ```text - /// template = getTemplate(id) - /// stack.push(template.cloneNode(true)) - /// ``` - push_template(id) = 25, -} diff --git a/src/change_list/interpreter.rs b/src/change_list/interpreter.rs new file mode 100644 index 0000000..2b011b1 --- /dev/null +++ b/src/change_list/interpreter.rs @@ -0,0 +1,380 @@ +use crate::cached_set::CacheId; +use crate::{Element, EventsTrampoline}; +use fxhash::FxHashMap; +use wasm_bindgen::{closure::Closure, JsCast}; +use web_sys::{window, Document, Event, Node}; + +#[derive(Debug)] +pub(crate) struct ChangeListInterpreter { + container: Element, + stack: Stack, + temporaries: FxHashMap, + templates: FxHashMap, + callback: Option>, + document: Document, +} + +#[derive(Debug, Default)] +struct Stack { + list: Vec, +} + +impl Stack { + pub fn with_capacity(cap: usize) -> Self { + Stack { + list: Vec::with_capacity(cap), + } + } + + pub fn push(&mut self, node: Node) { + debug!("stack-push: {:?}", node); + self.list.push(node); + } + + pub fn pop(&mut self) -> Node { + let res = self.list.pop().unwrap(); + debug!("stack-pop: {:?}", res); + + res + } + + pub fn clear(&mut self) { + self.list.clear(); + } + + pub fn top(&self) -> &Node { + &self.list[self.list.len() - 1] + } +} + +impl ChangeListInterpreter { + pub fn new(container: Element) -> Self { + let document = window() + .expect("must have access to the window") + .document() + .expect("must have access to the Document"); + + Self { + container, + stack: Stack::with_capacity(20), + temporaries: Default::default(), + templates: Default::default(), + callback: None, + document, + } + } + + pub fn unmount(&mut self) { + self.stack.clear(); + self.temporaries.clear(); + self.templates.clear(); + } + + pub fn start(&mut self) { + if let Some(child) = self.container.first_child() { + self.stack.push(child); + } + } + + pub fn reset(&mut self) { + self.stack.clear(); + self.temporaries.clear(); + } + + pub fn get_template(&self, id: CacheId) -> Option<&Node> { + self.templates.get(&id) + } + + pub fn init_events_trampoline(&mut self, mut trampoline: EventsTrampoline) { + self.callback = Some(Closure::wrap(Box::new(move |event: &web_sys::Event| { + let target = event + .target() + .expect("missing target") + .dyn_into::() + .expect("not a valid element"); + let typ = event.type_(); + let a: u32 = target + .get_attribute(&format!("dodrio-a-{}", typ)) + .and_then(|v| v.parse().ok()) + .unwrap_or_default(); + + let b: u32 = target + .get_attribute(&format!("dodrio-b-{}", typ)) + .and_then(|v| v.parse().ok()) + .unwrap_or_default(); + + // get a and b from the target + trampoline(event.clone(), a, b); + }) as Box)); + } + + // 0 + pub fn set_text(&mut self, text: &str) { + self.stack.top().set_text_content(Some(text)); + } + + // 1 + pub fn remove_self_and_next_siblings(&mut self) { + let node = self.stack.pop(); + let mut sibling = node.next_sibling(); + + while let Some(inner) = sibling { + let temp = inner.next_sibling(); + if let Some(sibling) = inner.dyn_ref::() { + sibling.remove(); + } + sibling = temp; + } + if let Some(node) = node.dyn_ref::() { + node.remove(); + } + } + + // 2 + pub fn replace_with(&mut self) { + let new_node = self.stack.pop(); + let old_node = self.stack.pop(); + + if old_node.has_type::() { + old_node + .dyn_ref::() + .unwrap() + .replace_with_with_node_1(&new_node) + .unwrap(); + } else if old_node.has_type::() { + old_node + .dyn_ref::() + .unwrap() + .replace_with_with_node_1(&new_node) + .unwrap(); + } else if old_node.has_type::() { + old_node + .dyn_ref::() + .unwrap() + .replace_with_with_node_1(&new_node) + .unwrap(); + } else { + panic!("Cannot replace node: {:?}", old_node); + } + + self.stack.push(new_node); + } + + // 3 + pub fn set_attribute(&mut self, name: &str, value: &str) { + let node = self.stack.top(); + + if let Some(node) = node.dyn_ref::() { + node.set_attribute(name, value).unwrap(); + + // Some attributes are "volatile" and don't work through `setAttribute`. + // TODO: + // if name == "value" { + // node.set_value(value); + // } + // if name == "checked" { + // node.set_checked(true); + // } + // if name == "selected" { + // node.set_selected(true); + // } + } + } + + // 4 + pub fn remove_attribute(&mut self, name: &str) { + let node = self.stack.top(); + if let Some(node) = node.dyn_ref::() { + node.remove_attribute(name).unwrap(); + + // Some attributes are "volatile" and don't work through `removeAttribute`. + // TODO: + // if name == "value" { + // node.set_value(""); + // } + // if name == "checked" { + // node.set_checked(false); + // } + // if name == "selected" { + // node.set_selected(false); + // } + } + } + + // 5 + pub fn push_reverse_child(&mut self, n: u32) { + let parent = self.stack.top(); + let children = parent.child_nodes(); + let child = children.get(children.length() - n - 1).unwrap(); + self.stack.push(child); + } + + // 6 + pub fn pop_push_child(&mut self, n: u32) { + self.stack.pop(); + let parent = self.stack.top(); + let children = parent.child_nodes(); + let child = children.get(n).unwrap(); + self.stack.push(child); + } + + // 7 + pub fn pop(&mut self) { + self.stack.pop(); + } + + // 8 + pub fn append_child(&mut self) { + let child = self.stack.pop(); + self.stack.top().append_child(&child).unwrap(); + } + + // 9 + pub fn create_text_node(&mut self, text: &str) { + self.stack.push( + self.document + .create_text_node(text) + .dyn_into::() + .unwrap(), + ); + } + + // 10 + pub fn create_element(&mut self, tag_name: &str) { + let el = self + .document + .create_element(tag_name) + .unwrap() + .dyn_into::() + .unwrap(); + self.stack.push(el); + } + + // 11 + pub fn new_event_listener(&mut self, event_type: &str, a: u32, b: u32) { + let el = self.stack.top(); + + let el = el + .dyn_ref::() + .expect(&format!("not an element: {:?}", el)); + el.add_event_listener_with_callback( + event_type, + self.callback.as_ref().unwrap().as_ref().unchecked_ref(), + ) + .unwrap(); + debug!("adding attributes: {}, {}", a, b); + el.set_attribute(&format!("dodrio-a-{}", event_type), &a.to_string()) + .unwrap(); + el.set_attribute(&format!("dodrio-b-{}", event_type), &b.to_string()) + .unwrap(); + } + + // 12 + pub fn update_event_listener(&mut self, event_type: &str, a: u32, b: u32) { + if let Some(el) = self.stack.top().dyn_ref::() { + el.set_attribute(&format!("dodrio-a-{}", event_type), &a.to_string()) + .unwrap(); + el.set_attribute(&format!("dodrio-b-{}", event_type), &b.to_string()) + .unwrap(); + } + } + + // 13 + pub fn remove_event_listener(&mut self, event_type: &str) { + if let Some(el) = self.stack.top().dyn_ref::() { + el.remove_event_listener_with_callback( + event_type, + self.callback.as_ref().unwrap().as_ref().unchecked_ref(), + ) + .unwrap(); + } + } + + // 16 + pub fn create_element_ns(&mut self, tag_name: &str, ns: &str) { + let el = self + .document + .create_element_ns(Some(ns), tag_name) + .unwrap() + .dyn_into::() + .unwrap(); + self.stack.push(el); + } + + // 17 + pub fn save_children_to_temporaries(&mut self, mut temp: u32, start: u32, end: u32) { + let parent = self.stack.top(); + let children = parent.child_nodes(); + for i in start..end { + self.temporaries.insert(temp, children.get(i).unwrap()); + temp += 1; + } + } + + // 18 + pub fn push_child(&mut self, n: u32) { + let parent = self.stack.top(); + let child = parent.child_nodes().get(n).unwrap(); + self.stack.push(child); + } + + // 19 + pub fn push_temporary(&mut self, temp: u32) { + let t = self.temporaries.get(&temp).unwrap().clone(); + self.stack.push(t); + } + + // 20 + pub fn insert_before(&mut self) { + let before = self.stack.pop(); + let after = self.stack.pop(); + after + .parent_node() + .unwrap() + .insert_before(&before, Some(&after)) + .unwrap(); + self.stack.push(before); + } + + // 21 + pub fn pop_push_reverse_child(&mut self, n: u32) { + self.stack.pop(); + let parent = self.stack.top(); + let children = parent.child_nodes(); + let child = children.get(children.length() - n - 1).unwrap(); + self.stack.push(child); + } + + // 22 + pub fn remove_child(&mut self, n: u32) { + let parent = self.stack.top(); + if let Some(child) = parent.child_nodes().get(n).unwrap().dyn_ref::() { + child.remove(); + } + } + + // 23 + pub fn set_class(&mut self, class_name: &str) { + if let Some(el) = self.stack.top().dyn_ref::() { + el.set_class_name(class_name); + } + } + + // 24 + pub fn save_template(&mut self, id: CacheId) { + let template = self.stack.top(); + let t = template.clone_node_with_deep(true).unwrap(); + self.templates.insert(id, t); + } + + // 25 + pub fn push_template(&mut self, id: CacheId) { + let template = self.get_template(id).unwrap(); + let t = template.clone_node_with_deep(true).unwrap(); + self.stack.push(t); + } + + pub fn has_template(&self, id: CacheId) -> bool { + self.templates.contains_key(&id) + } +} diff --git a/src/change_list/js.rs b/src/change_list/js.rs deleted file mode 100644 index 1a4b0c1..0000000 --- a/src/change_list/js.rs +++ /dev/null @@ -1,40 +0,0 @@ -cfg_if::cfg_if! { - if #[cfg(all(feature = "xxx-unstable-internal-use-only", not(target_arch = "wasm32")))] { - #[derive(Clone, Debug)] - pub struct ChangeListInterpreter {} - impl ChangeListInterpreter { - pub fn new(_container: &crate::Element) -> ChangeListInterpreter { - ChangeListInterpreter {} - } - pub fn unmount(&self) {} - pub fn add_change_list_range(&self, _start: usize, _len: usize) {} - pub fn init_events_trampoline(&self, _trampoline: &crate::EventsTrampoline) {} - } - } else { - use wasm_bindgen::prelude::*; - - #[wasm_bindgen(module = "/js/change-list-interpreter.js")] - extern "C" { - #[derive(Clone, Debug)] - pub type ChangeListInterpreter; - - #[wasm_bindgen(constructor)] - pub fn new(container: &web_sys::Element) -> ChangeListInterpreter; - - #[wasm_bindgen(structural, method)] - pub fn unmount(this: &ChangeListInterpreter); - - #[wasm_bindgen(structural, method, js_name = addChangeListRange)] - pub fn add_change_list_range(this: &ChangeListInterpreter, start: usize, len: usize); - - #[wasm_bindgen(structural, method, js_name = applyChanges)] - pub fn apply_changes(this: &ChangeListInterpreter, memory: JsValue); - - #[wasm_bindgen(structural, method, js_name = initEventsTrampoline)] - pub fn init_events_trampoline( - this: &ChangeListInterpreter, - trampoline: &crate::EventsTrampoline, - ); - } - } -} diff --git a/src/change_list/mod.rs b/src/change_list/mod.rs index e3af69d..da96561 100644 --- a/src/change_list/mod.rs +++ b/src/change_list/mod.rs @@ -1,23 +1,14 @@ -pub(crate) mod emitter; -pub(crate) mod strings; +pub(crate) mod interpreter; pub(crate) mod traversal; -// Note: has to be `pub` because of `wasm-bindgen` visibility restrictions. -pub mod js; - -use self::emitter::InstructionEmitter; -use self::strings::{StringKey, StringsCache}; +use self::interpreter::ChangeListInterpreter; use self::traversal::{MoveTo, Traversal}; use crate::{cached_set::CacheId, Listener}; -use fxhash::FxHashSet; #[derive(Debug)] pub(crate) struct ChangeListPersistentState { - strings: StringsCache, - emitter: InstructionEmitter, traversal: Traversal, - interpreter: js::ChangeListInterpreter, - templates: FxHashSet, + interpreter: ChangeListInterpreter, } pub(crate) struct ChangeListBuilder<'a> { @@ -34,66 +25,41 @@ impl Drop for ChangeListPersistentState { impl ChangeListPersistentState { pub(crate) fn new(container: &crate::Element) -> ChangeListPersistentState { - let strings = StringsCache::new(); - let emitter = InstructionEmitter::new(); let traversal = Traversal::new(); - let interpreter = js::ChangeListInterpreter::new(container); - let templates = Default::default(); + let interpreter = ChangeListInterpreter::new(container.clone()); + ChangeListPersistentState { - strings, - emitter, traversal, interpreter, - templates, } } - pub(crate) fn init_events_trampoline(&mut self, trampoline: &crate::EventsTrampoline) { + pub(crate) fn init_events_trampoline(&mut self, trampoline: crate::EventsTrampoline) { self.interpreter.init_events_trampoline(trampoline); } - pub(crate) fn builder<'a>(&'a mut self) -> ChangeListBuilder<'a> { - ChangeListBuilder { + pub(crate) fn builder(&mut self) -> ChangeListBuilder { + let builder = ChangeListBuilder { state: self, next_temporary: 0, forcing_new_listeners: false, - } - } -} - -cfg_if::cfg_if! { - if #[cfg(all(feature = "xxx-unstable-internal-use-only", not(target_arch = "wasm32")))] { - impl ChangeListBuilder<'_> { - pub(crate) fn finish(self) { - self.state.strings.drop_unused_strings(&self.state.emitter); + }; + debug!("emit: start"); + builder.state.interpreter.start(); - // Nothing to actually apply the changes to. - - self.state.emitter.reset(); - self.state.traversal.reset(); - } - } - } else { - impl ChangeListBuilder<'_> { - pub(crate) fn finish(self) { - self.state.strings.drop_unused_strings(&self.state.emitter); - - // Apply the changes. - let interpreter = &self.state.interpreter; - self.state.emitter.each_instruction_sequence(|seq| { - interpreter.add_change_list_range(seq.as_ptr() as usize, seq.len()); - }); - interpreter.apply_changes(wasm_bindgen::memory()); - - self.state.emitter.reset(); - self.state.traversal.reset(); - } - } + builder } } -/// Traversal methods. impl ChangeListBuilder<'_> { + pub(crate) fn finish(self) { + debug!("emit: reset"); + self.state.interpreter.reset(); + self.state.traversal.reset(); + } + + /// Traversal methods. + pub fn go_down(&mut self) { self.state.traversal.down(); } @@ -134,27 +100,27 @@ impl ChangeListBuilder<'_> { match mv { MoveTo::Parent => { debug!("emit: pop"); - self.state.emitter.pop(); + self.state.interpreter.pop(); } MoveTo::Child(n) => { debug!("emit: push_child({})", n); - self.state.emitter.push_child(n); + self.state.interpreter.push_child(n); } MoveTo::ReverseChild(n) => { debug!("emit: push_reverse_child({})", n); - self.state.emitter.push_reverse_child(n); + self.state.interpreter.push_reverse_child(n); } MoveTo::Sibling(n) => { debug!("emit: pop_push_child({})", n); - self.state.emitter.pop_push_child(n); + self.state.interpreter.pop_push_child(n); } MoveTo::ReverseSibling(n) => { debug!("emit: pop_push_reverse_child({})", n); - self.state.emitter.pop_push_reverse_child(n); + self.state.interpreter.pop_push_reverse_child(n); } MoveTo::TempChild(temp) => { debug!("emit: push_temporary({})", temp); - self.state.emitter.push_temporary(temp); + self.state.interpreter.push_temporary(temp); } } } @@ -184,107 +150,86 @@ impl ChangeListBuilder<'_> { ); self.next_temporary = temp_base + (end - start) as u32; self.state - .emitter + .interpreter .save_children_to_temporaries(temp_base, start as u32, end as u32); temp_base } - pub fn push_temporary(&self, temp: u32) { + pub fn push_temporary(&mut self, temp: u32) { debug_assert!(self.traversal_is_committed()); debug!("emit: push_temporary({})", temp); - self.state.emitter.push_temporary(temp); + self.state.interpreter.push_temporary(temp); } - pub fn remove_child(&self, child: usize) { + pub fn remove_child(&mut self, child: usize) { debug_assert!(self.traversal_is_committed()); debug!("emit: remove_child({})", child); - self.state.emitter.remove_child(child as u32); + self.state.interpreter.remove_child(child as u32); } - pub fn insert_before(&self) { + pub fn insert_before(&mut self) { debug_assert!(self.traversal_is_committed()); debug!("emit: insert_before()"); - self.state.emitter.insert_before(); - } - - pub fn ensure_string(&mut self, string: &str) -> StringKey { - self.state - .strings - .ensure_string(string, &self.state.emitter) + self.state.interpreter.insert_before(); } - pub fn set_text(&self, text: &str) { + pub fn set_text(&mut self, text: &str) { debug_assert!(self.traversal_is_committed()); debug!("emit: set_text({:?})", text); - self.state - .emitter - .set_text(text.as_ptr() as u32, text.len() as u32); + self.state.interpreter.set_text(text); } - pub fn remove_self_and_next_siblings(&self) { + pub fn remove_self_and_next_siblings(&mut self) { debug_assert!(self.traversal_is_committed()); debug!("emit: remove_self_and_next_siblings()"); - self.state.emitter.remove_self_and_next_siblings(); + self.state.interpreter.remove_self_and_next_siblings(); } - pub fn replace_with(&self) { + pub fn replace_with(&mut self) { debug_assert!(self.traversal_is_committed()); debug!("emit: replace_with()"); - self.state.emitter.replace_with(); + self.state.interpreter.replace_with(); } pub fn set_attribute(&mut self, name: &str, value: &str, is_namespaced: bool) { debug_assert!(self.traversal_is_committed()); if name == "class" && !is_namespaced { - let class_id = self.ensure_string(value); debug!("emit: set_class({:?})", value); - self.state.emitter.set_class(class_id.into()); + self.state.interpreter.set_class(value); } else { - let name_id = self.ensure_string(name); - let value_id = self.ensure_string(value); debug!("emit: set_attribute({:?}, {:?})", name, value); - self.state - .emitter - .set_attribute(name_id.into(), value_id.into()); + self.state.interpreter.set_attribute(name, value); } } pub fn remove_attribute(&mut self, name: &str) { debug_assert!(self.traversal_is_committed()); debug!("emit: remove_attribute({:?})", name); - let name_id = self.ensure_string(name); - self.state.emitter.remove_attribute(name_id.into()); + self.state.interpreter.remove_attribute(name); } - pub fn append_child(&self) { + pub fn append_child(&mut self) { debug_assert!(self.traversal_is_committed()); debug!("emit: append_child()"); - self.state.emitter.append_child(); + self.state.interpreter.append_child(); } - pub fn create_text_node(&self, text: &str) { + pub fn create_text_node(&mut self, text: &str) { debug_assert!(self.traversal_is_committed()); debug!("emit: create_text_node({:?})", text); - self.state - .emitter - .create_text_node(text.as_ptr() as u32, text.len() as u32); + self.state.interpreter.create_text_node(text); } pub fn create_element(&mut self, tag_name: &str) { debug_assert!(self.traversal_is_committed()); debug!("emit: create_element({:?})", tag_name); - let tag_name_id = self.ensure_string(tag_name); - self.state.emitter.create_element(tag_name_id.into()); + self.state.interpreter.create_element(tag_name); } pub fn create_element_ns(&mut self, tag_name: &str, ns: &str) { debug_assert!(self.traversal_is_committed()); debug!("emit: create_element_ns({:?}, {:?})", tag_name, ns); - let tag_name_id = self.ensure_string(tag_name); - let ns_id = self.ensure_string(ns); - self.state - .emitter - .create_element_ns(tag_name_id.into(), ns_id.into()); + self.state.interpreter.create_element_ns(tag_name, ns); } pub fn push_force_new_listeners(&mut self) -> bool { @@ -303,8 +248,10 @@ impl ChangeListBuilder<'_> { debug!("emit: new_event_listener({:?})", listener); let (a, b) = listener.get_callback_parts(); debug_assert!(a != 0); - let event_id = self.ensure_string(listener.event); - self.state.emitter.new_event_listener(event_id.into(), a, b); + + self.state + .interpreter + .new_event_listener(listener.event, a, b); } pub fn update_event_listener(&mut self, listener: &Listener) { @@ -318,36 +265,34 @@ impl ChangeListBuilder<'_> { debug!("emit: update_event_listener({:?})", listener); let (a, b) = listener.get_callback_parts(); debug_assert!(a != 0); - let event_id = self.ensure_string(listener.event); self.state - .emitter - .update_event_listener(event_id.into(), a, b); + .interpreter + .update_event_listener(listener.event, a, b); } pub fn remove_event_listener(&mut self, event: &str) { debug_assert!(self.traversal_is_committed()); debug!("emit: remove_event_listener({:?})", event); - let event_id = self.ensure_string(event); - self.state.emitter.remove_event_listener(event_id.into()); + + self.state.interpreter.remove_event_listener(event); } #[inline] pub fn has_template(&mut self, id: CacheId) -> bool { - self.state.templates.contains(&id) + self.state.interpreter.has_template(id) } pub fn save_template(&mut self, id: CacheId) { debug_assert!(self.traversal_is_committed()); debug_assert!(!self.has_template(id)); debug!("emit: save_template({:?})", id); - self.state.templates.insert(id); - self.state.emitter.save_template(id.into()); + self.state.interpreter.save_template(id); } pub fn push_template(&mut self, id: CacheId) { debug_assert!(self.traversal_is_committed()); debug_assert!(self.has_template(id)); debug!("emit: push_template({:?})", id); - self.state.emitter.push_template(id.into()); + self.state.interpreter.push_template(id); } } diff --git a/src/change_list/strings.rs b/src/change_list/strings.rs deleted file mode 100644 index d5bb554..0000000 --- a/src/change_list/strings.rs +++ /dev/null @@ -1,66 +0,0 @@ -use crate::change_list::emitter::InstructionEmitter; -use fxhash::FxHashMap; - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct StringKey(u32); - -impl From for u32 { - #[inline] - fn from(key: StringKey) -> u32 { - key.0 - } -} - -#[derive(Debug)] -struct StringsCacheEntry { - key: StringKey, - used: bool, -} - -#[derive(Debug, Default)] -pub(crate) struct StringsCache { - entries: FxHashMap, - next_string_key: u32, -} - -impl StringsCache { - /// Create a new, empty strings cache. - pub fn new() -> StringsCache { - Default::default() - } - - /// Ensure that the given string is cached, and get its key. - pub fn ensure_string(&mut self, string: &str, emitter: &InstructionEmitter) -> StringKey { - if let Some(entry) = self.entries.get_mut(string) { - entry.used = true; - entry.key - } else { - let key = StringKey(self.next_string_key); - self.next_string_key += 1; - let entry = StringsCacheEntry { key, used: true }; - self.entries.insert(string.to_string(), entry); - emitter.add_cached_string(string.as_ptr() as u32, string.len() as u32, key.into()); - key - } - } - - pub fn drop_unused_strings(&mut self, emitter: &InstructionEmitter) { - self.entries.retain(|string, entry| { - if entry.used { - // Since this entry was used during while rendering this frame, - // it is likely going to be used for the next frame as well. So - // we keep it in the cache so that we don't have to repopulate - // and resync the cache next frame (assuming it is reused), but - // we set the `used` flag to false so that if it is not used - // next frame, it will be cleaned up. - entry.used = false; - true - } else { - let key = entry.key.into(); - debug!("emit: drop_cached_string({}) = {:?}", key, string); - emitter.drop_cached_string(key); - false - } - }); - } -} diff --git a/src/diff.rs b/src/diff.rs index 139dda4..18c7442 100644 --- a/src/diff.rs +++ b/src/diff.rs @@ -26,6 +26,8 @@ pub(crate) fn diff( new: &Node, cached_roots: &mut FxHashSet, ) { + debug!("diff {:?} {:?}", new.kind, old.kind); + match (&new.kind, &old.kind) { ( &NodeKind::Text(TextNode { text: new_text }), @@ -78,7 +80,12 @@ pub(crate) fn diff( return; } diff_listeners(change_list, registry, old_listeners, new_listeners); - diff_attributes(change_list, old_attributes, new_attributes, new_namespace.is_some()); + diff_attributes( + change_list, + old_attributes, + new_attributes, + new_namespace.is_some(), + ); diff_children( cached_set, change_list, @@ -204,7 +211,12 @@ fn diff_listeners( // [... node] // // The change list stack is left unchanged. -fn diff_attributes(change_list: &mut ChangeListBuilder, old: &[Attribute], new: &[Attribute], is_namespaced: bool) { +fn diff_attributes( + change_list: &mut ChangeListBuilder, + old: &[Attribute], + new: &[Attribute], + is_namespaced: bool, +) { // Do O(n^2) passes to add/update and remove attributes, since // there are almost always very few attributes. 'outer: for new_attr in new { diff --git a/src/events.rs b/src/events.rs index aac74b1..6ee762b 100644 --- a/src/events.rs +++ b/src/events.rs @@ -26,7 +26,6 @@ cfg_if::cfg_if! { use fxhash::FxHashMap; use std::fmt; use std::mem; - use wasm_bindgen::closure::Closure; use wasm_bindgen::prelude::*; /// The events registry manages event listeners for a virtual DOM. @@ -62,7 +61,7 @@ cfg_if::cfg_if! { })); let weak_registry = Rc::downgrade(®istry); - let closure = Closure::wrap(Box::new(move |event, a, b| { + let closure = move |event: web_sys::Event, a: u32, b: u32| { debug_assert!(a != 0); // if the VdomInnerExclusive is keeping this closure alive, then the @@ -86,9 +85,9 @@ cfg_if::cfg_if! { callback(component, vdom_weak, event); } } - }) as Box); + }; - (registry, closure) + (registry, Box::new(closure)) } pub(crate) fn remove(&mut self, listener: &Listener) { diff --git a/src/lib.rs b/src/lib.rs index a6c7fd6..9a3e8a5 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -123,7 +123,7 @@ cfg_if::cfg_if! { /// An element node in the physical DOM. pub type Element = web_sys::Element; - pub(crate) type EventsTrampoline = wasm_bindgen::closure::Closure; + pub(crate) type EventsTrampoline = Box; } } diff --git a/src/vdom.rs b/src/vdom.rs index c2bbe07..1963cb3 100644 --- a/src/vdom.rs +++ b/src/vdom.rs @@ -61,7 +61,6 @@ pub(crate) struct VdomInnerExclusive { change_list: ManuallyDrop, container: crate::Element, events_registry: Option>>, - events_trampoline: Option, cached_set: crate::RefCell, templates: FxHashMap>, @@ -96,7 +95,6 @@ impl fmt::Debug for VdomInnerExclusive { .field("change_list", &self.change_list) .field("container", &self.container) .field("events_registry", &self.events_registry) - .field("events_trampoline", &"..") .field("current_root", &self.current_root) .finish() } @@ -190,7 +188,6 @@ impl Vdom { container, current_root, events_registry: None, - events_trampoline: None, cached_set: crate::RefCell::new(Default::default()), templates: Default::default(), }), @@ -201,9 +198,7 @@ impl Vdom { { let mut inner = inner.exclusive.borrow_mut(); inner.events_registry = Some(events_registry); - inner.change_list.init_events_trampoline(&events_trampoline); - debug_assert!(inner.events_trampoline.is_none()); - inner.events_trampoline = Some(events_trampoline); + inner.change_list.init_events_trampoline(events_trampoline); // Diff and apply the `contents` against our dummy `
`. inner.render(); diff --git a/tests/web/render.rs b/tests/web/render.rs index 13c3f03..dcfcbc6 100644 --- a/tests/web/render.rs +++ b/tests/web/render.rs @@ -1,7 +1,7 @@ use super::{assert_rendered, before_after, create_element, RenderFn}; use dodrio::{builder::*, bumpalo::collections::String, Node, Render, RenderContext, Vdom}; use std::rc::Rc; -use wasm_bindgen::{ JsCast}; +use wasm_bindgen::JsCast; use wasm_bindgen_test::*; #[wasm_bindgen_test] @@ -37,7 +37,7 @@ fn container_is_emptied_upon_drop() { /// Renders a child with a lifetime scoped to the RenderContext bump arena. #[wasm_bindgen_test] -fn render_bump_scoped_node() { +fn render_bump_scoped_node() { struct Child<'a> { name: &'a str, } @@ -60,9 +60,7 @@ fn render_bump_scoped_node() { } } - let parent = Rc::new(RenderFn(|cx| { - Parent.render(cx) - })); + let parent = Rc::new(RenderFn(|cx| Parent.render(cx))); let container = create_element("div"); let _vdom = Vdom::new(&container, parent.clone()); @@ -71,7 +69,7 @@ fn render_bump_scoped_node() { } /// Originally, dodrio would use the className property for SVGs. -/// +/// /// This is problematic because when SVG elements are created, the className is flagged as a read /// only property, so setting it causes an exception to be thrown. Here's an example of how this /// happens: @@ -83,22 +81,21 @@ fn render_bump_scoped_node() { /// .create_element_ns(Some("http://www.w3.org/2000/svg"), "svg") /// .unwrap(); /// -/// elem.set_class_name("does-not-work"); -/// +/// elem.set_class_name("does-not-work"); +/// /// ----------------------------------------------------------------------------------------------- /// /// wasm-bindgen: imported JS function that was not marked as `catch` threw an error: /// setting getter-only property "className" -/// +/// /// ----------------------------------------------------------------------------------------------- -/// +/// /// Now, dodrio passes the 'class' attribute of all namespaced elements into set_attribute. This /// satisfies the restrictions on SVG and keeps the optimized path for non-namespaced elements #[wasm_bindgen_test(async)] async fn test_svg_set_class() { let container = create_element("div"); - let valid_svg = Rc::new(RenderFn(|cx| { ElementBuilder::new(cx.bump, "svg") .namespace(Some("http://www.w3.org/2000/svg")) @@ -112,8 +109,9 @@ async fn test_svg_set_class() { weak.render().await.unwrap(); assert_eq!( - "works", - container.first_child() + "works", + container + .first_child() .expect("unable to get svg") .dyn_ref::() .expect("svg should be an element")