Skip to content

Commit

Permalink
Merge pull request #122 from hecrj/feature/event-subscriptions
Browse files Browse the repository at this point in the history
Event subscriptions
  • Loading branch information
hecrj authored Dec 16, 2019
2 parents 3702b10 + 430ab6e commit 0f2e20f
Show file tree
Hide file tree
Showing 20 changed files with 726 additions and 39 deletions.
8 changes: 5 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,16 @@ iced_wgpu = { version = "0.1.0", path = "wgpu" }
iced_web = { version = "0.1.0", path = "web" }

[dev-dependencies]
iced_native = { version = "0.1", path = "./native" }
iced_wgpu = { version = "0.1", path = "./wgpu" }
env_logger = "0.7"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
directories = "2.0"
reqwest = "0.9"
futures = "0.3"
async-std = { version = "1.3", features = ["unstable"] }
surf = { version = "1.0", git = "https://github.com/http-rs/surf.git", rev = "2ff0f95513e82bdb5ccc56767f9dd0985f2eb8fe" }
rand = "0.7"
iced_native = { version = "0.1", path = "./native" }
iced_wgpu = { version = "0.1", path = "./wgpu" }

[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
wasm-bindgen = "0.2.51"
2 changes: 2 additions & 0 deletions core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
4 changes: 2 additions & 2 deletions core/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down
6 changes: 6 additions & 0 deletions core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
182 changes: 182 additions & 0 deletions core/src/subscription.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
//! 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<Hasher, Input, Output> {
recipes: Vec<Box<dyn Recipe<Hasher, Input, Output = Output>>>,
}

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 {
Self {
recipes: vec![Box::new(recipe)],
}
}

/// 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 {
Self {
recipes: subscriptions
.flat_map(|subscription| subscription.recipes)
.collect(),
}
}

/// 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,
) -> Subscription<H, I, A>
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<dyn Recipe<H, I, Output = A>>
})
.collect(),
}
}
}

impl<I, O, H> std::fmt::Debug for Subscription<I, O, H> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Subscription").finish()
}
}

/// 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,
) -> futures::stream::BoxStream<'static, Self::Output>;
}

struct Map<Hasher, Input, A, B> {
recipe: Box<dyn Recipe<Hasher, Input, Output = A>>,
mapper: std::sync::Arc<dyn Fn(A) -> B + Send + Sync>,
}

impl<H, I, A, B> Map<H, I, A, B> {
fn new(
recipe: Box<dyn Recipe<H, I, Output = A>>,
mapper: std::sync::Arc<dyn Fn(A) -> B + Send + Sync + 'static>,
) -> Self {
Map { recipe, mapper }
}
}

impl<H, I, A, B> Recipe<H, I> for Map<H, I, A, B>
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::<B>().hash(state);
self.recipe.hash(state);
}

fn stream(
self: Box<Self>,
input: I,
) -> futures::stream::BoxStream<'static, Self::Output> {
use futures::StreamExt;

let mapper = self.mapper;

self.recipe
.stream(input)
.map(move |element| mapper(element))
.boxed()
}
}
91 changes: 91 additions & 0 deletions examples/events.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
use iced::{
Align, Application, Checkbox, Column, Command, Container, Element, Length,
Settings, Subscription, Text,
};

pub fn main() {
Events::run(Settings::default())
}

#[derive(Debug, Default)]
struct Events {
last: Vec<iced_native::Event>,
enabled: bool,
}

#[derive(Debug, Clone)]
enum Message {
EventOccurred(iced_native::Event),
Toggled(bool),
}

impl Application for Events {
type Message = Message;

fn new() -> (Events, Command<Message>) {
(Events::default(), Command::none())
}

fn title(&self) -> String {
String::from("Events - Iced")
}

fn update(&mut self, message: Message) -> Command<Message> {
match message {
Message::EventOccurred(event) => {
self.last.push(event);

if self.last.len() > 5 {
let _ = self.last.remove(0);
}
}
Message::Toggled(enabled) => {
self.enabled = enabled;
}
};

Command::none()
}

fn subscription(&self) -> Subscription<Message> {
if self.enabled {
iced_native::subscription::events().map(Message::EventOccurred)
} else {
Subscription::none()
}
}

fn view(&mut self) -> Element<Message> {
let events = self.last.iter().fold(
Column::new().width(Length::Shrink).spacing(10),
|column, event| {
column.push(
Text::new(format!("{:?}", event))
.size(40)
.width(Length::Shrink),
)
},
);

let toggle = Checkbox::new(
self.enabled,
"Listen to runtime events",
Message::Toggled,
)
.width(Length::Shrink);

let content = Column::new()
.width(Length::Shrink)
.align_items(Align::Center)
.spacing(20)
.push(events)
.push(toggle);

Container::new(content)
.width(Length::Fill)
.height(Length::Fill)
.center_x()
.center_y()
.into()
}
}
23 changes: 9 additions & 14 deletions examples/pokedex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,6 @@ impl Pokemon {
async fn search() -> Result<Pokemon, Error> {
use rand::Rng;
use serde::Deserialize;
use std::io::Read;

#[derive(Debug, Deserialize)]
struct Entry {
Expand Down Expand Up @@ -179,7 +178,11 @@ impl Pokemon {
let url = format!("https://pokeapi.co/api/v2/pokemon-species/{}", id);
let sprite = format!("https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/{}.png", id);

let entry: Entry = reqwest::get(&url)?.json()?;
let (entry, sprite): (Entry, _) = futures::future::try_join(
surf::get(&url).recv_json(),
surf::get(&sprite).recv_bytes(),
)
.await?;

let description = entry
.flavor_text_entries
Expand All @@ -188,13 +191,6 @@ impl Pokemon {
.next()
.ok_or(Error::LanguageError)?;

let mut sprite = reqwest::get(&sprite)?;
let mut bytes = Vec::new();

sprite
.read_to_end(&mut bytes)
.map_err(|_| Error::ImageError)?;

Ok(Pokemon {
number: id,
name: entry.name.to_uppercase(),
Expand All @@ -203,21 +199,20 @@ impl Pokemon {
.chars()
.map(|c| if c.is_control() { ' ' } else { c })
.collect(),
image: image::Handle::from_memory(bytes),
image: image::Handle::from_memory(sprite),
})
}
}

#[derive(Debug, Clone)]
enum Error {
APIError,
ImageError,
LanguageError,
}

impl From<reqwest::Error> for Error {
fn from(error: reqwest::Error) -> Error {
dbg!(&error);
impl From<surf::Exception> for Error {
fn from(exception: surf::Exception) -> Error {
dbg!(&exception);

Error::APIError
}
Expand Down
Loading

0 comments on commit 0f2e20f

Please sign in to comment.