Write docs for subscriptions and reorganize a bit

This commit is contained in:
Héctor Ramón Jiménez 2019-12-14 05:56:46 +01:00
parent 293314405f
commit d6c3da21f7
12 changed files with 224 additions and 122 deletions

View File

@ -34,8 +34,8 @@ impl<T> Command<T> {
}
}
/// Creates a [`Command`] that performs the actions of all the givens
/// futures.
/// Creates a [`Command`] that performs the actions of all the given
/// commands.
///
/// Once this command is run, all the futures will be exectued at once.
///

View File

@ -9,7 +9,7 @@
//! [Iced]: https://github.com/hecrj/iced
//! [`iced_native`]: https://github.com/hecrj/iced/tree/master/native
//! [`iced_web`]: https://github.com/hecrj/iced/tree/master/web
//#![deny(missing_docs)]
#![deny(missing_docs)]
#![deny(missing_debug_implementations)]
#![deny(unused_results)]
#![deny(unsafe_code)]

View File

@ -1,6 +1,21 @@
//! Generate events asynchronously for you application.
//! Listen to external events in your application.
/// An event subscription.
/// A request to listen to external events.
///
/// Besides performing async actions on demand with [`Command`], most
/// applications also need to listen to external events passively.
///
/// A [`Subscription`] is normally provided to some runtime, like a [`Command`],
/// and it will generate events as long as the user keeps requesting it.
///
/// For instance, you can use a [`Subscription`] to listen to a WebSocket
/// connection, keyboard presses, mouse events, time ticks, etc.
///
/// This type is normally aliased by runtimes with a specific `Input` and/or
/// `Hasher`.
///
/// [`Command`]: ../struct.Command.html
/// [`Subscription`]: struct.Subscription.html
pub struct Subscription<Hasher, Input, Output> {
recipes: Vec<Box<dyn Recipe<Hasher, Input, Output = Output>>>,
}
@ -9,12 +24,19 @@ impl<H, I, O> Subscription<H, I, O>
where
H: std::hash::Hasher,
{
/// Returns an empty [`Subscription`] that will not produce any output.
///
/// [`Subscription`]: struct.Subscription.html
pub fn none() -> Self {
Self {
recipes: Vec::new(),
}
}
/// Creates a [`Subscription`] from a [`Recipe`] describing it.
///
/// [`Subscription`]: struct.Subscription.html
/// [`Recipe`]: trait.Recipe.html
pub fn from_recipe(
recipe: impl Recipe<H, I, Output = O> + 'static,
) -> Self {
@ -23,6 +45,10 @@ where
}
}
/// Batches all the provided subscriptions and returns the resulting
/// [`Subscription`].
///
/// [`Subscription`]: struct.Subscription.html
pub fn batch(
subscriptions: impl Iterator<Item = Subscription<H, I, O>>,
) -> Self {
@ -33,10 +59,16 @@ where
}
}
/// Returns the different recipes of the [`Subscription`].
///
/// [`Subscription`]: struct.Subscription.html
pub fn recipes(self) -> Vec<Box<dyn Recipe<H, I, Output = O>>> {
self.recipes
}
/// Transforms the [`Subscription`] output with the given function.
///
/// [`Subscription`]: struct.Subscription.html
pub fn map<A>(
mut self,
f: impl Fn(O) -> A + Send + Sync + 'static,
@ -68,12 +100,37 @@ impl<I, O, H> std::fmt::Debug for Subscription<I, O, H> {
}
}
/// The connection of an event subscription.
/// The description of a [`Subscription`].
///
/// A [`Recipe`] is the internal definition of a [`Subscription`]. It is used
/// by runtimes to run and identify subscriptions. You can use it to create your
/// own!
///
/// [`Subscription`]: struct.Subscription.html
/// [`Recipe`]: trait.Recipe.html
pub trait Recipe<Hasher: std::hash::Hasher, Input> {
/// The events that will be produced by a [`Subscription`] with this
/// [`Recipe`].
///
/// [`Subscription`]: struct.Subscription.html
/// [`Recipe`]: trait.Recipe.html
type Output;
/// Hashes the [`Recipe`].
///
/// This is used by runtimes to uniquely identify a [`Subscription`].
///
/// [`Subscription`]: struct.Subscription.html
/// [`Recipe`]: trait.Recipe.html
fn hash(&self, state: &mut Hasher);
/// Executes the [`Recipe`] and produces the stream of events of its
/// [`Subscription`].
///
/// It receives some generic `Input`, which is normally defined by runtimes.
///
/// [`Subscription`]: struct.Subscription.html
/// [`Recipe`]: trait.Recipe.html
fn stream(
self: Box<Self>,
input: Input,

View File

@ -47,7 +47,7 @@ impl Application for Events {
Command::none()
}
fn subscriptions(&self) -> Subscription<Message> {
fn subscription(&self) -> Subscription<Message> {
if self.enabled {
iced_native::subscription::events().map(Message::EventOccurred)
} else {

View File

@ -74,7 +74,7 @@ impl Application for Stopwatch {
Command::none()
}
fn subscriptions(&self) -> Subscription<Message> {
fn subscription(&self) -> Subscription<Message> {
match self.state {
State::Idle => Subscription::none(),
State::Ticking { .. } => {

View File

@ -34,7 +34,7 @@
//! [`Windowed`]: renderer/trait.Windowed.html
//! [`UserInterface`]: struct.UserInterface.html
//! [renderer]: renderer/index.html
//#![deny(missing_docs)]
#![deny(missing_docs)]
#![deny(missing_debug_implementations)]
#![deny(unused_results)]
#![deny(unsafe_code)]

View File

@ -1,16 +1,42 @@
//! Listen to external events in your application.
use crate::{Event, Hasher};
use futures::stream::BoxStream;
pub type EventStream = BoxStream<'static, Event>;
/// A request to listen to external events.
///
/// Besides performing async actions on demand with [`Command`], most
/// applications also need to listen to external events passively.
///
/// A [`Subscription`] is normally provided to some runtime, like a [`Command`],
/// and it will generate events as long as the user keeps requesting it.
///
/// For instance, you can use a [`Subscription`] to listen to a WebSocket
/// connection, keyboard presses, mouse events, time ticks, etc.
///
/// [`Command`]: ../struct.Command.html
/// [`Subscription`]: struct.Subscription.html
pub type Subscription<T> = iced_core::Subscription<Hasher, EventStream, T>;
/// A stream of runtime events.
///
/// It is the input of a [`Subscription`] in the native runtime.
///
/// [`Subscription`]: type.Subscription.html
pub type EventStream = BoxStream<'static, Event>;
pub use iced_core::subscription::Recipe;
mod events;
use events::Events;
/// Returns a [`Subscription`] to all the runtime events.
///
/// This subscription will notify your application of any [`Event`] handled by
/// the runtime.
///
/// [`Subscription`]: type.Subscription.html
/// [`Event`]: ../enum.Event.html
pub fn events() -> Subscription<Event> {
Subscription::from_recipe(Events)
}

View File

@ -117,8 +117,17 @@ pub trait Application: Sized {
/// [`Command`]: struct.Command.html
fn update(&mut self, message: Self::Message) -> Command<Self::Message>;
/// TODO
fn subscriptions(&self) -> Subscription<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()
}
@ -173,8 +182,8 @@ where
self.0.update(message)
}
fn subscriptions(&self) -> Subscription<Self::Message> {
self.0.subscriptions()
fn subscription(&self) -> Subscription<Self::Message> {
self.0.subscription()
}
fn view(&mut self) -> Element<'_, Self::Message> {

View File

@ -149,7 +149,7 @@ where
Command::none()
}
fn subscriptions(&self) -> Subscription<T::Message> {
fn subscription(&self) -> Subscription<T::Message> {
Subscription::none()
}

View File

@ -2,10 +2,9 @@ use crate::{
conversion,
input::{keyboard, mouse},
renderer::{Target, Windowed},
Cache, Command, Container, Debug, Element, Event, Hasher, Length,
subscription, Cache, Command, Container, Debug, Element, Event, Length,
MouseCursor, Settings, Subscription, UserInterface,
};
use std::collections::HashMap;
/// An interactive, native cross-platform application.
///
@ -58,8 +57,14 @@ pub trait Application: Sized {
/// [`Command`]: struct.Command.html
fn update(&mut self, message: Self::Message) -> Command<Self::Message>;
/// TODO
fn subscriptions(&self) -> Subscription<Self::Message>;
/// Returns the event `Subscription` for the current state of the
/// application.
///
/// The messages produced by the `Subscription` will be handled by
/// [`update`](#tymethod.update).
///
/// A `Subscription` will be kept alive as long as you keep returning it!
fn subscription(&self) -> Subscription<Self::Message>;
/// Returns the widgets to display in the [`Application`].
///
@ -93,14 +98,14 @@ pub trait Application: Sized {
let proxy = event_loop.create_proxy();
let mut thread_pool =
futures::executor::ThreadPool::new().expect("Create thread pool");
let mut alive_subscriptions = Subscriptions::new();
let mut subscription_pool = subscription::Pool::new();
let mut external_messages = Vec::new();
let (mut application, init_command) = Self::new();
spawn(init_command, &mut thread_pool, &proxy);
let subscriptions = application.subscriptions();
alive_subscriptions.update(subscriptions, &mut thread_pool, &proxy);
let subscription = application.subscription();
subscription_pool.update(subscription, &mut thread_pool, &proxy);
let mut title = application.title();
@ -184,9 +189,9 @@ pub trait Application: Sized {
debug.layout_finished();
debug.event_processing_started();
events
.iter()
.for_each(|event| alive_subscriptions.send_event(*event));
events.iter().for_each(|event| {
subscription_pool.broadcast_event(*event)
});
let mut messages =
user_interface.update(&renderer, events.drain(..));
@ -215,9 +220,9 @@ pub trait Application: Sized {
debug.update_finished();
}
let subscriptions = application.subscriptions();
alive_subscriptions.update(
subscriptions,
let subscription = application.subscription();
subscription_pool.update(
subscription,
&mut thread_pool,
&proxy,
);
@ -424,99 +429,6 @@ fn spawn<Message: Send>(
}
}
pub struct Subscriptions {
alive: HashMap<u64, Connection>,
}
pub struct Connection {
_cancel: futures::channel::oneshot::Sender<()>,
listener: Option<futures::channel::mpsc::Sender<Event>>,
}
impl Subscriptions {
fn new() -> Self {
Self {
alive: HashMap::new(),
}
}
fn update<Message: Send>(
&mut self,
subscriptions: Subscription<Message>,
thread_pool: &mut futures::executor::ThreadPool,
proxy: &winit::event_loop::EventLoopProxy<Message>,
) {
use futures::{future::FutureExt, stream::StreamExt};
let recipes = subscriptions.recipes();
let mut alive = std::collections::HashSet::new();
for recipe in recipes {
let id = {
use std::hash::Hasher as _;
let mut hasher = Hasher::default();
recipe.hash(&mut hasher);
hasher.finish()
};
let _ = alive.insert(id);
if !self.alive.contains_key(&id) {
let (cancel, cancelled) = futures::channel::oneshot::channel();
let (event_sender, event_receiver) =
futures::channel::mpsc::channel(100);
let stream = recipe.stream(event_receiver.boxed());
let proxy = proxy.clone();
let future = futures::future::select(
cancelled,
stream.for_each(move |message| {
proxy
.send_event(message)
.expect("Send subscription result to event loop");
futures::future::ready(())
}),
)
.map(|_| ());
thread_pool.spawn_ok(future);
let _ = self.alive.insert(
id,
Connection {
_cancel: cancel,
listener: if event_sender.is_closed() {
None
} else {
Some(event_sender)
},
},
);
}
}
self.alive.retain(|id, _| alive.contains(&id));
}
fn send_event(&mut self, event: Event) {
self.alive
.values_mut()
.filter_map(|connection| connection.listener.as_mut())
.for_each(|listener| {
if let Err(error) = listener.try_send(event) {
log::warn!(
"Error sending event to subscription: {:?}",
error
);
}
});
}
}
// As defined in: http://www.unicode.org/faq/private_use.html
// TODO: Remove once https://github.com/rust-windowing/winit/pull/1254 lands
fn is_private_use_character(c: char) -> bool {

View File

@ -29,6 +29,7 @@ pub mod conversion;
pub mod settings;
mod application;
mod subscription;
pub use application::Application;
pub use settings::Settings;

97
winit/src/subscription.rs Normal file
View File

@ -0,0 +1,97 @@
use iced_native::{Event, Hasher, Subscription};
use std::collections::HashMap;
pub struct Pool {
alive: HashMap<u64, Handle>,
}
pub struct Handle {
_cancel: futures::channel::oneshot::Sender<()>,
listener: Option<futures::channel::mpsc::Sender<Event>>,
}
impl Pool {
pub fn new() -> Self {
Self {
alive: HashMap::new(),
}
}
pub fn update<Message: Send>(
&mut self,
subscription: Subscription<Message>,
thread_pool: &mut futures::executor::ThreadPool,
proxy: &winit::event_loop::EventLoopProxy<Message>,
) {
use futures::{future::FutureExt, stream::StreamExt};
let recipes = subscription.recipes();
let mut alive = std::collections::HashSet::new();
for recipe in recipes {
let id = {
use std::hash::Hasher as _;
let mut hasher = Hasher::default();
recipe.hash(&mut hasher);
hasher.finish()
};
let _ = alive.insert(id);
if !self.alive.contains_key(&id) {
let (cancel, cancelled) = futures::channel::oneshot::channel();
// TODO: Use bus if/when it supports async
let (event_sender, event_receiver) =
futures::channel::mpsc::channel(100);
let stream = recipe.stream(event_receiver.boxed());
let proxy = proxy.clone();
let future = futures::future::select(
cancelled,
stream.for_each(move |message| {
proxy
.send_event(message)
.expect("Send subscription result to event loop");
futures::future::ready(())
}),
)
.map(|_| ());
thread_pool.spawn_ok(future);
let _ = self.alive.insert(
id,
Handle {
_cancel: cancel,
listener: if event_sender.is_closed() {
None
} else {
Some(event_sender)
},
},
);
}
}
self.alive.retain(|id, _| alive.contains(&id));
}
pub fn broadcast_event(&mut self, event: Event) {
self.alive
.values_mut()
.filter_map(|connection| connection.listener.as_mut())
.for_each(|listener| {
if let Err(error) = listener.try_send(event) {
log::warn!(
"Error sending event to subscription: {:?}",
error
);
}
});
}
}