Merge pull request #164 from hecrj/feature/custom-runtime

Custom futures executor with `iced_futures`
This commit is contained in:
Héctor Ramón 2020-01-21 00:15:01 +01:00 committed by GitHub
commit 7016221556
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
70 changed files with 1097 additions and 329 deletions

View File

@ -23,11 +23,24 @@ maintenance = { status = "actively-developed" }
[workspace]
members = [
"core",
"futures",
"native",
"style",
"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]
@ -36,19 +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" }
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"

View File

@ -7,11 +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"]
[dependencies]
futures = { version = "0.3", optional = true }

View File

@ -32,15 +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;

View File

@ -4,11 +4,10 @@ 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)
## [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.
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__.
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__.
<div align="center">
<a href="https://gfycat.com/politeadorableiberianmole">
@ -16,7 +15,6 @@ The __[`tour`]__ file contains all the code of the example! All the cross-platfo
</a>
</div>
[`tour`]: tour.rs
[`iced_winit`]: ../winit
[`iced_native`]: ../native
[`iced_wgpu`]: ../wgpu
@ -26,19 +24,17 @@ The __[`tour`]__ file contains all the code of the example! All the cross-platfo
You can run the native version with `cargo run`:
```
cargo run --example tour
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.
## [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.
All the example code is located in the __[`todos`]__ file.
The example code is located in the __[`main`](todos/src/main.rs)__ file.
<div align="center">
<a href="https://gfycat.com/littlesanehalicore">
@ -48,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.
<div align="center">
<a href="https://gfycat.com/aggressivedarkelephantseal-rust-gui">
<img src="https://thumbs.gfycat.com/AggressiveDarkElephantseal-small.gif" height="400px">
</a>
</div>
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.
<div align="center">
<a href="https://user-images.githubusercontent.com/518289/71867993-acff4300-310c-11ea-85a3-d01d8f884346.gif">
<img src="https://user-images.githubusercontent.com/518289/71867993-acff4300-310c-11ea-85a3-d01d8f884346.gif" height="400px">
</a>
</div>
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 <example>
```
[`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.

View File

@ -0,0 +1,12 @@
[package]
name = "bezier_tool"
version = "0.1.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2018"
publish = false
[dependencies]
iced = { path = "../.." }
iced_native = { path = "../../native" }
iced_wgpu = { path = "../../wgpu" }
lyon = "0.15"

View File

@ -0,0 +1,9 @@
[package]
name = "counter"
version = "0.1.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2018"
publish = false
[dependencies]
iced = { path = "../.." }

View File

@ -0,0 +1,11 @@
[package]
name = "custom_widget"
version = "0.1.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2018"
publish = false
[dependencies]
iced = { path = "../.." }
iced_native = { path = "../../native" }
iced_wgpu = { path = "../../wgpu" }

View File

@ -0,0 +1,10 @@
[package]
name = "events"
version = "0.1.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2018"
publish = false
[dependencies]
iced = { path = "../.." }
iced_native = { path = "../../native" }

View File

@ -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<Message>) {

View File

@ -0,0 +1,11 @@
[package]
name = "geometry"
version = "0.1.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2018"
publish = false
[dependencies]
iced = { path = "../.." }
iced_native = { path = "../../native" }
iced_wgpu = { path = "../../wgpu" }

View File

@ -0,0 +1,14 @@
[package]
name = "pokedex"
version = "0.1.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
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"

View File

@ -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.
<div align="center">
<a href="https://gfycat.com/aggressivedarkelephantseal-rust-gui">
<img src="https://thumbs.gfycat.com/AggressiveDarkElephantseal-small.gif" height="400px">
</a>
</div>
You can run it on native platforms with `cargo run`:
```
cargo run --package pokedex
```
[PokéAPI]: https://pokeapi.co/

View File

@ -1,6 +1,6 @@
use iced::{
button, image, Align, Application, Button, Column, Command, Container,
Element, Image, Length, Row, Settings, Text,
button, futures, image, Align, Application, Button, Column, Command,
Container, Element, Image, Length, Row, Settings, Text,
};
pub fn main() {
@ -27,6 +27,7 @@ enum Message {
}
impl Application for Pokedex {
type Executor = iced_futures::executor::AsyncStd;
type Message = Message;
fn new() -> (Pokedex, Command<Message>) {

View File

@ -0,0 +1,9 @@
[package]
name = "progress_bar"
version = "0.1.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2018"
publish = false
[dependencies]
iced = { path = "../.." }

View File

@ -0,0 +1,12 @@
[package]
name = "stopwatch"
version = "0.1.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
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"] }

View File

@ -28,6 +28,7 @@ enum Message {
}
impl Application for Stopwatch {
type Executor = iced_futures::executor::AsyncStd;
type Message = Message;
fn new() -> (Stopwatch, Command<Message>) {
@ -142,6 +143,8 @@ impl Application for Stopwatch {
}
mod time {
use iced::futures;
pub fn every(
duration: std::time::Duration,
) -> iced::Subscription<std::time::Instant> {
@ -165,7 +168,7 @@ mod time {
fn stream(
self: Box<Self>,
_input: I,
_input: futures::stream::BoxStream<'static, I>,
) -> futures::stream::BoxStream<'static, Self::Output> {
use futures::stream::StreamExt;

View File

@ -0,0 +1,9 @@
[package]
name = "styling"
version = "0.1.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2018"
publish = false
[dependencies]
iced = { path = "../.." }

View File

@ -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.
<div align="center">
<a href="https://user-images.githubusercontent.com/518289/71867993-acff4300-310c-11ea-85a3-d01d8f884346.gif">
<img src="https://user-images.githubusercontent.com/518289/71867993-acff4300-310c-11ea-85a3-d01d8f884346.gif" height="400px">
</a>
</div>
You can run it with `cargo run`:
```
cargo run --package styling
```

View File

@ -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()
}
}

9
examples/svg/Cargo.toml Normal file
View File

@ -0,0 +1,9 @@
[package]
name = "svg"
version = "0.1.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2018"
publish = false
[dependencies]
iced = { path = "../..", features = ["svg"] }

View File

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 67 KiB

37
examples/svg/src/main.rs Normal file
View File

@ -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()
}
}

16
examples/todos/Cargo.toml Normal file
View File

@ -0,0 +1,16 @@
[package]
name = "todos"
version = "0.1.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
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"

20
examples/todos/README.md Normal file
View File

@ -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.
<div align="center">
<a href="https://gfycat.com/littlesanehalicore">
<img src="https://thumbs.gfycat.com/LittleSaneHalicore-small.gif" height="400px">
</a>
</div>
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/

View File

@ -38,6 +38,7 @@ enum Message {
}
impl Application for Todos {
type Executor = iced_futures::executor::AsyncStd;
type Message = Message;
fn new() -> (Todos, Command<Message>) {
@ -450,7 +451,7 @@ fn empty_message(message: &str) -> Element<'static, Message> {
// Fonts
const ICONS: Font = Font::External {
name: "Icons",
bytes: include_bytes!("resources/icons.ttf"),
bytes: include_bytes!("../fonts/icons.ttf"),
};
fn icon(unicode: char) -> Text {

13
examples/tour/Cargo.toml Normal file
View File

@ -0,0 +1,13 @@
[package]
name = "tour"
version = "0.1.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2018"
publish = false
[dependencies]
iced = { path = "../..", features = ["debug"] }
env_logger = "0.7"
[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen = "0.2.51"

28
examples/tour/README.md Normal file
View File

@ -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__.
<div align="center">
<a href="https://gfycat.com/politeadorableiberianmole">
<img src="https://thumbs.gfycat.com/PoliteAdorableIberianmole-small.gif">
</a>
</div>
[`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

View File

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -681,10 +681,10 @@ fn ferris<'a>(width: u16) -> Container<'a, StepMessage> {
// This should go away once we unify resource loading on native
// platforms
if cfg!(target_arch = "wasm32") {
Image::new("resources/ferris.png")
Image::new("images/ferris.png")
} else {
Image::new(format!(
"{}/examples/resources/ferris.png",
"{}/images/ferris.png",
env!("CARGO_MANIFEST_DIR")
))
}

32
futures/Cargo.toml Normal file
View File

@ -0,0 +1,32 @@
[package]
name = "iced_futures"
version = "0.1.0-alpha"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
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"]
[features]
thread-pool = ["futures/thread-pool"]
[dependencies]
log = "0.4"
[dependencies.futures]
version = "0.3"
[dependencies.tokio]
version = "0.2"
optional = true
features = ["rt-core"]
[dependencies.async-std]
version = "1.0"
optional = true
[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen-futures = "0.4"

55
futures/src/executor.rs Normal file
View File

@ -0,0 +1,55 @@
//! Choose your preferred executor to power a runtime.
mod null;
#[cfg(feature = "thread-pool")]
mod thread_pool;
#[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;
/// A type that can run futures.
pub trait Executor: Sized {
/// Creates a new [`Executor`].
///
/// [`Executor`]: trait.Executor.html
fn new() -> Result<Self, futures::io::Error>
where
Self: Sized;
/// Spawns a future in the [`Executor`].
///
/// [`Executor`]: trait.Executor.html
fn spawn(&self, future: impl Future<Output = ()> + 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<R>(&self, f: impl FnOnce() -> R) -> R {
f()
}
}

View File

@ -0,0 +1,17 @@
use crate::Executor;
use futures::Future;
/// An `async-std` runtime.
#[derive(Debug)]
pub struct AsyncStd;
impl Executor for AsyncStd {
fn new() -> Result<Self, futures::io::Error> {
Ok(Self)
}
fn spawn(&self, future: impl Future<Output = ()> + Send + 'static) {
let _ = async_std::task::spawn(future);
}
}

View File

@ -0,0 +1,15 @@
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 {
fn new() -> Result<Self, futures::io::Error> {
Ok(Self)
}
fn spawn(&self, _future: impl Future<Output = ()> + Send + 'static) {}
}

View File

@ -0,0 +1,16 @@
use crate::Executor;
use futures::Future;
/// A thread pool runtime for futures.
pub type ThreadPool = futures::executor::ThreadPool;
impl Executor for futures::executor::ThreadPool {
fn new() -> Result<Self, futures::io::Error> {
futures::executor::ThreadPool::new()
}
fn spawn(&self, future: impl Future<Output = ()> + Send + 'static) {
self.spawn_ok(future);
}
}

View File

@ -0,0 +1,20 @@
use crate::Executor;
use futures::Future;
/// A `tokio` runtime.
pub type Tokio = tokio::runtime::Runtime;
impl Executor for Tokio {
fn new() -> Result<Self, futures::io::Error> {
tokio::runtime::Runtime::new()
}
fn spawn(&self, future: impl Future<Output = ()> + Send + 'static) {
let _ = tokio::runtime::Runtime::spawn(self, future);
}
fn enter<R>(&self, f: impl FnOnce() -> R) -> R {
tokio::runtime::Runtime::enter(self, f)
}
}

View File

@ -0,0 +1,18 @@
use crate::Executor;
/// A `wasm-bindgen-futures` runtime.
#[derive(Debug)]
pub struct WasmBindgen;
impl Executor for WasmBindgen {
fn new() -> Result<Self, futures::io::Error> {
Ok(Self)
}
fn spawn(
&self,
future: impl futures::Future<Output = ()> + Send + 'static,
) {
wasm_bindgen_futures::spawn_local(future);
}
}

18
futures/src/lib.rs Normal file
View File

@ -0,0 +1,18 @@
//! 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;
mod runtime;
pub mod executor;
pub mod subscription;
pub use command::Command;
pub use executor::Executor;
pub use runtime::Runtime;
pub use subscription::Subscription;

119
futures/src/runtime.rs Normal file
View File

@ -0,0 +1,119 @@
//! Run commands and keep track of subscriptions.
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<Hasher, Event, Executor, Sender, Message> {
executor: Executor,
sender: Sender,
subscriptions: subscription::Tracker<Hasher, Event>,
_message: PhantomData<Message>,
}
impl<Hasher, Event, Executor, Sender, Message>
Runtime<Hasher, Event, Executor, Sender, Message>
where
Hasher: std::hash::Hasher + Default,
Event: Send + Clone + 'static,
Executor: self::Executor,
Sender: Sink<Message, Error = core::convert::Infallible>
+ Unpin
+ Send
+ Clone
+ '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,
sender,
subscriptions: subscription::Tracker::new(),
_message: PhantomData,
}
}
/// 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<R>(&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<Message>) {
use futures::{FutureExt, SinkExt};
let futures = command.futures();
for future in futures {
let mut sender = self.sender.clone();
self.executor.spawn(future.then(|message| {
async move {
let _ = sender.send(message).await;
()
}
}));
}
}
/// 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<Hasher, Event, Message>,
) {
let futures =
self.subscriptions.update(subscription, self.sender.clone());
for future in futures {
self.executor.spawn(future);
}
}
/// 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);
}
}

