diff options
author | 2019-11-18 23:06:28 +0100 | |
---|---|---|
committer | 2019-11-18 23:06:28 +0100 | |
commit | 5adefdf6613bfe0738b573eab1d280fa041f5417 (patch) | |
tree | 60ff05b3ee9e896d9538de19bb2412c7d6e63c6a | |
parent | 54ffefcc0b8f4558a93ca51f044594db009bacc8 (diff) | |
parent | 63dbf078fefea444073813e834c2d35fa25eb3a7 (diff) | |
download | iced-5adefdf6613bfe0738b573eab1d280fa041f5417.tar.gz iced-5adefdf6613bfe0738b573eab1d280fa041f5417.tar.bz2 iced-5adefdf6613bfe0738b573eab1d280fa041f5417.zip |
Merge pull request #62 from hecrj/feature/async-actions
Async actions
-rw-r--r-- | Cargo.toml | 3 | ||||
-rw-r--r-- | core/Cargo.toml | 7 | ||||
-rw-r--r-- | core/src/command.rs | 49 | ||||
-rw-r--r-- | core/src/lib.rs | 4 | ||||
-rw-r--r-- | core/src/widget/text_input.rs | 2 | ||||
-rw-r--r-- | examples/scroll.rs | 14 | ||||
-rw-r--r-- | examples/todos.rs | 352 | ||||
-rw-r--r-- | examples/tour.rs | 36 | ||||
-rw-r--r-- | native/Cargo.toml | 2 | ||||
-rw-r--r-- | native/src/lib.rs | 2 | ||||
-rw-r--r-- | src/lib.rs | 22 | ||||
-rw-r--r-- | src/winit.rs | 4 | ||||
-rw-r--r-- | winit/Cargo.toml | 1 | ||||
-rw-r--r-- | winit/src/application.rs | 66 |
14 files changed, 432 insertions, 132 deletions
@@ -36,6 +36,9 @@ iced_web = { version = "0.1.0-alpha", path = "web" } [dev-dependencies] env_logger = "0.7" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +directories = "2.0" [target.'cfg(target_arch = "wasm32")'.dev-dependencies] wasm-bindgen = "0.2.51" diff --git a/core/Cargo.toml b/core/Cargo.toml index a244bcba..f2492345 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -6,3 +6,10 @@ edition = "2018" description = "The essential concepts of Iced" license = "MIT" repository = "https://github.com/hecrj/iced" + +[features] +# Exposes a future-based `Command` type +command = ["futures"] + +[dependencies] +futures = { version = "0.3", optional = true } diff --git a/core/src/command.rs b/core/src/command.rs new file mode 100644 index 00000000..e1c865dd --- /dev/null +++ b/core/src/command.rs @@ -0,0 +1,49 @@ +use futures::future::{BoxFuture, Future, FutureExt}; + +pub struct Command<T> { + futures: Vec<BoxFuture<'static, T>>, +} + +impl<T> Command<T> { + pub fn none() -> Self { + Self { + futures: Vec::new(), + } + } + + pub fn perform<A>( + future: impl Future<Output = T> + 'static + Send, + f: impl Fn(T) -> A + 'static + Send, + ) -> Command<A> { + Command { + futures: vec![future.map(f).boxed()], + } + } + + pub fn batch(commands: impl Iterator<Item = Command<T>>) -> Self { + Self { + futures: commands.flat_map(|command| command.futures).collect(), + } + } + + pub fn futures(self) -> Vec<BoxFuture<'static, T>> { + self.futures + } +} + +impl<T, A> From<A> for Command<T> +where + A: Future<Output = T> + 'static + Send, +{ + fn from(future: A) -> Self { + Self { + futures: vec![future.boxed()], + } + } +} + +impl<T> std::fmt::Debug for Command<T> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Command").finish() + } +} diff --git a/core/src/lib.rs b/core/src/lib.rs index b61f2eae..3816f8a2 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -3,6 +3,8 @@ pub mod widget; mod align; mod background; mod color; +#[cfg(feature = "command")] +mod command; mod font; mod length; mod point; @@ -12,6 +14,8 @@ mod vector; pub use align::Align; pub use background::Background; pub use color::Color; +#[cfg(feature = "command")] +pub use command::Command; pub use font::Font; pub use length::Length; pub use point::Point; diff --git a/core/src/widget/text_input.rs b/core/src/widget/text_input.rs index 450a7cae..16c67954 100644 --- a/core/src/widget/text_input.rs +++ b/core/src/widget/text_input.rs @@ -80,7 +80,7 @@ where } } -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone)] pub struct State { pub is_focused: bool, cursor_position: usize, diff --git a/examples/scroll.rs b/examples/scroll.rs index 50701879..61ad2a53 100644 --- a/examples/scroll.rs +++ b/examples/scroll.rs @@ -1,12 +1,12 @@ use iced::{ - button, scrollable, Align, Application, Button, Container, Element, Image, - Length, Scrollable, Text, + button, scrollable, Align, Application, Button, Command, Container, + Element, Image, Length, Scrollable, Text, }; pub fn main() { env_logger::init(); - Example::default().run() + Example::run() } #[derive(Default)] @@ -25,16 +25,22 @@ pub enum Message { impl Application for Example { type Message = Message; + fn new() -> (Example, Command<Message>) { + (Example::default(), Command::none()) + } + fn title(&self) -> String { String::from("Scroll - Iced") } - fn update(&mut self, message: Message) { + fn update(&mut self, message: Message) -> Command<Message> { match message { Message::AddItem => { self.item_count += 1; } } + + Command::none() } fn view(&mut self) -> Element<Message> { diff --git a/examples/todos.rs b/examples/todos.rs index f921a666..a1dfc5aa 100644 --- a/examples/todos.rs +++ b/examples/todos.rs @@ -1,25 +1,36 @@ use iced::{ button, scrollable, text::HorizontalAlignment, text_input, Align, - Application, Background, Button, Checkbox, Color, Column, Container, - Element, Font, Length, Row, Scrollable, Text, TextInput, + Application, Background, Button, Checkbox, Color, Column, Command, + Container, Element, Font, Length, Row, Scrollable, Text, TextInput, }; +use serde::{Deserialize, Serialize}; pub fn main() { - Todos::default().run() + Todos::run() +} + +#[derive(Debug)] +enum Todos { + Loading, + Loaded(State), } #[derive(Debug, Default)] -struct Todos { +struct State { scroll: scrollable::State, input: text_input::State, input_value: String, filter: Filter, tasks: Vec<Task>, controls: Controls, + dirty: bool, + saving: bool, } #[derive(Debug, Clone)] -pub enum Message { +enum Message { + Loaded(Result<SavedState, LoadError>), + Saved(Result<(), SaveError>), InputChanged(String), CreateTask, FilterChanged(Filter), @@ -29,108 +40,180 @@ pub enum Message { impl Application for Todos { type Message = Message; + fn new() -> (Todos, Command<Message>) { + ( + Todos::Loading, + Command::perform(SavedState::load(), Message::Loaded), + ) + } + fn title(&self) -> String { - String::from("Todos - Iced") + let dirty = match self { + Todos::Loading => false, + Todos::Loaded(state) => state.dirty, + }; + + format!("Todos{} - Iced", if dirty { "*" } else { "" }) } - fn update(&mut self, message: Message) { - match message { - Message::InputChanged(value) => { - self.input_value = value; - } - Message::CreateTask => { - if !self.input_value.is_empty() { - self.tasks.push(Task::new(self.input_value.clone())); - self.input_value.clear(); + fn update(&mut self, message: Message) -> Command<Message> { + match self { + Todos::Loading => { + match message { + Message::Loaded(Ok(state)) => { + *self = Todos::Loaded(State { + input_value: state.input_value, + filter: state.filter, + tasks: state.tasks, + ..State::default() + }); + } + Message::Loaded(Err(_)) => { + *self = Todos::Loaded(State::default()); + } + _ => {} } + + Command::none() } - Message::FilterChanged(filter) => { - self.filter = filter; - } - Message::TaskMessage(i, TaskMessage::Delete) => { - self.tasks.remove(i); - } - Message::TaskMessage(i, task_message) => { - if let Some(task) = self.tasks.get_mut(i) { - task.update(task_message); + Todos::Loaded(state) => { + let mut saved = false; + + match message { + Message::InputChanged(value) => { + state.input_value = value; + } + Message::CreateTask => { + if !state.input_value.is_empty() { + state + .tasks + .push(Task::new(state.input_value.clone())); + state.input_value.clear(); + } + } + Message::FilterChanged(filter) => { + state.filter = filter; + } + Message::TaskMessage(i, TaskMessage::Delete) => { + state.tasks.remove(i); + } + Message::TaskMessage(i, task_message) => { + if let Some(task) = state.tasks.get_mut(i) { + task.update(task_message); + } + } + Message::Saved(_) => { + state.saving = false; + saved = true; + } + _ => {} + } + + if !saved { + state.dirty = true; + } + + if state.dirty && !state.saving { + state.dirty = false; + state.saving = true; + + Command::perform( + SavedState { + input_value: state.input_value.clone(), + filter: state.filter, + tasks: state.tasks.clone(), + } + .save(), + Message::Saved, + ) + } else { + Command::none() } } } - - dbg!(self); } fn view(&mut self) -> Element<Message> { - let Todos { - scroll, - input, - input_value, - filter, - tasks, - controls, - } = self; + match self { + Todos::Loading => loading_message(), + Todos::Loaded(State { + scroll, + input, + input_value, + filter, + tasks, + controls, + .. + }) => { + let title = Text::new("todos") + .size(100) + .color([0.5, 0.5, 0.5]) + .horizontal_alignment(HorizontalAlignment::Center); + + let input = TextInput::new( + input, + "What needs to be done?", + input_value, + Message::InputChanged, + ) + .padding(15) + .size(30) + .on_submit(Message::CreateTask); + + let controls = controls.view(&tasks, *filter); + let filtered_tasks = + tasks.iter().filter(|task| filter.matches(task)); + + let tasks: Element<_> = if filtered_tasks.count() > 0 { + tasks + .iter_mut() + .enumerate() + .filter(|(_, task)| filter.matches(task)) + .fold(Column::new().spacing(20), |column, (i, task)| { + column.push(task.view().map(move |message| { + Message::TaskMessage(i, message) + })) + }) + .into() + } else { + empty_message(match filter { + Filter::All => "You have not created a task yet...", + Filter::Active => "All your tasks are done! :D", + Filter::Completed => { + "You have not completed a task yet..." + } + }) + }; - let title = Text::new("todos") - .size(100) - .color([0.5, 0.5, 0.5]) - .horizontal_alignment(HorizontalAlignment::Center); + let content = Column::new() + .max_width(800) + .spacing(20) + .push(title) + .push(input) + .push(controls) + .push(tasks); - let input = TextInput::new( - input, - "What needs to be done?", - input_value, - Message::InputChanged, - ) - .padding(15) - .size(30) - .on_submit(Message::CreateTask); - - let controls = controls.view(&tasks, *filter); - let filtered_tasks = tasks.iter().filter(|task| filter.matches(task)); - - let tasks: Element<_> = - if filtered_tasks.count() > 0 { - tasks - .iter_mut() - .enumerate() - .filter(|(_, task)| filter.matches(task)) - .fold(Column::new().spacing(20), |column, (i, task)| { - column.push(task.view().map(move |message| { - Message::TaskMessage(i, message) - })) - }) + Scrollable::new(scroll) + .padding(40) + .push( + Container::new(content).width(Length::Fill).center_x(), + ) .into() - } else { - empty_message(match filter { - Filter::All => "You have not created a task yet...", - Filter::Active => "All your tasks are done! :D", - Filter::Completed => "You have not completed a task yet...", - }) - }; - - let content = Column::new() - .max_width(800) - .spacing(20) - .push(title) - .push(input) - .push(controls) - .push(tasks); - - Scrollable::new(scroll) - .padding(40) - .push(Container::new(content).width(Length::Fill).center_x()) - .into() + } + } } } -#[derive(Debug)] +#[derive(Debug, Clone, Serialize, Deserialize)] struct Task { description: String, completed: bool, + + #[serde(skip)] state: TaskState, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum TaskState { Idle { edit_button: button::State, @@ -141,6 +224,14 @@ pub enum TaskState { }, } +impl Default for TaskState { + fn default() -> Self { + TaskState::Idle { + edit_button: button::State::new(), + } + } +} + #[derive(Debug, Clone)] pub enum TaskMessage { Completed(bool), @@ -249,7 +340,7 @@ impl Task { } } -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone)] pub struct Controls { all_button: button::State, active_button: button::State, @@ -318,7 +409,7 @@ impl Controls { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum Filter { All, Active, @@ -341,6 +432,18 @@ impl Filter { } } +fn loading_message() -> Element<'static, Message> { + Container::new( + Text::new("Loading...") + .horizontal_alignment(HorizontalAlignment::Center) + .size(50), + ) + .width(Length::Fill) + .height(Length::Fill) + .center_y() + .into() +} + fn empty_message(message: &str) -> Element<'static, Message> { Container::new( Text::new(message) @@ -375,3 +478,80 @@ fn edit_icon() -> Text { fn delete_icon() -> Text { icon('\u{F1F8}') } + +// Persistence +#[derive(Debug, Clone, Serialize, Deserialize)] +struct SavedState { + input_value: String, + filter: Filter, + tasks: Vec<Task>, +} + +#[derive(Debug, Clone)] +enum LoadError { + FileError, + FormatError, +} + +#[derive(Debug, Clone)] +enum SaveError { + DirectoryError, + FileError, + WriteError, + FormatError, +} + +impl SavedState { + fn path() -> std::path::PathBuf { + let mut path = if let Some(project_dirs) = + directories::ProjectDirs::from("rs", "Iced", "Todos") + { + project_dirs.data_dir().into() + } else { + std::env::current_dir() + .expect("The current directory is not accessible") + }; + + path.push("todos.json"); + + path + } + + async fn load() -> Result<SavedState, LoadError> { + use std::io::Read; + + let mut contents = String::new(); + + let mut file = std::fs::File::open(Self::path()) + .map_err(|_| LoadError::FileError)?; + + file.read_to_string(&mut contents) + .map_err(|_| LoadError::FileError)?; + + serde_json::from_str(&contents).map_err(|_| LoadError::FormatError) + } + + async fn save(self) -> Result<(), SaveError> { + use std::io::Write; + + let json = serde_json::to_string_pretty(&self) + .map_err(|_| SaveError::FormatError)?; + + let path = Self::path(); + let dir = path.parent().ok_or(SaveError::DirectoryError)?; + + std::fs::create_dir_all(dir).map_err(|_| SaveError::DirectoryError)?; + + let mut file = + std::fs::File::create(path).map_err(|_| SaveError::FileError)?; + + file.write_all(json.as_bytes()) + .map_err(|_| SaveError::WriteError)?; + + // This is a simple way to save at most once every couple seconds + // We will be able to get rid of it once we implement event subscriptions + std::thread::sleep(std::time::Duration::from_secs(2)); + + Ok(()) + } +} diff --git a/examples/tour.rs b/examples/tour.rs index 34ad0a34..6d7a080f 100644 --- a/examples/tour.rs +++ b/examples/tour.rs @@ -1,13 +1,14 @@ use iced::{ button, scrollable, slider, text::HorizontalAlignment, text_input, - Application, Background, Button, Checkbox, Color, Column, Container, - Element, Image, Length, Radio, Row, Scrollable, Slider, Text, TextInput, + Application, Background, Button, Checkbox, Color, Column, Command, + Container, Element, Image, Length, Radio, Row, Scrollable, Slider, Text, + TextInput, }; pub fn main() { env_logger::init(); - Tour::new().run() + Tour::run() } pub struct Tour { @@ -18,26 +19,27 @@ pub struct Tour { debug: bool, } -impl Tour { - pub fn new() -> Tour { - Tour { - steps: Steps::new(), - scroll: scrollable::State::new(), - back_button: button::State::new(), - next_button: button::State::new(), - debug: true, - } - } -} - impl Application for Tour { type Message = Message; + fn new() -> (Tour, Command<Message>) { + ( + Tour { + steps: Steps::new(), + scroll: scrollable::State::new(), + back_button: button::State::new(), + next_button: button::State::new(), + debug: true, + }, + Command::none(), + ) + } + fn title(&self) -> String { format!("{} - Iced", self.steps.title()) } - fn update(&mut self, event: Message) { + fn update(&mut self, event: Message) -> Command<Message> { match event { Message::BackPressed => { self.steps.go_back(); @@ -49,6 +51,8 @@ impl Application for Tour { self.steps.update(step_msg, &mut self.debug); } } + + Command::none() } fn view(&mut self) -> Element<Message> { diff --git a/native/Cargo.toml b/native/Cargo.toml index 38db1610..5dc3ae1a 100644 --- a/native/Cargo.toml +++ b/native/Cargo.toml @@ -8,6 +8,6 @@ license = "MIT" repository = "https://github.com/hecrj/iced" [dependencies] -iced_core = { version = "0.1.0-alpha", path = "../core" } +iced_core = { version = "0.1.0-alpha", path = "../core", features = ["command"] } twox-hash = "1.5" raw-window-handle = "0.3" diff --git a/native/src/lib.rs b/native/src/lib.rs index 7e55064c..bd03ddcd 100644 --- a/native/src/lib.rs +++ b/native/src/lib.rs @@ -216,7 +216,7 @@ mod size; mod user_interface; pub use iced_core::{ - Align, Background, Color, Font, Length, Point, Rectangle, Vector, + Align, Background, Color, Command, Font, Length, Point, Rectangle, Vector, }; pub use element::Element; @@ -4,21 +4,23 @@ mod platform; pub use platform::*; -pub trait Application { - type Message: std::fmt::Debug; +pub trait Application: Sized { + type Message: std::fmt::Debug + Send; + + fn new() -> (Self, Command<Self::Message>); fn title(&self) -> String; - fn update(&mut self, message: Self::Message); + fn update(&mut self, message: Self::Message) -> Command<Self::Message>; fn view(&mut self) -> Element<Self::Message>; - fn run(self) + fn run() where Self: 'static + Sized, { #[cfg(not(target_arch = "wasm32"))] - iced_winit::Application::run(Instance(self)); + <Instance<Self> as iced_winit::Application>::run(); #[cfg(target_arch = "wasm32")] iced_web::Application::run(Instance(self)); @@ -35,12 +37,18 @@ where type Renderer = Renderer; type Message = A::Message; + fn new() -> (Self, Command<A::Message>) { + let (app, command) = A::new(); + + (Instance(app), command) + } + fn title(&self) -> String { self.0.title() } - fn update(&mut self, message: Self::Message) { - self.0.update(message); + fn update(&mut self, message: Self::Message) -> Command<Self::Message> { + self.0.update(message) } fn view(&mut self) -> Element<Self::Message> { diff --git a/src/winit.rs b/src/winit.rs index d35a339f..c869a269 100644 --- a/src/winit.rs +++ b/src/winit.rs @@ -2,8 +2,8 @@ pub use iced_wgpu::{Primitive, Renderer}; pub use iced_winit::{ button, scrollable, slider, text, text_input, winit, Align, Background, - Checkbox, Color, Font, Image, Length, Radio, Scrollable, Slider, Text, - TextInput, + Checkbox, Color, Command, Font, Image, Length, Radio, Scrollable, Slider, + Text, TextInput, }; pub type Element<'a, Message> = iced_winit::Element<'a, Message, Renderer>; diff --git a/winit/Cargo.toml b/winit/Cargo.toml index 2831ba2f..2a33255d 100644 --- a/winit/Cargo.toml +++ b/winit/Cargo.toml @@ -13,4 +13,5 @@ debug = [] [dependencies] iced_native = { version = "0.1.0-alpha", path = "../native" } winit = { version = "0.20.0-alpha3", git = "https://github.com/rust-windowing/winit", rev = "709808eb4e69044705fcb214bcc30556db761405"} +futures = { version = "0.3", features = ["thread-pool"] } log = "0.4" diff --git a/winit/src/application.rs b/winit/src/application.rs index 331bafa0..bf41d0c8 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -2,24 +2,26 @@ use crate::{ conversion, input::{keyboard, mouse}, renderer::{Target, Windowed}, - Cache, Container, Debug, Element, Event, Length, MouseCursor, + Cache, Command, Container, Debug, Element, Event, Length, MouseCursor, UserInterface, }; -pub trait Application { +pub trait Application: Sized { type Renderer: Windowed; - type Message: std::fmt::Debug; + type Message: std::fmt::Debug + Send; + + fn new() -> (Self, Command<Self::Message>); fn title(&self) -> String; - fn update(&mut self, message: Self::Message); + fn update(&mut self, message: Self::Message) -> Command<Self::Message>; fn view(&mut self) -> Element<Self::Message, Self::Renderer>; - fn run(mut self) + fn run() where - Self: 'static + Sized, + Self: 'static, { use winit::{ event::{self, WindowEvent}, @@ -28,10 +30,18 @@ pub trait Application { }; let mut debug = Debug::new(); - let mut title = self.title(); debug.startup_started(); - let event_loop = EventLoop::new(); + 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 external_messages = Vec::new(); + + let (mut application, init_command) = Self::new(); + spawn(init_command, &mut thread_pool, &proxy); + + let mut title = application.title(); // TODO: Ask for window settings and configure this properly let window = WindowBuilder::new() @@ -59,7 +69,7 @@ pub trait Application { debug.layout_started(); let user_interface = UserInterface::build( - document(&mut self, size, &mut debug), + document(&mut application, size, &mut debug), Cache::default(), &mut renderer, ); @@ -85,15 +95,16 @@ pub trait Application { // handled. debug.layout_started(); let mut user_interface = UserInterface::build( - document(&mut self, size, &mut debug), + document(&mut application, size, &mut debug), cache.take().unwrap(), &mut renderer, ); debug.layout_finished(); debug.event_processing_started(); - let messages = + let mut messages = user_interface.update(&renderer, events.drain(..)); + messages.extend(external_messages.drain(..)); debug.event_processing_finished(); if messages.is_empty() { @@ -113,12 +124,14 @@ pub trait Application { debug.log_message(&message); debug.update_started(); - self.update(message); + let command = application.update(message); + + spawn(command, &mut thread_pool, &proxy); debug.update_finished(); } // Update window title - let new_title = self.title(); + let new_title = application.title(); if title != new_title { window.set_title(&new_title); @@ -128,7 +141,7 @@ pub trait Application { debug.layout_started(); let user_interface = UserInterface::build( - document(&mut self, size, &mut debug), + document(&mut application, size, &mut debug), temp_cache, &mut renderer, ); @@ -143,6 +156,9 @@ pub trait Application { window.request_redraw(); } + event::Event::UserEvent(message) => { + external_messages.push(message); + } event::Event::RedrawRequested(_) => { debug.render_started(); @@ -288,3 +304,25 @@ where .height(Length::Units(size.height.round() as u16)) .into() } + +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); + } +} |