diff options
-rw-r--r-- | Cargo.toml | 2 | ||||
-rw-r--r-- | core/Cargo.toml | 2 | ||||
-rw-r--r-- | core/src/lib.rs | 8 | ||||
-rw-r--r-- | core/src/subscription.rs | 52 | ||||
-rw-r--r-- | examples/clock.rs | 137 | ||||
-rw-r--r-- | native/Cargo.toml | 2 | ||||
-rw-r--r-- | native/src/lib.rs | 4 | ||||
-rw-r--r-- | native/src/widget/checkbox.rs | 15 | ||||
-rw-r--r-- | src/application.rs | 11 | ||||
-rw-r--r-- | src/native.rs | 4 | ||||
-rw-r--r-- | src/sandbox.rs | 6 | ||||
-rw-r--r-- | winit/src/application.rs | 75 |
12 files changed, 307 insertions, 11 deletions
@@ -41,6 +41,8 @@ serde_json = "1.0" directories = "2.0" reqwest = "0.9" rand = "0.7" +chrono = "0.4" +futures = "0.3" iced_native = { version = "0.1", path = "./native" } iced_wgpu = { version = "0.1", path = "./wgpu" } 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..796982c7 --- /dev/null +++ b/core/src/subscription.rs @@ -0,0 +1,52 @@ +//! Generate events asynchronously for you application. + +/// An event subscription. +pub struct Subscription<T> { + handles: Vec<Box<dyn Handle<Output = T>>>, +} + +impl<T> Subscription<T> { + pub fn none() -> Self { + Self { + handles: Vec::new(), + } + } + + pub fn batch(subscriptions: impl Iterator<Item = Subscription<T>>) -> Self { + Self { + handles: subscriptions + .flat_map(|subscription| subscription.handles) + .collect(), + } + } + + pub fn handles(self) -> Vec<Box<dyn Handle<Output = T>>> { + self.handles + } +} + +impl<T, A> From<A> for Subscription<T> +where + A: Handle<Output = T> + 'static, +{ + fn from(handle: A) -> Self { + Self { + handles: vec![Box::new(handle)], + } + } +} + +/// The handle of an event subscription. +pub trait Handle { + type Output; + + fn id(&self) -> u64; + + fn stream(&self) -> futures::stream::BoxStream<'static, Self::Output>; +} + +impl<T> std::fmt::Debug for Subscription<T> { + 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 new file mode 100644 index 00000000..5a404bfa --- /dev/null +++ b/examples/clock.rs @@ -0,0 +1,137 @@ +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<chrono::Local>, + enabled: bool, +} + +#[derive(Debug, Clone)] +enum Message { + Ticked(chrono::DateTime<chrono::Local>), + Toggled(bool), +} + +impl Application for Clock { + type Message = Message; + + fn new() -> (Clock, Command<Message>) { + ( + Clock { + time: chrono::Local::now(), + enabled: false, + }, + Command::none(), + ) + } + + fn title(&self) -> String { + String::from("Clock - Iced") + } + + fn update(&mut self, message: Message) -> Command<Message> { + match message { + Message::Ticked(time) => { + self.time = time; + } + Message::Toggled(enabled) => { + self.enabled = enabled; + } + }; + + Command::none() + } + + fn subscriptions(&self) -> Subscription<Message> { + if self.enabled { + time::every(std::time::Duration::from_millis(500), Message::Ticked) + } else { + Subscription::none() + } + } + + fn view(&mut self) -> Element<Message> { + 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; + + pub fn every<Message>( + duration: std::time::Duration, + f: impl Fn(chrono::DateTime<chrono::Local>) -> Message + + 'static + + Send + + Sync, + ) -> iced::Subscription<Message> + where + Message: Send + 'static, + { + Tick { + duration, + message: Arc::new(f), + } + .into() + } + + struct Tick<Message> { + duration: std::time::Duration, + message: Arc< + dyn Fn(chrono::DateTime<chrono::Local>) -> Message + Send + Sync, + >, + } + + impl<Message> iced::subscription::Handle for Tick<Message> + where + Message: 'static, + { + type Output = Message; + + fn id(&self) -> u64 { + 0 + } + + fn stream(&self) -> 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() + } + } +} diff --git a/native/Cargo.toml b/native/Cargo.toml index 6ece36e4..7007855a 100644 --- a/native/Cargo.toml +++ b/native/Cargo.toml @@ -8,7 +8,7 @@ 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" unicode-segmentation = "1.6" diff --git a/native/src/lib.rs b/native/src/lib.rs index 45c3c699..9afb3bc9 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 9563291c..159cba84 100644 --- a/native/src/widget/checkbox.rs +++ b/native/src/widget/checkbox.rs @@ -31,6 +31,7 @@ pub struct Checkbox<Message> { on_toggle: Box<dyn Fn(bool) -> Message>, label: String, label_color: Option<Color>, + width: Length, } impl<Message> Checkbox<Message> { @@ -53,6 +54,7 @@ impl<Message> Checkbox<Message> { on_toggle: Box::new(f), label: String::from(label), label_color: None, + width: Length::Fill, } } @@ -63,6 +65,14 @@ impl<Message> Checkbox<Message> { 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<Message, Renderer> Widget<Message, Renderer> for Checkbox<Message> @@ -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 a4d20e68..95113344 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<Self::Message>; + /// TODO + fn subscriptions(&self) -> Subscription<Self::Message> { + 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::Message> { + self.0.subscriptions() + } + fn view(&mut self) -> Element<'_, Self::Message> { self.0.view() } diff --git a/src/native.rs b/src/native.rs index 3537dd52..e3733955 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 acf7f5e0..248aa152 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<T::Message> { + 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 85d06d9b..26ebdb05 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -3,8 +3,9 @@ use crate::{ input::{keyboard, mouse}, renderer::{Target, Windowed}, Cache, Command, Container, Debug, Element, Event, Length, MouseCursor, - Settings, UserInterface, + 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<Self::Message>; + /// TODO + fn subscriptions(&self) -> Subscription<Self::Message>; + /// 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(); @@ -406,6 +421,64 @@ fn spawn<Message: Send>( } } +pub struct Subscriptions { + alive: HashMap<u64, futures::channel::oneshot::Sender<()>>, +} + +impl Subscriptions { + fn new() -> Self { + Self { + alive: HashMap::new(), + } + } + + fn update<Message: Send>( + &mut self, + subscriptions: Subscription<Message>, + thread_pool: &mut futures::executor::ThreadPool, + proxy: &winit::event_loop::EventLoopProxy<Message>, + ) { + use futures::{future::FutureExt, stream::StreamExt}; + + let handles = subscriptions.handles(); + let mut alive = std::collections::HashSet::new(); + + for handle in handles { + let id = handle.id(); + let _ = alive.insert(id); + + if !self.alive.contains_key(&id) { + 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 = 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(()) + }), + ) + .map(|_| ()); + + thread_pool.spawn_ok(future); + + let _ = self.alive.insert(id, cancel); + } + } + + self.alive.retain(|id, _| alive.contains(&id)); + } +} + // 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 { |