From 32f7ca261f0655938ae7c8919599b020ddea8ff8 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 19 Jan 2020 08:36:44 +0100 Subject: Implement `subscription::Tracker` in `iced_core` --- core/Cargo.toml | 3 +- core/src/lib.rs | 2 +- core/src/subscription.rs | 11 +++- core/src/subscription/tracker.rs | 112 ++++++++++++++++++++++++++++++++++++++ examples/stopwatch.rs | 2 +- native/src/subscription.rs | 7 ++- native/src/subscription/events.rs | 2 +- 7 files changed, 131 insertions(+), 8 deletions(-) create mode 100644 core/src/subscription/tracker.rs diff --git a/core/Cargo.toml b/core/Cargo.toml index 0a8fd8ef..4e019ba9 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -11,7 +11,8 @@ repository = "https://github.com/hecrj/iced" # Exposes a future-based `Command` type command = ["futures"] # Exposes a future-based `Subscription` type -subscription = ["futures"] +subscription = ["futures", "log"] [dependencies] futures = { version = "0.3", optional = true } +log = { version = "0.4", optional = true } diff --git a/core/src/lib.rs b/core/src/lib.rs index 821b09c1..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)] diff --git a/core/src/subscription.rs b/core/src/subscription.rs index d9e7e388..87e51e48 100644 --- a/core/src/subscription.rs +++ b/core/src/subscription.rs @@ -1,4 +1,9 @@ //! Listen to external events in your application. +mod tracker; + +pub use tracker::Tracker; + +use futures::stream::BoxStream; /// A request to listen to external events. /// @@ -134,8 +139,8 @@ pub trait Recipe { /// [`Recipe`]: trait.Recipe.html fn stream( self: Box, - input: Input, - ) -> futures::stream::BoxStream<'static, Self::Output>; + input: BoxStream<'static, Input>, + ) -> BoxStream<'static, Self::Output>; } struct Map { @@ -169,7 +174,7 @@ where fn stream( self: Box, - input: I, + input: BoxStream<'static, I>, ) -> futures::stream::BoxStream<'static, Self::Output> { use futures::StreamExt; diff --git a/core/src/subscription/tracker.rs b/core/src/subscription/tracker.rs new file mode 100644 index 00000000..826f60c0 --- /dev/null +++ b/core/src/subscription/tracker.rs @@ -0,0 +1,112 @@ +use crate::Subscription; + +use futures::{future::BoxFuture, sink::Sink}; +use std::collections::HashMap; +use std::marker::PhantomData; + +#[derive(Debug)] +pub struct Tracker { + subscriptions: HashMap>, + _hasher: PhantomData, +} + +#[derive(Debug)] +pub struct Execution { + _cancel: futures::channel::oneshot::Sender<()>, + listener: Option>, +} + +impl Tracker +where + Hasher: std::hash::Hasher + Default, + Event: 'static + Send + Clone, +{ + pub fn new() -> Self { + Self { + subscriptions: HashMap::new(), + _hasher: PhantomData, + } + } + + pub fn update( + &mut self, + subscription: Subscription, + sink: S, + ) -> Vec> + where + Message: 'static + Send, + S: 'static + + Sink + + Unpin + + Send + + Clone, + { + use futures::{future::FutureExt, stream::StreamExt}; + + let mut futures = Vec::new(); + + let recipes = subscription.recipes(); + let mut alive = std::collections::HashSet::new(); + + for recipe in recipes { + let id = { + let mut hasher = Hasher::default(); + recipe.hash(&mut hasher); + + hasher.finish() + }; + + let _ = alive.insert(id); + + if self.subscriptions.contains_key(&id) { + continue; + } + + 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 future = futures::future::select( + cancelled, + stream.map(Ok).forward(sink.clone()), + ) + .map(|_| ()); + + let _ = self.subscriptions.insert( + id, + Execution { + _cancel: cancel, + listener: if event_sender.is_closed() { + None + } else { + Some(event_sender) + }, + }, + ); + + futures.push(future.boxed()); + } + + self.subscriptions.retain(|id, _| alive.contains(&id)); + + futures + } + + pub fn broadcast(&mut self, event: Event) { + self.subscriptions + .values_mut() + .filter_map(|connection| connection.listener.as_mut()) + .for_each(|listener| { + if let Err(error) = listener.try_send(event.clone()) { + log::error!( + "Error sending event to subscription: {:?}", + error + ); + } + }); + } +} diff --git a/examples/stopwatch.rs b/examples/stopwatch.rs index c9a61ee9..2bc85c4d 100644 --- a/examples/stopwatch.rs +++ b/examples/stopwatch.rs @@ -165,7 +165,7 @@ mod time { fn stream( self: Box, - _input: I, + _input: futures::stream::BoxStream<'static, I>, ) -> futures::stream::BoxStream<'static, Self::Output> { use futures::stream::StreamExt; diff --git a/native/src/subscription.rs b/native/src/subscription.rs index db88867a..cd0822c1 100644 --- a/native/src/subscription.rs +++ b/native/src/subscription.rs @@ -15,7 +15,7 @@ use futures::stream::BoxStream; /// /// [`Command`]: ../struct.Command.html /// [`Subscription`]: struct.Subscription.html -pub type Subscription = iced_core::Subscription; +pub type Subscription = iced_core::Subscription; /// A stream of runtime events. /// @@ -24,6 +24,11 @@ pub type Subscription = iced_core::Subscription; /// [`Subscription`]: type.Subscription.html pub type EventStream = BoxStream<'static, Event>; +/// A native [`Subscription`] tracker. +/// +/// [`Subscription`]: type.Subscription.html +pub type Tracker = iced_core::subscription::Tracker; + pub use iced_core::subscription::Recipe; mod events; diff --git a/native/src/subscription/events.rs b/native/src/subscription/events.rs index b7301828..6ff2c0fb 100644 --- a/native/src/subscription/events.rs +++ b/native/src/subscription/events.rs @@ -5,7 +5,7 @@ use crate::{ pub struct Events; -impl Recipe for Events { +impl Recipe for Events { type Output = Event; fn hash(&self, state: &mut Hasher) { -- cgit From d50ff9b5d97d9c3d6c6c70a9b4efe764b6126c86 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 19 Jan 2020 09:06:48 +0100 Subject: Implement `Runtime` and `Executor` in `iced_core` They can be leveraged by shells to easily execute commands and track subscriptions. --- core/Cargo.toml | 2 ++ core/src/lib.rs | 6 ++++ core/src/runtime.rs | 74 ++++++++++++++++++++++++++++++++++++++++ core/src/runtime/executor.rs | 11 ++++++ core/src/subscription/tracker.rs | 8 ++--- 5 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 core/src/runtime.rs create mode 100644 core/src/runtime/executor.rs diff --git a/core/Cargo.toml b/core/Cargo.toml index 4e019ba9..5e1a5532 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -12,6 +12,8 @@ repository = "https://github.com/hecrj/iced" command = ["futures"] # Exposes a future-based `Subscription` type subscription = ["futures", "log"] +# Exposes a `runtime` module meant to abstract over different future executors +runtime = ["command", "subscription"] [dependencies] futures = { version = "0.3", optional = true } diff --git a/core/src/lib.rs b/core/src/lib.rs index 6f13c310..760acefe 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -44,3 +44,9 @@ pub mod subscription; #[cfg(feature = "subscription")] pub use subscription::Subscription; + +#[cfg(feature = "runtime")] +mod runtime; + +#[cfg(feature = "runtime")] +pub use runtime::Runtime; diff --git a/core/src/runtime.rs b/core/src/runtime.rs new file mode 100644 index 00000000..31234d11 --- /dev/null +++ b/core/src/runtime.rs @@ -0,0 +1,74 @@ +mod executor; + +pub use executor::Executor; + +use crate::{subscription, Command, Subscription}; + +use futures::Sink; +use std::marker::PhantomData; + +#[derive(Debug)] +pub struct Runtime { + executor: Executor, + subscriptions: subscription::Tracker, + receiver: Receiver, + _message: PhantomData, +} + +impl + Runtime +where + Hasher: std::hash::Hasher + Default, + Event: Send + Clone + 'static, + Executor: self::Executor, + Receiver: Sink + + Unpin + + Send + + Clone + + 'static, + Message: Send + 'static, +{ + pub fn new(receiver: Receiver) -> Self { + Self { + executor: Executor::new(), + subscriptions: subscription::Tracker::new(), + receiver, + _message: PhantomData, + } + } + + pub fn spawn(&mut self, command: Command) { + use futures::{FutureExt, SinkExt}; + + let futures = command.futures(); + + for future in futures { + let mut receiver = self.receiver.clone(); + + self.executor.spawn(future.then(|message| { + async move { + let _ = receiver.send(message).await; + + () + } + })); + } + } + + pub fn track( + &mut self, + subscription: Subscription, + ) { + let futures = self + .subscriptions + .update(subscription, self.receiver.clone()); + + for future in futures { + self.executor.spawn(future); + } + } + + pub fn broadcast(&mut self, event: Event) { + self.subscriptions.broadcast(event); + } +} diff --git a/core/src/runtime/executor.rs b/core/src/runtime/executor.rs new file mode 100644 index 00000000..d171c6d5 --- /dev/null +++ b/core/src/runtime/executor.rs @@ -0,0 +1,11 @@ +use futures::Future; + +pub trait Executor { + fn new() -> Self; + + fn spawn(&self, future: impl Future + Send + 'static); + + fn enter(&self, f: impl FnOnce() -> R) -> R { + f() + } +} diff --git a/core/src/subscription/tracker.rs b/core/src/subscription/tracker.rs index 826f60c0..a942b619 100644 --- a/core/src/subscription/tracker.rs +++ b/core/src/subscription/tracker.rs @@ -28,14 +28,14 @@ where } } - pub fn update( + pub fn update( &mut self, subscription: Subscription, - sink: S, + receiver: Receiver, ) -> Vec> where Message: 'static + Send, - S: 'static + Receiver: 'static + Sink + Unpin + Send @@ -72,7 +72,7 @@ where let future = futures::future::select( cancelled, - stream.map(Ok).forward(sink.clone()), + stream.map(Ok).forward(receiver.clone()), ) .map(|_| ()); -- cgit From b5b17ed4d800c03beb3ad535d1069a7784e8dc1d Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 19 Jan 2020 10:17:08 +0100 Subject: Create `iced_futures` and wire everything up --- Cargo.toml | 1 + core/Cargo.toml | 10 -- core/src/command.rs | 100 ------------------- core/src/lib.rs | 18 ---- core/src/runtime.rs | 74 -------------- core/src/runtime/executor.rs | 11 --- core/src/subscription.rs | 188 ------------------------------------ core/src/subscription/tracker.rs | 112 --------------------- futures/Cargo.toml | 23 +++++ futures/src/command.rs | 100 +++++++++++++++++++ futures/src/lib.rs | 8 ++ futures/src/runtime.rs | 79 +++++++++++++++ futures/src/runtime/executor.rs | 26 +++++ futures/src/subscription.rs | 188 ++++++++++++++++++++++++++++++++++++ futures/src/subscription/tracker.rs | 112 +++++++++++++++++++++ native/Cargo.toml | 9 +- native/src/lib.rs | 7 +- native/src/runtime.rs | 14 +++ native/src/subscription.rs | 6 +- web/Cargo.toml | 9 +- web/src/lib.rs | 6 +- web/src/subscription.rs | 4 +- winit/Cargo.toml | 12 ++- winit/src/application.rs | 57 ++++------- winit/src/lib.rs | 3 +- winit/src/proxy.rs | 57 +++++++++++ winit/src/subscription.rs | 97 ------------------- 27 files changed, 666 insertions(+), 665 deletions(-) delete mode 100644 core/src/command.rs delete mode 100644 core/src/runtime.rs delete mode 100644 core/src/runtime/executor.rs delete mode 100644 core/src/subscription.rs delete mode 100644 core/src/subscription/tracker.rs create mode 100644 futures/Cargo.toml create mode 100644 futures/src/command.rs create mode 100644 futures/src/lib.rs create mode 100644 futures/src/runtime.rs create mode 100644 futures/src/runtime/executor.rs create mode 100644 futures/src/subscription.rs create mode 100644 futures/src/subscription/tracker.rs create mode 100644 native/src/runtime.rs create mode 100644 winit/src/proxy.rs delete mode 100644 winit/src/subscription.rs diff --git a/Cargo.toml b/Cargo.toml index aeb8382e..fbe3b9f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ maintenance = { status = "actively-developed" } [workspace] members = [ "core", + "futures", "native", "style", "web", diff --git a/core/Cargo.toml b/core/Cargo.toml index 5e1a5532..22bc7ceb 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -7,14 +7,4 @@ description = "The essential concepts of Iced" license = "MIT" repository = "https://github.com/hecrj/iced" -[features] -# Exposes a future-based `Command` type -command = ["futures"] -# Exposes a future-based `Subscription` type -subscription = ["futures", "log"] -# Exposes a `runtime` module meant to abstract over different future executors -runtime = ["command", "subscription"] - [dependencies] -futures = { version = "0.3", optional = true } -log = { version = "0.4", optional = true } diff --git a/core/src/command.rs b/core/src/command.rs deleted file mode 100644 index e7885fb8..00000000 --- a/core/src/command.rs +++ /dev/null @@ -1,100 +0,0 @@ -use futures::future::{BoxFuture, Future, FutureExt}; - -/// A collection of async operations. -/// -/// You should be able to turn a future easily into a [`Command`], either by -/// using the `From` trait or [`Command::perform`]. -/// -/// [`Command`]: struct.Command.html -pub struct Command { - futures: Vec>, -} - -impl Command { - /// Creates an empty [`Command`]. - /// - /// In other words, a [`Command`] that does nothing. - /// - /// [`Command`]: struct.Command.html - pub fn none() -> Self { - Self { - futures: Vec::new(), - } - } - - /// Creates a [`Command`] that performs the action of the given future. - /// - /// [`Command`]: struct.Command.html - pub fn perform( - future: impl Future + 'static + Send, - f: impl Fn(T) -> A + 'static + Send, - ) -> Command { - Command { - futures: vec![future.map(f).boxed()], - } - } - - /// Applies a transformation to the result of a [`Command`]. - /// - /// [`Command`]: struct.Command.html - pub fn map( - mut self, - f: impl Fn(T) -> A + 'static + Send + Sync, - ) -> Command - where - T: 'static, - { - let f = std::sync::Arc::new(f); - - Command { - futures: self - .futures - .drain(..) - .map(|future| { - let f = f.clone(); - - future.map(move |result| f(result)).boxed() - }) - .collect(), - } - } - - /// Creates a [`Command`] that performs the actions of all the given - /// commands. - /// - /// Once this command is run, all the commands will be exectued at once. - /// - /// [`Command`]: struct.Command.html - pub fn batch(commands: impl IntoIterator>) -> Self { - Self { - futures: commands - .into_iter() - .flat_map(|command| command.futures) - .collect(), - } - } - - /// Converts a [`Command`] into its underlying list of futures. - /// - /// [`Command`]: struct.Command.html - pub fn futures(self) -> Vec> { - self.futures - } -} - -impl From for Command -where - A: Future + 'static + Send, -{ - fn from(future: A) -> Self { - Self { - futures: vec![future.boxed()], - } - } -} - -impl std::fmt::Debug for Command { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Command").finish() - } -} diff --git a/core/src/lib.rs b/core/src/lib.rs index 760acefe..bec307ad 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -32,21 +32,3 @@ pub use length::Length; pub use point::Point; pub use rectangle::Rectangle; pub use vector::Vector; - -#[cfg(feature = "command")] -mod command; - -#[cfg(feature = "command")] -pub use command::Command; - -#[cfg(feature = "subscription")] -pub mod subscription; - -#[cfg(feature = "subscription")] -pub use subscription::Subscription; - -#[cfg(feature = "runtime")] -mod runtime; - -#[cfg(feature = "runtime")] -pub use runtime::Runtime; diff --git a/core/src/runtime.rs b/core/src/runtime.rs deleted file mode 100644 index 31234d11..00000000 --- a/core/src/runtime.rs +++ /dev/null @@ -1,74 +0,0 @@ -mod executor; - -pub use executor::Executor; - -use crate::{subscription, Command, Subscription}; - -use futures::Sink; -use std::marker::PhantomData; - -#[derive(Debug)] -pub struct Runtime { - executor: Executor, - subscriptions: subscription::Tracker, - receiver: Receiver, - _message: PhantomData, -} - -impl - Runtime -where - Hasher: std::hash::Hasher + Default, - Event: Send + Clone + 'static, - Executor: self::Executor, - Receiver: Sink - + Unpin - + Send - + Clone - + 'static, - Message: Send + 'static, -{ - pub fn new(receiver: Receiver) -> Self { - Self { - executor: Executor::new(), - subscriptions: subscription::Tracker::new(), - receiver, - _message: PhantomData, - } - } - - pub fn spawn(&mut self, command: Command) { - use futures::{FutureExt, SinkExt}; - - let futures = command.futures(); - - for future in futures { - let mut receiver = self.receiver.clone(); - - self.executor.spawn(future.then(|message| { - async move { - let _ = receiver.send(message).await; - - () - } - })); - } - } - - pub fn track( - &mut self, - subscription: Subscription, - ) { - let futures = self - .subscriptions - .update(subscription, self.receiver.clone()); - - for future in futures { - self.executor.spawn(future); - } - } - - pub fn broadcast(&mut self, event: Event) { - self.subscriptions.broadcast(event); - } -} diff --git a/core/src/runtime/executor.rs b/core/src/runtime/executor.rs deleted file mode 100644 index d171c6d5..00000000 --- a/core/src/runtime/executor.rs +++ /dev/null @@ -1,11 +0,0 @@ -use futures::Future; - -pub trait Executor { - fn new() -> Self; - - fn spawn(&self, future: impl Future + Send + 'static); - - fn enter(&self, f: impl FnOnce() -> R) -> R { - f() - } -} diff --git a/core/src/subscription.rs b/core/src/subscription.rs deleted file mode 100644 index 87e51e48..00000000 --- a/core/src/subscription.rs +++ /dev/null @@ -1,188 +0,0 @@ -//! Listen to external events in your application. -mod tracker; - -pub use tracker::Tracker; - -use futures::stream::BoxStream; - -/// 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>>, -} - -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 { - 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 IntoIterator>, - ) -> Self { - Self { - recipes: subscriptions - .into_iter() - .flat_map(|subscription| subscription.recipes) - .collect(), - } - } - - /// 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, - ) -> 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 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: BoxStream<'static, Input>, - ) -> BoxStream<'static, Self::Output>; -} - -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: Box, - input: BoxStream<'static, I>, - ) -> futures::stream::BoxStream<'static, Self::Output> { - use futures::StreamExt; - - let mapper = self.mapper; - - self.recipe - .stream(input) - .map(move |element| mapper(element)) - .boxed() - } -} diff --git a/core/src/subscription/tracker.rs b/core/src/subscription/tracker.rs deleted file mode 100644 index a942b619..00000000 --- a/core/src/subscription/tracker.rs +++ /dev/null @@ -1,112 +0,0 @@ -use crate::Subscription; - -use futures::{future::BoxFuture, sink::Sink}; -use std::collections::HashMap; -use std::marker::PhantomData; - -#[derive(Debug)] -pub struct Tracker { - subscriptions: HashMap>, - _hasher: PhantomData, -} - -#[derive(Debug)] -pub struct Execution { - _cancel: futures::channel::oneshot::Sender<()>, - listener: Option>, -} - -impl Tracker -where - Hasher: std::hash::Hasher + Default, - Event: 'static + Send + Clone, -{ - pub fn new() -> Self { - Self { - subscriptions: HashMap::new(), - _hasher: PhantomData, - } - } - - pub fn update( - &mut self, - subscription: Subscription, - receiver: Receiver, - ) -> Vec> - where - Message: 'static + Send, - Receiver: 'static - + Sink - + Unpin - + Send - + Clone, - { - use futures::{future::FutureExt, stream::StreamExt}; - - let mut futures = Vec::new(); - - let recipes = subscription.recipes(); - let mut alive = std::collections::HashSet::new(); - - for recipe in recipes { - let id = { - let mut hasher = Hasher::default(); - recipe.hash(&mut hasher); - - hasher.finish() - }; - - let _ = alive.insert(id); - - if self.subscriptions.contains_key(&id) { - continue; - } - - 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 future = futures::future::select( - cancelled, - stream.map(Ok).forward(receiver.clone()), - ) - .map(|_| ()); - - let _ = self.subscriptions.insert( - id, - Execution { - _cancel: cancel, - listener: if event_sender.is_closed() { - None - } else { - Some(event_sender) - }, - }, - ); - - futures.push(future.boxed()); - } - - self.subscriptions.retain(|id, _| alive.contains(&id)); - - futures - } - - pub fn broadcast(&mut self, event: Event) { - self.subscriptions - .values_mut() - .filter_map(|connection| connection.listener.as_mut()) - .for_each(|listener| { - if let Err(error) = listener.try_send(event.clone()) { - log::error!( - "Error sending event to subscription: {:?}", - error - ); - } - }); - } -} diff --git a/futures/Cargo.toml b/futures/Cargo.toml new file mode 100644 index 00000000..fe0d378c --- /dev/null +++ b/futures/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "iced_futures" +version = "0.1.0-alpha" +authors = ["Héctor Ramón Jiménez "] +edition = "2018" +description = "Commands, subscriptions, and runtimes for Iced" +license = "MIT" +repository = "https://github.com/hecrj/iced" +documentation = "https://docs.rs/iced_futures" +keywords = ["gui", "ui", "graphics", "interface", "futures"] +categories = ["gui"] + +[dependencies] +log = "0.4" + +[dependencies.futures] +version = "0.3" +features = ["thread-pool"] + +[dependencies.tokio] +version = "0.2" +optional = true +features = ["rt-core"] diff --git a/futures/src/command.rs b/futures/src/command.rs new file mode 100644 index 00000000..e7885fb8 --- /dev/null +++ b/futures/src/command.rs @@ -0,0 +1,100 @@ +use futures::future::{BoxFuture, Future, FutureExt}; + +/// A collection of async operations. +/// +/// You should be able to turn a future easily into a [`Command`], either by +/// using the `From` trait or [`Command::perform`]. +/// +/// [`Command`]: struct.Command.html +pub struct Command { + futures: Vec>, +} + +impl Command { + /// Creates an empty [`Command`]. + /// + /// In other words, a [`Command`] that does nothing. + /// + /// [`Command`]: struct.Command.html + pub fn none() -> Self { + Self { + futures: Vec::new(), + } + } + + /// Creates a [`Command`] that performs the action of the given future. + /// + /// [`Command`]: struct.Command.html + pub fn perform( + future: impl Future + 'static + Send, + f: impl Fn(T) -> A + 'static + Send, + ) -> Command { + Command { + futures: vec![future.map(f).boxed()], + } + } + + /// Applies a transformation to the result of a [`Command`]. + /// + /// [`Command`]: struct.Command.html + pub fn map( + mut self, + f: impl Fn(T) -> A + 'static + Send + Sync, + ) -> Command + where + T: 'static, + { + let f = std::sync::Arc::new(f); + + Command { + futures: self + .futures + .drain(..) + .map(|future| { + let f = f.clone(); + + future.map(move |result| f(result)).boxed() + }) + .collect(), + } + } + + /// Creates a [`Command`] that performs the actions of all the given + /// commands. + /// + /// Once this command is run, all the commands will be exectued at once. + /// + /// [`Command`]: struct.Command.html + pub fn batch(commands: impl IntoIterator>) -> Self { + Self { + futures: commands + .into_iter() + .flat_map(|command| command.futures) + .collect(), + } + } + + /// Converts a [`Command`] into its underlying list of futures. + /// + /// [`Command`]: struct.Command.html + pub fn futures(self) -> Vec> { + self.futures + } +} + +impl From for Command +where + A: Future + 'static + Send, +{ + fn from(future: A) -> Self { + Self { + futures: vec![future.boxed()], + } + } +} + +impl std::fmt::Debug for Command { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Command").finish() + } +} diff --git a/futures/src/lib.rs b/futures/src/lib.rs new file mode 100644 index 00000000..f6bcf85a --- /dev/null +++ b/futures/src/lib.rs @@ -0,0 +1,8 @@ +mod command; + +pub mod runtime; +pub mod subscription; + +pub use command::Command; +pub use runtime::Runtime; +pub use subscription::Subscription; diff --git a/futures/src/runtime.rs b/futures/src/runtime.rs new file mode 100644 index 00000000..bc1ad8ac --- /dev/null +++ b/futures/src/runtime.rs @@ -0,0 +1,79 @@ +//! Run commands and subscriptions. +mod executor; + +pub use executor::Executor; + +use crate::{subscription, Command, Subscription}; + +use futures::Sink; +use std::marker::PhantomData; + +#[derive(Debug)] +pub struct Runtime { + executor: Executor, + receiver: Receiver, + subscriptions: subscription::Tracker, + _message: PhantomData, +} + +impl + Runtime +where + Hasher: std::hash::Hasher + Default, + Event: Send + Clone + 'static, + Executor: self::Executor, + Receiver: Sink + + Unpin + + Send + + Clone + + 'static, + Message: Send + 'static, +{ + pub fn new(executor: Executor, receiver: Receiver) -> Self { + Self { + executor, + receiver, + subscriptions: subscription::Tracker::new(), + _message: PhantomData, + } + } + + pub fn enter(&self, f: impl FnOnce() -> R) -> R { + self.executor.enter(f) + } + + pub fn spawn(&mut self, command: Command) { + use futures::{FutureExt, SinkExt}; + + let futures = command.futures(); + + for future in futures { + let mut receiver = self.receiver.clone(); + + self.executor.spawn(future.then(|message| { + async move { + let _ = receiver.send(message).await; + + () + } + })); + } + } + + pub fn track( + &mut self, + subscription: Subscription, + ) { + let futures = self + .subscriptions + .update(subscription, self.receiver.clone()); + + for future in futures { + self.executor.spawn(future); + } + } + + pub fn broadcast(&mut self, event: Event) { + self.subscriptions.broadcast(event); + } +} diff --git a/futures/src/runtime/executor.rs b/futures/src/runtime/executor.rs new file mode 100644 index 00000000..855aa105 --- /dev/null +++ b/futures/src/runtime/executor.rs @@ -0,0 +1,26 @@ +use futures::Future; + +pub trait Executor { + fn spawn(&self, future: impl Future + Send + 'static); + + fn enter(&self, f: impl FnOnce() -> R) -> R { + f() + } +} + +impl Executor for futures::executor::ThreadPool { + fn spawn(&self, future: impl Future + Send + 'static) { + self.spawn_ok(future); + } +} + +#[cfg(feature = "tokio")] +impl Executor for tokio::runtime::Runtime { + fn spawn(&self, future: impl Future + Send + 'static) { + let _ = tokio::runtime::Runtime::spawn(self, future); + } + + fn enter(&self, f: impl FnOnce() -> R) -> R { + tokio::runtime::Runtime::enter(self, f) + } +} diff --git a/futures/src/subscription.rs b/futures/src/subscription.rs new file mode 100644 index 00000000..87e51e48 --- /dev/null +++ b/futures/src/subscription.rs @@ -0,0 +1,188 @@ +//! Listen to external events in your application. +mod tracker; + +pub use tracker::Tracker; + +use futures::stream::BoxStream; + +/// 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>>, +} + +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 { + 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 IntoIterator>, + ) -> Self { + Self { + recipes: subscriptions + .into_iter() + .flat_map(|subscription| subscription.recipes) + .collect(), + } + } + + /// 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, + ) -> 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 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: BoxStream<'static, Input>, + ) -> BoxStream<'static, Self::Output>; +} + +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: Box, + input: BoxStream<'static, I>, + ) -> futures::stream::BoxStream<'static, Self::Output> { + use futures::StreamExt; + + let mapper = self.mapper; + + self.recipe + .stream(input) + .map(move |element| mapper(element)) + .boxed() + } +} diff --git a/futures/src/subscription/tracker.rs b/futures/src/subscription/tracker.rs new file mode 100644 index 00000000..a942b619 --- /dev/null +++ b/futures/src/subscription/tracker.rs @@ -0,0 +1,112 @@ +use crate::Subscription; + +use futures::{future::BoxFuture, sink::Sink}; +use std::collections::HashMap; +use std::marker::PhantomData; + +#[derive(Debug)] +pub struct Tracker { + subscriptions: HashMap>, + _hasher: PhantomData, +} + +#[derive(Debug)] +pub struct Execution { + _cancel: futures::channel::oneshot::Sender<()>, + listener: Option>, +} + +impl Tracker +where + Hasher: std::hash::Hasher + Default, + Event: 'static + Send + Clone, +{ + pub fn new() -> Self { + Self { + subscriptions: HashMap::new(), + _hasher: PhantomData, + } + } + + pub fn update( + &mut self, + subscription: Subscription, + receiver: Receiver, + ) -> Vec> + where + Message: 'static + Send, + Receiver: 'static + + Sink + + Unpin + + Send + + Clone, + { + use futures::{future::FutureExt, stream::StreamExt}; + + let mut futures = Vec::new(); + + let recipes = subscription.recipes(); + let mut alive = std::collections::HashSet::new(); + + for recipe in recipes { + let id = { + let mut hasher = Hasher::default(); + recipe.hash(&mut hasher); + + hasher.finish() + }; + + let _ = alive.insert(id); + + if self.subscriptions.contains_key(&id) { + continue; + } + + 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 future = futures::future::select( + cancelled, + stream.map(Ok).forward(receiver.clone()), + ) + .map(|_| ()); + + let _ = self.subscriptions.insert( + id, + Execution { + _cancel: cancel, + listener: if event_sender.is_closed() { + None + } else { + Some(event_sender) + }, + }, + ); + + futures.push(future.boxed()); + } + + self.subscriptions.retain(|id, _| alive.contains(&id)); + + futures + } + + pub fn broadcast(&mut self, event: Event) { + self.subscriptions + .values_mut() + .filter_map(|connection| connection.listener.as_mut()) + .for_each(|listener| { + if let Err(error) = listener.try_send(event.clone()) { + log::error!( + "Error sending event to subscription: {:?}", + error + ); + } + }); + } +} diff --git a/native/Cargo.toml b/native/Cargo.toml index a31b6627..57a869e2 100644 --- a/native/Cargo.toml +++ b/native/Cargo.toml @@ -8,8 +8,15 @@ license = "MIT" repository = "https://github.com/hecrj/iced" [dependencies] -iced_core = { version = "0.1.0", path = "../core", features = ["command", "subscription"] } twox-hash = "1.5" raw-window-handle = "0.3" unicode-segmentation = "1.6" futures = "0.3" + +[dependencies.iced_core] +version = "0.1.0" +path = "../core" + +[dependencies.iced_futures] +version = "0.1.0-alpha" +path = "../futures" diff --git a/native/src/lib.rs b/native/src/lib.rs index 340b9ea7..7730c6a3 100644 --- a/native/src/lib.rs +++ b/native/src/lib.rs @@ -42,6 +42,7 @@ pub mod input; pub mod layout; pub mod renderer; +pub mod runtime; pub mod subscription; pub mod widget; pub mod window; @@ -55,9 +56,10 @@ mod size; mod user_interface; pub use iced_core::{ - Align, Background, Color, Command, Font, HorizontalAlignment, Length, - Point, Rectangle, Vector, VerticalAlignment, + Align, Background, Color, Font, HorizontalAlignment, Length, Point, + Rectangle, Vector, VerticalAlignment, }; +pub use iced_futures::Command; pub use clipboard::Clipboard; pub use element::Element; @@ -66,6 +68,7 @@ pub use hasher::Hasher; pub use layout::Layout; pub use mouse_cursor::MouseCursor; pub use renderer::Renderer; +pub use runtime::Runtime; pub use size::Size; pub use subscription::Subscription; pub use user_interface::{Cache, UserInterface}; diff --git a/native/src/runtime.rs b/native/src/runtime.rs new file mode 100644 index 00000000..2b3abbf1 --- /dev/null +++ b/native/src/runtime.rs @@ -0,0 +1,14 @@ +//! Run commands and subscriptions. +use crate::{Event, Hasher}; + +/// A native runtime with a generic executor and receiver of results. +/// +/// It can be used by shells to easily spawn a [`Command`] or track a +/// [`Subscription`]. +/// +/// [`Command`]: ../struct.Command.html +/// [`Subscription`]: ../struct.Subscription.html +pub type Runtime = + iced_futures::Runtime; + +pub use iced_futures::runtime::Executor; diff --git a/native/src/subscription.rs b/native/src/subscription.rs index cd0822c1..43f1758a 100644 --- a/native/src/subscription.rs +++ b/native/src/subscription.rs @@ -15,7 +15,7 @@ use futures::stream::BoxStream; /// /// [`Command`]: ../struct.Command.html /// [`Subscription`]: struct.Subscription.html -pub type Subscription = iced_core::Subscription; +pub type Subscription = iced_futures::Subscription; /// A stream of runtime events. /// @@ -27,9 +27,9 @@ pub type EventStream = BoxStream<'static, Event>; /// A native [`Subscription`] tracker. /// /// [`Subscription`]: type.Subscription.html -pub type Tracker = iced_core::subscription::Tracker; +pub type Tracker = iced_futures::subscription::Tracker; -pub use iced_core::subscription::Recipe; +pub use iced_futures::subscription::Recipe; mod events; diff --git a/web/Cargo.toml b/web/Cargo.toml index 605c7462..ea092575 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -15,12 +15,19 @@ categories = ["web-programming"] maintenance = { status = "actively-developed" } [dependencies] -iced_core = { version = "0.1.0", path = "../core", features = ["command", "subscription"] } dodrio = "0.1.0" wasm-bindgen = "0.2.51" wasm-bindgen-futures = "0.4" futures = "0.3" +[dependencies.iced_core] +version = "0.1.0" +path = "../core" + +[dependencies.iced_futures] +version = "0.1.0-alpha" +path = "../futures" + [dependencies.web-sys] version = "0.3.27" features = [ diff --git a/web/src/lib.rs b/web/src/lib.rs index 7ea22e85..b183c390 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -72,9 +72,10 @@ pub use dodrio; pub use element::Element; pub use hasher::Hasher; pub use iced_core::{ - Align, Background, Color, Command, Font, HorizontalAlignment, Length, + Align, Background, Color, Font, HorizontalAlignment, Length, VerticalAlignment, }; +pub use iced_futures::Command; pub use style::Style; pub use subscription::Subscription; pub use widget::*; @@ -148,7 +149,6 @@ pub trait Application { } } - struct Instance { title: String, ui: Rc>>>, @@ -167,7 +167,7 @@ impl Clone for Instance { impl Instance where - Message: 'static + Message: 'static, { fn new(ui: impl Application + 'static) -> Self { Self { diff --git a/web/src/subscription.rs b/web/src/subscription.rs index 4638c8ab..6b8415c0 100644 --- a/web/src/subscription.rs +++ b/web/src/subscription.rs @@ -14,6 +14,6 @@ use crate::Hasher; /// /// [`Command`]: ../struct.Command.html /// [`Subscription`]: struct.Subscription.html -pub type Subscription = iced_core::Subscription; +pub type Subscription = iced_futures::Subscription; -pub use iced_core::subscription::Recipe; +pub use iced_futures::subscription::Recipe; diff --git a/winit/Cargo.toml b/winit/Cargo.toml index 5727f8cf..3ed37dd5 100644 --- a/winit/Cargo.toml +++ b/winit/Cargo.toml @@ -14,11 +14,17 @@ categories = ["gui"] debug = [] [dependencies] -iced_native = { version = "0.1.0-alpha", path = "../native" } winit = { version = "0.20.0-alpha3", git = "https://github.com/hecrj/winit", rev = "709808eb4e69044705fcb214bcc30556db761405"} -window_clipboard = { git = "https://github.com/hecrj/window_clipboard", rev = "22c6dd6c04cd05d528029b50a30c56417cd4bebf" } -futures = { version = "0.3", features = ["thread-pool"] } log = "0.4" +futures = { version = "0.3", features = ["thread-pool"] } + +[dependencies.iced_native] +version = "0.1.0-alpha" +path = "../native" + +[dependencies.window_clipboard] +git = "https://github.com/hecrj/window_clipboard" +rev = "22c6dd6c04cd05d528029b50a30c56417cd4bebf" [target.'cfg(target_os = "windows")'.dependencies.winapi] version = "0.3.6" diff --git a/winit/src/application.rs b/winit/src/application.rs index a14924ac..076ac092 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -1,9 +1,10 @@ use crate::{ conversion, input::{keyboard, mouse}, - subscription, window, Cache, Clipboard, Command, Debug, Element, Event, - Mode, MouseCursor, Settings, Size, Subscription, UserInterface, + window, Cache, Clipboard, Command, Debug, Element, Event, Mode, + MouseCursor, Proxy, Settings, Size, Subscription, UserInterface, }; +use iced_native::Runtime; /// An interactive, native cross-platform application. /// @@ -109,17 +110,19 @@ pub trait Application: Sized { debug.startup_started(); let event_loop = EventLoop::with_user_event(); - let proxy = event_loop.create_proxy(); - let mut thread_pool = - futures::executor::ThreadPool::new().expect("Create thread pool"); - let mut subscription_pool = subscription::Pool::new(); + let mut runtime = { + let thread_pool = futures::executor::ThreadPool::new() + .expect("Create thread pool"); + + Runtime::new(thread_pool, Proxy::new(event_loop.create_proxy())) + }; let mut external_messages = Vec::new(); let (mut application, init_command) = Self::new(); - spawn(init_command, &mut thread_pool, &proxy); + runtime.spawn(init_command); let subscription = application.subscription(); - subscription_pool.update(subscription, &mut thread_pool, &proxy); + runtime.track(subscription); let mut title = application.title(); let mut mode = application.mode(); @@ -212,7 +215,7 @@ pub trait Application: Sized { events .iter() .cloned() - .for_each(|event| subscription_pool.broadcast_event(event)); + .for_each(|event| runtime.broadcast(event)); let mut messages = user_interface.update( &renderer, @@ -241,17 +244,15 @@ pub trait Application: Sized { debug.log_message(&message); debug.update_started(); - let command = application.update(message); - spawn(command, &mut thread_pool, &proxy); + let command = + runtime.enter(|| application.update(message)); + runtime.spawn(command); debug.update_finished(); } - let subscription = application.subscription(); - subscription_pool.update( - subscription, - &mut thread_pool, - &proxy, - ); + let subscription = + runtime.enter(|| application.subscription()); + runtime.track(subscription); // Update window title let new_title = application.title(); @@ -463,28 +464,6 @@ fn to_physical(size: winit::dpi::LogicalSize, dpi: f64) -> (u16, u16) { ) } -fn spawn( - command: Command, - thread_pool: &mut futures::executor::ThreadPool, - proxy: &winit::event_loop::EventLoopProxy, -) { - use futures::FutureExt; - - let futures = command.futures(); - - for future in futures { - let proxy = proxy.clone(); - - let future = future.map(move |message| { - proxy - .send_event(message) - .expect("Send command result to event loop"); - }); - - thread_pool.spawn_ok(future); - } -} - // 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 9000f977..056ae8f0 100644 --- a/winit/src/lib.rs +++ b/winit/src/lib.rs @@ -31,7 +31,7 @@ pub mod settings; mod application; mod clipboard; mod mode; -mod subscription; +mod proxy; // We disable debug capabilities on release builds unless the `debug` feature // is explicitly enabled. @@ -48,3 +48,4 @@ pub use settings::Settings; use clipboard::Clipboard; use debug::Debug; +use proxy::Proxy; diff --git a/winit/src/proxy.rs b/winit/src/proxy.rs new file mode 100644 index 00000000..7e8dee98 --- /dev/null +++ b/winit/src/proxy.rs @@ -0,0 +1,57 @@ +use futures::{ + task::{Context, Poll}, + Sink, +}; +use std::pin::Pin; + +pub struct Proxy { + raw: winit::event_loop::EventLoopProxy, +} + +impl Clone for Proxy { + fn clone(&self) -> Self { + Self { + raw: self.raw.clone(), + } + } +} + +impl Proxy { + pub fn new(raw: winit::event_loop::EventLoopProxy) -> Self { + Self { raw } + } +} + +impl Sink for Proxy { + type Error = core::convert::Infallible; + + fn poll_ready( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } + + fn start_send( + self: Pin<&mut Self>, + message: Message, + ) -> Result<(), Self::Error> { + let _ = self.raw.send_event(message); + + Ok(()) + } + + fn poll_flush( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } + + fn poll_close( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } +} diff --git a/winit/src/subscription.rs b/winit/src/subscription.rs deleted file mode 100644 index bad68d55..00000000 --- a/winit/src/subscription.rs +++ /dev/null @@ -1,97 +0,0 @@ -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.clone()) { - log::error!( - "Error sending event to subscription: {:?}", - error - ); - } - }); - } -} -- cgit From b8b0d97525aaa2641d8493aa65e3108d70c1560a Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 19 Jan 2020 11:08:32 +0100 Subject: Rename `Receiver` to `Sender` in `Runtime` --- futures/src/runtime.rs | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/futures/src/runtime.rs b/futures/src/runtime.rs index bc1ad8ac..37905c61 100644 --- a/futures/src/runtime.rs +++ b/futures/src/runtime.rs @@ -9,30 +9,30 @@ use futures::Sink; use std::marker::PhantomData; #[derive(Debug)] -pub struct Runtime { +pub struct Runtime { executor: Executor, - receiver: Receiver, + sender: Sender, subscriptions: subscription::Tracker, _message: PhantomData, } -impl - Runtime +impl + Runtime where Hasher: std::hash::Hasher + Default, Event: Send + Clone + 'static, Executor: self::Executor, - Receiver: Sink + Sender: Sink + Unpin + Send + Clone + 'static, Message: Send + 'static, { - pub fn new(executor: Executor, receiver: Receiver) -> Self { + pub fn new(executor: Executor, sender: Sender) -> Self { Self { executor, - receiver, + sender, subscriptions: subscription::Tracker::new(), _message: PhantomData, } @@ -48,11 +48,11 @@ where let futures = command.futures(); for future in futures { - let mut receiver = self.receiver.clone(); + let mut sender = self.sender.clone(); self.executor.spawn(future.then(|message| { async move { - let _ = receiver.send(message).await; + let _ = sender.send(message).await; () } @@ -64,9 +64,8 @@ where &mut self, subscription: Subscription, ) { - let futures = self - .subscriptions - .update(subscription, self.receiver.clone()); + let futures = + self.subscriptions.update(subscription, self.sender.clone()); for future in futures { self.executor.spawn(future); -- cgit From 35760ac68f06e783e64e9048aff0fff6df1c09cf Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 19 Jan 2020 11:08:47 +0100 Subject: Make `thread-pool` optional in `iced_futures` --- futures/Cargo.toml | 4 +++- futures/src/runtime/executor.rs | 1 + winit/Cargo.toml | 7 ++++++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/futures/Cargo.toml b/futures/Cargo.toml index fe0d378c..5b303e01 100644 --- a/futures/Cargo.toml +++ b/futures/Cargo.toml @@ -10,12 +10,14 @@ documentation = "https://docs.rs/iced_futures" keywords = ["gui", "ui", "graphics", "interface", "futures"] categories = ["gui"] +[features] +thread-pool = ["futures/thread-pool"] + [dependencies] log = "0.4" [dependencies.futures] version = "0.3" -features = ["thread-pool"] [dependencies.tokio] version = "0.2" diff --git a/futures/src/runtime/executor.rs b/futures/src/runtime/executor.rs index 855aa105..eec5e231 100644 --- a/futures/src/runtime/executor.rs +++ b/futures/src/runtime/executor.rs @@ -8,6 +8,7 @@ pub trait Executor { } } +#[cfg(feature = "thread-pool")] impl Executor for futures::executor::ThreadPool { fn spawn(&self, future: impl Future + Send + 'static) { self.spawn_ok(future); diff --git a/winit/Cargo.toml b/winit/Cargo.toml index 3ed37dd5..ba6d5229 100644 --- a/winit/Cargo.toml +++ b/winit/Cargo.toml @@ -16,12 +16,17 @@ debug = [] [dependencies] winit = { version = "0.20.0-alpha3", git = "https://github.com/hecrj/winit", rev = "709808eb4e69044705fcb214bcc30556db761405"} log = "0.4" -futures = { version = "0.3", features = ["thread-pool"] } +futures = "0.3" [dependencies.iced_native] version = "0.1.0-alpha" path = "../native" +[dependencies.iced_futures] +version = "0.1.0-alpha" +path = "../futures" +features = ["thread-pool"] + [dependencies.window_clipboard] git = "https://github.com/hecrj/window_clipboard" rev = "22c6dd6c04cd05d528029b50a30c56417cd4bebf" -- cgit From 90690702e1e4abab804ec91e8ff4183824bec436 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 20 Jan 2020 04:47:36 +0100 Subject: Add `Application::Executor` associated type --- Cargo.toml | 1 + examples/events.rs | 5 +++-- examples/pokedex.rs | 1 + examples/stopwatch.rs | 1 + examples/todos.rs | 1 + futures/Cargo.toml | 4 ++++ futures/src/executor.rs | 36 ++++++++++++++++++++++++++++++++++++ futures/src/executor/async_std.rs | 15 +++++++++++++++ futures/src/executor/null.rs | 13 +++++++++++++ futures/src/executor/thread_pool.rs | 15 +++++++++++++++ futures/src/executor/tokio.rs | 19 +++++++++++++++++++ futures/src/lib.rs | 6 +++++- futures/src/runtime.rs | 8 ++------ futures/src/runtime/executor.rs | 27 --------------------------- native/Cargo.toml | 2 +- native/src/lib.rs | 7 +++++-- native/src/runtime.rs | 2 -- native/src/subscription.rs | 2 +- native/src/subscription/events.rs | 3 ++- src/application.rs | 14 ++++++++++++-- src/lib.rs | 22 +++++++++++++++------- src/native.rs | 2 ++ src/native/executor.rs | 23 +++++++++++++++++++++++ src/sandbox.rs | 3 ++- web/Cargo.toml | 1 - web/src/lib.rs | 2 +- winit/Cargo.toml | 6 ------ winit/src/application.rs | 18 +++++++++++------- winit/src/proxy.rs | 2 +- 29 files changed, 192 insertions(+), 69 deletions(-) create mode 100644 futures/src/executor.rs create mode 100644 futures/src/executor/async_std.rs create mode 100644 futures/src/executor/null.rs create mode 100644 futures/src/executor/thread_pool.rs create mode 100644 futures/src/executor/tokio.rs delete mode 100644 futures/src/runtime/executor.rs create mode 100644 src/native/executor.rs diff --git a/Cargo.toml b/Cargo.toml index fbe3b9f2..87f3000e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,7 @@ iced_web = { version = "0.1.0", path = "web" } [dev-dependencies] iced_native = { version = "0.1", path = "./native" } iced_wgpu = { version = "0.1", path = "./wgpu" } +iced_futures = { version = "0.1.0-alpha", path = "./futures", features = ["async-std"] } env_logger = "0.7" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/examples/events.rs b/examples/events.rs index 74542171..0c9dca05 100644 --- a/examples/events.rs +++ b/examples/events.rs @@ -1,6 +1,6 @@ use iced::{ - Align, Application, Checkbox, Column, Command, Container, Element, Length, - Settings, Subscription, Text, + executor, Align, Application, Checkbox, Column, Command, Container, + Element, Length, Settings, Subscription, Text, }; pub fn main() { @@ -20,6 +20,7 @@ enum Message { } impl Application for Events { + type Executor = executor::Default; type Message = Message; fn new() -> (Events, Command) { diff --git a/examples/pokedex.rs b/examples/pokedex.rs index 7326f94f..505dbf19 100644 --- a/examples/pokedex.rs +++ b/examples/pokedex.rs @@ -27,6 +27,7 @@ enum Message { } impl Application for Pokedex { + type Executor = iced_futures::executor::AsyncStd; type Message = Message; fn new() -> (Pokedex, Command) { diff --git a/examples/stopwatch.rs b/examples/stopwatch.rs index 2bc85c4d..6e357039 100644 --- a/examples/stopwatch.rs +++ b/examples/stopwatch.rs @@ -28,6 +28,7 @@ enum Message { } impl Application for Stopwatch { + type Executor = iced_futures::executor::AsyncStd; type Message = Message; fn new() -> (Stopwatch, Command) { diff --git a/examples/todos.rs b/examples/todos.rs index 4166f75a..06595a1e 100644 --- a/examples/todos.rs +++ b/examples/todos.rs @@ -38,6 +38,7 @@ enum Message { } impl Application for Todos { + type Executor = iced_futures::executor::AsyncStd; type Message = Message; fn new() -> (Todos, Command) { diff --git a/futures/Cargo.toml b/futures/Cargo.toml index 5b303e01..13c2d6b7 100644 --- a/futures/Cargo.toml +++ b/futures/Cargo.toml @@ -23,3 +23,7 @@ version = "0.3" version = "0.2" optional = true features = ["rt-core"] + +[dependencies.async-std] +version = "1.0" +optional = true diff --git a/futures/src/executor.rs b/futures/src/executor.rs new file mode 100644 index 00000000..144a41f8 --- /dev/null +++ b/futures/src/executor.rs @@ -0,0 +1,36 @@ +//! Choose your preferred executor to power a runtime. +mod null; + +#[cfg(feature = "thread-pool")] +mod thread_pool; + +#[cfg(feature = "thread-pool")] +pub use thread_pool::ThreadPool; + +#[cfg(feature = "tokio")] +mod tokio; + +#[cfg(feature = "async-std")] +mod async_std; + +pub use null::Null; + +#[cfg(feature = "tokio")] +pub use self::tokio::Tokio; + +#[cfg(feature = "async-std")] +pub use self::async_std::AsyncStd; + +use futures::Future; + +pub trait Executor: Sized { + fn new() -> Result + where + Self: Sized; + + fn spawn(&self, future: impl Future + Send + 'static); + + fn enter(&self, f: impl FnOnce() -> R) -> R { + f() + } +} diff --git a/futures/src/executor/async_std.rs b/futures/src/executor/async_std.rs new file mode 100644 index 00000000..b056b23d --- /dev/null +++ b/futures/src/executor/async_std.rs @@ -0,0 +1,15 @@ +use crate::Executor; + +use futures::Future; + +pub struct AsyncStd; + +impl Executor for AsyncStd { + fn new() -> Result { + Ok(Self) + } + + fn spawn(&self, future: impl Future + Send + 'static) { + let _ = async_std::task::spawn(future); + } +} diff --git a/futures/src/executor/null.rs b/futures/src/executor/null.rs new file mode 100644 index 00000000..722073bb --- /dev/null +++ b/futures/src/executor/null.rs @@ -0,0 +1,13 @@ +use crate::Executor; + +use futures::Future; + +pub struct Null; + +impl Executor for Null { + fn new() -> Result { + Ok(Self) + } + + fn spawn(&self, _future: impl Future + Send + 'static) {} +} diff --git a/futures/src/executor/thread_pool.rs b/futures/src/executor/thread_pool.rs new file mode 100644 index 00000000..6393d0d5 --- /dev/null +++ b/futures/src/executor/thread_pool.rs @@ -0,0 +1,15 @@ +use crate::Executor; + +use futures::Future; + +pub type ThreadPool = futures::executor::ThreadPool; + +impl Executor for futures::executor::ThreadPool { + fn new() -> Result { + futures::executor::ThreadPool::new() + } + + fn spawn(&self, future: impl Future + Send + 'static) { + self.spawn_ok(future); + } +} diff --git a/futures/src/executor/tokio.rs b/futures/src/executor/tokio.rs new file mode 100644 index 00000000..aafa7e7b --- /dev/null +++ b/futures/src/executor/tokio.rs @@ -0,0 +1,19 @@ +use crate::Executor; + +use futures::Future; + +pub type Tokio = tokio::runtime::Runtime; + +impl Executor for Tokio { + fn new() -> Result { + tokio::runtime::Runtime::new() + } + + fn spawn(&self, future: impl Future + Send + 'static) { + let _ = tokio::runtime::Runtime::spawn(self, future); + } + + fn enter(&self, f: impl FnOnce() -> R) -> R { + tokio::runtime::Runtime::enter(self, f) + } +} diff --git a/futures/src/lib.rs b/futures/src/lib.rs index f6bcf85a..832a50f6 100644 --- a/futures/src/lib.rs +++ b/futures/src/lib.rs @@ -1,8 +1,12 @@ +pub use futures; + mod command; +mod runtime; -pub mod runtime; +pub mod executor; pub mod subscription; pub use command::Command; +pub use executor::Executor; pub use runtime::Runtime; pub use subscription::Subscription; diff --git a/futures/src/runtime.rs b/futures/src/runtime.rs index 37905c61..a508c46e 100644 --- a/futures/src/runtime.rs +++ b/futures/src/runtime.rs @@ -1,9 +1,5 @@ -//! Run commands and subscriptions. -mod executor; - -pub use executor::Executor; - -use crate::{subscription, Command, Subscription}; +//! Run commands and keep track of subscriptions. +use crate::{subscription, Command, Executor, Subscription}; use futures::Sink; use std::marker::PhantomData; diff --git a/futures/src/runtime/executor.rs b/futures/src/runtime/executor.rs deleted file mode 100644 index eec5e231..00000000 --- a/futures/src/runtime/executor.rs +++ /dev/null @@ -1,27 +0,0 @@ -use futures::Future; - -pub trait Executor { - fn spawn(&self, future: impl Future + Send + 'static); - - fn enter(&self, f: impl FnOnce() -> R) -> R { - f() - } -} - -#[cfg(feature = "thread-pool")] -impl Executor for futures::executor::ThreadPool { - fn spawn(&self, future: impl Future + Send + 'static) { - self.spawn_ok(future); - } -} - -#[cfg(feature = "tokio")] -impl Executor for tokio::runtime::Runtime { - fn spawn(&self, future: impl Future + Send + 'static) { - let _ = tokio::runtime::Runtime::spawn(self, future); - } - - fn enter(&self, f: impl FnOnce() -> R) -> R { - tokio::runtime::Runtime::enter(self, f) - } -} diff --git a/native/Cargo.toml b/native/Cargo.toml index 57a869e2..6276535e 100644 --- a/native/Cargo.toml +++ b/native/Cargo.toml @@ -11,7 +11,6 @@ repository = "https://github.com/hecrj/iced" twox-hash = "1.5" raw-window-handle = "0.3" unicode-segmentation = "1.6" -futures = "0.3" [dependencies.iced_core] version = "0.1.0" @@ -20,3 +19,4 @@ path = "../core" [dependencies.iced_futures] version = "0.1.0-alpha" path = "../futures" +features = ["thread-pool"] diff --git a/native/src/lib.rs b/native/src/lib.rs index 7730c6a3..b5856c00 100644 --- a/native/src/lib.rs +++ b/native/src/lib.rs @@ -42,7 +42,6 @@ pub mod input; pub mod layout; pub mod renderer; -pub mod runtime; pub mod subscription; pub mod widget; pub mod window; @@ -52,6 +51,7 @@ mod element; mod event; mod hasher; mod mouse_cursor; +mod runtime; mod size; mod user_interface; @@ -59,7 +59,10 @@ pub use iced_core::{ Align, Background, Color, Font, HorizontalAlignment, Length, Point, Rectangle, Vector, VerticalAlignment, }; -pub use iced_futures::Command; +pub use iced_futures::{executor, futures, Command}; + +#[doc(no_inline)] +pub use executor::Executor; pub use clipboard::Clipboard; pub use element::Element; diff --git a/native/src/runtime.rs b/native/src/runtime.rs index 2b3abbf1..9fa031f4 100644 --- a/native/src/runtime.rs +++ b/native/src/runtime.rs @@ -10,5 +10,3 @@ use crate::{Event, Hasher}; /// [`Subscription`]: ../struct.Subscription.html pub type Runtime = iced_futures::Runtime; - -pub use iced_futures::runtime::Executor; diff --git a/native/src/subscription.rs b/native/src/subscription.rs index 43f1758a..0d002c6c 100644 --- a/native/src/subscription.rs +++ b/native/src/subscription.rs @@ -1,6 +1,6 @@ //! Listen to external events in your application. use crate::{Event, Hasher}; -use futures::stream::BoxStream; +use iced_futures::futures::stream::BoxStream; /// A request to listen to external events. /// diff --git a/native/src/subscription/events.rs b/native/src/subscription/events.rs index 6ff2c0fb..7d33166e 100644 --- a/native/src/subscription/events.rs +++ b/native/src/subscription/events.rs @@ -2,6 +2,7 @@ use crate::{ subscription::{EventStream, Recipe}, Event, Hasher, }; +use iced_futures::futures::stream::BoxStream; pub struct Events; @@ -17,7 +18,7 @@ impl Recipe for Events { fn stream( self: Box, event_stream: EventStream, - ) -> futures::stream::BoxStream<'static, Self::Output> { + ) -> BoxStream<'static, Self::Output> { event_stream } } diff --git a/src/application.rs b/src/application.rs index b940cc17..3a526f1b 100644 --- a/src/application.rs +++ b/src/application.rs @@ -1,4 +1,4 @@ -use crate::{window, Command, Element, Settings, Subscription}; +use crate::{window, Command, Element, Executor, Settings, Subscription}; /// An interactive cross-platform application. /// @@ -19,7 +19,7 @@ use crate::{window, Command, Element, Settings, Subscription}; /// before](index.html#overview). We just need to fill in the gaps: /// /// ```no_run -/// use iced::{button, Application, Button, Column, Command, Element, Settings, Text}; +/// use iced::{button, executor, Application, Button, Column, Command, Element, Settings, Text}; /// /// pub fn main() { /// Counter::run(Settings::default()) @@ -39,6 +39,7 @@ use crate::{window, Command, Element, Settings, Subscription}; /// } /// /// impl Application for Counter { +/// type Executor = executor::Null; /// type Message = Message; /// /// fn new() -> (Self, Command) { @@ -80,6 +81,14 @@ use crate::{window, Command, Element, Settings, Subscription}; /// } /// ``` pub trait Application: Sized { + /// The [`Executor`] that will run commands and subscriptions. + /// + /// The [`executor::Default`] can be a good starting point! + /// + /// [`Executor`]: trait.Executor.html + /// [`executor::Default`]: executor/struct.Default.html + type Executor: Executor; + /// The type of __messages__ your [`Application`] will produce. /// /// [`Application`]: trait.Application.html @@ -185,6 +194,7 @@ where A: Application, { type Renderer = iced_wgpu::Renderer; + type Executor = A::Executor; type Message = A::Message; fn new() -> (Self, Command) { diff --git a/src/lib.rs b/src/lib.rs index 759dea2f..18dfc098 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -180,18 +180,26 @@ #![deny(unsafe_code)] #![deny(rust_2018_idioms)] mod application; -#[cfg(target_arch = "wasm32")] -#[path = "web.rs"] -mod platform; -#[cfg(not(target_arch = "wasm32"))] -#[path = "native.rs"] -mod platform; mod sandbox; +#[cfg(not(target_arch = "wasm32"))] +mod native; + +#[cfg(not(target_arch = "wasm32"))] +pub use native::*; + +#[cfg(target_arch = "wasm32")] +mod web; + +#[cfg(target_arch = "wasm32")] +pub use web::*; + pub mod settings; pub mod window; +#[doc(no_inline)] +pub use executor::Executor; + pub use application::Application; -pub use platform::*; pub use sandbox::Sandbox; pub use settings::Settings; diff --git a/src/native.rs b/src/native.rs index 35441a3e..86ccffab 100644 --- a/src/native.rs +++ b/src/native.rs @@ -3,6 +3,8 @@ pub use iced_winit::{ Space, Subscription, Vector, VerticalAlignment, }; +pub mod executor; + pub mod widget { //! Display information and interactive controls in your application. //! diff --git a/src/native/executor.rs b/src/native/executor.rs new file mode 100644 index 00000000..68a1d280 --- /dev/null +++ b/src/native/executor.rs @@ -0,0 +1,23 @@ +//! Choose your preferred executor to power your application. +pub use iced_winit::{executor::Null, Executor}; +use iced_winit::{executor::ThreadPool, futures}; + +/// The default cross-platform executor. +/// +/// - On native platforms, it will use a `ThreadPool`. +/// - On the Web, it will use `wasm-bindgen-futures::spawn_local`. +#[derive(Debug)] +pub struct Default(ThreadPool); + +impl Executor for Default { + fn new() -> Result { + Ok(Default(ThreadPool::new()?)) + } + + fn spawn( + &self, + future: impl futures::Future + Send + 'static, + ) { + self.0.spawn(future); + } +} diff --git a/src/sandbox.rs b/src/sandbox.rs index dda4c3f5..2c0332ff 100644 --- a/src/sandbox.rs +++ b/src/sandbox.rs @@ -1,4 +1,4 @@ -use crate::{Application, Command, Element, Settings, Subscription}; +use crate::{executor, Application, Command, Element, Settings, Subscription}; /// A sandboxed [`Application`]. /// @@ -133,6 +133,7 @@ impl Application for T where T: Sandbox, { + type Executor = executor::Null; type Message = T::Message; fn new() -> (Self, Command) { diff --git a/web/Cargo.toml b/web/Cargo.toml index ea092575..46953863 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -18,7 +18,6 @@ maintenance = { status = "actively-developed" } dodrio = "0.1.0" wasm-bindgen = "0.2.51" wasm-bindgen-futures = "0.4" -futures = "0.3" [dependencies.iced_core] version = "0.1.0" diff --git a/web/src/lib.rs b/web/src/lib.rs index b183c390..c44b99b5 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -75,7 +75,7 @@ pub use iced_core::{ Align, Background, Color, Font, HorizontalAlignment, Length, VerticalAlignment, }; -pub use iced_futures::Command; +pub use iced_futures::{futures, Command}; pub use style::Style; pub use subscription::Subscription; pub use widget::*; diff --git a/winit/Cargo.toml b/winit/Cargo.toml index ba6d5229..cef41e9c 100644 --- a/winit/Cargo.toml +++ b/winit/Cargo.toml @@ -16,17 +16,11 @@ debug = [] [dependencies] winit = { version = "0.20.0-alpha3", git = "https://github.com/hecrj/winit", rev = "709808eb4e69044705fcb214bcc30556db761405"} log = "0.4" -futures = "0.3" [dependencies.iced_native] version = "0.1.0-alpha" path = "../native" -[dependencies.iced_futures] -version = "0.1.0-alpha" -path = "../futures" -features = ["thread-pool"] - [dependencies.window_clipboard] git = "https://github.com/hecrj/window_clipboard" rev = "22c6dd6c04cd05d528029b50a30c56417cd4bebf" diff --git a/winit/src/application.rs b/winit/src/application.rs index 076ac092..4b21a930 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -1,10 +1,9 @@ use crate::{ conversion, input::{keyboard, mouse}, - window, Cache, Clipboard, Command, Debug, Element, Event, Mode, - MouseCursor, Proxy, Settings, Size, Subscription, UserInterface, + window, Cache, Clipboard, Command, Debug, Element, Event, Executor, Mode, + MouseCursor, Proxy, Runtime, Settings, Size, Subscription, UserInterface, }; -use iced_native::Runtime; /// An interactive, native cross-platform application. /// @@ -20,6 +19,11 @@ pub trait Application: Sized { /// [`Application`]: trait.Application.html type Renderer: window::Renderer; + /// The [`Executor`] that will run commands and subscriptions. + /// + /// [`Executor`]: trait.Executor.html + type Executor: Executor; + /// The type of __messages__ your [`Application`] will produce. /// /// [`Application`]: trait.Application.html @@ -110,13 +114,13 @@ pub trait Application: Sized { debug.startup_started(); let event_loop = EventLoop::with_user_event(); + let mut external_messages = Vec::new(); + let mut runtime = { - let thread_pool = futures::executor::ThreadPool::new() - .expect("Create thread pool"); + let executor = Self::Executor::new().expect("Create executor"); - Runtime::new(thread_pool, Proxy::new(event_loop.create_proxy())) + Runtime::new(executor, Proxy::new(event_loop.create_proxy())) }; - let mut external_messages = Vec::new(); let (mut application, init_command) = Self::new(); runtime.spawn(init_command); diff --git a/winit/src/proxy.rs b/winit/src/proxy.rs index 7e8dee98..cff9df33 100644 --- a/winit/src/proxy.rs +++ b/winit/src/proxy.rs @@ -1,4 +1,4 @@ -use futures::{ +use iced_native::futures::{ task::{Context, Poll}, Sink, }; -- cgit From 04086a90c9e933ebfb42de378054e1115b33529d Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 20 Jan 2020 05:43:09 +0100 Subject: Implement `WasmBindgen` executor and reorganize --- futures/Cargo.toml | 3 ++ futures/src/executor.rs | 12 +++++-- futures/src/executor/wasm_bindgen.rs | 17 +++++++++ src/element.rs | 9 +++++ src/executor.rs | 54 ++++++++++++++++++++++++++++ src/lib.rs | 30 +++++++++------- src/native.rs | 68 ------------------------------------ src/native/executor.rs | 23 ------------ src/web.rs | 1 - src/widget.rs | 60 +++++++++++++++++++++++++++++++ web/src/lib.rs | 9 +++-- 11 files changed, 176 insertions(+), 110 deletions(-) create mode 100644 futures/src/executor/wasm_bindgen.rs create mode 100644 src/element.rs create mode 100644 src/executor.rs delete mode 100644 src/native.rs delete mode 100644 src/native/executor.rs delete mode 100644 src/web.rs create mode 100644 src/widget.rs diff --git a/futures/Cargo.toml b/futures/Cargo.toml index 13c2d6b7..91860e1e 100644 --- a/futures/Cargo.toml +++ b/futures/Cargo.toml @@ -27,3 +27,6 @@ features = ["rt-core"] [dependencies.async-std] version = "1.0" optional = true + +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen-futures = "0.4" diff --git a/futures/src/executor.rs b/futures/src/executor.rs index 144a41f8..b2ff043e 100644 --- a/futures/src/executor.rs +++ b/futures/src/executor.rs @@ -4,23 +4,29 @@ mod null; #[cfg(feature = "thread-pool")] mod thread_pool; -#[cfg(feature = "thread-pool")] -pub use thread_pool::ThreadPool; - #[cfg(feature = "tokio")] mod tokio; #[cfg(feature = "async-std")] mod async_std; +#[cfg(target_arch = "wasm32")] +mod wasm_bindgen; + pub use null::Null; +#[cfg(feature = "thread-pool")] +pub use thread_pool::ThreadPool; + #[cfg(feature = "tokio")] pub use self::tokio::Tokio; #[cfg(feature = "async-std")] pub use self::async_std::AsyncStd; +#[cfg(target_arch = "wasm32")] +pub use wasm_bindgen::WasmBindgen; + use futures::Future; pub trait Executor: Sized { diff --git a/futures/src/executor/wasm_bindgen.rs b/futures/src/executor/wasm_bindgen.rs new file mode 100644 index 00000000..70a8ea8e --- /dev/null +++ b/futures/src/executor/wasm_bindgen.rs @@ -0,0 +1,17 @@ +use crate::Executor; + +#[derive(Debug)] +pub struct WasmBindgen; + +impl Executor for WasmBindgen { + fn new() -> Result { + Ok(Self) + } + + fn spawn( + &self, + future: impl futures::Future + Send + 'static, + ) { + wasm_bindgen_futures::spawn_local(future); + } +} diff --git a/src/element.rs b/src/element.rs new file mode 100644 index 00000000..e5356fb6 --- /dev/null +++ b/src/element.rs @@ -0,0 +1,9 @@ +/// A generic widget. +/// +/// This is an alias of an `iced_native` element with a default `Renderer`. +#[cfg(not(target_arch = "wasm32"))] +pub type Element<'a, Message> = + iced_winit::Element<'a, Message, iced_wgpu::Renderer>; + +#[cfg(target_arch = "wasm32")] +pub use iced_web::Element; diff --git a/src/executor.rs b/src/executor.rs new file mode 100644 index 00000000..cbbd8283 --- /dev/null +++ b/src/executor.rs @@ -0,0 +1,54 @@ +//! Choose your preferred executor to power your application. +pub use crate::common::{executor::Null, Executor}; + +pub use platform::Default; + +#[cfg(not(target_arch = "wasm32"))] +mod platform { + use iced_winit::{executor::ThreadPool, futures, Executor}; + + /// A default cross-platform executor. + /// + /// - On native platforms, it will use a `iced_futures::executor::ThreadPool`. + /// - On the Web, it will use `iced_futures::executor::WasmBindgen`. + #[derive(Debug)] + pub struct Default(ThreadPool); + + impl Executor for Default { + fn new() -> Result { + Ok(Default(ThreadPool::new()?)) + } + + fn spawn( + &self, + future: impl futures::Future + Send + 'static, + ) { + self.0.spawn(future); + } + } +} + +#[cfg(target_arch = "wasm32")] +mod platform { + use iced_web::{executor::WasmBindgen, futures, Executor}; + + /// A default cross-platform executor. + /// + /// - On native platforms, it will use a `iced_futures::executor::ThreadPool`. + /// - On the Web, it will use `iced_futures::executor::WasmBindgen`. + #[derive(Debug)] + pub struct Default(WasmBindgen); + + impl Executor for Default { + fn new() -> Result { + Ok(Default(WasmBindgen::new()?)) + } + + fn spawn( + &self, + future: impl futures::Future + Send + 'static, + ) { + self.0.spawn(future); + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 18dfc098..9c9bcff5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -180,26 +180,30 @@ #![deny(unsafe_code)] #![deny(rust_2018_idioms)] mod application; +mod element; mod sandbox; -#[cfg(not(target_arch = "wasm32"))] -mod native; - -#[cfg(not(target_arch = "wasm32"))] -pub use native::*; - -#[cfg(target_arch = "wasm32")] -mod web; - -#[cfg(target_arch = "wasm32")] -pub use web::*; - +pub mod executor; pub mod settings; +pub mod widget; pub mod window; #[doc(no_inline)] -pub use executor::Executor; +pub use widget::*; pub use application::Application; +pub use element::Element; +pub use executor::Executor; pub use sandbox::Sandbox; pub use settings::Settings; + +#[cfg(not(target_arch = "wasm32"))] +use iced_winit as common; + +#[cfg(target_arch = "wasm32")] +use iced_web as common; + +pub use common::{ + Align, Background, Color, Command, Font, HorizontalAlignment, Length, + Space, Subscription, Vector, VerticalAlignment, +}; diff --git a/src/native.rs b/src/native.rs deleted file mode 100644 index 86ccffab..00000000 --- a/src/native.rs +++ /dev/null @@ -1,68 +0,0 @@ -pub use iced_winit::{ - Align, Background, Color, Command, Font, HorizontalAlignment, Length, - Space, Subscription, Vector, VerticalAlignment, -}; - -pub mod executor; - -pub mod widget { - //! Display information and interactive controls in your application. - //! - //! # Re-exports - //! For convenience, the contents of this module are available at the root - //! module. Therefore, you can directly type: - //! - //! ``` - //! use iced::{button, Button}; - //! ``` - //! - //! # Stateful widgets - //! Some widgets need to keep track of __local state__. - //! - //! These widgets have their own module with a `State` type. For instance, a - //! [`TextInput`] has some [`text_input::State`]. - //! - //! [`TextInput`]: text_input/struct.TextInput.html - //! [`text_input::State`]: text_input/struct.State.html - pub use iced_wgpu::widget::*; - - pub mod image { - //! Display images in your user interface. - pub use iced_winit::image::{Handle, Image}; - } - - pub mod svg { - //! Display vector graphics in your user interface. - pub use iced_winit::svg::{Handle, Svg}; - } - - pub use iced_winit::Text; - - #[doc(no_inline)] - pub use { - button::Button, checkbox::Checkbox, container::Container, image::Image, - progress_bar::ProgressBar, radio::Radio, scrollable::Scrollable, - slider::Slider, svg::Svg, text_input::TextInput, - }; - - /// A container that distributes its contents vertically. - /// - /// This is an alias of an `iced_native` column with a default `Renderer`. - pub type Column<'a, Message> = - iced_winit::Column<'a, Message, iced_wgpu::Renderer>; - - /// A container that distributes its contents horizontally. - /// - /// This is an alias of an `iced_native` row with a default `Renderer`. - pub type Row<'a, Message> = - iced_winit::Row<'a, Message, iced_wgpu::Renderer>; -} - -#[doc(no_inline)] -pub use widget::*; - -/// A generic widget. -/// -/// This is an alias of an `iced_native` element with a default `Renderer`. -pub type Element<'a, Message> = - iced_winit::Element<'a, Message, iced_wgpu::Renderer>; diff --git a/src/native/executor.rs b/src/native/executor.rs deleted file mode 100644 index 68a1d280..00000000 --- a/src/native/executor.rs +++ /dev/null @@ -1,23 +0,0 @@ -//! Choose your preferred executor to power your application. -pub use iced_winit::{executor::Null, Executor}; -use iced_winit::{executor::ThreadPool, futures}; - -/// The default cross-platform executor. -/// -/// - On native platforms, it will use a `ThreadPool`. -/// - On the Web, it will use `wasm-bindgen-futures::spawn_local`. -#[derive(Debug)] -pub struct Default(ThreadPool); - -impl Executor for Default { - fn new() -> Result { - Ok(Default(ThreadPool::new()?)) - } - - fn spawn( - &self, - future: impl futures::Future + Send + 'static, - ) { - self.0.spawn(future); - } -} diff --git a/src/web.rs b/src/web.rs deleted file mode 100644 index 31f1a6fc..00000000 --- a/src/web.rs +++ /dev/null @@ -1 +0,0 @@ -pub use iced_web::*; diff --git a/src/widget.rs b/src/widget.rs new file mode 100644 index 00000000..7d3a1cef --- /dev/null +++ b/src/widget.rs @@ -0,0 +1,60 @@ +//! Display information and interactive controls in your application. +//! +//! # Re-exports +//! For convenience, the contents of this module are available at the root +//! module. Therefore, you can directly type: +//! +//! ``` +//! use iced::{button, Button}; +//! ``` +//! +//! # Stateful widgets +//! Some widgets need to keep track of __local state__. +//! +//! These widgets have their own module with a `State` type. For instance, a +//! [`TextInput`] has some [`text_input::State`]. +//! +//! [`TextInput`]: text_input/struct.TextInput.html +//! [`text_input::State`]: text_input/struct.State.html +#[cfg(not(target_arch = "wasm32"))] +mod platform { + pub use iced_wgpu::widget::*; + + pub mod image { + //! Display images in your user interface. + pub use iced_winit::image::{Handle, Image}; + } + + pub mod svg { + //! Display vector graphics in your user interface. + pub use iced_winit::svg::{Handle, Svg}; + } + + pub use iced_winit::Text; + + #[doc(no_inline)] + pub use { + button::Button, checkbox::Checkbox, container::Container, image::Image, + progress_bar::ProgressBar, radio::Radio, scrollable::Scrollable, + slider::Slider, svg::Svg, text_input::TextInput, + }; + + /// A container that distributes its contents vertically. + /// + /// This is an alias of an `iced_native` column with a default `Renderer`. + pub type Column<'a, Message> = + iced_winit::Column<'a, Message, iced_wgpu::Renderer>; + + /// A container that distributes its contents horizontally. + /// + /// This is an alias of an `iced_native` row with a default `Renderer`. + pub type Row<'a, Message> = + iced_winit::Row<'a, Message, iced_wgpu::Renderer>; +} + +#[cfg(target_arch = "wasm32")] +mod platform { + pub use iced_web::widget::*; +} + +pub use platform::*; diff --git a/web/src/lib.rs b/web/src/lib.rs index c44b99b5..b1bb80e3 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -72,14 +72,19 @@ pub use dodrio; pub use element::Element; pub use hasher::Hasher; pub use iced_core::{ - Align, Background, Color, Font, HorizontalAlignment, Length, + Align, Background, Color, Font, HorizontalAlignment, Length, Vector, VerticalAlignment, }; -pub use iced_futures::{futures, Command}; +pub use iced_futures::{executor, futures, Command}; pub use style::Style; pub use subscription::Subscription; + +#[doc(no_inline)] pub use widget::*; +#[doc(no_inline)] +pub use executor::Executor; + /// An interactive web application. /// /// This trait is the main entrypoint of Iced. Once implemented, you can run -- cgit From 7cea7371150e6de28032827519936008592f112d Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 20 Jan 2020 06:27:01 +0100 Subject: Package examples and remove `dev-dependencies` --- Cargo.toml | 29 +- examples/bezier_tool.rs | 366 ----------------- examples/bezier_tool/Cargo.toml | 12 + examples/bezier_tool/src/main.rs | 366 +++++++++++++++++ examples/counter.rs | 56 --- examples/counter/Cargo.toml | 9 + examples/counter/src/main.rs | 56 +++ examples/custom_widget.rs | 145 ------- examples/custom_widget/Cargo.toml | 11 + examples/custom_widget/src/main.rs | 145 +++++++ examples/events.rs | 86 ---- examples/events/Cargo.toml | 10 + examples/events/src/main.rs | 86 ++++ examples/geometry.rs | 210 ---------- examples/geometry/Cargo.toml | 11 + examples/geometry/src/main.rs | 210 ++++++++++ examples/pokedex.rs | 243 ------------ examples/pokedex/Cargo.toml | 14 + examples/pokedex/src/main.rs | 243 ++++++++++++ examples/progress_bar.rs | 47 --- examples/progress_bar/Cargo.toml | 9 + examples/progress_bar/src/main.rs | 47 +++ examples/resources/ferris.png | Bin 33061 -> 0 bytes examples/resources/icons.ttf | Bin 5596 -> 0 bytes examples/resources/tiger.svg | 725 --------------------------------- examples/stopwatch.rs | 204 ---------- examples/stopwatch/Cargo.toml | 12 + examples/stopwatch/src/main.rs | 206 ++++++++++ examples/styling.rs | 514 ------------------------ examples/styling/Cargo.toml | 9 + examples/styling/src/main.rs | 514 ++++++++++++++++++++++++ examples/svg.rs | 54 --- examples/svg/Cargo.toml | 9 + examples/svg/resources/tiger.svg | 725 +++++++++++++++++++++++++++++++++ examples/svg/src/main.rs | 37 ++ examples/todos.rs | 615 ---------------------------- examples/todos/Cargo.toml | 16 + examples/todos/fonts/icons.ttf | Bin 0 -> 5596 bytes examples/todos/src/main.rs | 615 ++++++++++++++++++++++++++++ examples/tour.rs | 794 ------------------------------------- examples/tour/Cargo.toml | 13 + examples/tour/images/ferris.png | Bin 0 -> 33061 bytes examples/tour/src/main.rs | 794 +++++++++++++++++++++++++++++++++++++ src/lib.rs | 4 +- 44 files changed, 4193 insertions(+), 4078 deletions(-) delete mode 100644 examples/bezier_tool.rs create mode 100644 examples/bezier_tool/Cargo.toml create mode 100644 examples/bezier_tool/src/main.rs delete mode 100644 examples/counter.rs create mode 100644 examples/counter/Cargo.toml create mode 100644 examples/counter/src/main.rs delete mode 100644 examples/custom_widget.rs create mode 100644 examples/custom_widget/Cargo.toml create mode 100644 examples/custom_widget/src/main.rs delete mode 100644 examples/events.rs create mode 100644 examples/events/Cargo.toml create mode 100644 examples/events/src/main.rs delete mode 100644 examples/geometry.rs create mode 100644 examples/geometry/Cargo.toml create mode 100644 examples/geometry/src/main.rs delete mode 100644 examples/pokedex.rs create mode 100644 examples/pokedex/Cargo.toml create mode 100644 examples/pokedex/src/main.rs delete mode 100644 examples/progress_bar.rs create mode 100644 examples/progress_bar/Cargo.toml create mode 100644 examples/progress_bar/src/main.rs delete mode 100644 examples/resources/ferris.png delete mode 100644 examples/resources/icons.ttf delete mode 100644 examples/resources/tiger.svg delete mode 100644 examples/stopwatch.rs create mode 100644 examples/stopwatch/Cargo.toml create mode 100644 examples/stopwatch/src/main.rs delete mode 100644 examples/styling.rs create mode 100644 examples/styling/Cargo.toml create mode 100644 examples/styling/src/main.rs delete mode 100644 examples/svg.rs create mode 100644 examples/svg/Cargo.toml create mode 100644 examples/svg/resources/tiger.svg create mode 100644 examples/svg/src/main.rs delete mode 100644 examples/todos.rs create mode 100644 examples/todos/Cargo.toml create mode 100644 examples/todos/fonts/icons.ttf create mode 100644 examples/todos/src/main.rs delete mode 100644 examples/tour.rs create mode 100644 examples/tour/Cargo.toml create mode 100644 examples/tour/images/ferris.png create mode 100644 examples/tour/src/main.rs diff --git a/Cargo.toml b/Cargo.toml index 87f3000e..28a97af9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,18 @@ members = [ "web", "wgpu", "winit", + "examples/bezier_tool", + "examples/counter", + "examples/custom_widget", + "examples/events", + "examples/geometry", + "examples/pokedex", + "examples/progress_bar", + "examples/stopwatch", + "examples/styling", + "examples/svg", + "examples/todos", + "examples/tour", ] [target.'cfg(not(target_arch = "wasm32"))'.dependencies] @@ -37,20 +49,3 @@ iced_wgpu = { version = "0.1.0", path = "wgpu" } [target.'cfg(target_arch = "wasm32")'.dependencies] iced_web = { version = "0.1.0", path = "web" } - -[dev-dependencies] -iced_native = { version = "0.1", path = "./native" } -iced_wgpu = { version = "0.1", path = "./wgpu" } -iced_futures = { version = "0.1.0-alpha", path = "./futures", features = ["async-std"] } -env_logger = "0.7" -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -directories = "2.0" -futures = "0.3" -async-std = { version = "1.3", features = ["unstable"] } -surf = "1.0" -rand = "0.7" -lyon = "0.15" - -[target.'cfg(target_arch = "wasm32")'.dev-dependencies] -wasm-bindgen = "0.2.51" diff --git a/examples/bezier_tool.rs b/examples/bezier_tool.rs deleted file mode 100644 index 043d265c..00000000 --- a/examples/bezier_tool.rs +++ /dev/null @@ -1,366 +0,0 @@ -//! This example showcases a simple native custom widget that renders arbitrary -//! path with `lyon`. -mod bezier { - // For now, to implement a custom native widget you will need to add - // `iced_native` and `iced_wgpu` to your dependencies. - // - // Then, you simply need to define your widget type and implement the - // `iced_native::Widget` trait with the `iced_wgpu::Renderer`. - // - // Of course, you can choose to make the implementation renderer-agnostic, - // if you wish to, by creating your own `Renderer` trait, which could be - // implemented by `iced_wgpu` and other renderers. - use iced_native::{ - input, layout, Clipboard, Color, Element, Event, Font, Hasher, - HorizontalAlignment, Layout, Length, MouseCursor, Point, Size, Vector, - VerticalAlignment, Widget, - }; - use iced_wgpu::{ - triangle::{Mesh2D, Vertex2D}, - Defaults, Primitive, Renderer, - }; - use lyon::tessellation::{ - basic_shapes, BuffersBuilder, StrokeAttributes, StrokeOptions, - StrokeTessellator, VertexBuffers, - }; - use std::sync::Arc; - - pub struct Bezier<'a, Message> { - state: &'a mut State, - curves: &'a [Curve], - // [from, to, ctrl] - on_click: Box Message>, - } - - #[derive(Debug, Clone, Copy)] - pub struct Curve { - from: Point, - to: Point, - control: Point, - } - - #[derive(Default)] - pub struct State { - pending: Option, - } - - enum Pending { - One { from: Point }, - Two { from: Point, to: Point }, - } - - impl<'a, Message> Bezier<'a, Message> { - pub fn new( - state: &'a mut State, - curves: &'a [Curve], - on_click: F, - ) -> Self - where - F: 'static + Fn(Curve) -> Message, - { - Self { - state, - curves, - on_click: Box::new(on_click), - } - } - } - - impl<'a, Message> Widget for Bezier<'a, Message> { - fn width(&self) -> Length { - Length::Fill - } - - fn height(&self) -> Length { - Length::Fill - } - - fn layout( - &self, - _renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - let size = limits - .height(Length::Fill) - .width(Length::Fill) - .resolve(Size::ZERO); - layout::Node::new(size) - } - - fn draw( - &self, - _renderer: &mut Renderer, - defaults: &Defaults, - layout: Layout<'_>, - cursor_position: Point, - ) -> (Primitive, MouseCursor) { - let mut buffer: VertexBuffers = VertexBuffers::new(); - let mut path_builder = lyon::path::Path::builder(); - - let bounds = layout.bounds(); - - // Draw rectangle border with lyon. - basic_shapes::stroke_rectangle( - &lyon::math::Rect::new( - lyon::math::Point::new(bounds.x + 0.5, bounds.y + 0.5), - lyon::math::Size::new( - bounds.width - 1.0, - bounds.height - 1.0, - ), - ), - &StrokeOptions::default().with_line_width(1.0), - &mut BuffersBuilder::new( - &mut buffer, - |pos: lyon::math::Point, _: StrokeAttributes| Vertex2D { - position: pos.to_array(), - color: [0.0, 0.0, 0.0, 1.0], - }, - ), - ) - .unwrap(); - - for curve in self.curves { - path_builder.move_to(lyon::math::Point::new( - curve.from.x + bounds.x, - curve.from.y + bounds.y, - )); - - path_builder.quadratic_bezier_to( - lyon::math::Point::new( - curve.control.x + bounds.x, - curve.control.y + bounds.y, - ), - lyon::math::Point::new( - curve.to.x + bounds.x, - curve.to.y + bounds.y, - ), - ); - } - - match self.state.pending { - None => {} - Some(Pending::One { from }) => { - path_builder.move_to(lyon::math::Point::new( - from.x + bounds.x, - from.y + bounds.y, - )); - path_builder.line_to(lyon::math::Point::new( - cursor_position.x, - cursor_position.y, - )); - } - Some(Pending::Two { from, to }) => { - path_builder.move_to(lyon::math::Point::new( - from.x + bounds.x, - from.y + bounds.y, - )); - path_builder.quadratic_bezier_to( - lyon::math::Point::new( - cursor_position.x, - cursor_position.y, - ), - lyon::math::Point::new( - to.x + bounds.x, - to.y + bounds.y, - ), - ); - } - } - - let mut tessellator = StrokeTessellator::new(); - - // Draw strokes with lyon. - tessellator - .tessellate( - &path_builder.build(), - &StrokeOptions::default().with_line_width(3.0), - &mut BuffersBuilder::new( - &mut buffer, - |pos: lyon::math::Point, _: StrokeAttributes| { - Vertex2D { - position: pos.to_array(), - color: [0.0, 0.0, 0.0, 1.0], - } - }, - ), - ) - .unwrap(); - - let mesh = Primitive::Mesh2D(Arc::new(Mesh2D { - vertices: buffer.vertices, - indices: buffer.indices, - })); - - ( - Primitive::Clip { - bounds, - offset: Vector::new(0, 0), - content: Box::new( - if self.curves.is_empty() - && self.state.pending.is_none() - { - let instructions = Primitive::Text { - bounds, - color: Color { - a: defaults.text.color.a * 0.7, - ..defaults.text.color - }, - content: String::from( - "Click to create bezier curves!", - ), - font: Font::Default, - size: 30.0, - horizontal_alignment: - HorizontalAlignment::Center, - vertical_alignment: VerticalAlignment::Center, - }; - - Primitive::Group { - primitives: vec![mesh, instructions], - } - } else { - mesh - }, - ), - }, - MouseCursor::OutOfBounds, - ) - } - - fn hash_layout(&self, _state: &mut Hasher) {} - - fn on_event( - &mut self, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - messages: &mut Vec, - _renderer: &Renderer, - _clipboard: Option<&dyn Clipboard>, - ) { - let bounds = layout.bounds(); - - if bounds.contains(cursor_position) { - match event { - Event::Mouse(input::mouse::Event::Input { - state: input::ButtonState::Pressed, - .. - }) => { - let new_point = Point::new( - cursor_position.x - bounds.x, - cursor_position.y - bounds.y, - ); - - match self.state.pending { - None => { - self.state.pending = - Some(Pending::One { from: new_point }); - } - Some(Pending::One { from }) => { - self.state.pending = Some(Pending::Two { - from, - to: new_point, - }); - } - Some(Pending::Two { from, to }) => { - self.state.pending = None; - - messages.push((self.on_click)(Curve { - from, - to, - control: new_point, - })); - } - } - } - _ => {} - } - } - } - } - - impl<'a, Message> Into> for Bezier<'a, Message> - where - Message: 'static, - { - fn into(self) -> Element<'a, Message, Renderer> { - Element::new(self) - } - } -} - -use bezier::Bezier; -use iced::{ - button, Align, Button, Column, Container, Element, Length, Sandbox, - Settings, Text, -}; - -pub fn main() { - Example::run(Settings::default()) -} - -#[derive(Default)] -struct Example { - bezier: bezier::State, - curves: Vec, - button_state: button::State, -} - -#[derive(Debug, Clone, Copy)] -enum Message { - AddCurve(bezier::Curve), - Clear, -} - -impl Sandbox for Example { - type Message = Message; - - fn new() -> Self { - Example::default() - } - - fn title(&self) -> String { - String::from("Bezier tool - Iced") - } - - fn update(&mut self, message: Message) { - match message { - Message::AddCurve(curve) => { - self.curves.push(curve); - } - Message::Clear => { - self.bezier = bezier::State::default(); - self.curves.clear(); - } - } - } - - fn view(&mut self) -> Element { - let content = Column::new() - .padding(20) - .spacing(20) - .align_items(Align::Center) - .push( - Text::new("Bezier tool example") - .width(Length::Shrink) - .size(50), - ) - .push(Bezier::new( - &mut self.bezier, - self.curves.as_slice(), - Message::AddCurve, - )) - .push( - Button::new(&mut self.button_state, Text::new("Clear")) - .padding(8) - .on_press(Message::Clear), - ); - - Container::new(content) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y() - .into() - } -} diff --git a/examples/bezier_tool/Cargo.toml b/examples/bezier_tool/Cargo.toml new file mode 100644 index 00000000..b13a0aa5 --- /dev/null +++ b/examples/bezier_tool/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "bezier_tool" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez "] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../.." } +iced_native = { path = "../../native" } +iced_wgpu = { path = "../../wgpu" } +lyon = "0.15" diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs new file mode 100644 index 00000000..043d265c --- /dev/null +++ b/examples/bezier_tool/src/main.rs @@ -0,0 +1,366 @@ +//! This example showcases a simple native custom widget that renders arbitrary +//! path with `lyon`. +mod bezier { + // For now, to implement a custom native widget you will need to add + // `iced_native` and `iced_wgpu` to your dependencies. + // + // Then, you simply need to define your widget type and implement the + // `iced_native::Widget` trait with the `iced_wgpu::Renderer`. + // + // Of course, you can choose to make the implementation renderer-agnostic, + // if you wish to, by creating your own `Renderer` trait, which could be + // implemented by `iced_wgpu` and other renderers. + use iced_native::{ + input, layout, Clipboard, Color, Element, Event, Font, Hasher, + HorizontalAlignment, Layout, Length, MouseCursor, Point, Size, Vector, + VerticalAlignment, Widget, + }; + use iced_wgpu::{ + triangle::{Mesh2D, Vertex2D}, + Defaults, Primitive, Renderer, + }; + use lyon::tessellation::{ + basic_shapes, BuffersBuilder, StrokeAttributes, StrokeOptions, + StrokeTessellator, VertexBuffers, + }; + use std::sync::Arc; + + pub struct Bezier<'a, Message> { + state: &'a mut State, + curves: &'a [Curve], + // [from, to, ctrl] + on_click: Box Message>, + } + + #[derive(Debug, Clone, Copy)] + pub struct Curve { + from: Point, + to: Point, + control: Point, + } + + #[derive(Default)] + pub struct State { + pending: Option, + } + + enum Pending { + One { from: Point }, + Two { from: Point, to: Point }, + } + + impl<'a, Message> Bezier<'a, Message> { + pub fn new( + state: &'a mut State, + curves: &'a [Curve], + on_click: F, + ) -> Self + where + F: 'static + Fn(Curve) -> Message, + { + Self { + state, + curves, + on_click: Box::new(on_click), + } + } + } + + impl<'a, Message> Widget for Bezier<'a, Message> { + fn width(&self) -> Length { + Length::Fill + } + + fn height(&self) -> Length { + Length::Fill + } + + fn layout( + &self, + _renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let size = limits + .height(Length::Fill) + .width(Length::Fill) + .resolve(Size::ZERO); + layout::Node::new(size) + } + + fn draw( + &self, + _renderer: &mut Renderer, + defaults: &Defaults, + layout: Layout<'_>, + cursor_position: Point, + ) -> (Primitive, MouseCursor) { + let mut buffer: VertexBuffers = VertexBuffers::new(); + let mut path_builder = lyon::path::Path::builder(); + + let bounds = layout.bounds(); + + // Draw rectangle border with lyon. + basic_shapes::stroke_rectangle( + &lyon::math::Rect::new( + lyon::math::Point::new(bounds.x + 0.5, bounds.y + 0.5), + lyon::math::Size::new( + bounds.width - 1.0, + bounds.height - 1.0, + ), + ), + &StrokeOptions::default().with_line_width(1.0), + &mut BuffersBuilder::new( + &mut buffer, + |pos: lyon::math::Point, _: StrokeAttributes| Vertex2D { + position: pos.to_array(), + color: [0.0, 0.0, 0.0, 1.0], + }, + ), + ) + .unwrap(); + + for curve in self.curves { + path_builder.move_to(lyon::math::Point::new( + curve.from.x + bounds.x, + curve.from.y + bounds.y, + )); + + path_builder.quadratic_bezier_to( + lyon::math::Point::new( + curve.control.x + bounds.x, + curve.control.y + bounds.y, + ), + lyon::math::Point::new( + curve.to.x + bounds.x, + curve.to.y + bounds.y, + ), + ); + } + + match self.state.pending { + None => {} + Some(Pending::One { from }) => { + path_builder.move_to(lyon::math::Point::new( + from.x + bounds.x, + from.y + bounds.y, + )); + path_builder.line_to(lyon::math::Point::new( + cursor_position.x, + cursor_position.y, + )); + } + Some(Pending::Two { from, to }) => { + path_builder.move_to(lyon::math::Point::new( + from.x + bounds.x, + from.y + bounds.y, + )); + path_builder.quadratic_bezier_to( + lyon::math::Point::new( + cursor_position.x, + cursor_position.y, + ), + lyon::math::Point::new( + to.x + bounds.x, + to.y + bounds.y, + ), + ); + } + } + + let mut tessellator = StrokeTessellator::new(); + + // Draw strokes with lyon. + tessellator + .tessellate( + &path_builder.build(), + &StrokeOptions::default().with_line_width(3.0), + &mut BuffersBuilder::new( + &mut buffer, + |pos: lyon::math::Point, _: StrokeAttributes| { + Vertex2D { + position: pos.to_array(), + color: [0.0, 0.0, 0.0, 1.0], + } + }, + ), + ) + .unwrap(); + + let mesh = Primitive::Mesh2D(Arc::new(Mesh2D { + vertices: buffer.vertices, + indices: buffer.indices, + })); + + ( + Primitive::Clip { + bounds, + offset: Vector::new(0, 0), + content: Box::new( + if self.curves.is_empty() + && self.state.pending.is_none() + { + let instructions = Primitive::Text { + bounds, + color: Color { + a: defaults.text.color.a * 0.7, + ..defaults.text.color + }, + content: String::from( + "Click to create bezier curves!", + ), + font: Font::Default, + size: 30.0, + horizontal_alignment: + HorizontalAlignment::Center, + vertical_alignment: VerticalAlignment::Center, + }; + + Primitive::Group { + primitives: vec![mesh, instructions], + } + } else { + mesh + }, + ), + }, + MouseCursor::OutOfBounds, + ) + } + + fn hash_layout(&self, _state: &mut Hasher) {} + + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + messages: &mut Vec, + _renderer: &Renderer, + _clipboard: Option<&dyn Clipboard>, + ) { + let bounds = layout.bounds(); + + if bounds.contains(cursor_position) { + match event { + Event::Mouse(input::mouse::Event::Input { + state: input::ButtonState::Pressed, + .. + }) => { + let new_point = Point::new( + cursor_position.x - bounds.x, + cursor_position.y - bounds.y, + ); + + match self.state.pending { + None => { + self.state.pending = + Some(Pending::One { from: new_point }); + } + Some(Pending::One { from }) => { + self.state.pending = Some(Pending::Two { + from, + to: new_point, + }); + } + Some(Pending::Two { from, to }) => { + self.state.pending = None; + + messages.push((self.on_click)(Curve { + from, + to, + control: new_point, + })); + } + } + } + _ => {} + } + } + } + } + + impl<'a, Message> Into> for Bezier<'a, Message> + where + Message: 'static, + { + fn into(self) -> Element<'a, Message, Renderer> { + Element::new(self) + } + } +} + +use bezier::Bezier; +use iced::{ + button, Align, Button, Column, Container, Element, Length, Sandbox, + Settings, Text, +}; + +pub fn main() { + Example::run(Settings::default()) +} + +#[derive(Default)] +struct Example { + bezier: bezier::State, + curves: Vec, + button_state: button::State, +} + +#[derive(Debug, Clone, Copy)] +enum Message { + AddCurve(bezier::Curve), + Clear, +} + +impl Sandbox for Example { + type Message = Message; + + fn new() -> Self { + Example::default() + } + + fn title(&self) -> String { + String::from("Bezier tool - Iced") + } + + fn update(&mut self, message: Message) { + match message { + Message::AddCurve(curve) => { + self.curves.push(curve); + } + Message::Clear => { + self.bezier = bezier::State::default(); + self.curves.clear(); + } + } + } + + fn view(&mut self) -> Element { + let content = Column::new() + .padding(20) + .spacing(20) + .align_items(Align::Center) + .push( + Text::new("Bezier tool example") + .width(Length::Shrink) + .size(50), + ) + .push(Bezier::new( + &mut self.bezier, + self.curves.as_slice(), + Message::AddCurve, + )) + .push( + Button::new(&mut self.button_state, Text::new("Clear")) + .padding(8) + .on_press(Message::Clear), + ); + + Container::new(content) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .into() + } +} diff --git a/examples/counter.rs b/examples/counter.rs deleted file mode 100644 index b85db70d..00000000 --- a/examples/counter.rs +++ /dev/null @@ -1,56 +0,0 @@ -use iced::{button, Button, Column, Element, Sandbox, Settings, Text}; - -pub fn main() { - Counter::run(Settings::default()) -} - -#[derive(Default)] -struct Counter { - value: i32, - increment_button: button::State, - decrement_button: button::State, -} - -#[derive(Debug, Clone, Copy)] -enum Message { - IncrementPressed, - DecrementPressed, -} - -impl Sandbox for Counter { - type Message = Message; - - fn new() -> Self { - Self::default() - } - - fn title(&self) -> String { - String::from("A simple counter") - } - - fn update(&mut self, message: Message) { - match message { - Message::IncrementPressed => { - self.value += 1; - } - Message::DecrementPressed => { - self.value -= 1; - } - } - } - - fn view(&mut self) -> Element { - Column::new() - .padding(20) - .push( - Button::new(&mut self.increment_button, Text::new("Increment")) - .on_press(Message::IncrementPressed), - ) - .push(Text::new(self.value.to_string()).size(50)) - .push( - Button::new(&mut self.decrement_button, Text::new("Decrement")) - .on_press(Message::DecrementPressed), - ) - .into() - } -} diff --git a/examples/counter/Cargo.toml b/examples/counter/Cargo.toml new file mode 100644 index 00000000..a763cd78 --- /dev/null +++ b/examples/counter/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "counter" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez "] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../.." } diff --git a/examples/counter/src/main.rs b/examples/counter/src/main.rs new file mode 100644 index 00000000..b85db70d --- /dev/null +++ b/examples/counter/src/main.rs @@ -0,0 +1,56 @@ +use iced::{button, Button, Column, Element, Sandbox, Settings, Text}; + +pub fn main() { + Counter::run(Settings::default()) +} + +#[derive(Default)] +struct Counter { + value: i32, + increment_button: button::State, + decrement_button: button::State, +} + +#[derive(Debug, Clone, Copy)] +enum Message { + IncrementPressed, + DecrementPressed, +} + +impl Sandbox for Counter { + type Message = Message; + + fn new() -> Self { + Self::default() + } + + fn title(&self) -> String { + String::from("A simple counter") + } + + fn update(&mut self, message: Message) { + match message { + Message::IncrementPressed => { + self.value += 1; + } + Message::DecrementPressed => { + self.value -= 1; + } + } + } + + fn view(&mut self) -> Element { + Column::new() + .padding(20) + .push( + Button::new(&mut self.increment_button, Text::new("Increment")) + .on_press(Message::IncrementPressed), + ) + .push(Text::new(self.value.to_string()).size(50)) + .push( + Button::new(&mut self.decrement_button, Text::new("Decrement")) + .on_press(Message::DecrementPressed), + ) + .into() + } +} diff --git a/examples/custom_widget.rs b/examples/custom_widget.rs deleted file mode 100644 index 0a570745..00000000 --- a/examples/custom_widget.rs +++ /dev/null @@ -1,145 +0,0 @@ -//! This example showcases a simple native custom widget that draws a circle. -mod circle { - // For now, to implement a custom native widget you will need to add - // `iced_native` and `iced_wgpu` to your dependencies. - // - // Then, you simply need to define your widget type and implement the - // `iced_native::Widget` trait with the `iced_wgpu::Renderer`. - // - // Of course, you can choose to make the implementation renderer-agnostic, - // if you wish to, by creating your own `Renderer` trait, which could be - // implemented by `iced_wgpu` and other renderers. - use iced_native::{ - layout, Background, Color, Element, Hasher, Layout, Length, - MouseCursor, Point, Size, Widget, - }; - use iced_wgpu::{Defaults, Primitive, Renderer}; - - pub struct Circle { - radius: u16, - } - - impl Circle { - pub fn new(radius: u16) -> Self { - Self { radius } - } - } - - impl Widget for Circle { - fn width(&self) -> Length { - Length::Shrink - } - - fn height(&self) -> Length { - Length::Shrink - } - - fn layout( - &self, - _renderer: &Renderer, - _limits: &layout::Limits, - ) -> layout::Node { - layout::Node::new(Size::new( - f32::from(self.radius) * 2.0, - f32::from(self.radius) * 2.0, - )) - } - - fn hash_layout(&self, state: &mut Hasher) { - use std::hash::Hash; - - self.radius.hash(state); - } - - fn draw( - &self, - _renderer: &mut Renderer, - _defaults: &Defaults, - layout: Layout<'_>, - _cursor_position: Point, - ) -> (Primitive, MouseCursor) { - ( - Primitive::Quad { - bounds: layout.bounds(), - background: Background::Color(Color::BLACK), - border_radius: self.radius, - border_width: 0, - border_color: Color::TRANSPARENT, - }, - MouseCursor::OutOfBounds, - ) - } - } - - impl<'a, Message> Into> for Circle { - fn into(self) -> Element<'a, Message, Renderer> { - Element::new(self) - } - } -} - -use circle::Circle; -use iced::{ - slider, Align, Column, Container, Element, Length, Sandbox, Settings, - Slider, Text, -}; - -pub fn main() { - Example::run(Settings::default()) -} - -struct Example { - radius: u16, - slider: slider::State, -} - -#[derive(Debug, Clone, Copy)] -enum Message { - RadiusChanged(f32), -} - -impl Sandbox for Example { - type Message = Message; - - fn new() -> Self { - Example { - radius: 50, - slider: slider::State::new(), - } - } - - fn title(&self) -> String { - String::from("Custom widget - Iced") - } - - fn update(&mut self, message: Message) { - match message { - Message::RadiusChanged(radius) => { - self.radius = radius.round() as u16; - } - } - } - - fn view(&mut self) -> Element { - let content = Column::new() - .padding(20) - .spacing(20) - .max_width(500) - .align_items(Align::Center) - .push(Circle::new(self.radius)) - .push(Text::new(format!("Radius: {}", self.radius.to_string()))) - .push(Slider::new( - &mut self.slider, - 1.0..=100.0, - f32::from(self.radius), - Message::RadiusChanged, - )); - - Container::new(content) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y() - .into() - } -} diff --git a/examples/custom_widget/Cargo.toml b/examples/custom_widget/Cargo.toml new file mode 100644 index 00000000..30747dc0 --- /dev/null +++ b/examples/custom_widget/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "custom_widget" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez "] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../.." } +iced_native = { path = "../../native" } +iced_wgpu = { path = "../../wgpu" } diff --git a/examples/custom_widget/src/main.rs b/examples/custom_widget/src/main.rs new file mode 100644 index 00000000..0a570745 --- /dev/null +++ b/examples/custom_widget/src/main.rs @@ -0,0 +1,145 @@ +//! This example showcases a simple native custom widget that draws a circle. +mod circle { + // For now, to implement a custom native widget you will need to add + // `iced_native` and `iced_wgpu` to your dependencies. + // + // Then, you simply need to define your widget type and implement the + // `iced_native::Widget` trait with the `iced_wgpu::Renderer`. + // + // Of course, you can choose to make the implementation renderer-agnostic, + // if you wish to, by creating your own `Renderer` trait, which could be + // implemented by `iced_wgpu` and other renderers. + use iced_native::{ + layout, Background, Color, Element, Hasher, Layout, Length, + MouseCursor, Point, Size, Widget, + }; + use iced_wgpu::{Defaults, Primitive, Renderer}; + + pub struct Circle { + radius: u16, + } + + impl Circle { + pub fn new(radius: u16) -> Self { + Self { radius } + } + } + + impl Widget for Circle { + fn width(&self) -> Length { + Length::Shrink + } + + fn height(&self) -> Length { + Length::Shrink + } + + fn layout( + &self, + _renderer: &Renderer, + _limits: &layout::Limits, + ) -> layout::Node { + layout::Node::new(Size::new( + f32::from(self.radius) * 2.0, + f32::from(self.radius) * 2.0, + )) + } + + fn hash_layout(&self, state: &mut Hasher) { + use std::hash::Hash; + + self.radius.hash(state); + } + + fn draw( + &self, + _renderer: &mut Renderer, + _defaults: &Defaults, + layout: Layout<'_>, + _cursor_position: Point, + ) -> (Primitive, MouseCursor) { + ( + Primitive::Quad { + bounds: layout.bounds(), + background: Background::Color(Color::BLACK), + border_radius: self.radius, + border_width: 0, + border_color: Color::TRANSPARENT, + }, + MouseCursor::OutOfBounds, + ) + } + } + + impl<'a, Message> Into> for Circle { + fn into(self) -> Element<'a, Message, Renderer> { + Element::new(self) + } + } +} + +use circle::Circle; +use iced::{ + slider, Align, Column, Container, Element, Length, Sandbox, Settings, + Slider, Text, +}; + +pub fn main() { + Example::run(Settings::default()) +} + +struct Example { + radius: u16, + slider: slider::State, +} + +#[derive(Debug, Clone, Copy)] +enum Message { + RadiusChanged(f32), +} + +impl Sandbox for Example { + type Message = Message; + + fn new() -> Self { + Example { + radius: 50, + slider: slider::State::new(), + } + } + + fn title(&self) -> String { + String::from("Custom widget - Iced") + } + + fn update(&mut self, message: Message) { + match message { + Message::RadiusChanged(radius) => { + self.radius = radius.round() as u16; + } + } + } + + fn view(&mut self) -> Element { + let content = Column::new() + .padding(20) + .spacing(20) + .max_width(500) + .align_items(Align::Center) + .push(Circle::new(self.radius)) + .push(Text::new(format!("Radius: {}", self.radius.to_string()))) + .push(Slider::new( + &mut self.slider, + 1.0..=100.0, + f32::from(self.radius), + Message::RadiusChanged, + )); + + Container::new(content) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .into() + } +} diff --git a/examples/events.rs b/examples/events.rs deleted file mode 100644 index 0c9dca05..00000000 --- a/examples/events.rs +++ /dev/null @@ -1,86 +0,0 @@ -use iced::{ - executor, 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, - enabled: bool, -} - -#[derive(Debug, Clone)] -enum Message { - EventOccurred(iced_native::Event), - Toggled(bool), -} - -impl Application for Events { - type Executor = executor::Default; - type Message = Message; - - fn new() -> (Events, Command) { - (Events::default(), Command::none()) - } - - fn title(&self) -> String { - String::from("Events - Iced") - } - - fn update(&mut self, message: Message) -> Command { - 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 { - if self.enabled { - iced_native::subscription::events().map(Message::EventOccurred) - } else { - Subscription::none() - } - } - - fn view(&mut self) -> Element { - let events = self.last.iter().fold( - Column::new().spacing(10), - |column, event| { - column.push(Text::new(format!("{:?}", event)).size(40)) - }, - ); - - let toggle = Checkbox::new( - self.enabled, - "Listen to runtime events", - Message::Toggled, - ); - - let content = Column::new() - .align_items(Align::Center) - .spacing(20) - .push(events) - .push(toggle); - - Container::new(content) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y() - .into() - } -} diff --git a/examples/events/Cargo.toml b/examples/events/Cargo.toml new file mode 100644 index 00000000..f883075f --- /dev/null +++ b/examples/events/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "events" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez "] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../.." } +iced_native = { path = "../../native" } diff --git a/examples/events/src/main.rs b/examples/events/src/main.rs new file mode 100644 index 00000000..0c9dca05 --- /dev/null +++ b/examples/events/src/main.rs @@ -0,0 +1,86 @@ +use iced::{ + executor, 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, + enabled: bool, +} + +#[derive(Debug, Clone)] +enum Message { + EventOccurred(iced_native::Event), + Toggled(bool), +} + +impl Application for Events { + type Executor = executor::Default; + type Message = Message; + + fn new() -> (Events, Command) { + (Events::default(), Command::none()) + } + + fn title(&self) -> String { + String::from("Events - Iced") + } + + fn update(&mut self, message: Message) -> Command { + 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 { + if self.enabled { + iced_native::subscription::events().map(Message::EventOccurred) + } else { + Subscription::none() + } + } + + fn view(&mut self) -> Element { + let events = self.last.iter().fold( + Column::new().spacing(10), + |column, event| { + column.push(Text::new(format!("{:?}", event)).size(40)) + }, + ); + + let toggle = Checkbox::new( + self.enabled, + "Listen to runtime events", + Message::Toggled, + ); + + let content = Column::new() + .align_items(Align::Center) + .spacing(20) + .push(events) + .push(toggle); + + Container::new(content) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .into() + } +} diff --git a/examples/geometry.rs b/examples/geometry.rs deleted file mode 100644 index 9d5fd611..00000000 --- a/examples/geometry.rs +++ /dev/null @@ -1,210 +0,0 @@ -//! This example showcases a simple native custom widget that renders using -//! arbitrary low-level geometry. -mod rainbow { - // For now, to implement a custom native widget you will need to add - // `iced_native` and `iced_wgpu` to your dependencies. - // - // Then, you simply need to define your widget type and implement the - // `iced_native::Widget` trait with the `iced_wgpu::Renderer`. - // - // Of course, you can choose to make the implementation renderer-agnostic, - // if you wish to, by creating your own `Renderer` trait, which could be - // implemented by `iced_wgpu` and other renderers. - use iced_native::{ - layout, Element, Hasher, Layout, Length, MouseCursor, Point, Size, - Widget, - }; - use iced_wgpu::{ - triangle::{Mesh2D, Vertex2D}, - Defaults, Primitive, Renderer, - }; - - pub struct Rainbow; - - impl Rainbow { - pub fn new() -> Self { - Self - } - } - - impl Widget for Rainbow { - fn width(&self) -> Length { - Length::Fill - } - - fn height(&self) -> Length { - Length::Shrink - } - - fn layout( - &self, - _renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - let size = limits.width(Length::Fill).resolve(Size::ZERO); - - layout::Node::new(Size::new(size.width, size.width)) - } - - fn hash_layout(&self, _state: &mut Hasher) {} - - fn draw( - &self, - _renderer: &mut Renderer, - _defaults: &Defaults, - layout: Layout<'_>, - cursor_position: Point, - ) -> (Primitive, MouseCursor) { - let b = layout.bounds(); - - // R O Y G B I V - let color_r = [1.0, 0.0, 0.0, 1.0]; - let color_o = [1.0, 0.5, 0.0, 1.0]; - let color_y = [1.0, 1.0, 0.0, 1.0]; - let color_g = [0.0, 1.0, 0.0, 1.0]; - let color_gb = [0.0, 1.0, 0.5, 1.0]; - let color_b = [0.0, 0.2, 1.0, 1.0]; - let color_i = [0.5, 0.0, 1.0, 1.0]; - let color_v = [0.75, 0.0, 0.5, 1.0]; - - let posn_center = { - if b.contains(cursor_position) { - [cursor_position.x, cursor_position.y] - } else { - [b.x + (b.width / 2.0), b.y + (b.height / 2.0)] - } - }; - - let posn_tl = [b.x, b.y]; - let posn_t = [b.x + (b.width / 2.0), b.y]; - let posn_tr = [b.x + b.width, b.y]; - let posn_r = [b.x + b.width, b.y + (b.height / 2.0)]; - let posn_br = [b.x + b.width, b.y + b.height]; - let posn_b = [b.x + (b.width / 2.0), b.y + b.height]; - let posn_bl = [b.x, b.y + b.height]; - let posn_l = [b.x, b.y + (b.height / 2.0)]; - - ( - Primitive::Mesh2D(std::sync::Arc::new(Mesh2D { - vertices: vec![ - Vertex2D { - position: posn_center, - color: [1.0, 1.0, 1.0, 1.0], - }, - Vertex2D { - position: posn_tl, - color: color_r, - }, - Vertex2D { - position: posn_t, - color: color_o, - }, - Vertex2D { - position: posn_tr, - color: color_y, - }, - Vertex2D { - position: posn_r, - color: color_g, - }, - Vertex2D { - position: posn_br, - color: color_gb, - }, - Vertex2D { - position: posn_b, - color: color_b, - }, - Vertex2D { - position: posn_bl, - color: color_i, - }, - Vertex2D { - position: posn_l, - color: color_v, - }, - ], - indices: vec![ - 0, 1, 2, // TL - 0, 2, 3, // T - 0, 3, 4, // TR - 0, 4, 5, // R - 0, 5, 6, // BR - 0, 6, 7, // B - 0, 7, 8, // BL - 0, 8, 1, // L - ], - })), - MouseCursor::OutOfBounds, - ) - } - } - - impl<'a, Message> Into> for Rainbow { - fn into(self) -> Element<'a, Message, Renderer> { - Element::new(self) - } - } -} - -use iced::{ - scrollable, Align, Column, Container, Element, Length, Sandbox, Scrollable, - Settings, Text, -}; -use rainbow::Rainbow; - -pub fn main() { - Example::run(Settings::default()) -} - -struct Example { - scroll: scrollable::State, -} - -impl Sandbox for Example { - type Message = (); - - fn new() -> Self { - Example { - scroll: scrollable::State::new(), - } - } - - fn title(&self) -> String { - String::from("Custom 2D geometry - Iced") - } - - fn update(&mut self, _: ()) {} - - fn view(&mut self) -> Element<()> { - let content = Column::new() - .padding(20) - .spacing(20) - .max_width(500) - .align_items(Align::Start) - .push(Rainbow::new()) - .push(Text::new( - "In this example we draw a custom widget Rainbow, using \ - the Mesh2D primitive. This primitive supplies a list of \ - triangles, expressed as vertices and indices.", - )) - .push(Text::new( - "Move your cursor over it, and see the center vertex \ - follow you!", - )) - .push(Text::new( - "Every Vertex2D defines its own color. You could use the \ - Mesh2D primitive to render virtually any two-dimensional \ - geometry for your widget.", - )); - - let scrollable = Scrollable::new(&mut self.scroll) - .push(Container::new(content).width(Length::Fill).center_x()); - - Container::new(scrollable) - .width(Length::Fill) - .height(Length::Fill) - .center_y() - .into() - } -} diff --git a/examples/geometry/Cargo.toml b/examples/geometry/Cargo.toml new file mode 100644 index 00000000..9df52454 --- /dev/null +++ b/examples/geometry/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "geometry" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez "] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../.." } +iced_native = { path = "../../native" } +iced_wgpu = { path = "../../wgpu" } diff --git a/examples/geometry/src/main.rs b/examples/geometry/src/main.rs new file mode 100644 index 00000000..9d5fd611 --- /dev/null +++ b/examples/geometry/src/main.rs @@ -0,0 +1,210 @@ +//! This example showcases a simple native custom widget that renders using +//! arbitrary low-level geometry. +mod rainbow { + // For now, to implement a custom native widget you will need to add + // `iced_native` and `iced_wgpu` to your dependencies. + // + // Then, you simply need to define your widget type and implement the + // `iced_native::Widget` trait with the `iced_wgpu::Renderer`. + // + // Of course, you can choose to make the implementation renderer-agnostic, + // if you wish to, by creating your own `Renderer` trait, which could be + // implemented by `iced_wgpu` and other renderers. + use iced_native::{ + layout, Element, Hasher, Layout, Length, MouseCursor, Point, Size, + Widget, + }; + use iced_wgpu::{ + triangle::{Mesh2D, Vertex2D}, + Defaults, Primitive, Renderer, + }; + + pub struct Rainbow; + + impl Rainbow { + pub fn new() -> Self { + Self + } + } + + impl Widget for Rainbow { + fn width(&self) -> Length { + Length::Fill + } + + fn height(&self) -> Length { + Length::Shrink + } + + fn layout( + &self, + _renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let size = limits.width(Length::Fill).resolve(Size::ZERO); + + layout::Node::new(Size::new(size.width, size.width)) + } + + fn hash_layout(&self, _state: &mut Hasher) {} + + fn draw( + &self, + _renderer: &mut Renderer, + _defaults: &Defaults, + layout: Layout<'_>, + cursor_position: Point, + ) -> (Primitive, MouseCursor) { + let b = layout.bounds(); + + // R O Y G B I V + let color_r = [1.0, 0.0, 0.0, 1.0]; + let color_o = [1.0, 0.5, 0.0, 1.0]; + let color_y = [1.0, 1.0, 0.0, 1.0]; + let color_g = [0.0, 1.0, 0.0, 1.0]; + let color_gb = [0.0, 1.0, 0.5, 1.0]; + let color_b = [0.0, 0.2, 1.0, 1.0]; + let color_i = [0.5, 0.0, 1.0, 1.0]; + let color_v = [0.75, 0.0, 0.5, 1.0]; + + let posn_center = { + if b.contains(cursor_position) { + [cursor_position.x, cursor_position.y] + } else { + [b.x + (b.width / 2.0), b.y + (b.height / 2.0)] + } + }; + + let posn_tl = [b.x, b.y]; + let posn_t = [b.x + (b.width / 2.0), b.y]; + let posn_tr = [b.x + b.width, b.y]; + let posn_r = [b.x + b.width, b.y + (b.height / 2.0)]; + let posn_br = [b.x + b.width, b.y + b.height]; + let posn_b = [b.x + (b.width / 2.0), b.y + b.height]; + let posn_bl = [b.x, b.y + b.height]; + let posn_l = [b.x, b.y + (b.height / 2.0)]; + + ( + Primitive::Mesh2D(std::sync::Arc::new(Mesh2D { + vertices: vec![ + Vertex2D { + position: posn_center, + color: [1.0, 1.0, 1.0, 1.0], + }, + Vertex2D { + position: posn_tl, + color: color_r, + }, + Vertex2D { + position: posn_t, + color: color_o, + }, + Vertex2D { + position: posn_tr, + color: color_y, + }, + Vertex2D { + position: posn_r, + color: color_g, + }, + Vertex2D { + position: posn_br, + color: color_gb, + }, + Vertex2D { + position: posn_b, + color: color_b, + }, + Vertex2D { + position: posn_bl, + color: color_i, + }, + Vertex2D { + position: posn_l, + color: color_v, + }, + ], + indices: vec![ + 0, 1, 2, // TL + 0, 2, 3, // T + 0, 3, 4, // TR + 0, 4, 5, // R + 0, 5, 6, // BR + 0, 6, 7, // B + 0, 7, 8, // BL + 0, 8, 1, // L + ], + })), + MouseCursor::OutOfBounds, + ) + } + } + + impl<'a, Message> Into> for Rainbow { + fn into(self) -> Element<'a, Message, Renderer> { + Element::new(self) + } + } +} + +use iced::{ + scrollable, Align, Column, Container, Element, Length, Sandbox, Scrollable, + Settings, Text, +}; +use rainbow::Rainbow; + +pub fn main() { + Example::run(Settings::default()) +} + +struct Example { + scroll: scrollable::State, +} + +impl Sandbox for Example { + type Message = (); + + fn new() -> Self { + Example { + scroll: scrollable::State::new(), + } + } + + fn title(&self) -> String { + String::from("Custom 2D geometry - Iced") + } + + fn update(&mut self, _: ()) {} + + fn view(&mut self) -> Element<()> { + let content = Column::new() + .padding(20) + .spacing(20) + .max_width(500) + .align_items(Align::Start) + .push(Rainbow::new()) + .push(Text::new( + "In this example we draw a custom widget Rainbow, using \ + the Mesh2D primitive. This primitive supplies a list of \ + triangles, expressed as vertices and indices.", + )) + .push(Text::new( + "Move your cursor over it, and see the center vertex \ + follow you!", + )) + .push(Text::new( + "Every Vertex2D defines its own color. You could use the \ + Mesh2D primitive to render virtually any two-dimensional \ + geometry for your widget.", + )); + + let scrollable = Scrollable::new(&mut self.scroll) + .push(Container::new(content).width(Length::Fill).center_x()); + + Container::new(scrollable) + .width(Length::Fill) + .height(Length::Fill) + .center_y() + .into() + } +} diff --git a/examples/pokedex.rs b/examples/pokedex.rs deleted file mode 100644 index 505dbf19..00000000 --- a/examples/pokedex.rs +++ /dev/null @@ -1,243 +0,0 @@ -use iced::{ - button, image, Align, Application, Button, Column, Command, Container, - Element, Image, Length, Row, Settings, Text, -}; - -pub fn main() { - Pokedex::run(Settings::default()) -} - -#[derive(Debug)] -enum Pokedex { - Loading, - Loaded { - pokemon: Pokemon, - search: button::State, - }, - Errored { - error: Error, - try_again: button::State, - }, -} - -#[derive(Debug, Clone)] -enum Message { - PokemonFound(Result), - Search, -} - -impl Application for Pokedex { - type Executor = iced_futures::executor::AsyncStd; - type Message = Message; - - fn new() -> (Pokedex, Command) { - ( - Pokedex::Loading, - Command::perform(Pokemon::search(), Message::PokemonFound), - ) - } - - fn title(&self) -> String { - let subtitle = match self { - Pokedex::Loading => "Loading", - Pokedex::Loaded { pokemon, .. } => &pokemon.name, - Pokedex::Errored { .. } => "Whoops!", - }; - - format!("{} - Pokédex", subtitle) - } - - fn update(&mut self, message: Message) -> Command { - match message { - Message::PokemonFound(Ok(pokemon)) => { - *self = Pokedex::Loaded { - pokemon, - search: button::State::new(), - }; - - Command::none() - } - Message::PokemonFound(Err(error)) => { - *self = Pokedex::Errored { - error, - try_again: button::State::new(), - }; - - Command::none() - } - Message::Search => match self { - Pokedex::Loading => Command::none(), - _ => { - *self = Pokedex::Loading; - - Command::perform(Pokemon::search(), Message::PokemonFound) - } - }, - } - } - - fn view(&mut self) -> Element { - let content = match self { - Pokedex::Loading => Column::new() - .push(Text::new("Searching for Pokémon...").size(40)), - Pokedex::Loaded { pokemon, search } => Column::new() - .max_width(500) - .spacing(20) - .align_items(Align::End) - .push(pokemon.view()) - .push( - button(search, "Keep searching!").on_press(Message::Search), - ), - Pokedex::Errored { try_again, .. } => Column::new() - .spacing(20) - .align_items(Align::End) - .push(Text::new("Whoops! Something went wrong...").size(40)) - .push(button(try_again, "Try again").on_press(Message::Search)), - }; - - Container::new(content) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y() - .into() - } -} - -#[derive(Debug, Clone)] -struct Pokemon { - number: u16, - name: String, - description: String, - image: image::Handle, -} - -impl Pokemon { - const TOTAL: u16 = 807; - - fn view(&self) -> Element { - Row::new() - .spacing(20) - .align_items(Align::Center) - .push(Image::new(self.image.clone())) - .push( - Column::new() - .spacing(20) - .push( - Row::new() - .align_items(Align::Center) - .spacing(20) - .push( - Text::new(&self.name) - .size(30) - .width(Length::Fill), - ) - .push( - Text::new(format!("#{}", self.number)) - .size(20) - .color([0.5, 0.5, 0.5]), - ), - ) - .push(Text::new(&self.description)), - ) - .into() - } - - async fn search() -> Result { - use rand::Rng; - use serde::Deserialize; - - #[derive(Debug, Deserialize)] - struct Entry { - id: u32, - name: String, - flavor_text_entries: Vec, - } - - #[derive(Debug, Deserialize)] - struct FlavorText { - flavor_text: String, - language: Language, - } - - #[derive(Debug, Deserialize)] - struct Language { - name: String, - } - - let id = { - let mut rng = rand::thread_rng(); - - rng.gen_range(0, Pokemon::TOTAL) - }; - - 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, sprite): (Entry, _) = futures::future::try_join( - surf::get(&url).recv_json(), - surf::get(&sprite).recv_bytes(), - ) - .await?; - - let description = entry - .flavor_text_entries - .iter() - .filter(|text| text.language.name == "en") - .next() - .ok_or(Error::LanguageError)?; - - Ok(Pokemon { - number: id, - name: entry.name.to_uppercase(), - description: description - .flavor_text - .chars() - .map(|c| if c.is_control() { ' ' } else { c }) - .collect(), - image: image::Handle::from_memory(sprite), - }) - } -} - -#[derive(Debug, Clone)] -enum Error { - APIError, - LanguageError, -} - -impl From for Error { - fn from(exception: surf::Exception) -> Error { - dbg!(&exception); - - Error::APIError - } -} - -fn button<'a>(state: &'a mut button::State, text: &str) -> Button<'a, Message> { - Button::new(state, Text::new(text)) - .padding(10) - .style(style::Button::Primary) -} - -mod style { - use iced::{button, Background, Color, Vector}; - - pub enum Button { - Primary, - } - - impl button::StyleSheet for Button { - fn active(&self) -> button::Style { - button::Style { - background: Some(Background::Color(match self { - Button::Primary => Color::from_rgb(0.11, 0.42, 0.87), - })), - border_radius: 12, - shadow_offset: Vector::new(1.0, 1.0), - text_color: Color::WHITE, - ..button::Style::default() - } - } - } -} diff --git a/examples/pokedex/Cargo.toml b/examples/pokedex/Cargo.toml new file mode 100644 index 00000000..2972590f --- /dev/null +++ b/examples/pokedex/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "pokedex" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez "] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../.." } +iced_futures = { path = "../../futures", features = ["async-std"] } +surf = "1.0" +rand = "0.7" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" diff --git a/examples/pokedex/src/main.rs b/examples/pokedex/src/main.rs new file mode 100644 index 00000000..283437b2 --- /dev/null +++ b/examples/pokedex/src/main.rs @@ -0,0 +1,243 @@ +use iced::{ + button, futures, image, Align, Application, Button, Column, Command, + Container, Element, Image, Length, Row, Settings, Text, +}; + +pub fn main() { + Pokedex::run(Settings::default()) +} + +#[derive(Debug)] +enum Pokedex { + Loading, + Loaded { + pokemon: Pokemon, + search: button::State, + }, + Errored { + error: Error, + try_again: button::State, + }, +} + +#[derive(Debug, Clone)] +enum Message { + PokemonFound(Result), + Search, +} + +impl Application for Pokedex { + type Executor = iced_futures::executor::AsyncStd; + type Message = Message; + + fn new() -> (Pokedex, Command) { + ( + Pokedex::Loading, + Command::perform(Pokemon::search(), Message::PokemonFound), + ) + } + + fn title(&self) -> String { + let subtitle = match self { + Pokedex::Loading => "Loading", + Pokedex::Loaded { pokemon, .. } => &pokemon.name, + Pokedex::Errored { .. } => "Whoops!", + }; + + format!("{} - Pokédex", subtitle) + } + + fn update(&mut self, message: Message) -> Command { + match message { + Message::PokemonFound(Ok(pokemon)) => { + *self = Pokedex::Loaded { + pokemon, + search: button::State::new(), + }; + + Command::none() + } + Message::PokemonFound(Err(error)) => { + *self = Pokedex::Errored { + error, + try_again: button::State::new(), + }; + + Command::none() + } + Message::Search => match self { + Pokedex::Loading => Command::none(), + _ => { + *self = Pokedex::Loading; + + Command::perform(Pokemon::search(), Message::PokemonFound) + } + }, + } + } + + fn view(&mut self) -> Element { + let content = match self { + Pokedex::Loading => Column::new() + .push(Text::new("Searching for Pokémon...").size(40)), + Pokedex::Loaded { pokemon, search } => Column::new() + .max_width(500) + .spacing(20) + .align_items(Align::End) + .push(pokemon.view()) + .push( + button(search, "Keep searching!").on_press(Message::Search), + ), + Pokedex::Errored { try_again, .. } => Column::new() + .spacing(20) + .align_items(Align::End) + .push(Text::new("Whoops! Something went wrong...").size(40)) + .push(button(try_again, "Try again").on_press(Message::Search)), + }; + + Container::new(content) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .into() + } +} + +#[derive(Debug, Clone)] +struct Pokemon { + number: u16, + name: String, + description: String, + image: image::Handle, +} + +impl Pokemon { + const TOTAL: u16 = 807; + + fn view(&self) -> Element { + Row::new() + .spacing(20) + .align_items(Align::Center) + .push(Image::new(self.image.clone())) + .push( + Column::new() + .spacing(20) + .push( + Row::new() + .align_items(Align::Center) + .spacing(20) + .push( + Text::new(&self.name) + .size(30) + .width(Length::Fill), + ) + .push( + Text::new(format!("#{}", self.number)) + .size(20) + .color([0.5, 0.5, 0.5]), + ), + ) + .push(Text::new(&self.description)), + ) + .into() + } + + async fn search() -> Result { + use rand::Rng; + use serde::Deserialize; + + #[derive(Debug, Deserialize)] + struct Entry { + id: u32, + name: String, + flavor_text_entries: Vec, + } + + #[derive(Debug, Deserialize)] + struct FlavorText { + flavor_text: String, + language: Language, + } + + #[derive(Debug, Deserialize)] + struct Language { + name: String, + } + + let id = { + let mut rng = rand::thread_rng(); + + rng.gen_range(0, Pokemon::TOTAL) + }; + + 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, sprite): (Entry, _) = futures::future::try_join( + surf::get(&url).recv_json(), + surf::get(&sprite).recv_bytes(), + ) + .await?; + + let description = entry + .flavor_text_entries + .iter() + .filter(|text| text.language.name == "en") + .next() + .ok_or(Error::LanguageError)?; + + Ok(Pokemon { + number: id, + name: entry.name.to_uppercase(), + description: description + .flavor_text + .chars() + .map(|c| if c.is_control() { ' ' } else { c }) + .collect(), + image: image::Handle::from_memory(sprite), + }) + } +} + +#[derive(Debug, Clone)] +enum Error { + APIError, + LanguageError, +} + +impl From for Error { + fn from(exception: surf::Exception) -> Error { + dbg!(&exception); + + Error::APIError + } +} + +fn button<'a>(state: &'a mut button::State, text: &str) -> Button<'a, Message> { + Button::new(state, Text::new(text)) + .padding(10) + .style(style::Button::Primary) +} + +mod style { + use iced::{button, Background, Color, Vector}; + + pub enum Button { + Primary, + } + + impl button::StyleSheet for Button { + fn active(&self) -> button::Style { + button::Style { + background: Some(Background::Color(match self { + Button::Primary => Color::from_rgb(0.11, 0.42, 0.87), + })), + border_radius: 12, + shadow_offset: Vector::new(1.0, 1.0), + text_color: Color::WHITE, + ..button::Style::default() + } + } + } +} diff --git a/examples/progress_bar.rs b/examples/progress_bar.rs deleted file mode 100644 index 43b09928..00000000 --- a/examples/progress_bar.rs +++ /dev/null @@ -1,47 +0,0 @@ -use iced::{slider, Column, Element, ProgressBar, Sandbox, Settings, Slider}; - -pub fn main() { - Progress::run(Settings::default()) -} - -#[derive(Default)] -struct Progress { - value: f32, - progress_bar_slider: slider::State, -} - -#[derive(Debug, Clone, Copy)] -enum Message { - SliderChanged(f32), -} - -impl Sandbox for Progress { - type Message = Message; - - fn new() -> Self { - Self::default() - } - - fn title(&self) -> String { - String::from("A simple Progressbar") - } - - fn update(&mut self, message: Message) { - match message { - Message::SliderChanged(x) => self.value = x, - } - } - - fn view(&mut self) -> Element { - Column::new() - .padding(20) - .push(ProgressBar::new(0.0..=100.0, self.value)) - .push(Slider::new( - &mut self.progress_bar_slider, - 0.0..=100.0, - self.value, - Message::SliderChanged, - )) - .into() - } -} diff --git a/examples/progress_bar/Cargo.toml b/examples/progress_bar/Cargo.toml new file mode 100644 index 00000000..4eccbf14 --- /dev/null +++ b/examples/progress_bar/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "progress_bar" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez "] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../.." } diff --git a/examples/progress_bar/src/main.rs b/examples/progress_bar/src/main.rs new file mode 100644 index 00000000..43b09928 --- /dev/null +++ b/examples/progress_bar/src/main.rs @@ -0,0 +1,47 @@ +use iced::{slider, Column, Element, ProgressBar, Sandbox, Settings, Slider}; + +pub fn main() { + Progress::run(Settings::default()) +} + +#[derive(Default)] +struct Progress { + value: f32, + progress_bar_slider: slider::State, +} + +#[derive(Debug, Clone, Copy)] +enum Message { + SliderChanged(f32), +} + +impl Sandbox for Progress { + type Message = Message; + + fn new() -> Self { + Self::default() + } + + fn title(&self) -> String { + String::from("A simple Progressbar") + } + + fn update(&mut self, message: Message) { + match message { + Message::SliderChanged(x) => self.value = x, + } + } + + fn view(&mut self) -> Element { + Column::new() + .padding(20) + .push(ProgressBar::new(0.0..=100.0, self.value)) + .push(Slider::new( + &mut self.progress_bar_slider, + 0.0..=100.0, + self.value, + Message::SliderChanged, + )) + .into() + } +} diff --git a/examples/resources/ferris.png b/examples/resources/ferris.png deleted file mode 100644 index ebce1a14..00000000 Binary files a/examples/resources/ferris.png and /dev/null differ diff --git a/examples/resources/icons.ttf b/examples/resources/icons.ttf deleted file mode 100644 index 4498299d..00000000 Binary files a/examples/resources/icons.ttf and /dev/null differ diff --git a/examples/resources/tiger.svg b/examples/resources/tiger.svg deleted file mode 100644 index 679edec2..00000000 --- a/examples/resources/tiger.svg +++ /dev/null @@ -1,725 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/examples/stopwatch.rs b/examples/stopwatch.rs deleted file mode 100644 index 6e357039..00000000 --- a/examples/stopwatch.rs +++ /dev/null @@ -1,204 +0,0 @@ -use iced::{ - button, Align, Application, Button, Column, Command, Container, Element, - HorizontalAlignment, Length, Row, Settings, Subscription, Text, -}; -use std::time::{Duration, Instant}; - -pub fn main() { - Stopwatch::run(Settings::default()) -} - -struct Stopwatch { - 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 Stopwatch { - type Executor = iced_futures::executor::AsyncStd; - type Message = Message; - - fn new() -> (Stopwatch, Command) { - ( - Stopwatch { - duration: Duration::default(), - state: State::Idle, - toggle: button::State::new(), - reset: button::State::new(), - }, - Command::none(), - ) - } - - fn title(&self) -> String { - String::from("Stopwatch - 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 subscription(&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 % HOUR) / MINUTE, - seconds % MINUTE, - self.duration.subsec_millis() / 10, - )) - .size(40); - - let button = |state, label, style| { - Button::new( - state, - Text::new(label) - .horizontal_alignment(HorizontalAlignment::Center), - ) - .min_width(80) - .padding(10) - .style(style) - }; - - let toggle_button = { - let (label, color) = match self.state { - State::Idle => ("Start", style::Button::Primary), - State::Ticking { .. } => ("Stop", style::Button::Destructive), - }; - - button(&mut self.toggle, label, color).on_press(Message::Toggle) - }; - - let reset_button = - button(&mut self.reset, "Reset", style::Button::Secondary) - .on_press(Message::Reset); - - let controls = Row::new() - .spacing(20) - .push(toggle_button) - .push(reset_button); - - let content = Column::new() - .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 - where - H: std::hash::Hasher, - { - type Output = std::time::Instant; - - fn hash(&self, state: &mut H) { - use std::hash::Hash; - - std::any::TypeId::of::().hash(state); - self.0.hash(state); - } - - fn stream( - self: Box, - _input: futures::stream::BoxStream<'static, I>, - ) -> futures::stream::BoxStream<'static, Self::Output> { - use futures::stream::StreamExt; - - async_std::stream::interval(self.0) - .map(|_| std::time::Instant::now()) - .boxed() - } - } -} - -mod style { - use iced::{button, Background, Color, Vector}; - - pub enum Button { - Primary, - Secondary, - Destructive, - } - - impl button::StyleSheet for Button { - fn active(&self) -> button::Style { - button::Style { - background: Some(Background::Color(match self { - Button::Primary => Color::from_rgb(0.11, 0.42, 0.87), - Button::Secondary => Color::from_rgb(0.5, 0.5, 0.5), - Button::Destructive => Color::from_rgb(0.8, 0.2, 0.2), - })), - border_radius: 12, - shadow_offset: Vector::new(1.0, 1.0), - text_color: Color::WHITE, - ..button::Style::default() - } - } - } -} diff --git a/examples/stopwatch/Cargo.toml b/examples/stopwatch/Cargo.toml new file mode 100644 index 00000000..1dae3b83 --- /dev/null +++ b/examples/stopwatch/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "stopwatch" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez "] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../.." } +iced_native = { path = "../../native" } +iced_futures = { path = "../../futures", features = ["async-std"] } +async-std = { version = "1.0", features = ["unstable"] } diff --git a/examples/stopwatch/src/main.rs b/examples/stopwatch/src/main.rs new file mode 100644 index 00000000..d84c4817 --- /dev/null +++ b/examples/stopwatch/src/main.rs @@ -0,0 +1,206 @@ +use iced::{ + button, Align, Application, Button, Column, Command, Container, Element, + HorizontalAlignment, Length, Row, Settings, Subscription, Text, +}; +use std::time::{Duration, Instant}; + +pub fn main() { + Stopwatch::run(Settings::default()) +} + +struct Stopwatch { + 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 Stopwatch { + type Executor = iced_futures::executor::AsyncStd; + type Message = Message; + + fn new() -> (Stopwatch, Command) { + ( + Stopwatch { + duration: Duration::default(), + state: State::Idle, + toggle: button::State::new(), + reset: button::State::new(), + }, + Command::none(), + ) + } + + fn title(&self) -> String { + String::from("Stopwatch - 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 subscription(&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 % HOUR) / MINUTE, + seconds % MINUTE, + self.duration.subsec_millis() / 10, + )) + .size(40); + + let button = |state, label, style| { + Button::new( + state, + Text::new(label) + .horizontal_alignment(HorizontalAlignment::Center), + ) + .min_width(80) + .padding(10) + .style(style) + }; + + let toggle_button = { + let (label, color) = match self.state { + State::Idle => ("Start", style::Button::Primary), + State::Ticking { .. } => ("Stop", style::Button::Destructive), + }; + + button(&mut self.toggle, label, color).on_press(Message::Toggle) + }; + + let reset_button = + button(&mut self.reset, "Reset", style::Button::Secondary) + .on_press(Message::Reset); + + let controls = Row::new() + .spacing(20) + .push(toggle_button) + .push(reset_button); + + let content = Column::new() + .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 { + use iced::futures; + + 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 + where + H: std::hash::Hasher, + { + type Output = std::time::Instant; + + fn hash(&self, state: &mut H) { + use std::hash::Hash; + + std::any::TypeId::of::().hash(state); + self.0.hash(state); + } + + fn stream( + self: Box, + _input: futures::stream::BoxStream<'static, I>, + ) -> futures::stream::BoxStream<'static, Self::Output> { + use futures::stream::StreamExt; + + async_std::stream::interval(self.0) + .map(|_| std::time::Instant::now()) + .boxed() + } + } +} + +mod style { + use iced::{button, Background, Color, Vector}; + + pub enum Button { + Primary, + Secondary, + Destructive, + } + + impl button::StyleSheet for Button { + fn active(&self) -> button::Style { + button::Style { + background: Some(Background::Color(match self { + Button::Primary => Color::from_rgb(0.11, 0.42, 0.87), + Button::Secondary => Color::from_rgb(0.5, 0.5, 0.5), + Button::Destructive => Color::from_rgb(0.8, 0.2, 0.2), + })), + border_radius: 12, + shadow_offset: Vector::new(1.0, 1.0), + text_color: Color::WHITE, + ..button::Style::default() + } + } + } +} diff --git a/examples/styling.rs b/examples/styling.rs deleted file mode 100644 index 50095ec7..00000000 --- a/examples/styling.rs +++ /dev/null @@ -1,514 +0,0 @@ -use iced::{ - button, scrollable, slider, text_input, Align, Button, Checkbox, Column, - Container, Element, Length, ProgressBar, Radio, Row, Sandbox, Scrollable, - Settings, Slider, Space, Text, TextInput, -}; - -pub fn main() { - Styling::run(Settings::default()) -} - -#[derive(Default)] -struct Styling { - theme: style::Theme, - scroll: scrollable::State, - input: text_input::State, - input_value: String, - button: button::State, - slider: slider::State, - slider_value: f32, - toggle_value: bool, -} - -#[derive(Debug, Clone)] -enum Message { - ThemeChanged(style::Theme), - InputChanged(String), - ButtonPressed, - SliderChanged(f32), - CheckboxToggled(bool), -} - -impl Sandbox for Styling { - type Message = Message; - - fn new() -> Self { - Styling::default() - } - - fn title(&self) -> String { - String::from("Styling - Iced") - } - - fn update(&mut self, message: Message) { - match message { - Message::ThemeChanged(theme) => self.theme = theme, - Message::InputChanged(value) => self.input_value = value, - Message::ButtonPressed => (), - Message::SliderChanged(value) => self.slider_value = value, - Message::CheckboxToggled(value) => self.toggle_value = value, - } - } - - fn view(&mut self) -> Element { - let choose_theme = style::Theme::ALL.iter().fold( - Column::new().spacing(10).push(Text::new("Choose a theme:")), - |column, theme| { - column.push( - Radio::new( - *theme, - &format!("{:?}", theme), - Some(self.theme), - Message::ThemeChanged, - ) - .style(self.theme), - ) - }, - ); - - let text_input = TextInput::new( - &mut self.input, - "Type something...", - &self.input_value, - Message::InputChanged, - ) - .padding(10) - .size(20) - .style(self.theme); - - let button = Button::new(&mut self.button, Text::new("Submit")) - .padding(10) - .on_press(Message::ButtonPressed) - .style(self.theme); - - let slider = Slider::new( - &mut self.slider, - 0.0..=100.0, - self.slider_value, - Message::SliderChanged, - ) - .style(self.theme); - - let progress_bar = - ProgressBar::new(0.0..=100.0, self.slider_value).style(self.theme); - - let scrollable = Scrollable::new(&mut self.scroll) - .height(Length::Units(100)) - .style(self.theme) - .push(Text::new("Scroll me!")) - .push(Space::with_height(Length::Units(800))) - .push(Text::new("You did it!")); - - let checkbox = Checkbox::new( - self.toggle_value, - "Toggle me!", - Message::CheckboxToggled, - ) - .style(self.theme); - - let content = Column::new() - .spacing(20) - .padding(20) - .max_width(600) - .push(choose_theme) - .push(Row::new().spacing(10).push(text_input).push(button)) - .push(slider) - .push(progress_bar) - .push( - Row::new() - .spacing(10) - .align_items(Align::Center) - .push(scrollable) - .push(checkbox), - ); - - Container::new(content) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y() - .style(self.theme) - .into() - } -} - -mod style { - use iced::{ - button, checkbox, container, progress_bar, radio, scrollable, slider, - text_input, - }; - - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub enum Theme { - Light, - Dark, - } - - impl Theme { - pub const ALL: [Theme; 2] = [Theme::Light, Theme::Dark]; - } - - impl Default for Theme { - fn default() -> Theme { - Theme::Light - } - } - - impl From for Box { - fn from(theme: Theme) -> Self { - match theme { - Theme::Light => Default::default(), - Theme::Dark => dark::Container.into(), - } - } - } - - impl From for Box { - fn from(theme: Theme) -> Self { - match theme { - Theme::Light => Default::default(), - Theme::Dark => dark::Radio.into(), - } - } - } - - impl From for Box { - fn from(theme: Theme) -> Self { - match theme { - Theme::Light => Default::default(), - Theme::Dark => dark::TextInput.into(), - } - } - } - - impl From for Box { - fn from(theme: Theme) -> Self { - match theme { - Theme::Light => light::Button.into(), - Theme::Dark => dark::Button.into(), - } - } - } - - impl From for Box { - fn from(theme: Theme) -> Self { - match theme { - Theme::Light => Default::default(), - Theme::Dark => dark::Scrollable.into(), - } - } - } - - impl From for Box { - fn from(theme: Theme) -> Self { - match theme { - Theme::Light => Default::default(), - Theme::Dark => dark::Slider.into(), - } - } - } - - impl From for Box { - fn from(theme: Theme) -> Self { - match theme { - Theme::Light => Default::default(), - Theme::Dark => dark::ProgressBar.into(), - } - } - } - - impl From for Box { - fn from(theme: Theme) -> Self { - match theme { - Theme::Light => Default::default(), - Theme::Dark => dark::Checkbox.into(), - } - } - } - - mod light { - use iced::{button, Background, Color, Vector}; - - pub struct Button; - - impl button::StyleSheet for Button { - fn active(&self) -> button::Style { - button::Style { - background: Some(Background::Color(Color::from_rgb( - 0.11, 0.42, 0.87, - ))), - border_radius: 12, - shadow_offset: Vector::new(1.0, 1.0), - text_color: Color::from_rgb8(0xEE, 0xEE, 0xEE), - ..button::Style::default() - } - } - - fn hovered(&self) -> button::Style { - button::Style { - text_color: Color::WHITE, - shadow_offset: Vector::new(1.0, 2.0), - ..self.active() - } - } - } - } - - mod dark { - use iced::{ - button, checkbox, container, progress_bar, radio, scrollable, - slider, text_input, Background, Color, - }; - - const SURFACE: Color = Color::from_rgb( - 0x40 as f32 / 255.0, - 0x44 as f32 / 255.0, - 0x4B as f32 / 255.0, - ); - - const ACCENT: Color = Color::from_rgb( - 0x6F as f32 / 255.0, - 0xFF as f32 / 255.0, - 0xE9 as f32 / 255.0, - ); - - const ACTIVE: Color = Color::from_rgb( - 0x72 as f32 / 255.0, - 0x89 as f32 / 255.0, - 0xDA as f32 / 255.0, - ); - - const HOVERED: Color = Color::from_rgb( - 0x67 as f32 / 255.0, - 0x7B as f32 / 255.0, - 0xC4 as f32 / 255.0, - ); - - pub struct Container; - - impl container::StyleSheet for Container { - fn style(&self) -> container::Style { - container::Style { - background: Some(Background::Color(Color::from_rgb8( - 0x36, 0x39, 0x3F, - ))), - text_color: Some(Color::WHITE), - ..container::Style::default() - } - } - } - - pub struct Radio; - - impl radio::StyleSheet for Radio { - fn active(&self) -> radio::Style { - radio::Style { - background: Background::Color(SURFACE), - dot_color: ACTIVE, - border_width: 1, - border_color: ACTIVE, - } - } - - fn hovered(&self) -> radio::Style { - radio::Style { - background: Background::Color(Color { a: 0.5, ..SURFACE }), - ..self.active() - } - } - } - - pub struct TextInput; - - impl text_input::StyleSheet for TextInput { - fn active(&self) -> text_input::Style { - text_input::Style { - background: Background::Color(SURFACE), - border_radius: 2, - border_width: 0, - border_color: Color::TRANSPARENT, - } - } - - fn focused(&self) -> text_input::Style { - text_input::Style { - border_width: 1, - border_color: ACCENT, - ..self.active() - } - } - - fn hovered(&self) -> text_input::Style { - text_input::Style { - border_width: 1, - border_color: Color { a: 0.3, ..ACCENT }, - ..self.focused() - } - } - - fn placeholder_color(&self) -> Color { - Color::from_rgb(0.4, 0.4, 0.4) - } - - fn value_color(&self) -> Color { - Color::WHITE - } - } - - pub struct Button; - - impl button::StyleSheet for Button { - fn active(&self) -> button::Style { - button::Style { - background: Some(Background::Color(ACTIVE)), - border_radius: 3, - text_color: Color::WHITE, - ..button::Style::default() - } - } - - fn hovered(&self) -> button::Style { - button::Style { - background: Some(Background::Color(HOVERED)), - text_color: Color::WHITE, - ..self.active() - } - } - - fn pressed(&self) -> button::Style { - button::Style { - border_width: 1, - border_color: Color::WHITE, - ..self.hovered() - } - } - } - - pub struct Scrollable; - - impl scrollable::StyleSheet for Scrollable { - fn active(&self) -> scrollable::Scrollbar { - scrollable::Scrollbar { - background: Some(Background::Color(SURFACE)), - border_radius: 2, - border_width: 0, - border_color: Color::TRANSPARENT, - scroller: scrollable::Scroller { - color: ACTIVE, - border_radius: 2, - border_width: 0, - border_color: Color::TRANSPARENT, - }, - } - } - - fn hovered(&self) -> scrollable::Scrollbar { - let active = self.active(); - - scrollable::Scrollbar { - background: Some(Background::Color(Color { - a: 0.5, - ..SURFACE - })), - scroller: scrollable::Scroller { - color: HOVERED, - ..active.scroller - }, - ..active - } - } - - fn dragging(&self) -> scrollable::Scrollbar { - let hovered = self.hovered(); - - scrollable::Scrollbar { - scroller: scrollable::Scroller { - color: Color::from_rgb(0.85, 0.85, 0.85), - ..hovered.scroller - }, - ..hovered - } - } - } - - pub struct Slider; - - impl slider::StyleSheet for Slider { - fn active(&self) -> slider::Style { - slider::Style { - rail_colors: (ACTIVE, Color { a: 0.1, ..ACTIVE }), - handle: slider::Handle { - shape: slider::HandleShape::Circle { radius: 9 }, - color: ACTIVE, - border_width: 0, - border_color: Color::TRANSPARENT, - }, - } - } - - fn hovered(&self) -> slider::Style { - let active = self.active(); - - slider::Style { - handle: slider::Handle { - color: HOVERED, - ..active.handle - }, - ..active - } - } - - fn dragging(&self) -> slider::Style { - let active = self.active(); - - slider::Style { - handle: slider::Handle { - color: Color::from_rgb(0.85, 0.85, 0.85), - ..active.handle - }, - ..active - } - } - } - - pub struct ProgressBar; - - impl progress_bar::StyleSheet for ProgressBar { - fn style(&self) -> progress_bar::Style { - progress_bar::Style { - background: Background::Color(SURFACE), - bar: Background::Color(ACTIVE), - border_radius: 10, - } - } - } - - pub struct Checkbox; - - impl checkbox::StyleSheet for Checkbox { - fn active(&self, is_checked: bool) -> checkbox::Style { - checkbox::Style { - background: Background::Color(if is_checked { - ACTIVE - } else { - SURFACE - }), - checkmark_color: Color::WHITE, - border_radius: 2, - border_width: 1, - border_color: ACTIVE, - } - } - - fn hovered(&self, is_checked: bool) -> checkbox::Style { - checkbox::Style { - background: Background::Color(Color { - a: 0.8, - ..if is_checked { ACTIVE } else { SURFACE } - }), - ..self.active(is_checked) - } - } - } - } -} diff --git a/examples/styling/Cargo.toml b/examples/styling/Cargo.toml new file mode 100644 index 00000000..eb729f93 --- /dev/null +++ b/examples/styling/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "styling" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez "] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../.." } diff --git a/examples/styling/src/main.rs b/examples/styling/src/main.rs new file mode 100644 index 00000000..50095ec7 --- /dev/null +++ b/examples/styling/src/main.rs @@ -0,0 +1,514 @@ +use iced::{ + button, scrollable, slider, text_input, Align, Button, Checkbox, Column, + Container, Element, Length, ProgressBar, Radio, Row, Sandbox, Scrollable, + Settings, Slider, Space, Text, TextInput, +}; + +pub fn main() { + Styling::run(Settings::default()) +} + +#[derive(Default)] +struct Styling { + theme: style::Theme, + scroll: scrollable::State, + input: text_input::State, + input_value: String, + button: button::State, + slider: slider::State, + slider_value: f32, + toggle_value: bool, +} + +#[derive(Debug, Clone)] +enum Message { + ThemeChanged(style::Theme), + InputChanged(String), + ButtonPressed, + SliderChanged(f32), + CheckboxToggled(bool), +} + +impl Sandbox for Styling { + type Message = Message; + + fn new() -> Self { + Styling::default() + } + + fn title(&self) -> String { + String::from("Styling - Iced") + } + + fn update(&mut self, message: Message) { + match message { + Message::ThemeChanged(theme) => self.theme = theme, + Message::InputChanged(value) => self.input_value = value, + Message::ButtonPressed => (), + Message::SliderChanged(value) => self.slider_value = value, + Message::CheckboxToggled(value) => self.toggle_value = value, + } + } + + fn view(&mut self) -> Element { + let choose_theme = style::Theme::ALL.iter().fold( + Column::new().spacing(10).push(Text::new("Choose a theme:")), + |column, theme| { + column.push( + Radio::new( + *theme, + &format!("{:?}", theme), + Some(self.theme), + Message::ThemeChanged, + ) + .style(self.theme), + ) + }, + ); + + let text_input = TextInput::new( + &mut self.input, + "Type something...", + &self.input_value, + Message::InputChanged, + ) + .padding(10) + .size(20) + .style(self.theme); + + let button = Button::new(&mut self.button, Text::new("Submit")) + .padding(10) + .on_press(Message::ButtonPressed) + .style(self.theme); + + let slider = Slider::new( + &mut self.slider, + 0.0..=100.0, + self.slider_value, + Message::SliderChanged, + ) + .style(self.theme); + + let progress_bar = + ProgressBar::new(0.0..=100.0, self.slider_value).style(self.theme); + + let scrollable = Scrollable::new(&mut self.scroll) + .height(Length::Units(100)) + .style(self.theme) + .push(Text::new("Scroll me!")) + .push(Space::with_height(Length::Units(800))) + .push(Text::new("You did it!")); + + let checkbox = Checkbox::new( + self.toggle_value, + "Toggle me!", + Message::CheckboxToggled, + ) + .style(self.theme); + + let content = Column::new() + .spacing(20) + .padding(20) + .max_width(600) + .push(choose_theme) + .push(Row::new().spacing(10).push(text_input).push(button)) + .push(slider) + .push(progress_bar) + .push( + Row::new() + .spacing(10) + .align_items(Align::Center) + .push(scrollable) + .push(checkbox), + ); + + Container::new(content) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .style(self.theme) + .into() + } +} + +mod style { + use iced::{ + button, checkbox, container, progress_bar, radio, scrollable, slider, + text_input, + }; + + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub enum Theme { + Light, + Dark, + } + + impl Theme { + pub const ALL: [Theme; 2] = [Theme::Light, Theme::Dark]; + } + + impl Default for Theme { + fn default() -> Theme { + Theme::Light + } + } + + impl From for Box { + fn from(theme: Theme) -> Self { + match theme { + Theme::Light => Default::default(), + Theme::Dark => dark::Container.into(), + } + } + } + + impl From for Box { + fn from(theme: Theme) -> Self { + match theme { + Theme::Light => Default::default(), + Theme::Dark => dark::Radio.into(), + } + } + } + + impl From for Box { + fn from(theme: Theme) -> Self { + match theme { + Theme::Light => Default::default(), + Theme::Dark => dark::TextInput.into(), + } + } + } + + impl From for Box { + fn from(theme: Theme) -> Self { + match theme { + Theme::Light => light::Button.into(), + Theme::Dark => dark::Button.into(), + } + } + } + + impl From for Box { + fn from(theme: Theme) -> Self { + match theme { + Theme::Light => Default::default(), + Theme::Dark => dark::Scrollable.into(), + } + } + } + + impl From for Box { + fn from(theme: Theme) -> Self { + match theme { + Theme::Light => Default::default(), + Theme::Dark => dark::Slider.into(), + } + } + } + + impl From for Box { + fn from(theme: Theme) -> Self { + match theme { + Theme::Light => Default::default(), + Theme::Dark => dark::ProgressBar.into(), + } + } + } + + impl From for Box { + fn from(theme: Theme) -> Self { + match theme { + Theme::Light => Default::default(), + Theme::Dark => dark::Checkbox.into(), + } + } + } + + mod light { + use iced::{button, Background, Color, Vector}; + + pub struct Button; + + impl button::StyleSheet for Button { + fn active(&self) -> button::Style { + button::Style { + background: Some(Background::Color(Color::from_rgb( + 0.11, 0.42, 0.87, + ))), + border_radius: 12, + shadow_offset: Vector::new(1.0, 1.0), + text_color: Color::from_rgb8(0xEE, 0xEE, 0xEE), + ..button::Style::default() + } + } + + fn hovered(&self) -> button::Style { + button::Style { + text_color: Color::WHITE, + shadow_offset: Vector::new(1.0, 2.0), + ..self.active() + } + } + } + } + + mod dark { + use iced::{ + button, checkbox, container, progress_bar, radio, scrollable, + slider, text_input, Background, Color, + }; + + const SURFACE: Color = Color::from_rgb( + 0x40 as f32 / 255.0, + 0x44 as f32 / 255.0, + 0x4B as f32 / 255.0, + ); + + const ACCENT: Color = Color::from_rgb( + 0x6F as f32 / 255.0, + 0xFF as f32 / 255.0, + 0xE9 as f32 / 255.0, + ); + + const ACTIVE: Color = Color::from_rgb( + 0x72 as f32 / 255.0, + 0x89 as f32 / 255.0, + 0xDA as f32 / 255.0, + ); + + const HOVERED: Color = Color::from_rgb( + 0x67 as f32 / 255.0, + 0x7B as f32 / 255.0, + 0xC4 as f32 / 255.0, + ); + + pub struct Container; + + impl container::StyleSheet for Container { + fn style(&self) -> container::Style { + container::Style { + background: Some(Background::Color(Color::from_rgb8( + 0x36, 0x39, 0x3F, + ))), + text_color: Some(Color::WHITE), + ..container::Style::default() + } + } + } + + pub struct Radio; + + impl radio::StyleSheet for Radio { + fn active(&self) -> radio::Style { + radio::Style { + background: Background::Color(SURFACE), + dot_color: ACTIVE, + border_width: 1, + border_color: ACTIVE, + } + } + + fn hovered(&self) -> radio::Style { + radio::Style { + background: Background::Color(Color { a: 0.5, ..SURFACE }), + ..self.active() + } + } + } + + pub struct TextInput; + + impl text_input::StyleSheet for TextInput { + fn active(&self) -> text_input::Style { + text_input::Style { + background: Background::Color(SURFACE), + border_radius: 2, + border_width: 0, + border_color: Color::TRANSPARENT, + } + } + + fn focused(&self) -> text_input::Style { + text_input::Style { + border_width: 1, + border_color: ACCENT, + ..self.active() + } + } + + fn hovered(&self) -> text_input::Style { + text_input::Style { + border_width: 1, + border_color: Color { a: 0.3, ..ACCENT }, + ..self.focused() + } + } + + fn placeholder_color(&self) -> Color { + Color::from_rgb(0.4, 0.4, 0.4) + } + + fn value_color(&self) -> Color { + Color::WHITE + } + } + + pub struct Button; + + impl button::StyleSheet for Button { + fn active(&self) -> button::Style { + button::Style { + background: Some(Background::Color(ACTIVE)), + border_radius: 3, + text_color: Color::WHITE, + ..button::Style::default() + } + } + + fn hovered(&self) -> button::Style { + button::Style { + background: Some(Background::Color(HOVERED)), + text_color: Color::WHITE, + ..self.active() + } + } + + fn pressed(&self) -> button::Style { + button::Style { + border_width: 1, + border_color: Color::WHITE, + ..self.hovered() + } + } + } + + pub struct Scrollable; + + impl scrollable::StyleSheet for Scrollable { + fn active(&self) -> scrollable::Scrollbar { + scrollable::Scrollbar { + background: Some(Background::Color(SURFACE)), + border_radius: 2, + border_width: 0, + border_color: Color::TRANSPARENT, + scroller: scrollable::Scroller { + color: ACTIVE, + border_radius: 2, + border_width: 0, + border_color: Color::TRANSPARENT, + }, + } + } + + fn hovered(&self) -> scrollable::Scrollbar { + let active = self.active(); + + scrollable::Scrollbar { + background: Some(Background::Color(Color { + a: 0.5, + ..SURFACE + })), + scroller: scrollable::Scroller { + color: HOVERED, + ..active.scroller + }, + ..active + } + } + + fn dragging(&self) -> scrollable::Scrollbar { + let hovered = self.hovered(); + + scrollable::Scrollbar { + scroller: scrollable::Scroller { + color: Color::from_rgb(0.85, 0.85, 0.85), + ..hovered.scroller + }, + ..hovered + } + } + } + + pub struct Slider; + + impl slider::StyleSheet for Slider { + fn active(&self) -> slider::Style { + slider::Style { + rail_colors: (ACTIVE, Color { a: 0.1, ..ACTIVE }), + handle: slider::Handle { + shape: slider::HandleShape::Circle { radius: 9 }, + color: ACTIVE, + border_width: 0, + border_color: Color::TRANSPARENT, + }, + } + } + + fn hovered(&self) -> slider::Style { + let active = self.active(); + + slider::Style { + handle: slider::Handle { + color: HOVERED, + ..active.handle + }, + ..active + } + } + + fn dragging(&self) -> slider::Style { + let active = self.active(); + + slider::Style { + handle: slider::Handle { + color: Color::from_rgb(0.85, 0.85, 0.85), + ..active.handle + }, + ..active + } + } + } + + pub struct ProgressBar; + + impl progress_bar::StyleSheet for ProgressBar { + fn style(&self) -> progress_bar::Style { + progress_bar::Style { + background: Background::Color(SURFACE), + bar: Background::Color(ACTIVE), + border_radius: 10, + } + } + } + + pub struct Checkbox; + + impl checkbox::StyleSheet for Checkbox { + fn active(&self, is_checked: bool) -> checkbox::Style { + checkbox::Style { + background: Background::Color(if is_checked { + ACTIVE + } else { + SURFACE + }), + checkmark_color: Color::WHITE, + border_radius: 2, + border_width: 1, + border_color: ACTIVE, + } + } + + fn hovered(&self, is_checked: bool) -> checkbox::Style { + checkbox::Style { + background: Background::Color(Color { + a: 0.8, + ..if is_checked { ACTIVE } else { SURFACE } + }), + ..self.active(is_checked) + } + } + } + } +} diff --git a/examples/svg.rs b/examples/svg.rs deleted file mode 100644 index 1895039d..00000000 --- a/examples/svg.rs +++ /dev/null @@ -1,54 +0,0 @@ -use iced::{Container, Element, Length, Sandbox, Settings}; - -pub fn main() { - Tiger::run(Settings::default()) -} - -#[derive(Default)] -struct Tiger; - -impl Sandbox for Tiger { - type Message = (); - - fn new() -> Self { - Self::default() - } - - fn title(&self) -> String { - String::from("SVG - Iced") - } - - fn update(&mut self, _message: ()) {} - - fn view(&mut self) -> Element<()> { - #[cfg(feature = "svg")] - let content = { - use iced::{Column, Svg}; - - Column::new().padding(20).push( - Svg::new(format!( - "{}/examples/resources/tiger.svg", - env!("CARGO_MANIFEST_DIR") - )) - .width(Length::Fill) - .height(Length::Fill), - ) - }; - - #[cfg(not(feature = "svg"))] - let content = { - use iced::{HorizontalAlignment, Text}; - - Text::new("You need to enable the `svg` feature!") - .horizontal_alignment(HorizontalAlignment::Center) - .size(30) - }; - - Container::new(content) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y() - .into() - } -} diff --git a/examples/svg/Cargo.toml b/examples/svg/Cargo.toml new file mode 100644 index 00000000..d8f83ac2 --- /dev/null +++ b/examples/svg/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "svg" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez "] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../..", features = ["svg"] } diff --git a/examples/svg/resources/tiger.svg b/examples/svg/resources/tiger.svg new file mode 100644 index 00000000..679edec2 --- /dev/null +++ b/examples/svg/resources/tiger.svg @@ -0,0 +1,725 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/svg/src/main.rs b/examples/svg/src/main.rs new file mode 100644 index 00000000..57358e24 --- /dev/null +++ b/examples/svg/src/main.rs @@ -0,0 +1,37 @@ +use iced::{Column, Container, Element, Length, Sandbox, Settings, Svg}; + +pub fn main() { + Tiger::run(Settings::default()) +} + +#[derive(Default)] +struct Tiger; + +impl Sandbox for Tiger { + type Message = (); + + fn new() -> Self { + Self::default() + } + + fn title(&self) -> String { + String::from("SVG - Iced") + } + + fn update(&mut self, _message: ()) {} + + fn view(&mut self) -> Element<()> { + let content = Column::new().padding(20).push( + Svg::new(format!("{}/tiger.svg", env!("CARGO_MANIFEST_DIR"))) + .width(Length::Fill) + .height(Length::Fill), + ); + + Container::new(content) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .into() + } +} diff --git a/examples/todos.rs b/examples/todos.rs deleted file mode 100644 index 06595a1e..00000000 --- a/examples/todos.rs +++ /dev/null @@ -1,615 +0,0 @@ -use iced::{ - button, scrollable, text_input, Align, Application, Button, Checkbox, - Column, Command, Container, Element, Font, HorizontalAlignment, Length, - Row, Scrollable, Settings, Text, TextInput, -}; -use serde::{Deserialize, Serialize}; - -pub fn main() { - Todos::run(Settings::default()) -} - -#[derive(Debug)] -enum Todos { - Loading, - Loaded(State), -} - -#[derive(Debug, Default)] -struct State { - scroll: scrollable::State, - input: text_input::State, - input_value: String, - filter: Filter, - tasks: Vec, - controls: Controls, - dirty: bool, - saving: bool, -} - -#[derive(Debug, Clone)] -enum Message { - Loaded(Result), - Saved(Result<(), SaveError>), - InputChanged(String), - CreateTask, - FilterChanged(Filter), - TaskMessage(usize, TaskMessage), -} - -impl Application for Todos { - type Executor = iced_futures::executor::AsyncStd; - type Message = Message; - - fn new() -> (Todos, Command) { - ( - Todos::Loading, - Command::perform(SavedState::load(), Message::Loaded), - ) - } - - fn title(&self) -> String { - let dirty = match self { - Todos::Loading => false, - Todos::Loaded(state) => state.dirty, - }; - - format!("Todos{} - Iced", if dirty { "*" } else { "" }) - } - - fn update(&mut self, message: Message) -> Command { - match self { - Todos::Loading => { - match message { - Message::Loaded(Ok(state)) => { - *self = Todos::Loaded(State { - input_value: state.input_value, - filter: state.filter, - tasks: state.tasks, - ..State::default() - }); - } - Message::Loaded(Err(_)) => { - *self = Todos::Loaded(State::default()); - } - _ => {} - } - - Command::none() - } - Todos::Loaded(state) => { - let mut saved = false; - - match message { - Message::InputChanged(value) => { - state.input_value = value; - } - Message::CreateTask => { - if !state.input_value.is_empty() { - state - .tasks - .push(Task::new(state.input_value.clone())); - state.input_value.clear(); - } - } - Message::FilterChanged(filter) => { - state.filter = filter; - } - Message::TaskMessage(i, TaskMessage::Delete) => { - state.tasks.remove(i); - } - Message::TaskMessage(i, task_message) => { - if let Some(task) = state.tasks.get_mut(i) { - task.update(task_message); - } - } - Message::Saved(_) => { - state.saving = false; - saved = true; - } - _ => {} - } - - if !saved { - state.dirty = true; - } - - if state.dirty && !state.saving { - state.dirty = false; - state.saving = true; - - Command::perform( - SavedState { - input_value: state.input_value.clone(), - filter: state.filter, - tasks: state.tasks.clone(), - } - .save(), - Message::Saved, - ) - } else { - Command::none() - } - } - } - } - - fn view(&mut self) -> Element { - match self { - Todos::Loading => loading_message(), - Todos::Loaded(State { - scroll, - input, - input_value, - filter, - tasks, - controls, - .. - }) => { - let title = Text::new("todos") - .width(Length::Fill) - .size(100) - .color([0.5, 0.5, 0.5]) - .horizontal_alignment(HorizontalAlignment::Center); - - let input = TextInput::new( - input, - "What needs to be done?", - input_value, - Message::InputChanged, - ) - .padding(15) - .size(30) - .on_submit(Message::CreateTask); - - let controls = controls.view(&tasks, *filter); - let filtered_tasks = - tasks.iter().filter(|task| filter.matches(task)); - - let tasks: Element<_> = if filtered_tasks.count() > 0 { - tasks - .iter_mut() - .enumerate() - .filter(|(_, task)| filter.matches(task)) - .fold(Column::new().spacing(20), |column, (i, task)| { - column.push(task.view().map(move |message| { - Message::TaskMessage(i, message) - })) - }) - .into() - } else { - empty_message(match filter { - Filter::All => "You have not created a task yet...", - Filter::Active => "All your tasks are done! :D", - Filter::Completed => { - "You have not completed a task yet..." - } - }) - }; - - let content = Column::new() - .max_width(800) - .spacing(20) - .push(title) - .push(input) - .push(controls) - .push(tasks); - - Scrollable::new(scroll) - .padding(40) - .push( - Container::new(content).width(Length::Fill).center_x(), - ) - .into() - } - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct Task { - description: String, - completed: bool, - - #[serde(skip)] - state: TaskState, -} - -#[derive(Debug, Clone)] -pub enum TaskState { - Idle { - edit_button: button::State, - }, - Editing { - text_input: text_input::State, - delete_button: button::State, - }, -} - -impl Default for TaskState { - fn default() -> Self { - TaskState::Idle { - edit_button: button::State::new(), - } - } -} - -#[derive(Debug, Clone)] -pub enum TaskMessage { - Completed(bool), - Edit, - DescriptionEdited(String), - FinishEdition, - Delete, -} - -impl Task { - fn new(description: String) -> Self { - Task { - description, - completed: false, - state: TaskState::Idle { - edit_button: button::State::new(), - }, - } - } - - fn update(&mut self, message: TaskMessage) { - match message { - TaskMessage::Completed(completed) => { - self.completed = completed; - } - TaskMessage::Edit => { - self.state = TaskState::Editing { - text_input: text_input::State::focused(), - delete_button: button::State::new(), - }; - } - TaskMessage::DescriptionEdited(new_description) => { - self.description = new_description; - } - TaskMessage::FinishEdition => { - if !self.description.is_empty() { - self.state = TaskState::Idle { - edit_button: button::State::new(), - } - } - } - TaskMessage::Delete => {} - } - } - - fn view(&mut self) -> Element { - match &mut self.state { - TaskState::Idle { edit_button } => { - let checkbox = Checkbox::new( - self.completed, - &self.description, - TaskMessage::Completed, - ) - .width(Length::Fill); - - Row::new() - .spacing(20) - .align_items(Align::Center) - .push(checkbox) - .push( - Button::new(edit_button, edit_icon()) - .on_press(TaskMessage::Edit) - .padding(10) - .style(style::Button::Icon), - ) - .into() - } - TaskState::Editing { - text_input, - delete_button, - } => { - let text_input = TextInput::new( - text_input, - "Describe your task...", - &self.description, - TaskMessage::DescriptionEdited, - ) - .on_submit(TaskMessage::FinishEdition) - .padding(10); - - Row::new() - .spacing(20) - .align_items(Align::Center) - .push(text_input) - .push( - Button::new( - delete_button, - Row::new() - .spacing(10) - .push(delete_icon()) - .push(Text::new("Delete")), - ) - .on_press(TaskMessage::Delete) - .padding(10) - .style(style::Button::Destructive), - ) - .into() - } - } - } -} - -#[derive(Debug, Default, Clone)] -pub struct Controls { - all_button: button::State, - active_button: button::State, - completed_button: button::State, -} - -impl Controls { - fn view(&mut self, tasks: &[Task], current_filter: Filter) -> Row { - let Controls { - all_button, - active_button, - completed_button, - } = self; - - let tasks_left = tasks.iter().filter(|task| !task.completed).count(); - - let filter_button = |state, label, filter, current_filter| { - let label = Text::new(label).size(16); - let button = - Button::new(state, label).style(style::Button::Filter { - selected: filter == current_filter, - }); - - button.on_press(Message::FilterChanged(filter)).padding(8) - }; - - Row::new() - .spacing(20) - .align_items(Align::Center) - .push( - Text::new(&format!( - "{} {} left", - tasks_left, - if tasks_left == 1 { "task" } else { "tasks" } - )) - .width(Length::Fill) - .size(16), - ) - .push( - Row::new() - .spacing(10) - .push(filter_button( - all_button, - "All", - Filter::All, - current_filter, - )) - .push(filter_button( - active_button, - "Active", - Filter::Active, - current_filter, - )) - .push(filter_button( - completed_button, - "Completed", - Filter::Completed, - current_filter, - )), - ) - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum Filter { - All, - Active, - Completed, -} - -impl Default for Filter { - fn default() -> Self { - Filter::All - } -} - -impl Filter { - fn matches(&self, task: &Task) -> bool { - match self { - Filter::All => true, - Filter::Active => !task.completed, - Filter::Completed => task.completed, - } - } -} - -fn loading_message() -> Element<'static, Message> { - Container::new( - Text::new("Loading...") - .horizontal_alignment(HorizontalAlignment::Center) - .size(50), - ) - .width(Length::Fill) - .height(Length::Fill) - .center_y() - .into() -} - -fn empty_message(message: &str) -> Element<'static, Message> { - Container::new( - Text::new(message) - .size(25) - .horizontal_alignment(HorizontalAlignment::Center) - .color([0.7, 0.7, 0.7]), - ) - .width(Length::Fill) - .height(Length::Units(200)) - .center_y() - .into() -} - -// Fonts -const ICONS: Font = Font::External { - name: "Icons", - bytes: include_bytes!("resources/icons.ttf"), -}; - -fn icon(unicode: char) -> Text { - Text::new(&unicode.to_string()) - .font(ICONS) - .width(Length::Units(20)) - .horizontal_alignment(HorizontalAlignment::Center) - .size(20) -} - -fn edit_icon() -> Text { - icon('\u{F303}') -} - -fn delete_icon() -> Text { - icon('\u{F1F8}') -} - -// Persistence -#[derive(Debug, Clone, Serialize, Deserialize)] -struct SavedState { - input_value: String, - filter: Filter, - tasks: Vec, -} - -#[derive(Debug, Clone)] -enum LoadError { - FileError, - FormatError, -} - -#[derive(Debug, Clone)] -enum SaveError { - DirectoryError, - FileError, - WriteError, - FormatError, -} - -impl SavedState { - fn path() -> std::path::PathBuf { - let mut path = if let Some(project_dirs) = - directories::ProjectDirs::from("rs", "Iced", "Todos") - { - project_dirs.data_dir().into() - } else { - std::env::current_dir().unwrap_or(std::path::PathBuf::new()) - }; - - path.push("todos.json"); - - path - } - - async fn load() -> Result { - use async_std::prelude::*; - - let mut contents = String::new(); - - 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 async_std::prelude::*; - - let json = serde_json::to_string_pretty(&self) - .map_err(|_| SaveError::FormatError)?; - - let path = Self::path(); - - if let Some(dir) = path.parent() { - async_std::fs::create_dir_all(dir) - .await - .map_err(|_| SaveError::DirectoryError)?; - } - - { - let mut file = async_std::fs::File::create(path) - .await - .map_err(|_| SaveError::FileError)?; - - file.write_all(json.as_bytes()) - .await - .map_err(|_| SaveError::WriteError)?; - } - - // This is a simple way to save at most once every couple seconds - async_std::task::sleep(std::time::Duration::from_secs(2)).await; - - Ok(()) - } -} - -mod style { - use iced::{button, Background, Color, Vector}; - - pub enum Button { - Filter { selected: bool }, - Icon, - Destructive, - } - - impl button::StyleSheet for Button { - fn active(&self) -> button::Style { - match self { - Button::Filter { selected } => { - if *selected { - button::Style { - background: Some(Background::Color( - Color::from_rgb(0.2, 0.2, 0.7), - )), - border_radius: 10, - text_color: Color::WHITE, - ..button::Style::default() - } - } else { - button::Style::default() - } - } - Button::Icon => button::Style { - text_color: Color::from_rgb(0.5, 0.5, 0.5), - ..button::Style::default() - }, - Button::Destructive => button::Style { - background: Some(Background::Color(Color::from_rgb( - 0.8, 0.2, 0.2, - ))), - border_radius: 5, - text_color: Color::WHITE, - shadow_offset: Vector::new(1.0, 1.0), - ..button::Style::default() - }, - } - } - - fn hovered(&self) -> button::Style { - let active = self.active(); - - button::Style { - text_color: match self { - Button::Icon => Color::from_rgb(0.2, 0.2, 0.7), - Button::Filter { selected } if !selected => { - Color::from_rgb(0.2, 0.2, 0.7) - } - _ => active.text_color, - }, - shadow_offset: active.shadow_offset + Vector::new(0.0, 1.0), - ..active - } - } - } -} diff --git a/examples/todos/Cargo.toml b/examples/todos/Cargo.toml new file mode 100644 index 00000000..53a135e6 --- /dev/null +++ b/examples/todos/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "todos" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez "] +edition = "2018" +publish = false + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +iced = { path = "../.." } +iced_futures = { path = "../../futures", features = ["async-std"] } +async-std = "1.0" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +directories = "2.0" diff --git a/examples/todos/fonts/icons.ttf b/examples/todos/fonts/icons.ttf new file mode 100644 index 00000000..4498299d Binary files /dev/null and b/examples/todos/fonts/icons.ttf differ diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs new file mode 100644 index 00000000..c6ddf2ea --- /dev/null +++ b/examples/todos/src/main.rs @@ -0,0 +1,615 @@ +use iced::{ + button, scrollable, text_input, Align, Application, Button, Checkbox, + Column, Command, Container, Element, Font, HorizontalAlignment, Length, + Row, Scrollable, Settings, Text, TextInput, +}; +use serde::{Deserialize, Serialize}; + +pub fn main() { + Todos::run(Settings::default()) +} + +#[derive(Debug)] +enum Todos { + Loading, + Loaded(State), +} + +#[derive(Debug, Default)] +struct State { + scroll: scrollable::State, + input: text_input::State, + input_value: String, + filter: Filter, + tasks: Vec, + controls: Controls, + dirty: bool, + saving: bool, +} + +#[derive(Debug, Clone)] +enum Message { + Loaded(Result), + Saved(Result<(), SaveError>), + InputChanged(String), + CreateTask, + FilterChanged(Filter), + TaskMessage(usize, TaskMessage), +} + +impl Application for Todos { + type Executor = iced_futures::executor::AsyncStd; + type Message = Message; + + fn new() -> (Todos, Command) { + ( + Todos::Loading, + Command::perform(SavedState::load(), Message::Loaded), + ) + } + + fn title(&self) -> String { + let dirty = match self { + Todos::Loading => false, + Todos::Loaded(state) => state.dirty, + }; + + format!("Todos{} - Iced", if dirty { "*" } else { "" }) + } + + fn update(&mut self, message: Message) -> Command { + match self { + Todos::Loading => { + match message { + Message::Loaded(Ok(state)) => { + *self = Todos::Loaded(State { + input_value: state.input_value, + filter: state.filter, + tasks: state.tasks, + ..State::default() + }); + } + Message::Loaded(Err(_)) => { + *self = Todos::Loaded(State::default()); + } + _ => {} + } + + Command::none() + } + Todos::Loaded(state) => { + let mut saved = false; + + match message { + Message::InputChanged(value) => { + state.input_value = value; + } + Message::CreateTask => { + if !state.input_value.is_empty() { + state + .tasks + .push(Task::new(state.input_value.clone())); + state.input_value.clear(); + } + } + Message::FilterChanged(filter) => { + state.filter = filter; + } + Message::TaskMessage(i, TaskMessage::Delete) => { + state.tasks.remove(i); + } + Message::TaskMessage(i, task_message) => { + if let Some(task) = state.tasks.get_mut(i) { + task.update(task_message); + } + } + Message::Saved(_) => { + state.saving = false; + saved = true; + } + _ => {} + } + + if !saved { + state.dirty = true; + } + + if state.dirty && !state.saving { + state.dirty = false; + state.saving = true; + + Command::perform( + SavedState { + input_value: state.input_value.clone(), + filter: state.filter, + tasks: state.tasks.clone(), + } + .save(), + Message::Saved, + ) + } else { + Command::none() + } + } + } + } + + fn view(&mut self) -> Element { + match self { + Todos::Loading => loading_message(), + Todos::Loaded(State { + scroll, + input, + input_value, + filter, + tasks, + controls, + .. + }) => { + let title = Text::new("todos") + .width(Length::Fill) + .size(100) + .color([0.5, 0.5, 0.5]) + .horizontal_alignment(HorizontalAlignment::Center); + + let input = TextInput::new( + input, + "What needs to be done?", + input_value, + Message::InputChanged, + ) + .padding(15) + .size(30) + .on_submit(Message::CreateTask); + + let controls = controls.view(&tasks, *filter); + let filtered_tasks = + tasks.iter().filter(|task| filter.matches(task)); + + let tasks: Element<_> = if filtered_tasks.count() > 0 { + tasks + .iter_mut() + .enumerate() + .filter(|(_, task)| filter.matches(task)) + .fold(Column::new().spacing(20), |column, (i, task)| { + column.push(task.view().map(move |message| { + Message::TaskMessage(i, message) + })) + }) + .into() + } else { + empty_message(match filter { + Filter::All => "You have not created a task yet...", + Filter::Active => "All your tasks are done! :D", + Filter::Completed => { + "You have not completed a task yet..." + } + }) + }; + + let content = Column::new() + .max_width(800) + .spacing(20) + .push(title) + .push(input) + .push(controls) + .push(tasks); + + Scrollable::new(scroll) + .padding(40) + .push( + Container::new(content).width(Length::Fill).center_x(), + ) + .into() + } + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct Task { + description: String, + completed: bool, + + #[serde(skip)] + state: TaskState, +} + +#[derive(Debug, Clone)] +pub enum TaskState { + Idle { + edit_button: button::State, + }, + Editing { + text_input: text_input::State, + delete_button: button::State, + }, +} + +impl Default for TaskState { + fn default() -> Self { + TaskState::Idle { + edit_button: button::State::new(), + } + } +} + +#[derive(Debug, Clone)] +pub enum TaskMessage { + Completed(bool), + Edit, + DescriptionEdited(String), + FinishEdition, + Delete, +} + +impl Task { + fn new(description: String) -> Self { + Task { + description, + completed: false, + state: TaskState::Idle { + edit_button: button::State::new(), + }, + } + } + + fn update(&mut self, message: TaskMessage) { + match message { + TaskMessage::Completed(completed) => { + self.completed = completed; + } + TaskMessage::Edit => { + self.state = TaskState::Editing { + text_input: text_input::State::focused(), + delete_button: button::State::new(), + }; + } + TaskMessage::DescriptionEdited(new_description) => { + self.description = new_description; + } + TaskMessage::FinishEdition => { + if !self.description.is_empty() { + self.state = TaskState::Idle { + edit_button: button::State::new(), + } + } + } + TaskMessage::Delete => {} + } + } + + fn view(&mut self) -> Element { + match &mut self.state { + TaskState::Idle { edit_button } => { + let checkbox = Checkbox::new( + self.completed, + &self.description, + TaskMessage::Completed, + ) + .width(Length::Fill); + + Row::new() + .spacing(20) + .align_items(Align::Center) + .push(checkbox) + .push( + Button::new(edit_button, edit_icon()) + .on_press(TaskMessage::Edit) + .padding(10) + .style(style::Button::Icon), + ) + .into() + } + TaskState::Editing { + text_input, + delete_button, + } => { + let text_input = TextInput::new( + text_input, + "Describe your task...", + &self.description, + TaskMessage::DescriptionEdited, + ) + .on_submit(TaskMessage::FinishEdition) + .padding(10); + + Row::new() + .spacing(20) + .align_items(Align::Center) + .push(text_input) + .push( + Button::new( + delete_button, + Row::new() + .spacing(10) + .push(delete_icon()) + .push(Text::new("Delete")), + ) + .on_press(TaskMessage::Delete) + .padding(10) + .style(style::Button::Destructive), + ) + .into() + } + } + } +} + +#[derive(Debug, Default, Clone)] +pub struct Controls { + all_button: button::State, + active_button: button::State, + completed_button: button::State, +} + +impl Controls { + fn view(&mut self, tasks: &[Task], current_filter: Filter) -> Row { + let Controls { + all_button, + active_button, + completed_button, + } = self; + + let tasks_left = tasks.iter().filter(|task| !task.completed).count(); + + let filter_button = |state, label, filter, current_filter| { + let label = Text::new(label).size(16); + let button = + Button::new(state, label).style(style::Button::Filter { + selected: filter == current_filter, + }); + + button.on_press(Message::FilterChanged(filter)).padding(8) + }; + + Row::new() + .spacing(20) + .align_items(Align::Center) + .push( + Text::new(&format!( + "{} {} left", + tasks_left, + if tasks_left == 1 { "task" } else { "tasks" } + )) + .width(Length::Fill) + .size(16), + ) + .push( + Row::new() + .spacing(10) + .push(filter_button( + all_button, + "All", + Filter::All, + current_filter, + )) + .push(filter_button( + active_button, + "Active", + Filter::Active, + current_filter, + )) + .push(filter_button( + completed_button, + "Completed", + Filter::Completed, + current_filter, + )), + ) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum Filter { + All, + Active, + Completed, +} + +impl Default for Filter { + fn default() -> Self { + Filter::All + } +} + +impl Filter { + fn matches(&self, task: &Task) -> bool { + match self { + Filter::All => true, + Filter::Active => !task.completed, + Filter::Completed => task.completed, + } + } +} + +fn loading_message() -> Element<'static, Message> { + Container::new( + Text::new("Loading...") + .horizontal_alignment(HorizontalAlignment::Center) + .size(50), + ) + .width(Length::Fill) + .height(Length::Fill) + .center_y() + .into() +} + +fn empty_message(message: &str) -> Element<'static, Message> { + Container::new( + Text::new(message) + .size(25) + .horizontal_alignment(HorizontalAlignment::Center) + .color([0.7, 0.7, 0.7]), + ) + .width(Length::Fill) + .height(Length::Units(200)) + .center_y() + .into() +} + +// Fonts +const ICONS: Font = Font::External { + name: "Icons", + bytes: include_bytes!("../fonts/icons.ttf"), +}; + +fn icon(unicode: char) -> Text { + Text::new(&unicode.to_string()) + .font(ICONS) + .width(Length::Units(20)) + .horizontal_alignment(HorizontalAlignment::Center) + .size(20) +} + +fn edit_icon() -> Text { + icon('\u{F303}') +} + +fn delete_icon() -> Text { + icon('\u{F1F8}') +} + +// Persistence +#[derive(Debug, Clone, Serialize, Deserialize)] +struct SavedState { + input_value: String, + filter: Filter, + tasks: Vec, +} + +#[derive(Debug, Clone)] +enum LoadError { + FileError, + FormatError, +} + +#[derive(Debug, Clone)] +enum SaveError { + DirectoryError, + FileError, + WriteError, + FormatError, +} + +impl SavedState { + fn path() -> std::path::PathBuf { + let mut path = if let Some(project_dirs) = + directories::ProjectDirs::from("rs", "Iced", "Todos") + { + project_dirs.data_dir().into() + } else { + std::env::current_dir().unwrap_or(std::path::PathBuf::new()) + }; + + path.push("todos.json"); + + path + } + + async fn load() -> Result { + use async_std::prelude::*; + + let mut contents = String::new(); + + 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 async_std::prelude::*; + + let json = serde_json::to_string_pretty(&self) + .map_err(|_| SaveError::FormatError)?; + + let path = Self::path(); + + if let Some(dir) = path.parent() { + async_std::fs::create_dir_all(dir) + .await + .map_err(|_| SaveError::DirectoryError)?; + } + + { + let mut file = async_std::fs::File::create(path) + .await + .map_err(|_| SaveError::FileError)?; + + file.write_all(json.as_bytes()) + .await + .map_err(|_| SaveError::WriteError)?; + } + + // This is a simple way to save at most once every couple seconds + async_std::task::sleep(std::time::Duration::from_secs(2)).await; + + Ok(()) + } +} + +mod style { + use iced::{button, Background, Color, Vector}; + + pub enum Button { + Filter { selected: bool }, + Icon, + Destructive, + } + + impl button::StyleSheet for Button { + fn active(&self) -> button::Style { + match self { + Button::Filter { selected } => { + if *selected { + button::Style { + background: Some(Background::Color( + Color::from_rgb(0.2, 0.2, 0.7), + )), + border_radius: 10, + text_color: Color::WHITE, + ..button::Style::default() + } + } else { + button::Style::default() + } + } + Button::Icon => button::Style { + text_color: Color::from_rgb(0.5, 0.5, 0.5), + ..button::Style::default() + }, + Button::Destructive => button::Style { + background: Some(Background::Color(Color::from_rgb( + 0.8, 0.2, 0.2, + ))), + border_radius: 5, + text_color: Color::WHITE, + shadow_offset: Vector::new(1.0, 1.0), + ..button::Style::default() + }, + } + } + + fn hovered(&self) -> button::Style { + let active = self.active(); + + button::Style { + text_color: match self { + Button::Icon => Color::from_rgb(0.2, 0.2, 0.7), + Button::Filter { selected } if !selected => { + Color::from_rgb(0.2, 0.2, 0.7) + } + _ => active.text_color, + }, + shadow_offset: active.shadow_offset + Vector::new(0.0, 1.0), + ..active + } + } + } +} diff --git a/examples/tour.rs b/examples/tour.rs deleted file mode 100644 index b0ee4d96..00000000 --- a/examples/tour.rs +++ /dev/null @@ -1,794 +0,0 @@ -use iced::{ - button, scrollable, slider, text_input, Button, Checkbox, Color, Column, - Container, Element, HorizontalAlignment, Image, Length, Radio, Row, - Sandbox, Scrollable, Settings, Slider, Space, Text, TextInput, -}; - -pub fn main() { - env_logger::init(); - - Tour::run(Settings::default()) -} - -pub struct Tour { - steps: Steps, - scroll: scrollable::State, - back_button: button::State, - next_button: button::State, - debug: bool, -} - -impl Sandbox for Tour { - type Message = Message; - - fn new() -> Tour { - Tour { - steps: Steps::new(), - scroll: scrollable::State::new(), - back_button: button::State::new(), - next_button: button::State::new(), - debug: false, - } - } - - fn title(&self) -> String { - format!("{} - Iced", self.steps.title()) - } - - fn update(&mut self, event: Message) { - match event { - Message::BackPressed => { - self.steps.go_back(); - } - Message::NextPressed => { - self.steps.advance(); - } - Message::StepMessage(step_msg) => { - self.steps.update(step_msg, &mut self.debug); - } - } - } - - fn view(&mut self) -> Element { - let Tour { - steps, - scroll, - back_button, - next_button, - .. - } = self; - - let mut controls = Row::new(); - - if steps.has_previous() { - controls = controls.push( - button(back_button, "Back") - .on_press(Message::BackPressed) - .style(style::Button::Secondary), - ); - } - - controls = controls.push(Space::with_width(Length::Fill)); - - if steps.can_continue() { - controls = controls.push( - button(next_button, "Next") - .on_press(Message::NextPressed) - .style(style::Button::Primary), - ); - } - - let content: Element<_> = Column::new() - .max_width(540) - .spacing(20) - .padding(20) - .push(steps.view(self.debug).map(Message::StepMessage)) - .push(controls) - .into(); - - let content = if self.debug { - content.explain(Color::BLACK) - } else { - content - }; - - let scrollable = Scrollable::new(scroll) - .push(Container::new(content).width(Length::Fill).center_x()); - - Container::new(scrollable) - .height(Length::Fill) - .center_y() - .into() - } -} - -#[derive(Debug, Clone)] -pub enum Message { - BackPressed, - NextPressed, - StepMessage(StepMessage), -} - -struct Steps { - steps: Vec, - current: usize, -} - -impl Steps { - fn new() -> Steps { - Steps { - steps: vec![ - Step::Welcome, - Step::Slider { - state: slider::State::new(), - value: 50, - }, - Step::RowsAndColumns { - layout: Layout::Row, - spacing_slider: slider::State::new(), - spacing: 20, - }, - Step::Text { - size_slider: slider::State::new(), - size: 30, - color_sliders: [slider::State::new(); 3], - color: Color::BLACK, - }, - Step::Radio { selection: None }, - Step::Image { - width: 300, - slider: slider::State::new(), - }, - Step::Scrollable, - Step::TextInput { - value: String::new(), - is_secure: false, - state: text_input::State::new(), - }, - Step::Debugger, - Step::End, - ], - current: 0, - } - } - - fn update(&mut self, msg: StepMessage, debug: &mut bool) { - self.steps[self.current].update(msg, debug); - } - - fn view(&mut self, debug: bool) -> Element { - self.steps[self.current].view(debug) - } - - fn advance(&mut self) { - if self.can_continue() { - self.current += 1; - } - } - - fn go_back(&mut self) { - if self.has_previous() { - self.current -= 1; - } - } - - fn has_previous(&self) -> bool { - self.current > 0 - } - - fn can_continue(&self) -> bool { - self.current + 1 < self.steps.len() - && self.steps[self.current].can_continue() - } - - fn title(&self) -> &str { - self.steps[self.current].title() - } -} - -enum Step { - Welcome, - Slider { - state: slider::State, - value: u16, - }, - RowsAndColumns { - layout: Layout, - spacing_slider: slider::State, - spacing: u16, - }, - Text { - size_slider: slider::State, - size: u16, - color_sliders: [slider::State; 3], - color: Color, - }, - Radio { - selection: Option, - }, - Image { - width: u16, - slider: slider::State, - }, - Scrollable, - TextInput { - value: String, - is_secure: bool, - state: text_input::State, - }, - Debugger, - End, -} - -#[derive(Debug, Clone)] -pub enum StepMessage { - SliderChanged(f32), - LayoutChanged(Layout), - SpacingChanged(f32), - TextSizeChanged(f32), - TextColorChanged(Color), - LanguageSelected(Language), - ImageWidthChanged(f32), - InputChanged(String), - ToggleSecureInput(bool), - DebugToggled(bool), -} - -impl<'a> Step { - fn update(&mut self, msg: StepMessage, debug: &mut bool) { - match msg { - StepMessage::DebugToggled(value) => { - if let Step::Debugger = self { - *debug = value; - } - } - StepMessage::LanguageSelected(language) => { - if let Step::Radio { selection } = self { - *selection = Some(language); - } - } - StepMessage::SliderChanged(new_value) => { - if let Step::Slider { value, .. } = self { - *value = new_value.round() as u16; - } - } - StepMessage::TextSizeChanged(new_size) => { - if let Step::Text { size, .. } = self { - *size = new_size.round() as u16; - } - } - StepMessage::TextColorChanged(new_color) => { - if let Step::Text { color, .. } = self { - *color = new_color; - } - } - StepMessage::LayoutChanged(new_layout) => { - if let Step::RowsAndColumns { layout, .. } = self { - *layout = new_layout; - } - } - StepMessage::SpacingChanged(new_spacing) => { - if let Step::RowsAndColumns { spacing, .. } = self { - *spacing = new_spacing.round() as u16; - } - } - StepMessage::ImageWidthChanged(new_width) => { - if let Step::Image { width, .. } = self { - *width = new_width.round() as u16; - } - } - StepMessage::InputChanged(new_value) => { - if let Step::TextInput { value, .. } = self { - *value = new_value; - } - } - StepMessage::ToggleSecureInput(toggle) => { - if let Step::TextInput { is_secure, .. } = self { - *is_secure = toggle; - } - } - }; - } - - fn title(&self) -> &str { - match self { - Step::Welcome => "Welcome", - Step::Radio { .. } => "Radio button", - Step::Slider { .. } => "Slider", - Step::Text { .. } => "Text", - Step::Image { .. } => "Image", - Step::RowsAndColumns { .. } => "Rows and columns", - Step::Scrollable => "Scrollable", - Step::TextInput { .. } => "Text input", - Step::Debugger => "Debugger", - Step::End => "End", - } - } - - fn can_continue(&self) -> bool { - match self { - Step::Welcome => true, - Step::Radio { selection } => *selection == Some(Language::Rust), - Step::Slider { .. } => true, - Step::Text { .. } => true, - Step::Image { .. } => true, - Step::RowsAndColumns { .. } => true, - Step::Scrollable => true, - Step::TextInput { value, .. } => !value.is_empty(), - Step::Debugger => true, - Step::End => false, - } - } - - fn view(&mut self, debug: bool) -> Element { - match self { - Step::Welcome => Self::welcome(), - Step::Radio { selection } => Self::radio(*selection), - Step::Slider { state, value } => Self::slider(state, *value), - Step::Text { - size_slider, - size, - color_sliders, - color, - } => Self::text(size_slider, *size, color_sliders, *color), - Step::Image { width, slider } => Self::image(*width, slider), - Step::RowsAndColumns { - layout, - spacing_slider, - spacing, - } => Self::rows_and_columns(*layout, spacing_slider, *spacing), - Step::Scrollable => Self::scrollable(), - Step::TextInput { - value, - is_secure, - state, - } => Self::text_input(value, *is_secure, state), - Step::Debugger => Self::debugger(debug), - Step::End => Self::end(), - } - .into() - } - - fn container(title: &str) -> Column<'a, StepMessage> { - Column::new().spacing(20).push(Text::new(title).size(50)) - } - - fn welcome() -> Column<'a, StepMessage> { - Self::container("Welcome!") - .push(Text::new( - "This is a simple tour meant to showcase a bunch of widgets \ - that can be easily implemented on top of Iced.", - )) - .push(Text::new( - "Iced is a cross-platform GUI library for Rust focused on \ - simplicity and type-safety. It is heavily inspired by Elm.", - )) - .push(Text::new( - "It was originally born as part of Coffee, an opinionated \ - 2D game engine for Rust.", - )) - .push(Text::new( - "On native platforms, Iced provides by default a renderer \ - built on top of wgpu, a graphics library supporting Vulkan, \ - Metal, DX11, and DX12.", - )) - .push(Text::new( - "Additionally, this tour can also run on WebAssembly thanks \ - to dodrio, an experimental VDOM library for Rust.", - )) - .push(Text::new( - "You will need to interact with the UI in order to reach the \ - end!", - )) - } - - fn slider( - state: &'a mut slider::State, - value: u16, - ) -> Column<'a, StepMessage> { - Self::container("Slider") - .push(Text::new( - "A slider allows you to smoothly select a value from a range \ - of values.", - )) - .push(Text::new( - "The following slider lets you choose an integer from \ - 0 to 100:", - )) - .push(Slider::new( - state, - 0.0..=100.0, - value as f32, - StepMessage::SliderChanged, - )) - .push( - Text::new(&value.to_string()) - .width(Length::Fill) - .horizontal_alignment(HorizontalAlignment::Center), - ) - } - - fn rows_and_columns( - layout: Layout, - spacing_slider: &'a mut slider::State, - spacing: u16, - ) -> Column<'a, StepMessage> { - let row_radio = Radio::new( - Layout::Row, - "Row", - Some(layout), - StepMessage::LayoutChanged, - ); - - let column_radio = Radio::new( - Layout::Column, - "Column", - Some(layout), - StepMessage::LayoutChanged, - ); - - let layout_section: Element<_> = match layout { - Layout::Row => Row::new() - .spacing(spacing) - .push(row_radio) - .push(column_radio) - .into(), - Layout::Column => Column::new() - .spacing(spacing) - .push(row_radio) - .push(column_radio) - .into(), - }; - - let spacing_section = Column::new() - .spacing(10) - .push(Slider::new( - spacing_slider, - 0.0..=80.0, - spacing as f32, - StepMessage::SpacingChanged, - )) - .push( - Text::new(&format!("{} px", spacing)) - .width(Length::Fill) - .horizontal_alignment(HorizontalAlignment::Center), - ); - - Self::container("Rows and columns") - .spacing(spacing) - .push(Text::new( - "Iced uses a layout model based on flexbox to position UI \ - elements.", - )) - .push(Text::new( - "Rows and columns can be used to distribute content \ - horizontally or vertically, respectively.", - )) - .push(layout_section) - .push(Text::new( - "You can also easily change the spacing between elements:", - )) - .push(spacing_section) - } - - fn text( - size_slider: &'a mut slider::State, - size: u16, - color_sliders: &'a mut [slider::State; 3], - color: Color, - ) -> Column<'a, StepMessage> { - let size_section = Column::new() - .padding(20) - .spacing(20) - .push(Text::new("You can change its size:")) - .push( - Text::new(&format!("This text is {} pixels", size)).size(size), - ) - .push(Slider::new( - size_slider, - 10.0..=70.0, - size as f32, - StepMessage::TextSizeChanged, - )); - - let [red, green, blue] = color_sliders; - let color_section = Column::new() - .padding(20) - .spacing(20) - .push(Text::new("And its color:")) - .push(Text::new(&format!("{:?}", color)).color(color)) - .push( - Row::new() - .spacing(10) - .push(Slider::new(red, 0.0..=1.0, color.r, move |r| { - StepMessage::TextColorChanged(Color { r, ..color }) - })) - .push(Slider::new(green, 0.0..=1.0, color.g, move |g| { - StepMessage::TextColorChanged(Color { g, ..color }) - })) - .push(Slider::new(blue, 0.0..=1.0, color.b, move |b| { - StepMessage::TextColorChanged(Color { b, ..color }) - })), - ); - - Self::container("Text") - .push(Text::new( - "Text is probably the most essential widget for your UI. \ - It will try to adapt to the dimensions of its container.", - )) - .push(size_section) - .push(color_section) - } - - fn radio(selection: Option) -> Column<'a, StepMessage> { - let question = Column::new() - .padding(20) - .spacing(10) - .push(Text::new("Iced is written in...").size(24)) - .push(Language::all().iter().cloned().fold( - Column::new().padding(10).spacing(20), - |choices, language| { - choices.push(Radio::new( - language, - language.into(), - selection, - StepMessage::LanguageSelected, - )) - }, - )); - - Self::container("Radio button") - .push(Text::new( - "A radio button is normally used to represent a choice... \ - Surprise test!", - )) - .push(question) - .push(Text::new( - "Iced works very well with iterators! The list above is \ - basically created by folding a column over the different \ - choices, creating a radio button for each one of them!", - )) - } - - fn image( - width: u16, - slider: &'a mut slider::State, - ) -> Column<'a, StepMessage> { - Self::container("Image") - .push(Text::new("An image that tries to keep its aspect ratio.")) - .push(ferris(width)) - .push(Slider::new( - slider, - 100.0..=500.0, - width as f32, - StepMessage::ImageWidthChanged, - )) - .push( - Text::new(&format!("Width: {} px", width.to_string())) - .width(Length::Fill) - .horizontal_alignment(HorizontalAlignment::Center), - ) - } - - fn scrollable() -> Column<'a, StepMessage> { - Self::container("Scrollable") - .push(Text::new( - "Iced supports scrollable content. Try it out! Find the \ - button further below.", - )) - .push( - Text::new( - "Tip: You can use the scrollbar to scroll down faster!", - ) - .size(16), - ) - .push(Column::new().height(Length::Units(4096))) - .push( - Text::new("You are halfway there!") - .width(Length::Fill) - .size(30) - .horizontal_alignment(HorizontalAlignment::Center), - ) - .push(Column::new().height(Length::Units(4096))) - .push(ferris(300)) - .push( - Text::new("You made it!") - .width(Length::Fill) - .size(50) - .horizontal_alignment(HorizontalAlignment::Center), - ) - } - - fn text_input( - value: &str, - is_secure: bool, - state: &'a mut text_input::State, - ) -> Column<'a, StepMessage> { - let text_input = TextInput::new( - state, - "Type something to continue...", - value, - StepMessage::InputChanged, - ) - .padding(10) - .size(30); - Self::container("Text input") - .push(Text::new( - "Use a text input to ask for different kinds of information.", - )) - .push(if is_secure { - text_input.password() - } else { - text_input - }) - .push(Checkbox::new( - is_secure, - "Enable password mode", - StepMessage::ToggleSecureInput, - )) - .push(Text::new( - "A text input produces a message every time it changes. It is \ - very easy to keep track of its contents:", - )) - .push( - Text::new(if value.is_empty() { - "You have not typed anything yet..." - } else { - value - }) - .width(Length::Fill) - .horizontal_alignment(HorizontalAlignment::Center), - ) - } - - fn debugger(debug: bool) -> Column<'a, StepMessage> { - Self::container("Debugger") - .push(Text::new( - "You can ask Iced to visually explain the layouting of the \ - different elements comprising your UI!", - )) - .push(Text::new( - "Give it a shot! Check the following checkbox to be able to \ - see element boundaries.", - )) - .push(if cfg!(target_arch = "wasm32") { - Element::new( - Text::new("Not available on web yet!") - .color([0.7, 0.7, 0.7]) - .horizontal_alignment(HorizontalAlignment::Center), - ) - } else { - Element::new(Checkbox::new( - debug, - "Explain layout", - StepMessage::DebugToggled, - )) - }) - .push(Text::new("Feel free to go back and take a look.")) - } - - fn end() -> Column<'a, StepMessage> { - Self::container("You reached the end!") - .push(Text::new( - "This tour will be updated as more features are added.", - )) - .push(Text::new("Make sure to keep an eye on it!")) - } -} - -fn ferris<'a>(width: u16) -> Container<'a, StepMessage> { - Container::new( - // This should go away once we unify resource loading on native - // platforms - if cfg!(target_arch = "wasm32") { - Image::new("resources/ferris.png") - } else { - Image::new(format!( - "{}/examples/resources/ferris.png", - env!("CARGO_MANIFEST_DIR") - )) - } - .width(Length::Units(width)), - ) - .width(Length::Fill) - .center_x() -} - -fn button<'a, Message>( - state: &'a mut button::State, - label: &str, -) -> Button<'a, Message> { - Button::new( - state, - Text::new(label).horizontal_alignment(HorizontalAlignment::Center), - ) - .padding(12) - .min_width(100) -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Language { - Rust, - Elm, - Ruby, - Haskell, - C, - Other, -} - -impl Language { - fn all() -> [Language; 6] { - [ - Language::C, - Language::Elm, - Language::Ruby, - Language::Haskell, - Language::Rust, - Language::Other, - ] - } -} - -impl From for &str { - fn from(language: Language) -> &'static str { - match language { - Language::Rust => "Rust", - Language::Elm => "Elm", - Language::Ruby => "Ruby", - Language::Haskell => "Haskell", - Language::C => "C", - Language::Other => "Other", - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Layout { - Row, - Column, -} - -mod style { - use iced::{button, Background, Color, Vector}; - - pub enum Button { - Primary, - Secondary, - } - - impl button::StyleSheet for Button { - fn active(&self) -> button::Style { - button::Style { - background: Some(Background::Color(match self { - Button::Primary => Color::from_rgb(0.11, 0.42, 0.87), - Button::Secondary => Color::from_rgb(0.5, 0.5, 0.5), - })), - border_radius: 12, - shadow_offset: Vector::new(1.0, 1.0), - text_color: Color::from_rgb8(0xEE, 0xEE, 0xEE), - ..button::Style::default() - } - } - - fn hovered(&self) -> button::Style { - button::Style { - text_color: Color::WHITE, - shadow_offset: Vector::new(1.0, 2.0), - ..self.active() - } - } - } -} - -// This should be gracefully handled by Iced in the future. Probably using our -// own proc macro, or maybe the whole process is streamlined by `wasm-pack` at -// some point. -#[cfg(target_arch = "wasm32")] -mod wasm { - use wasm_bindgen::prelude::*; - - #[wasm_bindgen(start)] - pub fn run() { - super::main() - } -} diff --git a/examples/tour/Cargo.toml b/examples/tour/Cargo.toml new file mode 100644 index 00000000..10c3f1da --- /dev/null +++ b/examples/tour/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "tour" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez "] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../.." } +env_logger = "0.7" + +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen = "0.2.51" diff --git a/examples/tour/images/ferris.png b/examples/tour/images/ferris.png new file mode 100644 index 00000000..ebce1a14 Binary files /dev/null and b/examples/tour/images/ferris.png differ diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs new file mode 100644 index 00000000..43c7e50f --- /dev/null +++ b/examples/tour/src/main.rs @@ -0,0 +1,794 @@ +use iced::{ + button, scrollable, slider, text_input, Button, Checkbox, Color, Column, + Container, Element, HorizontalAlignment, Image, Length, Radio, Row, + Sandbox, Scrollable, Settings, Slider, Space, Text, TextInput, +}; + +pub fn main() { + env_logger::init(); + + Tour::run(Settings::default()) +} + +pub struct Tour { + steps: Steps, + scroll: scrollable::State, + back_button: button::State, + next_button: button::State, + debug: bool, +} + +impl Sandbox for Tour { + type Message = Message; + + fn new() -> Tour { + Tour { + steps: Steps::new(), + scroll: scrollable::State::new(), + back_button: button::State::new(), + next_button: button::State::new(), + debug: false, + } + } + + fn title(&self) -> String { + format!("{} - Iced", self.steps.title()) + } + + fn update(&mut self, event: Message) { + match event { + Message::BackPressed => { + self.steps.go_back(); + } + Message::NextPressed => { + self.steps.advance(); + } + Message::StepMessage(step_msg) => { + self.steps.update(step_msg, &mut self.debug); + } + } + } + + fn view(&mut self) -> Element { + let Tour { + steps, + scroll, + back_button, + next_button, + .. + } = self; + + let mut controls = Row::new(); + + if steps.has_previous() { + controls = controls.push( + button(back_button, "Back") + .on_press(Message::BackPressed) + .style(style::Button::Secondary), + ); + } + + controls = controls.push(Space::with_width(Length::Fill)); + + if steps.can_continue() { + controls = controls.push( + button(next_button, "Next") + .on_press(Message::NextPressed) + .style(style::Button::Primary), + ); + } + + let content: Element<_> = Column::new() + .max_width(540) + .spacing(20) + .padding(20) + .push(steps.view(self.debug).map(Message::StepMessage)) + .push(controls) + .into(); + + let content = if self.debug { + content.explain(Color::BLACK) + } else { + content + }; + + let scrollable = Scrollable::new(scroll) + .push(Container::new(content).width(Length::Fill).center_x()); + + Container::new(scrollable) + .height(Length::Fill) + .center_y() + .into() + } +} + +#[derive(Debug, Clone)] +pub enum Message { + BackPressed, + NextPressed, + StepMessage(StepMessage), +} + +struct Steps { + steps: Vec, + current: usize, +} + +impl Steps { + fn new() -> Steps { + Steps { + steps: vec![ + Step::Welcome, + Step::Slider { + state: slider::State::new(), + value: 50, + }, + Step::RowsAndColumns { + layout: Layout::Row, + spacing_slider: slider::State::new(), + spacing: 20, + }, + Step::Text { + size_slider: slider::State::new(), + size: 30, + color_sliders: [slider::State::new(); 3], + color: Color::BLACK, + }, + Step::Radio { selection: None }, + Step::Image { + width: 300, + slider: slider::State::new(), + }, + Step::Scrollable, + Step::TextInput { + value: String::new(), + is_secure: false, + state: text_input::State::new(), + }, + Step::Debugger, + Step::End, + ], + current: 0, + } + } + + fn update(&mut self, msg: StepMessage, debug: &mut bool) { + self.steps[self.current].update(msg, debug); + } + + fn view(&mut self, debug: bool) -> Element { + self.steps[self.current].view(debug) + } + + fn advance(&mut self) { + if self.can_continue() { + self.current += 1; + } + } + + fn go_back(&mut self) { + if self.has_previous() { + self.current -= 1; + } + } + + fn has_previous(&self) -> bool { + self.current > 0 + } + + fn can_continue(&self) -> bool { + self.current + 1 < self.steps.len() + && self.steps[self.current].can_continue() + } + + fn title(&self) -> &str { + self.steps[self.current].title() + } +} + +enum Step { + Welcome, + Slider { + state: slider::State, + value: u16, + }, + RowsAndColumns { + layout: Layout, + spacing_slider: slider::State, + spacing: u16, + }, + Text { + size_slider: slider::State, + size: u16, + color_sliders: [slider::State; 3], + color: Color, + }, + Radio { + selection: Option, + }, + Image { + width: u16, + slider: slider::State, + }, + Scrollable, + TextInput { + value: String, + is_secure: bool, + state: text_input::State, + }, + Debugger, + End, +} + +#[derive(Debug, Clone)] +pub enum StepMessage { + SliderChanged(f32), + LayoutChanged(Layout), + SpacingChanged(f32), + TextSizeChanged(f32), + TextColorChanged(Color), + LanguageSelected(Language), + ImageWidthChanged(f32), + InputChanged(String), + ToggleSecureInput(bool), + DebugToggled(bool), +} + +impl<'a> Step { + fn update(&mut self, msg: StepMessage, debug: &mut bool) { + match msg { + StepMessage::DebugToggled(value) => { + if let Step::Debugger = self { + *debug = value; + } + } + StepMessage::LanguageSelected(language) => { + if let Step::Radio { selection } = self { + *selection = Some(language); + } + } + StepMessage::SliderChanged(new_value) => { + if let Step::Slider { value, .. } = self { + *value = new_value.round() as u16; + } + } + StepMessage::TextSizeChanged(new_size) => { + if let Step::Text { size, .. } = self { + *size = new_size.round() as u16; + } + } + StepMessage::TextColorChanged(new_color) => { + if let Step::Text { color, .. } = self { + *color = new_color; + } + } + StepMessage::LayoutChanged(new_layout) => { + if let Step::RowsAndColumns { layout, .. } = self { + *layout = new_layout; + } + } + StepMessage::SpacingChanged(new_spacing) => { + if let Step::RowsAndColumns { spacing, .. } = self { + *spacing = new_spacing.round() as u16; + } + } + StepMessage::ImageWidthChanged(new_width) => { + if let Step::Image { width, .. } = self { + *width = new_width.round() as u16; + } + } + StepMessage::InputChanged(new_value) => { + if let Step::TextInput { value, .. } = self { + *value = new_value; + } + } + StepMessage::ToggleSecureInput(toggle) => { + if let Step::TextInput { is_secure, .. } = self { + *is_secure = toggle; + } + } + }; + } + + fn title(&self) -> &str { + match self { + Step::Welcome => "Welcome", + Step::Radio { .. } => "Radio button", + Step::Slider { .. } => "Slider", + Step::Text { .. } => "Text", + Step::Image { .. } => "Image", + Step::RowsAndColumns { .. } => "Rows and columns", + Step::Scrollable => "Scrollable", + Step::TextInput { .. } => "Text input", + Step::Debugger => "Debugger", + Step::End => "End", + } + } + + fn can_continue(&self) -> bool { + match self { + Step::Welcome => true, + Step::Radio { selection } => *selection == Some(Language::Rust), + Step::Slider { .. } => true, + Step::Text { .. } => true, + Step::Image { .. } => true, + Step::RowsAndColumns { .. } => true, + Step::Scrollable => true, + Step::TextInput { value, .. } => !value.is_empty(), + Step::Debugger => true, + Step::End => false, + } + } + + fn view(&mut self, debug: bool) -> Element { + match self { + Step::Welcome => Self::welcome(), + Step::Radio { selection } => Self::radio(*selection), + Step::Slider { state, value } => Self::slider(state, *value), + Step::Text { + size_slider, + size, + color_sliders, + color, + } => Self::text(size_slider, *size, color_sliders, *color), + Step::Image { width, slider } => Self::image(*width, slider), + Step::RowsAndColumns { + layout, + spacing_slider, + spacing, + } => Self::rows_and_columns(*layout, spacing_slider, *spacing), + Step::Scrollable => Self::scrollable(), + Step::TextInput { + value, + is_secure, + state, + } => Self::text_input(value, *is_secure, state), + Step::Debugger => Self::debugger(debug), + Step::End => Self::end(), + } + .into() + } + + fn container(title: &str) -> Column<'a, StepMessage> { + Column::new().spacing(20).push(Text::new(title).size(50)) + } + + fn welcome() -> Column<'a, StepMessage> { + Self::container("Welcome!") + .push(Text::new( + "This is a simple tour meant to showcase a bunch of widgets \ + that can be easily implemented on top of Iced.", + )) + .push(Text::new( + "Iced is a cross-platform GUI library for Rust focused on \ + simplicity and type-safety. It is heavily inspired by Elm.", + )) + .push(Text::new( + "It was originally born as part of Coffee, an opinionated \ + 2D game engine for Rust.", + )) + .push(Text::new( + "On native platforms, Iced provides by default a renderer \ + built on top of wgpu, a graphics library supporting Vulkan, \ + Metal, DX11, and DX12.", + )) + .push(Text::new( + "Additionally, this tour can also run on WebAssembly thanks \ + to dodrio, an experimental VDOM library for Rust.", + )) + .push(Text::new( + "You will need to interact with the UI in order to reach the \ + end!", + )) + } + + fn slider( + state: &'a mut slider::State, + value: u16, + ) -> Column<'a, StepMessage> { + Self::container("Slider") + .push(Text::new( + "A slider allows you to smoothly select a value from a range \ + of values.", + )) + .push(Text::new( + "The following slider lets you choose an integer from \ + 0 to 100:", + )) + .push(Slider::new( + state, + 0.0..=100.0, + value as f32, + StepMessage::SliderChanged, + )) + .push( + Text::new(&value.to_string()) + .width(Length::Fill) + .horizontal_alignment(HorizontalAlignment::Center), + ) + } + + fn rows_and_columns( + layout: Layout, + spacing_slider: &'a mut slider::State, + spacing: u16, + ) -> Column<'a, StepMessage> { + let row_radio = Radio::new( + Layout::Row, + "Row", + Some(layout), + StepMessage::LayoutChanged, + ); + + let column_radio = Radio::new( + Layout::Column, + "Column", + Some(layout), + StepMessage::LayoutChanged, + ); + + let layout_section: Element<_> = match layout { + Layout::Row => Row::new() + .spacing(spacing) + .push(row_radio) + .push(column_radio) + .into(), + Layout::Column => Column::new() + .spacing(spacing) + .push(row_radio) + .push(column_radio) + .into(), + }; + + let spacing_section = Column::new() + .spacing(10) + .push(Slider::new( + spacing_slider, + 0.0..=80.0, + spacing as f32, + StepMessage::SpacingChanged, + )) + .push( + Text::new(&format!("{} px", spacing)) + .width(Length::Fill) + .horizontal_alignment(HorizontalAlignment::Center), + ); + + Self::container("Rows and columns") + .spacing(spacing) + .push(Text::new( + "Iced uses a layout model based on flexbox to position UI \ + elements.", + )) + .push(Text::new( + "Rows and columns can be used to distribute content \ + horizontally or vertically, respectively.", + )) + .push(layout_section) + .push(Text::new( + "You can also easily change the spacing between elements:", + )) + .push(spacing_section) + } + + fn text( + size_slider: &'a mut slider::State, + size: u16, + color_sliders: &'a mut [slider::State; 3], + color: Color, + ) -> Column<'a, StepMessage> { + let size_section = Column::new() + .padding(20) + .spacing(20) + .push(Text::new("You can change its size:")) + .push( + Text::new(&format!("This text is {} pixels", size)).size(size), + ) + .push(Slider::new( + size_slider, + 10.0..=70.0, + size as f32, + StepMessage::TextSizeChanged, + )); + + let [red, green, blue] = color_sliders; + let color_section = Column::new() + .padding(20) + .spacing(20) + .push(Text::new("And its color:")) + .push(Text::new(&format!("{:?}", color)).color(color)) + .push( + Row::new() + .spacing(10) + .push(Slider::new(red, 0.0..=1.0, color.r, move |r| { + StepMessage::TextColorChanged(Color { r, ..color }) + })) + .push(Slider::new(green, 0.0..=1.0, color.g, move |g| { + StepMessage::TextColorChanged(Color { g, ..color }) + })) + .push(Slider::new(blue, 0.0..=1.0, color.b, move |b| { + StepMessage::TextColorChanged(Color { b, ..color }) + })), + ); + + Self::container("Text") + .push(Text::new( + "Text is probably the most essential widget for your UI. \ + It will try to adapt to the dimensions of its container.", + )) + .push(size_section) + .push(color_section) + } + + fn radio(selection: Option) -> Column<'a, StepMessage> { + let question = Column::new() + .padding(20) + .spacing(10) + .push(Text::new("Iced is written in...").size(24)) + .push(Language::all().iter().cloned().fold( + Column::new().padding(10).spacing(20), + |choices, language| { + choices.push(Radio::new( + language, + language.into(), + selection, + StepMessage::LanguageSelected, + )) + }, + )); + + Self::container("Radio button") + .push(Text::new( + "A radio button is normally used to represent a choice... \ + Surprise test!", + )) + .push(question) + .push(Text::new( + "Iced works very well with iterators! The list above is \ + basically created by folding a column over the different \ + choices, creating a radio button for each one of them!", + )) + } + + fn image( + width: u16, + slider: &'a mut slider::State, + ) -> Column<'a, StepMessage> { + Self::container("Image") + .push(Text::new("An image that tries to keep its aspect ratio.")) + .push(ferris(width)) + .push(Slider::new( + slider, + 100.0..=500.0, + width as f32, + StepMessage::ImageWidthChanged, + )) + .push( + Text::new(&format!("Width: {} px", width.to_string())) + .width(Length::Fill) + .horizontal_alignment(HorizontalAlignment::Center), + ) + } + + fn scrollable() -> Column<'a, StepMessage> { + Self::container("Scrollable") + .push(Text::new( + "Iced supports scrollable content. Try it out! Find the \ + button further below.", + )) + .push( + Text::new( + "Tip: You can use the scrollbar to scroll down faster!", + ) + .size(16), + ) + .push(Column::new().height(Length::Units(4096))) + .push( + Text::new("You are halfway there!") + .width(Length::Fill) + .size(30) + .horizontal_alignment(HorizontalAlignment::Center), + ) + .push(Column::new().height(Length::Units(4096))) + .push(ferris(300)) + .push( + Text::new("You made it!") + .width(Length::Fill) + .size(50) + .horizontal_alignment(HorizontalAlignment::Center), + ) + } + + fn text_input( + value: &str, + is_secure: bool, + state: &'a mut text_input::State, + ) -> Column<'a, StepMessage> { + let text_input = TextInput::new( + state, + "Type something to continue...", + value, + StepMessage::InputChanged, + ) + .padding(10) + .size(30); + Self::container("Text input") + .push(Text::new( + "Use a text input to ask for different kinds of information.", + )) + .push(if is_secure { + text_input.password() + } else { + text_input + }) + .push(Checkbox::new( + is_secure, + "Enable password mode", + StepMessage::ToggleSecureInput, + )) + .push(Text::new( + "A text input produces a message every time it changes. It is \ + very easy to keep track of its contents:", + )) + .push( + Text::new(if value.is_empty() { + "You have not typed anything yet..." + } else { + value + }) + .width(Length::Fill) + .horizontal_alignment(HorizontalAlignment::Center), + ) + } + + fn debugger(debug: bool) -> Column<'a, StepMessage> { + Self::container("Debugger") + .push(Text::new( + "You can ask Iced to visually explain the layouting of the \ + different elements comprising your UI!", + )) + .push(Text::new( + "Give it a shot! Check the following checkbox to be able to \ + see element boundaries.", + )) + .push(if cfg!(target_arch = "wasm32") { + Element::new( + Text::new("Not available on web yet!") + .color([0.7, 0.7, 0.7]) + .horizontal_alignment(HorizontalAlignment::Center), + ) + } else { + Element::new(Checkbox::new( + debug, + "Explain layout", + StepMessage::DebugToggled, + )) + }) + .push(Text::new("Feel free to go back and take a look.")) + } + + fn end() -> Column<'a, StepMessage> { + Self::container("You reached the end!") + .push(Text::new( + "This tour will be updated as more features are added.", + )) + .push(Text::new("Make sure to keep an eye on it!")) + } +} + +fn ferris<'a>(width: u16) -> Container<'a, StepMessage> { + Container::new( + // This should go away once we unify resource loading on native + // platforms + if cfg!(target_arch = "wasm32") { + Image::new("images/ferris.png") + } else { + Image::new(format!( + "{}/images/ferris.png", + env!("CARGO_MANIFEST_DIR") + )) + } + .width(Length::Units(width)), + ) + .width(Length::Fill) + .center_x() +} + +fn button<'a, Message>( + state: &'a mut button::State, + label: &str, +) -> Button<'a, Message> { + Button::new( + state, + Text::new(label).horizontal_alignment(HorizontalAlignment::Center), + ) + .padding(12) + .min_width(100) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Language { + Rust, + Elm, + Ruby, + Haskell, + C, + Other, +} + +impl Language { + fn all() -> [Language; 6] { + [ + Language::C, + Language::Elm, + Language::Ruby, + Language::Haskell, + Language::Rust, + Language::Other, + ] + } +} + +impl From for &str { + fn from(language: Language) -> &'static str { + match language { + Language::Rust => "Rust", + Language::Elm => "Elm", + Language::Ruby => "Ruby", + Language::Haskell => "Haskell", + Language::C => "C", + Language::Other => "Other", + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Layout { + Row, + Column, +} + +mod style { + use iced::{button, Background, Color, Vector}; + + pub enum Button { + Primary, + Secondary, + } + + impl button::StyleSheet for Button { + fn active(&self) -> button::Style { + button::Style { + background: Some(Background::Color(match self { + Button::Primary => Color::from_rgb(0.11, 0.42, 0.87), + Button::Secondary => Color::from_rgb(0.5, 0.5, 0.5), + })), + border_radius: 12, + shadow_offset: Vector::new(1.0, 1.0), + text_color: Color::from_rgb8(0xEE, 0xEE, 0xEE), + ..button::Style::default() + } + } + + fn hovered(&self) -> button::Style { + button::Style { + text_color: Color::WHITE, + shadow_offset: Vector::new(1.0, 2.0), + ..self.active() + } + } + } +} + +// This should be gracefully handled by Iced in the future. Probably using our +// own proc macro, or maybe the whole process is streamlined by `wasm-pack` at +// some point. +#[cfg(target_arch = "wasm32")] +mod wasm { + use wasm_bindgen::prelude::*; + + #[wasm_bindgen(start)] + pub fn run() { + super::main() + } +} diff --git a/src/lib.rs b/src/lib.rs index 9c9bcff5..1da3f549 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -204,6 +204,6 @@ use iced_winit as common; use iced_web as common; pub use common::{ - Align, Background, Color, Command, Font, HorizontalAlignment, Length, - Space, Subscription, Vector, VerticalAlignment, + futures, Align, Background, Color, Command, Font, HorizontalAlignment, + Length, Space, Subscription, Vector, VerticalAlignment, }; -- cgit From e2ec092aeca64a8107070d72c42c5805dc2c245b Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 20 Jan 2020 06:40:20 +0100 Subject: Add `README` for `tour` example --- examples/README.md | 30 ------------------------------ examples/tour/Cargo.toml | 2 +- examples/tour/README.md | 28 ++++++++++++++++++++++++++++ web/README.md | 2 +- 4 files changed, 30 insertions(+), 32 deletions(-) create mode 100644 examples/tour/README.md diff --git a/examples/README.md b/examples/README.md index 95ec6c5c..a3abde54 100644 --- a/examples/README.md +++ b/examples/README.md @@ -4,36 +4,6 @@ you want to learn about a specific release, check out [the release list]. [the release list]: https://github.com/hecrj/iced/releases -## [Tour](tour.rs) - -A simple UI tour that can run both on native platforms and the web! It showcases different widgets that can be built using Iced. - -The __[`tour`]__ file contains all the code of the example! All the cross-platform GUI is defined in terms of __state__, __messages__, __update logic__ and __view logic__. - - - -[`tour`]: tour.rs -[`iced_winit`]: ../winit -[`iced_native`]: ../native -[`iced_wgpu`]: ../wgpu -[`iced_web`]: ../web -[`winit`]: https://github.com/rust-windowing/winit -[`wgpu`]: https://github.com/gfx-rs/wgpu-rs - -You can run the native version with `cargo run`: -``` -cargo run --example tour -``` - -The web version can be run by following [the usage instructions of `iced_web`] or by accessing [iced.rs](https://iced.rs/)! - -[the usage instructions of `iced_web`]: ../web#usage - - ## [Todos](todos.rs) A simple todos tracker inspired by [TodoMVC]. It showcases dynamic layout, text input, checkboxes, scrollables, icons, and async actions! It automatically saves your tasks in the background, even if you did not finish typing them. diff --git a/examples/tour/Cargo.toml b/examples/tour/Cargo.toml index 10c3f1da..7772df1b 100644 --- a/examples/tour/Cargo.toml +++ b/examples/tour/Cargo.toml @@ -6,7 +6,7 @@ edition = "2018" publish = false [dependencies] -iced = { path = "../.." } +iced = { path = "../..", features = ["debug"] } env_logger = "0.7" [target.'cfg(target_arch = "wasm32")'.dependencies] diff --git a/examples/tour/README.md b/examples/tour/README.md new file mode 100644 index 00000000..f380931a --- /dev/null +++ b/examples/tour/README.md @@ -0,0 +1,28 @@ +## Tour + +A simple UI tour that can run both on native platforms and the web! It showcases different widgets that can be built using Iced. + +The __[`main`]__ file contains all the code of the example! All the cross-platform GUI is defined in terms of __state__, __messages__, __update logic__ and __view logic__. + + + +[`main`]: src/main.rs +[`iced_winit`]: ../../winit +[`iced_native`]: ../../native +[`iced_wgpu`]: ../../wgpu +[`iced_web`]: ../../web +[`winit`]: https://github.com/rust-windowing/winit +[`wgpu`]: https://github.com/gfx-rs/wgpu-rs + +You can run the native version with `cargo run`: +``` +cargo run --package tour +``` + +The web version can be run by following [the usage instructions of `iced_web`] or by accessing [iced.rs](https://iced.rs/)! + +[the usage instructions of `iced_web`]: ../../web#usage diff --git a/web/README.md b/web/README.md index 6a3da7b4..cfd73320 100644 --- a/web/README.md +++ b/web/README.md @@ -35,7 +35,7 @@ For instance, let's say we want to build the [`tour` example]: ``` cd examples -cargo build --example tour --target wasm32-unknown-unknown +cargo build --package tour --target wasm32-unknown-unknown wasm-bindgen ../target/wasm32-unknown-unknown/debug/examples/tour.wasm --out-dir tour --web ``` -- cgit From 6e784e29de71ca1f6ccefd37f4ce7257bbbb4e43 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 20 Jan 2020 07:21:56 +0100 Subject: Add `README` for `pokedex` example --- examples/pokedex/README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 examples/pokedex/README.md diff --git a/examples/pokedex/README.md b/examples/pokedex/README.md new file mode 100644 index 00000000..50720f57 --- /dev/null +++ b/examples/pokedex/README.md @@ -0,0 +1,17 @@ +# Pokédex +An application that loads a random Pokédex entry using the [PokéAPI]. + +All the example code can be found in the __[`main`](src/main.rs)__ file. + + + +You can run it on native platforms with `cargo run`: +``` +cargo run --package pokedex +``` + +[PokéAPI]: https://pokeapi.co/ -- cgit From 472269580038294b67106aad4e3d1c1117ab9d59 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 20 Jan 2020 07:22:10 +0100 Subject: Add `README` for `styling` example --- examples/styling/README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 examples/styling/README.md diff --git a/examples/styling/README.md b/examples/styling/README.md new file mode 100644 index 00000000..6c198a54 --- /dev/null +++ b/examples/styling/README.md @@ -0,0 +1,15 @@ +# Styling +An example showcasing custom styling with a light and dark theme. + +All the example code is located in the __[`main`](src/main.rs)__ file. + + + +You can run it with `cargo run`: +``` +cargo run --package styling +``` -- cgit From fd36510807e663813b5fbb7551ae409da154d1cd Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 20 Jan 2020 07:36:59 +0100 Subject: Add `README` for `todos` example --- examples/todos/README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 examples/todos/README.md diff --git a/examples/todos/README.md b/examples/todos/README.md new file mode 100644 index 00000000..9c2598b9 --- /dev/null +++ b/examples/todos/README.md @@ -0,0 +1,20 @@ +## Todos + +A todos tracker inspired by [TodoMVC]. It showcases dynamic layout, text input, checkboxes, scrollables, icons, and async actions! It automatically saves your tasks in the background, even if you did not finish typing them. + +All the example code is located in the __[`main`]__ file. + + + +You can run the native version with `cargo run`: +``` +cargo run --package todos +``` +We have not yet implemented a `LocalStorage` version of the auto-save feature. Therefore, it does not work on web _yet_! + +[`main`]: src/main.rs +[TodoMVC]: http://todomvc.com/ -- cgit From 03da887339e3d0590dc84238431b61c211f7cf7e Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 20 Jan 2020 07:37:09 +0100 Subject: Update examples `README` --- examples/README.md | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 84 insertions(+), 6 deletions(-) diff --git a/examples/README.md b/examples/README.md index a3abde54..c7820f76 100644 --- a/examples/README.md +++ b/examples/README.md @@ -4,11 +4,37 @@ you want to learn about a specific release, check out [the release list]. [the release list]: https://github.com/hecrj/iced/releases -## [Todos](todos.rs) +## [Tour](tour) +A simple UI tour that can run both on native platforms and the web! It showcases different widgets that can be built using Iced. -A simple todos tracker inspired by [TodoMVC]. It showcases dynamic layout, text input, checkboxes, scrollables, icons, and async actions! It automatically saves your tasks in the background, even if you did not finish typing them. +The __[`main`](tour/src/main.rs)__ file contains all the code of the example! All the cross-platform GUI is defined in terms of __state__, __messages__, __update logic__ and __view logic__. -All the example code is located in the __[`todos`]__ file. + + +[`iced_winit`]: ../winit +[`iced_native`]: ../native +[`iced_wgpu`]: ../wgpu +[`iced_web`]: ../web +[`winit`]: https://github.com/rust-windowing/winit +[`wgpu`]: https://github.com/gfx-rs/wgpu-rs + +You can run the native version with `cargo run`: +``` +cargo run --package tour +``` + +The web version can be run by following [the usage instructions of `iced_web`] or by accessing [iced.rs](https://iced.rs/)! + +[the usage instructions of `iced_web`]: ../web#usage + +## [Todos](todos) +A todos tracker inspired by [TodoMVC]. It showcases dynamic layout, text input, checkboxes, scrollables, icons, and async actions! It automatically saves your tasks in the background, even if you did not finish typing them. + +The example code is located in the __[`main`](todos/src/main.rs)__ file.
@@ -18,15 +44,67 @@ All the example code is located in the __[`todos`]__ file. You can run the native version with `cargo run`: ``` -cargo run --example todos +cargo run --package todos ``` We have not yet implemented a `LocalStorage` version of the auto-save feature. Therefore, it does not work on web _yet_! -[`todos`]: todos.rs [TodoMVC]: http://todomvc.com/ -## [Coffee] +## [Pokédex](pokedex) +An application that helps you learn about Pokémon! It performs an asynchronous HTTP request to the [PokéAPI] in order to load and display a random Pokédex entry (sprite included!). + +The example code can be found in the __[`main`](pokedex/src/main.rs)__ file. + + + +You can run it on native platforms with `cargo run`: +``` +cargo run --package pokedex +``` + +[PokéAPI]: https://pokeapi.co/ + +## [Styling](styling) +An example showcasing custom styling with a light and dark theme. + +The example code is located in the __[`main`](styling/src/main.rs)__ file. + + +You can run it with `cargo run`: +``` +cargo run --package styling +``` + +## Extras +A bunch of simpler examples exist: + +- [`bezier_tool`](bezier_tool), a Paint-like tool for drawing Bezier curves using [`lyon`]. +- [`counter`](counter), the classic counter example explained in the [`README`](../README.md). +- [`custom_widget`](custom_widget), a demonstration of how to build a custom widget that draws a circle. +- [`events`](events), a log of native events displayed using a conditional `Subscription`. +- [`geometry`](geometry), a custom widget showcasing how to draw geometry with the `Mesh2D` primitive in [`iced_wgpu`](../wgpu). +- [`progress_bar`](progress_bar), a simple progress bar that can be filled by using a slider. +- [`stopwatch`](stopwatch), a watch with start/stop and reset buttons showcasing how to listen to time. +- [`svg`](svg), an application that renders the [Ghostscript Tiger] by leveraging the `Svg` widget. + +All of them are packaged in their own crate and, therefore, can be run using `cargo`: +``` +cargo run --package +``` + +[`lyon`]: https://github.com/nical/lyon +[Ghostscript Tiger]: https://commons.wikimedia.org/wiki/File:Ghostscript_Tiger.svg + +## [Coffee] Since [Iced was born in May], it has been powering the user interfaces in [Coffee], an experimental 2D game engine. -- cgit From f14009601e270e43bdf29b8f4842cf136fbbd8b9 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 20 Jan 2020 09:49:17 +0100 Subject: Write documentation for `iced_futures` --- core/src/lib.rs | 2 +- futures/src/executor.rs | 13 +++++++++++ futures/src/executor/async_std.rs | 2 ++ futures/src/executor/null.rs | 2 ++ futures/src/executor/thread_pool.rs | 1 + futures/src/executor/tokio.rs | 1 + futures/src/lib.rs | 6 +++++ futures/src/runtime.rs | 45 +++++++++++++++++++++++++++++++++++++ futures/src/subscription.rs | 39 ++++++++++++++++---------------- futures/src/subscription/tracker.rs | 36 +++++++++++++++++++++++++++++ style/src/lib.rs | 4 ++++ 11 files changed, 131 insertions(+), 20 deletions(-) diff --git a/core/src/lib.rs b/core/src/lib.rs index bec307ad..51863327 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/futures/src/executor.rs b/futures/src/executor.rs index b2ff043e..c2b9cc72 100644 --- a/futures/src/executor.rs +++ b/futures/src/executor.rs @@ -29,13 +29,26 @@ pub use wasm_bindgen::WasmBindgen; use futures::Future; +/// A type that can run futures. pub trait Executor: Sized { + /// Creates a new [`Executor`]. + /// + /// [`Executor`]: trait.Executor.html fn new() -> Result where Self: Sized; + /// Spawns a future in the [`Executor`]. + /// + /// [`Executor`]: trait.Executor.html fn spawn(&self, future: impl Future + Send + 'static); + /// Runs the given closure inside the [`Executor`]. + /// + /// Some executors, like `tokio`, require some global state to be in place + /// before creating futures. This method can be leveraged to set up this + /// global state, call a function, restore the state, and obtain the result + /// of the call. fn enter(&self, f: impl FnOnce() -> R) -> R { f() } diff --git a/futures/src/executor/async_std.rs b/futures/src/executor/async_std.rs index b056b23d..641dfbd2 100644 --- a/futures/src/executor/async_std.rs +++ b/futures/src/executor/async_std.rs @@ -2,6 +2,8 @@ use crate::Executor; use futures::Future; +/// A type representing the `async-std` runtime. +#[derive(Debug)] pub struct AsyncStd; impl Executor for AsyncStd { diff --git a/futures/src/executor/null.rs b/futures/src/executor/null.rs index 722073bb..6d5cf982 100644 --- a/futures/src/executor/null.rs +++ b/futures/src/executor/null.rs @@ -2,6 +2,8 @@ use crate::Executor; use futures::Future; +/// An executor that drops all the futures, instead of spawning them. +#[derive(Debug)] pub struct Null; impl Executor for Null { diff --git a/futures/src/executor/thread_pool.rs b/futures/src/executor/thread_pool.rs index 6393d0d5..09cb4d21 100644 --- a/futures/src/executor/thread_pool.rs +++ b/futures/src/executor/thread_pool.rs @@ -2,6 +2,7 @@ use crate::Executor; use futures::Future; +/// A thread pool for futures. pub type ThreadPool = futures::executor::ThreadPool; impl Executor for futures::executor::ThreadPool { diff --git a/futures/src/executor/tokio.rs b/futures/src/executor/tokio.rs index aafa7e7b..4c609686 100644 --- a/futures/src/executor/tokio.rs +++ b/futures/src/executor/tokio.rs @@ -2,6 +2,7 @@ use crate::Executor; use futures::Future; +/// The `tokio` runtime. pub type Tokio = tokio::runtime::Runtime; impl Executor for Tokio { diff --git a/futures/src/lib.rs b/futures/src/lib.rs index 832a50f6..4872df10 100644 --- a/futures/src/lib.rs +++ b/futures/src/lib.rs @@ -1,3 +1,9 @@ +//! Asynchronous tasks for GUI programming, inspired by Elm. +#![deny(missing_docs)] +#![deny(missing_debug_implementations)] +#![deny(unused_results)] +#![deny(unsafe_code)] +#![deny(rust_2018_idioms)] pub use futures; mod command; diff --git a/futures/src/runtime.rs b/futures/src/runtime.rs index a508c46e..9fd9899a 100644 --- a/futures/src/runtime.rs +++ b/futures/src/runtime.rs @@ -4,6 +4,15 @@ use crate::{subscription, Command, Executor, Subscription}; use futures::Sink; use std::marker::PhantomData; +/// A batteries-included runtime of commands and subscriptions. +/// +/// If you have an [`Executor`], a [`Runtime`] can be leveraged to run any +/// [`Command`] or [`Subscription`] and get notified of the results! +/// +/// [`Runtime`]: struct.Runtime.html +/// [`Executor`]: executor/trait.Executor.html +/// [`Command`]: struct.Command.html +/// [`Subscription`]: subscription/struct.Subscription.html #[derive(Debug)] pub struct Runtime { executor: Executor, @@ -25,6 +34,13 @@ where + 'static, Message: Send + 'static, { + /// Creates a new empty [`Runtime`]. + /// + /// You need to provide: + /// - an [`Executor`] to spawn futures + /// - a `Sender` implementing `Sink` to receive the results + /// + /// [`Runtime`]: struct.Runtime.html pub fn new(executor: Executor, sender: Sender) -> Self { Self { executor, @@ -34,10 +50,24 @@ where } } + /// Runs the given closure inside the [`Executor`] of the [`Runtime`]. + /// + /// See [`Executor::enter`] to learn more. + /// + /// [`Executor`]: executor/trait.Executor.html + /// [`Runtime`]: struct.Runtime.html + /// [`Executor::enter`]: executor/trait.Executor.html#method.enter pub fn enter(&self, f: impl FnOnce() -> R) -> R { self.executor.enter(f) } + /// Spawns a [`Command`] in the [`Runtime`]. + /// + /// The resulting `Message` will be forwarded to the `Sender` of the + /// [`Runtime`]. + /// + /// [`Command`]: struct.Command.html + /// [`Runtime`]: struct.Runtime.html pub fn spawn(&mut self, command: Command) { use futures::{FutureExt, SinkExt}; @@ -56,6 +86,14 @@ where } } + /// Tracks a [`Subscription`] in the [`Runtime`]. + /// + /// It will spawn new streams or close old ones as necessary! See + /// [`Tracker::update`] to learn more about this! + /// + /// [`Subscription`]: subscription/struct.Subscription.html + /// [`Runtime`]: struct.Runtime.html + /// [`Tracker::update`]: subscription/struct.Tracker.html#method.update pub fn track( &mut self, subscription: Subscription, @@ -68,6 +106,13 @@ where } } + /// Broadcasts an event to all the subscriptions currently alive in the + /// [`Runtime`]. + /// + /// See [`Tracker::broadcast`] to learn more. + /// + /// [`Runtime`]: struct.Runtime.html + /// [`Tracker::broadcast`]: subscription/struct.Tracker.html#method.broadcast pub fn broadcast(&mut self, event: Event) { self.subscriptions.broadcast(event); } diff --git a/futures/src/subscription.rs b/futures/src/subscription.rs index 87e51e48..b68444cd 100644 --- a/futures/src/subscription.rs +++ b/futures/src/subscription.rs @@ -16,16 +16,16 @@ use futures::stream::BoxStream; /// 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 +/// This type is normally aliased by runtimes with a specific `Event` and/or /// `Hasher`. /// /// [`Command`]: ../struct.Command.html /// [`Subscription`]: struct.Subscription.html -pub struct Subscription { - recipes: Vec>>, +pub struct Subscription { + recipes: Vec>>, } -impl Subscription +impl Subscription where H: std::hash::Hasher, { @@ -43,7 +43,7 @@ where /// [`Subscription`]: struct.Subscription.html /// [`Recipe`]: trait.Recipe.html pub fn from_recipe( - recipe: impl Recipe + 'static, + recipe: impl Recipe + 'static, ) -> Self { Self { recipes: vec![Box::new(recipe)], @@ -55,7 +55,7 @@ where /// /// [`Subscription`]: struct.Subscription.html pub fn batch( - subscriptions: impl IntoIterator>, + subscriptions: impl IntoIterator>, ) -> Self { Self { recipes: subscriptions @@ -68,7 +68,7 @@ where /// Returns the different recipes of the [`Subscription`]. /// /// [`Subscription`]: struct.Subscription.html - pub fn recipes(self) -> Vec>> { + pub fn recipes(self) -> Vec>> { self.recipes } @@ -78,10 +78,10 @@ where pub fn map( mut self, f: impl Fn(O) -> A + Send + Sync + 'static, - ) -> Subscription + ) -> Subscription where H: 'static, - I: 'static, + E: 'static, O: 'static, A: 'static, { @@ -93,7 +93,7 @@ where .drain(..) .map(|recipe| { Box::new(Map::new(recipe, function.clone())) - as Box> + as Box> }) .collect(), } @@ -114,7 +114,7 @@ impl std::fmt::Debug for Subscription { /// /// [`Subscription`]: struct.Subscription.html /// [`Recipe`]: trait.Recipe.html -pub trait Recipe { +pub trait Recipe { /// The events that will be produced by a [`Subscription`] with this /// [`Recipe`]. /// @@ -133,31 +133,32 @@ pub trait Recipe { /// Executes the [`Recipe`] and produces the stream of events of its /// [`Subscription`]. /// - /// It receives some generic `Input`, which is normally defined by runtimes. + /// It receives some stream of generic events, which is normally defined by + /// shells. /// /// [`Subscription`]: struct.Subscription.html /// [`Recipe`]: trait.Recipe.html fn stream( self: Box, - input: BoxStream<'static, Input>, + input: BoxStream<'static, Event>, ) -> BoxStream<'static, Self::Output>; } -struct Map { - recipe: Box>, +struct Map { + recipe: Box>, mapper: std::sync::Arc B + Send + Sync>, } -impl Map { +impl Map { fn new( - recipe: Box>, + recipe: Box>, mapper: std::sync::Arc B + Send + Sync + 'static>, ) -> Self { Map { recipe, mapper } } } -impl Recipe for Map +impl Recipe for Map where A: 'static, B: 'static, @@ -174,7 +175,7 @@ where fn stream( self: Box, - input: BoxStream<'static, I>, + input: BoxStream<'static, E>, ) -> futures::stream::BoxStream<'static, Self::Output> { use futures::StreamExt; diff --git a/futures/src/subscription/tracker.rs b/futures/src/subscription/tracker.rs index a942b619..c8a1ee18 100644 --- a/futures/src/subscription/tracker.rs +++ b/futures/src/subscription/tracker.rs @@ -4,6 +4,11 @@ use futures::{future::BoxFuture, sink::Sink}; use std::collections::HashMap; use std::marker::PhantomData; +/// A registry of subscription streams. +/// +/// If you have an application that continuously returns a [`Subscription`], +/// you can use a [`Tracker`] to keep track of the different recipes and keep +/// its executions alive. #[derive(Debug)] pub struct Tracker { subscriptions: HashMap>, @@ -21,6 +26,9 @@ where Hasher: std::hash::Hasher + Default, Event: 'static + Send + Clone, { + /// Creates a new empty [`Tracker`]. + /// + /// [`Tracker`]: struct.Tracker.html pub fn new() -> Self { Self { subscriptions: HashMap::new(), @@ -28,6 +36,26 @@ where } } + /// Updates the [`Tracker`] with the given [`Subscription`]. + /// + /// A [`Subscription`] can cause new streams to be spawned or old streams + /// to be closed. + /// + /// The [`Tracker`] keeps track of these streams between calls to this + /// method: + /// + /// - If the provided [`Subscription`] contains a new [`Recipe`] that is + /// currently not being run, it will spawn a new stream and keep it alive. + /// - On the other hand, if a [`Recipe`] is currently in execution and the + /// provided [`Subscription`] does not contain it anymore, then the + /// [`Tracker`] will close and drop the relevant stream. + /// + /// It returns a list of futures that need to be spawned to materialize + /// the [`Tracker`] changes. + /// + /// [`Tracker`]: struct.Tracker.html + /// [`Subscription`]: struct.Subscription.html + /// [`Recipe`]: trait.Recipe.html pub fn update( &mut self, subscription: Subscription, @@ -96,6 +124,14 @@ where futures } + /// Broadcasts an event to the subscriptions currently alive. + /// + /// A subscription's [`Recipe::stream`] always receives a stream of events + /// as input. This stream can be used by some subscription to listen to + /// shell events. + /// + /// This method publishes the given event to all the subscription streams + /// currently open. pub fn broadcast(&mut self, event: Event) { self.subscriptions .values_mut() diff --git a/style/src/lib.rs b/style/src/lib.rs index e0f56594..2c5977b5 100644 --- a/style/src/lib.rs +++ b/style/src/lib.rs @@ -1,3 +1,7 @@ +//! The styling library of Iced. +//! +//! It contains a set of styles and stylesheets for most of the built-in +//! widgets. pub mod button; pub mod checkbox; pub mod container; -- cgit From 7bb6411dfc6797c567a96ff940fc72f3a6747ff4 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 20 Jan 2020 10:39:17 +0100 Subject: Write documentation for `executor::WasmBindgen` --- futures/src/executor/wasm_bindgen.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/futures/src/executor/wasm_bindgen.rs b/futures/src/executor/wasm_bindgen.rs index 70a8ea8e..2a12a1c0 100644 --- a/futures/src/executor/wasm_bindgen.rs +++ b/futures/src/executor/wasm_bindgen.rs @@ -1,5 +1,6 @@ use crate::Executor; +/// A type representing a `wasm-bindgen-futures` runtime. #[derive(Debug)] pub struct WasmBindgen; -- cgit From 91d9d65a03ce9b211e4043726e7424949d314325 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 20 Jan 2020 10:49:25 +0100 Subject: Improve consistency in executor documentation --- futures/src/executor/async_std.rs | 2 +- futures/src/executor/thread_pool.rs | 2 +- futures/src/executor/tokio.rs | 2 +- futures/src/executor/wasm_bindgen.rs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/futures/src/executor/async_std.rs b/futures/src/executor/async_std.rs index 641dfbd2..27949e31 100644 --- a/futures/src/executor/async_std.rs +++ b/futures/src/executor/async_std.rs @@ -2,7 +2,7 @@ use crate::Executor; use futures::Future; -/// A type representing the `async-std` runtime. +/// An `async-std` runtime. #[derive(Debug)] pub struct AsyncStd; diff --git a/futures/src/executor/thread_pool.rs b/futures/src/executor/thread_pool.rs index 09cb4d21..1ec5bf69 100644 --- a/futures/src/executor/thread_pool.rs +++ b/futures/src/executor/thread_pool.rs @@ -2,7 +2,7 @@ use crate::Executor; use futures::Future; -/// A thread pool for futures. +/// A thread pool runtime for futures. pub type ThreadPool = futures::executor::ThreadPool; impl Executor for futures::executor::ThreadPool { diff --git a/futures/src/executor/tokio.rs b/futures/src/executor/tokio.rs index 4c609686..20802ceb 100644 --- a/futures/src/executor/tokio.rs +++ b/futures/src/executor/tokio.rs @@ -2,7 +2,7 @@ use crate::Executor; use futures::Future; -/// The `tokio` runtime. +/// A `tokio` runtime. pub type Tokio = tokio::runtime::Runtime; impl Executor for Tokio { diff --git a/futures/src/executor/wasm_bindgen.rs b/futures/src/executor/wasm_bindgen.rs index 2a12a1c0..69b7c7e2 100644 --- a/futures/src/executor/wasm_bindgen.rs +++ b/futures/src/executor/wasm_bindgen.rs @@ -1,6 +1,6 @@ use crate::Executor; -/// A type representing a `wasm-bindgen-futures` runtime. +/// A `wasm-bindgen-futures` runtime. #[derive(Debug)] pub struct WasmBindgen; -- cgit