Adding in interning system for strings

This commit is contained in:
Pauan 2019-06-21 01:57:47 +02:00
parent ec85fef904
commit fae50b2401
17 changed files with 398 additions and 385 deletions

View File

@ -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",
]

View File

@ -98,7 +98,7 @@ impl Drop for State {
// TODO move this into gloo
fn set_interval<F>(ms: i32, f: F) where F: FnMut() + 'static {
let f = wasm_bindgen::closure::Closure::wrap(Box::new(f) as Box<FnMut()>);
let f = wasm_bindgen::closure::Closure::wrap(Box::new(f) as Box<dyn FnMut()>);
web_sys::window()
.unwrap_throw()

View File

@ -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"
})
]
};

View File

@ -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"
})
]
};

View File

@ -29,6 +29,7 @@ features = ["rc"]
version = "0.3.22"
features = [
"Storage",
"Url",
]
[target."cfg(debug_assertions)".dependencies]

View File

@ -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<Item = Self> {
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]

View File

@ -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"
})
]
};

View File

@ -374,7 +374,7 @@ impl<A, F, S> SignalVec for AnimatedMap<S, F>
},
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<A, F, S> SignalVec for AnimatedMap<S, F>
// 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<A, F, S> SignalVec for AnimatedMap<S, F>
},
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<A, F, S> SignalVec for AnimatedMap<S, F>
},
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)

145
src/bindings.rs Normal file
View File

@ -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));
}

33
src/cache.rs Normal file
View File

@ -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<HashMap<String, JsString>> = 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())));
});
}

View File