View File

@ -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.
///
@ -11,16 +16,16 @@
/// 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<Hasher, Input, Output> {
recipes: Vec<Box<dyn Recipe<Hasher, Input, Output = Output>>>,
pub struct Subscription<Hasher, Event, Output> {
recipes: Vec<Box<dyn Recipe<Hasher, Event, Output = Output>>>,
}
impl<H, I, O> Subscription<H, I, O>
impl<H, E, O> Subscription<H, E, O>
where
H: std::hash::Hasher,
{
@ -38,7 +43,7 @@ where
/// [`Subscription`]: struct.Subscription.html
/// [`Recipe`]: trait.Recipe.html
pub fn from_recipe(
recipe: impl Recipe<H, I, Output = O> + 'static,
recipe: impl Recipe<H, E, Output = O> + 'static,
) -> Self {
Self {
recipes: vec![Box::new(recipe)],
@ -50,7 +55,7 @@ where
///
/// [`Subscription`]: struct.Subscription.html
pub fn batch(
subscriptions: impl IntoIterator<Item = Subscription<H, I, O>>,
subscriptions: impl IntoIterator<Item = Subscription<H, E, O>>,
) -> Self {
Self {
recipes: subscriptions
@ -63,7 +68,7 @@ where
/// Returns the different recipes of the [`Subscription`].
///
/// [`Subscription`]: struct.Subscription.html
pub fn recipes(self) -> Vec<Box<dyn Recipe<H, I, Output = O>>> {
pub fn recipes(self) -> Vec<Box<dyn Recipe<H, E, Output = O>>> {
self.recipes
}
@ -73,10 +78,10 @@ where
pub fn map<A>(
mut self,
f: impl Fn(O) -> A + Send + Sync + 'static,
) -> Subscription<H, I, A>
) -> Subscription<H, E, A>
where
H: 'static,
I: 'static,
E: 'static,
O: 'static,
A: 'static,
{
@ -88,7 +93,7 @@ where
.drain(..)
.map(|recipe| {
Box::new(Map::new(recipe, function.clone()))
as Box<dyn Recipe<H, I, Output = A>>
as Box<dyn Recipe<H, E, Output = A>>
})
.collect(),
}
@ -109,7 +114,7 @@ impl<I, O, H> std::fmt::Debug for Subscription<I, O, H> {
///
/// [`Subscription`]: struct.Subscription.html
/// [`Recipe`]: trait.Recipe.html
pub trait Recipe<Hasher: std::hash::Hasher, Input> {
pub trait Recipe<Hasher: std::hash::Hasher, Event> {
/// The events that will be produced by a [`Subscription`] with this
/// [`Recipe`].
///
@ -128,31 +133,32 @@ pub trait Recipe<Hasher: std::hash::Hasher, Input> {
/// 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<Self>,
input: Input,
) -> futures::stream::BoxStream<'static, Self::Output>;
input: BoxStream<'static, Event>,
) -> BoxStream<'static, Self::Output>;
}
struct Map<Hasher, Input, A, B> {
recipe: Box<dyn Recipe<Hasher, Input, Output = A>>,
struct Map<Hasher, Event, A, B> {
recipe: Box<dyn Recipe<Hasher, Event, Output = A>>,
mapper: std::sync::Arc<dyn Fn(A) -> B + Send + Sync>,
}
impl<H, I, A, B> Map<H, I, A, B> {
impl<H, E, A, B> Map<H, E, A, B> {
fn new(
recipe: Box<dyn Recipe<H, I, Output = A>>,
recipe: Box<dyn Recipe<H, E, Output = A>>,
mapper: std::sync::Arc<dyn Fn(A) -> B + Send + Sync + 'static>,
) -> Self {
Map { recipe, mapper }
}
}
impl<H, I, A, B> Recipe<H, I> for Map<H, I, A, B>
impl<H, E, A, B> Recipe<H, E> for Map<H, E, A, B>
where
A: 'static,
B: 'static,
@ -169,7 +175,7 @@ where
fn stream(
self: Box<Self>,
input: I,
input: BoxStream<'static, E>,
) -> futures::stream::BoxStream<'static, Self::Output> {
use futures::StreamExt;

View File

@ -0,0 +1,148 @@
use crate::Subscription;
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<Hasher, Event> {
subscriptions: HashMap<u64, Execution<Event>>,
_hasher: PhantomData<Hasher>,
}
#[derive(Debug)]
pub struct Execution<Event> {
_cancel: futures::channel::oneshot::Sender<()>,
listener: Option<futures::channel::mpsc::Sender<Event>>,
}
impl<Hasher, Event> Tracker<Hasher, Event>
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(),
_hasher: PhantomData,
}
}
/// 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<Message, Receiver>(
&mut self,
subscription: Subscription<Hasher, Event, Message>,
receiver: Receiver,
) -> Vec<BoxFuture<'static, ()>>
where
Message: 'static + Send,
Receiver: 'static
+ Sink<Message, Error = core::convert::Infallible>
+ 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
}
/// 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()
.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
);
}
});
}
}

