From fae50b2401ab6f4117ddb71a6a7191e1576e81e4 Mon Sep 17 00:00:00 2001 From: Pauan Date: Fri, 21 Jun 2019 01:57:47 +0200 Subject: [PATCH] Adding in interning system for strings --- Cargo.toml | 32 ++--- examples/animation/src/lib.rs | 2 +- examples/animation/webpack.config.js | 8 +- examples/counter/webpack.config.js | 8 +- examples/todomvc/Cargo.toml | 1 + examples/todomvc/src/lib.rs | 18 +-- examples/todomvc/webpack.config.js | 8 +- src/animation.rs | 8 +- src/bindings.rs | 145 ++++++++++++++++++++ src/cache.rs | 33 +++++ src/dom.rs | 189 +++++++++++++++------------ src/dom_operations.rs | 100 -------------- src/events.rs | 1 + src/lib.rs | 4 +- src/operations.rs | 83 +++--------- src/routing.rs | 133 +++++++------------ src/utils.rs | 10 +- 17 files changed, 398 insertions(+), 385 deletions(-) create mode 100644 src/bindings.rs create mode 100644 src/cache.rs delete mode 100644 src/dom_operations.rs diff --git a/Cargo.toml b/Cargo.toml index 5f495e7..531718d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,36 +30,36 @@ features = ["futures_0_3"] [dependencies.web-sys] version = "0.3.22" features = [ - "CharacterData", + #"CharacterData", "Comment", - "CssRule", - "CssRuleList", - "CssStyleDeclaration", + #"CssRule", + #"CssRuleList", + #"CssStyleDeclaration", "CssStyleRule", "CssStyleSheet", - "Document", - "DocumentFragment", - "DomTokenList", + #"Document", + #"DocumentFragment", + #"DomTokenList", "Element", - "Event", - "EventTarget", + #"Event", + #"EventTarget", "FocusEvent", - "History", + #"History", "InputEvent", "HtmlElement", - "HtmlHeadElement", + #"HtmlHeadElement", "HtmlInputElement", - "HtmlStyleElement", + #"HtmlStyleElement", "HtmlTextAreaElement", "KeyboardEvent", - "Location", + #"Location", "MouseEvent", "Node", - "NodeList", - "StyleSheet", + #"NodeList", + #"StyleSheet", "SvgElement", "Text", - "Url", + #"Url", "Window", ] diff --git a/examples/animation/src/lib.rs b/examples/animation/src/lib.rs index 032fbbe..bb564fd 100644 --- a/examples/animation/src/lib.rs +++ b/examples/animation/src/lib.rs @@ -98,7 +98,7 @@ impl Drop for State { // TODO move this into gloo fn set_interval(ms: i32, f: F) where F: FnMut() + 'static { - let f = wasm_bindgen::closure::Closure::wrap(Box::new(f) as Box); + let f = wasm_bindgen::closure::Closure::wrap(Box::new(f) as Box); web_sys::window() .unwrap_throw() diff --git a/examples/animation/webpack.config.js b/examples/animation/webpack.config.js index d33e9cb..e1bb304 100644 --- a/examples/animation/webpack.config.js +++ b/examples/animation/webpack.config.js @@ -18,10 +18,7 @@ module.exports = { liveReload: true, open: true, noInfo: true, - overlay: { - warnings: true, - errors: true - } + overlay: true }, plugins: [ new CopyPlugin([ @@ -29,7 +26,8 @@ module.exports = { ]), new WasmPackPlugin({ - crateDirectory: __dirname + crateDirectory: __dirname, + extraArgs: "--out-name index" }) ] }; diff --git a/examples/counter/webpack.config.js b/examples/counter/webpack.config.js index d33e9cb..e1bb304 100644 --- a/examples/counter/webpack.config.js +++ b/examples/counter/webpack.config.js @@ -18,10 +18,7 @@ module.exports = { liveReload: true, open: true, noInfo: true, - overlay: { - warnings: true, - errors: true - } + overlay: true }, plugins: [ new CopyPlugin([ @@ -29,7 +26,8 @@ module.exports = { ]), new WasmPackPlugin({ - crateDirectory: __dirname + crateDirectory: __dirname, + extraArgs: "--out-name index" }) ] }; diff --git a/examples/todomvc/Cargo.toml b/examples/todomvc/Cargo.toml index 927dfbc..48bd77f 100644 --- a/examples/todomvc/Cargo.toml +++ b/examples/todomvc/Cargo.toml @@ -29,6 +29,7 @@ features = ["rc"] version = "0.3.22" features = [ "Storage", + "Url", ] [target."cfg(debug_assertions)".dependencies] diff --git a/examples/todomvc/src/lib.rs b/examples/todomvc/src/lib.rs index d3c0d32..e311742 100644 --- a/examples/todomvc/src/lib.rs +++ b/examples/todomvc/src/lib.rs @@ -3,7 +3,7 @@ use std::cell::Cell; use wasm_bindgen::prelude::*; use serde_derive::{Serialize, Deserialize}; -use web_sys::{window, HtmlElement, Storage}; +use web_sys::{window, HtmlElement, Storage, Url}; use futures_signals::map_ref; use futures_signals::signal::{Signal, SignalExt, Mutable}; use futures_signals::signal_vec::{SignalVecExt, MutableVec}; @@ -37,13 +37,15 @@ enum Filter { impl Filter { fn signal() -> impl Signal { - routing::url().map(|url| { - match url.hash().as_str() { - "#/active" => Filter::Active, - "#/completed" => Filter::Completed, - _ => Filter::All, - } - }) + routing::url() + .signal_ref(|url| Url::new(&url).unwrap_throw()) + .map(|url| { + match url.hash().as_str() { + "#/active" => Filter::Active, + "#/completed" => Filter::Completed, + _ => Filter::All, + } + }) } #[inline] diff --git a/examples/todomvc/webpack.config.js b/examples/todomvc/webpack.config.js index d33e9cb..e1bb304 100644 --- a/examples/todomvc/webpack.config.js +++ b/examples/todomvc/webpack.config.js @@ -18,10 +18,7 @@ module.exports = { liveReload: true, open: true, noInfo: true, - overlay: { - warnings: true, - errors: true - } + overlay: true }, plugins: [ new CopyPlugin([ @@ -29,7 +26,8 @@ module.exports = { ]), new WasmPackPlugin({ - crateDirectory: __dirname + crateDirectory: __dirname, + extraArgs: "--out-name index" }) ] }; diff --git a/src/animation.rs b/src/animation.rs index d805f3f..aa1bc50 100644 --- a/src/animation.rs +++ b/src/animation.rs @@ -374,7 +374,7 @@ impl SignalVec for AnimatedMap }, VecDiff::UpdateAt { index, value } => { - let index = self.find_index(index).expect("Could not find value"); + let index = self.find_index(index).unwrap_throw(); let state = { let state = &self.as_mut().animations()[index]; AnimatedMapBroadcaster(state.animation.raw_clone()) @@ -386,7 +386,7 @@ impl SignalVec for AnimatedMap // TODO test this // TODO should this be treated as a removal + insertion ? VecDiff::Move { old_index, new_index } => { - let old_index = self.find_index(old_index).expect("Could not find value"); + let old_index = self.find_index(old_index).unwrap_throw(); let state = self.as_mut().animations().remove(old_index); @@ -398,7 +398,7 @@ impl SignalVec for AnimatedMap }, VecDiff::RemoveAt { index } => { - let index = self.find_index(index).expect("Could not find value"); + let index = self.find_index(index).unwrap_throw(); if self.as_mut().should_remove(cx, index) { self.remove_index(index) @@ -409,7 +409,7 @@ impl SignalVec for AnimatedMap }, VecDiff::Pop {} => { - let index = self.find_last_index().expect("Cannot pop from empty vec"); + let index = self.find_last_index().unwrap_throw(); if self.as_mut().should_remove(cx, index) { self.remove_index(index) diff --git a/src/bindings.rs b/src/bindings.rs new file mode 100644 index 0000000..cab3212 --- /dev/null +++ b/src/bindings.rs @@ -0,0 +1,145 @@ +use wasm_bindgen::prelude::*; +use js_sys::JsString; +use web_sys::{HtmlElement, Element, Node, Window, Text, Comment, CssStyleSheet, CssStyleRule}; + +use crate::cache::intern; + + +#[wasm_bindgen(inline_js = " + export function body() { return document.body; } + export function _window() { return window; } + + export function ready_state() { return document.readyState; } + + export function current_url() { return location.href; } + export function go_to_url(url) { history.pushState(null, \"\", url); } + + export function create_stylesheet() { + // TODO use createElementNS ? + var e = document.createElement(\"style\"); + e.type = \"text/css\"; + document.head.appendChild(e); + return e.sheet; + } + + export function make_style_rule(sheet, selector) { + var rules = sheet.cssRules; + var length = rules.length; + sheet.insertRule(selector + \" {}\", length); + return rules[length]; + } + + export function create_element(name) { return document.createElement(name); } + export function create_element_ns(namespace, name) { return document.createElementNS(namespace, name); } + + export function create_text_node(value) { return document.createTextNode(value); } + + // http://jsperf.com/textnode-performance + export function set_text(elem, value) { elem.data = value; } + + export function create_comment(value) { return document.createComment(value); } + + export function set_attribute(elem, key, value) { elem.setAttribute(key, value); } + export function set_attribute_ns(elem, namespace, key, value) { elem.setAttributeNS(namespace, key, value); } + + export function remove_attribute(elem, key) { elem.removeAttribute(key); } + export function remove_attribute_ns(elem, namespace, key) { elem.removeAttributeNS(namespace, key); } + + export function add_class(elem, value) { elem.classList.add(value); } + export function remove_class(elem, value) { elem.classList.remove(value); } + + export function set_text_content(elem, value) { elem.textContent = value; } + + export function get_style(elem, name) { return elem.style.getPropertyValue(name); } + export function remove_style(elem, name) { return elem.style.removeProperty(name); } + + export function set_style(elem, name, value, important) { + elem.style.setProperty(name, value, (important ? \"important\" : \"\")); + } + + export function get_at(parent, index) { return parent.childNodes[index]; } + export function insert_child_before(parent, child, other) { parent.insertBefore(child, other); } + export function replace_child(parent, child, other) { parent.replaceChild(child, other); } + export function append_child(parent, child) { parent.appendChild(child); } + export function remove_child(parent, child) { parent.removeChild(child); } + + export function focus(elem) { elem.focus(); } + export function blur(elem) { elem.blur(); } + + export function set_property(obj, name, value) { obj[name] = value; } +")] +extern "C" { + pub(crate) fn body() -> HtmlElement; + + #[wasm_bindgen(js_name = _window)] + pub(crate) fn window() -> Window; + + pub(crate) fn ready_state() -> JsString; + + pub(crate) fn current_url() -> JsString; + pub(crate) fn go_to_url(url: &JsString); + + pub(crate) fn create_stylesheet() -> CssStyleSheet; + pub(crate) fn make_style_rule(sheet: &CssStyleSheet, selector: &JsString) -> CssStyleRule; + + pub(crate) fn create_element(name: &JsString) -> Element; + pub(crate) fn create_element_ns(namespace: &JsString, name: &JsString) -> Element; + + pub(crate) fn create_text_node(value: &JsString) -> Text; + pub(crate) fn set_text(elem: &Text, value: &JsString); + + pub(crate) fn create_comment(value: &JsString) -> Comment; + + // TODO check that the attribute *actually* was changed + pub(crate) fn set_attribute(elem: &Element, key: &JsString, value: &JsString); + pub(crate) fn set_attribute_ns(elem: &Element, namespace: &JsString, key: &JsString, value: &JsString); + + pub(crate) fn remove_attribute(elem: &Element, key: &JsString); + pub(crate) fn remove_attribute_ns(elem: &Element, namespace: &JsString, key: &JsString); + + pub(crate) fn add_class(elem: &Element, value: &JsString); + pub(crate) fn remove_class(elem: &Element, value: &JsString); + + pub(crate) fn set_text_content(elem: &Node, value: &JsString); + + // TODO better type for elem + pub(crate) fn get_style(elem: &JsValue, name: &JsString) -> JsString; + pub(crate) fn remove_style(elem: &JsValue, name: &JsString); + pub(crate) fn set_style(elem: &JsValue, name: &JsString, value: &JsString, important: bool); + + pub(crate) fn get_at(parent: &Node, index: u32) -> Node; + pub(crate) fn insert_child_before(parent: &Node, child: &Node, other: &Node); + pub(crate) fn replace_child(parent: &Node, child: &Node, other: &Node); + pub(crate) fn append_child(parent: &Node, child: &Node); + pub(crate) fn remove_child(parent: &Node, child: &Node); + + pub(crate) fn focus(elem: &HtmlElement); + pub(crate) fn blur(elem: &HtmlElement); + + // TODO maybe use Object for obj ? + pub(crate) fn set_property(obj: &JsValue, name: &JsString, value: &JsValue); +} + + +#[inline] +pub(crate) fn remove_all_children(node: &Node) { + set_text_content(node, &intern("")); +} + +// TODO make this more efficient +#[inline] +pub(crate) fn insert_at(parent: &Node, index: u32, child: &Node) { + insert_child_before(parent, child, &get_at(parent, index)); +} + +// TODO make this more efficient +#[inline] +pub(crate) fn update_at(parent: &Node, index: u32, child: &Node) { + replace_child(parent, child, &get_at(parent, index)); +} + +// TODO make this more efficient +#[inline] +pub(crate) fn remove_at(parent: &Node, index: u32) { + remove_child(parent, &get_at(parent, index)); +} diff --git a/src/cache.rs b/src/cache.rs new file mode 100644 index 0000000..854ae3e --- /dev/null +++ b/src/cache.rs @@ -0,0 +1,33 @@ +use std::cell::RefCell; +use std::collections::HashMap; +use js_sys::JsString; + +thread_local! { + // TODO is it possible to avoid the RefCell ? + static CACHE: RefCell> = RefCell::new(HashMap::new()); +} + +// TODO make this more efficient +pub(crate) fn intern(x: &str) -> JsString { + CACHE.with(|cache| { + let mut cache = cache.borrow_mut(); + + match cache.get(x) { + Some(value) => value.clone(), + None => { + let js = JsString::from(x); + cache.insert(x.to_owned(), js.clone()); + js + }, + } + }) +} + + +#[doc(hidden)] +pub fn debug_cache() { + CACHE.with(|cache| { + let cache = cache.borrow(); + web_sys::console::log_1(&wasm_bindgen::JsValue::from(format!("{:#?}", cache.keys()))); + }); +} diff --git a/src/dom.rs b/src/dom.rs index 85ba5ac..b5d5dfb 100644 --- a/src/dom.rs +++ b/src/dom.rs @@ -12,16 +12,17 @@ use futures_util::FutureExt; use futures_channel::oneshot; use discard::{Discard, DiscardOnDrop}; use wasm_bindgen::{JsValue, UnwrapThrowExt, JsCast}; -use js_sys::Reflect; -use web_sys::{window, HtmlElement, Node, EventTarget, Element, CssStyleSheet, HtmlStyleElement, CssStyleRule, CssStyleDeclaration}; +use js_sys::JsString; +use web_sys::{HtmlElement, Node, EventTarget, Element, CssStyleSheet, CssStyleRule}; use gloo::events::{EventListener, EventListenerOptions}; +use crate::cache::intern; +use crate::bindings; use crate::callbacks::Callbacks; use crate::traits::*; use crate::operations; use crate::operations::{for_each, spawn_future}; -use crate::dom_operations; -use crate::utils::{document, on, on_with_options, ValueDiscard, FnDiscard, EventDiscard}; +use crate::utils::{on, on_with_options, ValueDiscard, FnDiscard, EventDiscard}; pub struct RefFn where B: ?Sized { @@ -92,7 +93,7 @@ lazy_static! { // TODO should return HtmlBodyElement ? pub fn body() -> HtmlElement { - document().body().unwrap_throw() + bindings::body() } @@ -104,14 +105,14 @@ pub struct DomHandle { impl Discard for DomHandle { #[inline] fn discard(self) { - self.parent.remove_child(&self.dom.element).unwrap_throw(); + bindings::remove_child(&self.parent, &self.dom.element); self.dom.callbacks.discard(); } } #[inline] pub fn append_dom(parent: &Node, mut dom: Dom) -> DomHandle { - parent.append_child(&dom.element).unwrap_throw(); + bindings::append_child(&parent, &dom.element); dom.callbacks.trigger_after_insert(); @@ -141,7 +142,7 @@ impl Signal for IsWindowLoaded { fn poll_change(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll> { let result = match *self { IsWindowLoaded::Initial {} => { - let is_ready = document().ready_state() == "complete"; + let is_ready = bindings::ready_state() == "complete"; if is_ready { Poll::Ready(Some(true)) @@ -151,7 +152,7 @@ impl Signal for IsWindowLoaded { *self = IsWindowLoaded::Pending { receiver, - _event: EventListener::once(&window().unwrap_throw(), "load", move |_| { + _event: EventListener::once(&bindings::window(), "load", move |_| { // TODO test this sender.send(Some(true)).unwrap_throw(); }), @@ -185,7 +186,7 @@ pub fn is_window_loaded() -> impl Signal { #[inline] pub fn text(value: &str) -> Dom { - Dom::new(document().create_text_node(value).into()) + Dom::new(bindings::create_text_node(&intern(value)).into()) } @@ -194,7 +195,7 @@ pub fn text_signal(value: B) -> Dom where A: AsStr, B: Signal + 'static { - let element = document().create_text_node(""); + let element = bindings::create_text_node(&intern("")); let mut callbacks = Callbacks::new(); @@ -204,8 +205,8 @@ pub fn text_signal(value: B) -> Dom callbacks.after_remove(for_each(value, move |value| { let value = value.as_str(); - // http://jsperf.com/textnode-performance - element.set_data(value); + // TODO maybe this should intern ? + bindings::set_text(&element, &JsString::from(value)); })); } @@ -236,7 +237,7 @@ impl Dom { #[inline] pub fn empty() -> Self { // TODO is there a better way of doing this ? - Self::new(document().create_comment("").into()) + Self::new(bindings::create_comment(&intern("")).into()) } #[inline] @@ -255,12 +256,12 @@ impl Dom { #[inline] pub fn create_element(name: &str) -> A where A: JsCast { - document().create_element(name).unwrap_throw().dyn_into().unwrap_throw() + bindings::create_element(&intern(name)).dyn_into().unwrap_throw() } #[inline] pub fn create_element_ns(name: &str, namespace: &str) -> A where A: JsCast { - document().create_element_ns(Some(namespace), name).unwrap_throw().dyn_into().unwrap_throw() + bindings::create_element_ns(&intern(namespace), &intern(name)).dyn_into().unwrap_throw() } @@ -289,37 +290,50 @@ fn set_option(element: A, callbacks: &mut Callbacks, value: D, mu })); } -fn set_style(style: &CssStyleDeclaration, name: &A, value: B, important: bool) +fn set_style(element: &JsValue, name: &A, value: B, important: bool, should_intern: bool) where A: MultiStr, B: MultiStr { let mut names = vec![]; let mut values = vec![]; - fn try_set_style(style: &CssStyleDeclaration, names: &mut Vec, values: &mut Vec, name: &str, value: &str, important: bool) -> Result { - assert!(value != ""); + fn try_set_style(element: &JsValue, names: &mut Vec, values: &mut Vec, name: &JsString, value: &JsString, important: bool) -> bool { + // TODO move this out of this function ? + let empty = intern(""); + + assert!(*value != empty); // TODO handle browser prefixes ? - style.remove_property(name)?; + bindings::remove_style(element, name); - style.set_property_with_priority(name, value, if important { "important" } else { "" })?; + bindings::set_style(element, name, value, important); // TODO maybe use cfg(debug_assertions) ? - let is_changed = style.get_property_value(name)? != ""; + let is_changed = bindings::get_style(element, name) != empty; if is_changed { - Ok(true) + true } else { - names.push(name.to_string()); - values.push(value.to_string()); - Ok(false) + names.push(String::from(name)); + values.push(String::from(value)); + false } } let okay = name.any(|name| { + let name = intern(name); + value.any(|value| { - try_set_style(style, &mut names, &mut values, name, value, important).unwrap_throw() + // TODO maybe always intern the value ? + let value = if should_intern { + intern(value) + + } else { + JsString::from(value) + }; + + try_set_style(element, &mut names, &mut values, &name, &value, important) }) }); @@ -329,21 +343,24 @@ fn set_style(style: &CssStyleDeclaration, name: &A, value: B, important: b } } -fn set_style_signal(style: CssStyleDeclaration, callbacks: &mut Callbacks, name: A, value: D, important: bool) +fn set_style_signal(element: &JsValue, callbacks: &mut Callbacks, name: A, value: D, important: bool) where A: MultiStr + 'static, B: MultiStr, C: OptionStr, D: Signal + 'static { - set_option(style, callbacks, value, move |style, value| { + // TODO verify that this is always a cheap O(1) clone + let element = element.clone(); + + set_option(element, callbacks, value, move |element, value| { match value { Some(value) => { - set_style(style, &name, value, important); + set_style(element, &name, value, important, false); }, None => { name.each(|name| { // TODO handle browser prefixes ? - style.remove_property(name).unwrap_throw(); + bindings::remove_style(element, &intern(name)); }); }, } @@ -351,13 +368,13 @@ fn set_style_signal(style: CssStyleDeclaration, callbacks: &mut Call } // TODO check that the property *actually* was changed ? +// TODO maybe use AsRef ? fn set_property(element: &A, name: &B, value: C) where A: AsRef, B: MultiStr, C: Into { let element = element.as_ref(); let value = value.into(); name.each(|name| { - // TODO can this be made more efficient ? - assert!(Reflect::set(element, &JsValue::from(name), &value).unwrap_throw()); + bindings::set_property(element, &intern(name), &value); }); } @@ -400,7 +417,7 @@ impl DomBuilder { pub fn global_event(mut self, listener: F) -> Self where T: StaticEvent, F: FnMut(T) + 'static { - self._event(&window().unwrap_throw(), listener); + self._event(&bindings::window(), listener); self } @@ -531,14 +548,14 @@ impl DomBuilder where A: AsRef { #[inline] pub fn children<'a, B: IntoIterator>(mut self, children: B) -> Self { self.check_children(); - operations::insert_children_iter(self.element.as_ref(), &mut self.callbacks, children).unwrap_throw(); + operations::insert_children_iter(self.element.as_ref(), &mut self.callbacks, children); self } #[inline] pub fn text(mut self, value: &str) -> Self { self.check_children(); - self.element.as_ref().set_text_content(Some(value)); + bindings::set_text_content(self.element.as_ref(), &intern(value)); self } @@ -550,7 +567,8 @@ impl DomBuilder where A: AsRef { self.callbacks.after_remove(for_each(value, move |value| { let value = value.as_str(); - element.set_text_content(Some(value)); + // TODO maybe intern this ? + bindings::set_text_content(&element, &JsString::from(value)); })); } @@ -581,26 +599,35 @@ impl DomBuilder where A: AsRef { impl DomBuilder where A: AsRef { #[inline] pub fn attribute(self, name: B, value: &str) -> Self where B: MultiStr { + let element = self.element.as_ref(); + let value = intern(value); + name.each(|name| { - dom_operations::set_attribute(self.element.as_ref(), name, value).unwrap_throw(); + bindings::set_attribute(element, &intern(name), &value); }); + self } #[inline] pub fn attribute_namespace(self, namespace: &str, name: B, value: &str) -> Self where B: MultiStr { + let element = self.element.as_ref(); + let namespace = intern(namespace); + let value = intern(value); + name.each(|name| { - dom_operations::set_attribute_ns(self.element.as_ref(), namespace, name, value).unwrap_throw(); + bindings::set_attribute_ns(element, &namespace, &intern(name), &value); }); + self } #[inline] pub fn class(self, name: B) -> Self where B: MultiStr { - let list = self.element.as_ref().class_list(); + let element = self.element.as_ref(); name.each(|name| { - dom_operations::add_class(&list, name).unwrap_throw(); + bindings::add_class(element, &intern(name)); }); self @@ -630,14 +657,16 @@ impl DomBuilder where A: AsRef { match value { Some(value) => { let value = value.as_str(); + // TODO should this intern ? + let value = intern(value); name.each(|name| { - dom_operations::set_attribute(element, &name, value).unwrap_throw(); + bindings::set_attribute(element, &intern(name), &value); }); }, None => { name.each(|name| { - dom_operations::remove_attribute(element, &name).unwrap_throw(); + bindings::remove_attribute(element, &intern(name)); }); }, } @@ -662,20 +691,22 @@ impl DomBuilder where A: AsRef { D: OptionStr, E: Signal + 'static { - let namespace = namespace.to_owned(); + let namespace = intern(namespace); set_option(self.element.as_ref().clone(), &mut self.callbacks, value, move |element, value| { match value { Some(value) => { let value = value.as_str(); + // TODO should this intern ? + let value = intern(value); name.each(|name| { - dom_operations::set_attribute_ns(element, &namespace, &name, value).unwrap_throw(); + bindings::set_attribute_ns(element, &namespace, &intern(name), &value); }); }, None => { name.each(|name| { - dom_operations::remove_attribute_ns(element, &namespace, &name).unwrap_throw(); + bindings::remove_attribute_ns(element, &namespace, &intern(name)); }); }, } @@ -698,7 +729,7 @@ impl DomBuilder where A: AsRef { where B: MultiStr + 'static, C: Signal + 'static { - let list = self.element.as_ref().class_list(); + let element = self.element.as_ref().clone(); let mut is_set = false; @@ -708,7 +739,7 @@ impl DomBuilder where A: AsRef { is_set = true; name.each(|name| { - dom_operations::add_class(&list, name).unwrap_throw(); + bindings::add_class(&element, &intern(name)); }); } @@ -717,7 +748,7 @@ impl DomBuilder where A: AsRef { is_set = false; name.each(|name| { - dom_operations::remove_class(&list, name).unwrap_throw(); + bindings::remove_class(&element, &intern(name)); }); } } @@ -759,12 +790,14 @@ impl DomBuilder where A: AsRef { #[inline] pub fn scroll_left_signal(mut self, signal: B) -> Self where B: Signal> + 'static { + // TODO bindings function for this ? self.set_scroll_signal(signal, Element::set_scroll_left); self } #[inline] pub fn scroll_top_signal(mut self, signal: B) -> Self where B: Signal> + 'static { + // TODO bindings function for this ? self.set_scroll_signal(signal, Element::set_scroll_top); self } @@ -775,7 +808,7 @@ impl DomBuilder where A: AsRef { pub fn style(self, name: B, value: C) -> Self where B: MultiStr, C: MultiStr { - set_style(&self.element.as_ref().style(), &name, value, false); + set_style(self.element.as_ref(), &name, value, false, true); self } @@ -783,7 +816,7 @@ impl DomBuilder where A: AsRef { pub fn style_important(self, name: B, value: C) -> Self where B: MultiStr, C: MultiStr { - set_style(&self.element.as_ref().style(), &name, value, true); + set_style(self.element.as_ref(), &name, value, true, true); self } } @@ -796,7 +829,7 @@ impl DomBuilder where A: AsRef { D: OptionStr, E: Signal + 'static { - set_style_signal(self.element.as_ref().style(), &mut self.callbacks, name, value, false); + set_style_signal(self.element.as_ref(), &mut self.callbacks, name, value, false); self } @@ -807,7 +840,7 @@ impl DomBuilder where A: AsRef { D: OptionStr, E: Signal + 'static { - set_style_signal(self.element.as_ref().style(), &mut self.callbacks, name, value, true); + set_style_signal(self.element.as_ref(), &mut self.callbacks, name, value, true); self } @@ -820,7 +853,12 @@ impl DomBuilder where A: AsRef { // This needs to use `after_insert` because calling `.focus()` on an element before it is in the DOM has no effect self.callbacks.after_insert(move |_| { // TODO avoid updating if the focused state hasn't changed ? - dom_operations::set_focused(&element, value).unwrap_throw(); + if value { + bindings::focus(&element); + + } else { + bindings::blur(&element); + } }); self @@ -837,7 +875,12 @@ impl DomBuilder where A: AsRef { // TODO verify that this is correct under all circumstances callbacks.after_remove(for_each(value, move |value| { // TODO avoid updating if the focused state hasn't changed ? - dom_operations::set_focused(&element, value).unwrap_throw(); + if value { + bindings::focus(&element); + + } else { + bindings::blur(&element); + } })); }); } @@ -855,7 +898,7 @@ impl DomBuilder where A: AsRef { // TODO better warning message for must_use #[must_use] pub struct StylesheetBuilder { - element: CssStyleDeclaration, + element: CssStyleRule, callbacks: Callbacks, } @@ -866,30 +909,14 @@ impl StylesheetBuilder { // TODO can this be made faster ? // TODO somehow share this safely between threads ? thread_local! { - static STYLESHEET: CssStyleSheet = { - // TODO use createElementNS ? - let e = document().create_element("style").unwrap_throw(); - // TODO maybe don't use unchecked ? - let e: &HtmlStyleElement = e.unchecked_ref(); - - e.set_type("text/css"); - - document().head().unwrap_throw().append_child(e).unwrap_throw(); - - // TODO maybe don't use unchecked ? - e.sheet().unwrap_throw().unchecked_into() - }; + static STYLESHEET: CssStyleSheet = bindings::create_stylesheet(); } - let element = STYLESHEET.with(|stylesheet| { - let rules = stylesheet.css_rules().unwrap_throw(); + // TODO maybe intern ? + let selector = JsString::from(selector); - let length = rules.length(); - - stylesheet.insert_rule_with_index(&format!("{}{{}}", selector), length).unwrap_throw(); - - // TODO maybe don't use unchecked ? - rules.get(length).unwrap_throw().unchecked_ref::().style() + let element = STYLESHEET.with(move |stylesheet| { + bindings::make_style_rule(stylesheet, &selector) }); Self { @@ -902,7 +929,7 @@ impl StylesheetBuilder { pub fn style(self, name: B, value: C) -> Self where B: MultiStr, C: MultiStr { - set_style(&self.element, &name, value, false); + set_style(&self.element, &name, value, false, true); self } @@ -910,7 +937,7 @@ impl StylesheetBuilder { pub fn style_important(self, name: B, value: C) -> Self where B: MultiStr, C: MultiStr { - set_style(&self.element, &name, value, true); + set_style(&self.element, &name, value, true, true); self } @@ -921,7 +948,7 @@ impl StylesheetBuilder { D: OptionStr, E: Signal + 'static { - set_style_signal(self.element.clone(), &mut self.callbacks, name, value, false); + set_style_signal(&self.element, &mut self.callbacks, name, value, false); self } @@ -932,7 +959,7 @@ impl StylesheetBuilder { D: OptionStr, E: Signal + 'static { - set_style_signal(self.element.clone(), &mut self.callbacks, name, value, true); + set_style_signal(&self.element, &mut self.callbacks, name, value, true); self } diff --git a/src/dom_operations.rs b/src/dom_operations.rs deleted file mode 100644 index 68b7203..0000000 --- a/src/dom_operations.rs +++ /dev/null @@ -1,100 +0,0 @@ -use wasm_bindgen::{JsValue, UnwrapThrowExt}; -use web_sys::{Node, HtmlElement, Element, DomTokenList}; - - -#[inline] -pub(crate) fn get_at(parent: &Node, index: u32) -> Node { - parent.child_nodes().get(index).unwrap_throw() -} - -// TODO make this more efficient -#[inline] -pub(crate) fn move_from_to(parent: &Node, old_index: u32, new_index: u32) -> Result<(), JsValue> { - let child = get_at(parent, old_index); - - parent.remove_child(&child)?; - - insert_at(parent, new_index, &child)?; - - Ok(()) -} - -// TODO make this more efficient -#[inline] -pub(crate) fn insert_at(parent: &Node, index: u32, child: &Node) -> Result<(), JsValue> { - parent.insert_before(child, Some(&get_at(parent, index)))?; - Ok(()) -} - -// TODO make this more efficient -#[inline] -pub(crate) fn update_at(parent: &Node, index: u32, child: &Node) -> Result<(), JsValue> { - parent.replace_child(child, &get_at(parent, index))?; - Ok(()) -} - -// TODO make this more efficient -#[inline] -pub(crate) fn remove_at(parent: &Node, index: u32) -> Result<(), JsValue> { - parent.remove_child(&get_at(parent, index))?; - Ok(()) -} - - -#[inline] -pub(crate) fn set_focused(element: &HtmlElement, focused: bool) -> Result<(), JsValue> { - if focused { - element.focus()?; - - } else { - element.blur()?; - } - - Ok(()) -} - -#[inline] -pub(crate) fn add_class(list: &DomTokenList, name: &str) -> Result<(), JsValue> { - list.add_1(name)?; - Ok(()) -} - -#[inline] -pub(crate) fn remove_class(list: &DomTokenList, name: &str) -> Result<(), JsValue> { - list.remove_1(name)?; - Ok(()) -} - - -// TODO check that the attribute *actually* was changed -#[inline] -pub(crate) fn set_attribute(element: &Element, name: &str, value: &str) -> Result<(), JsValue> { - element.set_attribute(name, value)?; - Ok(()) -} - -// TODO check that the attribute *actually* was changed -#[inline] -pub(crate) fn set_attribute_ns(element: &Element, namespace: &str, name: &str, value: &str) -> Result<(), JsValue> { - element.set_attribute_ns(Some(namespace), name, value)?; - Ok(()) -} - - -#[inline] -pub(crate) fn remove_attribute_ns(element: &Element, namespace: &str, name: &str) -> Result<(), JsValue> { - element.remove_attribute_ns(Some(namespace), name)?; - Ok(()) -} - -#[inline] -pub(crate) fn remove_attribute(element: &Element, name: &str) -> Result<(), JsValue> { - element.remove_attribute(name)?; - Ok(()) -} - - -#[inline] -pub(crate) fn remove_all_children(node: &Node) { - node.set_text_content(None); -} diff --git a/src/events.rs b/src/events.rs index 9682930..092035e 100644 --- a/src/events.rs +++ b/src/events.rs @@ -82,6 +82,7 @@ make_event!(Input, "input" => web_sys::InputEvent); impl Input { // TODO should this work on other types as well ? + // TODO return JsString ? pub fn value(&self) -> Option { let target = self.target()?; diff --git a/src/lib.rs b/src/lib.rs index d20e316..76195c9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,12 +6,14 @@ #[macro_use] mod macros; mod utils; +mod cache; +mod bindings; mod callbacks; mod operations; -mod dom_operations; mod dom; pub use dom::*; +pub use cache::*; pub mod traits; pub mod animation; pub mod routing; diff --git a/src/operations.rs b/src/operations.rs index 752a98b..2ed4f7c 100644 --- a/src/operations.rs +++ b/src/operations.rs @@ -8,13 +8,12 @@ use futures_signals::{cancelable_future, CancelableFutureHandle}; use futures_signals::signal::{Signal, SignalExt}; use futures_signals::signal_vec::{VecDiff, SignalVec, SignalVecExt}; use web_sys::Node; -use wasm_bindgen::{JsValue, UnwrapThrowExt}; +use wasm_bindgen::UnwrapThrowExt; use wasm_bindgen_futures::futures_0_3::spawn_local; -use crate::dom_operations; +use crate::bindings; use crate::dom::Dom; use crate::callbacks::Callbacks; -use crate::utils::document; #[inline] @@ -53,45 +52,15 @@ fn for_each_vec(signal: A, mut callback: B) -> CancelableFutureHandle } -/* -// TODO inline this ? -pub fn insert_children_signal(element: &A, callbacks: &mut Callbacks, signal: C) - where A: INode + Clone + 'static, - B: IntoIterator, - C: Signal + 'static { - - let element = element.clone(); - - let mut old_children: Vec = vec![]; - - let handle = for_each(signal, move |value| { - dom_operations::remove_all_children(&element); - - old_children = value.into_iter().map(|mut dom| { - element.append_child(&dom.element); - - // TODO don't trigger this if the parent isn't inserted into the DOM - dom.callbacks.trigger_after_insert(); - - dom - }).collect(); - }); - - // TODO verify that this will drop `old_children` - callbacks.after_remove(handle); -}*/ - #[inline] -pub(crate) fn insert_children_iter<'a, A: IntoIterator>(element: &Node, callbacks: &mut Callbacks, value: A) -> Result<(), JsValue> { +pub(crate) fn insert_children_iter<'a, A: IntoIterator>(element: &Node, callbacks: &mut Callbacks, value: A) { for dom in value.into_iter() { // TODO can this be made more efficient ? callbacks.after_insert.append(&mut dom.callbacks.after_insert); callbacks.after_remove.append(&mut dom.callbacks.after_remove); - element.append_child(&dom.element)?; + bindings::append_child(element, &dom.element); } - - Ok(()) } @@ -126,12 +95,12 @@ pub(crate) fn insert_children_signal_vec(element: Node, callbacks: &mut Callb }); } - fn process_change(state: &mut State, element: &Node, change: VecDiff) -> Result<(), JsValue> { + fn process_change(state: &mut State, element: &Node, change: VecDiff) { match change { VecDiff::Replace { values } => { // TODO is this correct ? if state.children.len() > 0 { - dom_operations::remove_all_children(element); + bindings::remove_all_children(element); for dom in state.children.drain(..) { dom.callbacks.discard(); @@ -142,25 +111,12 @@ pub(crate) fn insert_children_signal_vec(element: Node, callbacks: &mut Callb let is_inserted = state.is_inserted; - let mut has_inserts = false; - - // TODO is it worth it to use fragments ? - let fragment = document().create_document_fragment(); - for dom in state.children.iter_mut() { dom.callbacks.leak(); - fragment.append_child(&dom.element)?; + bindings::append_child(element, &dom.element); - if !dom.callbacks.after_insert.is_empty() { - has_inserts = true; - } - } - - element.append_child(&fragment)?; - - if is_inserted && has_inserts { - for dom in state.children.iter_mut() { + if is_inserted { dom.callbacks.trigger_after_insert(); } } @@ -168,7 +124,7 @@ pub(crate) fn insert_children_signal_vec(element: Node, callbacks: &mut Callb VecDiff::InsertAt { index, mut value } => { // TODO better usize -> u32 conversion - dom_operations::insert_at(element, index as u32, &value.element)?; + bindings::insert_at(element, index as u32, &value.element); value.callbacks.leak(); @@ -181,7 +137,7 @@ pub(crate) fn insert_children_signal_vec(element: Node, callbacks: &mut Callb }, VecDiff::Push { mut value } => { - element.append_child(&value.element)?; + bindings::append_child(element, &value.element); value.callbacks.leak(); @@ -195,7 +151,7 @@ pub(crate) fn insert_children_signal_vec(element: Node, callbacks: &mut Callb VecDiff::UpdateAt { index, mut value } => { // TODO better usize -> u32 conversion - dom_operations::update_at(element, index as u32, &value.element)?; + bindings::update_at(element, index as u32, &value.element); value.callbacks.leak(); @@ -213,15 +169,16 @@ pub(crate) fn insert_children_signal_vec(element: Node, callbacks: &mut Callb VecDiff::Move { old_index, new_index } => { let value = state.children.remove(old_index); - state.children.insert(new_index, value); - + bindings::remove_child(element, &value.element); // TODO better usize -> u32 conversion - dom_operations::move_from_to(element, old_index as u32, new_index as u32)?; + bindings::insert_at(element, new_index as u32, &value.element); + + state.children.insert(new_index, value); }, VecDiff::RemoveAt { index } => { // TODO better usize -> u32 conversion - dom_operations::remove_at(element, index as u32)?; + bindings::remove_at(element, index as u32); state.children.remove(index).callbacks.discard(); }, @@ -231,7 +188,7 @@ pub(crate) fn insert_children_signal_vec(element: Node, callbacks: &mut Callb // TODO create remove_last_child function ? // TODO better usize -> u32 conversion - dom_operations::remove_at(element, index as u32)?; + bindings::remove_at(element, index as u32); state.children.pop().unwrap_throw().callbacks.discard(); }, @@ -240,7 +197,7 @@ pub(crate) fn insert_children_signal_vec(element: Node, callbacks: &mut Callb // TODO is this correct ? // TODO is this needed, or is it guaranteed by VecDiff ? if state.children.len() > 0 { - dom_operations::remove_all_children(element); + bindings::remove_all_children(element); for dom in state.children.drain(..) { dom.callbacks.discard(); @@ -248,14 +205,12 @@ pub(crate) fn insert_children_signal_vec(element: Node, callbacks: &mut Callb } }, } - - Ok(()) } // TODO verify that this will drop `children` callbacks.after_remove(for_each_vec(signal, move |change| { let mut state = state.lock().unwrap_throw(); - process_change(&mut state, &element, change).unwrap_throw(); + process_change(&mut state, &element, change); })); } diff --git a/src/routing.rs b/src/routing.rs index 1a0add9..834a7b2 100644 --- a/src/routing.rs +++ b/src/routing.rs @@ -1,128 +1,81 @@ -use wasm_bindgen::{JsValue, UnwrapThrowExt}; -use web_sys::{window, Url, EventTarget, HtmlElement}; -use futures_signals::signal::{Mutable, Signal}; +use std::borrow::Cow; + +use js_sys::JsString; +use web_sys::{EventTarget, HtmlElement}; +use lazy_static::lazy_static; +use futures_signals::signal::{Mutable, ReadOnlyMutable}; use gloo::events::EventListener; +use crate::bindings; use crate::dom::{Dom, DomBuilder}; use crate::events; -/*pub struct State { - value: Mutable>, - callback: Value, -} - -impl State { - pub fn new() -> Self { - // TODO replace with stdweb function - let value = Mutable::new(js!( return history.state; ).try_into().unwrap_throw()); - - let callback = |state: Option| { - value.set(state); - }; - - Self { - value, - callback: js!( - var callback = @{callback}; - - addEventListener("popstate", function (e) { - callback(e.state); - }, true); - - return callback; - ), - } - } - - pub fn set(&self, value: A) { - window().history().replace_state(value, "", None).unwrap_throw(); - self.value.set(value); - } -}*/ - - -fn current_url_string() -> Result { - Ok(window().unwrap_throw().location().href()?) -} - // TODO inline ? -fn change_url(mutable: &Mutable) -> Result<(), JsValue> { +fn change_url(mutable: &Mutable) { let mut lock = mutable.lock_mut(); - let new_url = current_url_string()?; + let new_url = String::from(bindings::current_url()); - // TODO test that this doesn't notify if the URLs are the same // TODO helper method for this // TODO can this be made more efficient ? - if lock.href() != new_url { - *lock = Url::new(&new_url)?; + if *lock != new_url { + *lock = new_url; } - - Ok(()) } struct CurrentUrl { - value: Mutable, - _listener: EventListener, + value: Mutable, } impl CurrentUrl { - fn new() -> Result { + fn new() -> Self { // TODO can this be made more efficient ? - let value = Mutable::new(Url::new(¤t_url_string()?)?); + let value = Mutable::new(String::from(bindings::current_url())); - Ok(Self { - _listener: EventListener::new(&window().unwrap_throw(), "popstate", { - let value = value.clone(); - move |_| { - change_url(&value).unwrap_throw(); - } - }), + // TODO clean this up somehow ? + EventListener::new(&bindings::window(), "popstate", { + let value = value.clone(); + move |_| { + change_url(&value); + } + }).forget(); + + Self { value, - }) + } } } -// TODO somehow share this safely between threads ? -thread_local! { - static URL: CurrentUrl = CurrentUrl::new().unwrap_throw(); + +lazy_static! { + static ref URL: CurrentUrl = CurrentUrl::new(); } #[inline] -pub fn current_url() -> Url { - URL.with(|url| url.value.get_cloned()) -} - - -#[inline] -pub fn url() -> impl Signal { - URL.with(|url| url.value.signal_cloned()) +pub fn url() -> ReadOnlyMutable { + URL.value.read_only() } // TODO if URL hasn't been created yet, don't create it #[inline] pub fn go_to_url(new_url: &str) { - window() - .unwrap_throw() - .history() - .unwrap_throw() - // TODO is this the best state object to use ? - .push_state_with_url(&JsValue::NULL, "", Some(new_url)) - .unwrap_throw(); + // TODO intern ? + bindings::go_to_url(&JsString::from(new_url)); - URL.with(|url| { - change_url(&url.value).unwrap_throw(); - }); + change_url(&URL.value); } -// TODO somehow use &str rather than String, maybe Cow ? #[inline] -pub fn on_click_go_to_url(new_url: String) -> impl FnOnce(DomBuilder) -> DomBuilder where A: AsRef { +pub fn on_click_go_to_url(new_url: A) -> impl FnOnce(DomBuilder) -> DomBuilder + where A: Into>, + B: AsRef { + let new_url = new_url.into(); + #[inline] move |dom| { dom.event_preventable(move |e: events::Click| { @@ -134,12 +87,16 @@ pub fn on_click_go_to_url(new_url: String) -> impl FnOnce(DomBuilder) -> D // TODO better type than HtmlElement +// TODO maybe make this a macro ? #[inline] -pub fn link(url: &str, f: F) -> Dom where F: FnOnce(DomBuilder) -> DomBuilder { +pub fn link(url: A, f: F) -> Dom + where A: Into>, + F: FnOnce(DomBuilder) -> DomBuilder { + let url = url.into(); + html!("a", { - .attribute("href", url) - // TODO somehow avoid this allocation - .apply(on_click_go_to_url(url.to_string())) + .attribute("href", &url) + .apply(on_click_go_to_url(url)) .apply(f) }) } diff --git a/src/utils.rs b/src/utils.rs index e5a5466..f648f90 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,8 +1,9 @@ use std::mem::ManuallyDrop; + use discard::Discard; -use wasm_bindgen::UnwrapThrowExt; -use web_sys::{window, Document, EventTarget}; +use web_sys::{EventTarget}; use gloo::events::{EventListener, EventListenerOptions}; + use crate::traits::StaticEvent; @@ -23,11 +24,6 @@ pub(crate) fn on_with_options(element: &EventTarget, options: EventListene } -pub(crate) fn document() -> Document { - window().unwrap_throw().document().unwrap_throw() -} - - // TODO move this into the discard crate // TODO verify that this is correct and doesn't leak memory or cause memory safety pub(crate) struct ValueDiscard(ManuallyDrop);