From d575f4541126e2ab25908fe55c6805f16716b2a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 5 Dec 2019 06:10:13 +0100 Subject: [PATCH 01/21] Draft first version of event subscriptions :tada: --- Cargo.toml | 2 + core/Cargo.toml | 2 + core/src/lib.rs | 8 +++- core/src/subscription.rs | 61 +++++++++++++++++++++++++++ native/Cargo.toml | 2 +- native/src/lib.rs | 4 +- native/src/widget/checkbox.rs | 15 ++++++- src/application.rs | 11 ++++- src/native.rs | 4 +- src/sandbox.rs | 6 ++- winit/src/application.rs | 79 ++++++++++++++++++++++++++++++++++- 11 files changed, 182 insertions(+), 12 deletions(-) create mode 100644 core/src/subscription.rs diff --git a/Cargo.toml b/Cargo.toml index 888a1c0773..a07da0829e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,8 @@ serde_json = "1.0" directories = "2.0" reqwest = "0.9" rand = "0.7" +chrono = "0.4" +futures = "0.3" [target.'cfg(target_arch = "wasm32")'.dev-dependencies] wasm-bindgen = "0.2.51" diff --git a/core/Cargo.toml b/core/Cargo.toml index c623ba78f5..0a8fd8efd2 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 65304e8bff..6f13c31092 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 0000000000..1e6695d6d3 --- /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() + } +} diff --git a/native/Cargo.toml b/native/Cargo.toml index 7993676e5e..c5f9c7ac86 100644 --- a/native/Cargo.toml +++ b/native/Cargo.toml @@ -8,6 +8,6 @@ license = "MIT" repository = "https://github.com/hecrj/iced" [dependencies] -iced_core = { version = "0.1.0", path = "../core", features = ["command"] } +iced_core = { version = "0.1.0", path = "../core", features = ["command", "subscription"] } twox-hash = "1.5" raw-window-handle = "0.3" diff --git a/native/src/lib.rs b/native/src/lib.rs index 45c3c69919..9afb3bc9b9 100644 --- a/native/src/lib.rs +++ b/native/src/lib.rs @@ -52,8 +52,8 @@ mod size; mod user_interface; pub use iced_core::{ - Align, Background, Color, Command, Font, HorizontalAlignment, Length, - Point, Rectangle, Vector, VerticalAlignment, + subscription, Align, Background, Color, Command, Font, HorizontalAlignment, + Length, Point, Rectangle, Subscription, Vector, VerticalAlignment, }; pub use element::Element; diff --git a/native/src/widget/checkbox.rs b/native/src/widget/checkbox.rs index 9563291c9c..159cba84cf 100644 --- a/native/src/widget/checkbox.rs +++ b/native/src/widget/checkbox.rs @@ -31,6 +31,7 @@ pub struct Checkbox { on_toggle: Box Message>, label: String, label_color: Option, + width: Length, } impl Checkbox { @@ -53,6 +54,7 @@ impl Checkbox { on_toggle: Box::new(f), label: String::from(label), label_color: None, + width: Length::Fill, } } @@ -63,6 +65,14 @@ impl Checkbox { self.label_color = Some(color.into()); self } + + /// Sets the width of the [`Checkbox`]. + /// + /// [`Checkbox`]: struct.Checkbox.html + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } } impl Widget for Checkbox @@ -70,7 +80,7 @@ where Renderer: self::Renderer + text::Renderer + row::Renderer, { fn width(&self) -> Length { - Length::Fill + Length::Shrink } fn height(&self) -> Length { @@ -85,6 +95,7 @@ where let size = self::Renderer::default_size(renderer); Row::<(), Renderer>::new() + .width(self.width) .spacing(15) .align_items(Align::Center) .push( @@ -92,7 +103,7 @@ where .width(Length::Units(size as u16)) .height(Length::Units(size as u16)), ) - .push(Text::new(&self.label)) + .push(Text::new(&self.label).width(self.width)) .layout(renderer, limits) } diff --git a/src/application.rs b/src/application.rs index a4d20e68a7..95113344ed 100644 --- a/src/application.rs +++ b/src/application.rs @@ -1,4 +1,4 @@ -use crate::{Command, Element, Settings}; +use crate::{Command, Element, Settings, Subscription}; /// An interactive cross-platform application. /// @@ -117,6 +117,11 @@ pub trait Application: Sized { /// [`Command`]: struct.Command.html fn update(&mut self, message: Self::Message) -> Command; + /// TODO + fn subscriptions(&self) -> Subscription { + Subscription::none() + } + /// Returns the widgets to display in the [`Application`]. /// /// These widgets can produce __messages__ based on user interaction. @@ -168,6 +173,10 @@ where self.0.update(message) } + fn subscriptions(&self) -> Subscription { + self.0.subscriptions() + } + fn view(&mut self) -> Element<'_, Self::Message> { self.0.view() } diff --git a/src/native.rs b/src/native.rs index 3537dd5215..e37339559d 100644 --- a/src/native.rs +++ b/src/native.rs @@ -1,6 +1,6 @@ pub use iced_winit::{ - Align, Background, Color, Command, Font, HorizontalAlignment, Length, - VerticalAlignment, + subscription, Align, Background, Color, Command, Font, HorizontalAlignment, + Length, Subscription, VerticalAlignment, }; pub mod widget { diff --git a/src/sandbox.rs b/src/sandbox.rs index acf7f5e051..248aa152d8 100644 --- a/src/sandbox.rs +++ b/src/sandbox.rs @@ -1,4 +1,4 @@ -use crate::{Application, Command, Element, Settings}; +use crate::{Application, Command, Element, Settings, Subscription}; /// A sandboxed [`Application`]. /// @@ -149,6 +149,10 @@ where Command::none() } + fn subscriptions(&self) -> Subscription { + Subscription::none() + } + fn view(&mut self) -> Element<'_, T::Message> { T::view(self) } diff --git a/winit/src/application.rs b/winit/src/application.rs index 3772a667ce..959e142da7 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -2,9 +2,10 @@ use crate::{ conversion, input::{keyboard, mouse}, renderer::{Target, Windowed}, - Cache, Command, Container, Debug, Element, Event, Length, MouseCursor, - Settings, UserInterface, + subscription, Cache, Command, Container, Debug, Element, Event, Length, + MouseCursor, Settings, Subscription, UserInterface, }; +use std::collections::HashMap; /// An interactive, native cross-platform application. /// @@ -57,6 +58,9 @@ pub trait Application: Sized { /// [`Command`]: struct.Command.html fn update(&mut self, message: Self::Message) -> Command; + /// TODO + fn subscriptions(&self) -> Subscription; + /// Returns the widgets to display in the [`Application`]. /// /// These widgets can produce __messages__ based on user interaction. @@ -89,11 +93,15 @@ 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 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 mut title = application.title(); let window = { @@ -204,6 +212,13 @@ pub trait Application: Sized { debug.update_finished(); } + let subscriptions = application.subscriptions(); + alive_subscriptions.update( + subscriptions, + &mut thread_pool, + &proxy, + ); + // Update window title let new_title = application.title(); @@ -404,6 +419,66 @@ fn spawn( } } +pub struct Subscriptions { + alive: HashMap>, +} + +impl Subscriptions { + fn new() -> Self { + Self { + alive: HashMap::new(), + } + } + + fn update( + &mut self, + subscriptions: Subscription, + thread_pool: &mut futures::executor::ThreadPool, + proxy: &winit::event_loop::EventLoopProxy, + ) { + use futures::stream::StreamExt; + + let definitions = subscriptions.definitions(); + let mut alive = std::collections::HashSet::new(); + + for definition in definitions { + let id = definition.id(); + let _ = alive.insert(id); + + if !self.alive.contains_key(&id) { + let (stream, handle) = definition.stream(); + + let proxy = + std::sync::Arc::new(std::sync::Mutex::new(proxy.clone())); + + let future = stream.for_each(move |message| { + proxy + .lock() + .expect("Acquire event loop proxy lock") + .send_event(message) + .expect("Send subscription result to event loop"); + + futures::future::ready(()) + }); + + thread_pool.spawn_ok(future); + + let _ = self.alive.insert(id, handle); + } + } + + self.alive.retain(|id, handle| { + let is_still_alive = alive.contains(&id); + + if !is_still_alive { + handle.cancel(); + } + + is_still_alive + }); + } +} + // 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 { From e55dfa75510aa11ec197796668a772f3be4c52c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 5 Dec 2019 06:10:47 +0100 Subject: [PATCH 02/21] Add `clock` example --- examples/clock.rs | 166 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 examples/clock.rs diff --git a/examples/clock.rs b/examples/clock.rs new file mode 100644 index 0000000000..b1ee8ab1a5 --- /dev/null +++ b/examples/clock.rs @@ -0,0 +1,166 @@ +use iced::{ + Align, Application, Checkbox, Column, Command, Container, Element, Length, + Settings, Subscription, Text, +}; + +pub fn main() { + Clock::run(Settings::default()) +} + +#[derive(Debug)] +struct Clock { + time: chrono::DateTime, + enabled: bool, +} + +#[derive(Debug, Clone)] +enum Message { + Ticked(chrono::DateTime), + Toggled(bool), +} + +impl Application for Clock { + type Message = Message; + + fn new() -> (Clock, Command) { + ( + Clock { + time: chrono::Local::now(), + enabled: false, + }, + Command::none(), + ) + } + + fn title(&self) -> String { + String::from("Clock - Iced") + } + + fn update(&mut self, message: Message) -> Command { + match message { + Message::Ticked(time) => { + self.time = time; + } + Message::Toggled(enabled) => { + self.enabled = enabled; + } + }; + + Command::none() + } + + fn subscriptions(&self) -> Subscription { + if self.enabled { + time::every(std::time::Duration::from_millis(500), Message::Ticked) + } else { + Subscription::none() + } + } + + fn view(&mut self) -> Element { + let clock = Text::new(format!("{}", self.time.format("%H:%M:%S"))) + .size(40) + .width(Length::Shrink); + + let toggle = Checkbox::new(self.enabled, "Enabled", Message::Toggled) + .width(Length::Shrink); + + let content = Column::new() + .width(Length::Shrink) + .align_items(Align::Center) + .spacing(20) + .push(clock) + .push(toggle); + + Container::new(content) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .into() + } +} + +mod time { + use std::sync::{Arc, Mutex}; + + pub fn every( + duration: std::time::Duration, + f: impl Fn(chrono::DateTime) -> Message + + 'static + + Send + + Sync, + ) -> iced::Subscription + where + Message: Send + 'static, + { + Tick { + duration, + message: Arc::new(f), + } + .into() + } + + struct Tick { + duration: std::time::Duration, + message: Arc< + dyn Fn(chrono::DateTime) -> Message + Send + Sync, + >, + } + + struct TickState { + alive: Arc>, + } + + impl iced::subscription::Handle for TickState { + fn cancel(&mut self) { + match self.alive.lock() { + Ok(mut guard) => *guard = false, + _ => {} + } + } + } + + impl iced::subscription::Definition for Tick + where + Message: 'static, + { + type Message = Message; + + fn id(&self) -> u64 { + 0 + } + + fn stream( + &self, + ) -> ( + futures::stream::BoxStream<'static, Message>, + Box, + ) { + use futures::StreamExt; + + let duration = self.duration.clone(); + let function = self.message.clone(); + let alive = Arc::new(Mutex::new(true)); + + let state = TickState { + alive: alive.clone(), + }; + + let stream = futures::stream::poll_fn(move |_| { + std::thread::sleep(duration); + + if !*alive.lock().unwrap() { + return std::task::Poll::Ready(None); + } + + let now = chrono::Local::now(); + + std::task::Poll::Ready(Some(now)) + }) + .map(move |time| function(time)); + + (stream.boxed(), Box::new(state)) + } + } +} From 48145ba51e045f8b0b4788f3a75d20b9d9b7e6ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 7 Dec 2019 08:51:44 +0100 Subject: [PATCH 03/21] Use `oneshot` and `future::select` to cancel streams --- core/src/subscription.rs | 37 +++++++++++------------------ examples/clock.rs | 49 ++++++++------------------------------- winit/src/application.rs | 50 +++++++++++++++++++--------------------- 3 files changed, 48 insertions(+), 88 deletions(-) diff --git a/core/src/subscription.rs b/core/src/subscription.rs index 1e6695d6d3..796982c786 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() } } diff --git a/examples/clock.rs b/examples/clock.rs index b1ee8ab1a5..5a404bfa6c 100644 --- a/examples/clock.rs +++ b/examples/clock.rs @@ -82,7 +82,7 @@ impl Application for Clock { } mod time { - use std::sync::{Arc, Mutex}; + use std::sync::Arc; pub fn every( duration: std::time::Duration, @@ -108,59 +108,30 @@ mod time { >, } - struct TickState { - alive: Arc>, - } - - impl iced::subscription::Handle for TickState { - fn cancel(&mut self) { - match self.alive.lock() { - Ok(mut guard) => *guard = false, - _ => {} - } - } - } - - impl iced::subscription::Definition for Tick + impl iced::subscription::Handle for Tick where Message: 'static, { - type Message = Message; + type Output = Message; fn id(&self) -> u64 { 0 } - fn stream( - &self, - ) -> ( - futures::stream::BoxStream<'static, Message>, - Box, - ) { + fn stream(&self) -> futures::stream::BoxStream<'static, Message> { use futures::StreamExt; let duration = self.duration.clone(); let function = self.message.clone(); - let alive = Arc::new(Mutex::new(true)); - - let state = TickState { - alive: alive.clone(), - }; - - let stream = futures::stream::poll_fn(move |_| { - std::thread::sleep(duration); - - if !*alive.lock().unwrap() { - return std::task::Poll::Ready(None); - } - let now = chrono::Local::now(); + let stream = + futures::stream::iter(std::iter::repeat(())).map(move |_| { + std::thread::sleep(duration); - std::task::Poll::Ready(Some(now)) - }) - .map(move |time| function(time)); + function(chrono::Local::now()) + }); - (stream.boxed(), Box::new(state)) + stream.boxed() } } } diff --git a/winit/src/application.rs b/winit/src/application.rs index 959e142da7..62970810f2 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -2,8 +2,8 @@ use crate::{ conversion, input::{keyboard, mouse}, renderer::{Target, Windowed}, - subscription, Cache, Command, Container, Debug, Element, Event, Length, - MouseCursor, Settings, Subscription, UserInterface, + Cache, Command, Container, Debug, Element, Event, Length, MouseCursor, + Settings, Subscription, UserInterface, }; use std::collections::HashMap; @@ -420,7 +420,7 @@ fn spawn( } pub struct Subscriptions { - alive: HashMap>, + alive: HashMap>, } impl Subscriptions { @@ -436,46 +436,44 @@ impl Subscriptions { thread_pool: &mut futures::executor::ThreadPool, proxy: &winit::event_loop::EventLoopProxy, ) { - use futures::stream::StreamExt; + use futures::{future::FutureExt, stream::StreamExt}; - let definitions = subscriptions.definitions(); + let handles = subscriptions.handles(); let mut alive = std::collections::HashSet::new(); - for definition in definitions { - let id = definition.id(); + for handle in handles { + let id = handle.id(); let _ = alive.insert(id); if !self.alive.contains_key(&id) { - let (stream, handle) = definition.stream(); + let (cancel, cancelled) = futures::channel::oneshot::channel(); + + let stream = handle.stream(); let proxy = std::sync::Arc::new(std::sync::Mutex::new(proxy.clone())); - let future = stream.for_each(move |message| { - proxy - .lock() - .expect("Acquire event loop proxy lock") - .send_event(message) - .expect("Send subscription result to event loop"); + let future = futures::future::select( + cancelled, + stream.for_each(move |message| { + proxy + .lock() + .expect("Acquire event loop proxy lock") + .send_event(message) + .expect("Send subscription result to event loop"); - futures::future::ready(()) - }); + futures::future::ready(()) + }), + ) + .map(|_| ()); thread_pool.spawn_ok(future); - let _ = self.alive.insert(id, handle); + let _ = self.alive.insert(id, cancel); } } - self.alive.retain(|id, handle| { - let is_still_alive = alive.contains(&id); - - if !is_still_alive { - handle.cancel(); - } - - is_still_alive - }); + self.alive.retain(|id, _| alive.contains(&id)); } } From 98160406f714728afe718f305bf9d12be1676b2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sun, 8 Dec 2019 08:21:26 +0100 Subject: [PATCH 04/21] Allow listening to runtime events in subscriptions --- core/src/subscription.rs | 42 +++++++++++++++++++-------------- examples/clock.rs | 20 +++++++--------- native/Cargo.toml | 1 + native/src/lib.rs | 8 ++++--- native/src/subscription.rs | 6 +++++ src/native.rs | 4 ++-- winit/src/application.rs | 48 ++++++++++++++++++++++++++++++++------ 7 files changed, 89 insertions(+), 40 deletions(-) create mode 100644 native/src/subscription.rs diff --git a/core/src/subscription.rs b/core/src/subscription.rs index 796982c786..4c021d7572 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() } diff --git a/examples/clock.rs b/examples/clock.rs index 5a404bfa6c..f06762bd07 100644 --- a/examples/clock.rs +++ b/examples/clock.rs @@ -4,6 +4,8 @@ use iced::{ }; pub fn main() { + env_logger::init(); + Clock::run(Settings::default()) } @@ -108,30 +110,26 @@ mod time { >, } - impl iced::subscription::Handle for Tick + impl iced_native::subscription::Connection for Tick where Message: 'static, { + type Input = iced_native::subscription::Input; type Output = Message; fn id(&self) -> u64 { 0 } - fn stream(&self) -> futures::stream::BoxStream<'static, Message> { + fn stream( + &self, + input: iced_native::subscription::Input, + ) -> futures::stream::BoxStream<'static, Message> { use futures::StreamExt; - let duration = self.duration.clone(); let function = self.message.clone(); - let stream = - futures::stream::iter(std::iter::repeat(())).map(move |_| { - std::thread::sleep(duration); - - function(chrono::Local::now()) - }); - - stream.boxed() + input.map(move |_| function(chrono::Local::now())).boxed() } } } diff --git a/native/Cargo.toml b/native/Cargo.toml index 7007855a4d..a31b662756 100644 --- a/native/Cargo.toml +++ b/native/Cargo.toml @@ -12,3 +12,4 @@ iced_core = { version = "0.1.0", path = "../core", features = ["command", "subsc twox-hash = "1.5" raw-window-handle = "0.3" unicode-segmentation = "1.6" +futures = "0.3" diff --git a/native/src/lib.rs b/native/src/lib.rs index 9afb3bc9b9..af937a2f8d 100644 --- a/native/src/lib.rs +++ b/native/src/lib.rs @@ -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)] @@ -42,6 +42,7 @@ pub mod input; pub mod layout; pub mod renderer; +pub mod subscription; pub mod widget; mod element; @@ -52,8 +53,8 @@ mod size; mod user_interface; pub use iced_core::{ - subscription, Align, Background, Color, Command, Font, HorizontalAlignment, - Length, Point, Rectangle, Subscription, Vector, VerticalAlignment, + Align, Background, Color, Command, Font, HorizontalAlignment, Length, + Point, Rectangle, Vector, VerticalAlignment, }; pub use element::Element; @@ -63,5 +64,6 @@ pub use layout::Layout; pub use mouse_cursor::MouseCursor; pub use renderer::Renderer; pub use size::Size; +pub use subscription::Subscription; pub use user_interface::{Cache, UserInterface}; pub use widget::*; diff --git a/native/src/subscription.rs b/native/src/subscription.rs new file mode 100644 index 0000000000..5fa026a224 --- /dev/null +++ b/native/src/subscription.rs @@ -0,0 +1,6 @@ +use crate::Event; + +pub type Subscription = iced_core::Subscription; +pub type Input = futures::channel::mpsc::Receiver; + +pub use iced_core::subscription::Connection; diff --git a/src/native.rs b/src/native.rs index e37339559d..e517b05d8e 100644 --- a/src/native.rs +++ b/src/native.rs @@ -1,6 +1,6 @@ pub use iced_winit::{ - subscription, Align, Background, Color, Command, Font, HorizontalAlignment, - Length, Subscription, VerticalAlignment, + Align, Background, Color, Command, Font, HorizontalAlignment, Length, + Subscription, VerticalAlignment, }; pub mod widget { diff --git a/winit/src/application.rs b/winit/src/application.rs index 26ebdb0540..49a01320c2 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -184,6 +184,10 @@ pub trait Application: Sized { debug.layout_finished(); debug.event_processing_started(); + events + .iter() + .for_each(|event| alive_subscriptions.send_event(*event)); + let mut messages = user_interface.update(&renderer, events.drain(..)); messages.extend(external_messages.drain(..)); @@ -207,7 +211,6 @@ pub trait Application: Sized { debug.update_started(); let command = application.update(message); - spawn(command, &mut thread_pool, &proxy); debug.update_finished(); } @@ -422,7 +425,12 @@ fn spawn( } pub struct Subscriptions { - alive: HashMap>, + alive: HashMap, +} + +pub struct Connection { + _cancel: futures::channel::oneshot::Sender<()>, + listener: Option>, } impl Subscriptions { @@ -440,17 +448,19 @@ impl Subscriptions { ) { use futures::{future::FutureExt, stream::StreamExt}; - let handles = subscriptions.handles(); + let connections = subscriptions.connections(); let mut alive = std::collections::HashSet::new(); - for handle in handles { - let id = handle.id(); + for connection in connections { + let id = connection.id(); 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 = handle.stream(); + let stream = connection.stream(event_receiver); let proxy = std::sync::Arc::new(std::sync::Mutex::new(proxy.clone())); @@ -471,12 +481,36 @@ impl Subscriptions { thread_pool.spawn_ok(future); - let _ = self.alive.insert(id, cancel); + 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 From e189c22bb09e471e8f899ef184d2a99e2e22c484 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 9 Dec 2019 22:39:28 +0100 Subject: [PATCH 05/21] Rename `clock` example to `events` --- examples/{clock.rs => events.rs} | 80 +++++++++++++++----------------- 1 file changed, 37 insertions(+), 43 deletions(-) rename examples/{clock.rs => events.rs} (55%) diff --git a/examples/clock.rs b/examples/events.rs similarity index 55% rename from examples/clock.rs rename to examples/events.rs index f06762bd07..290aa975c7 100644 --- a/examples/clock.rs +++ b/examples/events.rs @@ -6,42 +6,40 @@ use iced::{ pub fn main() { env_logger::init(); - Clock::run(Settings::default()) + Events::run(Settings::default()) } -#[derive(Debug)] -struct Clock { - time: chrono::DateTime, +#[derive(Debug, Default)] +struct Events { + last: Vec, enabled: bool, } #[derive(Debug, Clone)] enum Message { - Ticked(chrono::DateTime), + EventOccurred(iced_native::Event), Toggled(bool), } -impl Application for Clock { +impl Application for Events { type Message = Message; - fn new() -> (Clock, Command) { - ( - Clock { - time: chrono::Local::now(), - enabled: false, - }, - Command::none(), - ) + fn new() -> (Events, Command) { + (Events::default(), Command::none()) } fn title(&self) -> String { - String::from("Clock - Iced") + String::from("Events - Iced") } fn update(&mut self, message: Message) -> Command { match message { - Message::Ticked(time) => { - self.time = time; + Message::EventOccurred(event) => { + self.last.push(event); + + if self.last.len() > 5 { + let _ = self.last.remove(0); + } } Message::Toggled(enabled) => { self.enabled = enabled; @@ -53,16 +51,23 @@ impl Application for Clock { fn subscriptions(&self) -> Subscription { if self.enabled { - time::every(std::time::Duration::from_millis(500), Message::Ticked) + events::all(Message::EventOccurred) } else { Subscription::none() } } fn view(&mut self) -> Element { - let clock = Text::new(format!("{}", self.time.format("%H:%M:%S"))) - .size(40) - .width(Length::Shrink); + 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, "Enabled", Message::Toggled) .width(Length::Shrink); @@ -71,7 +76,7 @@ impl Application for Clock { .width(Length::Shrink) .align_items(Align::Center) .spacing(20) - .push(clock) + .push(events) .push(toggle); Container::new(content) @@ -83,34 +88,23 @@ impl Application for Clock { } } -mod time { +mod events { use std::sync::Arc; - pub fn every( - duration: std::time::Duration, - f: impl Fn(chrono::DateTime) -> Message - + 'static - + Send - + Sync, + pub fn all( + f: impl Fn(iced_native::Event) -> Message + 'static + Send + Sync, ) -> iced::Subscription where Message: Send + 'static, { - Tick { - duration, - message: Arc::new(f), - } - .into() + All(Arc::new(f)).into() } - struct Tick { - duration: std::time::Duration, - message: Arc< - dyn Fn(chrono::DateTime) -> Message + Send + Sync, - >, - } + struct All( + Arc Message + Send + Sync>, + ); - impl iced_native::subscription::Connection for Tick + impl iced_native::subscription::Connection for All where Message: 'static, { @@ -127,9 +121,9 @@ mod time { ) -> futures::stream::BoxStream<'static, Message> { use futures::StreamExt; - let function = self.message.clone(); + let function = self.0.clone(); - input.map(move |_| function(chrono::Local::now())).boxed() + input.map(move |event| function(event)).boxed() } } } From cdb7acf6c20fe13a09e75ea1c47d53ced6174698 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 10 Dec 2019 03:43:00 +0100 Subject: [PATCH 06/21] Implement `Subscription::map` and `from_recipe` --- core/src/subscription.rs | 117 ++++++++++++++++++++++++++++--------- examples/events.rs | 38 +++++------- native/src/subscription.rs | 6 +- winit/src/application.rs | 20 +++++-- 4 files changed, 123 insertions(+), 58 deletions(-) diff --git a/core/src/subscription.rs b/core/src/subscription.rs index 4c021d7572..e9559f3c24 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() } } diff --git a/examples/events.rs b/examples/events.rs index 290aa975c7..10758519c9 100644 --- a/examples/events.rs +++ b/examples/events.rs @@ -51,7 +51,7 @@ impl Application for Events { fn subscriptions(&self) -> Subscription { if self.enabled { - events::all(Message::EventOccurred) + events::all().map(Message::EventOccurred) } else { Subscription::none() } @@ -89,41 +89,33 @@ impl Application for Events { } mod events { - use std::sync::Arc; - - pub fn all( - f: impl Fn(iced_native::Event) -> Message + 'static + Send + Sync, - ) -> iced::Subscription - where - Message: Send + 'static, - { - All(Arc::new(f)).into() + pub fn all() -> iced::Subscription { + iced::Subscription::from_recipe(All) } - struct All( - Arc Message + Send + Sync>, - ); + struct All; - impl iced_native::subscription::Connection for All + impl + iced_native::subscription::Recipe + for All where - Message: 'static, + H: std::hash::Hasher, { - type Input = iced_native::subscription::Input; - type Output = Message; + type Output = iced_native::Event; - fn id(&self) -> u64 { - 0 + fn hash(&self, state: &mut H) { + use std::hash::Hash; + + std::any::TypeId::of::().hash(state); } fn stream( &self, input: iced_native::subscription::Input, - ) -> futures::stream::BoxStream<'static, Message> { + ) -> futures::stream::BoxStream<'static, Self::Output> { use futures::StreamExt; - let function = self.0.clone(); - - input.map(move |event| function(event)).boxed() + input.boxed() } } } diff --git a/native/src/subscription.rs b/native/src/subscription.rs index 5fa026a224..4d00049049 100644 --- a/native/src/subscription.rs +++ b/native/src/subscription.rs @@ -1,6 +1,6 @@ -use crate::Event; +use crate::{Event, Hasher}; -pub type Subscription = iced_core::Subscription; +pub type Subscription = iced_core::Subscription; pub type Input = futures::channel::mpsc::Receiver; -pub use iced_core::subscription::Connection; +pub use iced_core::subscription::Recipe; diff --git a/winit/src/application.rs b/winit/src/application.rs index 49a01320c2..fb31c44ff3 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -2,8 +2,8 @@ use crate::{ conversion, input::{keyboard, mouse}, renderer::{Target, Windowed}, - Cache, Command, Container, Debug, Element, Event, Length, MouseCursor, - Settings, Subscription, UserInterface, + Cache, Command, Container, Debug, Element, Event, Hasher, Length, + MouseCursor, Settings, Subscription, UserInterface, }; use std::collections::HashMap; @@ -448,11 +448,19 @@ impl Subscriptions { ) { use futures::{future::FutureExt, stream::StreamExt}; - let connections = subscriptions.connections(); + let recipes = subscriptions.recipes(); let mut alive = std::collections::HashSet::new(); - for connection in connections { - let id = connection.id(); + 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) { @@ -460,7 +468,7 @@ impl Subscriptions { let (event_sender, event_receiver) = futures::channel::mpsc::channel(100); - let stream = connection.stream(event_receiver); + let stream = recipe.stream(event_receiver); let proxy = std::sync::Arc::new(std::sync::Mutex::new(proxy.clone())); From e06a4d1ce45be2d361879be480c4179e4532839d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 10 Dec 2019 04:06:12 +0100 Subject: [PATCH 07/21] Simplify `events` example --- examples/events.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/events.rs b/examples/events.rs index 10758519c9..03e8d99513 100644 --- a/examples/events.rs +++ b/examples/events.rs @@ -95,15 +95,15 @@ mod events { struct All; - impl - iced_native::subscription::Recipe - for All - where - H: std::hash::Hasher, + impl + iced_native::subscription::Recipe< + iced_native::Hasher, + iced_native::subscription::Input, + > for All { type Output = iced_native::Event; - fn hash(&self, state: &mut H) { + fn hash(&self, state: &mut iced_native::Hasher) { use std::hash::Hash; std::any::TypeId::of::().hash(state); From ffa46898d983fc15ce6051a427622c058ce4e151 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 13 Dec 2019 23:58:23 +0100 Subject: [PATCH 08/21] Add `timer` example --- Cargo.toml | 8 +- examples/events.rs | 2 - examples/timer.rs | 180 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 184 insertions(+), 6 deletions(-) create mode 100644 examples/timer.rs diff --git a/Cargo.toml b/Cargo.toml index 890c83774f..decc7d52dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,16 +35,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" +futures = "0.3" reqwest = "0.9" +async-std = { version = "1.3", features = ["unstable"] } rand = "0.7" -chrono = "0.4" -futures = "0.3" -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" diff --git a/examples/events.rs b/examples/events.rs index 03e8d99513..0a3d3ed352 100644 --- a/examples/events.rs +++ b/examples/events.rs @@ -4,8 +4,6 @@ use iced::{ }; pub fn main() { - env_logger::init(); - Events::run(Settings::default()) } diff --git a/examples/timer.rs b/examples/timer.rs new file mode 100644 index 0000000000..b64b3ef537 --- /dev/null +++ b/examples/timer.rs @@ -0,0 +1,180 @@ +use iced::{ + button, Align, Application, Background, Button, Color, Column, Command, + Container, Element, HorizontalAlignment, Length, Row, Settings, + Subscription, Text, +}; +use std::time::{Duration, Instant}; + +pub fn main() { + Timer::run(Settings::default()) +} + +struct Timer { + duration: Duration, + state: State, + toggle: button::State, + reset: button::State, +} + +enum State { + Idle, + Ticking { last_tick: Instant }, +} + +#[derive(Debug, Clone)] +enum Message { + Toggle, + Reset, + Tick(Instant), +} + +impl Application for Timer { + type Message = Message; + + fn new() -> (Timer, Command) { + ( + Timer { + duration: Duration::default(), + state: State::Idle, + toggle: button::State::new(), + reset: button::State::new(), + }, + Command::none(), + ) + } + + fn title(&self) -> String { + String::from("Timer - Iced") + } + + fn update(&mut self, message: Message) -> Command { + match message { + Message::Toggle => match self.state { + State::Idle => { + self.state = State::Ticking { + last_tick: Instant::now(), + }; + } + State::Ticking { .. } => { + self.state = State::Idle; + } + }, + Message::Tick(now) => match &mut self.state { + State::Ticking { last_tick } => { + self.duration += now - *last_tick; + *last_tick = now; + } + _ => {} + }, + Message::Reset => { + self.duration = Duration::default(); + } + } + + Command::none() + } + + fn subscriptions(&self) -> Subscription { + match self.state { + State::Idle => Subscription::none(), + State::Ticking { .. } => { + time::every(Duration::from_millis(10)).map(Message::Tick) + } + } + } + + fn view(&mut self) -> Element { + const MINUTE: u64 = 60; + const HOUR: u64 = 60 * MINUTE; + + let seconds = self.duration.as_secs(); + + let duration = Text::new(format!( + "{:0>2}:{:0>2}:{:0>2}.{:0>2}", + seconds / HOUR, + seconds / MINUTE, + seconds % MINUTE, + self.duration.subsec_millis() / 10, + )) + .width(Length::Shrink) + .size(40); + + let button = |state, label, color: [f32; 3]| { + Button::new( + state, + Text::new(label) + .color(Color::WHITE) + .horizontal_alignment(HorizontalAlignment::Center), + ) + .min_width(80) + .background(Background::Color(color.into())) + .border_radius(10) + .padding(10) + }; + + let toggle_button = { + let (label, color) = match self.state { + State::Idle => ("Start", [0.11, 0.42, 0.87]), + State::Ticking { .. } => ("Stop", [0.9, 0.4, 0.4]), + }; + + button(&mut self.toggle, label, color).on_press(Message::Toggle) + }; + + let reset_button = button(&mut self.reset, "Reset", [0.7, 0.7, 0.7]) + .on_press(Message::Reset); + + let controls = Row::new() + .width(Length::Shrink) + .spacing(20) + .push(toggle_button) + .push(reset_button); + + let content = Column::new() + .width(Length::Shrink) + .align_items(Align::Center) + .spacing(20) + .push(duration) + .push(controls); + + Container::new(content) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .into() + } +} + +mod time { + pub fn every( + duration: std::time::Duration, + ) -> iced::Subscription { + iced::Subscription::from_recipe(Every(duration)) + } + + struct Every(std::time::Duration); + + impl iced_native::subscription::Recipe + for Every + { + type Output = std::time::Instant; + + fn hash(&self, state: &mut iced_native::Hasher) { + use std::hash::Hash; + + std::any::TypeId::of::().hash(state); + } + + fn stream( + &self, + _input: Input, + ) -> futures::stream::BoxStream<'static, Self::Output> { + use futures::stream::StreamExt; + + async_std::stream::interval(self.0) + .map(|_| std::time::Instant::now()) + .boxed() + } + } +} From f0381a7fb305a371208df62d339540ce3d48f9da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 14 Dec 2019 00:32:05 +0100 Subject: [PATCH 09/21] Use `surf` in `pokedex` example --- Cargo.toml | 2 +- examples/pokedex.rs | 23 +++++++++-------------- examples/timer.rs | 2 +- 3 files changed, 11 insertions(+), 16 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index decc7d52dc..ac4d3bed71 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,8 +42,8 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" directories = "2.0" futures = "0.3" -reqwest = "0.9" async-std = { version = "1.3", features = ["unstable"] } +surf = { version = "1.0", git = "https://github.com/http-rs/surf.git", rev = "2ff0f95513e82bdb5ccc56767f9dd0985f2eb8fe" } rand = "0.7" [target.'cfg(target_arch = "wasm32")'.dev-dependencies] diff --git a/examples/pokedex.rs b/examples/pokedex.rs index b9daeabd79..2d595ec48c 100644 --- a/examples/pokedex.rs +++ b/examples/pokedex.rs @@ -150,7 +150,6 @@ impl Pokemon { async fn search() -> Result { use rand::Rng; use serde::Deserialize; - use std::io::Read; #[derive(Debug, Deserialize)] struct Entry { @@ -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 @@ -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(), @@ -203,7 +199,7 @@ impl Pokemon { .chars() .map(|c| if c.is_control() { ' ' } else { c }) .collect(), - image: image::Handle::from_memory(bytes), + image: image::Handle::from_memory(sprite), }) } } @@ -211,13 +207,12 @@ impl Pokemon { #[derive(Debug, Clone)] enum Error { APIError, - ImageError, LanguageError, } -impl From for Error { - fn from(error: reqwest::Error) -> Error { - dbg!(&error); +impl From for Error { + fn from(exception: surf::Exception) -> Error { + dbg!(&exception); Error::APIError } diff --git a/examples/timer.rs b/examples/timer.rs index b64b3ef537..367c5b2b2f 100644 --- a/examples/timer.rs +++ b/examples/timer.rs @@ -92,7 +92,7 @@ impl Application for Timer { let duration = Text::new(format!( "{:0>2}:{:0>2}:{:0>2}.{:0>2}", seconds / HOUR, - seconds / MINUTE, + (seconds % HOUR) / MINUTE, seconds % MINUTE, self.duration.subsec_millis() / 10, )) From 69ed631d449e74b38054aa052c620368cb65c72c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 14 Dec 2019 00:45:38 +0100 Subject: [PATCH 10/21] Rename `timer` example to `stopwatch` --- examples/{timer.rs => stopwatch.rs} | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) rename examples/{timer.rs => stopwatch.rs} (95%) diff --git a/examples/timer.rs b/examples/stopwatch.rs similarity index 95% rename from examples/timer.rs rename to examples/stopwatch.rs index 367c5b2b2f..4f93127820 100644 --- a/examples/timer.rs +++ b/examples/stopwatch.rs @@ -6,10 +6,10 @@ use iced::{ use std::time::{Duration, Instant}; pub fn main() { - Timer::run(Settings::default()) + Stopwatch::run(Settings::default()) } -struct Timer { +struct Stopwatch { duration: Duration, state: State, toggle: button::State, @@ -28,12 +28,12 @@ enum Message { Tick(Instant), } -impl Application for Timer { +impl Application for Stopwatch { type Message = Message; - fn new() -> (Timer, Command) { + fn new() -> (Stopwatch, Command) { ( - Timer { + Stopwatch { duration: Duration::default(), state: State::Idle, toggle: button::State::new(), @@ -44,7 +44,7 @@ impl Application for Timer { } fn title(&self) -> String { - String::from("Timer - Iced") + String::from("Stopwatch - Iced") } fn update(&mut self, message: Message) -> Command { From c688452d7beb1b17ef8416fc101f8868767fc457 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 14 Dec 2019 01:13:01 +0100 Subject: [PATCH 11/21] Consume `Recipe` when building a `Stream` --- core/src/subscription.rs | 4 ++-- examples/events.rs | 2 +- examples/stopwatch.rs | 2 +- winit/src/application.rs | 1 + 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/core/src/subscription.rs b/core/src/subscription.rs index e9559f3c24..3ba5e629c3 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; diff --git a/examples/events.rs b/examples/events.rs index 0a3d3ed352..f9e606d812 100644 --- a/examples/events.rs +++ b/examples/events.rs @@ -108,7 +108,7 @@ mod events { } fn stream( - &self, + self: Box, input: iced_native::subscription::Input, ) -> futures::stream::BoxStream<'static, Self::Output> { use futures::StreamExt; diff --git a/examples/stopwatch.rs b/examples/stopwatch.rs index 4f93127820..b902baae35 100644 --- a/examples/stopwatch.rs +++ b/examples/stopwatch.rs @@ -167,7 +167,7 @@ mod time { } fn stream( - &self, + self: Box, _input: Input, ) -> futures::stream::BoxStream<'static, Self::Output> { use futures::stream::StreamExt; diff --git a/winit/src/application.rs b/winit/src/application.rs index fb31c44ff3..67a035f762 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -470,6 +470,7 @@ impl Subscriptions { let stream = recipe.stream(event_receiver); + // TODO: Find out how to avoid using a mutex here let proxy = std::sync::Arc::new(std::sync::Mutex::new(proxy.clone())); From 65ff3744a06ca5bcb2e1682862d49520bf7ccbfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 14 Dec 2019 01:33:31 +0100 Subject: [PATCH 12/21] Remove unnecessary event loop proxy `Mutex` I am not sure why I had to use it in the first place... --- winit/src/application.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/winit/src/application.rs b/winit/src/application.rs index 67a035f762..25396b6f41 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -469,17 +469,12 @@ impl Subscriptions { futures::channel::mpsc::channel(100); let stream = recipe.stream(event_receiver); - - // TODO: Find out how to avoid using a mutex here - let proxy = - std::sync::Arc::new(std::sync::Mutex::new(proxy.clone())); + let proxy = proxy.clone(); let future = futures::future::select( cancelled, stream.for_each(move |message| { proxy - .lock() - .expect("Acquire event loop proxy lock") .send_event(message) .expect("Send subscription result to event loop"); From 77154869061594706185709d7b79ac520c8a125a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 14 Dec 2019 02:12:25 +0100 Subject: [PATCH 13/21] Use generic `Hasher` in `stopwatch` --- examples/stopwatch.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/stopwatch.rs b/examples/stopwatch.rs index b902baae35..686d239d2a 100644 --- a/examples/stopwatch.rs +++ b/examples/stopwatch.rs @@ -155,12 +155,13 @@ mod time { struct Every(std::time::Duration); - impl iced_native::subscription::Recipe - for Every + impl iced_native::subscription::Recipe for Every + where + Hasher: std::hash::Hasher, { type Output = std::time::Instant; - fn hash(&self, state: &mut iced_native::Hasher) { + fn hash(&self, state: &mut Hasher) { use std::hash::Hash; std::any::TypeId::of::().hash(state); From c13ef73e8be5ba38451731c8f7e99ee54acf42f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 14 Dec 2019 02:20:45 +0100 Subject: [PATCH 14/21] Hash `Duration` of `time::Every` in `stopwatch` --- examples/stopwatch.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/stopwatch.rs b/examples/stopwatch.rs index 686d239d2a..d21bdaa863 100644 --- a/examples/stopwatch.rs +++ b/examples/stopwatch.rs @@ -165,6 +165,7 @@ mod time { use std::hash::Hash; std::any::TypeId::of::().hash(state); + self.0.hash(state); } fn stream( From e71978456a0af40a1004a5c5ce1c7cdc8b709ac0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 14 Dec 2019 03:57:04 +0100 Subject: [PATCH 15/21] Remove unnecessary clone in `subscription::Map` --- core/src/subscription.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/subscription.rs b/core/src/subscription.rs index 3ba5e629c3..21868c2006 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) From ba06d458d33d98bfaa5e66b3512ce7f063e8d7ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 14 Dec 2019 04:12:42 +0100 Subject: [PATCH 16/21] Move native events subscription to `iced_native` --- examples/events.rs | 42 +++++++------------------------------- native/src/subscription.rs | 25 +++++++++++++++++++++++ 2 files changed, 32 insertions(+), 35 deletions(-) diff --git a/examples/events.rs b/examples/events.rs index f9e606d812..0b944495fa 100644 --- a/examples/events.rs +++ b/examples/events.rs @@ -49,7 +49,7 @@ impl Application for Events { fn subscriptions(&self) -> Subscription { if self.enabled { - events::all().map(Message::EventOccurred) + iced_native::subscription::events().map(Message::EventOccurred) } else { Subscription::none() } @@ -67,8 +67,12 @@ impl Application for Events { }, ); - let toggle = Checkbox::new(self.enabled, "Enabled", Message::Toggled) - .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) @@ -85,35 +89,3 @@ impl Application for Events { .into() } } - -mod events { - pub fn all() -> iced::Subscription { - iced::Subscription::from_recipe(All) - } - - struct All; - - impl - iced_native::subscription::Recipe< - iced_native::Hasher, - iced_native::subscription::Input, - > for All - { - type Output = iced_native::Event; - - fn hash(&self, state: &mut iced_native::Hasher) { - use std::hash::Hash; - - std::any::TypeId::of::().hash(state); - } - - fn stream( - self: Box, - input: iced_native::subscription::Input, - ) -> futures::stream::BoxStream<'static, Self::Output> { - use futures::StreamExt; - - input.boxed() - } - } -} diff --git a/native/src/subscription.rs b/native/src/subscription.rs index 4d00049049..c25e1cb4ac 100644 --- a/native/src/subscription.rs +++ b/native/src/subscription.rs @@ -4,3 +4,28 @@ pub type Subscription = iced_core::Subscription; pub type Input = futures::channel::mpsc::Receiver; pub use iced_core::subscription::Recipe; + +pub fn events() -> Subscription { + Subscription::from_recipe(Events) +} + +struct Events; + +impl Recipe for Events { + type Output = Event; + + fn hash(&self, state: &mut Hasher) { + use std::hash::Hash; + + std::any::TypeId::of::().hash(state); + } + + fn stream( + self: Box, + input: Input, + ) -> futures::stream::BoxStream<'static, Self::Output> { + use futures::StreamExt; + + input.boxed() + } +} From 293314405f5b8d4003db5ef8f428e659ae36872d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 14 Dec 2019 04:49:13 +0100 Subject: [PATCH 17/21] Make `iced_native` subscription input opaque --- examples/stopwatch.rs | 8 ++++---- native/src/subscription.rs | 31 ++++++++----------------------- native/src/subscription/events.rs | 23 +++++++++++++++++++++++ winit/src/application.rs | 2 +- 4 files changed, 36 insertions(+), 28 deletions(-) create mode 100644 native/src/subscription/events.rs diff --git a/examples/stopwatch.rs b/examples/stopwatch.rs index d21bdaa863..0d52a091db 100644 --- a/examples/stopwatch.rs +++ b/examples/stopwatch.rs @@ -155,13 +155,13 @@ mod time { struct Every(std::time::Duration); - impl iced_native::subscription::Recipe for Every + impl iced_native::subscription::Recipe for Every where - Hasher: std::hash::Hasher, + H: std::hash::Hasher, { type Output = std::time::Instant; - fn hash(&self, state: &mut Hasher) { + fn hash(&self, state: &mut H) { use std::hash::Hash; std::any::TypeId::of::().hash(state); @@ -170,7 +170,7 @@ mod time { fn stream( self: Box, - _input: Input, + _input: I, ) -> futures::stream::BoxStream<'static, Self::Output> { use futures::stream::StreamExt; diff --git a/native/src/subscription.rs b/native/src/subscription.rs index c25e1cb4ac..c49e24d2ca 100644 --- a/native/src/subscription.rs +++ b/native/src/subscription.rs @@ -1,31 +1,16 @@ use crate::{Event, Hasher}; +use futures::stream::BoxStream; -pub type Subscription = iced_core::Subscription; -pub type Input = futures::channel::mpsc::Receiver; +pub type EventStream = BoxStream<'static, Event>; -pub use iced_core::subscription::Recipe; - -pub fn events() -> Subscription { - Subscription::from_recipe(Events) -} +pub type Subscription = iced_core::Subscription; -struct Events; - -impl Recipe for Events { - type Output = Event; - - fn hash(&self, state: &mut Hasher) { - use std::hash::Hash; +pub use iced_core::subscription::Recipe; - std::any::TypeId::of::().hash(state); - } +mod events; - fn stream( - self: Box, - input: Input, - ) -> futures::stream::BoxStream<'static, Self::Output> { - use futures::StreamExt; +use events::Events; - input.boxed() - } +pub fn events() -> Subscription { + Subscription::from_recipe(Events) } diff --git a/native/src/subscription/events.rs b/native/src/subscription/events.rs new file mode 100644 index 0000000000..b730182873 --- /dev/null +++ b/native/src/subscription/events.rs @@ -0,0 +1,23 @@ +use crate::{ + subscription::{EventStream, Recipe}, + Event, Hasher, +}; + +pub struct Events; + +impl Recipe for Events { + type Output = Event; + + fn hash(&self, state: &mut Hasher) { + use std::hash::Hash; + + std::any::TypeId::of::().hash(state); + } + + fn stream( + self: Box, + event_stream: EventStream, + ) -> futures::stream::BoxStream<'static, Self::Output> { + event_stream + } +} diff --git a/winit/src/application.rs b/winit/src/application.rs index 25396b6f41..2d30e9f3af 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -468,7 +468,7 @@ impl Subscriptions { let (event_sender, event_receiver) = futures::channel::mpsc::channel(100); - let stream = recipe.stream(event_receiver); + let stream = recipe.stream(event_receiver.boxed()); let proxy = proxy.clone(); let future = futures::future::select( From d6c3da21f7fe7a79bcfbc2a180dc111e42300a04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 14 Dec 2019 05:56:46 +0100 Subject: [PATCH 18/21] Write docs for subscriptions and reorganize a bit --- core/src/command.rs | 4 +- core/src/lib.rs | 2 +- core/src/subscription.rs | 65 +++++++++++++++++-- examples/events.rs | 2 +- examples/stopwatch.rs | 2 +- native/src/lib.rs | 2 +- native/src/subscription.rs | 30 ++++++++- src/application.rs | 17 +++-- src/sandbox.rs | 2 +- winit/src/application.rs | 124 ++++++------------------------------- winit/src/lib.rs | 1 + winit/src/subscription.rs | 97 +++++++++++++++++++++++++++++ 12 files changed, 225 insertions(+), 123 deletions(-) create mode 100644 winit/src/subscription.rs diff --git a/core/src/command.rs b/core/src/command.rs index 14b48b5b42..e0e5ab5c41 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 6f13c31092..821b09c13d 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 21868c2006..8de6cae8b5 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, diff --git a/examples/events.rs b/examples/events.rs index 0b944495fa..7d83fbd86f 100644 --- a/examples/events.rs +++ b/examples/events.rs @@ -47,7 +47,7 @@ impl Application for Events { Command::none() } - fn subscriptions(&self) -> Subscription { + fn subscription(&self) -> Subscription { if self.enabled { iced_native::subscription::events().map(Message::EventOccurred) } else { diff --git a/examples/stopwatch.rs b/examples/stopwatch.rs index 0d52a091db..7a7f07934c 100644 --- a/examples/stopwatch.rs +++ b/examples/stopwatch.rs @@ -74,7 +74,7 @@ impl Application for Stopwatch { Command::none() } - fn subscriptions(&self) -> Subscription { + fn subscription(&self) -> Subscription { match self.state { State::Idle => Subscription::none(), State::Ticking { .. } => { diff --git a/native/src/lib.rs b/native/src/lib.rs index af937a2f8d..c4d72df83e 100644 --- a/native/src/lib.rs +++ b/native/src/lib.rs @@ -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)] diff --git a/native/src/subscription.rs b/native/src/subscription.rs index c49e24d2ca..db88867ab3 100644 --- a/native/src/subscription.rs +++ b/native/src/subscription.rs @@ -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 = iced_core::Subscription; +/// 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 { Subscription::from_recipe(Events) } diff --git a/src/application.rs b/src/application.rs index 95113344ed..98e160ce3f 100644 --- a/src/application.rs +++ b/src/application.rs @@ -117,8 +117,17 @@ pub trait Application: Sized { /// [`Command`]: struct.Command.html fn update(&mut self, message: Self::Message) -> Command; - /// TODO - fn subscriptions(&self) -> Subscription { + /// 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() } @@ -173,8 +182,8 @@ where self.0.update(message) } - fn subscriptions(&self) -> Subscription { - self.0.subscriptions() + fn subscription(&self) -> Subscription { + self.0.subscription() } fn view(&mut self) -> Element<'_, Self::Message> { diff --git a/src/sandbox.rs b/src/sandbox.rs index 248aa152d8..75020b16b9 100644 --- a/src/sandbox.rs +++ b/src/sandbox.rs @@ -149,7 +149,7 @@ where Command::none() } - fn subscriptions(&self) -> Subscription { + fn subscription(&self) -> Subscription { Subscription::none() } diff --git a/winit/src/application.rs b/winit/src/application.rs index 2d30e9f3af..3b8ac16b1d 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -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; - /// TODO - fn subscriptions(&self) -> Subscription; + /// 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; /// 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( } } -pub struct Subscriptions { - alive: HashMap, -} - -pub struct Connection { - _cancel: futures::channel::oneshot::Sender<()>, - listener: Option>, -} - -impl Subscriptions { - fn new() -> Self { - Self { - alive: HashMap::new(), - } - } - - fn update( - &mut self, - subscriptions: Subscription, - thread_pool: &mut futures::executor::ThreadPool, - proxy: &winit::event_loop::EventLoopProxy, - ) { - 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 { diff --git a/winit/src/lib.rs b/winit/src/lib.rs index df3a699794..8a1dc870d0 100644 --- a/winit/src/lib.rs +++ b/winit/src/lib.rs @@ -29,6 +29,7 @@ pub mod conversion; pub mod settings; mod application; +mod subscription; pub use application::Application; pub use settings::Settings; diff --git a/winit/src/subscription.rs b/winit/src/subscription.rs new file mode 100644 index 0000000000..610bdc102e --- /dev/null +++ b/winit/src/subscription.rs @@ -0,0 +1,97 @@ +use iced_native::{Event, Hasher, Subscription}; +use std::collections::HashMap; + +pub struct Pool { + alive: HashMap, +} + +pub struct Handle { + _cancel: futures::channel::oneshot::Sender<()>, + listener: Option>, +} + +impl Pool { + pub fn new() -> Self { + Self { + alive: HashMap::new(), + } + } + + pub fn update( + &mut self, + subscription: Subscription, + thread_pool: &mut futures::executor::ThreadPool, + proxy: &winit::event_loop::EventLoopProxy, + ) { + 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 + ); + } + }); + } +} From 3daa9c2cc886a323feb4fe52b14833617e92dd36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 14 Dec 2019 06:42:26 +0100 Subject: [PATCH 19/21] Log broadcast error as an error instead of a warning --- winit/src/subscription.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/winit/src/subscription.rs b/winit/src/subscription.rs index 610bdc102e..f55507afcc 100644 --- a/winit/src/subscription.rs +++ b/winit/src/subscription.rs @@ -87,7 +87,7 @@ impl Pool { .filter_map(|connection| connection.listener.as_mut()) .for_each(|listener| { if let Err(error) = listener.try_send(event) { - log::warn!( + log::error!( "Error sending event to subscription: {:?}", error ); From 5185d6a0f3e5993247923e857164842ef8be9105 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 14 Dec 2019 07:09:37 +0100 Subject: [PATCH 20/21] Fix `Widget::width` for `Checkbox` --- native/src/widget/checkbox.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/native/src/widget/checkbox.rs b/native/src/widget/checkbox.rs index 159cba84cf..ca4410b921 100644 --- a/native/src/widget/checkbox.rs +++ b/native/src/widget/checkbox.rs @@ -80,7 +80,7 @@ where Renderer: self::Renderer + text::Renderer + row::Renderer, { fn width(&self) -> Length { - Length::Shrink + self.width } fn height(&self) -> Length { From 430ab6e44432d044f8444575053d97651f0f7d20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 14 Dec 2019 20:48:32 +0100 Subject: [PATCH 21/21] Port `todos` to `async_std` --- examples/todos.rs | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/examples/todos.rs b/examples/todos.rs index 5f435fdcfb..42e88f6552 100644 --- a/examples/todos.rs +++ b/examples/todos.rs @@ -517,21 +517,23 @@ impl SavedState { } async fn load() -> Result { - use std::io::Read; + use async_std::prelude::*; let mut contents = String::new(); - let mut file = std::fs::File::open(Self::path()) + let mut file = async_std::fs::File::open(Self::path()) + .await .map_err(|_| LoadError::FileError)?; file.read_to_string(&mut contents) + .await .map_err(|_| LoadError::FileError)?; serde_json::from_str(&contents).map_err(|_| LoadError::FormatError) } async fn save(self) -> Result<(), SaveError> { - use std::io::Write; + use async_std::prelude::*; let json = serde_json::to_string_pretty(&self) .map_err(|_| SaveError::FormatError)?; @@ -539,20 +541,23 @@ impl SavedState { let path = Self::path(); if let Some(dir) = path.parent() { - std::fs::create_dir_all(dir) + async_std::fs::create_dir_all(dir) + .await .map_err(|_| SaveError::DirectoryError)?; } - let mut file = - std::fs::File::create(path).map_err(|_| SaveError::FileError)?; + { + let mut file = async_std::fs::File::create(path) + .await + .map_err(|_| SaveError::FileError)?; - file.write_all(json.as_bytes()) - .map_err(|_| SaveError::WriteError)?; + file.write_all(json.as_bytes()) + .await + .map_err(|_| SaveError::WriteError)?; + } // This is a simple way to save at most once every couple seconds - // We will be able to get rid of it once we implement event - // subscriptions - std::thread::sleep(std::time::Duration::from_secs(2)); + async_std::task::sleep(std::time::Duration::from_secs(2)).await; Ok(()) }