View File

@ -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"
features = ["thread-pool"]

View File

@ -51,13 +51,18 @@ mod element;
mod event;
mod hasher;
mod mouse_cursor;
mod runtime;
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::{executor, futures, Command};
#[doc(no_inline)]
pub use executor::Executor;
pub use clipboard::Clipboard;
pub use element::Element;
@ -66,6 +71,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};

12
native/src/runtime.rs Normal file
View File

@ -0,0 +1,12 @@
//! 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<Executor, Receiver, Message> =
iced_futures::Runtime<Hasher, Event, Executor, Receiver, Message>;

View File

@ -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.
///
@ -15,7 +15,7 @@ use futures::stream::BoxStream;
///
/// [`Command`]: ../struct.Command.html
/// [`Subscription`]: struct.Subscription.html
pub type Subscription<T> = iced_core::Subscription<Hasher, EventStream, T>;
pub type Subscription<T> = iced_futures::Subscription<Hasher, Event, T>;
/// A stream of runtime events.
///
@ -24,7 +24,12 @@ pub type Subscription<T> = iced_core::Subscription<Hasher, EventStream, T>;
/// [`Subscription`]: type.Subscription.html
pub type EventStream = BoxStream<'static, Event>;
pub use iced_core::subscription::Recipe;
/// A native [`Subscription`] tracker.
///
/// [`Subscription`]: type.Subscription.html
pub type Tracker = iced_futures::subscription::Tracker<Hasher, Event>;
pub use iced_futures::subscription::Recipe;
mod events;