@ -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<A, B, C> 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<Option<Self::Item>> {
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<Item = bool> {
#[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<A, B>(value: B) -> Dom
where A: AsStr,
B: Signal<Item = A> + '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<A, B>(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<A>(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<A>(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<A, B, C, D, F>(element: A, callbacks: &mut Callbacks, value: D, mu
}));
}
fn set_style<A, B>(style: &CssStyleDeclaration, name: &A, value: B, important: bool)
fn set_style<A, B>(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<String>, values: &mut Vec<String>, name: &str, value: &str, important: bool) -> Result<bool, JsValue> {
assert!(value != "");
fn try_set_style(element: &JsValue, names: &mut Vec<String>, values: &mut Vec<String>, 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<A, B>(style: &CssStyleDeclaration, name: &A, value: B, important: b
}
}
fn set_style_signal<A, B, C, D>(style: CssStyleDeclaration, callbacks: &mut Callbacks, name: A, value: D, important: bool)
fn set_style_signal<A, B, C, D>(element: &JsValue, callbacks: &mut Callbacks, name: A, value: D, important: bool)
where A: MultiStr + 'static,
B: MultiStr,
C: OptionStr<Output = B>,
D: Signal<Item = C> + '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<A, B, C, D>(style: CssStyleDeclaration, callbacks: &mut Call
}
// TODO check that the property *actually* was changed ?
// TODO maybe use AsRef<Object> ?
fn set_property<A, B, C>(element: &A, name: &B, value: C) where A: AsRef<JsValue>, B: MultiStr, C: Into<JsValue> {
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<A> DomBuilder<A> {
pub fn global_event<T, F>(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<A> DomBuilder<A> where A: AsRef<Node> {
#[inline]
pub fn children<'a, B: IntoIterator<Item = &'a mut Dom>>(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<A> DomBuilder<A> where A: AsRef<Node> {
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<A> DomBuilder<A> where A: AsRef<Node> {
impl<A> DomBuilder<A> where A: AsRef<Element> {
#[inline]
pub fn attribute<B>(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<B>(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<B>(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<A> DomBuilder<A> where A: AsRef<Element> {
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<A> DomBuilder<A> where A: AsRef<Element> {
D: OptionStr<Output = C>,
E: Signal<Item = D> + '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<A> DomBuilder<A> where A: AsRef<Element> {
where B: MultiStr + 'static,
C: Signal<Item = bool> + '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<A> DomBuilder<A> where A: AsRef<Element> {
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<A> DomBuilder<A> where A: AsRef<Element> {
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<A> DomBuilder<A> where A: AsRef<Element> {
#[inline]
pub fn scroll_left_signal<B>(mut self, signal: B) -> Self where B: Signal<Item = Option<i32>> + 'static {
// TODO bindings function for this ?
self.set_scroll_signal(signal, Element::set_scroll_left);
self
}
#[inline]
pub fn scroll_top_signal<B>(mut self, signal: B) -> Self where B: Signal<Item = Option<i32>> + 'static {
// TODO bindings function for this ?
self.set_scroll_signal(signal, Element::set_scroll_top);
self
}
@ -775,7 +808,7 @@ impl<A> DomBuilder<A> where A: AsRef<HtmlElement> {
pub fn style<B, C>(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<A> DomBuilder<A> where A: AsRef<HtmlElement> {
pub fn style_important<B, C>(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<A> DomBuilder<A> where A: AsRef<HtmlElement> {
D: OptionStr<Output = C>,
E: Signal<Item = D> + '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<A> DomBuilder<A> where A: AsRef<HtmlElement> {
D: OptionStr<Output = C>,
E: Signal<Item = D> + '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<A> DomBuilder<A> where A: AsRef<HtmlElement> {
// 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<A> DomBuilder<A> where A: AsRef<HtmlElement> {
// 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<A> DomBuilder<A> where A: AsRef<HtmlElement> {
// 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::<CssStyleRule>().style()
let element = STYLESHEET.with(move |stylesheet| {
bindings::make_style_rule(stylesheet, &selector)
});
Self {
@ -902,7 +929,7 @@ impl StylesheetBuilder {
pub fn style<B, C>(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<B, C>(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<Output = C>,
E: Signal<Item = D> + '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<Output = C>,
E: Signal<Item = D> + '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
}

View File

@ -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);
}

View File

@ -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<String> {
let target = self.target()?;

View File

@ -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;

View File

@ -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<A, B>(signal: A, mut callback: B) -> CancelableFutureHandle
}
/*
// TODO inline this ?
pub fn insert_children_signal<A, B, C>(element: &A, callbacks: &mut Callbacks, signal: C)
where A: INode + Clone + 'static,
B: IntoIterator<Item = Dom>,
C: Signal<Item = B> + 'static {
let element = element.clone();
let mut old_children: Vec<Dom> = 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<Item = &'a mut Dom>>(element: &Node, callbacks: &mut Callbacks, value: A) -> Result<(), JsValue> {
pub(crate) fn insert_children_iter<'a, A: IntoIterator<Item = &'a mut Dom>>(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<A>(element: Node, callbacks: &mut Callb
});
}
fn process_change(state: &mut State, element: &Node, change: VecDiff<Dom>) -> Result<(), JsValue> {
fn process_change(state: &mut State, element: &Node, change: VecDiff<Dom>) {
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<A>(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<A>(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<A>(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<A>(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<A>(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<A>(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<A>(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<A>(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);
}));
}

View File

@ -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<A> {
value: Mutable<Option<A>>,
callback: Value,
}
impl<A> State<A> {
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<A>| {
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<String, JsValue> {
Ok(window().unwrap_throw().location().href()?)
}
// TODO inline ?
fn change_url(mutable: &Mutable<Url>) -> Result<(), JsValue> {
fn change_url(mutable: &Mutable<String>) {
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<Url>,
_listener: EventListener,
value: Mutable<String>,
}
impl CurrentUrl {
fn new() -> Result<Self, JsValue> {
fn new() -> Self {
// TODO can this be made more efficient ?
let value = Mutable::new(Url::new(&current_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<Item = Url> {
URL.with(|url| url.value.signal_cloned())
pub fn url() -> ReadOnlyMutable<String> {
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<A>(new_url: String) -> impl FnOnce(DomBuilder<A>) -> DomBuilder<A> where A: AsRef<EventTarget> {
pub fn on_click_go_to_url<A, B>(new_url: A) -> impl FnOnce(DomBuilder<B>) -> DomBuilder<B>
where A: Into<Cow<'static, str>>,
B: AsRef<EventTarget> {
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<A>(new_url: String) -> impl FnOnce(DomBuilder<A>) -> D
// TODO better type than HtmlElement
// TODO maybe make this a macro ?
#[inline]
pub fn link<F>(url: &str, f: F) -> Dom where F: FnOnce(DomBuilder<HtmlElement>) -> DomBuilder<HtmlElement> {
pub fn link<A, F>(url: A, f: F) -> Dom
where A: Into<Cow<'static, str>>,
F: FnOnce(DomBuilder<HtmlElement>) -> DomBuilder<HtmlElement> {
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)
})
}

View File

@ -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<E, F>(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<A>(ManuallyDrop<A>);