From 874832e82b76f49c3f4f98c4668f8b5b343e0185 Mon Sep 17 00:00:00 2001 From: Pauan Date: Tue, 20 Feb 2018 20:37:09 -1000 Subject: [PATCH] Initial commit --- .gitignore | 4 + Cargo.toml | 10 + examples/todomvc/Cargo.toml | 8 + examples/todomvc/src/main.rs | 94 ++++ src/dom.rs | 869 +++++++++++++++++++++++++++++++++++ src/lib.rs | 23 + src/signal.rs | 356 ++++++++++++++ 7 files changed, 1364 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 examples/todomvc/Cargo.toml create mode 100644 examples/todomvc/src/main.rs create mode 100644 src/dom.rs create mode 100644 src/lib.rs create mode 100644 src/signal.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a821aa9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ + +/target +**/*.rs.bk +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..11fdbe1 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "dominator" +version = "0.1.0" +authors = ["Pauan "] + +[dependencies] +stdweb = { path = "../stdweb" } +stdweb-derive = { path = "../stdweb/stdweb-derive" } +futures = "0.1.18" +lazy_static = "1.0.0" diff --git a/examples/todomvc/Cargo.toml b/examples/todomvc/Cargo.toml new file mode 100644 index 0000000..f700767 --- /dev/null +++ b/examples/todomvc/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "todomvc" +version = "0.1.0" +authors = ["Pauan "] + +[dependencies] +stdweb = { path = "../../../stdweb" } +dominator = { path = "../.." } diff --git a/examples/todomvc/src/main.rs b/examples/todomvc/src/main.rs new file mode 100644 index 0000000..59ca3b3 --- /dev/null +++ b/examples/todomvc/src/main.rs @@ -0,0 +1,94 @@ +#[macro_use] +extern crate stdweb; + +#[macro_use] +extern crate dominator; + +use std::rc::Rc; +use stdweb::web::{document, HtmlElement}; +use stdweb::web::event::ClickEvent; +use stdweb::web::IParentNode; + +use dominator::{Dom, signal}; +use dominator::signal::Signal; + + +fn main() { + stylesheet!("div", { + style("border", "5px solid black"); + }); + + let foobar = class! { + style("border-right", "10px solid purple"); + }; + + /*let media_query = stylesheet!(format!("@media (max-width: 500px) .{}", foobar), { + style("border-left", "10px solid teal"); + });*/ + + let mut width = 100; + + let (sender, receiver) = signal::unsync::mutable(width); + + html!("div", { + style("border", "10px solid blue"); + children(&mut [ + html!("div", { + style("width", receiver.map(|x| Some(format!("{}px", x)))); + style("height", "50px"); + style("background-color", "green"); + event(move |event: ClickEvent| { + width += 100; + console!(log, &event); + sender.set(width).unwrap(); + }); + }), + + html!("div", { + style("width", "50px"); + style("height", "50px"); + style("background-color", "red"); + children(&mut [ + html!("div", { + style("width", "10px"); + style("height", "10px"); + style("background-color", "orange"); + }) + ]); + }), + + html!("div", { + style("width", "50px"); + style("height", "50px"); + style("background-color", "red"); + class(&foobar, true); + children(&mut [ + html!("div", { + style("width", "10px"); + style("height", "10px"); + style("background-color", "orange"); + }) + ]); + }), + + Dom::with_state(Rc::new(vec![1, 2, 3]), |a| { + html!("div", { + style("width", "100px"); + style("height", "100px"); + style("background-color", "orange"); + class("foo", true); + class("bar", false); + event(clone!({ a } move |event: ClickEvent| { + console!(log, &*a, &event); + })); + }) + }), + + html!("input", { + focused(true); + }), + ]); + }).insert_into( + &document().query_selector("body").unwrap().unwrap() + ); +} diff --git a/src/dom.rs b/src/dom.rs new file mode 100644 index 0000000..e83a88e --- /dev/null +++ b/src/dom.rs @@ -0,0 +1,869 @@ +use std; +use std::rc::Rc; +use std::cell::RefCell; +use std::sync::Mutex; +use stdweb::{Reference, Value, ReferenceType}; +use stdweb::unstable::{TryFrom, TryInto}; +use stdweb::web::{IEventTarget, INode, IElement, IHtmlElement, Node, TextNode}; +use stdweb::web::event::ConcreteEvent; +use signal::{Signal, DropHandle}; + + +// TODO this should be in stdweb +pub trait IStyle: ReferenceType { + // TODO check that the style *actually* was changed + // TODO handle browser prefixes + #[inline] + fn set_style(&self, name: &str, value: &str, important: bool) { + let important = if important { "important" } else { "" }; + + js! { @(no_return) + @{self.as_ref()}.style.setProperty(@{name}, @{value}, @{important}); + } + } +} + +impl IStyle for A {} + + +// TODO this should be in stdweb +#[derive(Clone, Debug, PartialEq, Eq, ReferenceType)] +#[reference(instance_of = "CSSStyleRule")] +pub struct CssStyleRule(Reference); + +impl IStyle for CssStyleRule {} + + +pub mod traits { + use super::IStyle; + use super::internal::Callbacks; + use stdweb::Reference; + use stdweb::web::{INode, IElement, IHtmlElement, TextNode}; + + pub trait DomBuilder { + type Value; + + fn value(&self) -> &Self::Value; + + fn callbacks(&mut self) -> &mut Callbacks; + } + + pub trait DomText { + fn set_text(self, &mut A) + // TODO use an interface rather than TextNode + where A: DomBuilder; + } + + pub trait DomProperty { + fn set_property(self, &mut B, &str) + // TODO it would be nice to be able to remove this Clone constraint somehow + where A: AsRef + Clone + 'static, + B: DomBuilder; + } + + pub trait DomAttribute { + fn set_attribute(self, &mut B, &str, Option<&str>) + // TODO it would be nice to be able to remove this Clone constraint somehow + where A: IElement + Clone + 'static, + B: DomBuilder; + } + + pub trait DomClass { + fn toggle_class(self, &mut B, &str) + // TODO it would be nice to be able to remove this Clone constraint somehow + where A: IElement + Clone + 'static, + B: DomBuilder; + } + + pub trait DomStyle { + fn set_style(self, &mut B, &str, bool) + // TODO it would be nice to be able to remove this Clone constraint somehow + where A: IStyle + Clone + 'static, + B: DomBuilder; + } + + pub trait DomFocused { + fn set_focused(self, &mut B) + // TODO it would be nice to be able to remove this Clone constraint somehow + where A: IHtmlElement + Clone + 'static, + B: DomBuilder; + } + + pub trait DomChildren { + fn insert_children(self, &mut B) + // TODO it would be nice to be able to remove this Clone constraint somehow + where A: INode + Clone + 'static, + B: DomBuilder; + } +} + + +pub mod dom_operations { + use std; + use stdweb::unstable::{TryFrom, TryInto}; + use stdweb::{Value, Reference}; + use stdweb::web::{TextNode, IHtmlElement, IElement}; + + + #[inline] + pub fn create_element_ns(name: &str, namespace: &str) -> A + where >::Error: std::fmt::Debug { + js!( return document.createElementNS(@{namespace}, @{name}); ).try_into().unwrap() + } + + // TODO this should be in stdweb + #[inline] + pub fn set_text(element: &TextNode, value: &str) { + js! { @(no_return) + // http://jsperf.com/textnode-performance + @{element}.data = @{value}; + } + } + + // TODO replace with element.focus() and element.blur() + // TODO make element.focus() and element.blur() inline + #[inline] + pub fn set_focused(element: &A, focused: bool) { + js! { @(no_return) + var element = @{element.as_ref()}; + + if (@{focused}) { + element.focus(); + + } else { + element.blur(); + } + } + } + + #[inline] + pub fn toggle_class(element: &A, name: &str, toggle: bool) { + js! { @(no_return) + @{element.as_ref()}.classList.toggle(@{name}, @{toggle}); + } + } + + + #[inline] + fn _set_attribute_ns(element: &A, name: &str, value: &str, namespace: &str) { + js! { @(no_return) + @{element.as_ref()}.setAttributeNS(@{namespace}, @{name}, @{value}); + } + } + + #[inline] + fn _set_attribute(element: &A, name: &str, value: &str) { + js! { @(no_return) + @{element.as_ref()}.setAttribute(@{name}, @{value}); + } + } + + // TODO check that the attribute *actually* was changed + #[inline] + pub fn set_attribute(element: &A, name: &str, value: &str, namespace: Option<&str>) { + match namespace { + Some(namespace) => _set_attribute_ns(element, name, value, namespace), + None => _set_attribute(element, name, value), + } + } + + + #[inline] + fn _remove_attribute_ns(element: &A, name: &str, namespace: &str) { + js! { @(no_return) + @{element.as_ref()}.removeAttributeNS(@{namespace}, @{name}); + } + } + + #[inline] + fn _remove_attribute(element: &A, name: &str) { + js! { @(no_return) + @{element.as_ref()}.removeAttribute(@{name}); + } + } + + #[inline] + pub fn remove_attribute(element: &A, name: &str, namespace: Option<&str>) { + match namespace { + Some(namespace) => _remove_attribute_ns(element, name, namespace), + None => _remove_attribute(element, name), + } + } + + + // TODO check that the property *actually* was changed ? + #[inline] + pub fn set_property>(obj: &A, name: &str, value: &str) { + js! { @(no_return) + @{obj.as_ref()}[@{name}] = @{value}; + } + } +} + + +pub mod internal { + use std; + + + // TODO replace this with FnOnce later + trait IRemoveCallback { + fn call(self: Box); + } + + impl IRemoveCallback for F { + #[inline] + fn call(self: Box) { + self(); + } + } + + + // TODO replace this with FnOnce later + trait IInsertCallback { + fn call(self: Box, &mut Callbacks); + } + + impl IInsertCallback for F { + #[inline] + fn call(self: Box, callbacks: &mut Callbacks) { + self(callbacks); + } + } + + + pub struct InsertCallback(Box); + + pub struct RemoveCallback(Box); + + + pub struct Callbacks { + pub after_insert: Vec, + pub after_remove: Vec, + // TODO figure out a better way + pub(crate) trigger_remove: bool, + } + + impl Callbacks { + #[inline] + pub fn new() -> Self { + Self { + after_insert: vec![], + after_remove: vec![], + trigger_remove: true, + } + } + + #[inline] + pub fn after_insert(&mut self, callback: A) { + self.after_insert.push(InsertCallback(Box::new(callback))); + } + + #[inline] + pub fn after_remove(&mut self, callback: A) { + self.after_remove.push(RemoveCallback(Box::new(callback))); + } + + // TODO runtime checks to make sure this isn't called multiple times ? + #[inline] + pub fn trigger_after_insert(&mut self) { + let mut callbacks = Callbacks::new(); + + // TODO verify that this is correct + // TODO is this the most efficient way to accomplish this ? + std::mem::swap(&mut callbacks.after_remove, &mut self.after_remove); + + for f in self.after_insert.drain(..) { + f.0.call(&mut callbacks); + } + + self.after_insert.shrink_to_fit(); + + // TODO figure out a better way of verifying this + assert_eq!(callbacks.after_insert.len(), 0); + + // TODO verify that this is correct + std::mem::swap(&mut callbacks.after_remove, &mut self.after_remove); + } + + #[inline] + fn trigger_after_remove(&mut self) { + for f in self.after_remove.drain(..) { + f.0.call(); + } + + // TODO is this a good idea? + self.after_remove.shrink_to_fit(); + } + } + + impl Drop for Callbacks { + #[inline] + fn drop(&mut self) { + if self.trigger_remove { + self.trigger_after_remove(); + } + } + } +} + + +use self::traits::{DomBuilder, DomText, DomProperty, DomAttribute, DomClass, DomStyle, DomFocused, DomChildren}; +use self::internal::Callbacks; + + +// https://developer.mozilla.org/en-US/docs/Web/API/Document/createElementNS#Valid%20Namespace%20URIs +pub const HTML_NAMESPACE: &str = "http://www.w3.org/1999/xhtml"; +pub const SVG_NAMESPACE: &str = "http://www.w3.org/2000/svg"; + + +pub struct TextBuilder { + element: TextNode, + callbacks: Callbacks, +} + +impl DomBuilder for TextBuilder { + type Value = TextNode; + + #[inline] + fn value(&self) -> &Self::Value { + &self.element + } + + #[inline] + fn callbacks(&mut self) -> &mut Callbacks { + &mut self.callbacks + } +} + + +pub struct Dom { + element: Node, + callbacks: Callbacks, +} + +impl Dom { + #[inline] + fn new(element: Node) -> Self { + Self { + element, + callbacks: Callbacks::new(), + } + } + + #[inline] + pub fn empty() -> Self { + // TODO is there a better way of doing this ? + Self::new(js!( return document.createComment(""); ).try_into().unwrap()) + } + + #[inline] + pub fn with_state(mut state: A, initializer: F) -> Dom + where A: 'static, + F: FnOnce(&mut A) -> Dom { + + let mut dom = initializer(&mut state); + + dom.callbacks.after_remove(move || drop(state)); + + dom + } + + // TODO return a Handle + #[inline] + pub fn insert_into(mut self, parent: &A) { + parent.append_child(&self.element); + + self.callbacks.trigger_after_insert(); + + // This prevents it from calling trigger_after_remove + self.callbacks.trigger_remove = false; + } +} + +impl From for Dom { + #[inline] + fn from(dom: A) -> Self { + let mut text = TextBuilder { + element: js!( return document.createTextNode(""); ).try_into().unwrap(), + callbacks: Callbacks::new(), + }; + + dom.set_text(&mut text); + + Self { + element: text.element.into(), + callbacks: text.callbacks, + } + } +} + +impl<'a> From<&'a str> for Dom { + #[inline] + fn from(value: &'a str) -> Self { + Self::new(js!( return document.createTextNode(@{value}); ).try_into().unwrap()) + } +} + + +pub struct HtmlBuilder { + element: A, + callbacks: Callbacks, + // TODO verify this with static types instead ? + has_children: bool, +} + +impl DomBuilder for HtmlBuilder { + type Value = A; + + #[inline] + fn value(&self) -> &Self::Value { + &self.element + } + + #[inline] + fn callbacks(&mut self) -> &mut Callbacks { + &mut self.callbacks + } +} + +// TODO add in SVG nodes +impl HtmlBuilder + where >::Error: std::fmt::Debug { + + #[inline] + pub fn new(name: &str) -> Self { + Self { + element: dom_operations::create_element_ns(name, HTML_NAMESPACE), + callbacks: Callbacks::new(), + has_children: false, + } + } +} + +impl + Clone + 'static> HtmlBuilder { + #[inline] + pub fn property(mut self, name: &str, value: B) -> Self { + value.set_property(&mut self, name); + self + } +} + +impl HtmlBuilder { + // TODO maybe inline this ? + // TODO replace with element.add_event_listener + fn _event(&mut self, listener: F) + where T: ConcreteEvent, + F: FnMut(T) + 'static { + + let element = self.element.as_ref(); + + let listener = js!( + var listener = @{listener}; + @{&element}.addEventListener(@{T::EVENT_TYPE}, listener); + return listener; + ); + + let element = element.clone(); + + self.callbacks.after_remove(move || { + js! { @(no_return) + var listener = @{listener}; + @{element}.removeEventListener(@{T::EVENT_TYPE}, listener); + listener.drop(); + } + }); + } + + #[inline] + pub fn event(mut self, listener: F) -> Self + where T: ConcreteEvent, + F: FnMut(T) + 'static { + self._event(listener); + self + } +} + +impl HtmlBuilder { + #[inline] + pub fn children(mut self, children: B) -> Self { + assert_eq!(self.has_children, false); + self.has_children = true; + + children.insert_children(&mut self); + self + } +} + +impl HtmlBuilder { + #[inline] + pub fn attribute(mut self, name: &str, value: B) -> Self { + value.set_attribute(&mut self, name, None); + self + } + + #[inline] + pub fn attribute_namespace(mut self, name: &str, value: B, namespace: &str) -> Self { + value.set_attribute(&mut self, name, Some(namespace)); + self + } + + #[inline] + pub fn class(mut self, name: &str, value: B) -> Self { + value.toggle_class(&mut self, name); + self + } +} + +impl HtmlBuilder { + #[inline] + pub fn style(mut self, name: &str, value: B) -> Self { + value.set_style(&mut self, name, false); + self + } + + #[inline] + pub fn style_important(mut self, name: &str, value: B) -> Self { + value.set_style(&mut self, name, true); + self + } + + #[inline] + pub fn focused(mut self, value: B) -> Self { + value.set_focused(&mut self); + self + } +} + +impl> From> for Dom { + #[inline] + fn from(dom: HtmlBuilder) -> Self { + Self { + element: dom.element.into(), + callbacks: dom.callbacks, + } + } +} + + +impl<'a> DomProperty for &'a str { + #[inline] + fn set_property, B: DomBuilder>(self, builder: &mut B, name: &str) { + dom_operations::set_property(builder.value(), name, self); + } +} + +impl<'a> DomAttribute for &'a str { + #[inline] + fn set_attribute>(self, builder: &mut B, name: &str, namespace: Option<&str>) { + dom_operations::set_attribute(builder.value(), name, self, namespace); + } +} + +impl DomClass for bool { + #[inline] + fn toggle_class>(self, builder: &mut B, name: &str) { + dom_operations::toggle_class(builder.value(), name, self); + } +} + +impl<'a> DomStyle for &'a str { + #[inline] + fn set_style>(self, builder: &mut B, name: &str, important: bool) { + builder.value().set_style(name, self, important); + } +} + +impl DomFocused for bool { + #[inline] + fn set_focused>(self, builder: &mut B) { + let value = builder.value().clone(); + + // This needs to use `after_insert` because calling `.focus()` on an element before it is in the DOM has no effect + builder.callbacks().after_insert(move |_| { + dom_operations::set_focused(&value, self); + }); + } +} + +// TODO figure out how to make this owned rather than &mut +impl<'a, A: IntoIterator> DomChildren for A { + #[inline] + fn insert_children>(self, builder: &mut C) { + for dom in self.into_iter() { + { + let callbacks = builder.callbacks(); + callbacks.after_insert.append(&mut dom.callbacks.after_insert); + callbacks.after_remove.append(&mut dom.callbacks.after_remove); + } + + builder.value().append_child(&dom.element); + } + } +} + + +impl + 'static> DomProperty for S { + // TODO inline this ? + fn set_property + Clone + 'static, B: DomBuilder>(self, builder: &mut B, name: &str) { + let element = builder.value().clone(); + let name = name.to_owned(); + + let handle = self.for_each(move |value| { + dom_operations::set_property(&element, &name, &value); + }); + + builder.callbacks().after_remove(move || handle.stop()); + } +} + +impl> + 'static> DomAttribute for S { + // TODO inline this ? + fn set_attribute>(self, builder: &mut B, name: &str, namespace: Option<&str>) { + let element = builder.value().clone(); + let name = name.to_owned(); + let namespace = namespace.map(|x| x.to_owned()); + + let handle = self.for_each(move |value| { + // TODO figure out a way to avoid this + let namespace = namespace.as_ref().map(|x| x.as_str()); + + match value { + Some(value) => dom_operations::set_attribute(&element, &name, &value, namespace), + None => dom_operations::remove_attribute(&element, &name, namespace), + } + }); + + builder.callbacks().after_remove(move || handle.stop()); + } +} + +impl + 'static> DomClass for S { + // TODO inline this ? + fn toggle_class>(self, builder: &mut B, name: &str) { + let element = builder.value().clone(); + let name = name.to_owned(); + + let handle = self.for_each(move |value| { + dom_operations::toggle_class(&element, &name, value); + }); + + builder.callbacks().after_remove(move || handle.stop()); + } +} + +impl> + 'static> DomStyle for S { + // TODO inline this ? + fn set_style>(self, builder: &mut B, name: &str, important: bool) { + let element = builder.value().clone(); + let name = name.to_owned(); + + let handle = self.for_each(move |value| { + match value { + Some(value) => element.set_style(&name, &value, important), + None => element.set_style(&name, "", important), + } + }); + + builder.callbacks().after_remove(move || handle.stop()); + } +} + +impl + 'static> DomFocused for S { + // TODO inline this ? + fn set_focused>(self, builder: &mut B) { + let element = builder.value().clone(); + let callbacks = builder.callbacks(); + + // This needs to use `after_insert` because calling `.focus()` on an element before it is in the DOM has no effect + callbacks.after_insert(move |callbacks| { + let handle = self.for_each(move |value| { + dom_operations::set_focused(&element, value); + }); + + // TODO verify that this is correct under all circumstances + callbacks.after_remove(move || handle.stop()); + }); + } +} + + +pub struct StylesheetBuilder { + element: CssStyleRule, + callbacks: Callbacks, +} + +impl StylesheetBuilder { + #[inline] + pub fn new(selector: &str) -> Self { + lazy_static! { + // TODO better static type for this + static ref STYLESHEET: Reference = js!( + // TODO use createElementNS ? + var e = document.createElement("style"); + e.type = "text/css"; + document.head.appendChild(e); + return e.sheet; + ).try_into().unwrap(); + } + + Self { + element: js!( + var stylesheet = @{&*STYLESHEET}; + var length = stylesheet.cssRules.length; + stylesheet.insertRule(@{selector} + "{}", length); + return stylesheet.cssRules[length]; + ).try_into().unwrap(), + callbacks: Callbacks::new(), + } + } + + #[inline] + pub fn style(mut self, name: &str, value: B) -> Self { + value.set_style(&mut self, name, false); + self + } + + #[inline] + pub fn style_important(mut self, name: &str, value: B) -> Self { + value.set_style(&mut self, name, true); + self + } + + // TODO return a Handle + #[inline] + pub fn done(mut self) { + self.callbacks.trigger_after_insert(); + + // This prevents it from calling trigger_after_remove + self.callbacks.trigger_remove = false; + } +} + +impl DomBuilder for StylesheetBuilder { + type Value = CssStyleRule; + + #[inline] + fn value(&self) -> &Self::Value { + &self.element + } + + #[inline] + fn callbacks(&mut self) -> &mut Callbacks { + &mut self.callbacks + } +} + + +pub struct ClassBuilder { + stylesheet: StylesheetBuilder, + class_name: String, +} + +impl ClassBuilder { + #[inline] + pub fn new() -> Self { + let class_name = { + lazy_static! { + // TODO can this be made more efficient ? + static ref CLASS_ID: Mutex = Mutex::new(0); + } + + let mut id = CLASS_ID.lock().unwrap(); + + *id += 1; + + // TODO make this more efficient ? + format!("__class_{}__", id) + }; + + Self { + // TODO make this more efficient ? + stylesheet: StylesheetBuilder::new(&format!(".{}", class_name)), + class_name, + } + } + + #[inline] + pub fn style(mut self, name: &str, value: B) -> Self { + self.stylesheet = self.stylesheet.style(name, value); + self + } + + #[inline] + pub fn style_important(mut self, name: &str, value: B) -> Self { + self.stylesheet = self.stylesheet.style_important(name, value); + self + } + + // TODO return a Handle ? + #[inline] + pub fn done(self) -> String { + self.stylesheet.done(); + self.class_name + } +} + +impl DomBuilder for ClassBuilder { + type Value = CssStyleRule; + + #[inline] + fn value(&self) -> &Self::Value { + self.stylesheet.value() + } + + #[inline] + fn callbacks(&mut self) -> &mut Callbacks { + self.stylesheet.callbacks() + } +} + + +#[macro_export] +macro_rules! html { + ($kind:expr => $t:ty) => { + html!($kind => $t, {}) + }; + ($kind:expr => $t:ty, { $( $name:ident( $( $args:expr ),* ); )* }) => {{ + let a: $crate::HtmlBuilder<$t> = $crate::HtmlBuilder::new($kind)$(.$name($($args),*))*; + let b: $crate::Dom = a.into(); + b + }}; + + ($kind:expr) => { + // TODO need better hygiene for HtmlElement + html!($kind => HtmlElement) + }; + ($kind:expr, { $( $name:ident( $( $args:expr ),* ); )* }) => {{ + // TODO need better hygiene for HtmlElement + html!($kind => HtmlElement, { $( $name( $( $args ),* ); )* }) + }}; +} + + +#[macro_export] +macro_rules! stylesheet { + ($rule:expr) => { + stylesheet!($rule, {}) + }; + ($rule:expr, { $( $name:ident( $( $args:expr ),* ); )* }) => {{ + $crate::StylesheetBuilder::new($rule)$(.$name($($args),*))*.done() + }}; +} + + +#[macro_export] +macro_rules! class { + ($( $name:ident( $( $args:expr ),* ); )*) => {{ + $crate::ClassBuilder::new()$(.$name($($args),*))*.done() + }}; +} + + +// TODO move into stdweb +#[macro_export] +macro_rules! clone { + ({$($x:ident),+} $y:expr) => {{ + $(let $x = $x.clone();)+ + $y + }}; +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..4141559 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,23 @@ +#[macro_use] +extern crate stdweb; + +#[macro_use] +extern crate stdweb_derive; + +#[macro_use] +extern crate lazy_static; + +extern crate futures; + +mod dom; +pub use dom::*; + +pub mod signal; + +#[cfg(test)] +mod tests { + #[test] + fn it_works() { + assert_eq!(2 + 2, 4); + } +} diff --git a/src/signal.rs b/src/signal.rs new file mode 100644 index 0000000..316a4e4 --- /dev/null +++ b/src/signal.rs @@ -0,0 +1,356 @@ +use std::rc::Rc; +use std::cell::Cell; +use futures::{Async, Poll}; +use futures::future::ok; +use futures::stream::Stream; +use stdweb::PromiseFuture; + + +pub trait Signal { + type Value; + + // TODO use Async> to allow the Signal to end ? + fn poll(&mut self) -> Async; + + #[inline] + fn to_stream(self) -> SignalStream + where Self: Sized { + SignalStream { + signal: self, + } + } + + #[inline] + fn map(self, callback: A) -> Map + where A: FnMut(Self::Value) -> B, + Self: Sized { + Map { + signal: self, + callback, + } + } + + #[inline] + fn map_dedupe(self, callback: A) -> MapDedupe + where A: FnMut(&Self::Value) -> B, + Self: Sized { + MapDedupe { + old_value: None, + signal: self, + callback, + } + } + + #[inline] + fn filter_map(self, callback: A) -> FilterMap + where A: FnMut(Self::Value) -> Option, + Self: Sized { + FilterMap { + signal: self, + callback, + first: true, + } + } + + #[inline] + fn flatten(self) -> Flatten + where Self::Value: Signal, + Self: Sized { + Flatten { + signal: self, + inner: None, + } + } + + #[inline] + fn and_then(self, callback: A) -> Flatten> + where A: FnMut(Self::Value) -> B, + B: Signal, + Self: Sized { + self.map(callback).flatten() + } + + // TODO make this more efficient + fn for_each(self, callback: A) -> DropHandle + where A: Fn(Self::Value) + 'static, + Self: Sized + 'static { + + let (handle, stream) = drop_handle(self.to_stream()); + + PromiseFuture::spawn( + stream.for_each(move |value| { + callback(value); + ok(()) + }) + ); + + handle + } +} + + +// TODO figure out a more efficient way to implement this +#[inline] +fn drop_handle(stream: A) -> (DropHandle, DropStream) { + let done: Rc> = Rc::new(Cell::new(false)); + + let drop_handle = DropHandle { + done: done.clone(), + }; + + let drop_stream = DropStream { + done, + stream, + }; + + (drop_handle, drop_stream) +} + + +// TODO rename this to something else ? +#[must_use] +pub struct DropHandle { + done: Rc>, +} + +// TODO change this to use Drop, but it requires some changes to the after_remove callback system +impl DropHandle { + #[inline] + pub fn stop(self) { + self.done.set(true); + } +} + + +struct DropStream { + done: Rc>, + stream: A, +} + +impl Stream for DropStream { + type Item = A::Item; + type Error = A::Error; + + #[inline] + fn poll(&mut self) -> Poll, Self::Error> { + if self.done.get() { + Ok(Async::Ready(None)) + + } else { + self.stream.poll() + } + } +} + + +pub struct SignalStream { + signal: A, +} + +impl Stream for SignalStream { + type Item = A::Value; + // TODO use Void instead ? + type Error = (); + + #[inline] + fn poll(&mut self) -> Poll, Self::Error> { + Ok(self.signal.poll().map(Some)) + } +} + + +pub struct Map { + signal: A, + callback: B, +} + +impl Signal for Map + where A: Signal, + B: FnMut(A::Value) -> C { + type Value = C; + + #[inline] + fn poll(&mut self) -> Async { + self.signal.poll().map(|value| (self.callback)(value)) + } +} + + +pub struct MapDedupe { + old_value: Option, + signal: A, + callback: B, +} + +impl Signal for MapDedupe + where A: Signal, + A::Value: PartialEq, + // TODO should this use Fn instead ? + B: FnMut(&A::Value) -> C { + + type Value = C; + + // TODO should this use #[inline] ? + fn poll(&mut self) -> Async { + loop { + match self.signal.poll() { + Async::Ready(value) => { + let has_changed = match self.old_value { + Some(ref old_value) => *old_value != value, + None => true, + }; + + if has_changed { + let output = (self.callback)(&value); + self.old_value = Some(value); + return Async::Ready(output); + } + }, + Async::NotReady => return Async::NotReady, + } + } + } +} + + +pub struct FilterMap { + signal: A, + callback: B, + first: bool, +} + +impl Signal for FilterMap + where A: Signal, + B: FnMut(A::Value) -> Option { + type Value = Option; + + // TODO should this use #[inline] ? + #[inline] + fn poll(&mut self) -> Async { + loop { + match self.signal.poll() { + Async::Ready(value) => match (self.callback)(value) { + Some(value) => { + self.first = false; + return Async::Ready(Some(value)); + }, + None => if self.first { + self.first = false; + return Async::Ready(None); + }, + }, + Async::NotReady => return Async::NotReady, + } + } + } +} + + +pub struct Flatten { + signal: A, + inner: Option, +} + +impl Signal for Flatten + where A: Signal, + A::Value: Signal { + type Value = <::Value as Signal>::Value; + + #[inline] + fn poll(&mut self) -> Async { + match self.signal.poll() { + Async::Ready(mut inner) => { + let poll = inner.poll(); + self.inner = Some(inner); + poll + }, + + Async::NotReady => match self.inner { + Some(ref mut inner) => inner.poll(), + None => Async::NotReady, + }, + } + } +} + + +// TODO verify that this is correct +pub mod unsync { + use super::Signal; + use std::rc::{Rc, Weak}; + use std::cell::RefCell; + use futures::Async; + use futures::task; + + + struct Inner { + value: Option, + task: Option, + } + + + pub struct Sender { + inner: Weak>>, + } + + impl Sender { + pub fn set(&self, value: A) -> Result<(), A> { + if let Some(inner) = self.inner.upgrade() { + let mut inner = inner.borrow_mut(); + + inner.value = Some(value); + + if let Some(task) = inner.task.take() { + drop(inner); + task.notify(); + } + + Ok(()) + + } else { + Err(value) + } + } + } + + + pub struct Receiver { + inner: Rc>>, + } + + impl Signal for Receiver { + type Value = A; + + #[inline] + fn poll(&mut self) -> Async { + let mut inner = self.inner.borrow_mut(); + + // TODO is this correct ? + match inner.value.take() { + Some(value) => Async::Ready(value), + None => { + inner.task = Some(task::current()); + Async::NotReady + }, + } + } + } + + + pub fn mutable(initial_value: A) -> (Sender, Receiver) { + let inner = Rc::new(RefCell::new(Inner { + value: Some(initial_value), + task: None, + })); + + let sender = Sender { + inner: Rc::downgrade(&inner), + }; + + let receiver = Receiver { + inner, + }; + + (sender, receiver) + } +}