View File

@ -2,10 +2,11 @@ use crate::{
subscription::{EventStream, Recipe},
Event, Hasher,
};
use iced_futures::futures::stream::BoxStream;
pub struct Events;
impl Recipe<Hasher, EventStream> for Events {
impl Recipe<Hasher, Event> for Events {
type Output = Event;
fn hash(&self, state: &mut Hasher) {
@ -17,7 +18,7 @@ impl Recipe<Hasher, EventStream> for Events {
fn stream(
self: Box<Self>,
event_stream: EventStream,
) -> futures::stream::BoxStream<'static, Self::Output> {
) -> BoxStream<'static, Self::Output> {
event_stream
}
}

View File

@ -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<Message>) {
@ -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<A::Message>) {

9
src/element.rs Normal file
View File

@ -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;

54
src/executor.rs Normal file
View File

@ -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<Self, futures::io::Error> {
Ok(Default(ThreadPool::new()?))
}
fn spawn(
&self,
future: impl futures::Future<Output = ()> + 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<Self, futures::io::Error> {
Ok(Default(WasmBindgen::new()?))
}
fn spawn(
&self,
future: impl futures::Future<Output = ()> + Send + 'static,
) {
self.0.spawn(future);
}
}
}

View File

@ -180,18 +180,30 @@
#![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 element;
mod sandbox;
pub mod executor;
pub mod settings;
pub mod widget;
pub mod window;
#[doc(no_inline)]
pub use widget::*;
pub use application::Application;
pub use platform::*;
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::{
futures, Align, Background, Color, Command, Font, HorizontalAlignment,
Length, Space, Subscription, Vector, VerticalAlignment,
};

