Support event subscriptions in iced_web
Also improves the overall web runtime, avoiding nested update loops.
This commit is contained in:
parent
f5186f31f1
commit
6d46833eb2
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,3 +2,4 @@
|
|||||||
pkg/
|
pkg/
|
||||||
**/*.rs.bk
|
**/*.rs.bk
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
|
.cargo/
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
//! Run commands and keep track of subscriptions.
|
//! Run commands and keep track of subscriptions.
|
||||||
use crate::{subscription, Command, Executor, Subscription};
|
use crate::{subscription, Command, Executor, Subscription};
|
||||||
|
|
||||||
use futures::Sink;
|
use futures::{channel::mpsc, Sink};
|
||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
/// A batteries-included runtime of commands and subscriptions.
|
/// A batteries-included runtime of commands and subscriptions.
|
||||||
@ -27,11 +27,8 @@ where
|
|||||||
Hasher: std::hash::Hasher + Default,
|
Hasher: std::hash::Hasher + Default,
|
||||||
Event: Send + Clone + 'static,
|
Event: Send + Clone + 'static,
|
||||||
Executor: self::Executor,
|
Executor: self::Executor,
|
||||||
Sender: Sink<Message, Error = core::convert::Infallible>
|
Sender:
|
||||||
+ Unpin
|
Sink<Message, Error = mpsc::SendError> + Unpin + Send + Clone + 'static,
|
||||||
+ Send
|
|
||||||
+ Clone
|
|
||||||
+ 'static,
|
|
||||||
Message: Send + 'static,
|
Message: Send + 'static,
|
||||||
{
|
{
|
||||||
/// Creates a new empty [`Runtime`].
|
/// Creates a new empty [`Runtime`].
|
||||||
@ -76,12 +73,10 @@ where
|
|||||||
for future in futures {
|
for future in futures {
|
||||||
let mut sender = self.sender.clone();
|
let mut sender = self.sender.clone();
|
||||||
|
|
||||||
self.executor.spawn(future.then(|message| {
|
self.executor.spawn(future.then(|message| async move {
|
||||||
async move {
|
|
||||||
let _ = sender.send(message).await;
|
let _ = sender.send(message).await;
|
||||||
|
|
||||||
()
|
()
|
||||||
}
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -112,7 +107,8 @@ where
|
|||||||
/// See [`Tracker::broadcast`] to learn more.
|
/// See [`Tracker::broadcast`] to learn more.
|
||||||
///
|
///
|
||||||
/// [`Runtime`]: struct.Runtime.html
|
/// [`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) {
|
pub fn broadcast(&mut self, event: Event) {
|
||||||
self.subscriptions.broadcast(event);
|
self.subscriptions.broadcast(event);
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
use crate::Subscription;
|
use crate::Subscription;
|
||||||
|
|
||||||
use futures::{future::BoxFuture, sink::Sink};
|
use futures::{channel::mpsc, future::BoxFuture, sink::Sink};
|
||||||
use std::collections::HashMap;
|
use std::{collections::HashMap, marker::PhantomData};
|
||||||
use std::marker::PhantomData;
|
|
||||||
|
|
||||||
/// A registry of subscription streams.
|
/// A registry of subscription streams.
|
||||||
///
|
///
|
||||||
@ -64,7 +63,7 @@ where
|
|||||||
where
|
where
|
||||||
Message: 'static + Send,
|
Message: 'static + Send,
|
||||||
Receiver: 'static
|
Receiver: 'static
|
||||||
+ Sink<Message, Error = core::convert::Infallible>
|
+ Sink<Message, Error = mpsc::SendError>
|
||||||
+ Unpin
|
+ Unpin
|
||||||
+ Send
|
+ Send
|
||||||
+ Clone,
|
+ Clone,
|
||||||
|
@ -233,6 +233,7 @@ where
|
|||||||
A: Application,
|
A: Application,
|
||||||
{
|
{
|
||||||
type Message = A::Message;
|
type Message = A::Message;
|
||||||
|
type Executor = A::Executor;
|
||||||
|
|
||||||
fn new() -> (Self, Command<A::Message>) {
|
fn new() -> (Self, Command<A::Message>) {
|
||||||
let (app, command) = A::new();
|
let (app, command) = A::new();
|
||||||
@ -248,6 +249,10 @@ where
|
|||||||
self.0.update(message)
|
self.0.update(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn subscription(&self) -> Subscription<Self::Message> {
|
||||||
|
self.0.subscription()
|
||||||
|
}
|
||||||
|
|
||||||
fn view(&mut self) -> Element<'_, Self::Message> {
|
fn view(&mut self) -> Element<'_, Self::Message> {
|
||||||
self.0.view()
|
self.0.view()
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
use crate::Instance;
|
use iced_futures::futures::channel::mpsc;
|
||||||
|
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
/// A publisher of messages.
|
/// A publisher of messages.
|
||||||
@ -9,13 +8,13 @@ use std::rc::Rc;
|
|||||||
/// [`Application`]: trait.Application.html
|
/// [`Application`]: trait.Application.html
|
||||||
#[allow(missing_debug_implementations)]
|
#[allow(missing_debug_implementations)]
|
||||||
pub struct Bus<Message> {
|
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> {
|
impl<Message> Clone for Bus<Message> {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
Self {
|
Bus {
|
||||||
publish: Rc::clone(&self.publish),
|
publish: self.publish.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -24,12 +23,10 @@ impl<Message> Bus<Message>
|
|||||||
where
|
where
|
||||||
Message: 'static,
|
Message: 'static,
|
||||||
{
|
{
|
||||||
pub(crate) fn new() -> Self {
|
pub(crate) fn new(publish: mpsc::UnboundedSender<Message>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
publish: Rc::new(Box::new(|message, root| {
|
publish: Rc::new(Box::new(move |message| {
|
||||||
let app = root.unwrap_mut::<Instance<Message>>();
|
publish.unbounded_send(message).expect("Send message");
|
||||||
|
|
||||||
app.update(message)
|
|
||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -37,8 +34,8 @@ where
|
|||||||
/// Publishes a new message for the [`Application`].
|
/// Publishes a new message for the [`Application`].
|
||||||
///
|
///
|
||||||
/// [`Application`]: trait.Application.html
|
/// [`Application`]: trait.Application.html
|
||||||
pub fn publish(&self, message: Message, root: &mut dyn dodrio::RootRender) {
|
pub fn publish(&self, message: Message) {
|
||||||
(self.publish)(message, root);
|
(self.publish)(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a new [`Bus`] that applies the given function to the messages
|
/// Creates a new [`Bus`] that applies the given function to the messages
|
||||||
@ -52,9 +49,7 @@ where
|
|||||||
let publish = self.publish.clone();
|
let publish = self.publish.clone();
|
||||||
|
|
||||||
Bus {
|
Bus {
|
||||||
publish: Rc::new(Box::new(move |message, root| {
|
publish: Rc::new(Box::new(move |message| publish(mapper(message)))),
|
||||||
publish(mapper(message), root)
|
|
||||||
})),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
154
web/src/lib.rs
154
web/src/lib.rs
@ -97,7 +97,15 @@ pub trait Application {
|
|||||||
/// The type of __messages__ your [`Application`] will produce.
|
/// The type of __messages__ your [`Application`] will produce.
|
||||||
///
|
///
|
||||||
/// [`Application`]: trait.Application.html
|
/// [`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`].
|
/// Initializes the [`Application`].
|
||||||
///
|
///
|
||||||
@ -140,6 +148,20 @@ pub trait Application {
|
|||||||
/// [`Application`]: trait.Application.html
|
/// [`Application`]: trait.Application.html
|
||||||
fn view(&mut self) -> Element<'_, Self::Message>;
|
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`].
|
/// Runs the [`Application`].
|
||||||
///
|
///
|
||||||
/// [`Application`]: trait.Application.html
|
/// [`Application`]: trait.Application.html
|
||||||
@ -147,96 +169,66 @@ pub trait Application {
|
|||||||
where
|
where
|
||||||
Self: 'static + Sized,
|
Self: 'static + Sized,
|
||||||
{
|
{
|
||||||
|
use futures::stream::StreamExt;
|
||||||
|
|
||||||
let (app, command) = Self::new();
|
let (app, command) = Self::new();
|
||||||
|
|
||||||
let instance = Instance::new(app);
|
|
||||||
instance.run(command);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 window = web_sys::window().unwrap();
|
let window = web_sys::window().unwrap();
|
||||||
let document = window.document().unwrap();
|
let document = window.document().unwrap();
|
||||||
|
|
||||||
if self.title != title {
|
|
||||||
document.set_title(&title);
|
|
||||||
|
|
||||||
self.title = title;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn spawn(&mut self, command: Command<Message>) {
|
|
||||||
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<Message>) {
|
|
||||||
let window = web_sys::window().unwrap();
|
|
||||||
|
|
||||||
let document = window.document().unwrap();
|
|
||||||
document.set_title(&self.title);
|
|
||||||
|
|
||||||
let body = document.body().unwrap();
|
let body = document.body().unwrap();
|
||||||
|
|
||||||
let weak = self.vdom.clone();
|
let mut title = app.title();
|
||||||
self.spawn(command);
|
document.set_title(&title);
|
||||||
|
|
||||||
let vdom = dodrio::Vdom::new(&body, self);
|
let (sender, receiver) =
|
||||||
*weak.borrow_mut() = Some(vdom.weak());
|
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<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
|
where
|
||||||
Message: 'static,
|
A: Application,
|
||||||
{
|
{
|
||||||
fn render<'a, 'bump>(
|
fn render<'a, 'bump>(
|
||||||
&'a self,
|
&'a self,
|
||||||
@ -247,11 +239,11 @@ where
|
|||||||
{
|
{
|
||||||
use dodrio::builder::*;
|
use dodrio::builder::*;
|
||||||
|
|
||||||
let mut ui = self.ui.borrow_mut();
|
let mut ui = self.application.borrow_mut();
|
||||||
let element = ui.view();
|
let element = ui.view();
|
||||||
let mut style_sheet = style::Sheet::new();
|
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)
|
div(bump)
|
||||||
.attr("style", "width: 100%; height: 100%")
|
.attr("style", "width: 100%; height: 100%")
|
||||||
|
@ -163,10 +163,8 @@ where
|
|||||||
if let Some(on_press) = self.on_press.clone() {
|
if let Some(on_press) = self.on_press.clone() {
|
||||||
let event_bus = bus.clone();
|
let event_bus = bus.clone();
|
||||||
|
|
||||||
node = node.on("click", move |root, vdom, _event| {
|
node = node.on("click", move |_root, _vdom, _event| {
|
||||||
event_bus.publish(on_press.clone(), root);
|
event_bus.publish(on_press.clone());
|
||||||
|
|
||||||
vdom.schedule_render();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,9 +84,9 @@ where
|
|||||||
input(bump)
|
input(bump)
|
||||||
.attr("type", "checkbox")
|
.attr("type", "checkbox")
|
||||||
.bool_attr("checked", self.is_checked)
|
.bool_attr("checked", self.is_checked)
|
||||||
.on("click", move |root, vdom, _event| {
|
.on("click", move |_root, vdom, _event| {
|
||||||
let msg = on_toggle(!is_checked);
|
let msg = on_toggle(!is_checked);
|
||||||
event_bus.publish(msg, root);
|
event_bus.publish(msg);
|
||||||
|
|
||||||
vdom.schedule_render();
|
vdom.schedule_render();
|
||||||
})
|
})
|
||||||
|
@ -93,10 +93,8 @@ where
|
|||||||
.attr("type", "radio")
|
.attr("type", "radio")
|
||||||
.attr("style", "margin-right: 10px")
|
.attr("style", "margin-right: 10px")
|
||||||
.bool_attr("checked", self.is_selected)
|
.bool_attr("checked", self.is_selected)
|
||||||
.on("click", move |root, vdom, _event| {
|
.on("click", move |_root, _vdom, _event| {
|
||||||
event_bus.publish(on_click.clone(), root);
|
event_bus.publish(on_click.clone());
|
||||||
|
|
||||||
vdom.schedule_render();
|
|
||||||
})
|
})
|
||||||
.finish(),
|
.finish(),
|
||||||
text(radio_label.into_bump_str()),
|
text(radio_label.into_bump_str()),
|
||||||
|
@ -111,7 +111,7 @@ where
|
|||||||
.attr("max", max.into_bump_str())
|
.attr("max", max.into_bump_str())
|
||||||
.attr("value", value.into_bump_str())
|
.attr("value", value.into_bump_str())
|
||||||
.attr("style", "width: 100%")
|
.attr("style", "width: 100%")
|
||||||
.on("input", move |root, vdom, event| {
|
.on("input", move |_root, _vdom, event| {
|
||||||
let slider = match event.target().and_then(|t| {
|
let slider = match event.target().and_then(|t| {
|
||||||
t.dyn_into::<web_sys::HtmlInputElement>().ok()
|
t.dyn_into::<web_sys::HtmlInputElement>().ok()
|
||||||
}) {
|
}) {
|
||||||
@ -120,8 +120,7 @@ where
|
|||||||
};
|
};
|
||||||
|
|
||||||
if let Ok(value) = slider.value().parse::<f32>() {
|
if let Ok(value) = slider.value().parse::<f32>() {
|
||||||
event_bus.publish(on_change(value), root);
|
event_bus.publish(on_change(value));
|
||||||
vdom.schedule_render();
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.finish()
|
.finish()
|
||||||
|
@ -175,7 +175,7 @@ where
|
|||||||
"type",
|
"type",
|
||||||
bumpalo::format!(in bump, "{}", if self.is_secure { "password" } else { "text" }).into_bump_str(),
|
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| {
|
let text_input = match event.target().and_then(|t| {
|
||||||
t.dyn_into::<web_sys::HtmlInputElement>().ok()
|
t.dyn_into::<web_sys::HtmlInputElement>().ok()
|
||||||
}) {
|
}) {
|
||||||
@ -183,8 +183,7 @@ where
|
|||||||
Some(text_input) => text_input,
|
Some(text_input) => text_input,
|
||||||
};
|
};
|
||||||
|
|
||||||
event_bus.publish(on_change(text_input.value()), root);
|
event_bus.publish(on_change(text_input.value()));
|
||||||
vdom.schedule_render();
|
|
||||||
})
|
})
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
use iced_native::futures::{
|
use iced_native::futures::{
|
||||||
|
channel::mpsc,
|
||||||
task::{Context, Poll},
|
task::{Context, Poll},
|
||||||
Sink,
|
Sink,
|
||||||
};
|
};
|
||||||
@ -23,7 +24,7 @@ impl<Message: 'static> Proxy<Message> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<Message: 'static> Sink<Message> for Proxy<Message> {
|
impl<Message: 'static> Sink<Message> for Proxy<Message> {
|
||||||
type Error = core::convert::Infallible;
|
type Error = mpsc::SendError;
|
||||||
|
|
||||||
fn poll_ready(
|
fn poll_ready(
|
||||||
self: Pin<&mut Self>,
|
self: Pin<&mut Self>,
|
||||||
|
Loading…
Reference in New Issue
Block a user