diff options
70 files changed, 1097 insertions, 329 deletions
@@ -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" diff --git a/core/Cargo.toml b/core/Cargo.toml index 0a8fd8ef..22bc7ceb 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -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 } diff --git a/core/src/lib.rs b/core/src/lib.rs index 821b09c1..51863327 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -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; diff --git a/examples/README.md b/examples/README.md index 95ec6c5c..c7820f76 100644 --- a/examples/README.md +++ b/examples/README.md @@ -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. 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 <hector0193@gmail.com>"] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../.." } +iced_native = { path = "../../native" } +iced_wgpu = { path = "../../wgpu" } +lyon = "0.15" diff --git a/examples/bezier_tool.rs b/examples/bezier_tool/src/main.rs index 043d265c..043d265c 100644 --- a/examples/bezier_tool.rs +++ b/examples/bezier_tool/src/main.rs 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 <hector0193@gmail.com>"] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../.." } diff --git a/examples/counter.rs b/examples/counter/src/main.rs index b85db70d..b85db70d 100644 --- a/examples/counter.rs +++ b/examples/counter/src/main.rs 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 <hector0193@gmail.com>"] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../.." } +iced_native = { path = "../../native" } +iced_wgpu = { path = "../../wgpu" } diff --git a/examples/custom_widget.rs b/examples/custom_widget/src/main.rs index 0a570745..0a570745 100644 --- a/examples/custom_widget.rs +++ b/examples/custom_widget/src/main.rs 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 <hector0193@gmail.com>"] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../.." } +iced_native = { path = "../../native" } diff --git a/examples/events.rs b/examples/events/src/main.rs index 74542171..0c9dca05 100644 --- a/examples/events.rs +++ b/examples/events/src/main.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<Message>) { 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 <hector0193@gmail.com>"] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../.." } +iced_native = { path = "../../native" } +iced_wgpu = { path = "../../wgpu" } diff --git a/examples/geometry.rs b/examples/geometry/src/main.rs index 9d5fd611..9d5fd611 100644 --- a/examples/geometry.rs +++ b/examples/geometry/src/main.rs 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 <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" 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. + +<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/ diff --git a/examples/pokedex.rs b/examples/pokedex/src/main.rs index 7326f94f..283437b2 100644 --- a/examples/pokedex.rs +++ b/examples/pokedex/src/main.rs @@ -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>) { 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 <hector0193@gmail.com>"] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../.." } diff --git a/examples/progress_bar.rs b/examples/progress_bar/src/main.rs index 43b09928..43b09928 100644 --- a/examples/progress_bar.rs +++ b/examples/progress_bar/src/main.rs 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 <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"] } diff --git a/examples/stopwatch.rs b/examples/stopwatch/src/main.rs index c9a61ee9..d84c4817 100644 --- a/examples/stopwatch.rs +++ b/examples/stopwatch/src/main.rs @@ -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; 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 <hector0193@gmail.com>"] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../.." } 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. + +<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 +``` diff --git a/examples/styling.rs b/examples/styling/src/main.rs index 50095ec7..50095ec7 100644 --- a/examples/styling.rs +++ b/examples/styling/src/main.rs 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 <hector0193@gmail.com>"] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../..", features = ["svg"] } diff --git a/examples/resources/tiger.svg b/examples/svg/resources/tiger.svg index 679edec2..679edec2 100644 --- a/examples/resources/tiger.svg +++ b/examples/svg/resources/tiger.svg 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/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 <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" 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. + +<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/ diff --git a/examples/resources/icons.ttf b/examples/todos/fonts/icons.ttf Binary files differindex 4498299d..4498299d 100644 --- a/examples/resources/icons.ttf +++ b/examples/todos/fonts/icons.ttf diff --git a/examples/todos.rs b/examples/todos/src/main.rs index 4166f75a..c6ddf2ea 100644 --- a/examples/todos.rs +++ b/examples/todos/src/main.rs @@ -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 { diff --git a/examples/tour/Cargo.toml b/examples/tour/Cargo.toml new file mode 100644 index 00000000..7772df1b --- /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 <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" 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__. + +<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 diff --git a/examples/resources/ferris.png b/examples/tour/images/ferris.png Binary files differindex ebce1a14..ebce1a14 100644 --- a/examples/resources/ferris.png +++ b/examples/tour/images/ferris.png diff --git a/examples/tour.rs b/examples/tour/src/main.rs index b0ee4d96..43c7e50f 100644 --- a/examples/tour.rs +++ b/examples/tour/src/main.rs @@ -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") )) } diff --git a/futures/Cargo.toml b/futures/Cargo.toml new file mode 100644 index 00000000..91860e1e --- /dev/null +++ b/futures/Cargo.toml @@ -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" diff --git a/core/src/command.rs b/futures/src/command.rs index e7885fb8..e7885fb8 100644 --- a/core/src/command.rs +++ b/futures/src/command.rs diff --git a/futures/src/executor.rs b/futures/src/executor.rs new file mode 100644 index 00000000..c2b9cc72 --- /dev/null +++ b/futures/src/executor.rs @@ -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() + } +} diff --git a/futures/src/executor/async_std.rs b/futures/src/executor/async_std.rs new file mode 100644 index 00000000..27949e31 --- /dev/null +++ b/futures/src/executor/async_std.rs @@ -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); + } +} diff --git a/futures/src/executor/null.rs b/futures/src/executor/null.rs new file mode 100644 index 00000000..6d5cf982 --- /dev/null +++ b/futures/src/executor/null.rs @@ -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) {} +} diff --git a/futures/src/executor/thread_pool.rs b/futures/src/executor/thread_pool.rs new file mode 100644 index 00000000..1ec5bf69 --- /dev/null +++ b/futures/src/executor/thread_pool.rs @@ -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); + } +} diff --git a/futures/src/executor/tokio.rs b/futures/src/executor/tokio.rs new file mode 100644 index 00000000..20802ceb --- /dev/null +++ b/futures/src/executor/tokio.rs @@ -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) + } +} diff --git a/futures/src/executor/wasm_bindgen.rs b/futures/src/executor/wasm_bindgen.rs new file mode 100644 index 00000000..69b7c7e2 --- /dev/null +++ b/futures/src/executor/wasm_bindgen.rs @@ -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); + } +} diff --git a/futures/src/lib.rs b/futures/src/lib.rs new file mode 100644 index 00000000..4872df10 --- /dev/null +++ b/futures/src/lib.rs @@ -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; diff --git a/futures/src/runtime.rs b/futures/src/runtime.rs new file mode 100644 index 00000000..9fd9899a --- /dev/null +++ b/futures/src/runtime.rs @@ -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); + } +} diff --git a/core/src/subscription.rs b/futures/src/subscription.rs index d9e7e388..b68444cd 100644 --- a/core/src/subscription.rs +++ b/futures/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. /// @@ -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; diff --git a/futures/src/subscription/tracker.rs b/futures/src/subscription/tracker.rs new file mode 100644 index 00000000..c8a1ee18 --- /dev/null +++ b/futures/src/subscription/tracker.rs @@ -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 + ); + } + }); + } +} diff --git a/native/Cargo.toml b/native/Cargo.toml index a31b6627..6276535e 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" +features = ["thread-pool"] diff --git a/native/src/lib.rs b/native/src/lib.rs index 340b9ea7..b5856c00 100644 --- a/native/src/lib.rs +++ b/native/src/lib.rs @@ -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}; diff --git a/native/src/runtime.rs b/native/src/runtime.rs new file mode 100644 index 00000000..9fa031f4 --- /dev/null +++ b/native/src/runtime.rs @@ -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>; diff --git a/native/src/subscription.rs b/native/src/subscription.rs index db88867a..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. /// @@ -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; diff --git a/native/src/subscription/events.rs b/native/src/subscription/events.rs index b7301828..7d33166e 100644 --- a/native/src/subscription/events.rs +++ b/native/src/subscription/events.rs @@ -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 } } 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<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>) { 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<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); + } + } +} @@ -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, +}; 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<T> Application for T where T: Sandbox, { + type Executor = executor::Null; type Message = T::Message; fn new() -> (Self, Command<T::Message>) { 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/native.rs b/src/widget.rs index 35441a3e..7d3a1cef 100644 --- a/src/native.rs +++ b/src/widget.rs @@ -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::*; 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; diff --git a/web/Cargo.toml b/web/Cargo.toml index 605c7462..46953863 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -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" 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 ``` diff --git a/web/src/lib.rs b/web/src/lib.rs index 7ea22e85..b1bb80e3 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -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 { 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<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; diff --git a/winit/Cargo.toml b/winit/Cargo.toml index 5727f8cf..cef41e9c 100644 --- a/winit/Cargo.toml +++ b/winit/Cargo.toml @@ -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" diff --git a/winit/src/application.rs b/winit/src/application.rs index a14924ac..4b21a930 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -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 { 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..cff9df33 --- /dev/null +++ b/winit/src/proxy.rs @@ -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(())) + } +} 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<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 - ); - } - }); - } -} |