From 6d46833eb2a068bd3655859ea828dad04293e5ba Mon Sep 17 00:00:00 2001
From: Héctor Ramón Jiménez <hector0193@gmail.com>
Date: Tue, 4 Feb 2020 03:28:47 +0100
Subject: Support event subscriptions in `iced_web`

Also improves the overall web runtime, avoiding nested update loops.
---
 .gitignore                          |   1 +
 futures/src/runtime.rs              |  20 +++---
 futures/src/subscription/tracker.rs |   7 +-
 src/application.rs                  |   5 ++
 web/src/bus.rs                      |  25 +++----
 web/src/lib.rs                      | 136 +++++++++++++++++-------------------
 web/src/widget/button.rs            |   6 +-
 web/src/widget/checkbox.rs          |   4 +-
 web/src/widget/radio.rs             |   6 +-
 web/src/widget/slider.rs            |   5 +-
 web/src/widget/text_input.rs        |   5 +-
 winit/src/proxy.rs                  |   3 +-
 12 files changed, 103 insertions(+), 120 deletions(-)

diff --git a/.gitignore b/.gitignore
index 99cebb8a..eee98b1e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,4 @@
 pkg/
 **/*.rs.bk
 Cargo.lock
+.cargo/
diff --git a/futures/src/runtime.rs b/futures/src/runtime.rs
index 9fd9899a..3be45a26 100644
--- a/futures/src/runtime.rs
+++ b/futures/src/runtime.rs
@@ -1,7 +1,7 @@
 //! Run commands and keep track of subscriptions.
 use crate::{subscription, Command, Executor, Subscription};
 
-use futures::Sink;
+use futures::{channel::mpsc, Sink};
 use std::marker::PhantomData;
 
 /// A batteries-included runtime of commands and subscriptions.
@@ -27,11 +27,8 @@ where
     Hasher: std::hash::Hasher + Default,
     Event: Send + Clone + 'static,
     Executor: self::Executor,
-    Sender: Sink<Message, Error = core::convert::Infallible>
-        + Unpin
-        + Send
-        + Clone
-        + 'static,
+    Sender:
+        Sink<Message, Error = mpsc::SendError> + Unpin + Send + Clone + 'static,
     Message: Send + 'static,
 {
     /// Creates a new empty [`Runtime`].
@@ -76,12 +73,10 @@ where
         for future in futures {
             let mut sender = self.sender.clone();
 
-            self.executor.spawn(future.then(|message| {
-                async move {
-                    let _ = sender.send(message).await;
+            self.executor.spawn(future.then(|message| async move {
+                let _ = sender.send(message).await;
 
-                    ()
-                }
+                ()
             }));
         }
     }
@@ -112,7 +107,8 @@ where
     /// See [`Tracker::broadcast`] to learn more.
     ///
     /// [`Runtime`]: struct.Runtime.html
-    /// [`Tracker::broadcast`]: subscription/struct.Tracker.html#method.broadcast
+    /// [`Tracker::broadcast`]:
+    /// subscription/struct.Tracker.html#method.broadcast
     pub fn broadcast(&mut self, event: Event) {
         self.subscriptions.broadcast(event);
     }
diff --git a/futures/src/subscription/tracker.rs b/futures/src/subscription/tracker.rs
index c8a1ee18..cfa36170 100644
--- a/futures/src/subscription/tracker.rs
+++ b/futures/src/subscription/tracker.rs
@@ -1,8 +1,7 @@
 use crate::Subscription;
 
-use futures::{future::BoxFuture, sink::Sink};
-use std::collections::HashMap;
-use std::marker::PhantomData;
+use futures::{channel::mpsc, future::BoxFuture, sink::Sink};
+use std::{collections::HashMap, marker::PhantomData};
 
 /// A registry of subscription streams.
 ///
@@ -64,7 +63,7 @@ where
     where
         Message: 'static + Send,
         Receiver: 'static
-            + Sink<Message, Error = core::convert::Infallible>
+            + Sink<Message, Error = mpsc::SendError>
             + Unpin
             + Send
             + Clone,
diff --git a/src/application.rs b/src/application.rs
index 3a526f1b..926a2986 100644
--- a/src/application.rs
+++ b/src/application.rs
@@ -233,6 +233,7 @@ where
     A: Application,
 {
     type Message = A::Message;
+    type Executor = A::Executor;
 
     fn new() -> (Self, Command<A::Message>) {
         let (app, command) = A::new();
@@ -248,6 +249,10 @@ where
         self.0.update(message)
     }
 
+    fn subscription(&self) -> Subscription<Self::Message> {
+        self.0.subscription()
+    }
+
     fn view(&mut self) -> Element<'_, Self::Message> {
         self.0.view()
     }
diff --git a/web/src/bus.rs b/web/src/bus.rs
index b3984aff..c66e9659 100644
--- a/web/src/bus.rs
+++ b/web/src/bus.rs
@@ -1,5 +1,4 @@
-use crate::Instance;
-
+use iced_futures::futures::channel::mpsc;
 use std::rc::Rc;
 
 /// A publisher of messages.
@@ -9,13 +8,13 @@ use std::rc::Rc;
 /// [`Application`]: trait.Application.html
 #[allow(missing_debug_implementations)]
 pub struct Bus<Message> {
-    publish: Rc<Box<dyn Fn(Message, &mut dyn dodrio::RootRender)>>,
+    publish: Rc<Box<dyn Fn(Message) -> ()>>,
 }
 
 impl<Message> Clone for Bus<Message> {
     fn clone(&self) -> Self {
-        Self {
-            publish: Rc::clone(&self.publish),
+        Bus {
+            publish: self.publish.clone(),
         }
     }
 }
@@ -24,12 +23,10 @@ impl<Message> Bus<Message>
 where
     Message: 'static,
 {
-    pub(crate) fn new() -> Self {
+    pub(crate) fn new(publish: mpsc::UnboundedSender<Message>) -> Self {
         Self {
-            publish: Rc::new(Box::new(|message, root| {
-                let app = root.unwrap_mut::<Instance<Message>>();
-
-                app.update(message)
+            publish: Rc::new(Box::new(move |message| {
+                publish.unbounded_send(message).expect("Send message");
             })),
         }
     }
@@ -37,8 +34,8 @@ where
     /// Publishes a new message for the [`Application`].
     ///
     /// [`Application`]: trait.Application.html
-    pub fn publish(&self, message: Message, root: &mut dyn dodrio::RootRender) {
-        (self.publish)(message, root);
+    pub fn publish(&self, message: Message) {
+        (self.publish)(message)
     }
 
     /// Creates a new [`Bus`] that applies the given function to the messages
@@ -52,9 +49,7 @@ where
         let publish = self.publish.clone();
 
         Bus {
-            publish: Rc::new(Box::new(move |message, root| {
-                publish(mapper(message), root)
-            })),
+            publish: Rc::new(Box::new(move |message| publish(mapper(message)))),
         }
     }
 }
diff --git a/web/src/lib.rs b/web/src/lib.rs
index 1b265bc9..0b9c0c3d 100644
--- a/web/src/lib.rs
+++ b/web/src/lib.rs
@@ -97,7 +97,15 @@ pub trait Application {
     /// The type of __messages__ your [`Application`] will produce.
     ///
     /// [`Application`]: trait.Application.html
-    type Message;
+    type Message: Send;
+
+    /// The [`Executor`] that will run commands and subscriptions.
+    ///
+    /// The [`executor::WasmBindgen`] can be a good choice for the Web.
+    ///
+    /// [`Executor`]: trait.Executor.html
+    /// [`executor::Default`]: executor/struct.Default.html
+    type Executor: Executor;
 
     /// Initializes the [`Application`].
     ///
@@ -140,6 +148,20 @@ pub trait Application {
     /// [`Application`]: trait.Application.html
     fn view(&mut self) -> Element<'_, Self::Message>;
 
+    /// Returns the event [`Subscription`] for the current state of the
+    /// application.
+    ///
+    /// A [`Subscription`] will be kept alive as long as you keep returning it,
+    /// and the __messages__ produced will be handled by
+    /// [`update`](#tymethod.update).
+    ///
+    /// By default, this method returns an empty [`Subscription`].
+    ///
+    /// [`Subscription`]: struct.Subscription.html
+    fn subscription(&self) -> Subscription<Self::Message> {
+        Subscription::none()
+    }
+
     /// Runs the [`Application`].
     ///
     /// [`Application`]: trait.Application.html
@@ -147,96 +169,66 @@ pub trait Application {
     where
         Self: 'static + Sized,
     {
-        let (app, command) = Self::new();
-
-        let instance = Instance::new(app);
-        instance.run(command);
-    }
-}
+        use futures::stream::StreamExt;
 
-struct Instance<Message> {
-    title: String,
-    ui: Rc<RefCell<Box<dyn Application<Message = Message>>>>,
-    vdom: Rc<RefCell<Option<dodrio::VdomWeak>>>,
-}
-
-impl<Message> Clone for Instance<Message> {
-    fn clone(&self) -> Self {
-        Self {
-            title: self.title.clone(),
-            ui: Rc::clone(&self.ui),
-            vdom: Rc::clone(&self.vdom),
-        }
-    }
-}
-
-impl<Message> Instance<Message>
-where
-    Message: 'static,
-{
-    fn new(ui: impl Application<Message = Message> + 'static) -> Self {
-        Self {
-            title: ui.title(),
-            ui: Rc::new(RefCell::new(Box::new(ui))),
-            vdom: Rc::new(RefCell::new(None)),
-        }
-    }
-
-    fn update(&mut self, message: Message) {
-        let command = self.ui.borrow_mut().update(message);
-        let title = self.ui.borrow().title();
-
-        self.spawn(command);
+        let (app, command) = Self::new();
 
         let window = web_sys::window().unwrap();
         let document = window.document().unwrap();
+        let body = document.body().unwrap();
 
-        if self.title != title {
-            document.set_title(&title);
+        let mut title = app.title();
+        document.set_title(&title);
 
-            self.title = title;
-        }
-    }
+        let (sender, receiver) =
+            iced_futures::futures::channel::mpsc::unbounded();
 
-    fn spawn(&mut self, command: Command<Message>) {
-        use futures::FutureExt;
+        let mut runtime = iced_futures::Runtime::new(
+            Self::Executor::new().expect("Create executor"),
+            sender.clone(),
+        );
+        runtime.spawn(command);
 
-        for future in command.futures() {
-            let mut instance = self.clone();
+        let application = Rc::new(RefCell::new(app));
 
-            let future = future.map(move |message| {
-                instance.update(message);
+        let instance = Instance {
+            application: application.clone(),
+            bus: Bus::new(sender),
+        };
 
-                if let Some(ref vdom) = *instance.vdom.borrow() {
-                    vdom.schedule_render();
-                }
-            });
+        let vdom = dodrio::Vdom::new(&body, instance);
 
-            wasm_bindgen_futures::spawn_local(future);
-        }
-    }
+        let event_loop = receiver.for_each(move |message| {
+            let command = application.borrow_mut().update(message);
+            let subscription = application.borrow().subscription();
+            let new_title = application.borrow().title();
 
-    fn run(mut self, command: Command<Message>) {
-        let window = web_sys::window().unwrap();
+            runtime.spawn(command);
+            runtime.track(subscription);
 
-        let document = window.document().unwrap();
-        document.set_title(&self.title);
+            if title != new_title {
+                document.set_title(&new_title);
 
-        let body = document.body().unwrap();
+                title = new_title;
+            }
 
-        let weak = self.vdom.clone();
-        self.spawn(command);
+            vdom.weak().schedule_render();
 
-        let vdom = dodrio::Vdom::new(&body, self);
-        *weak.borrow_mut() = Some(vdom.weak());
+            futures::future::ready(())
+        });
 
-        vdom.forget();
+        wasm_bindgen_futures::spawn_local(event_loop);
     }
 }
 
-impl<Message> dodrio::Render for Instance<Message>
+struct Instance<A: Application> {
+    application: Rc<RefCell<A>>,
+    bus: Bus<A::Message>,
+}
+
+impl<A> dodrio::Render for Instance<A>
 where
-    Message: 'static,
+    A: Application,
 {
     fn render<'a, 'bump>(
         &'a self,
@@ -247,11 +239,11 @@ where
     {
         use dodrio::builder::*;
 
-        let mut ui = self.ui.borrow_mut();
+        let mut ui = self.application.borrow_mut();
         let element = ui.view();
         let mut style_sheet = style::Sheet::new();
 
-        let node = element.widget.node(bump, &Bus::new(), &mut style_sheet);
+        let node = element.widget.node(bump, &self.bus, &mut style_sheet);
 
         div(bump)
             .attr("style", "width: 100%; height: 100%")
diff --git a/web/src/widget/button.rs b/web/src/widget/button.rs
index e628bd18..6fef48ce 100644
--- a/web/src/widget/button.rs
+++ b/web/src/widget/button.rs
@@ -163,10 +163,8 @@ where
         if let Some(on_press) = self.on_press.clone() {
             let event_bus = bus.clone();
 
-            node = node.on("click", move |root, vdom, _event| {
-                event_bus.publish(on_press.clone(), root);
-
-                vdom.schedule_render();
+            node = node.on("click", move |_root, _vdom, _event| {
+                event_bus.publish(on_press.clone());
             });
         }
 
diff --git a/web/src/widget/checkbox.rs b/web/src/widget/checkbox.rs
index 34d13a1b..1e864875 100644
--- a/web/src/widget/checkbox.rs
+++ b/web/src/widget/checkbox.rs
@@ -84,9 +84,9 @@ where
                 input(bump)
                     .attr("type", "checkbox")
                     .bool_attr("checked", self.is_checked)
-                    .on("click", move |root, vdom, _event| {
+                    .on("click", move |_root, vdom, _event| {
                         let msg = on_toggle(!is_checked);
-                        event_bus.publish(msg, root);
+                        event_bus.publish(msg);
 
                         vdom.schedule_render();
                     })
diff --git a/web/src/widget/radio.rs b/web/src/widget/radio.rs
index 4e7d02b8..6dd0ad45 100644
--- a/web/src/widget/radio.rs
+++ b/web/src/widget/radio.rs
@@ -93,10 +93,8 @@ where
                     .attr("type", "radio")
                     .attr("style", "margin-right: 10px")
                     .bool_attr("checked", self.is_selected)
-                    .on("click", move |root, vdom, _event| {
-                        event_bus.publish(on_click.clone(), root);
-
-                        vdom.schedule_render();
+                    .on("click", move |_root, _vdom, _event| {
+                        event_bus.publish(on_click.clone());
                     })
                     .finish(),
                 text(radio_label.into_bump_str()),
diff --git a/web/src/widget/slider.rs b/web/src/widget/slider.rs
index fc955781..25c57933 100644
--- a/web/src/widget/slider.rs
+++ b/web/src/widget/slider.rs
@@ -111,7 +111,7 @@ where
             .attr("max", max.into_bump_str())
             .attr("value", value.into_bump_str())
             .attr("style", "width: 100%")
-            .on("input", move |root, vdom, event| {
+            .on("input", move |_root, _vdom, event| {
                 let slider = match event.target().and_then(|t| {
                     t.dyn_into::<web_sys::HtmlInputElement>().ok()
                 }) {
@@ -120,8 +120,7 @@ where
                 };
 
                 if let Ok(value) = slider.value().parse::<f32>() {
-                    event_bus.publish(on_change(value), root);
-                    vdom.schedule_render();
+                    event_bus.publish(on_change(value));
                 }
             })
             .finish()
diff --git a/web/src/widget/text_input.rs b/web/src/widget/text_input.rs
index a478874a..078e05b2 100644
--- a/web/src/widget/text_input.rs
+++ b/web/src/widget/text_input.rs
@@ -175,7 +175,7 @@ where
                 "type",
                 bumpalo::format!(in bump, "{}", if self.is_secure { "password" } else { "text" }).into_bump_str(),
             )
-            .on("input", move |root, vdom, event| {
+            .on("input", move |_root, _vdom, event| {
                 let text_input = match event.target().and_then(|t| {
                     t.dyn_into::<web_sys::HtmlInputElement>().ok()
                 }) {
@@ -183,8 +183,7 @@ where
                     Some(text_input) => text_input,
                 };
 
-                event_bus.publish(on_change(text_input.value()), root);
-                vdom.schedule_render();
+                event_bus.publish(on_change(text_input.value()));
             })
             .finish()
     }
diff --git a/winit/src/proxy.rs b/winit/src/proxy.rs
index cff9df33..cff6ca72 100644
--- a/winit/src/proxy.rs
+++ b/winit/src/proxy.rs
@@ -1,4 +1,5 @@
 use iced_native::futures::{
+    channel::mpsc,
     task::{Context, Poll},
     Sink,
 };
@@ -23,7 +24,7 @@ impl<Message: 'static> Proxy<Message> {
 }
 
 impl<Message: 'static> Sink<Message> for Proxy<Message> {
-    type Error = core::convert::Infallible;
+    type Error = mpsc::SendError;
 
     fn poll_ready(
         self: Pin<&mut Self>,
-- 
cgit