From 6d46833eb2a068bd3655859ea828dad04293e5ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 4 Feb 2020 03:28:47 +0100 Subject: [PATCH] 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 | 154 +++++++++++++--------------- 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, 112 insertions(+), 129 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 - + Unpin - + Send - + Clone - + 'static, + Sender: + Sink + 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 + + Sink + 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) { let (app, command) = A::new(); @@ -248,6 +249,10 @@ where self.0.update(message) } + fn subscription(&self) -> Subscription { + 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 { - publish: Rc>, + publish: Rc ()>>, } impl Clone for Bus { fn clone(&self) -> Self { - Self { - publish: Rc::clone(&self.publish), + Bus { + publish: self.publish.clone(), } } } @@ -24,12 +23,10 @@ impl Bus where Message: 'static, { - pub(crate) fn new() -> Self { + pub(crate) fn new(publish: mpsc::UnboundedSender) -> Self { Self { - publish: Rc::new(Box::new(|message, root| { - let app = root.unwrap_mut::>(); - - 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 { + Subscription::none() + } + /// Runs the [`Application`]. /// /// [`Application`]: trait.Application.html @@ -147,96 +169,66 @@ pub trait Application { where Self: 'static + Sized, { + use futures::stream::StreamExt; + let (app, command) = Self::new(); - let instance = Instance::new(app); - instance.run(command); - } -} - -struct Instance { - title: String, - ui: Rc>>>, - vdom: Rc>>, -} - -impl Clone for Instance { - fn clone(&self) -> Self { - Self { - title: self.title.clone(), - ui: Rc::clone(&self.ui), - vdom: Rc::clone(&self.vdom), - } - } -} - -impl Instance -where - Message: 'static, -{ - fn new(ui: impl Application + '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 window = web_sys::window().unwrap(); let document = window.document().unwrap(); - - if self.title != title { - document.set_title(&title); - - self.title = title; - } - } - - fn spawn(&mut self, command: Command) { - use futures::FutureExt; - - for future in command.futures() { - let mut instance = self.clone(); - - let future = future.map(move |message| { - instance.update(message); - - if let Some(ref vdom) = *instance.vdom.borrow() { - vdom.schedule_render(); - } - }); - - wasm_bindgen_futures::spawn_local(future); - } - } - - fn run(mut self, command: Command) { - let window = web_sys::window().unwrap(); - - let document = window.document().unwrap(); - document.set_title(&self.title); - let body = document.body().unwrap(); - let weak = self.vdom.clone(); - self.spawn(command); + let mut title = app.title(); + document.set_title(&title); - let vdom = dodrio::Vdom::new(&body, self); - *weak.borrow_mut() = Some(vdom.weak()); + let (sender, receiver) = + iced_futures::futures::channel::mpsc::unbounded(); - vdom.forget(); + let mut runtime = iced_futures::Runtime::new( + Self::Executor::new().expect("Create executor"), + sender.clone(), + ); + runtime.spawn(command); + + let application = Rc::new(RefCell::new(app)); + + let instance = Instance { + application: application.clone(), + bus: Bus::new(sender), + }; + + let vdom = dodrio::Vdom::new(&body, instance); + + 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(); + + runtime.spawn(command); + runtime.track(subscription); + + if title != new_title { + document.set_title(&new_title); + + title = new_title; + } + + vdom.weak().schedule_render(); + + futures::future::ready(()) + }); + + wasm_bindgen_futures::spawn_local(event_loop); } } -impl dodrio::Render for Instance +struct Instance { + application: Rc>, + bus: Bus, +} + +impl dodrio::Render for Instance 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::().ok() }) { @@ -120,8 +120,7 @@ where }; if let Ok(value) = slider.value().parse::() { - 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::().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 Proxy { } impl Sink for Proxy { - type Error = core::convert::Infallible; + type Error = mpsc::SendError; fn poll_ready( self: Pin<&mut Self>,