From d575f4541126e2ab25908fe55c6805f16716b2a5 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 5 Dec 2019 06:10:13 +0100 Subject: Draft first version of event subscriptions :tada: --- core/Cargo.toml | 2 ++ core/src/lib.rs | 8 ++++++- core/src/subscription.rs | 61 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 core/src/subscription.rs (limited to 'core') diff --git a/core/Cargo.toml b/core/Cargo.toml index c623ba78..0a8fd8ef 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -10,6 +10,8 @@ repository = "https://github.com/hecrj/iced" [features] # Exposes a future-based `Command` type command = ["futures"] +# Exposes a future-based `Subscription` type +subscription = ["futures"] [dependencies] futures = { version = "0.3", optional = true } diff --git a/core/src/lib.rs b/core/src/lib.rs index 65304e8b..6f13c310 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -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)] @@ -38,3 +38,9 @@ mod command; #[cfg(feature = "command")] pub use command::Command; + +#[cfg(feature = "subscription")] +pub mod subscription; + +#[cfg(feature = "subscription")] +pub use subscription::Subscription; diff --git a/core/src/subscription.rs b/core/src/subscription.rs new file mode 100644 index 00000000..1e6695d6 --- /dev/null +++ b/core/src/subscription.rs @@ -0,0 +1,61 @@ +//! Generate events asynchronously for you application. + +/// An event subscription. +pub struct Subscription { + definitions: Vec>>, +} + +impl Subscription { + pub fn none() -> Self { + Self { + definitions: Vec::new(), + } + } + + pub fn batch(subscriptions: impl Iterator>) -> Self { + Self { + definitions: subscriptions + .flat_map(|subscription| subscription.definitions) + .collect(), + } + } + + pub fn definitions(self) -> Vec>> { + self.definitions + } +} + +impl From for Subscription +where + A: Definition + 'static, +{ + fn from(definition: A) -> Self { + Self { + definitions: vec![Box::new(definition)], + } + } +} + +/// The definition of an event subscription. +pub trait Definition { + type Message; + + fn id(&self) -> u64; + + fn stream( + &self, + ) -> ( + futures::stream::BoxStream<'static, Self::Message>, + Box, + ); +} + +pub trait Handle { + fn cancel(&mut self); +} + +impl std::fmt::Debug for Subscription { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Command").finish() + } +} -- cgit From 48145ba51e045f8b0b4788f3a75d20b9d9b7e6ad Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 7 Dec 2019 08:51:44 +0100 Subject: Use `oneshot` and `future::select` to cancel streams --- core/src/subscription.rs | 37 ++++++++++++++----------------------- 1 file changed, 14 insertions(+), 23 deletions(-) (limited to 'core') diff --git a/core/src/subscription.rs b/core/src/subscription.rs index 1e6695d6..796982c7 100644 --- a/core/src/subscription.rs +++ b/core/src/subscription.rs @@ -2,60 +2,51 @@ /// An event subscription. pub struct Subscription { - definitions: Vec>>, + handles: Vec>>, } impl Subscription { pub fn none() -> Self { Self { - definitions: Vec::new(), + handles: Vec::new(), } } pub fn batch(subscriptions: impl Iterator>) -> Self { Self { - definitions: subscriptions - .flat_map(|subscription| subscription.definitions) + handles: subscriptions + .flat_map(|subscription| subscription.handles) .collect(), } } - pub fn definitions(self) -> Vec>> { - self.definitions + pub fn handles(self) -> Vec>> { + self.handles } } impl From for Subscription where - A: Definition + 'static, + A: Handle + 'static, { - fn from(definition: A) -> Self { + fn from(handle: A) -> Self { Self { - definitions: vec![Box::new(definition)], + handles: vec![Box::new(handle)], } } } -/// The definition of an event subscription. -pub trait Definition { - type Message; +/// The handle of an event subscription. +pub trait Handle { + type Output; fn id(&self) -> u64; - fn stream( - &self, - ) -> ( - futures::stream::BoxStream<'static, Self::Message>, - Box, - ); -} - -pub trait Handle { - fn cancel(&mut self); + fn stream(&self) -> futures::stream::BoxStream<'static, Self::Output>; } impl std::fmt::Debug for Subscription { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Command").finish() + f.debug_struct("Subscription").finish() } } -- cgit From 98160406f714728afe718f305bf9d12be1676b2d Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 8 Dec 2019 08:21:26 +0100 Subject: Allow listening to runtime events in subscriptions --- core/src/subscription.rs | 42 +++++++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 17 deletions(-) (limited to 'core') diff --git a/core/src/subscription.rs b/core/src/subscription.rs index 796982c7..4c021d75 100644 --- a/core/src/subscription.rs +++ b/core/src/subscription.rs @@ -1,51 +1,59 @@ //! Generate events asynchronously for you application. /// An event subscription. -pub struct Subscription { - handles: Vec>>, +pub struct Subscription { + connections: Vec>>, } -impl Subscription { +impl Subscription { pub fn none() -> Self { Self { - handles: Vec::new(), + connections: Vec::new(), } } - pub fn batch(subscriptions: impl Iterator>) -> Self { + pub fn batch( + subscriptions: impl Iterator>, + ) -> Self { Self { - handles: subscriptions - .flat_map(|subscription| subscription.handles) + connections: subscriptions + .flat_map(|subscription| subscription.connections) .collect(), } } - pub fn handles(self) -> Vec>> { - self.handles + pub fn connections( + self, + ) -> Vec>> { + self.connections } } -impl From for Subscription +impl From for Subscription where - A: Handle + 'static, + T: Connection + 'static, { - fn from(handle: A) -> Self { + fn from(handle: T) -> Self { Self { - handles: vec![Box::new(handle)], + connections: vec![Box::new(handle)], } } } -/// The handle of an event subscription. -pub trait Handle { +/// The connection of an event subscription. +pub trait Connection { + type Input; type Output; fn id(&self) -> u64; - fn stream(&self) -> futures::stream::BoxStream<'static, Self::Output>; + fn stream( + &self, + input: Self::Input, + ) -> futures::stream::BoxStream<'static, Self::Output>; } -impl std::fmt::Debug for Subscription { +impl std::fmt::Debug for Subscription { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Subscription").finish() } -- cgit From cdb7acf6c20fe13a09e75ea1c47d53ced6174698 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 10 Dec 2019 03:43:00 +0100 Subject: Implement `Subscription::map` and `from_recipe` --- core/src/subscription.rs | 117 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 91 insertions(+), 26 deletions(-) (limited to 'core') diff --git a/core/src/subscription.rs b/core/src/subscription.rs index 4c021d75..e9559f3c 100644 --- a/core/src/subscription.rs +++ b/core/src/subscription.rs @@ -1,60 +1,125 @@ //! Generate events asynchronously for you application. /// An event subscription. -pub struct Subscription { - connections: Vec>>, +pub struct Subscription { + recipes: Vec>>, } -impl Subscription { +impl Subscription +where + H: std::hash::Hasher, +{ pub fn none() -> Self { Self { - connections: Vec::new(), + recipes: Vec::new(), + } + } + + pub fn from_recipe( + recipe: impl Recipe + 'static, + ) -> Self { + Self { + recipes: vec![Box::new(recipe)], } } pub fn batch( - subscriptions: impl Iterator>, + subscriptions: impl Iterator>, ) -> Self { Self { - connections: subscriptions - .flat_map(|subscription| subscription.connections) + recipes: subscriptions + .flat_map(|subscription| subscription.recipes) .collect(), } } - pub fn connections( - self, - ) -> Vec>> { - self.connections + pub fn recipes(self) -> Vec>> { + self.recipes } -} -impl From for Subscription -where - T: Connection + 'static, -{ - fn from(handle: T) -> Self { - Self { - connections: vec![Box::new(handle)], + pub fn map( + mut self, + f: impl Fn(O) -> A + Send + Sync + 'static, + ) -> Subscription + where + H: 'static, + I: 'static, + O: 'static, + A: 'static, + { + let function = std::sync::Arc::new(f); + + Subscription { + recipes: self + .recipes + .drain(..) + .map(|recipe| { + Box::new(Map::new(recipe, function.clone())) + as Box> + }) + .collect(), } } } +impl std::fmt::Debug for Subscription { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Subscription").finish() + } +} + /// The connection of an event subscription. -pub trait Connection { - type Input; +pub trait Recipe { type Output; - fn id(&self) -> u64; + fn hash(&self, state: &mut Hasher); fn stream( &self, - input: Self::Input, + input: Input, ) -> futures::stream::BoxStream<'static, Self::Output>; } -impl std::fmt::Debug for Subscription { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Subscription").finish() +struct Map { + recipe: Box>, + mapper: std::sync::Arc B + Send + Sync>, +} + +impl Map { + fn new( + recipe: Box>, + mapper: std::sync::Arc B + Send + Sync + 'static>, + ) -> Self { + Map { recipe, mapper } + } +} + +impl Recipe for Map +where + A: 'static, + B: 'static, + H: std::hash::Hasher, +{ + type Output = B; + + fn hash(&self, state: &mut H) { + use std::hash::Hash; + + std::any::TypeId::of::().hash(state); + self.recipe.hash(state); + } + + fn stream( + &self, + input: I, + ) -> futures::stream::BoxStream<'static, Self::Output> { + use futures::StreamExt; + + let mapper = self.mapper.clone(); + + self.recipe + .stream(input) + .map(move |element| mapper(element)) + .boxed() } } -- cgit From c688452d7beb1b17ef8416fc101f8868767fc457 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 14 Dec 2019 01:13:01 +0100 Subject: Consume `Recipe` when building a `Stream` --- core/src/subscription.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'core') diff --git a/core/src/subscription.rs b/core/src/subscription.rs index e9559f3c..3ba5e629 100644 --- a/core/src/subscription.rs +++ b/core/src/subscription.rs @@ -75,7 +75,7 @@ pub trait Recipe { fn hash(&self, state: &mut Hasher); fn stream( - &self, + self: Box, input: Input, ) -> futures::stream::BoxStream<'static, Self::Output>; } @@ -110,7 +110,7 @@ where } fn stream( - &self, + self: Box, input: I, ) -> futures::stream::BoxStream<'static, Self::Output> { use futures::StreamExt; -- cgit From e71978456a0af40a1004a5c5ce1c7cdc8b709ac0 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 14 Dec 2019 03:57:04 +0100 Subject: Remove unnecessary clone in `subscription::Map` --- core/src/subscription.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'core') diff --git a/core/src/subscription.rs b/core/src/subscription.rs index 3ba5e629..21868c20 100644 --- a/core/src/subscription.rs +++ b/core/src/subscription.rs @@ -115,7 +115,7 @@ where ) -> futures::stream::BoxStream<'static, Self::Output> { use futures::StreamExt; - let mapper = self.mapper.clone(); + let mapper = self.mapper; self.recipe .stream(input) -- cgit From d6c3da21f7fe7a79bcfbc2a180dc111e42300a04 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 14 Dec 2019 05:56:46 +0100 Subject: Write docs for subscriptions and reorganize a bit --- core/src/command.rs | 4 +-- core/src/lib.rs | 2 +- core/src/subscription.rs | 65 +++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 64 insertions(+), 7 deletions(-) (limited to 'core') diff --git a/core/src/command.rs b/core/src/command.rs index 14b48b5b..e0e5ab5c 100644 --- a/core/src/command.rs +++ b/core/src/command.rs @@ -34,8 +34,8 @@ impl Command { } } - /// 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. /// diff --git a/core/src/lib.rs b/core/src/lib.rs index 6f13c310..821b09c1 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -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)] diff --git a/core/src/subscription.rs b/core/src/subscription.rs index 21868c20..8de6cae8 100644 --- a/core/src/subscription.rs +++ b/core/src/subscription.rs @@ -1,6 +1,21 @@ -//! Generate events asynchronously for you application. - -/// An event subscription. +//! Listen to external events in your application. + +/// 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 { recipes: Vec>>, } @@ -9,12 +24,19 @@ impl Subscription 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 + '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>, ) -> Self { @@ -33,10 +59,16 @@ where } } + /// Returns the different recipes of the [`Subscription`]. + /// + /// [`Subscription`]: struct.Subscription.html pub fn recipes(self) -> Vec>> { self.recipes } + /// Transforms the [`Subscription`] output with the given function. + /// + /// [`Subscription`]: struct.Subscription.html pub fn map( mut self, f: impl Fn(O) -> A + Send + Sync + 'static, @@ -68,12 +100,37 @@ impl std::fmt::Debug for Subscription { } } -/// 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 { + /// 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, input: Input, -- cgit