View File

@ -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<T> Application for T
where
T: Sandbox,
{
type Executor = executor::Null;
type Message = T::Message;
fn new() -> (Self, Command<T::Message>) {

View File

@ -1 +0,0 @@
pub use iced_web::*;

View File

@ -1,27 +1,23 @@
pub use iced_winit::{
Align, Background, Color, Command, Font, HorizontalAlignment, Length,
Space, Subscription, Vector, VerticalAlignment,
};
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
//! 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 {
@ -56,11 +52,9 @@ pub mod widget {
iced_winit::Row<'a, Message, iced_wgpu::Renderer>;
}
#[doc(no_inline)]
pub use widget::*;
#[cfg(target_arch = "wasm32")]
mod platform {
pub use iced_web::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>;
pub use platform::*;

View File

@ -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;

View File

@ -15,11 +15,17 @@ 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"

View File

@ -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
```

View File

@ -72,13 +72,19 @@ 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, Vector,
VerticalAlignment,
};
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
@ -148,7 +154,6 @@ pub trait Application {
}
}
struct Instance<Message> {
title: String,
ui: Rc<RefCell<Box<dyn Application<Message = Message>>>>,
@ -167,7 +172,7 @@ impl<Message> Clone for Instance<Message> {
impl<Message> Instance<Message>
where
Message: 'static
Message: 'static,
{
fn new(ui: impl Application<Message = Message> + 'static) -> Self {
Self {

View File

@ -14,6 +14,6 @@ use crate::Hasher;
///
/// [`Command`]: ../struct.Command.html
/// [`Subscription`]: struct.Subscription.html
pub type Subscription<T> = iced_core::Subscription<Hasher, (), T>;
pub type Subscription<T> = iced_futures::Subscription<Hasher, (), T>;
pub use iced_core::subscription::Recipe;
pub use iced_futures::subscription::Recipe;

View File

@ -14,11 +14,16 @@ 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"
[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"

View File

@ -1,8 +1,8 @@
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, Executor, Mode,
MouseCursor, Proxy, Runtime, Settings, Size, Subscription, UserInterface,
};
/// An interactive, native cross-platform application.
@ -19,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
@ -109,17 +114,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 external_messages = Vec::new();
let mut runtime = {
let executor = Self::Executor::new().expect("Create executor");
Runtime::new(executor, Proxy::new(event_loop.create_proxy()))
};
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 +219,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 +248,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 +468,6 @@ fn to_physical(size: winit::dpi::LogicalSize, dpi: f64) -> (u16, u16) {
)
}
fn spawn<Message: Send>(
command: Command<Message>,
thread_pool: &mut futures::executor::ThreadPool,
proxy: &winit::event_loop::EventLoopProxy<Message>,
) {
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 {

View File

@ -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;

57
winit/src/proxy.rs Normal file
View File

@ -0,0 +1,57 @@
use iced_native::futures::{
task::{Context, Poll},
Sink,
};
use std::pin::Pin;
pub struct Proxy<Message: 'static> {
raw: winit::event_loop::EventLoopProxy<Message>,
}
impl<Message: 'static> Clone for Proxy<Message> {
fn clone(&self) -> Self {
Self {
raw: self.raw.clone(),
}
}
}
impl<Message: 'static> Proxy<Message> {
pub fn new(raw: winit::event_loop::EventLoopProxy<Message>) -> Self {
Self { raw }
}
}
impl<Message: 'static> Sink<Message> for Proxy<Message> {
type Error = core::convert::Infallible;
fn poll_ready(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
) -> Poll<Result<(), Self::Error>> {
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<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn poll_close(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
}

View File

@ -1,97 +0,0 @@
use iced_native::{Event, Hasher, Subscription};
use std::collections::HashMap;
pub struct Pool {
alive: HashMap<u64, Handle>,
}
pub struct Handle {
_cancel: futures::channel::oneshot::Sender<()>,
listener: Option<futures::channel::mpsc::Sender<Event>>,
}
impl Pool {
pub fn new() -> Self {
Self {
alive: HashMap::new(),
}
}
pub fn update<Message: Send>(
&mut self,
subscription: Subscription<Message>,
thread_pool: &mut futures::executor::ThreadPool,
proxy: &winit::event_loop::EventLoopProxy<Message>,
) {
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
);
}
});
}
}