Merge pull request #62 from hecrj/feature/async-actions

Async actions
This commit is contained in:
Héctor Ramón 2019-11-18 23:06:28 +01:00 committed by GitHub
commit 5adefdf661
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 431 additions and 131 deletions

View File

@ -36,6 +36,9 @@ iced_web = { version = "0.1.0-alpha", path = "web" }
[dev-dependencies] [dev-dependencies]
env_logger = "0.7" env_logger = "0.7"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
directories = "2.0"
[target.'cfg(target_arch = "wasm32")'.dev-dependencies] [target.'cfg(target_arch = "wasm32")'.dev-dependencies]
wasm-bindgen = "0.2.51" wasm-bindgen = "0.2.51"

View File

@ -6,3 +6,10 @@ edition = "2018"
description = "The essential concepts of Iced" description = "The essential concepts of Iced"
license = "MIT" license = "MIT"
repository = "https://github.com/hecrj/iced" repository = "https://github.com/hecrj/iced"
[features]
# Exposes a future-based `Command` type
command = ["futures"]
[dependencies]
futures = { version = "0.3", optional = true }

49
core/src/command.rs Normal file
View File

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

View File

@ -3,6 +3,8 @@ pub mod widget;
mod align; mod align;
mod background; mod background;
mod color; mod color;
#[cfg(feature = "command")]
mod command;
mod font; mod font;
mod length; mod length;
mod point; mod point;
@ -12,6 +14,8 @@ mod vector;
pub use align::Align; pub use align::Align;
pub use background::Background; pub use background::Background;
pub use color::Color; pub use color::Color;
#[cfg(feature = "command")]
pub use command::Command;
pub use font::Font; pub use font::Font;
pub use length::Length; pub use length::Length;
pub use point::Point; pub use point::Point;

View File

