From f15ec280887ebd75c909a52559a15c4e45c010cd Mon Sep 17 00:00:00 2001 From: Junichi Sugiura Date: Mon, 10 Oct 2022 14:04:54 +0200 Subject: [PATCH] PoC: Async action support (#118) * poc AsyncActionPlugin * mockup AsyncActionPlugin derive macro * poc AsyncActionCreator and add NoAsynAction type alias * replace UiActionParser with ActionParser * support AsyncAction with CliPlugin * add DipStartupStage --- Cargo.toml | 20 +- README.md | 10 +- examples/cli/async.rs | 114 +++++++++ examples/cli/cli.rs | 143 +----------- examples/desktop/async.rs | 94 ++++++++ examples/desktop/counter.rs | 4 +- examples/desktop/keyboard/bindings.rs | 4 +- examples/desktop/keyboard/keyboard_event.rs | 4 +- examples/desktop/minimum.rs | 4 +- examples/desktop/root_props.rs | 7 +- examples/desktop/state_management/ecs.rs | 4 +- .../desktop/state_management/global_state.rs | 4 +- .../desktop/state_management/local_state.rs | 4 +- examples/desktop/window/multiple_windows.rs | 6 +- examples/desktop/window/render_mode.rs | 4 +- .../desktop/window/scale_factor_override.rs | 4 +- examples/desktop/window/settings.rs | 4 +- examples/todomvc/src/main.rs | 4 +- examples/todomvc/src/ui.rs | 2 +- packages/core/Cargo.toml | 1 + packages/core/src/lib.rs | 5 +- packages/core/src/schedule.rs | 27 ++- packages/desktop/src/context.rs | 11 +- packages/desktop/src/event.rs | 4 +- packages/desktop/src/event_loop.rs | 23 +- packages/desktop/src/hooks.rs | 10 +- packages/desktop/src/plugin.rs | 42 ++-- packages/desktop/src/virtual_dom.rs | 12 +- packages/desktop/src/window.rs | 19 +- .../src/{ui_action.rs => action_parser.rs} | 220 ++++++++++++------ packages/macro/src/cli.rs | 67 +++++- packages/macro/src/lib.rs | 15 +- packages/macro/src/subcommand.rs | 2 +- packages/macro/src/ui_state.rs | 4 +- packages/task/Cargo.toml | 15 ++ packages/task/src/async_action.rs | 31 +++ packages/task/src/lib.rs | 8 + src/lib.rs | 2 + src/main.rs | 3 +- 39 files changed, 653 insertions(+), 308 deletions(-) create mode 100644 examples/cli/async.rs create mode 100644 examples/desktop/async.rs rename packages/macro/src/{ui_action.rs => action_parser.rs} (58%) create mode 100644 packages/task/Cargo.toml create mode 100644 packages/task/src/async_action.rs create mode 100644 packages/task/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index c7abd06..80f7d07 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,17 +14,20 @@ keywords = ["declarative-ui", "ecs", "bevy", "dioxus", "cross-platform"] bevy = { version = "0.8", default-features = false } clap = { version = "3.2", features = ["derive"], optional = true } dioxus = { version = "0.2", features = ["fermi"] } +tokio = { version = "1.18", features = ["rt-multi-thread", "sync"], default-features = false } dip_core = { version = "0.1", path = "./packages/core" } -dip_desktop = { version = "0.1", path = "./packages/desktop", optional = true } dip_macro = { version = "0.1", path = "./packages/macro" } + dip_cli = { version = "0.1", path = "./packages/cli", optional = true } +dip_desktop = { version = "0.1", path = "./packages/desktop", optional = true } [dev-dependencies] leafwing-input-manager = { version = "0.5", default-features = false } config = "0.13" serde = { version = "1.0", features = ["derive"] } dirs = "4.0" +reqwest = { version = "0.11", features = ["json"] } [features] default = ["cli"] @@ -37,6 +40,7 @@ members = [ "packages/core", "packages/desktop", "packages/macro", + "packages/task", "examples/todomvc", ] @@ -46,6 +50,11 @@ name = "cli" path = "examples/cli/cli.rs" required-features = ["cli"] +[[example]] +name = "cli_async" +path = "examples/cli/async.rs" +required-features = ["cli"] + [[example]] name = "cli_config" path = "examples/cli/config/main.rs" @@ -53,8 +62,8 @@ required-features = ["cli"] # Desktop [[example]] -name = "minimum" -path = "examples/desktop/minimum.rs" +name = "desktop_async" +path = "examples/desktop/async.rs" required-features = ["desktop"] [[example]] @@ -62,6 +71,11 @@ name = "counter" path = "examples/desktop/counter.rs" required-features = ["desktop"] +[[example]] +name = "minimum" +path = "examples/desktop/minimum.rs" +required-features = ["desktop"] + [[example]] name = "root_props" path = "examples/desktop/root_props.rs" diff --git a/README.md b/README.md index 23faa13..afdc374 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ fn main() { title: "dip Plugin Example".to_string(), ..Default::default() }) - .add_plugin(DesktopPlugin::::new(Root)) + .add_plugin(DesktopPlugin::::new(Root)) .run(); } @@ -80,7 +80,7 @@ fn main() { title: "Desktop App".to_string(), ..Default::default() }) - .add_plugin(DesktopPlugin::::new(Root)) + .add_plugin(DesktopPlugin::::new(Root)) .run(); } @@ -127,7 +127,7 @@ use dip::{bevy::log::LogPlugin, prelude::*}; fn main() { App::new() - .add_plugin(CliPlugin) + .add_plugin(CliPlugin::::oneshot()) .add_plugin(ActionPlugin) .add_plugin(LogPlugin) .add_system(log_root_arg) @@ -248,7 +248,7 @@ use dip::prelude::*; fn main() { App::new() // Step 7. Put it all together - .add_plugin(DesktopPlugin::::new(Root)) + .add_plugin(DesktopPlugin::::new(Root)) .add_plugin(UiStatePlugin) // generated by #[ui_state] .add_plugin(UiActionPlugin) // generated by #[ui_action] .add_system(update_name) @@ -308,7 +308,7 @@ fn Root(cx: Scope) -> Element { // Step 5. Select state let name = use_read(&cx, NAME); - let window = use_window::(&cx); + let window = use_window::(&cx); cx.render(rsx! { h1 { "Hello, {name.value} !" } diff --git a/examples/cli/async.rs b/examples/cli/async.rs new file mode 100644 index 0000000..fc81d75 --- /dev/null +++ b/examples/cli/async.rs @@ -0,0 +1,114 @@ +use dip::{ + bevy::{ + app::AppExit, + log::{self, LogPlugin}, + }, + prelude::*, +}; +use serde::Deserialize; + +fn main() { + App::new() + .add_plugin(CliPlugin::::continuous()) + .add_plugin(ActionPlugin) + .add_plugin(AsyncActionPlugin) + .add_plugin(LogPlugin) + .add_startup_system(fetch_ip_address) + .add_startup_system(fetch_user_agent) + .add_system(handle_get_ip_address) + .add_system(handle_get_user_agent) + .run(); +} + +#[derive(CliPlugin, clap::Parser)] +struct Cli { + #[clap(subcommand)] + action: Action, +} + +#[derive(SubcommandPlugin, clap::Subcommand, Clone)] +pub enum Action { + IpAddress, + UserAgent, +} + +#[allow(dead_code)] +#[derive(Clone, Debug, Deserialize, Default)] +pub struct GetIpAddress { + origin: String, +} + +#[allow(dead_code)] +#[derive(Clone, Debug, Deserialize, Default)] +pub struct GetUserAgent { + #[serde(rename = "user-agent")] + user_agent: Option, +} + +#[async_action] +impl AsyncActionCreator { + async fn get_ip_address() -> GetIpAddress { + reqwest::get("https://httpbin.org/ip") + .await + .unwrap() + .json::() + .await + .unwrap() + } + + async fn get_user_agent() -> GetUserAgent { + static APP_USER_AGENT: &str = + concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),); + let client = reqwest::Client::builder() + .user_agent(APP_USER_AGENT) + .build() + .unwrap(); + + client + .get("https://httpbin.org/user-agent") + .send() + .await + .unwrap() + .json::() + .await + .unwrap() + } +} + +fn fetch_ip_address( + mut events: EventReader, + async_action: Res>, +) { + for _ in events.iter() { + async_action.send(AsyncAction::get_ip_address()); + } +} + +fn fetch_user_agent( + mut events: EventReader, + async_action: Res>, +) { + for _ in events.iter() { + async_action.send(AsyncAction::get_user_agent()); + } +} + +fn handle_get_ip_address( + mut actions: EventReader, + mut app_exit: EventWriter, +) { + for action in actions.iter() { + log::info!("{action:#?}"); + app_exit.send(AppExit); + } +} + +fn handle_get_user_agent( + mut actions: EventReader, + mut app_exit: EventWriter, +) { + for action in actions.iter() { + log::info!("{action:#?}"); + app_exit.send(AppExit); + } +} diff --git a/examples/cli/cli.rs b/examples/cli/cli.rs index 0bbc005..0944112 100644 --- a/examples/cli/cli.rs +++ b/examples/cli/cli.rs @@ -2,7 +2,7 @@ use dip::{bevy::log::LogPlugin, prelude::*}; fn main() { App::new() - .add_plugin(CliPlugin) + .add_plugin(CliPlugin::::oneshot()) .add_plugin(ActionPlugin) .add_plugin(TodoActionPlugin) .add_plugin(LogPlugin) @@ -116,144 +116,3 @@ fn handle_list_todo(mut events: EventReader) { info!("{e:?}"); } } - -// generated - -// // CliPlugin -// pub struct CliPlugin; - -// impl ::bevy::app::Plugin for CliPlugin { -// fn build(&self, app: &mut ::bevy::app::App) { -// use clap::Parser; -// // use ::dip::bevy::ecs::{ -// // schedule::ParallelSystemDescriptorCoercion, -// // system::IntoSystem, -// // }; - -// let cli = Cli::parse(); - -// app.add_plugin(::dip::core::schedule::UiSchedulePlugin) -// .insert_resource(cli.action.clone()) -// .insert_resource(cli) -// .add_event::() -// .set_runner(|mut app| { -// app.update(); -// }) -// .add_system_to_stage( -// ::dip::core::schedule::UiStage::Action, -// convert_subcommand_to_event.before(handle_action), -// ); -// } -// } - -// fn convert_subcommand_to_event( -// subcommand: ::dip::bevy::ecs::system::Res, -// mut action: ::dip::bevy::ecs::event::EventWriter, -// ) { -// action.send(subcommand.clone()); -// } - -// // Action Subcommand -// pub struct ActionPlugin; - -// impl ::bevy::app::Plugin for ActionPlugin { -// fn build(&self, app: &mut ::bevy::app::App) { -// // use ::dip::bevy::ecs::{ -// // schedule::ParallelSystemDescriptorCoercion, -// // system::IntoSystem, -// // }; - -// app.add_event::() -// .add_event::() -// .add_event::() -// .add_event::() -// .add_system_to_stage( -// ::dip::core::schedule::UiStage::Action, -// handle_action.before(handle_todo_action), -// ); -// } -// } - -// // Events -// #[derive(Clone, Debug)] -// pub struct PingAction; - -// #[derive(Clone, Debug)] -// pub struct HelloAction { -// name: Option, -// } - -// // only when type name is different (if variant_ident != first_field_ty) -// pub type Hello2Action = Hello2Args; - -// pub fn handle_action( -// mut events: ::dip::bevy::ecs::event::EventReader, -// mut ping_action: ::dip::bevy::ecs::event::EventWriter, -// mut hello_action: ::dip::bevy::ecs::event::EventWriter, -// mut hello2_action: ::dip::bevy::ecs::event::EventWriter, -// mut todo_action: ::dip::bevy::ecs::event::EventWriter, -// ) { -// for e in events.iter() { -// match e { -// Action::Ping => { -// ping_action.send(PingAction); -// } -// Action::Hello { name } => hello_action.send(HelloAction { name: name.clone() }), -// Action::Hello2(x) => { -// hello2_action.send(x.clone()); -// } -// Action::Todo(x) => { -// todo_action.send(x.clone()); -// } -// } -// } -// } - -// // TodoAction Subcommand -// pub struct TodoActionPlugin; - -// impl ::bevy::app::Plugin for TodoActionPlugin { -// fn build(&self, app: &mut ::bevy::app::App) { -// // use ::dip::bevy::ecs::{ -// // schedule::ParallelSystemDescriptorCoercion, -// // system::IntoSystem, -// // }; - -// app.add_event::() -// .add_event::() -// .add_event::() -// .add_system_to_stage(::dip::core::schedule::UiStage::Action, handle_todo_action); -// } -// } - -// // Events -// #[derive(Clone, Debug)] -// pub struct ListTodoAction; - -// #[derive(Clone, Debug)] -// pub struct AddTodoAction { -// name: Option, -// } - -// // pub type RemoveTodoAction = RemoveTodoAction; - -// pub fn handle_todo_action( -// mut events: ::dip::bevy::ecs::event::EventReader, -// mut list_todo_action: ::dip::bevy::ecs::event::EventWriter, -// mut add_todo_action: ::dip::bevy::ecs::event::EventWriter, -// mut remove_todo_action: ::dip::bevy::ecs::event::EventWriter, -// ) { -// for e in events.iter() { -// match e { -// TodoAction::List => { -// list_todo_action.send(ListTodoAction); -// } -// TodoAction::Add { name } => { -// add_todo_action.send(AddTodoAction { name: name.clone() }); -// } -// TodoAction::Remove(x) => { -// remove_todo_action.send(x.clone()); -// } -// } -// } -// } diff --git a/examples/desktop/async.rs b/examples/desktop/async.rs new file mode 100644 index 0000000..51525c8 --- /dev/null +++ b/examples/desktop/async.rs @@ -0,0 +1,94 @@ +use dip::{bevy::log::LogPlugin, prelude::*}; +use serde::Deserialize; + +fn main() { + App::new() + .add_plugin(DesktopPlugin::::new(Root)) + .add_plugin(LogPlugin) + .add_plugin(UiStatePlugin) + .add_plugin(AsyncActionPlugin) + .add_startup_system(fetch_all) + .add_system(handle_get_ip_address) + .add_system(handle_get_user_agent) + .run(); +} + +#[ui_state] +struct UiState { + ip_address: GetIpAddress, + user_agent: GetUserAgent, +} + +#[derive(Clone, Debug, Deserialize, Default)] +pub struct GetIpAddress { + origin: String, +} + +#[derive(Clone, Debug, Deserialize, Default)] +pub struct GetUserAgent { + #[serde(rename = "user-agent")] + user_agent: Option, +} + +#[async_action] +impl AsyncActionCreator { + async fn get_ip_address() -> GetIpAddress { + reqwest::get("https://httpbin.org/ip") + .await + .unwrap() + .json::() + .await + .unwrap() + } + + async fn get_user_agent() -> GetUserAgent { + static APP_USER_AGENT: &str = + concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),); + let client = reqwest::Client::builder() + .user_agent(APP_USER_AGENT) + .build() + .unwrap(); + client + .get("https://httpbin.org/user-agent") + .send() + .await + .unwrap() + .json::() + .await + .unwrap() + } +} + +fn fetch_all(async_action: Res>) { + async_action.send(AsyncAction::get_ip_address()); + async_action.send(AsyncAction::get_user_agent()); +} + +fn handle_get_ip_address( + mut actions: EventReader, + mut ip_address: ResMut, +) { + for action in actions.iter() { + *ip_address = action.clone(); + } +} + +fn handle_get_user_agent( + mut actions: EventReader, + mut user_agent: ResMut, +) { + for action in actions.iter() { + *user_agent = action.clone(); + } +} + +#[allow(non_snake_case)] +fn Root(cx: Scope) -> Element { + let ip_address = use_read(&cx, IP_ADDRESS); + let user_agent = use_read(&cx, USER_AGENT); + + cx.render(rsx! { + h1 { "ip address: {ip_address.origin}" } + h1 { "user_agent: {user_agent.user_agent:?}" } + }) +} diff --git a/examples/desktop/counter.rs b/examples/desktop/counter.rs index 1cdd4f1..07b11fc 100644 --- a/examples/desktop/counter.rs +++ b/examples/desktop/counter.rs @@ -3,7 +3,7 @@ use dip::{bevy::log::LogPlugin, prelude::*}; fn main() { App::new() // Step 7. Put it all together - .add_plugin(DesktopPlugin::::new(Root)) + .add_plugin(DesktopPlugin::::new(Root)) // ui_state attribute macro automatically generates UiStatePlugin .add_plugin(UiStatePlugin) // automatically generated by ui_action macro @@ -22,7 +22,7 @@ fn Root(cx: Scope) -> Element { let disabled = count.value == 0; - let window = use_window::(&cx); + let window = use_window::(&cx); cx.render(rsx! { h1 { "Counter Example" } diff --git a/examples/desktop/keyboard/bindings.rs b/examples/desktop/keyboard/bindings.rs index 7d09c94..83043a1 100644 --- a/examples/desktop/keyboard/bindings.rs +++ b/examples/desktop/keyboard/bindings.rs @@ -21,7 +21,9 @@ fn main() { }) .add_plugin(LogPlugin) .add_plugin(TimePlugin) - .add_plugin(DesktopPlugin::::new(Root)) + .add_plugin(DesktopPlugin::::new( + Root, + )) .add_plugin(InputManagerPlugin::::default()) .add_startup_system(setup) .add_system(close_window) diff --git a/examples/desktop/keyboard/keyboard_event.rs b/examples/desktop/keyboard/keyboard_event.rs index 79f9623..ee157c8 100644 --- a/examples/desktop/keyboard/keyboard_event.rs +++ b/examples/desktop/keyboard/keyboard_event.rs @@ -9,7 +9,7 @@ fn main() { keyboard_event: true, ..Default::default() }) - .add_plugin(DesktopPlugin::::new(Root)) + .add_plugin(DesktopPlugin::::new(Root)) .add_plugin(UiStatePlugin) .add_plugin(UiActionPlugin) .add_plugin(LogPlugin) @@ -23,7 +23,7 @@ fn main() { fn Root(cx: Scope) -> Element { let event_type = use_read(&cx, EVENT_TYPE); let input_result = use_read(&cx, INPUT_RESULT); - let window = use_window::(&cx); + let window = use_window::(&cx); cx.render(rsx! { h1 { "Keyboard Event Example" } diff --git a/examples/desktop/minimum.rs b/examples/desktop/minimum.rs index 44843ce..a7b8f05 100644 --- a/examples/desktop/minimum.rs +++ b/examples/desktop/minimum.rs @@ -6,7 +6,9 @@ fn main() { title: "Minimum Example".to_string(), ..Default::default() }) - .add_plugin(DesktopPlugin::::new(Root)) + .add_plugin(DesktopPlugin::::new( + Root, + )) .run(); } diff --git a/examples/desktop/root_props.rs b/examples/desktop/root_props.rs index f202d61..3197894 100644 --- a/examples/desktop/root_props.rs +++ b/examples/desktop/root_props.rs @@ -6,7 +6,12 @@ fn main() { title: "Props Example".to_string(), ..Default::default() }) - .add_plugin(DesktopPlugin::::new(Root)) + .add_plugin(DesktopPlugin::< + NoUiState, + NoUiAction, + NoAsyncAction, + RootProps, + >::new(Root)) .add_plugin(LogPlugin) .run(); } diff --git a/examples/desktop/state_management/ecs.rs b/examples/desktop/state_management/ecs.rs index a448f11..054aff3 100644 --- a/examples/desktop/state_management/ecs.rs +++ b/examples/desktop/state_management/ecs.rs @@ -2,7 +2,7 @@ use dip::prelude::*; fn main() { App::new() - .add_plugin(DesktopPlugin::::new(Root)) + .add_plugin(DesktopPlugin::::new(Root)) .add_plugin(UiStatePlugin) .add_plugin(UiActionPlugin) .add_system(update_name) @@ -12,7 +12,7 @@ fn main() { #[allow(non_snake_case)] fn Root(cx: Scope) -> Element { let name = use_read(&cx, NAME); - let window = use_window::(&cx); + let window = use_window::(&cx); cx.render(rsx! { h1 { "Hello, {name.value} !" } diff --git a/examples/desktop/state_management/global_state.rs b/examples/desktop/state_management/global_state.rs index ea9a5ee..2a2019b 100644 --- a/examples/desktop/state_management/global_state.rs +++ b/examples/desktop/state_management/global_state.rs @@ -6,7 +6,9 @@ fn main() { title: "Local State".to_string(), ..Default::default() }) - .add_plugin(DesktopPlugin::::new(Root)) + .add_plugin(DesktopPlugin::::new( + Root, + )) .run(); } diff --git a/examples/desktop/state_management/local_state.rs b/examples/desktop/state_management/local_state.rs index 1488aa6..ed045e6 100644 --- a/examples/desktop/state_management/local_state.rs +++ b/examples/desktop/state_management/local_state.rs @@ -6,7 +6,9 @@ fn main() { title: "Local State".to_string(), ..Default::default() }) - .add_plugin(DesktopPlugin::::new(Root)) + .add_plugin(DesktopPlugin::::new( + Root, + )) .run(); } diff --git a/examples/desktop/window/multiple_windows.rs b/examples/desktop/window/multiple_windows.rs index ec4a034..eb82120 100644 --- a/examples/desktop/window/multiple_windows.rs +++ b/examples/desktop/window/multiple_windows.rs @@ -10,7 +10,9 @@ use dip::{ fn main() { App::new() .add_plugin(LogPlugin) - .add_plugin(DesktopPlugin::::new(Root)) + .add_plugin(DesktopPlugin::::new( + Root, + )) .add_event::() .add_system(create_new_window) .run(); @@ -18,7 +20,7 @@ fn main() { #[allow(non_snake_case)] fn Root(cx: Scope) -> Element { - let window = use_window::(&cx); + let window = use_window::(&cx); cx.render(rsx! { h1 { "Multiple Windows isn't supported yet!" } diff --git a/examples/desktop/window/render_mode.rs b/examples/desktop/window/render_mode.rs index e837aa6..d5f9ff0 100644 --- a/examples/desktop/window/render_mode.rs +++ b/examples/desktop/window/render_mode.rs @@ -11,7 +11,7 @@ fn main() { App::new() .add_plugin(LogPlugin) .add_plugin(TimePlugin) - .add_plugin(DesktopPlugin::::new(Root)) + .add_plugin(DesktopPlugin::::new(Root)) .add_plugin(UiStatePlugin) .add_plugin(UiActionPlugin) .add_system(increment_frame) @@ -21,7 +21,7 @@ fn main() { #[allow(non_snake_case)] fn Root(cx: Scope) -> Element { - let window = use_window::(&cx); + let window = use_window::(&cx); let frame = use_read(&cx, FRAME); let render_mode = use_read(&cx, RENDER_MODE); diff --git a/examples/desktop/window/scale_factor_override.rs b/examples/desktop/window/scale_factor_override.rs index 9ff31c2..65ae45a 100644 --- a/examples/desktop/window/scale_factor_override.rs +++ b/examples/desktop/window/scale_factor_override.rs @@ -12,7 +12,9 @@ fn main() { keyboard_event: true, ..Default::default() }) - .add_plugin(DesktopPlugin::::new(Root)) + .add_plugin(DesktopPlugin::::new( + Root, + )) .add_plugin(LogPlugin) .add_system(toggle_override) .add_system(change_scale_factor) diff --git a/examples/desktop/window/settings.rs b/examples/desktop/window/settings.rs index 18f4d6c..d369f04 100644 --- a/examples/desktop/window/settings.rs +++ b/examples/desktop/window/settings.rs @@ -15,7 +15,9 @@ fn main() { }) .add_plugin(LogPlugin) .add_plugin(TimePlugin) - .add_plugin(DesktopPlugin::::new(Root)) + .add_plugin(DesktopPlugin::::new( + Root, + )) .add_system(change_title) .add_system(toggle_cursor) // .add_system(cycle_cursor_icon) diff --git a/examples/todomvc/src/main.rs b/examples/todomvc/src/main.rs index 7c77a36..8473d25 100644 --- a/examples/todomvc/src/main.rs +++ b/examples/todomvc/src/main.rs @@ -33,7 +33,7 @@ fn main() { custom_head: Some(format!("", css)), ..Default::default() }) - .add_plugin(DesktopPlugin::::new(Root)) + .add_plugin(DesktopPlugin::::new(Root)) .add_plugin(LogPlugin) .add_plugin(UiStatePlugin) .add_plugin(UiActionPlugin) @@ -48,6 +48,6 @@ fn main() { .add_system(toggle_all.before(toggle_done)) .add_system(change_filter) .add_system(clear_completed) - .add_system_to_stage(UiStage::Prepare, new_ui_todo_list) + .add_system_to_stage(DipStage::Prepare, new_ui_todo_list) .run(); } diff --git a/examples/todomvc/src/ui.rs b/examples/todomvc/src/ui.rs index f57445b..02b3061 100644 --- a/examples/todomvc/src/ui.rs +++ b/examples/todomvc/src/ui.rs @@ -3,7 +3,7 @@ use dip::prelude::*; #[allow(non_snake_case)] pub fn Root(cx: Scope) -> Element { - let window = use_window::(&cx); + let window = use_window::(&cx); let todo_list = use_read(&cx, TODO_LIST); let filter = use_read(&cx, FILTER); diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index d051ba7..49c0116 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -13,3 +13,4 @@ keywords = ["declarative-ui", "ecs", "bevy", "dioxus", "cross-platform"] [dependencies] bevy = { version = "0.8", default-features = false } dioxus = { version = "0.2", features = ["fermi"] } +dip_task = { version = "0.1", path = "../task" } diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index a8d5033..7c208ba 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -3,9 +3,12 @@ pub mod schedule; pub mod ui_state; +pub use dip_task as task; + pub mod prelude { pub use crate::{ - schedule::{UiSchedulePlugin, UiStage}, + schedule::{DipStage, DipStartupStage, UiSchedulePlugin}, + task::prelude::*, ui_state::{NoRootProps, NoUiAction, NoUiState, UiStateHandler}, }; } diff --git a/packages/core/src/schedule.rs b/packages/core/src/schedule.rs index 12c92c7..863b137 100644 --- a/packages/core/src/schedule.rs +++ b/packages/core/src/schedule.rs @@ -1,12 +1,18 @@ -//! UiStage for Bevy +//! DipStage for Bevy use bevy::{ - app::{App, CoreStage, Plugin}, + app::{App, CoreStage, Plugin, StartupStage}, ecs::schedule::{StageLabel, SystemStage}, }; +#[derive(Debug, Hash, PartialEq, Eq, Clone, StageLabel)] +pub enum DipStartupStage { + /// Place to send cli subcommand + Action, +} + /// The names of the default [`Ui`] stages. #[derive(Debug, Hash, PartialEq, Eq, Clone, StageLabel)] -pub enum UiStage { +pub enum DipStage { /// Place to send Ui event or cli subcommand Action, /// Stage to query spawned component. Use this stage to add system requires to wait 1 @@ -22,17 +28,22 @@ pub struct UiSchedulePlugin; impl Plugin for UiSchedulePlugin { fn build(&self, app: &mut App) { - app.add_stage_after( + app.add_startup_stage_after( + StartupStage::PreStartup, + DipStartupStage::Action, + SystemStage::parallel(), + ) + .add_stage_after( CoreStage::PreUpdate, - UiStage::Action, + DipStage::Action, SystemStage::parallel(), ) .add_stage_after( CoreStage::PostUpdate, - UiStage::Prepare, + DipStage::Prepare, SystemStage::parallel(), ) - .add_stage_after(UiStage::Prepare, UiStage::Apply, SystemStage::parallel()) - .add_stage_after(UiStage::Prepare, UiStage::Render, SystemStage::parallel()); + .add_stage_after(DipStage::Prepare, DipStage::Apply, SystemStage::parallel()) + .add_stage_after(DipStage::Prepare, DipStage::Render, SystemStage::parallel()); } } diff --git a/packages/desktop/src/context.rs b/packages/desktop/src/context.rs index 151383c..8893650 100644 --- a/packages/desktop/src/context.rs +++ b/packages/desktop/src/context.rs @@ -6,19 +6,20 @@ use std::fmt::Debug; use tokio::sync::mpsc::Sender; use wry::application::event_loop::EventLoopProxy; -pub type ProxyType = EventLoopProxy>; +pub type ProxyType = EventLoopProxy>; #[derive(Clone)] -pub struct UiContext { - proxy: ProxyType, +pub struct UiContext { + proxy: ProxyType, ui_action_tx: Sender, } -impl UiContext +impl UiContext where UiAction: Debug + Clone, + AsyncAction: Debug + Clone, { - pub fn new(proxy: ProxyType, ui_action_tx: Sender) -> Self { + pub fn new(proxy: ProxyType, ui_action_tx: Sender) -> Self { Self { proxy, ui_action_tx, diff --git a/packages/desktop/src/event.rs b/packages/desktop/src/event.rs index 85dc33e..6824747 100644 --- a/packages/desktop/src/event.rs +++ b/packages/desktop/src/event.rs @@ -13,7 +13,7 @@ use std::fmt::Debug; /// Tao events that emit from UI side #[derive(Debug)] -pub enum UiEvent { +pub enum UiEvent { /// UI events regards window manipulation WindowEvent(WindowEvent), /// User defined UiAction coming from Ui @@ -21,6 +21,8 @@ pub enum UiEvent { /// KeyboardEvent which dispatched from `window.document`. Make sure to pass `keyboard_event: /// true` to `DioxusSettings`. KeyboardEvent(KeyboardEvent), + /// User defined AsyncAction + AsyncAction(AsyncAction), } #[derive(serde::Serialize, serde::Deserialize)] diff --git a/packages/desktop/src/event_loop.rs b/packages/desktop/src/event_loop.rs index 61d6340..474b579 100644 --- a/packages/desktop/src/event_loop.rs +++ b/packages/desktop/src/event_loop.rs @@ -26,21 +26,22 @@ use wry::application::{ event_loop::{ControlFlow, EventLoop, EventLoopWindowTarget}, }; -pub fn start_event_loop(mut app: App) +pub fn start_event_loop(mut app: App) where UiAction: 'static + Send + Sync + Clone + Debug, + AsyncAction: 'static + Send + Sync + Clone + Debug, RootProps: 'static + Send + Sync + Clone + Default, { let event_loop = app .world - .remove_non_send_resource::>>() + .remove_non_send_resource::>>() .unwrap(); let mut tao_state = TaoPersistentState::default(); event_loop.run( - move |event: Event>, - _event_loop: &EventLoopWindowTarget>, + move |event: Event>, + _event_loop: &EventLoopWindowTarget>, control_flow: &mut ControlFlow| { log::trace!("{event:?}"); match event { @@ -336,6 +337,13 @@ where .expect("Provide UiAction event to bevy"); events.send(action); } + UiEvent::AsyncAction(action) => { + let mut events = app + .world + .get_resource_mut::>() + .expect("Provide AsyncAction event to bevy"); + events.send(action); + } UiEvent::KeyboardEvent(event) => { let mut keyboard_events = app .world @@ -378,7 +386,7 @@ where tao_state.active = true; } Event::MainEventsCleared => { - handle_create_window_events::(&mut app.world); + handle_create_window_events::(&mut app.world); let desktop_settings = app.world.non_send_resource::>(); let update = if !tao_state.active { @@ -448,9 +456,10 @@ where ); } -fn handle_create_window_events(world: &mut World) +fn handle_create_window_events(world: &mut World) where UiAction: 'static + Send + Sync + Clone + Debug, + AsyncAction: 'static + Send + Sync + Clone + Debug, RootProps: 'static + Send + Sync + Clone, { let world = world.cell(); @@ -462,7 +471,7 @@ where for create_window_event in create_window_events_reader.iter(&create_window_events) { warn!("Multiple Windows isn't supported yet!"); - let window = dioxus_windows.create::( + let window = dioxus_windows.create::( &world, create_window_event.id, &create_window_event.descriptor, diff --git a/packages/desktop/src/hooks.rs b/packages/desktop/src/hooks.rs index da08c97..f175c89 100644 --- a/packages/desktop/src/hooks.rs +++ b/packages/desktop/src/hooks.rs @@ -5,21 +5,23 @@ use dioxus_core::*; use std::fmt::Debug; /// Get an imperative handle to the current window -pub fn use_window(cx: &ScopeState) -> &UiContext +pub fn use_window(cx: &ScopeState) -> &UiContext where UiAction: Debug + Clone, + AsyncAction: Debug + Clone, { - cx.use_hook(|_| cx.consume_context::>()) + cx.use_hook(|_| cx.consume_context::>()) .as_ref() .expect("Failed to find UiContext, check UiAction type parameter") } /// Get a closure that executes any JavaScript in the WebView context. -pub fn use_eval(cx: &ScopeState) -> &dyn Fn(S) +pub fn use_eval(cx: &ScopeState) -> &dyn Fn(S) where UiAction: Debug + Clone + 'static, + AsyncAction: Debug + Clone + 'static, { - let window = use_window::(cx).clone(); + let window = use_window::(cx).clone(); cx.use_hook(|_| move |script| window.eval(script)) } diff --git a/packages/desktop/src/plugin.rs b/packages/desktop/src/plugin.rs index ed4b497..db584a6 100644 --- a/packages/desktop/src/plugin.rs +++ b/packages/desktop/src/plugin.rs @@ -18,33 +18,38 @@ use bevy::{ window::{CreateWindow, ModifiesWindows, WindowCreated, WindowPlugin, Windows}, }; use dioxus_core::{Component as DioxusComponent, SchedulerMsg}; -use dip_core::{schedule::UiSchedulePlugin, ui_state::UiStateHandler}; +use dip_core::{schedule::UiSchedulePlugin, task::AsyncActionPool, ui_state::UiStateHandler}; use futures_channel::mpsc as futures_mpsc; use std::{fmt::Debug, marker::PhantomData, sync::Arc, sync::Mutex}; -use tokio::{runtime::Runtime, sync::mpsc}; +use tokio::{runtime::Runtime, select, sync::mpsc}; use wry::application::event_loop::EventLoop; /// Dioxus Plugin for Bevy -pub struct DesktopPlugin { +pub struct DesktopPlugin { /// Root component pub Root: DioxusComponent, ui_state_type: PhantomData, ui_action_type: PhantomData, + async_action_type: PhantomData, } -impl Plugin for DesktopPlugin +impl Plugin + for DesktopPlugin where UiState: 'static + Send + Sync + UiStateHandler, UiAction: 'static + Send + Sync + Clone + Debug, + AsyncAction: 'static + Send + Sync + Clone + Debug, RootProps: 'static + Send + Sync + Clone + Default, { fn build(&self, app: &mut App) { let (vdom_scheduler_tx, vdom_scheduler_rx) = futures_mpsc::unbounded::(); let (ui_state_tx, ui_state_rx) = mpsc::channel::(8); let (ui_action_tx, mut ui_action_rx) = mpsc::channel::(8); + let (async_action_tx, mut async_action_rx) = mpsc::channel::(8); + let async_action = AsyncActionPool::new(async_action_tx.clone()); - let event_loop = EventLoop::>::with_user_event(); + let event_loop = EventLoop::>::with_user_event(); let settings = app .world .remove_non_send_resource::>() @@ -57,9 +62,17 @@ where let proxy_clone = proxy.clone(); runtime.spawn(async move { - while let Some(action) = ui_action_rx.recv().await { - log::trace!("UiAction: {:#?}", action); - proxy_clone.send_event(UiEvent::UiAction(action)).unwrap(); + loop { + select! { + action = ui_action_rx.recv() => { + log::trace!("UiAction: {:#?}", action); + proxy_clone.send_event(UiEvent::UiAction(action.unwrap())).unwrap(); + } + action = async_action_rx.recv() => { + log::trace!("AsyncAction: {:#?}", action); + proxy_clone.send_event(UiEvent::AsyncAction(action.unwrap())).unwrap(); + } + } } }); @@ -71,7 +84,7 @@ where .add_plugin(UiSchedulePlugin) .add_plugin(InputPlugin) .add_event::() - .add_event::() + .insert_resource(async_action) .insert_resource(runtime) .insert_resource(vdom_scheduler_tx) .insert_resource(ui_state_tx) @@ -79,7 +92,7 @@ where .init_non_send_resource::() .insert_non_send_resource(settings) .insert_non_send_resource(event_loop) - .set_runner(|app| start_event_loop::(app)) + .set_runner(|app| start_event_loop::(app)) .add_system_to_stage(CoreStage::PostUpdate, change_window.label(ModifiesWindows)); std::thread::spawn(move || { @@ -101,7 +114,8 @@ where } } -impl DesktopPlugin +impl + DesktopPlugin where UiState: Send + Sync + UiStateHandler, UiAction: Clone + Debug + Send + Sync, @@ -114,7 +128,7 @@ where /// /// fn main() { /// App::new() - /// .add_plugin(DesktopPlugin::::new(Root)) + /// .add_plugin(DesktopPlugin::::new(Root)) /// .run(); /// } /// @@ -129,12 +143,14 @@ where Root, ui_state_type: PhantomData, ui_action_type: PhantomData, + async_action_type: PhantomData, } } fn handle_initial_window_events(world: &mut World) where UiAction: 'static + Send + Sync + Clone + Debug, + AsyncAction: 'static + Send + Sync + Clone + Debug, RootProps: 'static + Send + Sync + Clone, { let world = world.cell(); @@ -144,7 +160,7 @@ where let mut window_created_events = world.get_resource_mut::>().unwrap(); for create_window_event in create_window_events.drain() { - let window = dioxus_windows.create::( + let window = dioxus_windows.create::( &world, create_window_event.id, &create_window_event.descriptor, diff --git a/packages/desktop/src/virtual_dom.rs b/packages/desktop/src/virtual_dom.rs index f394148..59fab8d 100644 --- a/packages/desktop/src/virtual_dom.rs +++ b/packages/desktop/src/virtual_dom.rs @@ -13,18 +13,20 @@ use std::{ }; use tokio::{select, sync::mpsc::Receiver}; -pub struct VirtualDom { +pub struct VirtualDom { virtual_dom: DioxusVirtualDom, edit_queue: Arc>>, ui_state_rx: Receiver, scheduler_tx: UnboundedSender, ui_action_type: PhantomData, + async_action_type: PhantomData, } -impl VirtualDom +impl VirtualDom where UiState: UiStateHandler, UiAction: 'static + Clone + Debug, + AsyncAction: 'static + Clone + Debug, { pub fn new( Root: Component, @@ -51,6 +53,7 @@ where ui_state_rx, scheduler_tx, ui_action_type: PhantomData, + async_action_type: PhantomData, } } @@ -89,9 +92,10 @@ where } } - pub fn provide_ui_context(&self, context: UiContext) + pub fn provide_ui_context(&self, context: UiContext) where UiAction: Clone + Debug, + AsyncAction: Clone + Debug, { self.virtual_dom.base_scope().provide_context(context); } @@ -115,7 +119,7 @@ where } fn rerender(&self) { - let ui_context: UiContext = + let ui_context: UiContext = self.virtual_dom.base_scope().consume_context().unwrap(); ui_context.rerender(); } diff --git a/packages/desktop/src/window.rs b/packages/desktop/src/window.rs index e728bc2..23ab57a 100644 --- a/packages/desktop/src/window.rs +++ b/packages/desktop/src/window.rs @@ -76,7 +76,7 @@ impl DioxusWindows { self.windows.remove(&tao_window_id) } - pub fn create( + pub fn create( &mut self, world: &WorldCell, window_id: WindowId, @@ -84,10 +84,11 @@ impl DioxusWindows { ) -> BevyWindow where UiAction: 'static + Send + Sync + Clone + Debug, + AsyncAction: 'static + Send + Sync + Clone + Debug, RootProps: 'static + Send + Sync + Clone, { let event_loop = world - .get_non_send_resource_mut::>>() + .get_non_send_resource_mut::>>() .unwrap(); let proxy = event_loop.create_proxy(); let dom_tx = world @@ -98,11 +99,12 @@ impl DioxusWindows { .unwrap() .clone(); - let tao_window = Self::create_tao_window::(&event_loop, &window_descriptor); + let tao_window = + Self::create_tao_window::(&event_loop, &window_descriptor); let tao_window_id = tao_window.id(); let bevy_window = Self::create_bevy_window(window_id, &tao_window, &window_descriptor); - let (webview, is_ready) = Self::create_webview::( + let (webview, is_ready) = Self::create_webview::( world, window_descriptor, tao_window, @@ -163,8 +165,8 @@ impl DioxusWindows { modes.first().unwrap().clone() } - fn create_tao_window( - event_loop: &EventLoop>, + fn create_tao_window( + event_loop: &EventLoop>, window_descriptor: &WindowDescriptor, ) -> TaoWindow where @@ -311,15 +313,16 @@ impl DioxusWindows { ) } - fn create_webview( + fn create_webview( world: &WorldCell, window_descriptor: &WindowDescriptor, tao_window: TaoWindow, - proxy: ProxyType, + proxy: ProxyType, dom_tx: mpsc::UnboundedSender, ) -> (WebView, Arc) where UiAction: 'static + Send + Sync + Clone + Debug, + AsyncAction: 'static + Send + Sync + Clone + Debug, RootProps: 'static, { let mut settings = world diff --git a/packages/macro/src/ui_action.rs b/packages/macro/src/action_parser.rs similarity index 58% rename from packages/macro/src/ui_action.rs rename to packages/macro/src/action_parser.rs index 25d1587..3951e5f 100644 --- a/packages/macro/src/ui_action.rs +++ b/packages/macro/src/action_parser.rs @@ -2,26 +2,38 @@ use convert_case::{Case, Casing}; use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; use quote::quote; -use std::{collections::HashSet, str::FromStr}; +use std::{collections::HashSet, fmt, str::FromStr}; use syn::{FnArg, ImplItem, ImplItemMethod, ItemImpl, ReturnType, Type}; -pub struct UiActionParser { +pub struct ActionParser { + action_type: ActionType, action_creator_impl: ItemImpl, } -impl From for UiActionParser { - fn from(action_creator_impl: ItemImpl) -> Self { +impl ActionParser { + pub fn async_action(action_creator_impl: ItemImpl) -> Self { Self { + action_type: ActionType::AsyncAction, action_creator_impl, } } -} -impl UiActionParser { - pub fn parse(&self) -> UiActionToken { - let mut tokens = UiActionToken::default(); - tokens.action_creator_name = self.action_creator_name(); - tokens.action_creator_impl = self.action_creator_impl(); + pub fn ui_action(action_creator_impl: ItemImpl) -> Self { + Self { + action_type: ActionType::UiAction, + action_creator_impl, + } + } + + pub fn parse(&self) -> ActionToken { + let mut tokens = ActionToken { + plugin_name: self.plugin_name(), + action_name: self.action_name(), + action_creator_impl: self.action_creator_impl(), + action_creator_name: self.action_creator_name(), + handler_name: self.handler_name(), + ..Default::default() + }; let mut actions = HashSet::new(); @@ -32,11 +44,21 @@ impl UiActionParser { let method_name = quote! { #method_name_raw }; let action = Self::action(&m); let (arg_keys, args) = Self::method_args(&m); + let (async_key, await_key) = if m.sig.asyncness.is_some() { + (quote! { async }, quote! { .await }) + } else { + (quote! {}, quote! {}) + }; actions.insert(action.to_string()); - tokens - .methods - .push(self.method(&method_name, &action, args, arg_keys)); + tokens.action_methods.push(self.action_method( + &method_name, + &action, + args, + arg_keys, + &async_key, + &await_key, + )); } _ => {} } @@ -51,12 +73,20 @@ impl UiActionParser { tokens .handler_args .push(Self::handler_arg(&action, &action_snake)); - tokens.handlers.push(Self::handler(&action, &action_snake)); + tokens.handlers.push(self.handler(&action, &action_snake)); } tokens } + fn plugin_name(&self) -> TokenStream2 { + TokenStream2::from_str(self.action_type.plugin_name()).unwrap() + } + + fn action_name(&self) -> TokenStream2 { + TokenStream2::from_str(self.action_type.name()).unwrap() + } + fn action_creator_name(&self) -> TokenStream2 { let name = match &**&self.action_creator_impl.self_ty { Type::Path(p) => { @@ -75,56 +105,28 @@ impl UiActionParser { quote! { #input } } + fn handler_name(&self) -> TokenStream2 { + TokenStream2::from_str(self.action_type.handler_name()).unwrap() + } + // example // pub fn create_todo(title: &String) -> Self { // Self::CreateTodo(ActionCreator::create_todo(title)) // } - fn method( + fn action_method( &self, method_name: &TokenStream2, action: &TokenStream2, args: Vec, arg_keys: Vec, + async_key: &TokenStream2, + await_key: &TokenStream2, ) -> TokenStream2 { let action_creator_name = self.action_creator_name(); quote! { - pub fn #method_name(#(#args)*) -> Self { - Self::#action(#action_creator_name::#method_name(#(#arg_keys)*)) - } - } - } - - fn method_args(method: &ImplItemMethod) -> (Vec, Vec) { - let mut arg_keys = vec![]; - let mut args = vec![]; - for arg in method.sig.inputs.iter() { - match arg { - FnArg::Typed(pt) => { - let ident = &pt.pat; - arg_keys.push(quote! { #ident, }); - } - _ => {} - } - args.push(quote! { #arg, }); - } - - (arg_keys, args) - } - - fn action(method: &ImplItemMethod) -> TokenStream2 { - match &method.sig.output { - ReturnType::Type(_, return_type) => match *return_type.clone() { - Type::Path(type_path) => { - let action = type_path.path.segments[0].ident.clone(); - quote! { #action } - } - _ => { - panic!("Cannot find event name. Make sure to sepcify return event in action creator methods."); - } - }, - _ => { - panic!("Cannot find event name. Make sure to sepcify return event in action creator methods."); + pub #async_key fn #method_name(#(#args)*) -> Self { + Self::#action(#action_creator_name::#method_name(#(#arg_keys)*)#await_key) } } } @@ -154,51 +156,94 @@ impl UiActionParser { // UiAction::CreateTodo(event) => { // create_todo.send(event.clone()); // } - fn handler(action: &TokenStream2, action_snake: &TokenStream2) -> TokenStream2 { + fn handler(&self, action: &TokenStream2, action_snake: &TokenStream2) -> TokenStream2 { + let action_type = TokenStream2::from_str(&self.action_type.to_string()).unwrap(); + quote! { - UiAction::#action(event) => { + #action_type::#action(event) => { #action_snake.send(event.clone()); } } } + + fn action(method: &ImplItemMethod) -> TokenStream2 { + match &method.sig.output { + ReturnType::Type(_, return_type) => match *return_type.clone() { + Type::Path(type_path) => { + let action = type_path.path.segments[0].ident.clone(); + quote! { #action } + } + _ => { + panic!("Cannot find event name. Make sure to sepcify return event in action creator methods."); + } + }, + _ => { + panic!("Cannot find event name. Make sure to sepcify return event in action creator methods."); + } + } + } + + fn method_args(method: &ImplItemMethod) -> (Vec, Vec) { + let mut arg_keys = vec![]; + let mut args = vec![]; + for arg in method.sig.inputs.iter() { + match arg { + FnArg::Typed(pt) => { + let ident = &pt.pat; + arg_keys.push(quote! { #ident, }); + } + _ => {} + } + args.push(quote! { #arg, }); + } + + (arg_keys, args) + } } #[derive(Default)] -pub struct UiActionToken { +pub struct ActionToken { + plugin_name: TokenStream2, + action_name: TokenStream2, + action_creator_impl: TokenStream2, + action_creator_name: TokenStream2, enum_variants: Vec, add_events: Vec, + action_methods: Vec, + handler_name: TokenStream2, handler_args: Vec, handlers: Vec, - action_creator_name: TokenStream2, - action_creator_impl: TokenStream2, - methods: Vec, } -impl UiActionToken { +impl ActionToken { pub fn gen(&self) -> TokenStream { let Self { + plugin_name, + action_name, + action_creator_name, + action_creator_impl, enum_variants, add_events, + action_methods, + handler_name, handler_args, handlers, - action_creator_name, - action_creator_impl, - methods, } = self; let gen = quote! { - pub struct UiActionPlugin; + pub struct #plugin_name; - impl Plugin for UiActionPlugin { + impl ::dip::bevy::app::Plugin for #plugin_name { fn build(&self, app: &mut App) { app + .add_event::<#action_name>() #(#add_events)* - .add_system_to_stage(UiStage::Action, send_ui_action_event); + .add_system_to_stage(::dip::core::schedule::DipStage::Action, #handler_name); } } #[derive(Clone, Debug)] - pub enum UiAction { + pub enum #action_name { #(#enum_variants)* } @@ -206,12 +251,12 @@ impl UiActionToken { #action_creator_impl - impl UiAction { - #(#methods)* + impl #action_name { + #(#action_methods)* } - pub fn send_ui_action_event( - mut events: EventReader, + pub fn #handler_name( + mut events: EventReader<#action_name>, #(#handler_args)* ) { for action in events.iter() { @@ -225,3 +270,40 @@ impl UiActionToken { gen.into() } } + +pub enum ActionType { + AsyncAction, + UiAction, +} + +impl fmt::Display for ActionType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ActionType::AsyncAction => write!(f, "AsyncAction"), + ActionType::UiAction => write!(f, "UiAction"), + } + } +} + +impl ActionType { + fn name(&self) -> &'static str { + match self { + ActionType::AsyncAction => "AsyncAction", + ActionType::UiAction => "UiAction", + } + } + + fn plugin_name(&self) -> &'static str { + match self { + ActionType::AsyncAction => "AsyncActionPlugin", + ActionType::UiAction => "UiActionPlugin", + } + } + + fn handler_name(&self) -> &'static str { + match self { + ActionType::AsyncAction => "send_async_action_event", + ActionType::UiAction => "send_ui_action_event", + } + } +} diff --git a/packages/macro/src/cli.rs b/packages/macro/src/cli.rs index f65dda1..747f649 100644 --- a/packages/macro/src/cli.rs +++ b/packages/macro/src/cli.rs @@ -32,10 +32,7 @@ impl CliParser { if ident.to_string() == "subcommand" { let subcommand_name = f.ident.as_ref().unwrap(); let ty = &f.ty; - - token.add_event = quote! { - .add_event::<#ty>() - }; + token.add_event = quote! { .add_event::<#ty>() }; token.insert_subcommand_resource = quote! { .insert_resource(cli.#subcommand_name.clone()) }; @@ -44,8 +41,8 @@ impl CliParser { ) .unwrap(); token.add_subcommand_handler = quote! { - .add_system_to_stage( - ::dip::core::schedule::UiStage::Action, + .add_startup_system_to_stage( + ::dip::core::schedule::DipStartupStage::Action, convert_subcommand_to_event.before( #subcommand_handler_name ) @@ -95,9 +92,31 @@ impl CliToken { } = self; let gen = quote! { - pub struct CliPlugin; + pub struct CliPlugin { + async_action_type: std::marker::PhantomData, + continuous: bool, + } + + impl CliPlugin { + pub fn oneshot() -> Self { + Self { + async_action_type: std::marker::PhantomData, + continuous: false, + } + } + + pub fn continuous() -> Self { + Self { + async_action_type: std::marker::PhantomData, + continuous: true, + } + } + } - impl ::bevy::app::Plugin for CliPlugin { + impl ::dip::bevy::app::Plugin for CliPlugin + where + AsyncAction: 'static + Send + Sync + Clone, + { fn build(&self, app: &mut ::bevy::app::App) { use ::clap::Parser; use ::dip::bevy::ecs::{ @@ -106,13 +125,41 @@ impl CliToken { }; let cli = #cli_name::parse(); + let continuous = self.continuous; app.add_plugin(::dip::core::schedule::UiSchedulePlugin) #insert_subcommand_resource .insert_resource(cli) #add_event - .set_runner(|mut app| { - app.update(); + .set_runner(move |mut app| { + if !continuous { + app.update(); + } else { + let (async_action_tx, mut async_action_rx) = ::tokio::sync::mpsc::channel::(8); + let async_action = ::dip::core::task::AsyncActionPool::new(async_action_tx.clone()); + app.world.insert_resource(async_action); + + app.update(); + + loop { + if let Some(app_exit_events) = app.world.get_resource::<::dip::bevy::ecs::event::Events<::dip::bevy::app::AppExit>>() { + let mut app_exit_event_reader = ::dip::bevy::ecs::event::ManualEventReader::<::dip::bevy::app::AppExit>::default(); + if app_exit_event_reader.iter(app_exit_events).last().is_some() { + break + } + } + + while let Ok(action) = async_action_rx.try_recv() { + let mut events = app + .world + .get_resource_mut::<::dip::bevy::ecs::event::Events>() + .expect("Provide AsyncAction event to bevy"); + events.send(action); + + app.update(); + } + } + }; }) #add_subcommand_handler; } diff --git a/packages/macro/src/lib.rs b/packages/macro/src/lib.rs index 401cade..ad6064e 100644 --- a/packages/macro/src/lib.rs +++ b/packages/macro/src/lib.rs @@ -1,14 +1,14 @@ extern crate proc_macro; +mod action_parser; mod cli; mod config; mod subcommand; -mod ui_action; mod ui_state; use crate::{ - cli::CliParser, config::ConfigParser, subcommand::SubcommandParser, ui_action::UiActionParser, - ui_state::UiStateParser, + action_parser::ActionParser, cli::CliParser, config::ConfigParser, + subcommand::SubcommandParser, ui_state::UiStateParser, }; use proc_macro::TokenStream; use syn::{parse_macro_input, AttributeArgs, ItemEnum, ItemImpl, ItemStruct}; @@ -24,7 +24,7 @@ pub fn ui_state(_attr: TokenStream, tokens: TokenStream) -> TokenStream { pub fn ui_action(_attr: TokenStream, tokens: TokenStream) -> TokenStream { let input = parse_macro_input!(tokens as ItemImpl); - UiActionParser::from(input).parse().gen() + ActionParser::ui_action(input).parse().gen() } #[proc_macro_derive(CliPlugin)] @@ -48,3 +48,10 @@ pub fn config_plugin(attr: TokenStream, tokens: TokenStream) -> TokenStream { ConfigParser::new(attrs, input).parse().gen() } + +#[proc_macro_attribute] +pub fn async_action(_attr: TokenStream, tokens: TokenStream) -> TokenStream { + let input = parse_macro_input!(tokens as ItemImpl); + + ActionParser::async_action(input).parse().gen() +} diff --git a/packages/macro/src/subcommand.rs b/packages/macro/src/subcommand.rs index c910894..df8c723 100644 --- a/packages/macro/src/subcommand.rs +++ b/packages/macro/src/subcommand.rs @@ -84,7 +84,7 @@ impl SubcommandParser { let handler_token = TokenStream2::from_str(&handler).unwrap(); quote! { - .add_system_to_stage(::dip::core::schedule::UiStage::Action, #handler_token); + .add_startup_system_to_stage(::dip::core::schedule::DipStartupStage::Action, #handler_token); } } diff --git a/packages/macro/src/ui_state.rs b/packages/macro/src/ui_state.rs index b665a0e..9bdd950 100644 --- a/packages/macro/src/ui_state.rs +++ b/packages/macro/src/ui_state.rs @@ -100,10 +100,10 @@ impl UiStateParser { } } - // example: .add_system_to_stage(UiStage::Apply, dispatch_todo_list) + // example: .add_system_to_stage(DipStage::Apply, dispatch_todo_list) fn add_dispatch_system(system_name: &TokenStream2) -> TokenStream2 { quote! { - .add_system_to_stage(UiStage::Apply, #system_name) + .add_system_to_stage(DipStage::Apply, #system_name) } } diff --git a/packages/task/Cargo.toml b/packages/task/Cargo.toml new file mode 100644 index 0000000..0de2cc5 --- /dev/null +++ b/packages/task/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "dip_task" +version = "0.1.0" +authors = ["Junichi Sugiura"] +edition = "2021" +description = "Write cross-platform application with React-like declarative UI framework and scalable ECS architecture all in Rust." +license = "MIT OR Apache-2.0" +repository = "https://github.com/diptools/dip/" +homepage = "https://github.com/diptools/dip/" +documentation = "https://docs.rs/dip/latest/dip/" +keywords = ["declarative-ui", "ecs", "bevy", "dioxus", "cross-platform"] + +[dependencies] +tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync"], default-features = false } +dip_macro = { version = "0.1", path = "../macro" } diff --git a/packages/task/src/async_action.rs b/packages/task/src/async_action.rs new file mode 100644 index 0000000..84a7a84 --- /dev/null +++ b/packages/task/src/async_action.rs @@ -0,0 +1,31 @@ +use std::future::Future; +use tokio::{runtime::Runtime, sync::mpsc}; + +pub struct AsyncActionPool { + runner: Runtime, + tx: mpsc::Sender, +} + +impl AsyncActionPool { + pub fn new(tx: mpsc::Sender) -> Self { + Self { + runner: Runtime::new().unwrap(), + tx, + } + } + + pub fn send(&self, future: F) + where + F: Future + Send + 'static, + F::Output: Send + 'static + std::fmt::Debug, + { + let tx = self.tx.clone(); + + self.runner.spawn(async move { + let task = future.await; + tx.send(task).await.unwrap(); + }); + } +} + +pub type NoAsyncAction = (); diff --git a/packages/task/src/lib.rs b/packages/task/src/lib.rs new file mode 100644 index 0000000..1181123 --- /dev/null +++ b/packages/task/src/lib.rs @@ -0,0 +1,8 @@ +mod async_action; + +pub use crate::async_action::{AsyncActionPool, NoAsyncAction}; +pub use dip_macro::async_action; + +pub mod prelude { + pub use crate::{async_action, AsyncActionPool, NoAsyncAction}; +} diff --git a/src/lib.rs b/src/lib.rs index ed48968..308aad3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,6 +10,8 @@ pub use dip_cli as cli; pub use dip_desktop as desktop; pub use bevy; + +#[cfg(feature = "desktop")] pub use dioxus; /// diff --git a/src/main.rs b/src/main.rs index d6312e8..f0a92ac 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,11 +8,12 @@ use dip::{ log::{self, LogPlugin}, }, cli::{CliPlugin, SubcommandPlugin}, + prelude::NoAsyncAction, }; fn main() { App::new() - .add_plugin(CliPlugin) + .add_plugin(CliPlugin::::oneshot()) .add_plugin(ActionPlugin) .add_plugin(ToolActionPlugin) .add_plugin(ConfigActionPlugin)