@ -80,7 +80,7 @@ where
} }
} }
#[derive(Debug, Default)] #[derive(Debug, Default, Clone)]
pub struct State { pub struct State {
pub is_focused: bool, pub is_focused: bool,
cursor_position: usize, cursor_position: usize,

View File

@ -1,12 +1,12 @@
use iced::{ use iced::{
button, scrollable, Align, Application, Button, Container, Element, Image, button, scrollable, Align, Application, Button, Command, Container,
Length, Scrollable, Text, Element, Image, Length, Scrollable, Text,
}; };
pub fn main() { pub fn main() {
env_logger::init(); env_logger::init();
Example::default().run() Example::run()
} }
#[derive(Default)] #[derive(Default)]
@ -25,16 +25,22 @@ pub enum Message {
impl Application for Example { impl Application for Example {
type Message = Message; type Message = Message;
fn new() -> (Example, Command<Message>) {
(Example::default(), Command::none())
}
fn title(&self) -> String { fn title(&self) -> String {
String::from("Scroll - Iced") String::from("Scroll - Iced")
} }
fn update(&mut self, message: Message) { fn update(&mut self, message: Message) -> Command<Message> {
match message { match message {
Message::AddItem => { Message::AddItem => {
self.item_count += 1; self.item_count += 1;
} }
} }
Command::none()
} }
fn view(&mut self) -> Element<Message> { fn view(&mut self) -> Element<Message> {

View File

@ -1,25 +1,36 @@
use iced::{ use iced::{
button, scrollable, text::HorizontalAlignment, text_input, Align, button, scrollable, text::HorizontalAlignment, text_input, Align,
Application, Background, Button, Checkbox, Color, Column, Container, Application, Background, Button, Checkbox, Color, Column, Command,
Element, Font, Length, Row, Scrollable, Text, TextInput, Container, Element, Font, Length, Row, Scrollable, Text, TextInput,
}; };
use serde::{Deserialize, Serialize};
pub fn main() { pub fn main() {
Todos::default().run() Todos::run()
}
#[derive(Debug)]
enum Todos {
Loading,
Loaded(State),
} }
#[derive(Debug, Default)] #[derive(Debug, Default)]
struct Todos { struct State {
scroll: scrollable::State, scroll: scrollable::State,
input: text_input::State, input: text_input::State,
input_value: String, input_value: String,
filter: Filter, filter: Filter,
tasks: Vec<Task>, tasks: Vec<Task>,
controls: Controls, controls: Controls,
dirty: bool,
saving: bool,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Message { enum Message {
Loaded(Result<SavedState, LoadError>),
Saved(Result<(), SaveError>),
InputChanged(String), InputChanged(String),
CreateTask, CreateTask,
FilterChanged(Filter), FilterChanged(Filter),
@ -29,108 +40,180 @@ pub enum Message {
impl Application for Todos { impl Application for Todos {
type Message = Message; type Message = Message;
fn title(&self) -> String { fn new() -> (Todos, Command<Message>) {
String::from("Todos - Iced") (
Todos::Loading,
Command::perform(SavedState::load(), Message::Loaded),
)
} }
fn update(&mut self, message: Message) { fn title(&self) -> String {
match message { let dirty = match self {
Message::InputChanged(value) => { Todos::Loading => false,
self.input_value = value; Todos::Loaded(state) => state.dirty,
} };
Message::CreateTask => {
if !self.input_value.is_empty() { format!("Todos{} - Iced", if dirty { "*" } else { "" })
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) => { Todos::Loaded(state) => {
self.filter = filter; let mut saved = false;
}
Message::TaskMessage(i, TaskMessage::Delete) => { match message {
self.tasks.remove(i); Message::InputChanged(value) => {
} state.input_value = value;
Message::TaskMessage(i, task_message) => { }
if let Some(task) = self.tasks.get_mut(i) { Message::CreateTask => {
task.update(task_message); 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> { fn view(&mut self) -> Element<Message> {
let Todos { match self {
scroll, Todos::Loading => loading_message(),
input, Todos::Loaded(State {
input_value, scroll,
filter, input,
tasks, input_value,
controls, filter,
} = self; tasks,
controls,
..
}) => {
let title = Text::new("todos")
.size(100)
.color([0.5, 0.5, 0.5])
.horizontal_alignment(HorizontalAlignment::Center);
let title = Text::new("todos") let input = TextInput::new(
.size(100) input,
.color([0.5, 0.5, 0.5]) "What needs to be done?",
.horizontal_alignment(HorizontalAlignment::Center); input_value,
Message::InputChanged,
)
.padding(15)
.size(30)
.on_submit(Message::CreateTask);
let input = TextInput::new( let controls = controls.view(&tasks, *filter);
input, let filtered_tasks =
"What needs to be done?", tasks.iter().filter(|task| filter.matches(task));
input_value,
Message::InputChanged,
)
.padding(15)
.size(30)
.on_submit(Message::CreateTask);
let controls = controls.view(&tasks, *filter); let tasks: Element<_> = if filtered_tasks.count() > 0 {
let filtered_tasks = tasks.iter().filter(|task| filter.matches(task)); tasks
.iter_mut()
let tasks: Element<_> = .enumerate()
if filtered_tasks.count() > 0 { .filter(|(_, task)| filter.matches(task))
tasks .fold(Column::new().spacing(20), |column, (i, task)| {
.iter_mut() column.push(task.view().map(move |message| {
.enumerate() Message::TaskMessage(i, message)
.filter(|(_, task)| filter.matches(task)) }))
.fold(Column::new().spacing(20), |column, (i, task)| { })
column.push(task.view().map(move |message| { .into()
Message::TaskMessage(i, message) } 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() .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 { struct Task {
description: String, description: String,
completed: bool, completed: bool,
#[serde(skip)]
state: TaskState, state: TaskState,
} }
#[derive(Debug)] #[derive(Debug, Clone)]
pub enum TaskState { pub enum TaskState {
Idle { Idle {
edit_button: button::State, 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)] #[derive(Debug, Clone)]
pub enum TaskMessage { pub enum TaskMessage {
Completed(bool), Completed(bool),
@ -249,7 +340,7 @@ impl Task {
} }
} }
#[derive(Debug, Default)] #[derive(Debug, Default, Clone)]
pub struct Controls { pub struct Controls {
all_button: button::State, all_button: button::State,
active_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 { pub enum Filter {
All, All,
Active, 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> { fn empty_message(message: &str) -> Element<'static, Message> {
Container::new( Container::new(
Text::new(message) Text::new(message)
@ -375,3 +478,80 @@ fn edit_icon() -> Text {
fn delete_icon() -> Text { fn delete_icon() -> Text {
icon('\u{F1F8}') 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(())
}
}

View File

@ -1,13 +1,14 @@
use iced::{ use iced::{
button, scrollable, slider, text::HorizontalAlignment, text_input, button, scrollable, slider, text::HorizontalAlignment, text_input,
Application, Background, Button, Checkbox, Color, Column, Container, Application, Background, Button, Checkbox, Color, Column, Command,
Element, Image, Length, Radio, Row, Scrollable, Slider, Text, TextInput, Container, Element, Image, Length, Radio, Row, Scrollable, Slider, Text,
TextInput,
}; };
pub fn main() { pub fn main() {
env_logger::init(); env_logger::init();
Tour::new().run() Tour::run()
} }
pub struct Tour { pub struct Tour {
@ -18,26 +19,27 @@ pub struct Tour {
debug: bool, 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 { impl Application for Tour {
type Message = Message; 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 { fn title(&self) -> String {
format!("{} - Iced", self.steps.title()) format!("{} - Iced", self.steps.title())
} }
fn update(&mut self, event: Message) { fn update(&mut self, event: Message) -> Command<Message> {
match event { match event {
Message::BackPressed => { Message::BackPressed => {
self.steps.go_back(); self.steps.go_back();
@ -49,6 +51,8 @@ impl Application for Tour {
self.steps.update(step_msg, &mut self.debug); self.steps.update(step_msg, &mut self.debug);
} }
} }
Command::none()
} }
fn view(&mut self) -> Element<Message> { fn view(&mut self) -> Element<Message> {

View File

@ -8,6 +8,6 @@ license = "MIT"
repository = "https://github.com/hecrj/iced" repository = "https://github.com/hecrj/iced"
[dependencies] [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" twox-hash = "1.5"
raw-window-handle = "0.3" raw-window-handle = "0.3"

View File

@ -216,7 +216,7 @@ mod size;
mod user_interface; mod user_interface;
pub use iced_core::{ 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; pub use element::Element;

View File

@ -4,21 +4,23 @@ mod platform;
pub use platform::*; pub use platform::*;
pub trait Application { pub trait Application: Sized {
type Message: std::fmt::Debug; type Message: std::fmt::Debug + Send;
fn new() -> (Self, Command<Self::Message>);
fn title(&self) -> String; 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 view(&mut self) -> Element<Self::Message>;
fn run(self) fn run()
where where
Self: 'static + Sized, Self: 'static + Sized,
{ {
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
iced_winit::Application::run(Instance(self)); <Instance<Self> as iced_winit::Application>::run();
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
iced_web::Application::run(Instance(self)); iced_web::Application::run(Instance(self));
@ -35,12 +37,18 @@ where
type Renderer = Renderer; type Renderer = Renderer;
type Message = A::Message; type Message = A::Message;
fn new() -> (Self, Command<A::Message>) {
let (app, command) = A::new();
(Instance(app), command)
}
fn title(&self) -> String { fn title(&self) -> String {
self.0.title() self.0.title()
} }
fn update(&mut self, message: Self::Message) { fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
self.0.update(message); self.0.update(message)
} }
fn view(&mut self) -> Element<Self::Message> { fn view(&mut self) -> Element<Self::Message> {

View File

@ -2,8 +2,8 @@ pub use iced_wgpu::{Primitive, Renderer};
pub use iced_winit::{ pub use iced_winit::{
button, scrollable, slider, text, text_input, winit, Align, Background, button, scrollable, slider, text, text_input, winit, Align, Background,
Checkbox, Color, Font, Image, Length, Radio, Scrollable, Slider, Text, Checkbox, Color, Command, Font, Image, Length, Radio, Scrollable, Slider,
TextInput, Text, TextInput,
}; };
pub type Element<'a, Message> = iced_winit::Element<'a, Message, Renderer>; pub type Element<'a, Message> = iced_winit::Element<'a, Message, Renderer>;

View File

@ -13,4 +13,5 @@ debug = []
[dependencies] [dependencies]
iced_native = { version = "0.1.0-alpha", path = "../native" } iced_native = { version = "0.1.0-alpha", path = "../native" }
winit = { version = "0.20.0-alpha3", git = "https://github.com/rust-windowing/winit", rev = "709808eb4e69044705fcb214bcc30556db761405"} 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" log = "0.4"

View File

@ -2,24 +2,26 @@ use crate::{
conversion, conversion,
input::{keyboard, mouse}, input::{keyboard, mouse},
renderer::{Target, Windowed}, renderer::{Target, Windowed},
Cache, Container, Debug, Element, Event, Length, MouseCursor, Cache, Command, Container, Debug, Element, Event, Length, MouseCursor,
UserInterface, UserInterface,
}; };
pub trait Application { pub trait Application: Sized {
type Renderer: Windowed; 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 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 view(&mut self) -> Element<Self::Message, Self::Renderer>;
fn run(mut self) fn run()
where where
Self: 'static + Sized, Self: 'static,
{ {
use winit::{ use winit::{
event::{self, WindowEvent}, event::{self, WindowEvent},
@ -28,10 +30,18 @@ pub trait Application {
}; };
let mut debug = Debug::new(); let mut debug = Debug::new();
let mut title = self.title();
debug.startup_started(); 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 // TODO: Ask for window settings and configure this properly
let window = WindowBuilder::new() let window = WindowBuilder::new()
@ -59,7 +69,7 @@ pub trait Application {
debug.layout_started(); debug.layout_started();
let user_interface = UserInterface::build( let user_interface = UserInterface::build(
document(&mut self, size, &mut debug), document(&mut application, size, &mut debug),
Cache::default(), Cache::default(),
&mut renderer, &mut renderer,
); );
@ -85,15 +95,16 @@ pub trait Application {
// handled. // handled.
debug.layout_started(); debug.layout_started();
let mut user_interface = UserInterface::build( let mut user_interface = UserInterface::build(
document(&mut self, size, &mut debug), document(&mut application, size, &mut debug),
cache.take().unwrap(), cache.take().unwrap(),
&mut renderer, &mut renderer,
); );
debug.layout_finished(); debug.layout_finished();
debug.event_processing_started(); debug.event_processing_started();
let messages = let mut messages =
user_interface.update(&renderer, events.drain(..)); user_interface.update(&renderer, events.drain(..));
messages.extend(external_messages.drain(..));
debug.event_processing_finished(); debug.event_processing_finished();
if messages.is_empty() { if messages.is_empty() {
@ -113,12 +124,14 @@ pub trait Application {
debug.log_message(&message); debug.log_message(&message);
debug.update_started(); debug.update_started();
self.update(message); let command = application.update(message);
spawn(command, &mut thread_pool, &proxy);
debug.update_finished(); debug.update_finished();
} }
// Update window title // Update window title
let new_title = self.title(); let new_title = application.title();
if title != new_title { if title != new_title {
window.set_title(&new_title); window.set_title(&new_title);
@ -128,7 +141,7 @@ pub trait Application {
debug.layout_started(); debug.layout_started();
let user_interface = UserInterface::build( let user_interface = UserInterface::build(
document(&mut self, size, &mut debug), document(&mut application, size, &mut debug),
temp_cache, temp_cache,
&mut renderer, &mut renderer,
); );
@ -143,6 +156,9 @@ pub trait Application {
window.request_redraw(); window.request_redraw();
} }
event::Event::UserEvent(message) => {
external_messages.push(message);
}
event::Event::RedrawRequested(_) => { event::Event::RedrawRequested(_) => {
debug.render_started(); debug.render_started();
@ -288,3 +304,25 @@ where
.height(Length::Units(size.height.round() as u16)) .height(Length::Units(size.height.round() as u16))
.into() .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);
}
}